From 5842b4ddc7747d3bc55bd47bf60b1dcfdbe5bfde Mon Sep 17 00:00:00 2001 From: David Faulkner Date: Thu, 6 Aug 2026 20:09:03 -0500 Subject: feat(cli): add persistent XDG logging, CLI flags, and task metrics - Implement getopt_long for -v (--view-log), -c (--clear-log), -V (--version), and -h (--help) - Add XDG-compliant persistent logging at ~/.local/state/ud/ud.log - Implement target item tallying to record counts for autoremove, apt clean, thumbnails, and screenshots - Bump utility version to 1.1.0 --- main.c | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 277 insertions(+), 15 deletions(-) (limited to 'main.c') diff --git a/main.c b/main.c index 9ee5384..78847c8 100644 --- a/main.c +++ b/main.c @@ -14,17 +14,169 @@ #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. + * @brief Prints an underlined section header to standard output. * * @param message Header text to display. */ @@ -42,23 +194,29 @@ static void print_message(const char *message) { /** * @brief Formats and executes a shell command with indented stdout. * - * Wraps the shell command to route standard error to /dev/null and - * pipes standard output to sed for clean line indentation. + * 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. - * @return true if the command executed and returned status 0, false otherwise. + * @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) { +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); - /* Verify buffer bounds to prevent shell command truncation vulnerabilities */ 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; } @@ -66,21 +224,37 @@ static bool run_task(const char *message, const char *command) { 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; } - /* Inspect POSIX exit status from subshell execution */ 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; } @@ -109,25 +283,83 @@ static int count_upgradable_packages(void) { int status = pclose(fp); if (status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { - /* If apt list fails, report 0 to safely skip upgrade block without crashing */ return 0; } return count; } -int main(void) { +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"); + "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), @@ -137,24 +369,54 @@ int main(void) { 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); + 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"); + "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"); + "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"); + "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"); + "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; } -- cgit v1.2.3