/* $OpenBSD$ */

/*
* Copyright (c) 2007 Nicholas Marriott <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/file.h>

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "tmux.h"

static struct tmuxproc  *client_proc;
static struct tmuxpeer  *client_peer;
static uint64_t          client_flags;
static int               client_suspended;
static enum {
       CLIENT_EXIT_NONE,
       CLIENT_EXIT_DETACHED,
       CLIENT_EXIT_DETACHED_HUP,
       CLIENT_EXIT_LOST_TTY,
       CLIENT_EXIT_TERMINATED,
       CLIENT_EXIT_LOST_SERVER,
       CLIENT_EXIT_EXITED,
       CLIENT_EXIT_SERVER_EXITED,
       CLIENT_EXIT_MESSAGE_PROVIDED
} client_exitreason = CLIENT_EXIT_NONE;
static int               client_exitflag;
static int               client_exitval;
static enum msgtype      client_exittype;
static const char       *client_exitsession;
static char             *client_exitmessage;
static const char       *client_execshell;
static const char       *client_execcmd;
static int               client_attached;
static struct client_files client_files = RB_INITIALIZER(&client_files);

static __dead void       client_exec(const char *,const char *);
static int               client_get_lock(char *);
static int               client_connect(struct event_base *, const char *,
                            uint64_t);
static void              client_send_identify(const char *, const char *,
                            char **, u_int, const char *, int);
static void              client_signal(int);
static void              client_dispatch(struct imsg *, void *);
static void              client_dispatch_attached(struct imsg *);
static void              client_dispatch_wait(struct imsg *);
static const char       *client_exit_message(void);

/*
* Get server create lock. If already held then server start is happening in
* another client, so block until the lock is released and return -2 to
* retry. Return -1 on failure to continue and start the server anyway.
*/
static int
client_get_lock(char *lockfile)
{
       int lockfd;

       log_debug("lock file is %s", lockfile);

       if ((lockfd = open(lockfile, O_WRONLY|O_CREAT, 0600)) == -1) {
               log_debug("open failed: %s", strerror(errno));
               return (-1);
       }

       if (flock(lockfd, LOCK_EX|LOCK_NB) == -1) {
               log_debug("flock failed: %s", strerror(errno));
               if (errno != EAGAIN)
                       return (lockfd);
               while (flock(lockfd, LOCK_EX) == -1 && errno == EINTR)
                       /* nothing */;
               close(lockfd);
               return (-2);
       }
       log_debug("flock succeeded");

       return (lockfd);
}

/* Connect client to server. */
static int
client_connect(struct event_base *base, const char *path, uint64_t flags)
{
       struct sockaddr_un      sa;
       size_t                  size;
       int                     fd, lockfd = -1, locked = 0;
       char                   *lockfile = NULL;

       memset(&sa, 0, sizeof sa);
       sa.sun_family = AF_UNIX;
       size = strlcpy(sa.sun_path, path, sizeof sa.sun_path);
       if (size >= sizeof sa.sun_path) {
               errno = ENAMETOOLONG;
               return (-1);
       }
       log_debug("socket is %s", path);

retry:
       if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
               return (-1);

       log_debug("trying connect");
       if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == -1) {
               log_debug("connect failed: %s", strerror(errno));
               if (errno != ECONNREFUSED && errno != ENOENT)
                       goto failed;
               if (flags & CLIENT_NOSTARTSERVER)
                       goto failed;
               if (~flags & CLIENT_STARTSERVER)
                       goto failed;
               close(fd);

               if (!locked) {
                       xasprintf(&lockfile, "%s.lock", path);
                       if ((lockfd = client_get_lock(lockfile)) < 0) {
                               log_debug("didn't get lock (%d)", lockfd);

                               free(lockfile);
                               lockfile = NULL;

                               if (lockfd == -2)
                                       goto retry;
                       }
                       log_debug("got lock (%d)", lockfd);

                       /*
                        * Always retry at least once, even if we got the lock,
                        * because another client could have taken the lock,
                        * started the server and released the lock between our
                        * connect() and flock().
                        */
                       locked = 1;
                       goto retry;
               }

               if (lockfd >= 0 && unlink(path) != 0 && errno != ENOENT) {
                       free(lockfile);
                       close(lockfd);
                       return (-1);
               }
               fd = server_start(client_proc, flags, base, lockfd, lockfile);
       }

       if (locked && lockfd >= 0) {
               free(lockfile);
               close(lockfd);
       }
       setblocking(fd, 0);
       return (fd);

