Skip to content

Commit 16b005c

Browse files
committed
Add tail command to read session logs
1 parent e1109b5 commit 16b005c

3 files changed

Lines changed: 183 additions & 1 deletion

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ If no command is given, `$SHELL` is used.
8383
| `push <session>` | Copy stdin verbatim to the session. |
8484
| `kill [-f] <session>` | Gracefully stop a session (SIGTERM, then SIGKILL after 5 s if needed). With `-f` / `--force`, skip the grace period and send SIGKILL immediately. |
8585
| `clear [<session>]` | Truncate the on-disk session log. Defaults to the current session when run inside one. |
86+
| `tail [-f] [-n N] <session>` | Print the last N lines of the session log (default: 10). With `-f`, follow new output as it is written. |
8687
| `list` | List all sessions. Shows `[attached]` when a client is connected, `[stale]` for leftover sockets with no running master. Prints `(no sessions)` when the list is empty. |
8788
| `current` | Print the current session name and exit 0 if inside a session; exit 1 silently if not. |
8889

@@ -166,6 +167,16 @@ atch -e '^A' attach work
166167
atch list
167168
```
168169

170+
**Inspect the last 20 lines of a session log:**
171+
```sh
172+
atch tail -n 20 work
173+
```
174+
175+
**Follow a session's output without attaching:**
176+
```sh
177+
atch tail -f work
178+
```
179+
169180
**Kill a session:**
170181
```sh
171182
atch kill work

atch.c

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,116 @@ static int cmd_clear(int argc, char **argv)
542542
return 0;
543543
}
544544

