chiark / gitweb /
src/jobclient.c: Cope if the jobserver pipe is set nonblocking.
[distorted-chroot] / src / jobclient.c
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 -------------------------------------------------*/