← Back to davo.co
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Faulkner <[email protected]>2026-08-06 20:09:03 -0500
committerDavid Faulkner <[email protected]>2026-08-06 20:09:03 -0500
commit5842b4ddc7747d3bc55bd47bf60b1dcfdbe5bfde (patch)
tree6f91ac8e10ee039585937a167f247c0235d4c1e8
parentb5aca4db048e251fb17590c1e13f4cc4e53a529c (diff)
feat(cli): add persistent XDG logging, CLI flags, and task metricsv1.1.0
- 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
-rw-r--r--main.c292
1 files changed, 277 insertions, 15 deletions
diff --git a/main.c b/main.c
index 9ee5384..78847c8 100644
--- a/main.c
+++ b/main.c
@@ -14,17 +14,169 @@
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
+#include <time.h>
+#include <getopt.h>
+#include <unistd.h>
#include <sys/wait.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#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;
}