diff --git a/.gitignore b/.gitignore index 48606313cce..aecc6ae0f07 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,7 @@ /test-path-util /test-prioq /test-ratelimit +/test-pty /test-replace-var /test-resolve /test-ring diff --git a/Makefile.am b/Makefile.am index fe680b0d948..a9ee8b070e4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -843,6 +843,8 @@ libsystemd_shared_la_SOURCES = \ src/shared/ring.h \ src/shared/barrier.c \ src/shared/barrier.h \ + src/shared/pty.c \ + src/shared/pty.h \ src/shared/async.c \ src/shared/async.h \ src/shared/copy.c \ @@ -1252,6 +1254,7 @@ tests += \ test-util \ test-ring \ test-barrier \ + test-pty \ test-tmpfiles \ test-namespace \ test-date \ @@ -1428,6 +1431,12 @@ test_barrier_SOURCES = \ test_barrier_LDADD = \ libsystemd-core.la +test_pty_SOURCES = \ + src/test/test-pty.c + +test_pty_LDADD = \ + libsystemd-core.la + test_tmpfiles_SOURCES = \ src/test/test-tmpfiles.c diff --git a/src/shared/pty.c b/src/shared/pty.c new file mode 100644 index 00000000000..11d76f825fd --- /dev/null +++ b/src/shared/pty.c @@ -0,0 +1,640 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +/* + * PTY + * A PTY object represents a single PTY connection between a master and a + * child. The child process is fork()ed so the caller controls what program + * will be run. + * + * Programs like /bin/login tend to perform a vhangup() on their TTY + * before running the login procedure. This also causes the pty master + * to get a EPOLLHUP event as long as no client has the TTY opened. + * This means, we cannot use the TTY connection as reliable way to track + * the client. Instead, we _must_ rely on the PID of the client to track + * them. + * However, this has the side effect that if the client forks and the + * parent exits, we loose them and restart the client. But this seems to + * be the expected behavior so we implement it here. + * + * Unfortunately, epoll always polls for EPOLLHUP so as long as the + * vhangup() is ongoing, we will _always_ get EPOLLHUP and cannot sleep. + * This gets worse if the client closes the TTY but doesn't exit. + * Therefore, the fd must be edge-triggered in the epoll-set so we + * only get the events once they change. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "barrier.h" +#include "macro.h" +#include "pty.h" +#include "ring.h" +#include "util.h" + +#define PTY_BUFSIZE 16384 + +enum { + PTY_ROLE_UNKNOWN, + PTY_ROLE_PARENT, + PTY_ROLE_CHILD, +}; + +struct Pty { + unsigned long ref; + Barrier barrier; + int fd; + pid_t child; + sd_event_source *fd_source; + sd_event_source *child_source; + + char in_buf[PTY_BUFSIZE]; + Ring out_buf; + + pty_event_t event_fn; + void *event_fn_userdata; + + bool needs_requeue : 1; + unsigned int role : 2; +}; + +int pty_new(Pty **out) { + _pty_unref_ Pty *pty = NULL; + int r; + + assert_return(out, -EINVAL); + + pty = new0(Pty, 1); + if (!pty) + return -ENOMEM; + + pty->ref = 1; + pty->fd = -1; + + pty->fd = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC | O_NONBLOCK); + if (pty->fd < 0) + return -errno; + + /* + * The slave-node is initialized to uid/gid of the caller of + * posix_openpt(). Only if devpts is mounted with fixed uid/gid this is + * skipped. In that case, grantpt() can overwrite these, but then you + * have to be root to use chown() (or a pt_chown helper has to be + * present). In those cases grantpt() really does something, + * otherwise it's a no-op. We call grantpt() here to try supporting + * those cases, even though no-one uses that, I guess. If you need other + * access-rights, set them yourself after this call returns (no, this is + * not racy, it looks racy, but races regarding your own UID are never + * important as an attacker could ptrace you; and the slave-pty is also + * still locked). + */ + r = grantpt(pty->fd); + if (r < 0) + return -errno; + + r = barrier_init(&pty->barrier); + if (r < 0) + return r; + + *out = pty; + pty = NULL; + return 0; +} + +Pty *pty_ref(Pty *pty) { + if (!pty || pty->ref < 1) + return NULL; + + ++pty->ref; + return pty; +} + +Pty *pty_unref(Pty *pty) { + if (!pty || pty->ref < 1 || --pty->ref > 0) + return NULL; + + pty_close(pty); + pty->child_source = sd_event_source_unref(pty->child_source); + barrier_destroy(&pty->barrier); + ring_clear(&pty->out_buf); + free(pty); + + return NULL; +} + +Barrier *pty_get_barrier(Pty *pty) { + assert(pty); + return &pty->barrier; +} + +bool pty_is_unknown(Pty *pty) { + return pty && pty->role == PTY_ROLE_UNKNOWN; +} + +bool pty_is_parent(Pty *pty) { + return pty && pty->role == PTY_ROLE_PARENT; +} + +bool pty_is_child(Pty *pty) { + return pty && pty->role == PTY_ROLE_CHILD; +} + +bool pty_has_child(Pty *pty) { + return pty_is_parent(pty) && pty->child > 0; +} + +pid_t pty_get_child(Pty *pty) { + return pty_has_child(pty) ? pty->child : -ECHILD; +} + +bool pty_is_open(Pty *pty) { + return pty && pty->fd >= 0; +} + +int pty_get_fd(Pty *pty) { + assert_return(pty, -EINVAL); + + return pty_is_open(pty) ? pty->fd : -EPIPE; +} + +int pty_make_child(Pty *pty) { + char slave_name[1024]; + int r, fd; + + assert_return(pty, -EINVAL); + assert_return(pty_is_unknown(pty), -EALREADY); + + r = ptsname_r(pty->fd, slave_name, sizeof(slave_name)); + if (r < 0) + return -errno; + + fd = open(slave_name, O_RDWR | O_CLOEXEC | O_NOCTTY); + if (fd < 0) + return -errno; + + safe_close(pty->fd); + pty->fd = fd; + pty->child = getpid(); + pty->role = PTY_ROLE_CHILD; + barrier_set_role(&pty->barrier, BARRIER_CHILD); + + return 0; +} + +int pty_make_parent(Pty *pty, pid_t child) { + assert_return(pty, -EINVAL); + assert_return(pty_is_unknown(pty), -EALREADY); + + pty->child = child; + pty->role = PTY_ROLE_PARENT; + + return 0; +} + +int pty_unlock(Pty *pty) { + assert_return(pty, -EINVAL); + assert_return(pty_is_unknown(pty) || pty_is_parent(pty), -EINVAL); + assert_return(pty_is_open(pty), -ENODEV); + + return unlockpt(pty->fd) < 0 ? -errno : 0; +} + +int pty_setup_child(Pty *pty) { + struct termios attr; + pid_t pid; + int r; + + assert_return(pty, -EINVAL); + assert_return(pty_is_child(pty), -EINVAL); + assert_return(pty_is_open(pty), -EALREADY); + + r = sigprocmask_many(SIG_SETMASK, -1); + if (r < 0) + return r; + + r = reset_all_signal_handlers(); + if (r < 0) + return r; + + pid = setsid(); + if (pid < 0 && errno != EPERM) + return -errno; + + r = ioctl(pty->fd, TIOCSCTTY, 0); + if (r < 0) + return -errno; + + r = tcgetattr(pty->fd, &attr); + if (r < 0) + return -errno; + + /* erase character should be normal backspace, PLEASEEE! */ + attr.c_cc[VERASE] = 010; + /* always set UTF8 flag */ + attr.c_iflag |= IUTF8; + + r = tcsetattr(pty->fd, TCSANOW, &attr); + if (r < 0) + return -errno; + + if (dup2(pty->fd, STDIN_FILENO) != STDIN_FILENO || + dup2(pty->fd, STDOUT_FILENO) != STDOUT_FILENO || + dup2(pty->fd, STDERR_FILENO) != STDERR_FILENO) + return -errno; + + /* only close FD if it's not a std-fd */ + pty->fd = (pty->fd > 2) ? safe_close(pty->fd) : -1; + + return 0; +} + +void pty_close(Pty *pty) { + if (!pty_is_open(pty)) + return; + + pty->fd_source = sd_event_source_unref(pty->fd_source); + pty->fd = safe_close(pty->fd); +} + +/* + * Drain input-queue and dispatch data via the event-handler. Returns <0 on + * error, 0 if queue is empty and 1 if we couldn't empty the input queue fast + * enough and there's still data left. + */ +static int pty_dispatch_read(Pty *pty) { + unsigned int i; + ssize_t len; + int r; + + /* + * We're edge-triggered, means we need to read the whole queue. This, + * however, might cause us to stall if the writer is faster than we + * are. Therefore, we read twice and if the second read still returned + * data, we reschedule. + */ + + for (i = 0; i < 2; ++i) { + len = read(pty->fd, pty->in_buf, sizeof(pty->in_buf) - 1); + if (len < 0) { + if (errno == EINTR) + continue; + + return (errno == EAGAIN) ? 0 : -errno; + } else if (len == 0) { + continue; + } + + /* set terminating zero for debugging safety */ + pty->in_buf[len] = 0; + r = pty->event_fn(pty, pty->event_fn_userdata, PTY_DATA, pty->in_buf, len); + if (r < 0) + return r; + } + + /* still data left, make sure we're queued again */ + pty->needs_requeue = true; + + return 1; +} + +/* + * Drain output-queue by writing data to the pty. Returns <0 on error, 0 if the + * output queue is empty now and 1 if we couldn't empty the output queue fast + * enough and there's still data left. + */ +static int pty_dispatch_write(Pty *pty) { + struct iovec vec[2]; + unsigned int i; + ssize_t len; + size_t num; + + /* + * Same as pty_dispatch_read(), we're edge-triggered so we need to call + * write() until either all data is written or it returns EAGAIN. We + * call it twice and if it still writes successfully, we reschedule. + */ + + for (i = 0; i < 2; ++i) { + num = ring_peek(&pty->out_buf, vec); + if (num < 1) + return 0; + + len = writev(pty->fd, vec, (int)num); + if (len < 0) { + if (errno == EINTR) + continue; + + return (errno == EAGAIN) ? 1 : -errno; + } else if (len == 0) { + continue; + } + + ring_pull(&pty->out_buf, (size_t)len); + } + + /* still data left, make sure we're queued again */ + if (ring_get_size(&pty->out_buf) > 0) { + pty->needs_requeue = true; + return 1; + } + + return 0; +} + +static int pty_fd_fn(sd_event_source *source, int fd, uint32_t revents, void *userdata) { + Pty *pty = userdata; + int r_hup = 0, r_write = 0, r_read = 0, r; + + /* + * Whenever we encounter I/O errors, we have to make sure to drain the + * input queue first, before we handle any HUP. A child might send us + * a message and immediately close the queue. We must not handle the + * HUP first or we loose data. + * Therefore, if we read a message successfully, we always return + * success and wait for the next event-loop iteration. Furthermore, + * whenever there is a write-error, we must try reading from the input + * queue even if EPOLLIN is not set. The input might have arrived in + * between epoll_wait() and write(). Therefore, write-errors are only + * ever handled if the input-queue is empty. In all other cases they + * are ignored until either reading fails or the input queue is empty. + */ + + if (revents & (EPOLLHUP | EPOLLERR)) + r_hup = -EPIPE; + + if (revents & EPOLLOUT) + r_write = pty_dispatch_write(pty); + + /* Awesome! Kernel signals HUP without IN but queues are not empty.. */ + if ((revents & EPOLLIN) || r_hup < 0 || r_write < 0) { + r_read = pty_dispatch_read(pty); + if (r_read > 0) + return 0; /* still data left to fetch next round */ + } + + if (r_hup < 0 || r_write < 0 || r_read < 0) { + /* PTY closed and input-queue drained */ + pty_close(pty); + r = pty->event_fn(pty, pty->event_fn_userdata, PTY_HUP, NULL, 0); + if (r < 0) + return r; + } + + return 0; +} + +static int pty_fd_prepare_fn(sd_event_source *source, void *userdata) { + Pty *pty = userdata; + int r; + + if (pty->needs_requeue) { + /* + * We're edge-triggered. In case we couldn't handle all events + * or in case new write-data is queued, we set needs_requeue. + * Before going asleep, we set the io-events *again*. sd-event + * notices that we're edge-triggered and forwards the call to + * the kernel even if the events didn't change. The kernel will + * check the events and re-queue us on the ready queue in case + * an event is pending. + */ + r = sd_event_source_set_io_events(source, EPOLLHUP | EPOLLERR | EPOLLIN | EPOLLOUT | EPOLLET); + if (r >= 0) + pty->needs_requeue = false; + } + + return 0; +} + +static int pty_child_fn(sd_event_source *source, const siginfo_t *si, void *userdata) { + Pty *pty = userdata; + int r; + + pty->child = 0; + + r = pty->event_fn(pty, pty->event_fn_userdata, PTY_CHILD, si, sizeof(*si)); + if (r < 0) + return r; + + return 0; +} + +int pty_attach_event(Pty *pty, sd_event *event, pty_event_t event_fn, void *event_fn_userdata) { + int r; + + assert_return(pty, -EINVAL); + assert_return(event, -EINVAL); + assert_return(event_fn, -EINVAL); + assert_return(pty_is_parent(pty), -EINVAL); + + pty_detach_event(pty); + + if (pty_is_open(pty)) { + r = sd_event_add_io(event, + &pty->fd_source, + pty->fd, + EPOLLHUP | EPOLLERR | EPOLLIN | EPOLLOUT | EPOLLET, + pty_fd_fn, + pty); + if (r < 0) + goto error; + + r = sd_event_source_set_prepare(pty->fd_source, pty_fd_prepare_fn); + if (r < 0) + goto error; + } + + if (pty_has_child(pty)) { + r = sd_event_add_child(event, + &pty->child_source, + pty->child, + WEXITED, + pty_child_fn, + pty); + if (r < 0) + goto error; + } + + pty->event_fn = event_fn; + pty->event_fn_userdata = event_fn_userdata; + + return 0; + +error: + pty_detach_event(pty); + return r; +} + +void pty_detach_event(Pty *pty) { + if (!pty) + return; + + pty->child_source = sd_event_source_unref(pty->child_source); + pty->fd_source = sd_event_source_unref(pty->fd_source); + pty->event_fn = NULL; + pty->event_fn_userdata = NULL; +} + +int pty_write(Pty *pty, const void *buf, size_t size) { + bool was_empty; + int r; + + assert_return(pty, -EINVAL); + assert_return(pty_is_open(pty), -ENODEV); + assert_return(pty_is_parent(pty), -ENODEV); + + if (size < 1) + return 0; + + /* + * Push @buf[0..@size] into the output ring-buffer. In case the + * ring-buffer wasn't empty beforehand, we're already waiting for + * EPOLLOUT and we're done. If it was empty, we have to re-queue the + * FD for EPOLLOUT as we're edge-triggered and wouldn't get any new + * EPOLLOUT event. + */ + + was_empty = ring_get_size(&pty->out_buf) < 1; + + r = ring_push(&pty->out_buf, buf, size); + if (r < 0) + return r; + + if (was_empty) + pty->needs_requeue = true; + + return 0; +} + +int pty_signal(Pty *pty, int sig) { + assert_return(pty, -EINVAL); + assert_return(pty_is_open(pty), -ENODEV); + assert_return(pty_is_parent(pty), -ENODEV); + + return ioctl(pty->fd, TIOCSIG, sig) < 0 ? -errno : 0; +} + +int pty_resize(Pty *pty, unsigned short term_width, unsigned short term_height) { + struct winsize ws; + + assert_return(pty, -EINVAL); + assert_return(pty_is_open(pty), -ENODEV); + assert_return(pty_is_parent(pty), -ENODEV); + + zero(ws); + ws.ws_col = term_width; + ws.ws_row = term_height; + + /* + * This will send SIGWINCH to the pty slave foreground process group. + * We will also get one, but we don't need it. + */ + return ioctl(pty->fd, TIOCSWINSZ, &ws) < 0 ? -errno : 0; +} + +pid_t pty_fork(Pty **out, sd_event *event, pty_event_t event_fn, void *event_fn_userdata, unsigned short initial_term_width, unsigned short initial_term_height) { + _pty_unref_ Pty *pty = NULL; + int r; + pid_t pid; + + assert_return(out, -EINVAL); + assert_return((event && event_fn) || (!event && !event_fn), -EINVAL); + + r = pty_new(&pty); + if (r < 0) + return r; + + r = pty_unlock(pty); + if (r < 0) + return r; + + pid = fork(); + if (pid < 0) + return -errno; + + if (pid == 0) { + /* child */ + + r = pty_make_child(pty); + if (r < 0) + _exit(-r); + + r = pty_setup_child(pty); + if (r < 0) + _exit(-r); + + /* sync with parent */ + if (!barrier_place_and_sync(&pty->barrier)) + _exit(1); + + /* fallthrough and return the child's PTY object */ + } else { + /* parent */ + + r = pty_make_parent(pty, pid); + if (r < 0) + goto parent_error; + + r = pty_resize(pty, initial_term_width, initial_term_height); + if (r < 0) + goto parent_error; + + if (event) { + r = pty_attach_event(pty, event, event_fn, event_fn_userdata); + if (r < 0) + goto parent_error; + } + + /* sync with child */ + if (!barrier_place_and_sync(&pty->barrier)) { + r = -ECHILD; + goto parent_error; + } + + /* fallthrough and return the parent's PTY object */ + } + + *out = pty; + pty = NULL; + return pid; + +parent_error: + barrier_abort(&pty->barrier); + waitpid(pty->child, NULL, 0); + pty->child = 0; + return r; +} diff --git a/src/shared/pty.h b/src/shared/pty.h new file mode 100644 index 00000000000..a87ceb58ca3 --- /dev/null +++ b/src/shared/pty.h @@ -0,0 +1,77 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +#pragma once + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include + +#include "barrier.h" +#include "macro.h" +#include "sd-event.h" +#include "util.h" + +typedef struct Pty Pty; + +enum { + PTY_CHILD, + PTY_HUP, + PTY_DATA, +}; + +typedef int (*pty_event_t) (Pty *pty, void *userdata, unsigned int event, const void *ptr, size_t size); + +int pty_new(Pty **out); +Pty *pty_ref(Pty *pty); +Pty *pty_unref(Pty *pty); + +#define _pty_unref_ _cleanup_(pty_unrefp) +DEFINE_TRIVIAL_CLEANUP_FUNC(Pty*, pty_unref); + +Barrier *pty_get_barrier(Pty *pty); + +bool pty_is_unknown(Pty *pty); +bool pty_is_parent(Pty *pty); +bool pty_is_child(Pty *pty); +bool pty_has_child(Pty *pty); +pid_t pty_get_child(Pty *pty); + +bool pty_is_open(Pty *pty); +int pty_get_fd(Pty *pty); + +int pty_make_child(Pty *pty); +int pty_make_parent(Pty *pty, pid_t child); +int pty_unlock(Pty *pty); +int pty_setup_child(Pty *pty); +void pty_close(Pty *pty); + +int pty_attach_event(Pty *pty, sd_event *event, pty_event_t event_fn, void *event_fn_userdata); +void pty_detach_event(Pty *pty); + +int pty_write(Pty *pty, const void *buf, size_t size); +int pty_signal(Pty *pty, int sig); +int pty_resize(Pty *pty, unsigned short term_width, unsigned short term_height); + +pid_t pty_fork(Pty **out, sd_event *event, pty_event_t event_fn, void *event_fn_userdata, unsigned short initial_term_width, unsigned short initial_term_height); diff --git a/src/test/test-pty.c b/src/test/test-pty.c new file mode 100644 index 00000000000..73c5c853303 --- /dev/null +++ b/src/test/test-pty.c @@ -0,0 +1,143 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/*** + This file is part of systemd. + + Copyright 2014 David Herrmann + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include +#include +#include +#include +#include +#include + +#include "def.h" +#include "pty.h" +#include "util.h" + +static const char sndmsg[] = "message\n"; +static const char rcvmsg[] = "message\r\n"; +static char rcvbuf[128]; +static size_t rcvsiz = 0; +static sd_event *event; + +static void run_child(Pty *pty) { + int r, l; + char buf[512]; + + r = read(0, buf, sizeof(buf)); + assert_se(r == strlen(sndmsg)); + assert_se(!strncmp(buf, sndmsg, r)); + + l = write(1, buf, r); + assert_se(l == r); +} + +static int pty_fn(Pty *pty, void *userdata, unsigned int ev, const void *ptr, size_t size) { + switch (ev) { + case PTY_DATA: + assert_se(rcvsiz < strlen(rcvmsg) * 2); + assert_se(rcvsiz + size < sizeof(rcvbuf)); + + memcpy(&rcvbuf[rcvsiz], ptr, size); + rcvsiz += size; + + if (rcvsiz >= strlen(rcvmsg) * 2) { + assert_se(rcvsiz == strlen(rcvmsg) * 2); + assert_se(!memcmp(rcvbuf, rcvmsg, strlen(rcvmsg))); + assert_se(!memcmp(&rcvbuf[strlen(rcvmsg)], rcvmsg, strlen(rcvmsg))); + } + + break; + case PTY_HUP: + /* This is guaranteed to appear _after_ the input queues are + * drained! */ + assert_se(rcvsiz == strlen(rcvmsg) * 2); + break; + case PTY_CHILD: + /* this may appear at any time */ + break; + default: + assert_se(0); + break; + } + + /* if we got HUP _and_ CHILD, exit */ + if (pty_get_fd(pty) < 0 && pty_get_child(pty) < 0) + sd_event_exit(event, 0); + + return 0; +} + +static void run_parent(Pty *pty) { + int r; + + /* write message to pty, ECHO mode guarantees that we get it back + * twice: once via ECHO, once from the run_child() fn */ + assert_se(pty_write(pty, sndmsg, strlen(sndmsg)) >= 0); + + r = sd_event_loop(event); + assert_se(r >= 0); +} + +static void test_pty(void) { + pid_t pid; + Pty *pty; + + rcvsiz = 0; + memset(rcvbuf, 0, sizeof(rcvbuf)); + + assert_se(sd_event_default(&event) >= 0); + + pid = pty_fork(&pty, event, pty_fn, NULL, 80, 25); + assert_se(pid >= 0); + + if (pid == 0) { + /* child */ + run_child(pty); + exit(0); + } + + /* parent */ + run_parent(pty); + + /* Make sure the PTY recycled the child; yeah, this is racy if the + * PID was already reused; but that seems fine for a test. */ + assert_se(waitpid(pid, NULL, WNOHANG) < 0 && errno == ECHILD); + + pty_unref(pty); + sd_event_unref(event); +} + +int main(int argc, char *argv[]) { + unsigned int i; + + log_parse_environment(); + log_open(); + + assert_se(sigprocmask_many(SIG_BLOCK, SIGCHLD, -1) >= 0); + + /* Oh, there're ugly races in the TTY layer regarding HUP vs IN. Turns + * out they appear only 10% of the time. I fixed all of them and + * don't see them, anymore. But lets be safe and run this 1000 times + * so we catch any new ones, in case they appear again. */ + for (i = 0; i < 1000; ++i) + test_pty(); + + return 0; +}