diff options
| -rw-r--r-- | README.md | 32 | ||||
| -rw-r--r-- | main.c | 331 |
2 files changed, 216 insertions, 147 deletions
@@ -1,15 +1,17 @@ # ud -A clean, minimalist, robust C utility designed to automate Debian-based Linux system maintenance tasks. It manages `apt` package list updates, performs conditional upgrades, purges orphaned dependencies, clears package caches, and cleans local user thumbnail and screenshot caches. +A clean, minimalist, robust C utility designed to automate Debian-based Linux system maintenance tasks. It manages `apt` package list updates, performs conditional upgrades, purges orphaned dependencies, clears package caches, and cleans local user thumbnail and screenshot directories while logging execution metrics. ## Features - **Automated APT Maintenance:** Updates repository indices, checks for upgradeable packages, and runs `apt full-upgrade` only when updates exist. - **Dependency & Cache Cleanup:** Automatically purges unused dependencies and lingering configuration files (`apt autoremove --purge`) and empties cached installer packages (`apt clean`). -- **User Cache Housekeeping:** Safely cleans desktop thumbnail caches and user screenshot directories without failing if the directories are empty. -- **Robust POSIX C Implementation:** Built using strict standard C (`-std=c11`, POSIX 2008) with explicit bounds checking on formatted buffers and POSIX exit status inspection (`WIFEXITED`, `WEXITSTATUS`). +- **User Cache Housekeeping:** Safely cleans desktop thumbnail caches and user screenshot directories using native POSIX directory APIs (`dirent.h`). +- **Persistent Summary Logging:** Automatically writes session timestamps and task completion metrics (item counts purged/deleted) to `~/.local/state/ud/ud.log` following XDG Base Directory standards. +- **CLI Management Flags:** Built-in option parsing (`getopt_long`) to view persistent logs, clear log files, or inspect utility version information. When `$PAGER` is unset, viewing logs streams the last 100 lines non-interactively directly to standard output. +- **High-Performance POSIX C Implementation:** Built using standard C (`-std=c11`, POSIX 2008) with zero external process forks for filesystem operations or stream formatting, alongside explicit bounds checking on formatted buffers. - **Hardened & Optimized Build:** Compiled with `-O3`, `-flto`, `-march=native`, stack protection (`-fstack-protector-strong`), runtime buffer fortification (`-D_FORTIFY_SOURCE=2`), and strict warning checks (`-Wall -Wextra -Wpedantic -Werror`). -- **Clean Terminal Output:** Indents subprocess output using `sed` for a clear, readable terminal log. +- **Clean Terminal Output:** Natively indents subprocess output streams in C without invoking external piping utilities. --- @@ -20,7 +22,6 @@ This utility requires a **Debian-based distribution** (Debian, Ubuntu, Linux Min - `gcc` or `clang` - `make` - `apt` -- `sed` - `sudo` privileges for package management operations --- @@ -60,9 +61,22 @@ make uninstall Run the utility directly from your terminal: ```bash -# Run directly from the source directory -./ud - -# Or run from anywhere if ~/.local/bin is in your $PATH +# Execute standard maintenance routine ud + +# View persistent maintenance log using $PAGER (defaults to less) +ud -v +ud --view-log + +# Clear log history (~/.local/state/ud/ud.log) +ud -c +ud --clear-log + +# Display utility version +ud -V +ud --version + +# Display help message +ud -h +ud --help ``` @@ -1,11 +1,11 @@ /** * @file main.c - * @brief Automated Debian System Maintenance Utility + * @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 (Debian, Ubuntu, Linux Mint) with apt, sed, and sudo. + * Requirements: Debian-based distribution with apt and sudo. */ #define _POSIX_C_SOURCE 200809L @@ -17,15 +17,17 @@ #include <time.h> #include <getopt.h> #include <unistd.h> +#include <dirent.h> #include <sys/wait.h> #include <sys/stat.h> #include <sys/types.h> -#define VERSION "1.1.0" +#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\"" @@ -34,12 +36,6 @@ 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"); @@ -50,7 +46,6 @@ static bool get_log_path(char *path_buf, size_t max_len) { 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); @@ -66,7 +61,8 @@ static bool get_log_path(char *path_buf, size_t max_len) { } /** - * @brief Opens the log file using $PAGER or less. + * @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]; @@ -81,16 +77,44 @@ static void view_log(void) { } const char *pager = getenv("PAGER"); - if (!pager || strlen(pager) == 0) { - pager = "less"; - } + 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]; - 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"); + 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); + } } } @@ -121,64 +145,105 @@ static void print_version(void) { } /** - * @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. + * @brief Natively counts non-hidden files in a directory. */ -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; +static int count_directory_files(const char *dir_path) { + DIR *dir = opendir(dir_path); + if (!dir) return 0; int count = 0; - if (fscanf(fp, "%d", &count) != 1) { - count = 0; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] != '.') { + count++; + } } - pclose(fp); + closedir(dir); return count; } /** - * @brief Queries autoremove simulation to count candidate orphaned packages. - * - * @return Count of auto-installed packages eligible for autoremove. + * @brief Recursively counts files in nested directories. */ -static int count_orphaned_packages(void) { +static int count_recursive_files(const char *dir_path) { + DIR *dir = opendir(dir_path); + if (!dir) return 0; + 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; + 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++; + } } - pclose(sim_fp); } + closedir(dir); return count; } /** - * @brief Counts cached package archives (.deb) in /var/cache/apt/archives. - * - * @return Count of downloaded package files. + * @brief Helper to count files matching extension in a directory. */ -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; +static int count_extension_files(const char *dir_path, const char *ext) { + DIR *dir = opendir(dir_path); + if (!dir) return 0; int count = 0; - if (fscanf(fp, "%d", &count) != 1) { - 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++; + } } - pclose(fp); + 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. - * - * @param message Header text to display. */ static void print_message(const char *message) { if (!message) return; @@ -192,59 +257,48 @@ static void print_message(const char *message) { } /** - * @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. + * @brief Executes command directly, streams stdout with native indentation, + * and optionally matches output patterns on the fly. */ -static bool run_task(const char *message, const char *command, const char *log_msg_override) { +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]; - int bytes_written = snprintf(full_cmd, sizeof(full_cmd), - "%s 2>/dev/null | sed 's/^/ /'", command); + snprintf(full_cmd, sizeof(full_cmd), "%s 2>/dev/null", command); - if (bytes_written < 0 || (size_t)bytes_written >= sizeof(full_cmd)) { - fprintf(stderr, "Error: Command string truncated or invalid.\n"); + FILE *fp = popen(full_cmd, "r"); + if (!fp) { + perror("Error invoking subprocess"); if (log_file) { - fprintf(log_file, " [FAIL] %s (Command string truncated)\n", log_text); + fprintf(log_file, " [FAIL] %s (Subprocess execution error)\n", log_text); fflush(log_file); } return false; } - int status = system(full_cmd); + char line[MAX_LINE_LEN]; + if (match_counter) *match_counter = 0; - 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); + while (fgets(line, sizeof(line), fp) != NULL) { + printf(" %s", line); + + if (match_pattern && match_counter) { + if (strstr(line, match_pattern) != NULL) { + (*match_counter)++; + } } - return false; } - if (WIFEXITED(status)) { + int status = pclose(fp); + + if (status == -1 || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { 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"); + fprintf(stderr, " [!] Command failed or returned exit code: %d\n", exit_code); if (log_file) { - fprintf(log_file, " [FAIL] %s (Abnormal termination)\n", log_text); + fprintf(log_file, " [FAIL] %s (Exit code %d)\n", log_text, exit_code); fflush(log_file); } return false; @@ -260,17 +314,10 @@ static bool run_task(const char *message, const char *command, const char *log_m /** * @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; - } + if (!fp) return -1; int count = 0; char buffer[MAX_LINE_LEN]; @@ -328,7 +375,7 @@ int main(int argc, char *argv[]) { } } - /* Initialize persistent logger for current session */ + /* Initialize persistent logger */ char log_path[PATH_MAX_LEN]; if (get_log_path(log_path, sizeof(log_path))) { log_file = fopen(log_path, "a"); @@ -341,11 +388,11 @@ int main(int argc, char *argv[]) { } } - /* Step 1: Update local package index with parallel fetch queue */ - run_task("UPDATING LOCAL CACHE `apt update`", - "sudo apt " APT_OPT " update", NULL); + /* 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 for upgrades */ + /* Step 2: Query and perform upgrades conditionally */ int upgrade_count = count_upgradable_packages(); if (upgrade_count < 0) { @@ -362,51 +409,59 @@ int main(int argc, char *argv[]) { } } 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); - } + 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: 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); + /* 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); + } - int deb_count = count_cached_debs(); + /* 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("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); + 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'); |
