]> git.ipfire.org Git - thirdparty/util-linux.git/blob - term-utils/scriptreplay.c
cfc0ca43a633380c086da4568a6fa56929994935
[thirdparty/util-linux.git] / term-utils / scriptreplay.c
1 /*
2 * Copyright (C) 2008, Karel Zak <kzak@redhat.com>
3 * Copyright (C) 2008, James Youngman <jay@gnu.org>
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 *
16 * Based on scriptreplay.pl by Joey Hess <joey@kitenet.net>
17 */
18
19 #include <stdio.h>
20 #include <stdarg.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <errno.h>
24 #include <time.h>
25 #include <limits.h>
26 #include <math.h>
27 #include <sys/select.h>
28 #include <unistd.h>
29 #include <getopt.h>
30
31
32 #include "c.h"
33 #include "debug.h"
34 #include "xalloc.h"
35 #include "closestream.h"
36 #include "nls.h"
37 #include "strutils.h"
38
39 static UL_DEBUG_DEFINE_MASK(scriptreplay);
40 UL_DEBUG_DEFINE_MASKNAMES(scriptreplay) = UL_DEBUG_EMPTY_MASKNAMES;
41
42 #define SCRIPTREPLAY_DEBUG_INIT (1 << 1)
43 #define SCRIPTREPLAY_DEBUG_TIMING (1 << 2)
44 #define SCRIPTREPLAY_DEBUG_LOG (1 << 3)
45 #define SCRIPTREPLAY_DEBUG_MISC (1 << 4)
46 #define SCRIPTREPLAY_DEBUG_ALL 0xFFFF
47
48 #define DBG(m, x) __UL_DBG(scriptreplay, SCRIPTREPLAY_DEBUG_, m, x)
49 #define ON_DBG(m, x) __UL_DBG_CALL(scriptreplay, SCRIPTREPLAY_DEBUG_, m, x)
50
51 #define SCRIPT_MIN_DELAY 0.0001 /* from original sripreplay.pl */
52
53 /*
54 * The script replay is driven by timing file where each entry describes one
55 * step in the replay. The timing step may refer input or output (or
56 * signal, extra informations, etc.)
57 *
58 * The step data are stored in log files, the right log file for the step is
59 * selected from replay_setup.
60 *
61 * TODO: move struct replay_{log,step,setup} to script-playutils.c to make it
62 * usable for scriptlive(1) code.
63 */
64
65 enum {
66 REPLAY_TIMING_SIMPLE, /* timing info in classic "<delta> <offset>" format */
67 REPLAY_TIMING_MULTI /* multiple streams in format "<type> <delta> <offset|etc> */
68 };
69
70 struct replay_log {
71 const char *streams; /* 'I'nput, 'O'utput or both */
72 const char *filename;
73 FILE *fp;
74 };
75
76 struct replay_step {
77 char type; /* 'I'nput, 'O'utput, ... */
78 double delay;
79 size_t size;
80
81 struct replay_log *data;
82 };
83
84 struct replay_setup {
85 struct replay_log *logs;
86 size_t nlogs;
87
88 struct replay_step step; /* current step */
89
90 FILE *timing_fp;
91 const char *timing_filename;
92 int timing_format;
93 int timing_line;
94 };
95
96 static void scriptreplay_init_debug(void)
97 {
98 __UL_INIT_DEBUG_FROM_ENV(scriptreplay, SCRIPTREPLAY_DEBUG_, 0, SCRIPTREPLAY_DEBUG);
99 }
100
101 static int ignore_line(FILE *f)
102 {
103 int c;
104
105 while((c = fgetc(f)) != EOF && c != '\n');
106 if (ferror(f))
107 return -errno;
108
109 DBG(LOG, ul_debug(" ignore line"));
110 return 0;
111 }
112
113 static int replay_set_timing_file(struct replay_setup *stp, const char *filename)
114 {
115 int c, rc = 0;
116
117 assert(stp);
118 assert(filename);
119
120 stp->timing_filename = filename;
121 stp->timing_line = 0;
122
123 stp->timing_fp = fopen(filename, "r");
124 if (!stp->timing_fp)
125 rc = -errno;
126 else {
127 /* detect timing file format */
128 c = fgetc(stp->timing_fp);
129 if (c != EOF) {
130 if (isdigit((unsigned int) c))
131 stp->timing_format = REPLAY_TIMING_SIMPLE;
132 else
133 stp->timing_format = REPLAY_TIMING_MULTI;
134 ungetc(c, stp->timing_fp);
135 } else if (ferror(stp->timing_fp))
136 rc = -errno;
137 }
138
139 if (rc) {
140 fclose(stp->timing_fp);
141 stp->timing_fp = NULL;
142 }
143
144 DBG(TIMING, ul_debug("timing file set to %s [rc=%d]", filename, rc));
145 return rc;
146 }
147
148 static int replay_associate_log(struct replay_setup *stp,
149 const char *streams, const char *filename)
150 {
151 struct replay_log *log;
152 int rc;
153
154 assert(stp);
155 assert(streams);
156 assert(filename);
157
158 stp->logs = xrealloc(stp->logs, (stp->nlogs + 1) * sizeof(*log));
159 log = &stp->logs[stp->nlogs];
160 stp->nlogs++;
161
162 log->filename = filename;
163 log->streams = streams;
164
165 /* open the file and skip the first line */
166 log->fp = fopen(filename, "r");
167 rc = log->fp == NULL ? -errno : ignore_line(log->fp);
168
169 DBG(LOG, ul_debug("accociate log file %s with '%s' [rc=%d]", filename, streams, rc));
170 return rc;
171 }
172
173 static int is_wanted_stream(char type, const char *streams)
174 {
175 if (streams == NULL)
176 return 1;
177 if (strchr(streams, type))
178 return 1;
179 return 0;
180 }
181
182 static int read_multistream_step(struct replay_step *step, FILE *f, char type)
183 {
184 int rc = 0;
185 char nl;
186
187 switch (type) {
188 case 'O': /* output */
189 case 'I': /* input */
190 rc = fscanf(f, "%lf %zu%c\n", &step->delay, &step->size, &nl);
191 if (rc != 3 || nl != '\n')
192 rc = -EINVAL;
193 else
194 rc = 0;
195 break;
196
197 case 'S': /* signal */
198 rc = ignore_line(f); /* not implemnted yet */
199 break;
200
201 case 'H': /* header */
202 rc = ignore_line(f); /* not implemnted yet */
203 break;
204 default:
205 break;
206 }
207
208 DBG(TIMING, ul_debug(" read step delay & size [rc=%d]", rc));
209 return rc;
210 }
211
212 static struct replay_log *replay_get_stream_log(struct replay_setup *stp, char stream)
213 {
214 size_t i;
215
216 for (i = 0; i < stp->nlogs; i++) {
217 struct replay_log *log = &stp->logs[i];
218
219 if (is_wanted_stream(stream, log->streams))
220 return log;
221 }
222 return NULL;
223 }
224
225 static int replay_seek_log(struct replay_log *log, size_t move)
226 {
227 DBG(LOG, ul_debug(" %s: seek ++ %zu", log->filename, move));
228 return fseek(log->fp, move, SEEK_CUR) == (off_t) -1 ? -errno : 0;
229 }
230
231 /* returns next step with pointer to the right log file for specified streams (e.g.
232 * "IOS" for in/out/signals) or all streams if stream is NULL.
233 *
234 * returns: 0 = success, <0 = error, 1 = done (EOF)
235 */
236 static int replay_get_next_step(struct replay_setup *stp, char *streams, struct replay_step **xstep)
237 {
238 struct replay_step *step;
239 int rc;
240 double ignored_delay = 0;
241
242 assert(stp);
243 assert(stp->timing_fp);
244 assert(xstep && *xstep);
245
246 /* old format supports only 'O'utput */
247 if (stp->timing_format == REPLAY_TIMING_SIMPLE &&
248 !is_wanted_stream('O', streams))
249 return 1;
250
251
252 step = &stp->step;
253 *xstep = NULL;
254
255 do {
256 struct replay_log *log = NULL;
257
258 rc = 1; /* done */
259 if (feof(stp->timing_fp))
260 break;
261
262 DBG(TIMING, ul_debug("reading next step"));
263
264 memset(step, 0, sizeof(*step));
265 stp->timing_line++;
266
267 switch (stp->timing_format) {
268 case REPLAY_TIMING_SIMPLE:
269 /* old format supports only output entries and format is the same
270 * as new format, but without <type> prefix */
271 rc = read_multistream_step(step, stp->timing_fp, 'O');
272 if (rc == 0)
273 step->type = 'O'; /* 'O'utput */
274 break;
275 case REPLAY_TIMING_MULTI:
276 rc = fscanf(stp->timing_fp, "%c ", &step->type);
277 if (rc != 1)
278 rc = -EINVAL;
279 else
280 rc = read_multistream_step(step,
281 stp->timing_fp,
282 step->type);
283 break;
284 }
285
286 if (rc)
287 break;; /* error */
288
289 DBG(TIMING, ul_debug(" step entry is '%c'", step->type));
290
291 log = replay_get_stream_log(stp, step->type);
292 if (log) {
293 if (is_wanted_stream(step->type, streams)) {
294 step->data = log;
295 *xstep = step;
296 DBG(LOG, ul_debug(" use %s as data source", log->filename));
297 goto done;
298 }
299 /* The step entry is unwanted, but we keep the right
300 * position in the log file although the data are ignored.
301 */
302 replay_seek_log(log, step->size);
303 } else
304 DBG(TIMING, ul_debug(" not found log for '%c' stream", step->type));
305
306 DBG(TIMING, ul_debug(" ignore step '%c' [delay=%f]",
307 step->type, step->delay));
308 ignored_delay += step->delay;
309 } while (rc == 0);
310
311 done:
312 if (ignored_delay)
313 step->delay += ignored_delay;
314
315 DBG(TIMING, ul_debug("reading next step done [rc=%d delay=%f (ignored=%f) size=%zu]",
316 rc, step->delay, ignored_delay, step->size));
317 return rc;
318 }
319
320 /* return: 0 = success, <0 = error, 1 = done (EOF) */
321 static int replay_emit_step_data(struct replay_step *step, int fd)
322 {
323 size_t ct;
324 int rc = 0;
325 char buf[BUFSIZ];
326
327 assert(step);
328 assert(step->size);
329 assert(step->data);
330 assert(step->data->fp);
331
332 for (ct = step->size; ct > 0; ) {
333 size_t len, cc;
334
335 cc = ct > sizeof(buf) ? sizeof(buf): ct;
336 len = fread(buf, 1, cc, step->data->fp);
337
338 if (!len) {
339 DBG(LOG, ul_debug("log data emit: failed to read log %m"));
340 break;
341 }
342
343 ct -= len;
344 cc = write(fd, buf, len);
345 if (cc != len) {
346 rc = -errno;
347 DBG(LOG, ul_debug("log data emit: failed write data %m"));
348 break;
349 }
350 }
351
352 if (ct && ferror(step->data->fp))
353 rc = -errno;
354 if (ct && feof(step->data->fp))
355 rc = 1;
356
357 DBG(LOG, ul_debug("log data emited [rc=%d size=%zu]", rc, step->size));
358 return rc;
359 }
360
361 static void __attribute__((__noreturn__))
362 usage(void)
363 {
364 FILE *out = stdout;
365 fputs(USAGE_HEADER, out);
366 fprintf(out,
367 _(" %s [-t] timingfile [typescript] [divisor]\n"),
368 program_invocation_short_name);
369
370 fputs(USAGE_SEPARATOR, out);
371 fputs(_("Play back terminal typescripts, using timing information.\n"), out);
372
373 fputs(USAGE_OPTIONS, out);
374 fputs(_(" -t, --timing <file> script timing log file\n"), out);
375 fputs(_(" -s, --typescript <file> script data log file\n"), out);
376 fputs(_(" -d, --divisor <num> speed up or slow down execution with time divisor\n"), out);
377 fputs(_(" -m, --maxdelay <num> wait at most this many seconds between updates\n"), out);
378 printf(USAGE_HELP_OPTIONS(25));
379
380 printf(USAGE_MAN_TAIL("scriptreplay(1)"));
381 exit(EXIT_SUCCESS);
382 }
383
384 static double
385 getnum(const char *s)
386 {
387 const double d = strtod_or_err(s, _("failed to parse number"));
388
389 if (isnan(d)) {
390 errno = EINVAL;
391 err(EXIT_FAILURE, "%s: %s", _("failed to parse number"), s);
392 }
393 return d;
394 }
395
396 static void
397 delay_for(double delay)
398 {
399 #ifdef HAVE_NANOSLEEP
400 struct timespec ts, remainder;
401 ts.tv_sec = (time_t) delay;
402 ts.tv_nsec = (delay - ts.tv_sec) * 1.0e9;
403
404 DBG(TIMING, ul_debug("going to sleep for %fs", delay));
405
406 while (-1 == nanosleep(&ts, &remainder)) {
407 if (EINTR == errno)
408 ts = remainder;
409 else
410 break;
411 }
412 #else
413 struct timeval tv;
414 tv.tv_sec = (long) delay;
415 tv.tv_usec = (delay - tv.tv_sec) * 1.0e6;
416 select(0, NULL, NULL, NULL, &tv);
417 #endif
418 }
419
420 int
421 main(int argc, char *argv[])
422 {
423 struct replay_setup setup = { .nlogs = 0 };
424 struct replay_step *step;
425 int rc;
426
427 const char *sname = NULL, *tname = NULL;
428 double divi = 1, maxdelay = 0;
429 int diviopt = FALSE, maxdelayopt = FALSE, idx;
430 int ch;
431
432 static const struct option longopts[] = {
433 { "timing", required_argument, 0, 't' },
434 { "typescript", required_argument, 0, 's' },
435 { "divisor", required_argument, 0, 'd' },
436 { "maxdelay", required_argument, 0, 'm' },
437 { "version", no_argument, 0, 'V' },
438 { "help", no_argument, 0, 'h' },
439 { NULL, 0, 0, 0 }
440 };
441
442 /* Because we use space as a separator, we can't afford to use any
443 * locale which tolerates a space in a number. In any case, script.c
444 * sets the LC_NUMERIC locale to C, anyway.
445 */
446 setlocale(LC_ALL, "");
447 setlocale(LC_NUMERIC, "C");
448
449 bindtextdomain(PACKAGE, LOCALEDIR);
450 textdomain(PACKAGE);
451 close_stdout_atexit();
452
453 scriptreplay_init_debug();
454
455 while ((ch = getopt_long(argc, argv, "t:s:d:m:Vh", longopts, NULL)) != -1)
456 switch(ch) {
457 case 't':
458 tname = optarg;
459 break;
460 case 's':
461 sname = optarg;
462 break;
463 case 'd':
464 diviopt = TRUE;
465 divi = getnum(optarg);
466 break;
467 case 'm':
468 maxdelayopt = TRUE;
469 maxdelay = getnum(optarg);
470 break;
471
472 case 'V':
473 print_version(EXIT_SUCCESS);
474 case 'h':
475 usage();
476 default:
477 errtryhelp(EXIT_FAILURE);
478 }
479 argc -= optind;
480 argv += optind;
481 idx = 0;
482
483 if ((argc < 1 && !tname) || argc > 3) {
484 warnx(_("wrong number of arguments"));
485 errtryhelp(EXIT_FAILURE);
486 }
487 if (!tname)
488 tname = argv[idx++];
489 if (!sname)
490 sname = idx < argc ? argv[idx++] : "typescript";
491 if (!diviopt)
492 divi = idx < argc ? getnum(argv[idx]) : 1;
493 if (maxdelay < 0)
494 maxdelay = 0;
495
496 if (replay_set_timing_file(&setup, tname) != 0)
497 err(EXIT_FAILURE, _("cannot open %s"), tname);
498
499 if (replay_associate_log(&setup, "O", sname) != 0)
500 err(EXIT_FAILURE, _("cannot open %s"), sname);
501
502 do {
503 rc = replay_get_next_step(&setup, "O", &step);
504 if (rc)
505 break;
506
507 step->delay /= divi;
508 if (maxdelayopt && step->delay > maxdelay)
509 step->delay = maxdelay;
510 if (step->delay > SCRIPT_MIN_DELAY)
511 delay_for(step->delay);
512
513 rc = replay_emit_step_data(step, STDOUT_FILENO);
514 } while (rc == 0);
515
516 if (step && rc < 0)
517 err(EXIT_FAILURE, _("%s: log file error"), step->data->filename);
518 else if (rc < 0)
519 err(EXIT_FAILURE, _("%s: line %d: timing file error"),
520 setup.timing_filename,
521 setup.timing_line);
522 printf("\n");
523 exit(EXIT_SUCCESS);
524 }