Files
ovh-ttyrec-bash-recorder/ttyrec.c
Stéphane Lesimple a258f760fd release v1.1.6.7
2021-03-30 09:49:51 +02:00

2115 lines
68 KiB
C

// vim: noai:sts=4:ts=4:sw=4:et
/* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
* Copyright 2001-2019 The ovh-ttyrec Authors. All rights reserved.
*
* This work is based on the original ttyrec, whose license text
* can be found below unmodified.
*
* Copyright (c) 1980 Regents of the University of California.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/* 1999-02-22 Arkadiusz Miśkiewicz <misiek@misiek.eu.org>
* - added Native Language Support
*/
/* 2000-12-27 Satoru Takabayashi <satoru@namazu.org>
* - modify `script' to create `ttyrec'.
*/
/* 2001-2010 Several OVH contributors who will recognize themselves
* - lots of forgotten things
*/
/* 2010-2019 Stéphane Lesimple <stephane.lesimple@corp.ovh.com>
* - bugfixes (SIGWINCH handling and others)
* - BSD/MacOS compatibility
* - SIGUSR1 handling for ttyrec log rotation
* - input timeout locking mechanism
* - more things (see features in the README.md file)
*/
/*
* script
*/
#include <sys/types.h> // open, waitpid
#include <sys/stat.h> // open
#include <fcntl.h> // open, access
#include <termios.h> // tcsetattr
#include <sys/ioctl.h> // ioctl
#include <sys/time.h> // gettimeofday
#ifdef __HAIKU__
# include <posix/sys/wait.h> // waitpid
#else
# include <sys/wait.h> // waitpid
#endif
#include <libgen.h> // dirname
#include <stdio.h> // printf, ...
#include <unistd.h> // read, write, usleep, ...
#include <string.h> // strlen, memset, ...
#include <stdlib.h> // exit, free, getpt, ...
#include <errno.h> // errno
#include <pthread.h> // pthread_create
#include <signal.h> // sigaction
#include <sys/utsname.h> // uname
#include <time.h> // localtime
#include <getopt.h> // getopt_long
#include "configure.h"
#include "ttyrec.h"
#include "io.h"
#include "compress.h"
#ifdef HAVE_openpty
# if defined(HAVE_openpty_pty_h)
# include <pty.h>
# elif defined(HAVE_openpty_util_h)
# include <util.h>
# elif defined(HAVE_openpty_libutil_h)
# include <libutil.h>
# endif
#endif
// for ZSTD_versionNumber()
// and zstd_set_max_flush()
#ifdef HAVE_zstd
# include <zstd.h>
# include "compress_zstd.h"
#endif
#if defined(__linux__)
# define OS_STR "Linux"
#elif defined(__FreeBSD__)
# define OS_STR "FreeBSD"
#elif defined(__NetBSD__)
# define OS_STR "NetBSD"
#elif defined(__OpenBSD__)
# define OS_STR "OpenBSD"
#elif defined(__DragonFly__)
# define OS_STR "DragonFlyBSD"
#elif defined(__bsdi__)
# define OS_STR "BSD"
#elif defined(__SVR4) || defined(__svr4__) || defined(sun) || defined(__sun)
# define SUN_OS
# define OS_STR "SUN"
#elif defined(macintosh) || defined(Macintosh) || (defined(__APPLE__) && defined(__MACH__))
# define OS_STR "Darwin"
#elif defined(__HAIKU__)
# define OS_STR "Haiku"
#else
# define OS_STR "UnknownOS"
#endif
#ifdef HAVE_isastream
# include <stropts.h>
#endif
#ifdef SUN_OS
# include <sys/termios.h>
#endif
#ifdef SUN_OS
# define PID_T_FORMAT "%ld"
#else
# define PID_T_FORMAT "%d"
#endif
#define HAVE_inet_aton
#define HAVE_scsi_h
#define HAVE_kd_h
#if !defined(CDEL)
# if defined(_POSIX_VDISABLE)
# define CDEL _POSIX_VDISABLE
# elif defined(CDISABLE)
# define CDEL CDISABLE
# else /* not _POSIX_VISIBLE && not CDISABLE */
# define CDEL 255
# endif /* not _POSIX_VISIBLE && not CDISABLE */
#endif /* !CDEL */
#define printdbg(...) if (opt_debug > 0) { fprintf(stderr, __VA_ARGS__); }
#define printdbg2(...) if (opt_debug > 1) { fprintf(stderr, __VA_ARGS__); }
// functions used in the main() before the forks
void fixtty(void);
void help(void);
void set_ttyrec_file_name(char **nameptr);
void getmaster(void);
// functions used by the parent
void doinput(void);
void sigwinch_handler_parent(int signal);
void *timeout_watcher(void *arg);
void do_lock(void);
void handle_cheatcodes(char c);
// functions used by the child
void dooutput(void);
void sigwinch_handler_child(int signal);
// functions used by the subchild
void doshell(const char *, char **);
void getslave(void);
// sighandlers (parent and child)
void swing_output_file(int signal);
void unlock_session(int signal);
void lock_session(int signal);
void finish(int signal);
void sigterm_handler(int signal);
void sighup_handler(int signal);
// other functions used by parent and child
void done(int status);
void fail(void);
void print_termios_info(int fd, const char *prefix);
// ansi control codes
static const char *ansi_clear = "\033[2J";
static const char *ansi_home = "\033[H";
static const char *ansi_hidecursor = "\033[?25l";
static const char *ansi_showcursor = "\033[?25h";
static const char *ansi_save = "\033[?47h";
static const char *ansi_restore = "\033[?47l";
static const char *ansi_savecursor = "\0337";
static const char *ansi_restorecursor = "\0338";
static time_t last_activity = 0;
static time_t locked_since = 0;
static int lock_warned = 0;
static int kill_warned = 0;
static const char version[] = "1.1.6.7";
static FILE *fscript;
static int child;
static int subchild;
static char *me = NULL;
#ifdef HAVE_openpty
static int openpty_used = 0;
static int openpty_disable = 0;
#endif
// below: only used in notty mode
int stdout_pipe[2]; // subchild will write to it, child will read from it
int stderr_pipe[2]; // subchild will write to it, child will read from it
// below: only used in tty mode
static int master;
static int slave;
static char *fname = NULL;
static char *dname = NULL;
static char *uuid = NULL;
static char *namefmt = NULL;
static long timeout_lock = 0;
static long timeout_kill = 0;
static long warn_before_lock_seconds = 0;
static long warn_before_kill_seconds = 0;
static struct termios parent_stdin_termios;
static struct winsize parent_stdin_winsize;
static int parent_stdin_isatty = 0;
#if !defined(HAVE_openpty)
static char line[] = "/dev/ptyXX";
#endif
static long opt_compress_level = 0;
static int opt_zstd = 0;
static int opt_want_tty = 1; // never=0, auto=1, force=2
static int opt_append = 0;
static int opt_debug = 0;
static int opt_count_bytes = 0;
static int opt_cheatcodes = 0;
static char *opt_custom_message = NULL;
static int use_tty = 1; // no=0, yes=1
static int can_exit = 0;
static int childexit = 254;
int main(int argc, char **argv)
{
char *command = NULL;
char **params = NULL;
char *shell = NULL;
int legacy = 0;
int ch;
shell = getenv("SHELL");
if (shell == NULL)
{
shell = "/bin/sh";
}
while (1)
{
static struct option long_options[] =
{
{ "zstd", 0, 0, 0 },
{ "level", 1, 0, 'l' },
{ "verbose", 0, 0, 'v' },
{ "append", 0, 0, 'a' },
{ "cheatcodes", 0, 0, 'c' },
{ "no-cheatcodes", 0, 0, 'C' },
{ "shell-cmd", 1, 0, 'e' },
{ "dir", 1, 0, 'd' },
{ "output", 1, 0, 'f' },
{ "uuid", 1, 0, 'z' },
{ "no-openpty", 0, 0, 'p' },
{ "lock-timeout", 1, 0, 'l' },
{ "kill-timeout", 1, 0, 'k' },
{ "msg", 1, 0, 's' },
{ "count-bytes", 0, 0, 'n' },
{ "term", 1, 0, 'T' },
{ "version", 0, 0, 'V' },
{ "help", 0, 0, 'h' },
{ "max-flush-time", 1, 0, 0 },
{ "name-format", 1, 0, 'F' },
{ "warn-before-lock", 1, 0, 0 },
{ "warn-before-kill", 1, 0, 0 },
{ "help", 0, 0, 'h' },
{ "usage", 0, 0, 'h' },
{ 0, 0, 0, 0 }
};
int option_index = 0;
ch = getopt_long(argc, argv, "ZcCupVhvanf:z:d:t:T:k:s:e:l:F:", long_options, &option_index);
if (ch == -1)
{
break;
}
switch ((char)ch)
{
// long option without short-option counterpart
case 0:
if (strcmp(long_options[option_index].name, "zstd") == 0)
{
if (set_compress_mode(COMPRESS_ZSTD) != 0)
{
fprintf(stderr, "zstd support has not been enabled at compile time.\r\n");
fail();
}
opt_zstd++;
}
else if (strcmp(long_options[option_index].name, "max-flush-time") == 0)
{
#ifdef HAVE_zstd
errno = 0;
long max_flush_seconds = strtol(optarg, NULL, 10);
if ((errno != 0) || (max_flush_seconds <= 0))
{
help();
fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg);
exit(EXIT_FAILURE);
}
zstd_set_max_flush(max_flush_seconds);
#endif
}
else if (strcmp(long_options[option_index].name, "warn-before-lock") == 0)
{
errno = 0;
warn_before_lock_seconds = strtol(optarg, NULL, 10);
if ((errno != 0) || (warn_before_lock_seconds <= 0))
{
help();
fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg);
exit(EXIT_FAILURE);
}
}
else if (strcmp(long_options[option_index].name, "warn-before-kill") == 0)
{
errno = 0;
warn_before_kill_seconds = strtol(optarg, NULL, 10);
if ((errno != 0) || (warn_before_kill_seconds <= 0))
{
help();
fprintf(stderr, "Invalid value passed to --%s (%s), expected a strictly positive integer\r\n", long_options[option_index].name, optarg);
exit(EXIT_FAILURE);
}
}
else
{
fprintf(stderr, "Unknown long option %s\r\n", long_options[option_index].name);
fail();
}
break;
// on-the-fly zstd compression
case 'Z':
if (set_compress_mode(COMPRESS_ZSTD) == 0)
{
opt_zstd++;
}
break;
// compression level of compression algorithm
case 'l':
errno = 0;
opt_compress_level = strtol(optarg, NULL, 10);
if ((errno != 0) || (opt_compress_level <= 0))
{
help();
fprintf(stderr, "Invalid value passed to -%c (%s), expected a strictly positive integer\r\n", (char)ch, optarg);
exit(EXIT_FAILURE);
}
printdbg("level %c=%ld\r\n", ch, opt_compress_level);
set_compress_level(opt_compress_level);
break;
// debug ttyrec
case 'v':
opt_debug++;
break;
// open ttyrec file in append mode instead of write mode
case 'a':
opt_append++;
break;
// inhibit cheatcodes (force lock, force kill)
case 'C':
opt_cheatcodes = 0;
break;
// enable cheatcodes (force lock, force kill)
case 'c':
opt_cheatcodes = 1;
break;
// ignored (for compatibility with ttyrec classic)
case 'u':
break;
// ttyrec classic way of specifying the command to launch, it uses sh -c
case 'e':
if (legacy == 1)
{
help();
fprintf(stderr, "Option -e specified more than once.\r\n");
exit(EXIT_FAILURE);
}
legacy = 1;
params = malloc(sizeof(char *) * 4);
command = shell;
params[0] = strrchr(shell, '/') + 1;
params[1] = "-c";
params[2] = strdup(optarg);
params[3] = NULL;
break;
// directory to write ttyrec files to (autogenerated)
case 'd':
dname = strdup(optarg);
break;
// fullpath of ttyrec file to write to (optional, autogenerated if missing)
case 'f':
fname = strdup(optarg);
break;
// uuid, will appear in my ttyrec output file names, to keep track even after rotation (if omitted, will default to my pid)
case 'z':
uuid = strdup(optarg);
break;
// custom format
case 'F':
namefmt = strdup(optarg);
break;
// openpty_disable, don't prefer openpty() on systems that support it
case 'p':
#ifdef HAVE_openpty
openpty_disable++;
#else
fprintf(stderr, "Ignored option 'p': openpty() not supported on this system.\r\n");
#endif
break;
// timeout before lock (t) or kill (k)
case 't':
case 'k':
errno = 0;
long timeout = strtol(optarg, NULL, 10);
if ((errno != 0) || (timeout <= 0))
{
help();
fprintf(stderr, "Invalid value passed to -%c (%s), expected a strictly positive integer\r\n", (char)ch, optarg);
exit(EXIT_FAILURE);
}
printdbg("timeout %c=%ld\r\n", ch, timeout_lock);
if ((char)ch == 't')
{
timeout_lock = timeout;
}
else if ((char)ch == 'k')
{
timeout_kill = timeout;
}
break;
case 's':
opt_custom_message = strdup(optarg);
break;
// if specified, will count number of bytes out and print it on termination (experimental)
case 'n':
opt_count_bytes++;
break;
case 'T':
if (strncmp(optarg, "never", strlen("never")) == 0)
{
opt_want_tty = 0;
}
else if (strncmp(optarg, "auto", strlen("auto")) == 0)
{
opt_want_tty = 1;
}
else if (strncmp(optarg, "always", strlen("always")) == 0)
{
opt_want_tty = 2;
}
else
{
help();
fprintf(stderr, "Invalid value passed to -T (%s), expected either 'never', 'auto' or 'always'\r\n", optarg);
exit(EXIT_FAILURE);
}
break;
// version
case 'V':
printf("ttyrec v%s (%s)\n", version, MACHINE_STR);
#ifdef DEFINES_STR
printf("%s (%s)\n", DEFINES_STR, OS_STR);
#endif
#ifdef __VERSION__
printf("compiler version %s (%s)\n", __VERSION__, COMPILER_NAME);
#endif
#ifdef HAVE_zstd
printf("libzstd version %u (%d.%d.%d)\n", ZSTD_versionNumber(), ZSTD_VERSION_MAJOR, ZSTD_VERSION_MINOR, ZSTD_VERSION_RELEASE);
#endif
exit(0);
// 'h', and any other unknown option
case 'h':
default:
help();
exit(EXIT_FAILURE);
}
}
argc -= optind;
argv += optind;
printdbg("remaining non-parsed options argc=%d\r\n", argc);
for (int i = 0; i < argc; i++)
{
printdbg("option %d: <%s>\r\n", i, argv[i]);
}
if ((namefmt != NULL) && ((dname != NULL) || (uuid != NULL)))
{
fprintf(stderr, "Option -F (--name-format) can't be used with -d (--dir) or -z (--uuid)\n");
fail();
}
if (uuid == NULL)
{
uuid = malloc(sizeof(char) * BUFSIZ);
snprintf(uuid, BUFSIZ, PID_T_FORMAT, getpid());
}
if ((timeout_lock > 0) && (timeout_kill > 0) && (timeout_kill < timeout_lock))
{
help();
fprintf(stderr, "specified timeout_lock (%ld) is higher than timeout_kill (%ld), this doesn't make sense\r\n", timeout_lock, timeout_kill);
exit(EXIT_FAILURE);
}
if ((warn_before_lock_seconds > 0) && (timeout_lock == 0))
{
help();
fprintf(stderr, "You specified --warn-before-lock without enabling --timeout-lock, this doesn't make sense\r\n");
exit(EXIT_FAILURE);
}
if (warn_before_lock_seconds > timeout_lock)
{
help();
fprintf(stderr, "The specified value for --warn-before-lock is higher than --timeout-lock, this doesn't make sense\r\n");
exit(EXIT_FAILURE);
}
if ((warn_before_kill_seconds > 0) && (timeout_kill == 0))
{
help();
fprintf(stderr, "You specified --warn-before-kill without enabling --timeout-kill, this doesn't make sense\r\n");
exit(EXIT_FAILURE);
}
if (warn_before_kill_seconds > timeout_kill)
{
help();
fprintf(stderr, "The specified value for --warn-before-kill is higher than --timeout-kill, this doesn't make sense\r\n");
exit(EXIT_FAILURE);
}
if (legacy)
{
// strdup: make it free()able
fname = (argv[0] == NULL ? strdup("ttyrecord") : strdup(argv[0]));
}
else
{
if (argv[0] == NULL)
{
command = shell;
params = malloc(sizeof(char *) * 3);
params[0] = strrchr(shell, '/') + 1;
params[1] = "-i";
params[2] = NULL;
}
else
{
command = argv[0];
params = argv;
}
}
printdbg("will execvp %s with the following params:\r\n", command);
if (params == NULL)
{
printdbg("(none)\r\n");
}
else
{
for (int index = 0; params[index] != NULL; index++)
{
printdbg("- '%s'\r\n", params[index]);
}
}
// if neither dname nor fname are given, set dname to current dir
if ((dname == NULL) && (fname == NULL))
{
dname = strdup(".");
}
// if no file name given, generate it (dname is used as directory)
if (fname == NULL)
{
set_ttyrec_file_name(&fname);
}
else
{
// otherwise, append .zst if applicable
if (opt_zstd)
{
fname = realloc(fname, strlen(fname) + 4 + 1);
if (fname == NULL)
{
perror("realloc");
exit(EXIT_FAILURE);
}
strcat(fname, ".zst");
}
}
// if dname == ".", it might be because we've set it
if (dname == NULL)
{
char *tmpfname = strdup(fname);
// strdup(dirname) because in done() we free() dname
dname = strdup(dirname(tmpfname));
free(tmpfname);
}
if (dname == NULL)
{
fprintf(stderr, "failed to find a proper dname from fname=<%s>\r\n", fname);
exit(EXIT_FAILURE);
}
printdbg("will use %s as dname\r\n", dname);
if ((fscript = fopen(fname, opt_append ? "a" : "w")) == NULL)
{
perror(fname);
exit(EXIT_FAILURE);
}
free(fname);
setbuf(fscript, NULL);
{
struct sigaction act;
memset(&act, '\0', sizeof(act));
act.sa_handler = &finish;
act.sa_flags = SA_NOCLDSTOP | SA_RESTART;
if (sigaction(SIGCHLD, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
memset(&act, '\0', sizeof(act));
act.sa_handler = &swing_output_file;
act.sa_flags = SA_RESTART;
if (sigaction(SIGUSR1, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
memset(&act, '\0', sizeof(act));
act.sa_handler = &unlock_session;
act.sa_flags = SA_RESTART;
if (sigaction(SIGUSR2, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
memset(&act, '\0', sizeof(act));
act.sa_handler = &lock_session;
act.sa_flags = SA_RESTART;
if (sigaction(SIGURG, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
memset(&act, '\0', sizeof(act));
act.sa_handler = &sigterm_handler;
act.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
// we can get SIGHUP if our tty is closed, fclose() properly in that case
memset(&act, '\0', sizeof(act));
act.sa_handler = &sighup_handler;
act.sa_flags = SA_RESTART;
if (sigaction(SIGHUP, &act, NULL))
{
perror("sigaction");
exit(EXIT_FAILURE);
}
}
parent_stdin_isatty = isatty(0);
switch (opt_want_tty)
{
case 2:
use_tty = 1;
break;
case 0:
use_tty = 0;
break;
default:
use_tty = parent_stdin_isatty;
}
printdbg("parent: isatty(stdin) == %d, opt_want_tty == %d, use_tty == %d\r\n", parent_stdin_isatty, opt_want_tty, use_tty);
if (use_tty)
{
getmaster();
print_termios_info(master, "parent master");
print_termios_info(0, "parent stdin b4 fixtty");
if (parent_stdin_isatty)
{
fixtty();
}
print_termios_info(0, "parent stdin after fixtty");
}
else
{
// pipe[0] is read, pipe[1] is write
if (pipe(stdout_pipe))
{
perror("pipe");
exit(EXIT_FAILURE);
}
if (pipe(stderr_pipe))
{
perror("pipe");
exit(EXIT_FAILURE);
}
}
child = fork();
if (child < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if (child == 0)
{
// we are the child
printdbg("child pid is %ld\r\n", (long int)getpid());
print_termios_info(0, "child stdin");
subchild = child = fork();
if (child < 0)
{
perror("fork");
fail();
}
else if (child)
{
// we are still the child (parent of subchild)
me = "child";
dooutput();
}
else
{
// we are the subchild
printdbg("subchild pid is %ld\r\n", (long int)getpid());
print_termios_info(0, "subchild stdin");
me = "subchild";
doshell(command, params);
}
}
else
{
// we are the parent
me = "parent";
printdbg("parent pid is %ld\r\n", (long int)getpid());
sigwinch_handler_parent(SIGWINCH);
if (timeout_lock || timeout_kill)
{
pthread_t watcher_thread;
if (pthread_create(&watcher_thread, NULL, timeout_watcher, NULL) == -1)
{
perror("pthread");
fail();
}
}
doinput();
}
return 0;
}
void handle_cheatcodes(char c)
{
static int lockseq = 0;
static int killseq = 0;
if (opt_cheatcodes != 1)
{
return;
}
// LOCK
if (c == '\x0c') // ^L
{
if (++lockseq >= 8)
{
lockseq = 0;
do_lock();
}
}
else
{
lockseq = 0;
}
// KILL
if (((c == '\x0b') && ((killseq == 0) || (killseq == 4))) || // ^K
((c == '\x09') && ((killseq == 1) || (killseq == 5))) || // ^I
((c == '\x0c') && ((killseq == 2) || (killseq == 3) || (killseq == 6) || (killseq == 7)))) // ^L
{
killseq++;
}
else
{
killseq = 0;
}
if (killseq >= 8)
{
kill(child, SIGTERM);
}
}
// called by parent
void doinput(void)
{
int cc;
char ibuf[BUFSIZ];
(void)fclose_wrapper(fscript);
#ifdef HAVE_openpty
if (openpty_used)
{
// openpty opens the master and the slave in a single call to getmaster(),
// but in that case we don't want the slave (we won't call getslave())
(void)close(slave);
}
#endif
struct sigaction act;
memset(&act, '\0', sizeof(act));
act.sa_handler = &sigwinch_handler_parent;
act.sa_flags = SA_RESTART;
if (sigaction(SIGWINCH, &act, NULL))
{
perror("sigaction");
fail();
}
last_activity = time(NULL);
lock_warned = 0;
kill_warned = 0;
if (use_tty)
{
print_termios_info(master, "parent master in doinput");
#ifdef __HAIKU__
// under Haiku, if we use BUFSIZ as read size, it reads 4 bytes per 4 bytes
// instead of returning read data as soon as possible
const size_t readsz = 1;
#else
const size_t readsz = BUFSIZ;
#endif
while ((cc = read(0, ibuf, readsz)) > 0)
{
printdbg2("[in:%d]", cc);
if (!locked_since)
{
if (write(master, ibuf, cc) == -1)
{
perror("write[parent-master]");
fail();
}
last_activity = time(NULL);
lock_warned = 0;
kill_warned = 0;
if (cc == 1)
{
handle_cheatcodes(ibuf[0]);
}
}
}
if (opt_debug && (cc == -1))
{
perror("read");
}
}
else
{
// we won't use a pseudotty, just pipes, but as we're the parent so we don't need those
// also our STDIN is passed thru the subchild, so we don't need to handle it ourselves, we'll just wait for our child exit
close(0);
close(stdout_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[0]);
close(stderr_pipe[1]);
}
printdbg("%s("PID_T_FORMAT "): end doinput, waiting for child\r\n", me, getpid());
waitpid(-1, &childexit, 0);
printdbg("%s("PID_T_FORMAT "): end doinput, child exited with status=%d, exiting too\r\n", me, getpid(), childexit);
done(childexit);
}
// handler of SIGCHLD
void finish(int signal)
{
int waitedpid;
int die = 0;
(void)signal;
printdbg("%s("PID_T_FORMAT "): got SIGCHLD, calling waitpid\r\n", me, getpid());
while ((waitedpid = waitpid(-1, &childexit, WNOHANG)) > 0)
{
if (waitedpid == subchild)
{
printdbg("%s("PID_T_FORMAT "): subchild exited with %d, setting can_exit to 1\r\n", me, getpid(), childexit);
can_exit = 1;
}
else if (waitedpid == child)
{
printdbg("%s("PID_T_FORMAT "): child exited with %d, exiting too\r\n", me, getpid(), childexit);
die = 1;
}
}
if (die)
{
done(childexit);
}
}
void set_ttyrec_file_name(char **nameptr)
{
struct timeval tv;
struct tm *t = NULL;
if (gettimeofday(&tv, NULL))
{
perror("gettimeofday()");
fail();
}
t = localtime((const time_t *)&tv.tv_sec);
if (t == NULL)
{
perror("localtime()");
fail();
}
*nameptr = malloc(sizeof(char) * BUFSIZ);
if (*nameptr == NULL)
{
perror("malloc()");
fail();
}
if (namefmt == NULL)
{
// - 4: length of potential ".zst" we might add below
if (snprintf(*nameptr, BUFSIZ - 4, "%s/%04u-%02u-%02u.%02u-%02u-%02u.%06lu.%s.ttyrec", dname, t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec, (long unsigned int)tv.tv_usec, uuid) == -1)
{
perror("snprintf()");
free(*nameptr);
fail();
}
}
else
{
// - 4: length of potential ".zst" we might add below
if (strftime(*nameptr, BUFSIZ - 4, namefmt, t) == 0)
{
perror("strftime()");
free(*nameptr);
fail();
}
(*nameptr)[BUFSIZ - 5] = '\0';
char usec[7];
if (snprintf(usec, 7, "%06lu", (unsigned long)tv.tv_usec) < 0)
{
perror("snprintf()");
free(*nameptr);
fail();
}
char *ptr = strstr(*nameptr, "#usec#");
while (ptr != NULL)
{
memcpy(ptr, usec, 6);
ptr = strstr(ptr + 6, "#usec#");
}
}
if (opt_zstd)
{
// we can strcat safely because we used BUFSIZ - 4 above
strcat(*nameptr, ".zst");
}
}
void swing_output_file(int signal)
{
char *newname = NULL;
(void)signal;
if (subchild != 0)
{
set_ttyrec_file_name(&newname);
fclose_wrapper(fscript);
if ((fscript = fopen(newname, "w")) == NULL)
{
perror("fopen()");
free(newname);
fail();
}
free(newname);
setbuf(fscript, NULL);
}
}
// SIGUSR2
void unlock_session(int signal)
{
last_activity = time(NULL);
lock_warned = 0;
kill_warned = 0;
// to avoid signal storm, abort if not locked
if (!locked_since)
{
return;
}
printdbg("%s("PID_T_FORMAT "): unlock_session()\r\n", me, getpid());
locked_since = 0;
// in case only the parent or the child got the SIG,
// ensure the other also gets it
kill(subchild > 0 ? getppid() : child, signal);
if (subchild == 0)
{
// if we're the parent, force our children to redraw after unlock
usleep(1000 * 300);
struct winsize tmpwin;
(void)ioctl(master, TIOCGWINSZ, (char *)&tmpwin);
int pixels_per_row = tmpwin.ws_ypixel / tmpwin.ws_row;
tmpwin.ws_row++;
tmpwin.ws_ypixel += pixels_per_row;
(void)ioctl(master, TIOCSWINSZ, (char *)&tmpwin);
kill(child, SIGWINCH);
usleep(1000 * 300);
tmpwin.ws_row--;
tmpwin.ws_ypixel -= pixels_per_row;
(void)ioctl(master, TIOCSWINSZ, (char *)&tmpwin);
kill(child, SIGWINCH);
}
else
{
// child: restore console, make cursor visible again, restore its position
(void)fputs(ansi_restore, stdout);
(void)fputs(ansi_restorecursor, stdout);
(void)fputs(ansi_showcursor, stdout);
}
}
// SIGURG
void lock_session(int signal)
{
(void)signal;
// to avoid signal storm, abort if locked
if (locked_since)
{
return;
}
printdbg("%s("PID_T_FORMAT "): lock_session()\r\n", me, getpid());
locked_since = time(NULL);
// in case only the parent or the child got the SIG,
// ensure the other also gets it
kill(subchild > 0 ? getppid() : child, signal);
// if we're the parent, nothing more to do
if (subchild == 0)
{
return;
}
const char *lock = "\033[31m" \
"██╗ ██████╗ ██████╗██╗ ██╗███████╗██████╗ \r\n"
"██║ ██╔═══██╗██╔════╝██║ ██╔╝██╔════╝██╔══██╗\r\n"
"██║ ██║ ██║██║ █████╔╝ █████╗ ██║ ██║\r\n"
"██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ██║\r\n"
"███████╗╚██████╔╝╚██████╗██║ ██╗███████╗██████╔╝\r\n"
"╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ \033[0m\r\n" \
" .--------. \r\n" \
" / .------. \\ \r\n" \
" / / \\ \\ \033[33mThe current session is \033[31mLOCKED\033[0m\r\n" \
" | | | | \r\n" \
" _| |________| |_ \033[33mSince %s\033[0m\r\n" \
".' |_| |_| '. \r\n" \
"'._____ ____ _____.' \033[33mDue to input inactivity timeout\033[0m\r\n" \
"| .'____'. | \r\n" \
"'.__.'.' '.'.__.' \r\n" \
"'.__ | | __.' \r\n" \
"| '.'.____.'.' | \033[33m%s,\033[0m\r\n" \
"'.____'.____.'____.' \r\n" \
"'.________________.' \033[33m%s.\033[0m\r\n";
char hostname[BUFSIZ];
struct utsname name;
if (uname(&name))
{
hostname[0] = '\0';
}
else
{
strncpy(hostname, name.nodename, BUFSIZ);
hostname[BUFSIZ - 1] = '\0';
}
const char *salute[] =
{
"Kind regards", "Your dearest friend",
"Love", "xoxoxo", "XoXoXo",
"Respectfully yours", "Best",
"Sincerely yours", "Take care",
"Cheers", "With deepest sympathy",
};
#define salute_len (sizeof(salute) / sizeof(const char *))
// save cursor pos, save buffer, clear screen, put cursor at home position, hide cursor
(void)fputs(ansi_savecursor, stdout);
(void)fputs(ansi_save, stdout);
(void)fputs(ansi_clear, stdout);
(void)fputs(ansi_home, stdout);
(void)fputs(ansi_hidecursor, stdout);
time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
char ctime_now[BUFSIZ];
strftime(ctime_now, BUFSIZ, "%A %Y-%m-%d %H:%M:%S", tm_now);
srand(now);
(void)printf(lock, ctime_now, salute[rand() % salute_len], hostname);
if (opt_custom_message != NULL)
{
(void)fputs("\r\n", stdout);
(void)puts(opt_custom_message);
}
}
void do_lock(void)
{
locked_since = time(NULL);
kill(child, SIGURG);
}
void *timeout_watcher(void *arg)
{
(void)arg;
for ( ; ;)
{
sleep(1);
time_t now = time(NULL);
if (use_tty && !locked_since)
{
// handle warn: if input is idle and we didn't already, warn
if ((warn_before_lock_seconds > 0) && (lock_warned == 0) && (now - last_activity + warn_before_lock_seconds > timeout_lock))
{
lock_warned = 1;
fprintf(stderr, "warning: your session will be locked in %lu seconds if no input activity is detected.", warn_before_lock_seconds);
}
// handle lock: if input is idle, and warn wasn't enough, lock
if ((timeout_lock > 0) && (now - last_activity > timeout_lock))
{
printdbg("parent: timeout_watcher: do_lock()\r\n");
do_lock();
}
}
// handle kill
if (timeout_kill > 0)
{
// if we're locked, check against the locked_since (never happens if !use_tty)
if (locked_since)
{
if ((warn_before_kill_seconds > 0) && (kill_warned == 0) && (now - locked_since > timeout_kill - timeout_lock - warn_before_kill_seconds))
{
kill_warned = 1;
fprintf(stderr, "warning: your session will be killed in %lu seconds if no input activity is detected.", warn_before_kill_seconds);
}
else if (now - locked_since > timeout_kill - timeout_lock)
{
printdbg("parent: timeout_watcher: kill (locked)\r\n");
kill(child, SIGTERM);
}
}
// handle kill cont'd: if we're not locked, check against the last_activity
else
{
if ((warn_before_kill_seconds > 0) && (kill_warned == 0) && (now - last_activity > timeout_kill - warn_before_kill_seconds))
{
kill_warned = 1;
fprintf(stderr, "warning: your session will be killed in %lu seconds if no input activity is detected.", warn_before_kill_seconds);
}
else if (now - last_activity > timeout_kill)
{
printdbg("parent: timeout_watcher: kill (unlocked), now=%d last_activity=%d timeout_kill=%ld\r\n", (int)now, (int)last_activity, timeout_kill);
kill(child, SIGTERM);
}
}
}
}
}
// called by child
void dooutput(void)
{
int cc;
char obuf[BUFSIZ];
int waitedpid;
unsigned long long bytes_out = 0;
int stdout_pipe_opened = 1;
int stderr_pipe_opened = 1;
int target_fd = 1; // stdout by default
setbuf(stdout, NULL);
(void)close(0); // the subchild will consume it, not us
#ifdef HAVE_openpty
if (openpty_used)
{
// openpty opens the master and the slave in a single call to getmaster(),
// but in that case we don't want the slave (we won't call getslave())
(void)close(slave);
}
#endif
struct sigaction act;
memset(&act, '\0', sizeof(act));
act.sa_handler = &sigwinch_handler_child;
act.sa_flags = SA_RESTART;
if (sigaction(SIGWINCH, &act, NULL))
{
perror("sigaction");
fail();
}
if (!use_tty)
{
close(stdout_pipe[1]);
close(stderr_pipe[1]);
}
else
{
// ensure that our user's terminal is not in altscreen on launch,
// or (sh)he might get surprised when (s)he launches then exits programs such as vim
(void)fputs(ansi_restore, stdout);
}
for ( ; ;)
{
Header h;
// we have a tty
if (use_tty)
{
cc = read(master, obuf, BUFSIZ);
if (cc == 0)
{
printdbg("\r\n%s(" PID_T_FORMAT "): got EOF, there's nothing left to read from\r\n", me, getpid());
break;
}
if (cc < 0)
{
printdbg2("[out:%d,%s]", cc, strerror(errno));
if (errno != EINTR)
{
printdbg("\r\n%s(" PID_T_FORMAT "): got fatal error when reading, there's nothing left to read from\r\n", me, getpid());
break;
}
}
}
// we don't have a tty and use pipes
else
{
fd_set rfds;
int nfds = 0;
FD_ZERO(&rfds);
if (stdout_pipe_opened)
{
FD_SET(stdout_pipe[0], &rfds);
if (stdout_pipe[0] > nfds)
{
nfds = stdout_pipe[0];
}
}
if (stderr_pipe_opened)
{
FD_SET(stderr_pipe[0], &rfds);
if (stderr_pipe[0] > nfds)
{
nfds = stderr_pipe[0];
}
}
printdbg2("[select:%d:%d]", stdout_pipe_opened, stderr_pipe_opened);
int retval = select(nfds + 1, &rfds, NULL, NULL, NULL);
int current_fd = -1;
cc = 0;
if (retval == -1) // select failed
{
if (errno != EINTR)
{
perror("select()");
}
continue;
}
else if (retval)
{
const char *current_fd_name;
int *pipe_flag;
if (FD_ISSET(stderr_pipe[0], &rfds))
{
current_fd = stderr_pipe[0];
current_fd_name = "stderr";
pipe_flag = &stderr_pipe_opened;
target_fd = 2; // stderr
}
else if (FD_ISSET(stdout_pipe[0], &rfds))
{
current_fd = stdout_pipe[0];
current_fd_name = "stdout";
pipe_flag = &stdout_pipe_opened;
target_fd = 1; // stdout
}
else
{
perror("select() returned invalid fd");
continue;
}
cc = read(current_fd, obuf, BUFSIZ);
// Handle EOF or error
if ((cc == 0) || ((cc < 0) && (errno != EINTR)))
{
if (cc == 0)
{
printdbg2("[%s:eof]", current_fd_name);
}
else
{
printdbg2("[%s:err=%d,%s]", current_fd_name, cc, strerror(errno));
}
*pipe_flag = 0;
close(current_fd);
if ((stderr_pipe_opened == 0) && (stdout_pipe_opened == 0))
{
printdbg2("[alleof]");
break;
}
}
}
}
// here, we have cc with the number of bytes read, from either the tty or the pipes
printdbg2("[out:%d]", cc);
if (!locked_since && (cc > 0))
{
h.len = cc;
gettimeofday(&h.tv, NULL);
if (write(target_fd, obuf, cc) == -1)
{
if (errno != EINTR)
{
printdbg("write(child-stdout,len=%d): %s", cc, strerror(errno));
if (stdout_pipe_opened)
{
close(stdout_pipe[0]);
}
if (stderr_pipe_opened)
{
close(stderr_pipe[0]);
}
break;
}
}
(void)write_header(fscript, &h);
(void)fwrite_wrapper(obuf, 1, cc, fscript);
bytes_out += cc;
last_activity = time(NULL);
lock_warned = 0;
kill_warned = 0;
}
}
printdbg("child: end dooutput, waiting can_exit (== %d)\r\n", can_exit);
while (can_exit == 0)
{
waitedpid = waitpid(-1, &childexit, 0);
if (waitedpid < 0) // oops, all our children are already dead (ECHILD)
{
printdbg("child: oops, subchild is already dead!\r\n");
can_exit = 1;
}
}
printdbg("child: end dooutput, can_exit done, status %d, exiting\r\n", childexit);
if (opt_count_bytes)
{
fprintf(stderr, "\r\nTTY_BYTES_OUT=%llu\r\n", bytes_out);
}
done(childexit);
}
// called by subchild
void doshell(const char *command, char **params)
{
(void)fclose_wrapper(fscript);
if (use_tty)
{
getslave();
print_termios_info(slave, "subchild slave");
(void)close(master);
(void)dup2(slave, 0);
(void)dup2(slave, 1);
(void)dup2(slave, 2);
(void)close(slave);
}
else
{
(void)dup2(stdout_pipe[1], 1);
(void)dup2(stderr_pipe[1], 2);
(void)close(stdout_pipe[1]);
(void)close(stderr_pipe[1]);
}
execvp(command, params);
perror(command);
fail();
}
void fixtty(void)
{
struct termios rtt;
rtt = parent_stdin_termios;
#ifdef HAVE_cfmakeraw
cfmakeraw(&rtt);
rtt.c_lflag &= ~ECHO;
#else
rtt.c_iflag = 0;
rtt.c_lflag &= ~(ISIG | ICANON | XCASE | ECHO | ECHOE | ECHOK | ECHONL);
rtt.c_oflag = OPOST;
rtt.c_cc[VINTR] = CDEL;
rtt.c_cc[VQUIT] = CDEL;
rtt.c_cc[VERASE] = CDEL;
rtt.c_cc[VKILL] = CDEL;
rtt.c_cc[VEOF] = 1;
rtt.c_cc[VEOL] = 0;
#endif
if (tcsetattr(0, TCSAFLUSH, &rtt))
{
perror("tcsetattr(0) in fixtty");
}
}
void fail(void)
{
fprintf(stderr, "\r\nttyrec: aborting!\r\n");
(void)kill(0, SIGTERM);
done(EXIT_FAILURE);
}
void sigterm_handler(int signal)
{
(void)signal;
if (subchild > 0)
{
// unlock_session() is also called in done(), but we need to do it first here
// or the below message won't be visible if we happen to be locked
unlock_session(SIGUSR2);
// terminate our subchild BEFORE printing the msg, to avoid an interlock case where
// our child is stuck writing to us, while we're writing to our stdout
// read by our caller, stuck writing to our child
kill(subchild, SIGTERM);
(void)puts("\r\nttyrec: ending your session, sorry (kill timeout expired, you manually typed the kill key sequence, or we got a SIGTERM).\r");
}
done(EXIT_SUCCESS);
}
void sighup_handler(int signal)
{
(void)signal;
if (subchild > 0)
{
kill(subchild, SIGTERM);
}
done(EXIT_SUCCESS);
}
void done(int status)
{
// Sometimes (happens once every ~1 million executions in some environments), we might get a SIGHUP
// while we're calling exit() from this function. As the SIGHUP handler ends up calling this function
// again, we end up doing double-frees and calling exit() twice, which gets us a segfault. Hereby, ensure
// that once we've entered this function once, we'll never re-enter it through a sighandler.
static pthread_mutex_t entered_done = PTHREAD_MUTEX_INITIALIZER;
if (pthread_mutex_trylock(&entered_done) != 0)
{
return;
}
if (subchild)
{
printdbg("child: done, cleaning up and exiting with %d (child=%d subchild=%d)\r\n", WEXITSTATUS(status), child, subchild);
// if we were locked, unlock before exiting to avoid leaving the real terminal of our user stuck in altscreen
unlock_session(SIGUSR2);
(void)fclose_wrapper(fscript);
(void)close(master);
}
else
{
printdbg("parent: done, cleaning up and exiting with %d (child=%d subchild=%d)\r\n", WEXITSTATUS(status), child, subchild);
if (use_tty && parent_stdin_isatty)
{
// don't check for result because if it fails, we can't do anything interesting,
// not even printing an error (and actually; perror() stucks sometimes if we use it here)
tcsetattr(0, TCSAFLUSH, &parent_stdin_termios);
}
}
free(dname);
free(uuid);
exit(WEXITSTATUS(status));
}
void getmaster(void)
{
if (parent_stdin_isatty)
{
if (tcgetattr(0, &parent_stdin_termios))
{
perror("tcgetattr(0, parent_stdin_termios)");
}
if (ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize))
{
perror("ioctl(0, TIOCGWINSZ)");
}
}
#ifdef HAVE_openpty
if (openpty_disable)
{
#endif
#ifdef HAVE_grantpt
// with getpt, posix_openpt and ptmx, we need grantpt in getslave()
# if defined(HAVE_getpt)
if ((master = getpt()) < 0)
{
perror("getpt()");
fail();
}
return;
# elif defined(HAVE_posix_openpt)
if ((master = posix_openpt(O_RDWR | O_NOCTTY)) < 0)
{
perror("posix_openpt()");
fail();
}
return;
# else
if (access("/dev/ptmx", F_OK) == 0)
{
if ((master = open("/dev/ptmx", O_RDWR | O_NOCTTY)) < 0)
{
perror("open(\"/dev/ptmx\", O_RDWR)");
fail();
}
return;
}
# endif
#endif /* !HAVE_grantpt */
#ifdef HAVE_openpty
}
#endif
//#if !defined(HAVE_grantpt) || ( !defined(HAVE_getpt) && !defined(HAVE_posix_openpt) )
# ifdef HAVE_openpty
// BUG HERE: the getpt(), posix_openpt() and /dev/ptmx ways work correctly under Linux,
// where openpty() doesn't in the case of: ssh -T user@bastion -- -t user@remote -- 'pwd' </dev/null
// when the remote machine is a Solaris, 'pwd' gives no output.
// For now, we just rely on openpty() as a last resort
if (openpty(&master, &slave, NULL, &parent_stdin_termios, &parent_stdin_winsize) < 0)
{
perror("openpty failed");
fail();
}
openpty_used = 1;
// fixup master pty if its lflag is 0, to make some OSes happy (OmniOS at least)
// this is mainly useful when -T always has been specified, and our stdin is not a tty,
// in that case openpty() gives us a very raw terminal, and it prevents some OSes to output
// anything... which would have to be expected when you know it, but in most cases
// it's not *actually* expected by people using -T always without a tty
struct termios mastert;
memset(&mastert, '\0', sizeof(mastert));
if ((tcgetattr(master, &mastert) == 0) && (mastert.c_lflag == 0))
{
printdbg("fixing master pty attrs\r\n");
mastert.c_iflag = IXON + ICRNL; // 02400
mastert.c_oflag = OPOST + ONLCR; // 05
mastert.c_cflag = 0277; //B38400 + CS8 + CREAD;
mastert.c_lflag = ISIG + ICANON + ECHO + ECHOE + ECHOK + IEXTEN;
#ifdef ECHOKE /* undef under NetBSD */
mastert.c_lflag += ECHOKE;
#endif
#ifdef ECHOCTL /* undef under NetBSD */
mastert.c_lflag += ECHOCTL;
#endif
// apply the c_cc config of a classic pseudotty given by posix_openpt()
mastert.c_cc[VINTR] = 3;
mastert.c_cc[VQUIT] = 28;
mastert.c_cc[VERASE] = 127;
mastert.c_cc[VKILL] = 21;
mastert.c_cc[VEOF] = 4;
mastert.c_cc[VTIME] = 0;
mastert.c_cc[VMIN] = 1;
#ifdef VSWTC /* not defined under at least OmniOS */
mastert.c_cc[VSWTC] = 0;
#endif
mastert.c_cc[VSTART] = 17;
mastert.c_cc[VSTOP] = 19;
mastert.c_cc[VSUSP] = 26;
mastert.c_cc[VEOL] = 0;
#ifdef VREPRINT /* not defined under at least Haiku */
mastert.c_cc[VREPRINT] = 18;
#endif
#ifdef VDISCARD /* not defined under at least Haiku */
mastert.c_cc[VDISCARD] = 15;
#endif
#ifdef VWERASE /* not defined under at least Haiku */
mastert.c_cc[VWERASE] = 23;
#endif
#ifdef VLNEXT /* not defined under at least Haiku */
mastert.c_cc[VLNEXT] = 22;
#endif
print_termios_info(master, "termios master before fix");
if (tcsetattr(master, TCSANOW, &mastert))
{
printdbg("tcsetattr returned %s\r\n", strerror(errno));
}
print_termios_info(master, "termios master after fix");
}
print_termios_info(master, "openpty master termios");
print_termios_info(slave, "openpty slave termios");
return;
# else
// else, try the /dev/ptyXX way
char *pty, *bank, *cp;
struct stat stb;
pty = &line[strlen("/dev/ptyp")];
unsigned int tries = 0;
for (bank = "pqrs"; *bank; bank++)
{
line[strlen("/dev/pty")] = *bank;
*pty = '0';
if (stat(line, &stb) < 0)
{
break;
}
tries++;
for (cp = "0123456789abcdef"; *cp; cp++)
{
*pty = *cp;
master = open(line, O_RDWR);
if (master >= 0)
{
char *tp = &line[strlen("/dev/")];
int ok;
/* verify slave side is usable */
*tp = 't';
ok = access(line, R_OK | W_OK) == 0;
*tp = 'p';
if (ok)
{
return;
}
(void)close(master);
}
}
}
if (tries == 0)
{
fprintf(stderr, "Found no way to allocate a pseudo-tty on your system!\r\n");
}
else
{
fprintf(stderr, "Out of pty's (tried %u pty's)\r\n", tries);
}
fail();
# endif
//#endif
}
void getslave(void)
{
#ifdef HAVE_openpty
printdbg("openpty_used == %d\r\n", openpty_used);
if (openpty_used)
{
if (setsid() < 0)
{
perror("setsid");
fail();
}
if (ioctl(slave, TIOCSCTTY, 0) < 0)
{
#ifndef SUN_OS
perror("ioctl");
fail();
#endif
}
return;
}
#endif
#if defined(HAVE_grantpt)
if (setsid() < 0)
{
perror("setsid");
fail();
}
if (grantpt(master) != 0)
{
perror("grantpt");
fail();
}
if (unlockpt(master) != 0)
{
perror("unlockpt");
fail();
}
const char *slavename = ptsname(master);
printdbg("ptsname(master) is %s\r\n", slavename);
if ((slave = open(slavename, O_RDWR)) < 0)
{
perror("slave = open(fd, O_RDWR)");
fail();
}
if (ioctl(slave, TIOCSCTTY, 0) < 0)
{
#ifndef SUN_OS
perror("ioctl");
fail();
#endif
}
# ifdef HAVE_isastream
if (isastream(slave))
{
if (ioctl(slave, I_PUSH, "ptem") < 0)
{
perror("ioctl(fd, I_PUSH, ptem)");
fail();
}
if (ioctl(slave, I_PUSH, "ldterm") < 0)
{
perror("ioctl(fd, I_PUSH, ldterm)");
fail();
}
# ifndef _HPUX_SOURCE
if (ioctl(slave, I_PUSH, "ttcompat") < 0)
{
perror("ioctl(fd, I_PUSH, ttcompat)");
fail();
}
# endif
if (parent_stdin_isatty)
{
if (ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize))
{
perror("ioctl(0, TIOCGWINSZ)");
}
}
}
# endif /* !HAVE_isastream */
#elif !defined(HAVE_openpty) /* !HAVE_grantpt */
line[strlen("/dev/")] = 't';
slave = open(line, O_RDWR);
if (slave < 0)
{
perror(line);
fail();
}
if (parent_stdin_isatty)
{
if (tcsetattr(slave, TCSAFLUSH, &parent_stdin_termios))
{
perror("tcsetattr(slave, TCSAFLUSH, parent_stdin_termios)");
}
if (ioctl(slave, TIOCSWINSZ, (char *)&parent_stdin_winsize))
{
perror("ioctl(slave, TIOCSWINSZ, parent_stdin_winsize)");
}
}
#endif /* HAVE_grantpt */
}
void sigwinch_handler_parent(int signal)
{
(void)signal;
if (parent_stdin_isatty)
{
(void)ioctl(0, TIOCGWINSZ, (char *)&parent_stdin_winsize);
(void)ioctl(master, TIOCSWINSZ, (char *)&parent_stdin_winsize);
}
kill(child, SIGWINCH);
}
void sigwinch_handler_child(int signal)
{
(void)signal;
kill(child, SIGWINCH);
}
void print_termios_info(int fd, const char *prefix)
{
struct termios t;
if (opt_debug)
{
memset(&t, '\0', sizeof(t));
if (tcgetattr(fd, &t))
{
fprintf(stderr, "%25s: %s\r\n", prefix, strerror(errno));
}
else
{
char dbgline[BUFSIZ];
dbgline[0] = '\0';
snprintf(dbgline, BUFSIZ, "%25s: i=%05lo o=%03lo c=%05lo l=%06lo, i: ", prefix, (unsigned long)t.c_iflag, (unsigned long)t.c_oflag, (unsigned long)t.c_cflag, (unsigned long)t.c_lflag);
#define IFLAG(f) \
if (t.c_iflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); \
}
#define OFLAG(f) if (t.c_oflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); }
#define CFLAG(f) if (t.c_cflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); }
#define LFLAG(f) if (t.c_lflag & f) { strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); }
#define CSWITCH(f) \
case f: \
strncat(dbgline + strlen(dbgline), # f " ", BUFSIZ - strlen(dbgline)); break;
IFLAG(IGNBRK);
IFLAG(BRKINT);
IFLAG(IGNPAR);
IFLAG(PARMRK);
IFLAG(INPCK);
IFLAG(ISTRIP);
IFLAG(INLCR);
IFLAG(IGNCR);
IFLAG(ICRNL);
#ifdef IUCLC /* undef under NetBSD */
IFLAG(IUCLC);
#endif
IFLAG(IXON);
#ifdef IXANY /* undef under NetBSD */
IFLAG(IXANY);
#endif
IFLAG(IXOFF);
#ifdef IMAXBEL /* undef at least under Haiku */
IFLAG(IMAXBEL);
#endif
#ifdef IUTF8
IFLAG(IUTF8);
#endif
strncat(dbgline + strlen(dbgline), "o: ", BUFSIZ - strlen(dbgline));
OFLAG(OPOST);
#ifdef OLCUC /* undef under NetBSD */
OFLAG(OLCUC);
#endif
OFLAG(ONLCR);
OFLAG(OCRNL);
OFLAG(ONLRET);
#ifdef OFILL /* undef under NetBSD */
OFLAG(OFILL);
#endif
#ifdef OFDEL /* undef under NetBSD */
OFLAG(OFDEL);
#endif
strncat(dbgline + strlen(dbgline), "c: ", BUFSIZ - strlen(dbgline));
switch (t.c_cflag & B38400)
{
CSWITCH(B38400);
CSWITCH(B19200);
CSWITCH(B9600);
CSWITCH(B4800);
CSWITCH(B2400);
CSWITCH(B1200);
CSWITCH(B600);
CSWITCH(B300);
CSWITCH(B150);
CSWITCH(B75);
CSWITCH(B50);
CSWITCH(B0);
}
switch (t.c_cflag & CSIZE)
{
CSWITCH(CS8);
CSWITCH(CS7);
#if (CS6 != CS5) && (CS6 != CS7) && (CS6 != CS8) /* Haiku defines CS4 and CS5 to 0x00 */
CSWITCH(CS6);
#endif
#if (CS5 != CS6) && (CS5 != CS7) && (CS5 != CS8)
CSWITCH(CS5);
#endif
}
CFLAG(CSTOPB);
CFLAG(CREAD);
CFLAG(PARENB);
CFLAG(PARODD);
CFLAG(CLOCAL);
strncat(dbgline + strlen(dbgline), "l: ", BUFSIZ - strlen(dbgline));
LFLAG(ISIG);
LFLAG(ICANON);
#ifdef XCASE /* undef under NetBSD */
LFLAG(XCASE);
#endif
LFLAG(ECHO);
LFLAG(ECHOE);
LFLAG(ECHOK);
LFLAG(ECHONL);
LFLAG(NOFLSH);
LFLAG(TOSTOP);
#ifdef ECHOCTL /* undef under NetBSD */
LFLAG(ECHOCTL);
#endif
#ifdef ECHOPRT /* undef under NetBSD */
LFLAG(ECHOPRT);
#endif
#ifdef ECHOKE /* undef under NetBSD */
LFLAG(ECHOKE);
#endif
#ifdef FLUSH0 /* undef under NetBSD */
LFLAG(FLUSHO);
#endif
#ifdef PENDIN /* undef under NetBSD */
LFLAG(PENDIN);
#endif
LFLAG(IEXTEN);
#undef IFLAG
#undef OFLAG
#undef CFLAG
#undef LFLAG
#undef CSWITCH
strncat(dbgline + strlen(dbgline), "cc: ", BUFSIZ - strlen(dbgline));
for (cc_t i = 0; i < NCCS; i++)
{
snprintf(dbgline + strlen(dbgline), BUFSIZ - strlen(dbgline), "%02x/", t.c_cc[i]);
}
fprintf(stderr, "%s\r\n", dbgline);
}
}
}
void help(void)
{
fprintf(stderr, \
"Usage: ttyrec [options] -- <command> [command options]\n" \
"\n" \
"Usage (legacy compatibility mode): ttyrec -e <command> [options] [ttyrec file name]\n" \
"\n" \
"Options:\n" \
" -z, --uuid UUID specify an UUID (can be any string) that will appear in the ttyrec output file names,\n" \
" and kept with SIGUSR1 rotations (default: own PID)\n" \
" -f, --output FILE full path of the first ttyrec file to write to (autogenerated if omitted)\n" \
" -d, --dir FOLDER folder where to write the ttyrec files (taken from -f if omitted,\n" \
" defaulting to working directory if both -f and -d are omitted)\n" \
" -F, --name-format FMT custom strftime-compatible format string to qualify the full path of the output files,\n" \
" including the SIGUSR1 rotated ones\n" \
" -a, --append open the ttyrec output file in append mode instead of write-clobber mode\n");
#ifdef HAVE_zstd
fprintf(stderr, \
" -Z enable on-the-fly compression if available, silently fallback to no compression if not\n" \
" --zstd force on-the-fly compression of output file using zstd,\n" \
" the resulting file will have a '.ttyrec.zst' extension\n" \
" --max-flush-time S specify the maximum number of seconds after which we'll force zstd to flush its output buffers\n" \
" to ensure that even somewhat quiet sessions gets regularly written out to disk, default is %d\n" \
" -l, --level LEVEL set compression level, must be between 1 and 19 for zstd, default is 3\n" \
, ZSTD_MAX_FLUSH_SECONDS_DEFAULT);
#endif
fprintf(stderr, \
" -n, --count-bytes count the number of bytes out and print it on termination (experimental)\n" \
" -t, --lock-timeout S lock session on input timeout after S seconds\n" \
" --warn-before-lock S warn S seconds before locking (see --lock-timeout)\n" \
" -k, --kill-timeout S kill session on input timeout after S seconds\n" \
" --warn-before-kill S warn S seconds before killing (see --kill-timeout)\n" \
" -C, --no-cheatcodes disable cheat-codes (see below), this is the default\n" \
" -c, --cheatcodes enable cheat-codes (see below)\n" \
" -p, --no-openpty don't use openpty() even when it's available\n" \
" -T, --term MODE MODE can be either 'never' (never allocate a pseudotty, even if stdin is a tty, and use pipes to\n" \
" handle stdout/stderr instead), 'always' (always allocate a pseudotty, even if stdin is not a tty)\n" \
" or 'auto' (default, allocate a pseudotty if stdin is a tty, uses pipes otherwise)\n" \
" -v, --verbose verbose (debug) mode, use twice for more verbosity\n" \
" -V, --version show version information\n" \
" -e, --shell-cmd CMD enables legacy compatibility mode and specifies the command to be run under the user's $SHELL -c\n" \
"\n" \
"Examples:\n" \
" Run some shell commands in legacy mode: ttyrec -e 'for i in a b c; do echo $i; done' outfile.ttyrec\n" \
" Run some shell commands in normal mode: ttyrec -f /tmp/normal.ttyrec -- sh -c 'for i in a b c; do echo $i; done'\n" \
" Connect to a remote machine interactively: ttyrec -t 60 -k 300 -- ssh remoteserver\n" \
" Execute a local script remotely with the default remote shell: ttyrec -- ssh remoteserver < script.sh\n" \
" Record a screen session: ttyrec screen\n" \
"\n" \
"Handled signals:\n" \
" SIGUSR1 close current ttyrec file and reopen a new one (log rotation)\n" \
" SIGURG lock session\n" \
" SIGUSR2 unlock session\n" \
"\n" \
"Cheat-codes (magic keystrokes combinations):\n" \
" ^L^L^L^L^L^L^L^L lock your session (that's 8 CTRL+L's)\n" \
" ^K^I^L^L^K^I^L^L kill your session\n" \
"\n" \
"Remark about session lock and session kill:\n" \
" If we don't have a tty, we can't lock, so -t will be ignored,\n" \
" whereas -k will be applied without warning, as there's no tty to output a warning to.\n" \
);
}