romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 10m ago
src/ops.c
/* Operational surface for sysadmins: `doctor`, `backup`, `restore`. * Zero new dependencies — sqlite3_backup API, statvfs, and the same * fork/exec discipline used elsewhere in the codebase. */ #include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <dirent.h> #include <errno.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/statvfs.h> #include <sys/wait.h> #include <sqlite3.h> #include "web.h" #include "db.h" #include "util.h" #include "repo.h" #include "ops.h" /* ------------------------------------------------------------------ */ /* doctor */ static int fails = 0, warns = 0; static void rep(int lvl, const char *what, const char *fmt, ...) { va_list ap; char det[256]; va_start(ap, fmt); vsnprintf(det, sizeof det, fmt, ap); va_end(ap); const char *tag = lvl == 0 ? "ok " : lvl == 1 ? "warn" : "FAIL"; if (lvl == 1) warns++; if (lvl == 2) fails++; printf("[%s] %-16s %s\n", tag, what, det); } static long file_lines(const char *path) { FILE *f = fopen(path, "r"); if (!f) return -1; long n = 0; int c, prev = '\n'; while ((c = fgetc(f)) != EOF) { if (c == '\n') n++; prev = c; } if (prev != '\n' && prev != 0) n++; fclose(f); return n; } static int q1i(struct app_ctx *c, const char *sql) { int v = -1; sqlite3_stmt *st; if (sqlite3_prepare_v2(c->db, sql, -1, &st, NULL) == SQLITE_OK) { if (sqlite3_step(st) == SQLITE_ROW) v = sqlite3_column_int(st, 0); sqlite3_finalize(st); } return v; } /* is a runnable file on PATH? (independent of main.c's static helper) */ static int on_path(const char *bin) { const char *p = getenv("PATH"); if (!p) return 0; size_t bl = strlen(bin); for (const char *d = p; ;) { const char *colon = strchr(d, ':'); size_t dl = colon ? (size_t)(colon - d) : strlen(d); if (dl && dl + bl + 2 <= PATH_MAX) { char probe[PATH_MAX]; memcpy(probe, d, dl); probe[dl] = '/'; memcpy(probe + dl + 1, bin, bl + 1); if (access(probe, X_OK) == 0) return 1; } if (!colon) break; d = colon + 1; } return 0; } int gt_doctor(struct app_ctx *app) { puts("git-trace doctor — instance health check\n"); /* 1. database */ { char *err = NULL; int rc = sqlite3_exec(app->db, "PRAGMA quick_check", NULL, NULL, &err); int nu = q1i(app, "SELECT COUNT(*) FROM users"); int nr = q1i(app, "SELECT COUNT(*) FROM repos"); if (rc == SQLITE_OK) rep(0, "database", "%s — %d users, %d repos", app->db_path, nu, nr); else rep(2, "database", "quick_check: %s", err ? err : "corrupt"); sqlite3_free(err); } /* 2. repos tree + hooks */ { DIR *root = opendir(app->repos_root); if (!root) { rep(2, "repos dir", "%s: %s", app->repos_root, strerror(errno)); } else { int repos = 0, nohook = 0; struct dirent *oe; while ((oe = readdir(root))) { if (oe->d_name[0] == '.') continue; char od[PATH_MAX]; if (snprintf(od, sizeof od, "%s/%s", app->repos_root, oe->d_name) >= (int)sizeof od) continue; DIR *d2 = opendir(od); if (!d2) continue; struct dirent *re_; while ((re_ = readdir(d2))) { size_t nl = strlen(re_->d_name); if (nl < 5 || strcmp(re_->d_name + nl - 4, ".git")) continue; repos++; char hp[PATH_MAX]; if (snprintf(hp, sizeof hp, "%s/%s/hooks/update", od, re_->d_name) < (int)sizeof hp) { struct stat s; if (stat(hp, &s) != 0 || !(s.st_mode & 0111)) nohook++; } } closedir(d2); } closedir(root); if (nohook) rep(1, "repos", "%d found, %d missing update hook" " (run: git-trace rehook)", repos, nohook); else rep(0, "repos", "%d found, hooks installed", repos); } } /* 3. disk headroom */ { struct statvfs v; if (statvfs(app->repos_root, &v) == 0) { double fr = (double)v.f_bavail * v.f_frsize; double to = (double)v.f_blocks * v.f_frsize; int pct = to > 0 ? (int)(fr * 100 / to) : 0; rep(pct < 5 || fr < 512e6 ? 2 : pct < 15 ? 1 : 0, "disk", "%.1f GB free (%.0f%%)", fr / 1e9, (double)pct); } } /* 4. ssh keyfile */ { char kp[PATH_MAX]; snprintf(kp, sizeof kp, "%s", app->db_path); char *sl = strrchr(kp, '/'); if (sl) strcpy(sl + 1, "authorized_keys"); else snprintf(kp, sizeof kp, "authorized_keys"); long n = file_lines(kp); if (n < 0) rep(1, "ssh", "no %s (keyfile --write after first key)", kp); else rep(0, "ssh", "%s — %ld keys", kp, n); } /* 5. webhook queue */ { int pend = q1i(app, "SELECT COUNT(*) FROM webhook_deliveries" " WHERE status='pending'"); int fail = q1i(app, "SELECT COUNT(*) FROM webhook_deliveries" " WHERE status='fail'"); rep(fail > 0 ? 1 : 0, "webhooks", "%d pending, %d failed", pend < 0 ? 0 : pend, fail < 0 ? 0 : fail); } /* 6. runtime tools */ { char miss[128] = ""; if (!on_path("git")) strcat(miss, "git "); if (!on_path("tar")) strcat(miss, "tar "); if (!on_path("sh")) strcat(miss, "sh "); if (miss[0]) rep(2, "tools", "missing on PATH: %s", miss); else rep(0, "tools", "git, tar, sh on PATH%s", on_path("curl") ? " (+curl)" : " — no curl: HTTPS webhooks disabled"); } /* 7. self footprint */ { FILE *f = fopen("/proc/self/status", "r"); char line[128]; long rss = -1; if (f) { while (fgets(line, sizeof line, f)) if (sscanf(line, "VmRSS: %ld kB", &rss) == 1) break; fclose(f); } if (rss >= 0) rep(0, "memory", "daemon image %.1f MB RSS", rss / 1024.0); } printf("\n%d checks failed, %d warnings\n", fails, warns); return fails ? 1 : 0; } /* ------------------------------------------------------------------ */ /* backup / restore */ static int db_backup_to(sqlite3 *src, const char *dest) { sqlite3 *d; if (sqlite3_open(dest, &d) != SQLITE_OK) return -1; sqlite3_backup *b = sqlite3_backup_init(d, "main", src, "main"); int rc = b ? sqlite3_backup_step(b, -1) : SQLITE_NOMEM; if (b) sqlite3_backup_finish(b); sqlite3_close(d); return rc == SQLITE_DONE ? 0 : -1; } static int tar_run(const char *mode, const char *a, const char *b, const char *c) { /* tar -C a -czf b c (pack) * tar -C a -xzf b (unpack) */ pid_t pid = fork(); if (pid == 0) { if (c) execlp("tar", "tar", "-C", a, mode, b, c, (char *)NULL); execlp("tar", "tar", "-C", a, mode, b, (char *)NULL); _exit(127); } int st = 0; waitpid(pid, &st, 0); return WIFEXITED(st) ? WEXITSTATUS(st) : -1; } int gt_backup(struct app_ctx *app, const char *dir) { if (mkdir_p(dir) != 0) { fprintf(stderr, "backup: cannot create %s: %s\n", dir, strerror(errno)); return 1; } char dbp[PATH_MAX], tgz[PATH_MAX]; snprintf(dbp, sizeof dbp, "%s/git-trace.db", dir); snprintf(tgz, sizeof tgz, "%s/repos.tar.gz", dir); if (db_backup_to(app->db, dbp) != 0) { fprintf(stderr, "backup: sqlite snapshot failed\n"); return 1; } printf("db -> %s\n", dbp); if (tar_run("-czf", app->repos_root, tgz, ".") != 0) { fprintf(stderr, "backup: tar of %s failed\n", app->repos_root); return 1; } printf("repos -> %s\n", tgz); puts("backup complete"); return 0; } /* restore: db + repos from <dir>; rehooks every repo so hooks point * at this installation's paths/binary */ int gt_restore(struct app_ctx *app, const char *dir) { char dbp[PATH_MAX], tgz[PATH_MAX]; snprintf(dbp, sizeof dbp, "%s/git-trace.db", dir); snprintf(tgz, sizeof tgz, "%s/repos.tar.gz", dir); struct stat s; if (stat(dbp, &s) != 0 || stat(tgz, &s) != 0) { fprintf(stderr, "restore: %s must contain git-trace.db and" " repos.tar.gz\n", dir); return 1; } if (tar_run("-xzf", app->repos_root, tgz, NULL) != 0) { fprintf(stderr, "restore: untar failed\n"); return 1; } printf("repos <- %s\n", tgz); /* replace the open db file via sqlite backup back-channel */ sqlite3 *src; if (sqlite3_open(dbp, &src) != SQLITE_OK) { fprintf(stderr, "restore: cannot open %s\n", dbp); return 1; } sqlite3_backup *b = sqlite3_backup_init(app->db, "main", src, "main"); int rc = b ? sqlite3_backup_step(b, -1) : SQLITE_NOMEM; if (b) sqlite3_backup_finish(b); sqlite3_close(src); if (rc != SQLITE_DONE) { fprintf(stderr, "restore: db restore failed\n"); return 1; } printf("db <- %s\n", dbp); /* every restored repo gets fresh hooks + regenerated keyfile */ DIR *root = opendir(app->repos_root); int n = 0; if (root) { struct dirent *oe; while ((oe = readdir(root))) { if (oe->d_name[0] == '.') continue; char od[PATH_MAX]; if (snprintf(od, sizeof od, "%s/%s", app->repos_root, oe->d_name) >= (int)sizeof od) continue; DIR *d2 = opendir(od); if (!d2) continue; struct dirent *re_; while ((re_ = readdir(d2))) { size_t nl = strlen(re_->d_name); if (nl < 5 || strcmp(re_->d_name + nl - 4, ".git")) continue; char rd[PATH_MAX]; if (snprintf(rd, sizeof rd, "%s/%s", od, re_->d_name) < (int)sizeof rd && repo_install_hooks(app, rd) == 0) n++; } closedir(d2); } closedir(root); } printf("hooks : reinstalled on %d repos\n", n); puts("restore complete — start the service and run `keyfile --write`"); return 0; }