/** * @file main.c * @brief Automated Debian System Maintenance Utility * * Automates package list updates, conditional upgrades, orphaned dependency * purging, package archive cleaning, and temporary user cache removal. * * Requirements: Debian-based distribution (Debian, Ubuntu, Linux Mint) with apt, sed, and sudo. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #define VERSION "1.1.0" #define MAX_CMD_LEN 512 #define MAX_MSG_LEN 128 #define MAX_LINE_LEN 1024 #define PATH_MAX_LEN 512 /* Optimization flags for parallel package downloads */ #define APT_OPT "-o Acquire::Queue-Mode=\"access\" -o Acquire::http::Pipeline-Depth=\"10\"" static FILE *log_file = NULL; /** * @brief Resolves the default XDG log file path (~/.local/state/ud/ud.log). * * Creates parent directories if they do not exist. * * @param path_buf Buffer to write the path string to. * @param max_len Maximum capacity of path_buf. * @return true if path resolved and directories exist/created, false otherwise. */ static bool get_log_path(char *path_buf, size_t max_len) { const char *home = getenv("HOME"); if (!home) return false; char dir_path[PATH_MAX_LEN]; if (snprintf(dir_path, sizeof(dir_path), "%s/.local/state/ud", home) >= (int)sizeof(dir_path)) { return false; } /* Ensure ~/.local/state/ud directories exist */ char sub_path[PATH_MAX_LEN]; snprintf(sub_path, sizeof(sub_path), "%s/.local", home); mkdir(sub_path, 0755); snprintf(sub_path, sizeof(sub_path), "%s/.local/state", home); mkdir(sub_path, 0755); mkdir(dir_path, 0755); if (snprintf(path_buf, max_len, "%s/ud.log", dir_path) >= (int)max_len) { return false; } return true; } /** * @brief Opens the log file using $PAGER or less. */ static void view_log(void) { char log_path[PATH_MAX_LEN]; if (!get_log_path(log_path, sizeof(log_path))) { fprintf(stderr, "Error resolving log file path.\n"); exit(EXIT_FAILURE); } if (access(log_path, F_OK) != 0) { printf("No log file found at %s\n", log_path); exit(EXIT_SUCCESS); } const char *pager = getenv("PAGER"); if (!pager || strlen(pager) == 0) { pager = "less"; } char view_cmd[PATH_MAX_LEN * 2]; snprintf(view_cmd, sizeof(view_cmd), "%s %s", pager, log_path); int status = system(view_cmd); if (status == -1) { perror("Error launching log pager"); } } /** * @brief Truncates the persistent log file. */ static void clear_log(void) { char log_path[PATH_MAX_LEN]; if (!get_log_path(log_path, sizeof(log_path))) { fprintf(stderr, "Error resolving log file path.\n"); exit(EXIT_FAILURE); } FILE *f = fopen(log_path, "w"); if (f) { fclose(f); printf("Log cleared: %s\n", log_path); } else { perror("Error clearing log file"); } } /** * @brief Prints the utility version string. */ static void print_version(void) { printf("ud version %s\n", VERSION); } /** * @brief Counts files matching a shell glob pattern via popen/ls. * * @param glob_pattern Shell glob string (e.g., ~/.cache/thumbnails/). * @return Number of matching files, or 0 if none found/error. */ static int count_glob_files(const char *glob_pattern) { char cmd[MAX_CMD_LEN]; snprintf(cmd, sizeof(cmd), "ls -1d %s 2>/dev/null | wc -l", glob_pattern); FILE *fp = popen(cmd, "r"); if (!fp) return 0; int count = 0; if (fscanf(fp, "%d", &count) != 1) { count = 0; } pclose(fp); return count; } /** * @brief Queries autoremove simulation to count candidate orphaned packages. * * @return Count of auto-installed packages eligible for autoremove. */ static int count_orphaned_packages(void) { int count = 0; FILE *sim_fp = popen("apt-get autoremove -s 2>/dev/null | grep -E '^Remv ' | wc -l", "r"); if (sim_fp) { if (fscanf(sim_fp, "%d", &count) != 1) { count = 0; } pclose(sim_fp); } return count; } /** * @brief Counts cached package archives (.deb) in /var/cache/apt/archives. * * @return Count of downloaded package files. */ static int count_cached_debs(void) { FILE *fp = popen("ls -1 /var/cache/apt/archives/*.deb 2>/dev/null | wc -l", "r"); if (!fp) return 0; int count = 0; if (fscanf(fp, "%d", &count) != 1) { count = 0; } pclose(fp); return count; } /** * @brief Prints an underlined section header to standard output. * * @param message Header text to display. */ static void print_message(const char *message) { if (!message) return; printf("\n%s\n", message); size_t len = strlen(message); for (size_t i = 0; i < len; i++) { putchar('-'); } putchar('\n'); } /** * @brief Formats and executes a shell command with indented stdout. * * Routes standard error to /dev/null and pipes standard output to sed for terminal display. * Writes a high-level completion status summary to the persistent log file. * * @param message Display title for the task. * @param command Command string to execute via subshell. * @param log_msg_override Optional detailed message for log file (or NULL to use message). * @return true if command returned status 0, false otherwise. */ static bool run_task(const char *message, const char *command, const char *log_msg_override) { print_message(message); const char *log_text = log_msg_override ? log_msg_override : message; char full_cmd[MAX_CMD_LEN]; int bytes_written = snprintf(full_cmd, sizeof(full_cmd), "%s 2>/dev/null | sed 's/^/ /'", command); if (bytes_written < 0 || (size_t)bytes_written >= sizeof(full_cmd)) { fprintf(stderr, "Error: Command string truncated or invalid.\n"); if (log_file) { fprintf(log_file, " [FAIL] %s (Command string truncated)\n", log_text); fflush(log_file); } return false; } int status = system(full_cmd); if (status == -1) { perror("Error invoking shell process"); if (log_file) { fprintf(log_file, " [FAIL] %s (Subshell invocation error)\n", log_text); fflush(log_file); } return false; } if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); if (exit_code != 0) { fprintf(stderr, " [!] Command returned non-zero exit code: %d\n", exit_code); if (log_file) { fprintf(log_file, " [FAIL] %s (Exit code %d)\n", log_text, exit_code); fflush(log_file); } return false; } } else { fprintf(stderr, " [!] Command terminated abnormally.\n"); if (log_file) { fprintf(log_file, " [FAIL] %s (Abnormal termination)\n", log_text); fflush(log_file); } return false; } if (log_file) { fprintf(log_file, " [OK] %s\n", log_text); fflush(log_file); } return true; } /** * @brief Queries APT for pending upgradable packages. * * Spawns a read pipeline to apt list --upgradable and tallies target lines. * * @return Count of upgradable packages, or -1 on execution error. */ static int count_upgradable_packages(void) { FILE *fp = popen("apt list --upgradable 2>/dev/null", "r"); if (!fp) { perror("Error opening pipe to apt"); return -1; } int count = 0; char buffer[MAX_LINE_LEN]; while (fgets(buffer, sizeof(buffer), fp) != NULL) { if (strstr(buffer, "upgradable from:") != NULL) { count++; } } int status = pclose(fp); if (status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { return 0; } return count; } static void print_usage(const char *prog_name) { printf("Usage: %s [OPTIONS]\n\n", prog_name); printf("Options:\n"); printf(" -v, --view-log Open the maintenance log (~/.local/state/ud/ud.log) using $PAGER\n"); printf(" -c, --clear-log Clear the maintenance log file\n"); printf(" -V, --version Display version information and exit\n"); printf(" -h, --help Display this help message and exit\n"); } int main(int argc, char *argv[]) { static struct option long_options[] = { {"view-log", no_argument, 0, 'v'}, {"clear-log", no_argument, 0, 'c'}, {"version", no_argument, 0, 'V'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; int opt; while ((opt = getopt_long(argc, argv, "vcVh", long_options, NULL)) != -1) { switch (opt) { case 'v': view_log(); return EXIT_SUCCESS; case 'c': clear_log(); return EXIT_SUCCESS; case 'V': print_version(); return EXIT_SUCCESS; case 'h': print_usage(argv[0]); return EXIT_SUCCESS; default: print_usage(argv[0]); return EXIT_FAILURE; } } /* Initialize persistent logger for current session */ char log_path[PATH_MAX_LEN]; if (get_log_path(log_path, sizeof(log_path))) { log_file = fopen(log_path, "a"); if (log_file) { time_t now = time(NULL); char date_str[64]; strftime(date_str, sizeof(date_str), "%Y-%m-%d %H:%M:%S", localtime(&now)); fprintf(log_file, "[%s] MAINTENANCE SESSION STARTED\n", date_str); fflush(log_file); } } /* Step 1: Update local package index with parallel fetch queue */ run_task("UPDATING LOCAL CACHE `apt update`", "sudo apt " APT_OPT " update", NULL); /* Step 2: Query for upgrades */ int upgrade_count = count_upgradable_packages(); if (upgrade_count < 0) { fprintf(stderr, "Failed to query pending updates.\n"); if (log_file) { fprintf(log_file, " [FAIL] Querying pending package updates\n"); fflush(log_file); } } else if (upgrade_count == 0) { print_message("NO UPDATES AVAILABLE | UPGRADE SKIPPED"); if (log_file) { fprintf(log_file, " [INFO] 0 packages upgradable (Upgrade skipped)\n"); fflush(log_file); } } else { char msg[MAX_MSG_LEN]; int bytes = snprintf(msg, sizeof(msg), "%d UPDATE%s AVAILABLE | UPGRADING `apt full-upgrade`", upgrade_count, (upgrade_count > 1) ? "S" : ""); if (bytes > 0 && (size_t)bytes < sizeof(msg)) { char cmd[MAX_CMD_LEN]; snprintf(cmd, sizeof(cmd), "sudo apt full-upgrade -y"); run_task(msg, cmd, NULL); } } /* Step 3: Remove lingering packages and clear package archive caches */ int orphan_count = count_orphaned_packages(); char orphan_log[MAX_MSG_LEN]; snprintf(orphan_log, sizeof(orphan_log), "PURGING ORPHANED DEPENDENCIES & CONFIGS (`apt autoremove`) - %d package%s removed", orphan_count, (orphan_count == 1) ? "" : "s"); run_task("PURGING ORPHANED DEPENDENCIES & CONFIGS `apt autoremove --purge`", "sudo apt autoremove --purge -y", orphan_log); int deb_count = count_cached_debs(); char deb_log[MAX_MSG_LEN]; snprintf(deb_log, sizeof(deb_log), "WIPING ALL DOWNLOADED PACKAGE FILES (`apt clean`) - %d .deb archive%s purged", deb_count, (deb_count == 1) ? "" : "s"); run_task("WIPING ALL DOWNLOADED PACKAGE FILES `apt clean`", "sudo apt clean", deb_log); /* Step 4: Clean local user caches safely (suppressing error if glob matches 0 files) */ int thumb_count = count_glob_files("~/.cache/thumbnails/*/*"); if (thumb_count == 0) thumb_count = count_glob_files("~/.cache/thumbnails/*"); char thumb_log[MAX_MSG_LEN]; snprintf(thumb_log, sizeof(thumb_log), "CLEARING LOCAL THUMBNAIL CACHE - %d item%s deleted", thumb_count, (thumb_count == 1) ? "" : "s"); run_task("CLEARING LOCAL THUMBNAIL CACHE `rm -rf ~/.cache/thumbnails/*`", "rm -rf ~/.cache/thumbnails/* 2>/dev/null || true", thumb_log); int shot_count = count_glob_files("~/Pictures/Screenshots/*"); char shot_log[MAX_MSG_LEN]; snprintf(shot_log, sizeof(shot_log), "CLEARING SCREENSHOTS FOLDER - %d file%s deleted", shot_count, (shot_count == 1) ? "" : "s"); run_task("CLEARING SCREENSHOTS FOLDER `rm -rf ~/Pictures/Screenshots/*`", "rm -rf ~/Pictures/Screenshots/* 2>/dev/null || true", shot_log); putchar('\n'); if (log_file) { time_t end_time = time(NULL); char end_str[64]; strftime(end_str, sizeof(end_str), "%Y-%m-%d %H:%M:%S", localtime(&end_time)); fprintf(log_file, "[%s] MAINTENANCE SESSION COMPLETED\n\n", end_str); fclose(log_file); } return EXIT_SUCCESS; }