]> git.ipfire.org Git - thirdparty/util-linux.git/blob - term-utils/scriptreplay.c
7e1eec7dba857cb8ffea9d9a280a3d3cd6775576
[thirdparty/util-linux.git] / term-utils / scriptreplay.c
1 /*
2 * Copyright (C) 2024, Jonathan Ketchker <jonathan@ketchker.com>
3 * Copyright (C) 2008-2019, Karel Zak <kzak@redhat.com>
4 * Copyright (C) 2008, James Youngman <jay@gnu.org>
5 *
6 * This file is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This file is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 *
17 * Based on scriptreplay.pl by Joey Hess <joey@kitenet.net>
18 */
19
20 #include <stdio.h>
21 #include <stdarg.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <errno.h>
25 #include <time.h>
26 #include <limits.h>
27 #include <math.h>
28 #include <sys/select.h>
29 #include <unistd.h>
30 #include <getopt.h>
31 #include <sys/time.h>
32 #include <termios.h>
33 #include <fcntl.h>
34 #include <stdbool.h>
35
36 #include "c.h"
37 #include "xalloc.h"
38 #include "closestream.h"
39 #include "nls.h"
40 #include "strutils.h"
41 #include "optutils.h"
42 #include "script-playutils.h"
43
44 static void __attribute__((__noreturn__))
45 usage(void)
46 {
47 FILE *out = stdout;
48 fputs(USAGE_HEADER, out);
49 fprintf(out,
50 _(" %s [options] <timingfile> [<typescript> [<divisor>]]\n"),
51 program_invocation_short_name);
52
53 fputs(USAGE_SEPARATOR, out);
54 fputs(_("Play back terminal typescripts, using timing information.\n"), out);
55
56 fputs(USAGE_OPTIONS, out);
57 fputs(_(" -t, --timing <file> script timing log file\n"), out);
58 fputs(_(" -T, --log-timing <file> alias to -t\n"), out);
59 fputs(_(" -I, --log-in <file> script stdin log file\n"), out);
60 fputs(_(" -O, --log-out <file> script stdout log file (default)\n"), out);
61 fputs(_(" -B, --log-io <file> script stdin and stdout log file\n"), out);
62 fputs(USAGE_SEPARATOR, out);
63 fputs(_(" -s, --typescript <file> deprecated alias to -O\n"), out);
64
65 fputs(USAGE_SEPARATOR, out);
66 fputs(_(" --summary display overview about recorded session and exit\n"), out);
67 fputs(_(" -d, --divisor <num> speed up or slow down execution with time divisor\n"), out);
68 fputs(_(" -m, --maxdelay <num> wait at most this many seconds between updates\n"), out);
69 fputs(_(" -x, --stream <name> stream type (out, in, signal or info)\n"), out);
70 fputs(_(" -c, --cr-mode <type> CR char mode (auto, never, always)\n"), out);
71
72 fputs(USAGE_SEPARATOR, out);
73 fprintf(out, USAGE_HELP_OPTIONS(25));
74
75 fputs(USAGE_SEPARATOR, out);
76 fputs(_("Key bindings:\n"), out);
77 fputs(_(" space toggles between pause and play\n"), out);
78 fputs(_(" up-arrow increases playback speed with ten percent\n"), out);
79 fputs(_(" down-arrow decreases playback speed with ten percent\n"), out);
80
81 fprintf(out, USAGE_MAN_TAIL("scriptreplay(1)"));
82 exit(EXIT_SUCCESS);
83 }
84
85 static double
86 getnum(const char *s)
87 {
88 const double d = strtod_or_err(s, _("failed to parse number"));
89
90 if (isnan(d)) {
91 errno = EINVAL;
92 err(EXIT_FAILURE, "%s: %s", _("failed to parse number"), s);
93 }
94 return d;
95 }
96
97 static void
98 delay_for(const struct timeval *delay)
99 {
100 #ifdef HAVE_NANOSLEEP
101 struct timespec ts, remainder;
102 ts.tv_sec = (time_t) delay->tv_sec;
103 ts.tv_nsec = delay->tv_usec * 1000;
104
105 DBG(TIMING, ul_debug("going to sleep for %"PRId64".%06"PRId64,
106 (int64_t) delay->tv_sec, (int64_t) delay->tv_usec));
107
108 while (-1 == nanosleep(&ts, &remainder)) {
109 if (EINTR == errno)
110 ts = remainder;
111 else
112 break;
113 }
114 #else
115 {
116 struct timeval timeout;
117
118 /* On Linux, select() modifies timeout */
119 memcpy(&timeout, delay, sizeof(struct timeval));
120 select(0, NULL, NULL, NULL, &timeout);
121 }
122 #endif
123 }
124
125 static void
126 appendchr(char *buf, size_t bufsz, int c)
127 {
128 size_t sz;
129
130 if (strchr(buf, c))
131 return; /* already in */
132
133 sz = strlen(buf);
134 if (sz + 1 < bufsz)
135 buf[sz] = c;
136 }
137
138 static int
139 setterm(struct termios *backup, int *saved_flag)
140 {
141 struct termios tattr;
142
143 *saved_flag = fcntl(STDIN_FILENO, F_GETFL);
144 if (*saved_flag == -1)
145 err(EXIT_FAILURE, _("unexpected fcntl failure"));
146 fcntl(STDIN_FILENO, F_SETFL, *saved_flag | O_NONBLOCK);
147
148 if (tcgetattr(STDOUT_FILENO, backup) != 0) {
149 if (errno != ENOTTY) /* For debugger. */
150 err(EXIT_FAILURE, _("unexpected tcgetattr failure"));
151 return 0;
152 }
153 tattr = *backup;
154 cfmakeraw(&tattr);
155 tattr.c_lflag |= ISIG;
156 tattr.c_iflag |= IXON;
157 tcsetattr(STDOUT_FILENO, TCSANOW, &tattr);
158 return 1;
159 }
160
161 int
162 main(int argc, char *argv[])
163 {
164 static const struct timeval mindelay = { .tv_sec = 0, .tv_usec = 100 };
165 static const struct timeval input_delay = { .tv_sec = 0, .tv_usec = 100000 };
166 struct timeval step_delay = { 0, 0 };
167 struct timeval maxdelay;
168
169 int isterm;
170 int saved_flag;
171 struct termios saved;
172
173 struct replay_setup *setup = NULL;
174 struct replay_step *step = NULL;
175 char streams[6] = {0}; /* IOSI - in, out, signal,info */
176 const char *log_out = NULL,
177 *log_in = NULL,
178 *log_io = NULL,
179 *log_tm = NULL;
180 double divi = 1;
181 int diviopt = FALSE, idx;
182 int ch, rc = 0, crmode = REPLAY_CRMODE_AUTO, summary = 0;
183 enum {
184 OPT_SUMMARY = CHAR_MAX + 1
185 };
186
187 static const struct option longopts[] = {
188 { "cr-mode", required_argument, 0, 'c' },
189 { "timing", required_argument, 0, 't' },
190 { "log-timing", required_argument, 0, 'T' },
191 { "log-in", required_argument, 0, 'I' },
192 { "log-out", required_argument, 0, 'O' },
193 { "log-io", required_argument, 0, 'B' },
194 { "typescript", required_argument, 0, 's' },
195 { "divisor", required_argument, 0, 'd' },
196 { "maxdelay", required_argument, 0, 'm' },
197 { "stream", required_argument, 0, 'x' },
198 { "summary", no_argument, 0, OPT_SUMMARY },
199 { "version", no_argument, 0, 'V' },
200 { "help", no_argument, 0, 'h' },
201 { NULL, 0, 0, 0 }
202 };
203 static const ul_excl_t excl[] = { /* rows and cols in ASCII order */
204 { 'O', 's' },
205 { 0 }
206 };
207 int excl_st[ARRAY_SIZE(excl)] = UL_EXCL_STATUS_INIT;
208 /* Because we use space as a separator, we can't afford to use any
209 * locale which tolerates a space in a number. In any case, script.c
210 * sets the LC_NUMERIC locale to C, anyway.
211 */
212 setlocale(LC_ALL, "");
213 setlocale(LC_NUMERIC, "C");
214
215 bindtextdomain(PACKAGE, LOCALEDIR);
216 textdomain(PACKAGE);
217 close_stdout_atexit();
218
219 replay_init_debug();
220 timerclear(&maxdelay);
221
222 while ((ch = getopt_long(argc, argv, "B:c:I:O:T:t:s:d:m:x:Vh", longopts, NULL)) != -1) {
223
224 err_exclusive_options(ch, longopts, excl, excl_st);
225
226 switch(ch) {
227 case 'c':
228 if (strcmp("auto", optarg) == 0)
229 crmode = REPLAY_CRMODE_AUTO;
230 else if (strcmp("never", optarg) == 0)
231 crmode = REPLAY_CRMODE_NEVER;
232 else if (strcmp("always", optarg) == 0)
233 crmode = REPLAY_CRMODE_ALWAYS;
234 else
235 errx(EXIT_FAILURE, _("unsupported mode name: '%s'"), optarg);
236 break;
237 case 't':
238 case 'T':
239 log_tm = optarg;
240 break;
241 case 'O':
242 case 's':
243 log_out = optarg;
244 break;
245 case 'I':
246 log_in = optarg;
247 break;
248 case 'B':
249 log_io = optarg;
250 break;
251 case 'd':
252 diviopt = TRUE;
253 divi = getnum(optarg);
254 break;
255 case 'm':
256 strtotimeval_or_err(optarg, &maxdelay, _("failed to parse maximal delay argument"));
257 break;
258 case 'x':
259 if (strcmp("in", optarg) == 0)
260 appendchr(streams, sizeof(streams), 'I');
261 else if (strcmp("out", optarg) == 0)
262 appendchr(streams, sizeof(streams), 'O');
263 else if (strcmp("signal", optarg) == 0)
264 appendchr(streams, sizeof(streams), 'S');
265 else if (strcmp("info", optarg) == 0)
266 appendchr(streams, sizeof(streams), 'H');
267 else
268 errx(EXIT_FAILURE, _("unsupported stream name: '%s'"), optarg);
269 break;
270 case OPT_SUMMARY:
271 summary = 1;
272 break;
273 case 'V':
274 print_version(EXIT_SUCCESS);
275 case 'h':
276 usage();
277 default:
278 errtryhelp(EXIT_FAILURE);
279 }
280 }
281 argc -= optind;
282 argv += optind;
283 idx = 0;
284
285 if (summary)
286 streams[0] = 'H', streams[1] = '\0';
287
288 if (!log_tm && idx < argc)
289 log_tm = argv[idx++];
290 if (!log_out && !summary && !log_in && !log_io)
291 log_out = idx < argc ? argv[idx++] : "typescript";
292
293 if (!diviopt)
294 divi = idx < argc ? getnum(argv[idx]) : 1;
295
296 if (!log_tm)
297 errx(EXIT_FAILURE, _("timing file not specified"));
298 if (!(log_out || log_in || log_io) && !summary)
299 errx(EXIT_FAILURE, _("data log file not specified"));
300
301 setup = replay_new_setup();
302
303 if (replay_set_timing_file(setup, log_tm) != 0)
304 err(EXIT_FAILURE, _("cannot open %s"), log_tm);
305
306 if (log_out && replay_associate_log(setup, "O", log_out) != 0)
307 err(EXIT_FAILURE, _("cannot open %s"), log_out);
308
309 if (log_in && replay_associate_log(setup, "I", log_in) != 0)
310 err(EXIT_FAILURE, _("cannot open %s"), log_in);
311
312 if (log_io && replay_associate_log(setup, "IO", log_io) != 0)
313 err(EXIT_FAILURE, _("cannot open %s"), log_io);
314
315 if (!*streams) {
316 /* output is preferred default */
317 if (log_out || log_io)
318 appendchr(streams, sizeof(streams), 'O');
319 else if (log_in)
320 appendchr(streams, sizeof(streams), 'I');
321 }
322
323 replay_set_default_type(setup,
324 *streams && streams[1] == '\0' ? *streams : 'O');
325 replay_set_crmode(setup, crmode);
326
327 if (divi != 1)
328 replay_set_delay_div(setup, divi);
329 if (timerisset(&maxdelay))
330 replay_set_delay_max(setup, &maxdelay);
331 replay_set_delay_min(setup, &mindelay);
332
333 isterm = setterm(&saved, &saved_flag);
334
335 do {
336 switch (fgetc(stdin)) {
337 case ' ':
338 replay_toggle_pause(setup);
339 break;
340 case '\033':
341 ch = fgetc(stdin);
342 if (ch == '[') {
343 ch = fgetc(stdin);
344 if (ch == 'A') { /* Up arrow */
345 divi *= 1.1;
346 replay_set_delay_div(setup, divi);
347 } else if (ch == 'B') { /* Down arrow */
348 divi *= 0.9;
349 if (divi < 0.1)
350 divi = 0.1;
351 replay_set_delay_div(setup, divi);
352 } else if (ch == 'C') { /* Right arrow */
353 rc = replay_emit_step_data(setup, step, STDOUT_FILENO);
354 if (!rc)
355 rc = replay_get_next_step(setup, streams, &step);
356 if (!rc) {
357 struct timeval *delay = replay_step_get_delay(step);
358 if (delay && timerisset(delay))
359 step_delay = *delay;
360 }
361 }
362 }
363 break;
364 }
365 if (rc)
366 break;
367
368 if (replay_get_is_paused(setup)) {
369 delay_for(&input_delay);
370 continue;
371 }
372
373 if (timerisset(&step_delay)) {
374 const struct timeval *timeout = (timercmp(&step_delay, &input_delay, <) ? (&step_delay) : (&input_delay));
375 delay_for(timeout);
376 timersub(&step_delay, timeout, &step_delay);
377 if (step_delay.tv_sec < 0 || step_delay.tv_usec < 0)
378 timerclear(&step_delay);
379 continue;
380 }
381
382 if (!timerisset(&step_delay) && step)
383 rc = replay_emit_step_data(setup, step, STDOUT_FILENO);
384 if (rc)
385 break;
386
387 rc = replay_get_next_step(setup, streams, &step);
388 if (rc)
389 break;
390
391 if (!summary) {
392 struct timeval *delay = replay_step_get_delay(step);
393
394 if (delay && timerisset(delay))
395 step_delay = *delay;
396 }
397 } while (rc == 0);
398
399 if (isterm) {
400 fcntl(STDIN_FILENO, F_SETFL, &saved_flag);
401 tcsetattr(STDOUT_FILENO, TCSADRAIN, &saved);
402 }
403
404 if (step && rc < 0)
405 err(EXIT_FAILURE, _("%s: log file error"), replay_step_get_filename(step));
406 else if (rc < 0)
407 err(EXIT_FAILURE, _("%s: line %d: timing file error"),
408 replay_get_timing_file(setup),
409 replay_get_timing_line(setup));
410 printf("\n");
411 replay_free_setup(setup);
412
413 exit(EXIT_SUCCESS);
414 }