/* EXERCISE 06 — REPAIRED.
 *
 * An app that persists its window state to a small file whenever it changes,
 * and a helper process that reads that file to restore the layout.
 *
 * The repair: never modify the file a reader might be opening. Write the new
 * state to a sibling temporary file, force it out with fsync(2), then rename(2)
 * it over the target. A rename within one filesystem replaces the directory
 * entry in a single step, so every open(2) sees either the whole old file or
 * the whole new one and never a state in between.
 *
 * Note what the fsync is FOR. It does not make rename atomic — rename is
 * atomic by itself. It orders the contents before the name, so a crash cannot
 * leave the new name pointing at a file whose bytes never reached storage.
 * fsync(2) on macOS flushes to the device but does not force the device's own
 * cache; F_FULLFSYNC does, at a cost measured in milliseconds.
 *
 * Build and run:
 *   clang -O2 -g -Wall -Wextra statefile.c -o /tmp/statefile_fixed
 *   /tmp/statefile_fixed
 *
 * Every measurement is printed as key=value so a script can assert on it.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>

#define ROUNDS      3000        /* how many times the state is rewritten */
#define BODY_BYTES  32768       /* size of the serialized state          */
#define CHUNK       4096        /* the writer emits the body in chunks   */
#define WATCHDOG_S  180

static void *watchdog(void *unused) {
    (void)unused;
    sleep(WATCHDOG_S);
    const char m[] = "WATCHDOG: no progress within budget - forcing _exit(75).\n";
    ssize_t ignored = write(2, m, sizeof m - 1); (void)ignored;
    _exit(75);
}

/* On-disk format: an 8-byte header (length, checksum) then the body. */
struct header { unsigned int length; unsigned int checksum; };

static unsigned int sum_of(const unsigned char *p, size_t n) {
    unsigned int s = 0;
    for (size_t i = 0; i < n; i++) s = s * 31u + p[i];
    return s;
}

/* ------------------------------------------------------------------ writer */
/* Write the new state somewhere nobody is looking, make it durable, and then
 * publish it by replacing the name in one step. A reader's open(2) resolves
 * the name either before or after the rename, never during it. */
static int write_state(const char *path, unsigned char fill) {
    unsigned char *body = malloc(BODY_BYTES);
    memset(body, fill, BODY_BYTES);
    struct header h = { BODY_BYTES, sum_of(body, BODY_BYTES) };

    /* The temporary must be in the SAME directory: rename(2) is only atomic
     * within one filesystem, and a cross-device rename fails with EXDEV. */
    char tmp[512];
    snprintf(tmp, sizeof tmp, "%s.tmp", path);

    int fd = open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0600);
    if (fd < 0) { free(body); return -1; }
    if (write(fd, &h, sizeof h) != (ssize_t)sizeof h) { close(fd); free(body); return -1; }
    for (size_t off = 0; off < BODY_BYTES; off += CHUNK)
        if (write(fd, body + off, CHUNK) != (ssize_t)CHUNK) { close(fd); free(body); return -1; }

    /* Order the contents before the name. Without this a crash can publish a
     * name whose bytes are still only in the page cache. */
    if (fsync(fd) != 0) { close(fd); free(body); return -1; }
    close(fd);

    if (rename(tmp, path) != 0) { unlink(tmp); free(body); return -1; }
    free(body);
    return 0;
}

/* ------------------------------------------------------------------ reader */
/* Returns 1 if the file read as a complete, self-consistent state. */
static int read_state(const char *path) {
    int fd = open(path, O_RDONLY);
    if (fd < 0) return 0;
    struct header h;
    if (read(fd, &h, sizeof h) != (ssize_t)sizeof h) { close(fd); return 0; }
    if (h.length != BODY_BYTES) { close(fd); return 0; }
    unsigned char *body = malloc(h.length);
    ssize_t got = 0, n;
    while (got < (ssize_t)h.length && (n = read(fd, body + got, h.length - (size_t)got)) > 0) got += n;
    close(fd);
    int ok = (got == (ssize_t)h.length) && (sum_of(body, h.length) == h.checksum);
    free(body);
    return ok;
}

int main(void) {
    pthread_t wd;
    pthread_create(&wd, NULL, watchdog, NULL);
    pthread_detach(wd);

    char path[512];
    snprintf(path, sizeof path, "%s/statefile.%d.dat",
             getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp", (int)getpid());
    if (write_state(path, 1) != 0) { perror("write_state"); return 1; }

    int report[2];
    if (pipe(report) != 0) { perror("pipe"); return 1; }

    pid_t reader = fork();
    if (reader == 0) {
        close(report[0]);
        long attempts = 0, torn = 0, missing = 0;
        for (;;) {
            struct stat st;
            if (stat(path, &st) != 0) { missing++; }
            attempts++;
            if (!read_state(path)) torn++;
            if (attempts >= ROUNDS * 4) break;
        }
        long out[3] = { attempts, torn, missing };
        ssize_t ignored = write(report[1], out, sizeof out); (void)ignored;
        close(report[1]);
        _exit(0);
    }
    close(report[1]);

    for (int r = 0; r < ROUNDS; r++)
        if (write_state(path, (unsigned char)(r % 251 + 1)) != 0) { perror("write_state"); break; }

    long out[3] = { 0, 0, 0 };
    ssize_t got = read(report[0], out, sizeof out);
    close(report[0]);
    int st; waitpid(reader, &st, 0);
    unlink(path);

    if (got != (ssize_t)sizeof out) { fprintf(stderr, "reader did not report\n"); return 1; }

    printf("rewrites=%d\n", ROUNDS);
    printf("readerAttempts=%ld\n", out[0]);
    printf("tornReads=%ld\n", out[1]);
    printf("missingFile=%ld\n", out[2]);
    printf("tornFraction=%.4f\n", out[0] ? (double)out[1] / (double)out[0] : 0.0);
    printf("verdict=%s\n", out[1] == 0 ? "atomic" : "torn");
    return 0;
}
