Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/undo/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os/exec"
"path/filepath"
"strings"
"syscall"

"github.com/edaywalid/undo/internal/session"
)
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 21 additions & 4 deletions shim/undo_shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
35 changes: 34 additions & 1 deletion test/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down