chiark / gitweb /
src/jobclient.c: Low-level magic for GNU Make's jobserver protocol.
[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 <unistd.h>
37
38 #define PY_SSIZE_T_CLEAN
39 #include <Python.h>
40
41 /*----- Static variables --------------------------------------------------*/
42
43 static volatile int sigfd = -1;
44
45 /*----- Main code ---------------------------------------------------------*/
46
47 static void chld(int sig)
48   { int fd = sigfd; if (fd != -1) { close(fd); sigfd = -1; } }
49
50 static PyObject *pymeth_jobclient(PyObject *me, PyObject *arg)
51 {
52   PyObject *rc = 0;
53   struct sigaction sa, oldsa;
54   sigset_t mask, oldmask;
55   int rstrchld = 0;
56   int fd, sfd;
57   int kid, st;
58   ssize_t n;
59   char ch;
60
61   /* Parse the arguments. */
62   if (!PyArg_ParseTuple(arg, "i", &fd)) goto end;
63
64   /* Prepare to be able to block `SIGCHLD'. */
65   sigemptyset(&mask); sigaddset(&mask, SIGCHLD);
66   if (sigprocmask(SIG_SETMASK, 0, &oldmask)) goto oserr;
67
68   /* Establish the signal handler. */
69   sa.sa_handler = chld;
70   sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGCHLD);
71   sa.sa_flags = SA_NOCLDSTOP;
72   if (sigaction(SIGCHLD, &sa, &oldsa)) goto oserr;
73   rstrchld = 1;
74
75   /* Establish a copy of the file descriptor.  If a signal occurs, we'll
76    * clobber this descriptor: see below.  If we come back here, we'll
77    * probably need to reinstate this, but check just in case: we don't want a
78    * descriptor leak.
79    */
80 again:
81   if (sigprocmask(SIG_BLOCK, &mask, 0)) goto oserr;
82   sfd = sigfd;
83   if (sfd == -1) {
84     sfd = dup(fd); if (sfd == -1) goto oserr;
85     sigfd = sfd;
86   }
87   if (sigprocmask(SIG_UNBLOCK, &mask, 0)) goto oserr;
88
89   /* Check for child processes.  If a child exit somehow failed to be
90    * spotted earlier, this is our chance to notice and do something about it.
91    */
92   kid = waitpid(-1, &st, WNOHANG);
93   if (kid == -1 && errno != ECHILD) goto oserr;
94   else if (kid > 0)
95     { rc = Py_BuildValue("(Oii)", Py_None, kid, st); goto end; }
96
97   /* Read from the pipe.  If a child exits between the `waitpid' above and
98    * the `read' here, the signal handler will pop and close the descriptor,
99    * so `read' will report `EBADF'.  If a child exits while we're waiting for
100    * a byte on the descriptor, then `read' will report `EINTR'.  If anything
101    * like this happens, then we go back and try reaping children again.
102    */
103   Py_BEGIN_ALLOW_THREADS
104   n = read(sigfd, &ch, 1);
105   Py_END_ALLOW_THREADS
106   if (n == 1) rc = Py_BuildValue("(cOO)", ch, Py_None, Py_None);
107   else if (!n) rc = Py_BuildValue("(sOO)", "", Py_None, Py_None);
108   else if (n != -1) {
109     PyErr_SetString(PyExc_SystemError, "impossible return from read");
110     goto end;
111   } else if (errno == EINTR || errno == EBADF) goto again;
112   else goto oserr;
113
114   /* We're done. */
115   goto end;
116
117 oserr:
118   /* Report a system error. */
119   PyErr_SetFromErrnoWithFilename(PyExc_OSError, 0); goto end;
120
121 end:
122   /* Clean up and return. */
123   if (rstrchld) {
124     sigprocmask(SIG_SETMASK, &oldmask, 0);
125     sigaction(SIGCHLD, &oldsa, 0);
126   }
127   sfd = sigfd; if (sfd != -1) close(sfd);
128   return (rc);
129 }
130
131 static PyMethodDef methods[] = {
132   { "jobclient", pymeth_jobclient, METH_VARARGS,
133     "jobclient(FD) -> (TOKEN, PID, STATUS)" },
134   { 0 }
135 };
136
137 PyMODINIT_FUNC initjobclient(void)
138   { Py_InitModule("jobclient", methods); }
139
140 /*----- That's all, folks -------------------------------------------------*/