failed:
       if (locked) {
               free(lockfile);
               close(lockfd);
       }
       close(fd);
       return (-1);
}

/* Get exit string from reason number. */
const char *
client_exit_message(void)
{
       static char msg[256];

       switch (client_exitreason) {
       case CLIENT_EXIT_NONE:
               break;
       case CLIENT_EXIT_DETACHED:
               if (client_exitsession != NULL) {
                       xsnprintf(msg, sizeof msg, "detached "
                           "(from session %s)", client_exitsession);
                       return (msg);
               }
               return ("detached");
       case CLIENT_EXIT_DETACHED_HUP:
               if (client_exitsession != NULL) {
                       xsnprintf(msg, sizeof msg, "detached and SIGHUP "
                           "(from session %s)", client_exitsession);
                       return (msg);
               }
               return ("detached and SIGHUP");
       case CLIENT_EXIT_LOST_TTY:
               return ("lost tty");
       case CLIENT_EXIT_TERMINATED:
               return ("terminated");
       case CLIENT_EXIT_LOST_SERVER:
               return ("server exited unexpectedly");
       case CLIENT_EXIT_EXITED:
               return ("exited");
       case CLIENT_EXIT_SERVER_EXITED:
               return ("server exited");
       case CLIENT_EXIT_MESSAGE_PROVIDED:
               return (client_exitmessage);
       }
       return ("unknown reason");
}

/* Exit if all streams flushed. */
static void
client_exit(void)
{
       if (!file_write_left(&client_files))
               proc_exit(client_proc);
}

