← Back to davo.co
aboutsummaryrefslogtreecommitdiffstats
path: root/main.c
diff options
context:
space:
mode:
Diffstat (limited to 'main.c')
-rw-r--r--main.c331
1 files changed, 193 insertions, 138 deletions
diff --git a/main.c b/main.c
index 78847c8..cc607e7 100644
--- a/main.c
+++ b/main.c
@@ -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');