// 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.7.1";

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 int  opt_stealth_stdout  = 0;
static int  opt_stealth_stderr  = 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[] =
        {
            /*
             * "long-opt-name",   a, b, c
             * a: 1 if requires an arg, 0 otherwise
             * b: always 0
             * c: an optional char for the corresponding short-option, 0 otherwise
             */
            { "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' },
            { "stealth-stdout",   0, 0, 0   },
            { "stealth-stderr",   0, 0, 0   },
            { "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 if (strcmp(long_options[option_index].name, "stealth-stdout") == 0)
            {
                opt_stealth_stdout = 1;
            }
            else if (strcmp(long_options[option_index].name, "stealth-stderr") == 0)
            {
                opt_stealth_stderr = 1;
            }
            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;
        int    dont_write = 0;

        // 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
                    if (opt_stealth_stderr)
                    {
                        dont_write = 1;  // don't write buffer to ttyrec file, see below
                    }
                }
                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
                    if (opt_stealth_stdout)
                    {
                        dont_write = 1;  // don't write buffer to ttyrec file, see below
                    }
                }
                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;
                }
            }
            if (!dont_write)
            {
                (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"                 \
            "      --stealth-stdout      when no pseudotty is allocated, don't record stdout\n"                                                 \
            "      --stealth-stderr      when no pseudotty is allocated, don't record stderr\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"                                         \
            );
}
