Line data Source code
1 : /**
2 : * @file pty_sync.c
3 : * @brief PTY output reading and synchronisation (poll + timeout).
4 : */
5 :
6 : #define _DEFAULT_SOURCE
7 : #define _XOPEN_SOURCE 600
8 :
9 : #include "pty_internal.h"
10 : #include <errno.h>
11 : #include <poll.h>
12 : #include <stdio.h>
13 : #include <unistd.h>
14 : #include <time.h>
15 : #include <sys/wait.h>
16 :
17 : /* ── Time helpers ────────────────────────────────────────────────────── */
18 :
19 947 : static long now_ms(void) {
20 : struct timespec ts;
21 947 : clock_gettime(CLOCK_MONOTONIC, &ts);
22 947 : return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
23 : }
24 :
25 : /* ── Read and feed ───────────────────────────────────────────────────── */
26 :
27 : int g_trace_active = 0;
28 0 : void pty_trace_enable(int on) { g_trace_active = on; }
29 :
30 : /** @brief Reads available data from master fd and feeds to screen. */
31 890 : static int read_and_feed(PtySession *s) {
32 : char buf[8192];
33 890 : ssize_t n = read(s->master_fd, buf, sizeof(buf));
34 890 : if (n > 0) {
35 515 : if (g_trace_active) {
36 0 : printf(" [TRACE %zd bytes]:", n);
37 0 : for (ssize_t i = 0; i < n && i < 120; i++) {
38 0 : unsigned char c = (unsigned char)buf[i];
39 0 : if (c == 0x1B) printf(" ESC");
40 0 : else if (c < 0x20 || c > 0x7E) printf(" %02X", c);
41 0 : else printf(" %c", c);
42 : }
43 0 : printf("\n"); fflush(stdout);
44 : }
45 515 : pty_screen_feed(s->screen, buf, (size_t)n);
46 515 : return (int)n;
47 : }
48 : /* Return -2 to distinguish EAGAIN from EIO/EOF */
49 375 : if (n < 0 && errno == EAGAIN) return -2;
50 248 : return 0;
51 : }
52 :
53 254 : int pty_drain(PtySession *s) {
54 254 : if (!s || s->master_fd < 0) return 0;
55 254 : int total = 0;
56 : /* Retry on EAGAIN — kernel may need a moment to deliver buffered PTY data
57 : * after the slave side closes (POLLHUP race on non-blocking master fds). */
58 375 : for (int attempt = 0; attempt < 3; attempt++) {
59 68 : for (;;) {
60 443 : int n = read_and_feed(s);
61 443 : if (n > 0) { total += n; continue; }
62 375 : if (n == -2) break; /* EAGAIN: no data right now, retry after sleep */
63 248 : return total; /* EIO/EOF: slave closed and buffer empty */
64 : }
65 127 : if (total > 0) break; /* Got data — no need to retry */
66 121 : usleep(5000); /* 5 ms: give kernel time to deliver buffered data */
67 : }
68 6 : return total;
69 : }
70 :
71 : /* ── Synchronisation ─────────────────────────────────────────────────── */
72 :
73 388 : int pty_wait_for(PtySession *s, const char *text, int timeout_ms) {
74 388 : if (!s || s->master_fd < 0) return -1;
75 :
76 388 : long deadline = now_ms() + timeout_ms;
77 388 : struct pollfd pfd = { .fd = s->master_fd, .events = POLLIN };
78 :
79 425 : for (;;) {
80 : /* Check if text is already on screen */
81 813 : if (pty_screen_contains(s, text)) return 0;
82 :
83 426 : long remaining = deadline - now_ms();
84 426 : if (remaining <= 0) return -1; /* Timeout */
85 :
86 426 : int ret = poll(&pfd, 1, (int)remaining);
87 426 : if (ret > 0 && (pfd.revents & POLLIN)) {
88 425 : read_and_feed(s);
89 1 : } else if (ret == 0) {
90 1 : return -1; /* Timeout */
91 : } else {
92 : /* POLLERR/POLLHUP — child may have exited; drain any buffered output */
93 0 : pty_drain(s);
94 0 : if (pty_screen_contains(s, text)) return 0;
95 : /* One more short drain in case the kernel delivered data after the HUP */
96 0 : usleep(10000);
97 0 : pty_drain(s);
98 0 : return pty_screen_contains(s, text) ? 0 : -1;
99 : }
100 : }
101 : }
102 :
103 31 : int pty_settle(PtySession *s, int quiet_ms) {
104 31 : if (!s || s->master_fd < 0) return -1;
105 :
106 31 : struct pollfd pfd = { .fd = s->master_fd, .events = POLLIN };
107 :
108 22 : for (;;) {
109 53 : int ret = poll(&pfd, 1, quiet_ms);
110 53 : if (ret == 0) return 0; /* Quiet period elapsed — settled */
111 22 : if (ret > 0 && (pfd.revents & POLLIN)) {
112 22 : if (read_and_feed(s) <= 0) return -1; /* EOF — child exited */
113 : } else {
114 0 : return -1; /* Error */
115 : }
116 : }
117 : }
118 :
119 127 : int pty_wait_exit(PtySession *s, int timeout_ms) {
120 127 : if (!s || s->child_pid <= 0) return -1;
121 :
122 : /* Drain any remaining output first so the screen buffer is up to date. */
123 127 : pty_drain(s);
124 :
125 127 : long deadline = now_ms() + timeout_ms;
126 6 : for (;;) {
127 133 : int status = 0;
128 133 : pid_t rc = waitpid(s->child_pid, &status, WNOHANG);
129 133 : if (rc == s->child_pid) {
130 : /* Drain once more to capture any output flushed just before exit. */
131 127 : pty_drain(s);
132 127 : s->child_pid = -1;
133 132 : if (WIFEXITED(status)) return WEXITSTATUS(status);
134 5 : if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
135 0 : return -1;
136 : }
137 6 : if (rc < 0) return -1;
138 6 : if (now_ms() >= deadline) return -1;
139 6 : usleep(10000); /* poll at 10 ms intervals */
140 : }
141 : }
|