git-trace

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;
}