/* Client main loop. */
int
client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
   int feat)
{
       struct cmd_parse_result *pr;
       struct msg_command      *data;
       int                      fd, i;
       const char              *ttynam, *termname, *cwd;
       pid_t                    ppid;
       enum msgtype             msg;
       struct termios           tio, saved_tio;
       size_t                   size, linesize = 0;
       ssize_t                  linelen;
       char                    *line = NULL, **caps = NULL, *cause;
       u_int                    ncaps = 0;
       struct args_value       *values;

       /* Set up the initial command. */
       if (shell_command != NULL) {
               msg = MSG_SHELL;
               flags |= CLIENT_STARTSERVER;
       } else if (argc == 0) {
               msg = MSG_COMMAND;
               flags |= CLIENT_STARTSERVER;
       } else {
               msg = MSG_COMMAND;

               /*
                * It's annoying parsing the command string twice (in client
                * and later in server) but it is necessary to get the start
                * server flag.
                */
               values = args_from_vector(argc, argv);
               pr = cmd_parse_from_arguments(values, argc, NULL);
               if (pr->status == CMD_PARSE_SUCCESS) {
                       if (cmd_list_any_have(pr->cmdlist, CMD_STARTSERVER))
                               flags |= CLIENT_STARTSERVER;
                       cmd_list_free(pr->cmdlist);
               } else
                       free(pr->error);
               args_free_values(values, argc);
               free(values);
       }

       /* Create client process structure (starts logging). */
       client_proc = proc_start("client");
       proc_set_signals(client_proc, client_signal);

       /* Save the flags. */
       client_flags = flags;
       log_debug("flags are %#llx", (unsigned long long)client_flags);

       /* Initialize the client socket and start the server. */
#ifdef HAVE_SYSTEMD
       if (systemd_activated()) {
               /* socket-based activation, do not even try to be a client. */
               fd = server_start(client_proc, flags, base, 0, NULL);
       } else
#endif
       fd = client_connect(base, socket_path, client_flags);
       if (fd == -1) {
               if (errno == ECONNREFUSED) {
                       fprintf(stderr, "no server running on %s\n",
                           socket_path);
               } else {
                       fprintf(stderr, "error connecting to %s (%s)\n",
                           socket_path, strerror(errno));
               }
               return (1);
       }
       client_peer = proc_add_peer(client_proc, fd, client_dispatch, NULL);

       /* Save these before pledge(). */
       if ((cwd = find_cwd()) == NULL && (cwd = find_home()) == NULL)
               cwd = "/";
       if ((ttynam = ttyname(STDIN_FILENO)) == NULL)
               ttynam = "";
       if ((termname = getenv("TERM")) == NULL)
               termname = "";

       /*
        * Drop privileges for client. "proc exec" is needed for -c and for
        * locking (which uses system(3)).
        *
        * "tty" is needed to restore termios(4) and also for some reason -CC
        * does not work properly without it (input is not recognised).
        *
        * "sendfd" is dropped later in client_dispatch_wait().
        */
       if (pledge(
           "stdio rpath wpath cpath unix sendfd proc exec tty",
           NULL) != 0)
               fatal("pledge failed");

       /* Load terminfo entry if any. */
       if (isatty(STDIN_FILENO) &&
           *termname != '\0' &&
           tty_term_read_list(termname, STDIN_FILENO, &caps, &ncaps,
           &cause) != 0) {
               fprintf(stderr, "%s\n", cause);
               free(cause);
               return (1);
       }

       /* Free stuff that is not used in the client. */
       if (ptm_fd != -1)
               close(ptm_fd);
       options_free(global_options);
       options_free(global_s_options);
       options_free(global_w_options);
       environ_free(global_environ);

       /* Set up control mode. */
       if (client_flags & CLIENT_CONTROLCONTROL) {
               if (tcgetattr(STDIN_FILENO, &saved_tio) != 0) {
                       fprintf(stderr, "tcgetattr failed: %s\n",
                           strerror(errno));
                       return (1);
               }
               cfmakeraw(&tio);
               tio.c_iflag = ICRNL|IXANY;
               tio.c_oflag = OPOST|ONLCR;
#ifdef NOKERNINFO
               tio.c_lflag = NOKERNINFO;
#endif
               tio.c_cflag = CREAD|CS8|HUPCL;
               tio.c_cc[VMIN] = 1;
               tio.c_cc[VTIME] = 0;
               cfsetispeed(&tio, cfgetispeed(&saved_tio));
               cfsetospeed(&tio, cfgetospeed(&saved_tio));
               tcsetattr(STDIN_FILENO, TCSANOW, &tio);
       }

       /* Send identify messages. */
       client_send_identify(ttynam, termname, caps, ncaps, cwd, feat);
       tty_term_free_list(caps, ncaps);
       proc_flush_peer(client_peer);

       /* Send first command. */
       if (msg == MSG_COMMAND) {
               /* How big is the command? */
               size = 0;
               for (i = 0; i < argc; i++)
                       size += strlen(argv[i]) + 1;
               if (size > MAX_IMSGSIZE - (sizeof *data)) {
                       fprintf(stderr, "command too long\n");
                       return (1);
               }
               data = xmalloc((sizeof *data) + size);

               /* Prepare command for server. */
               data->argc = argc;
               if (cmd_pack_argv(argc, argv, (char *)(data + 1), size) != 0) {
                       fprintf(stderr, "command too long\n");
                       free(data);
                       return (1);
               }
               size += sizeof *data;

               /* Send the command. */
               if (proc_send(client_peer, msg, -1, data, size) != 0) {
                       fprintf(stderr, "failed to send command\n");
                       free(data);
                       return (1);
               }
               free(data);
       } else if (msg == MSG_SHELL)
               proc_send(client_peer, msg, -1, NULL, 0);

       /* Start main loop. */
       proc_loop(client_proc, NULL);

       /* Run command if user requested exec, instead of exiting. */
       if (client_exittype == MSG_EXEC) {
               if (client_flags & CLIENT_CONTROLCONTROL)
                       tcsetattr(STDOUT_FILENO, TCSAFLUSH, &saved_tio);
               client_exec(client_execshell, client_execcmd);
       }

       /* Restore streams to blocking. */
       setblocking(STDIN_FILENO, 1);
       setblocking(STDOUT_FILENO, 1);
       setblocking(STDERR_FILENO, 1);

       /* Print the exit message, if any, and exit. */
       if (client_attached) {
               if (client_exitreason != CLIENT_EXIT_NONE)
                       printf("[%s]\n", client_exit_message());

               ppid = getppid();
               if (client_exittype == MSG_DETACHKILL && ppid > 1)
                       kill(ppid, SIGHUP);
       } else if (client_flags & CLIENT_CONTROL) {
               if (client_exitreason != CLIENT_EXIT_NONE)
                       printf("%%exit %s\n", client_exit_message());
               else
                       printf("%%exit\n");
               fflush(stdout);
               if (client_flags & CLIENT_CONTROL_WAITEXIT) {
                       setvbuf(stdin, NULL, _IOLBF, 0);
                       for (;;) {
                               linelen = getline(&line, &linesize, stdin);
                               if (linelen <= 1)
                                       break;
                       }
                       free(line);
               }
               if (client_flags & CLIENT_CONTROLCONTROL) {
                       printf("\033\\");
                       fflush(stdout);
                       tcsetattr(STDOUT_FILENO, TCSAFLUSH, &saved_tio);
               }
       } else if (client_exitreason != CLIENT_EXIT_NONE)
               fprintf(stderr, "%s\n", client_exit_message());
       return (client_exitval);
}

