diff --git a/cmd/undo/run.go b/cmd/undo/run.go index 1a03177..7f14c91 100644 --- a/cmd/undo/run.go +++ b/cmd/undo/run.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "github.com/edaywalid/undo/internal/session" ) @@ -105,6 +106,11 @@ func cmdRun(argv []string) { if runErr != nil { if ee, ok := runErr.(*exec.ExitError); ok { code = ee.ExitCode() + // killed by a signal: ExitCode is -1, which os.Exit turns + // into 255. Report it the way a shell does. + if ws, ok := ee.Sys().(syscall.WaitStatus); ok && ws.Signaled() { + code = 128 + int(ws.Signal()) + } } else { s.Remove() fatal(runErr) diff --git a/shim/undo_shim.c b/shim/undo_shim.c index 52ea7da..9304bc6 100644 --- a/shim/undo_shim.c +++ b/shim/undo_shim.c @@ -344,6 +344,20 @@ static int ignored(const char *abs) #define DEDUP_CAP 16384 static char *dedup_tab[DEDUP_CAP]; static int dedup_count; +static char dedup_dir[PATH_MAX]; + +/* The table covers one command. A shell preloaded with the shim + * (UNDO_CAPTURE_SHELL=1) outlives every session it runs, so a path saved + * for an earlier command must not suppress its backup in the next one. */ +static void dedup_reset(const char *dir) +{ + for (int i = 0; i < DEDUP_CAP; i++) { + free(dedup_tab[i]); + dedup_tab[i] = NULL; + } + dedup_count = 0; + snprintf(dedup_dir, sizeof dedup_dir, "%s", dir); +} static unsigned long path_hash(const char *s) { @@ -359,6 +373,11 @@ static unsigned long path_hash(const char *s) * records it and returns 0. */ static int mod_seen(const char *abs) { + const char *dir = session_dir(); + if (!dir) + return 0; + if (strcmp(dedup_dir, dir) != 0) + dedup_reset(dir); if (dedup_count * 4 >= DEDUP_CAP * 3) return 0; /* table nearly full: stop deduping, keep saving */ unsigned long i = path_hash(abs) & (DEDUP_CAP - 1); @@ -547,11 +566,9 @@ int rmdir(const char *path) int ok; handle_rmdir_pre(AT_FDCWD, path, abs, mode, &ok); int rc = real_rmdir(path); - if (rc == 0 && ok) { - in_shim = 1; + if (rc == 0 && ok) jwrite("rmdir", abs, mode, NULL); - in_shim = 0; - } + in_shim = 0; return rc; } diff --git a/test/e2e.sh b/test/e2e.sh index 159eab0..4065e92 100755 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -187,7 +187,40 @@ sd="$UNDO_DATA_DIR/sessions/$last" "$UNDO" -y >/dev/null [[ $(cat "$PLAY/churn.txt") == original ]] || fail "dedup broke restore, got $(cat "$PLAY/churn.txt")" -echo "== case 19: undo doctor passes its live self-test" +echo "== case 19: a failed rmdir does not stop the rest of the command being recorded" +mkdir -p "$PLAY/full/x" "$PLAY/gone" +run_armed "rmdir $PLAY/full $PLAY/gone || true" +[[ -d $PLAY/full && ! -d $PLAY/gone ]] || fail "rmdir did not run as expected" +"$UNDO" -y >/dev/null +[[ -d $PLAY/gone ]] || fail "second rmdir was not journaled after the first failed" + +# One long-lived process writing the same file under two sessions in a +# row: what UNDO_CAPTURE_SHELL=1 does, where the shim is loaded into the +# shell itself rather than into a fresh child per command. +if command -v python3 >/dev/null 2>&1; then + echo "== case 20: one process spanning two sessions backs up in both" + f=$PLAY/shared.txt + echo v0 >"$f" + s1=$UNDO_DATA_DIR/sessions/$(date +%s%N | cut -c1-16); mkdir -p "$s1/data" + echo "write v1" >"$s1/cmd"; sleep 0.01 + s2=$UNDO_DATA_DIR/sessions/$(date +%s%N | cut -c1-16); mkdir -p "$s2/data" + echo "write v2" >"$s2/cmd" + LD_PRELOAD="$LIB" python3 -c " +import os, sys +for sess, body in ((sys.argv[1], 'v1'), (sys.argv[2], 'v2')): + os.environ['UNDO_SESSION'] = sess + with open(sys.argv[3], 'w') as fh: + fh.write(body + '\n') +" "$s1" "$s2" "$f" + [[ $(cat "$f") == v2 ]] || fail "writes did not run" + grep -q "shared.txt" "$s2/journal" 2>/dev/null || fail "second session recorded nothing" + "$UNDO" -y >/dev/null + [[ $(cat "$f") == v1 ]] || fail "second session did not restore v1, got $(cat "$f")" +else + echo "== case 20: skipped (no python3)" +fi + +echo "== case 21: undo doctor passes its live self-test" out=$("$UNDO" doctor 2>&1) || fail "doctor exited non-zero: $out" grep -q "\[ok \] capture" <<<"$out" || fail "doctor capture check did not pass" grep -q "\[ok \] restore" <<<"$out" || fail "doctor restore check did not pass"