545+
/* Scan log fd backward and return the byte offset to seek to before
546+
** printing the last nlines lines of output. */
547+
static off_t find_tail_start(int fd, off_t size, int nlines)
548+
{
549+
char buf[BUFSIZE];
550+
int count = 0;
551+
off_t pos = size;
552+
553+
while (pos > 0 && count <= nlines) {
554+
off_t chunk = pos > (off_t)sizeof(buf) ? (off_t)sizeof(buf) : pos;
555+
ssize_t n;
556+
ssize_t i;
557+
558+
pos -= chunk;
559+
lseek(fd, pos, SEEK_SET);
560+
n = read(fd, buf, (size_t)chunk);
561+
if (n <= 0)
562+
break;
563+
for (i = n - 1; i >= 0; i--) {
564+
if (buf[i] == '\n') {
565+
if (++count > nlines)
566+
return pos + i + 1;
567+
}
568+
}
569+
}
570+
return 0;
571+
}
572+
573+
/* atch tail [-f] [-n N] <session> — print last N lines of session log */
574+
static int cmd_tail(int argc, char **argv)
575+
{
576+
int follow = 0, nlines = 10;
577+
char log_path[600];
578+
unsigned char rbuf[BUFSIZE];
579+
off_t size, start;
580+
ssize_t n;
581+
int fd;
582+
583+
/* Parse -f and -n N (also -nN) before the session name */
584+
while (argc >= 1 && argv[0][0] == '-' && argv[0][1] != '\0') {
585+
if (strcmp(argv[0], "-f") == 0) {
586+
follow = 1;
587+
argc--;
588+
argv++;
589+
} else if (strcmp(argv[0], "-n") == 0) {
590+
if (argc < 2) {
591+
printf("%s: -n requires an argument\n", progname);
592+
printf("Try '%s --help' for more information.\n",
593+
progname);
594+
return 1;
595+
}
596+
nlines = atoi(argv[1]);
597+
argc -= 2;
598+
argv += 2;
599+
} else if (strncmp(argv[0], "-n", 2) == 0 &&
600+
argv[0][2] != '\0') {
601+
nlines = atoi(argv[0] + 2);
602+
argc--;
603+
argv++;
604+
} else {
605+
printf("%s: Invalid option '%s'\n", progname, argv[0]);
606+
printf("Try '%s --help' for more information.\n",
607+
progname);
608+
return 1;
609+
}
610+
}
611+
if (nlines < 1)
612+
nlines = 1;
613+
614+
if (consume_session(&argc, &argv))
615+
return 1;
616+
if (argc > 0) {
617+
printf("%s: Invalid number of arguments.\n", progname);
618+
printf("Try '%s --help' for more information.\n", progname);
619+
return 1;
620+
}
621+
622+
snprintf(log_path, sizeof(log_path), "%s.log", sockname);
623+
fd = open(log_path, O_RDONLY);
624+
if (fd < 0) {
625+
if (errno == ENOENT)
626+
printf("%s: no log for session '%s'\n", progname,
627+
session_shortname());
628+
else
629+
printf("%s: %s: %s\n", progname, log_path,
630+
strerror(errno));
631+
return 1;
632+
}
633+
634+
size = lseek(fd, 0, SEEK_END);
635+
if (size > 0) {
636+
start = find_tail_start(fd, size, nlines);
637+
lseek(fd, start, SEEK_SET);
638+
while ((n = read(fd, rbuf, sizeof(rbuf))) > 0)
639+
write(1, rbuf, (size_t)n);
640+
}
641+
642+
if (follow) {
643+
signal(SIGPIPE, SIG_IGN);
644+
for (;;) {
645+
usleep(250000);
646+
while ((n = read(fd, rbuf, sizeof(rbuf))) > 0)
647+
write(1, rbuf, (size_t)n);
648+
}
649+
}
650+
651+
close(fd);
652+
return 0;
653+
}
654+
545655
/* Default: atch <session> [cmd...] — attach-or-create */
546656
static int cmd_open(char *session, int argc, char **argv)
547657
{
@@ -595,6 +705,10 @@ static void usage(void)
595705
" -f, --force\t\t\tSkip grace period, send SIGKILL immediately\n"
596706
" clear [<session>]"
597707
"\t\t\tTruncate the session log\n"
708+
" tail [-f] [-n N] <session>"
709+
"\tPrint last N lines of session log\n"
710+
" -f\t\t\t\tFollow log output\n"
711+
" -n <lines>\t\t\tNumber of lines (default 10)\n"
598712
" list\t\t\t\t\tList all sessions\n"
599713
" current\t\t\t\tPrint current session name\n"
600714
"\n"
@@ -779,6 +893,8 @@ int main(int argc, char **argv)
779893
return cmd_kill(argc, argv);
780894
if (is_cmd(cmd, "clear", NULL, NULL))
781895
return cmd_clear(argc, argv);
896+
if (is_cmd(cmd, "tail", NULL, NULL))
897+
return cmd_tail(argc, argv);
782898

783899
/* Smart default: treat first arg as session name → attach-or-create */
784900
return cmd_open((char *)cmd, argc, argv);

tests/test.sh

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,14 +628,69 @@ run "$ATCH" start -C foo C-bad sleep 999
628628
assert_exit "-C invalid: exit 1" 1 "$rc"
629629
assert_contains "-C invalid: message" "Invalid log size" "$out"
630630

631-
# ── 21. no-args → usage ──────────────────────────────────────────────────────
631+
# ── 21. tail command ─────────────────────────────────────────────────────────
632+
633+
# tail with no session
634+
run "$ATCH" tail
635+
assert_exit "tail: no session → exit 1" 1 "$rc"
636+
assert_contains "tail: no session → message" "No session was specified" "$out"
637+
638+
# tail on nonexistent session (no log file)
639+
run "$ATCH" tail s-noexist-tail
640+
assert_exit "tail: no log → exit 1" 1 "$rc"
641+
assert_contains "tail: no log → message" "no log" "$out"
642+
643+
# tail basic: start session that writes 20 numbered lines then sleeps
644+
# Use zero-padded numbers so e.g. "line01" is not a substring of "line11"
645+
"$ATCH" start s-tail sh -c \
646+
'i=1; while [ $i -le 20 ]; do printf "line%02d\n" $i; i=$((i+1)); done; sleep 999'
647+
sleep 0.2
648+
# default 10 lines: should see line11..line20 but not line01
649+
run "$ATCH" tail s-tail
650+
assert_exit "tail: exits 0" 0 "$rc"
651+
assert_contains "tail: shows recent line" "line20" "$out"
652+
assert_not_contains "tail: omits early lines" "line01" "$out"
653+
654+
# -n 5: should see line16..line20
655+
run "$ATCH" tail -n 5 s-tail
656+
assert_exit "tail -n 5: exits 0" 0 "$rc"
657+
assert_contains "tail -n 5: shows line in range" "line20" "$out"
658+
assert_not_contains "tail -n 5: omits earlier lines" "line10" "$out"
659+
660+
# -n with combined flag style (-n5)
661+
run "$ATCH" tail -n5 s-tail
662+
assert_exit "tail -n5 (compact): exits 0" 0 "$rc"
663+
assert_contains "tail -n5 (compact): shows line20" "line20" "$out"
664+
665+
# extra args rejected
666+
run "$ATCH" tail s-tail extra
667+
assert_exit "tail: extra arg → exit 1" 1 "$rc"
668+
assert_contains "tail: extra arg → message" "Invalid number of arguments" "$out"
669+
670+
# invalid option
671+
run "$ATCH" tail -x s-tail
672+
assert_exit "tail: invalid option → exit 1" 1 "$rc"
673+
assert_contains "tail: invalid option → message" "Invalid option" "$out"
674+
675+
# -n missing argument
676+
run "$ATCH" tail -n
677+
assert_exit "tail -n missing arg: exit 1" 1 "$rc"
678+
assert_contains "tail -n missing arg: message" "-n requires an argument" "$out"
679+
680+
tidy s-tail
681+
682+
# ── 22. no-args → usage ──────────────────────────────────────────────────────
632683

633684
# Invoking with zero arguments calls usage() (exits 0, prints help).
634685
# We already consumed the binary name in main, so argc < 1 → usage().
635686
run "$ATCH"
636687
assert_exit "no args: exits 0 (usage)" 0 "$rc"
637688
assert_contains "no args: shows Usage:" "Usage:" "$out"
638689

690+
# tail command appears in help
691+
run "$ATCH" --help
692+
assert_contains "help: shows tail command" "tail" "$out"
693+
639694
# ── summary ──────────────────────────────────────────────────────────────────
640695

641696
printf "\n1..%d\n" "$T"

0 commit comments

Comments
 (0)