/* Send identify messages to server. */
static void
client_send_identify(const char *ttynam, const char *termname, char **caps,
   u_int ncaps, const char *cwd, int feat)
{
       char    **ss;
       size_t    sslen;
       int       fd;
       uint64_t  flags = client_flags;
       pid_t     pid;
       u_int     i;

       proc_send(client_peer, MSG_IDENTIFY_LONGFLAGS, -1, &flags, sizeof flags);
       proc_send(client_peer, MSG_IDENTIFY_LONGFLAGS, -1, &client_flags,
           sizeof client_flags);

       proc_send(client_peer, MSG_IDENTIFY_TERM, -1, termname,
           strlen(termname) + 1);
       proc_send(client_peer, MSG_IDENTIFY_FEATURES, -1, &feat, sizeof feat);

       proc_send(client_peer, MSG_IDENTIFY_TTYNAME, -1, ttynam,
           strlen(ttynam) + 1);
       proc_send(client_peer, MSG_IDENTIFY_CWD, -1, cwd, strlen(cwd) + 1);

       for (i = 0; i < ncaps; i++) {
               proc_send(client_peer, MSG_IDENTIFY_TERMINFO, -1,
                   caps[i], strlen(caps[i]) + 1);
       }

       if ((fd = dup(STDIN_FILENO)) == -1)
               fatal("dup failed");
       proc_send(client_peer, MSG_IDENTIFY_STDIN, fd, NULL, 0);
       if ((fd = dup(STDOUT_FILENO)) == -1)
               fatal("dup failed");
       proc_send(client_peer, MSG_IDENTIFY_STDOUT, fd, NULL, 0);

       pid = getpid();
       proc_send(client_peer, MSG_IDENTIFY_CLIENTPID, -1, &pid, sizeof pid);

       for (ss = environ; *ss != NULL; ss++) {
               sslen = strlen(*ss) + 1;
               if (sslen > MAX_IMSGSIZE - IMSG_HEADER_SIZE)
                       continue;
               proc_send(client_peer, MSG_IDENTIFY_ENVIRON, -1, *ss, sslen);
       }

       proc_send(client_peer, MSG_IDENTIFY_DONE, -1, NULL, 0);
}

/* Run command in shell; used for -c. */
static __dead void
client_exec(const char *shell, const char *shellcmd)
{
       char    *argv0;

       log_debug("shell %s, command %s", shell, shellcmd);
       argv0 = shell_argv0(shell, !!(client_flags & CLIENT_LOGIN));
       setenv("SHELL", shell, 1);

       proc_clear_signals(client_proc, 1);

       setblocking(STDIN_FILENO, 1);
       setblocking(STDOUT_FILENO, 1);
       setblocking(STDERR_FILENO, 1);
       closefrom(STDERR_FILENO + 1);

       execl(shell, argv0, "-c", shellcmd, (char *) NULL);
       fatal("execl failed");
}

