/** * @file main.c * @brief Automated Debian System Maintenance Utility (Optimized) * * Automates package list updates, conditional upgrades, orphaned dependency * purging, package archive cleaning, and temporary user cache removal. * * Requirements: Debian-based distribution with apt and sudo. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #define VERSION "1.2.0" #define MAX_CMD_LEN 512 #define MAX_MSG_LEN 128 #define MAX_LINE_LEN 1024 #define PATH_MAX_LEN 512 #define TAIL_LINE_COUNT 100 /* 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). */ 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; } 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 Views the log file using $PAGER, or streams the last 100 lines * directly to stdout if $PAGER is unset. */ 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) { 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"); } } else { FILE *f = fopen(log_path, "r"); if (!f) { perror("Error reading log file"); exit(EXIT_FAILURE); } /* Ring buffer to hold the last TAIL_LINE_COUNT lines */ char lines[TAIL_LINE_COUNT][MAX_LINE_LEN]; int count = 0; char buffer[MAX_LINE_LEN]; while (fgets(buffer, sizeof(buffer), f) != NULL) { strncpy(lines[count % TAIL_LINE_COUNT], buffer, MAX_LINE_LEN - 1); lines[count % TAIL_LINE_COUNT][MAX_LINE_LEN - 1] = '\0'; count++; } fclose(f); int start = 0; int print_count = count; if (count > TAIL_LINE_COUNT) { start = count % TAIL_LINE_COUNT; print_count = TAIL_LINE_COUNT; } for (int i = 0; i < print_count; i++) { fputs(lines[(start + i) % TAIL_LINE_COUNT], stdout); } } } /** * @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 Natively counts non-hidden files in a directory. */ static int count_directory_files(const char *dir_path) { DIR *dir = opendir(dir_path); if (!dir) return 0; int count = 0; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] != '.') { count++; } } closedir(dir); return count; } /** * @brief Recursively counts files in nested directories. */ static int count_recursive_files(const char *dir_path) { DIR *dir = opendir(dir_path); if (!dir) return 0; int count = 0; struct dirent *entry; char full_path[PATH_MAX_LEN]; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); struct stat st; if (stat(full_path, &st) == 0) { if (S_ISDIR(st.st_mode)) { count += count_recursive_files(full_path); } else if (S_ISREG(st.st_mode)) { count++; } } } closedir(dir); return count; } /** * @brief Helper to count files matching extension in a directory. */ static int count_extension_files(const char *dir_path, const char *ext) { DIR *dir = opendir(dir_path); if (!dir) return 0; int count = 0; struct dirent *entry; size_t ext_len = strlen(ext); while ((entry = readdir(dir)) != NULL) { size_t len = strlen(entry->d_name); if (len > ext_len && strcmp(entry->d_name + len - ext_len, ext) == 0) { count++; } } closedir(dir); return count; } /** * @brief Safely removes contents of a directory using POSIX system APIs. */ static void remove_directory_contents(const char *dir_path) { DIR *dir = opendir(dir_path); if (!dir) return; struct dirent *entry; char full_path[PATH_MAX_LEN]; while ((entry = readdir(dir)) != NULL) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); struct stat st; if (stat(full_path, &st) == 0) { if (S_ISDIR(st.st_mode)) { remove_directory_contents(full_path); rmdir(full_path); } else { unlink(full_path); } } } closedir(dir); } /** * @brief Prints an underlined section header to standard output. */ 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 Executes command directly, streams stdout with native indentation, * and optionally matches output patterns on the fly. */ static bool run_task_stream(const char *message, const char *command, const char *log_msg_override, const char *match_pattern, int *match_counter) { print_message(message); const char *log_text = log_msg_override ? log_msg_override : message; char full_cmd[MAX_CMD_LEN]; snprintf(full_cmd, sizeof(full_cmd), "%s 2>/dev/null", command); FILE *fp = popen(full_cmd, "r"); if (!fp) { perror("Error invoking subprocess"); if (log_file) { fprintf(log_file, " [FAIL] %s (Subprocess execution error)\n", log_text); fflush(log_file); } return false; } char line[MAX_LINE_LEN]; if (match_counter) *match_counter = 0; while (fgets(line, sizeof(line), fp) != NULL) { printf(" %s", line); if (match_pattern && match_counter) { if (strstr(line, match_pattern) != NULL) { (*match_counter)++; } } } int status = pclose(fp); if (status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { int exit_code = WEXITSTATUS(status); fprintf(stderr, " [!] Command failed or returned 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; } if (log_file) { fprintf(log_file, " [OK] %s\n", log_text); fflush(log_file); } return true; } /** * @brief Queries APT for pending upgradable packages. */ static int count_upgradable_packages(void) { FILE *fp = popen("apt list --upgradable 2>/dev/null", "r"); if (!fp) 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 */ 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 */ run_task_stream("UPDATING LOCAL CACHE `apt update`", "sudo apt " APT_OPT " update", NULL, NULL, NULL); /* Step 2: Query and perform upgrades conditionally */ 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]; snprintf(msg, sizeof(msg), "%d UPDATE%s AVAILABLE | UPGRADING `apt full-upgrade`", upgrade_count, (upgrade_count > 1) ? "S" : ""); run_task_stream(msg, "sudo apt full-upgrade -y", NULL, NULL, NULL); } /* Step 3: Single-pass autoremove and stream-based removal counting */ int orphan_count = 0; run_task_stream("PURGING ORPHANED DEPENDENCIES & CONFIGS `apt autoremove --purge`", "sudo apt autoremove --purge -y", NULL, "Removing ", &orphan_count); if (log_file) { fprintf(log_file, " [INFO] %d orphaned packages purged\n", orphan_count); fflush(log_file); } /* Count cached debs natively using POSIX dirent */ int deb_count = count_extension_files("/var/cache/apt/archives", ".deb"); 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_stream("WIPING ALL DOWNLOADED PACKAGE FILES `apt clean`", "sudo apt clean", deb_log, NULL, NULL); /* Step 4: Clean local user caches natively using dirent */ const char *home = getenv("HOME"); if (home) { char thumb_path[PATH_MAX_LEN]; snprintf(thumb_path, sizeof(thumb_path), "%s/.cache/thumbnails", home); int thumb_count = count_recursive_files(thumb_path); print_message("CLEARING LOCAL THUMBNAIL CACHE `rm -rf ~/.cache/thumbnails/*`"); remove_directory_contents(thumb_path); printf(" Cleared %d thumbnail items.\n", thumb_count); if (log_file) { fprintf(log_file, " [OK] CLEARING LOCAL THUMBNAIL CACHE - %d items deleted\n", thumb_count); fflush(log_file); } char shot_path[PATH_MAX_LEN]; snprintf(shot_path, sizeof(shot_path), "%s/Pictures/Screenshots", home); int shot_count = count_directory_files(shot_path); print_message("CLEARING SCREENSHOTS FOLDER `rm -rf ~/Pictures/Screenshots/*`"); remove_directory_contents(shot_path); printf(" Cleared %d screenshot files.\n", shot_count); if (log_file) { fprintf(log_file, " [OK] CLEARING SCREENSHOTS FOLDER - %d files deleted\n", shot_count); fflush(log_file); } } 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; }