| 1 | /* -*-c-*- |
| 2 | * |
| 3 | * Job client low-level magic |
| 4 | * |
| 5 | * (c) 2019 Mark Wooding |
| 6 | */ |
| 7 | |
| 8 | /*----- Licensing notice --------------------------------------------------* |
| 9 | * |
| 10 | * This file is part of the distorted.org.uk chroot maintenance tools. |
| 11 | * |
| 12 | * distorted-chroot is free software: you can redistribute it and/or |
| 13 | * modify it under the terms of the GNU General Public License as |
| 14 | * published by the Free Software Foundation; either version 2 of the |
| 15 | * License, or (at your option) any later version. |
| 16 | * |
| 17 | * distorted-chroot is distributed in the hope that it will be useful, |
| 18 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 19 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 20 | * General Public License for more details. |
| 21 | * |
| 22 | * You should have received a copy of the GNU General Public License |
| 23 | * along with distorted-chroot. If not, write to the Free Software |
| 24 | * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
| 25 | * USA. |
| 26 | */ |
| 27 | |
| 28 | /*----- Header files ------------------------------------------------------*/ |
| 29 | |
| 30 | #include <assert.h> |
| 31 | #include <errno.h> |
| 32 | #include <signal.h> |
| 33 | |
| 34 | #include <sys/types.h> |
| 35 | #include <sys/wait.h> |
| 36 | #include <sys/select.h> |
| 37 | #include <unistd.h> |
| 38 | |
| 39 | #define PY_SSIZE_T_CLEAN |
| 40 | #include <Python.h> |
| 41 | |
| 42 | /*----- Static variables --------------------------------------------------*/ |
| 43 | |
| 44 | static volatile int sigfd = -1; |
| 45 | |
| 46 | /*----- Main code ---------------------------------------------------------*/ |
| 47 | |
| 48 | static void chld(int sig) |
| 49 | { int fd = sigfd; if (fd != -1) { close(fd); sigfd = -1; } } |
| 50 | |
| 51 | static PyObject *pymeth_jobclient(PyObject *me, PyObject *arg) |
| 52 | { |
| 53 | PyObject *rc = 0; |
| 54 | struct sigaction sa, oldsa; |
| 55 | sigset_t mask, oldmask; |
| 56 | fd_set infd; |
| 57 | int rstrchld = 0; |
| 58 | int fd, sfd; |
| 59 | int kid, st; |
| 60 | ssize_t n; |
| 61 | char ch; |
| 62 | |
| 63 | /* Parse the arguments. */ |
| 64 | if (!PyArg_ParseTuple(arg, "i", &fd)) goto end; |
| 65 | |
| 66 | /* Prepare to be able to block `SIGCHLD'. */ |
| 67 | sigemptyset(&mask); sigaddset(&mask, SIGCHLD); |
| 68 | if (sigprocmask(SIG_SETMASK, 0, &oldmask)) goto oserr; |
| 69 | |
| 70 | /* Establish the signal handler. */ |
| 71 | sa.sa_handler = chld; |
| 72 | sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGCHLD); |
| 73 | sa.sa_flags = SA_NOCLDSTOP; |
| 74 | if (sigaction(SIGCHLD, &sa, &oldsa)) goto oserr; |
| 75 | rstrchld = 1; |
| 76 | |
| 77 | /* Establish a copy of the file descriptor. If a signal occurs, we'll |
| 78 | * clobber this descriptor: see below. If we come back here, we'll |
| 79 | * probably need to reinstate this, but check just in case: we don't want a |
| 80 | * descriptor leak. |
| 81 | */ |
| 82 | again: |
| 83 | if (sigprocmask(SIG_BLOCK, &mask, 0)) goto oserr; |
| 84 | sfd = sigfd; |
| 85 | if (sfd == -1) { |
| 86 | sfd = dup(fd); if (sfd == -1) goto oserr; |
| 87 | sigfd = sfd; |
| 88 | } |
| 89 | if (sigprocmask(SIG_UNBLOCK, &mask, 0)) goto oserr; |
| 90 | |
| 91 | /* Check for child processes. If a child exit somehow failed to be |
| 92 | * spotted earlier, this is our chance to notice and do something about it. |
| 93 | */ |
| 94 | kid = waitpid(-1, &st, WNOHANG); |
| 95 | if (kid == -1 && errno != ECHILD) goto oserr; |
| 96 | else if (kid > 0) |
| 97 | { rc = Py_BuildValue("(Oii)", Py_None, kid, st); goto end; } |
| 98 | |
| 99 | /* Read from the pipe. If a child exits between the `waitpid' above and |
| 100 | * the `read' here, the signal handler will pop and close the descriptor, |
| 101 | * so `read' will report `EBADF'. If a child exits while we're waiting for |
| 102 | * a byte on the descriptor, then `read' will report `EINTR'. If anything |
| 103 | * like this happens, then we go back and try reaping children again. |
| 104 | */ |
| 105 | Py_BEGIN_ALLOW_THREADS |
| 106 | for (;;) { |
| 107 | n = read(sigfd, &ch, 1); if (n >= 0 || errno != EAGAIN) break; |
| 108 | FD_ZERO(&infd); FD_SET(sigfd, &infd); |
| 109 | n = select(sigfd + 1, &infd, 0, 0, 0); if (n < 0) break; |
| 110 | } |
| 111 | Py_END_ALLOW_THREADS |
| 112 | if (n == 1) rc = Py_BuildValue("(cOO)", ch, Py_None, Py_None); |
| 113 | else if (!n) rc = Py_BuildValue("(sOO)", "", Py_None, Py_None); |
| 114 | else if (n != -1) { |
| 115 | PyErr_SetString(PyExc_SystemError, "impossible return from read"); |
| 116 | goto end; |
| 117 | } else if (errno == EINTR || errno == EBADF) goto again; |
| 118 | else goto oserr; |
| 119 | |
| 120 | /* We're done. */ |
| 121 | goto end; |
| 122 | |
| 123 | oserr: |
| 124 | /* Report a system error. */ |
| 125 | PyErr_SetFromErrnoWithFilename(PyExc_OSError, 0); goto end; |
| 126 | |
| 127 | end: |
| 128 | /* Clean up and return. */ |
| 129 | if (rstrchld) { |
| 130 | sigprocmask(SIG_SETMASK, &oldmask, 0); |
| 131 | sigaction(SIGCHLD, &oldsa, 0); |
| 132 | } |
| 133 | sfd = sigfd; if (sfd != -1) { close(sfd); sigfd = -1; } |
| 134 | return (rc); |
| 135 | } |
| 136 | |
| 137 | static PyMethodDef methods[] = { |
| 138 | { "jobclient", pymeth_jobclient, METH_VARARGS, |
| 139 | "jobclient(FD) -> (TOKEN, PID, STATUS)" }, |
| 140 | { 0 } |
| 141 | }; |
| 142 | |
| 143 | PyMODINIT_FUNC initjobclient(void) |
| 144 | { Py_InitModule("jobclient", methods); } |
| 145 | |
| 146 | /*----- That's all, folks -------------------------------------------------*/ |