/* Callback to handle signals in the client. */
static void
client_signal(int sig)
{
       struct sigaction sigact;
       int              status;
       pid_t            pid;

       log_debug("%s: %s", __func__, strsignal(sig));
       if (sig == SIGCHLD) {
               for (;;) {
                       pid = waitpid(WAIT_ANY, &status, WNOHANG);
                       if (pid == 0)
                               break;
                       if (pid == -1) {
                               if (errno == ECHILD)
                                       break;
                               log_debug("waitpid failed: %s",
                                   strerror(errno));
                       }
               }
       } else if (!client_attached) {
               if (sig == SIGTERM || sig == SIGHUP)
                       proc_exit(client_proc);
       } else {
               switch (sig) {
               case SIGHUP:
                       client_exitreason = CLIENT_EXIT_LOST_TTY;
                       client_exitval = 1;
                       proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
                       break;
               case SIGTERM:
                       if (!client_suspended)
                               client_exitreason = CLIENT_EXIT_TERMINATED;
                       client_exitval = 1;
                       proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
                       break;
               case SIGWINCH:
                       proc_send(client_peer, MSG_RESIZE, -1, NULL, 0);
                       break;
               case SIGCONT:
                       memset(&sigact, 0, sizeof sigact);
                       sigemptyset(&sigact.sa_mask);
                       sigact.sa_flags = SA_RESTART;
                       sigact.sa_handler = SIG_IGN;
                       if (sigaction(SIGTSTP, &sigact, NULL) != 0)
                               fatal("sigaction failed");
                       proc_send(client_peer, MSG_WAKEUP, -1, NULL, 0);
                       client_suspended = 0;
                       break;
               }
       }
}

/* Callback for file write error or close. */
static void
client_file_check_cb(__unused struct client *c, __unused const char *path,
   __unused int error, __unused int closed, __unused struct evbuffer *buffer,
   __unused void *data)
{
       if (client_exitflag)
               client_exit();
}

/* Callback for client read events. */
static void
client_dispatch(struct imsg *imsg, __unused void *arg)
{
       if (imsg == NULL) {
               if (!client_exitflag) {
                       client_exitreason = CLIENT_EXIT_LOST_SERVER;
                       client_exitval = 1;
               }
               proc_exit(client_proc);
               return;
       }

       if (client_attached)
               client_dispatch_attached(imsg);
       else
               client_dispatch_wait(imsg);
}

/* Process an exit message. */
static void
client_dispatch_exit_message(char *data, size_t datalen)
{
       int     retval;

       if (datalen < sizeof retval && datalen != 0)
               fatalx("bad MSG_EXIT size");

       if (datalen >= sizeof retval) {
               memcpy(&retval, data, sizeof retval);
               client_exitval = retval;
       }

       if (datalen > sizeof retval) {
               datalen -= sizeof retval;
               data += sizeof retval;

               client_exitmessage = xmalloc(datalen);
               memcpy(client_exitmessage, data, datalen);
               client_exitmessage[datalen - 1] = '\0';

               client_exitreason = CLIENT_EXIT_MESSAGE_PROVIDED;
       }
}

/* Dispatch imsgs when in wait state (before MSG_READY). */
static void
client_dispatch_wait(struct imsg *imsg)
{
       char            *data;
       ssize_t          datalen;
       static int       pledge_applied;

       /*
        * "sendfd" is no longer required once all of the identify messages
        * have been sent. We know the server won't send us anything until that
        * point (because we don't ask it to), so we can drop "sendfd" once we
        * get the first message from the server.
        */
       if (!pledge_applied) {
               if (pledge(
                   "stdio rpath wpath cpath unix proc exec tty",
                   NULL) != 0)
                       fatal("pledge failed");
               pledge_applied = 1;
       }

       data = imsg->data;
       datalen = imsg->hdr.len - IMSG_HEADER_SIZE;

       switch (imsg->hdr.type) {
       case MSG_EXIT:
       case MSG_SHUTDOWN:
               client_dispatch_exit_message(data, datalen);
               client_exitflag = 1;
               client_exit();
               break;
       case MSG_READY:
               if (datalen != 0)
                       fatalx("bad MSG_READY size");

               client_attached = 1;
               proc_send(client_peer, MSG_RESIZE, -1, NULL, 0);
               break;
       case MSG_VERSION:
               if (datalen != 0)
                       fatalx("bad MSG_VERSION size");

               fprintf(stderr, "protocol version mismatch "
                   "(client %d, server %u)\n", PROTOCOL_VERSION,
                   imsg->hdr.peerid & 0xff);
               client_exitval = 1;
               proc_exit(client_proc);
               break;
       case MSG_FLAGS:
               if (datalen != sizeof client_flags)
                       fatalx("bad MSG_FLAGS string");

               memcpy(&client_flags, data, sizeof client_flags);
               log_debug("new flags are %#llx",
                   (unsigned long long)client_flags);
               break;
       case MSG_SHELL:
               if (datalen == 0 || data[datalen - 1] != '\0')
                       fatalx("bad MSG_SHELL string");

               client_exec(data, shell_command);
               /* NOTREACHED */
       case MSG_DETACH:
       case MSG_DETACHKILL:
               proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
               break;
       case MSG_EXITED:
               proc_exit(client_proc);
               break;
       case MSG_READ_OPEN:
               file_read_open(&client_files, client_peer, imsg, 1,
                   !(client_flags & CLIENT_CONTROL), client_file_check_cb,
                   NULL);
               break;
       case MSG_READ_CANCEL:
               file_read_cancel(&client_files, imsg);
               break;
       case MSG_WRITE_OPEN:
               file_write_open(&client_files, client_peer, imsg, 1,
                   !(client_flags & CLIENT_CONTROL), client_file_check_cb,
                   NULL);
               break;
       case MSG_WRITE:
               file_write_data(&client_files, imsg);
               break;
       case MSG_WRITE_CLOSE:
               file_write_close(&client_files, imsg);
               break;
       case MSG_OLDSTDERR:
       case MSG_OLDSTDIN:
       case MSG_OLDSTDOUT:
               fprintf(stderr, "server version is too old for client\n");
               proc_exit(client_proc);
               break;
       }
}

