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
19 changes: 18 additions & 1 deletion attach.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,12 @@ static int log_already_replayed;
** killed/crashed (socket still on disk), ENOENT means clean exit (socket was
** unlinked; end marker is already in the log).
** Pass 0 when replaying for a running session (no end message printed).
** Returns 1 if a log was found and replayed, 0 if no log exists. */
** Returns 1 if a log was found and replayed, 0 if no log exists.
**
** Only the last SCROLLBACK_SIZE bytes of the log are replayed to avoid
** overwhelming the terminal when attaching to a session with a large log
** (e.g. a long-running build). This matches the in-memory ring-buffer cap
** used when replaying a live session's scrollback. */
int replay_session_log(int saved_errno)
{
char log_path[600];
Expand All @@ -239,6 +244,18 @@ int replay_session_log(int saved_errno)
{
unsigned char rbuf[BUFSIZE];
ssize_t n;
off_t log_size;

/* Seek to the last SCROLLBACK_SIZE bytes so that a very large
* log (e.g. from a long build session) does not flood the
* terminal. If the log is smaller than SCROLLBACK_SIZE, start
* from the beginning. */
log_size = lseek(logfd, 0, SEEK_END);
if (log_size > (off_t)SCROLLBACK_SIZE)
lseek(logfd, log_size - (off_t)SCROLLBACK_SIZE,
SEEK_SET);
else
lseek(logfd, 0, SEEK_SET);

while ((n = read(logfd, rbuf, sizeof(rbuf))) > 0)
write(1, rbuf, (size_t)n);
Expand Down
78 changes: 77 additions & 1 deletion tests/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,83 @@ run "$ATCH" start -C foo C-bad sleep 999
assert_exit "-C invalid: exit 1" 1 "$rc"
assert_contains "-C invalid: message" "Invalid log size" "$out"

# ── 21. no-args → usage ──────────────────────────────────────────────────────
# ── 21. replay_session_log: bounded replay (last SCROLLBACK_SIZE bytes only) ──
#
# Regression test for: replay_session_log must replay at most SCROLLBACK_SIZE
# (128 KB) of the session log. Without this cap, attaching a session with a
# large log (e.g. a long-running build) causes an overwhelming scroll that
# appears to loop indefinitely.
#
# Strategy: create a synthetic .log file larger than SCROLLBACK_SIZE (128 KB),
# attach to the dead session using expect(1) to supply a PTY (required by
# attach_main), and verify the output byte count and content.
#
# expect(1) is available on macOS by default and on most Linux distros.
# If absent, the test is skipped.

if command -v expect >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then
mkdir -p "$HOME/.cache/atch"

REPLAY_SOCK="$HOME/.cache/atch/replay-cap-sess"
REPLAY_LOG="${REPLAY_SOCK}.log"

# Build a log of ~290 KB: OLD_DATA fills the first 160 KB,
# NEW_DATA fills the last 128 KB. Only NEW_DATA should appear in replay.
python3 -c "
import sys
old = b'OLD_DATA_LINE_PADDED_TO_EXACTLY_32B\n'
new = b'NEW_DATA_LINE_PADDED_TO_EXACTLY_32B\n'
old_count = (160 * 1024) // len(old) + 1
new_count = (128 * 1024) // len(new) + 1
sys.stdout.buffer.write(old * old_count)
sys.stdout.buffer.write(new * new_count)
" > "$REPLAY_LOG"

# Use expect to run atch attach with a real PTY, capturing all output.
# atch exits immediately after replaying the log for a dead session.
REPLAY_OUT=$(mktemp)
expect - << EXPECT_EOF > "$REPLAY_OUT" 2>/dev/null
set timeout 10
spawn $ATCH attach replay-cap-sess
expect eof
EXPECT_EOF

OUT_BYTES=$(wc -c < "$REPLAY_OUT")

# Output must stay within SCROLLBACK_SIZE + some terminal-overhead margin
# (expect may inject a few extra bytes; 256 KB is a safe upper bound).
MAX_BYTES=262144
if [ "$OUT_BYTES" -le "$MAX_BYTES" ]; then
ok "replay-log: output bounded ($OUT_BYTES <= $MAX_BYTES bytes)"
else
fail "replay-log: output bounded" \
"<= $MAX_BYTES bytes" "$OUT_BYTES bytes"
fi

# Replayed content must come from the tail (NEW_DATA present).
if grep -q "NEW_DATA" "$REPLAY_OUT" 2>/dev/null; then
ok "replay-log: tail of log replayed (NEW_DATA present)"
else
fail "replay-log: tail of log replayed (NEW_DATA present)" \
"NEW_DATA in output" "not found"
fi

# HEAD of log must NOT appear (OLD_DATA absent).
if grep -q "OLD_DATA" "$REPLAY_OUT" 2>/dev/null; then
fail "replay-log: head of log skipped (OLD_DATA absent)" \
"no OLD_DATA" "OLD_DATA found"
else
ok "replay-log: head of log skipped (OLD_DATA absent)"
fi

rm -f "$REPLAY_OUT" "$REPLAY_LOG"
else
ok "replay-log: skip (expect or python3 not available)"
ok "replay-log: skip (expect or python3 not available)"
ok "replay-log: skip (expect or python3 not available)"
fi

# ── 22. no-args → usage ──────────────────────────────────────────────────────

# Invoking with zero arguments calls usage() (exits 0, prints help).
# We already consumed the binary name in main, so argc < 1 → usage().
Expand Down