/* Dispatch imsgs in attached state (after MSG_READY). */
static void
client_dispatch_attached(struct imsg *imsg)
{
       struct sigaction         sigact;
       char                    *data;
       ssize_t                  datalen;

       data = imsg->data;
       datalen = imsg->hdr.len - IMSG_HEADER_SIZE;

       switch (imsg->hdr.type) {
       case MSG_FLAGS:
               if (datalen != sizeof client_flags)
                       fatalx("bad MSG_FLAGS string");

               memcpy(&client_flags, data, sizeof client_flags);
               log_debug("new flags are %#llx",
                   (unsigned long long)client_flags);
               break;
       case MSG_DETACH:
       case MSG_DETACHKILL:
               if (datalen == 0 || data[datalen - 1] != '\0')
                       fatalx("bad MSG_DETACH string");

               client_exitsession = xstrdup(data);
               client_exittype = imsg->hdr.type;
               if (imsg->hdr.type == MSG_DETACHKILL)
                       client_exitreason = CLIENT_EXIT_DETACHED_HUP;
               else
                       client_exitreason = CLIENT_EXIT_DETACHED;
               proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
               break;
       case MSG_EXEC:
               if (datalen == 0 || data[datalen - 1] != '\0' ||
                   strlen(data) + 1 == (size_t)datalen)
                       fatalx("bad MSG_EXEC string");
               client_execcmd = xstrdup(data);
               client_execshell = xstrdup(data + strlen(data) + 1);

               client_exittype = imsg->hdr.type;
               proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
               break;
       case MSG_EXIT:
               client_dispatch_exit_message(data, datalen);
               if (client_exitreason == CLIENT_EXIT_NONE)
                       client_exitreason = CLIENT_EXIT_EXITED;
               proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
               break;
       case MSG_EXITED:
               if (datalen != 0)
                       fatalx("bad MSG_EXITED size");

               proc_exit(client_proc);
               break;
       case MSG_SHUTDOWN:
               if (datalen != 0)
                       fatalx("bad MSG_SHUTDOWN size");

               proc_send(client_peer, MSG_EXITING, -1, NULL, 0);
               client_exitreason = CLIENT_EXIT_SERVER_EXITED;
               client_exitval = 1;
               break;
       case MSG_SUSPEND:
               if (datalen != 0)
                       fatalx("bad MSG_SUSPEND size");

               memset(&sigact, 0, sizeof sigact);
               sigemptyset(&sigact.sa_mask);
               sigact.sa_flags = SA_RESTART;
               sigact.sa_handler = SIG_DFL;
               if (sigaction(SIGTSTP, &sigact, NULL) != 0)
                       fatal("sigaction failed");
               client_suspended = 1;
               kill(getpid(), SIGTSTP);
               break;
       case MSG_LOCK:
               if (datalen == 0 || data[datalen - 1] != '\0')
                       fatalx("bad MSG_LOCK string");

               system(data);
               proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
               break;
       }
}