Add missing Revision keyword expansions, so that --version tells the truth.
[sgt/utils] / buildrun / buildrun.c
1 /*
2 * buildrun.c: wait to run one command until an instance of another
3 * completes successfully
4 */
5
6 #include <stdio.h>
7 #include <string.h>
8 #include <stdlib.h>
9 #include <errno.h>
10
11 #include <sys/types.h>
12 #include <sys/wait.h>
13 #include <sys/stat.h>
14 #include <fcntl.h>
15 #include <unistd.h>
16
17 /*
18 * Implementation strategy
19 * -----------------------
20 *
21 * The basic plan for making buildrun -r wait for buildrun -w is that
22 * buildrun -w creates a named pipe, opens it for writing, and does
23 * not actually write to it; then buildrun -r opens the same named
24 * pipe for reading. When buildrun -w terminates, anything which was
25 * trying to read from the pipe receives EOF, so buildrun -r can wake
26 * up and look to see if the run was successful.
27 *
28 * buildrun -w's report of success or failure is done by the very
29 * simple approach of deleting the pipe file before terminating, if
30 * the run was successful. So after buildrun -r receives EOF on the
31 * pipe, it immediately tries to open it again - and if it gets
32 * ENOENT, that's the success condition. Otherwise it reopens the pipe
33 * and blocks again until another buildrun -w succeeds.
34 *
35 * If the run was unsuccessful, buildrun -r must block immediately and
36 * stay blocked until buildrun -w next runs. This is achieved
37 * automatically by the above strategy, since attempting to open a
38 * pipe for reading when nothing is trying to write it will cause the
39 * open(2) call to block until something opens the pipe for writing.
40 *
41 * Likewise, if the last buildrun -w was unsuccessful and no -w run is
42 * active at the moment buildrun -r starts in the first place,
43 * buildrun -r must block until one starts up - and again, this is
44 * done automatically by the attempt to open the pipe for reading.
45 *
46 * Of course, the blocking open behaviour of named pipes works both
47 * ways: if buildrun -w tried to open the pipe for writing when no
48 * buildrun -r is trying to read it, _it_ would block. For that
49 * reason, buildrun -w actually opens with O_RDWR, so it can't block.
50 *
51 * Just in case the user tries to run two buildrun -w instances on the
52 * same control file, we also flock(LOCK_EX) the named pipe once we've
53 * opened it, so that the second -w instance can cleanly report the
54 * error condition.
55 *
56 * Finally, there is a small race condition in the mechanism I've just
57 * described. Suppose one buildrun -w instance finishes and deletes
58 * its pipe file - but before buildrun -r manages to unblock and
59 * notice the file's absence, another buildrun -w starts up and
60 * recreates it. This is a more or less theoretical risk in the
61 * interactive environments where I expect this tool to be used, but
62 * even so, I like to solve these problems where I can; what I do is
63 * to create not just a named pipe, but a _subdirectory_ containing a
64 * named pipe. So a successful buildrun -w deletes the pipe and
65 * removes the containing directory; a subsequent buildrun -w will
66 * create a fresh directory and pipe. Then buildrun -r starts by
67 * setting its _cwd_ to the directory containing the pipe - which
68 * means that if one buildrun -w finishes and another one starts,
69 * buildrun -r will still have its cwd pointing at the orphaned
70 * directory from the first, and won't accidentally find the pipe file
71 * created by the second in its own directory.
72 */
73
74 #define PIPEFILE "pipe"
75
76 char *ctldir, *pipepath, **command;
77
78 int writemode(void)
79 {
80 pid_t pid;
81 int fd, status;
82
83 if (mkdir(ctldir, 0700) < 0 && errno != EEXIST) {
84 fprintf(stderr, "buildrun: %s: mkdir: %s\n", ctldir, strerror(errno));
85 return 1;
86 }
87 if (mknod(pipepath, S_IFIFO | 0600, 0) < 0 && errno != EEXIST) {
88 fprintf(stderr, "buildrun: %s: mknod: %s\n", pipepath,strerror(errno));
89 return 1;
90 }
91 fd = open(pipepath, O_RDWR);
92 if (fd < 0) {
93 fprintf(stderr, "buildrun: %s: open: %s\n", pipepath, strerror(errno));
94 return 1;
95 }
96 if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
97 if (errno == EAGAIN) {
98 fprintf(stderr, "buildrun: another write process "
99 "is already active\n");
100 } else {
101 fprintf(stderr, "buildrun: %s: flock: %s\n",
102 pipepath, strerror(errno));
103 }
104 return 1;
105 }
106
107 pid = fork();
108 if (pid < 0) {
109 fprintf(stderr, "buildrun: fork: %s\n", strerror(errno));
110 return 1;
111 } else if (pid == 0) {
112 close(fd);
113 execvp(command[0], command);
114 fprintf(stderr, "buildrun: %s: exec: %s\n",
115 command[0], strerror(errno));
116 return 127;
117 }
118
119 while (1) {
120 if (waitpid(pid, &status, 0) < 0) {
121 fprintf(stderr, "buildrun: wait: %s\n", strerror(errno));
122 return 1;
123 }
124 if (WIFEXITED(status)) {
125 status = WEXITSTATUS(status);
126 break;
127 } else if (WIFSIGNALED(status)) {
128 status = 128 | WTERMSIG(status);
129 break;
130 }
131 }
132
133 if (status == 0) {
134 if (remove(pipepath) < 0) {
135 fprintf(stderr, "buildrun: %s: remove: %s\n", pipepath,
136 strerror(errno));
137 return 1;
138 }
139 if (rmdir(ctldir) < 0) {
140 fprintf(stderr, "buildrun: %s: rmdir: %s\n", ctldir,
141 strerror(errno));
142 return 1;
143 }
144 }
145
146 return status;
147 }
148
149 int readmode()
150 {
151 pid_t pid;
152 int fd, ret, status;
153 char buf[1];
154
155 /*
156 * Our strategy against race conditions relies on changing our cwd
157 * to the control dir, but that will confuse the user command if
158 * we exec one afterwards. So we do most of our work in a
159 * subprocess, and don't pollute our main process context.
160 */
161 pid = fork();
162 if (pid < 0) {
163 fprintf(stderr, "buildrun: fork: %s\n", strerror(errno));
164 return 1;
165 } else if (pid == 0) {
166 /*
167 * The main show.
168 */
169 if (chdir(ctldir) < 0) {
170 if (errno == ENOENT) {
171 /*
172 * If the control directory doesn't exist at all, it
173 * must be because the last -w run completed
174 * successfully, so we immediately return success.
175 */
176 return 0;
177 }
178 fprintf(stderr, "buildrun: %s: chdir: %s\n",
179 ctldir, strerror(errno));
180 return 1;
181 }
182 while (1) {
183 fd = open(PIPEFILE, O_RDONLY);
184 if (fd < 0) {
185 if (errno == ENOENT)
186 return 0; /* success! */
187
188 fprintf(stderr, "buildrun: %s: open: %s\n",
189 pipepath, strerror(errno));
190 return 1;
191 }
192
193 ret = read(fd, buf, 1);
194 if (ret < 0) {
195 fprintf(stderr, "buildrun: %s: read: %s\n",
196 pipepath, strerror(errno));
197 return 1;
198 } else if (ret > 0) {
199 fprintf(stderr, "buildrun: %s: read: unexpectedly received "
200 "some data!\n", pipepath, strerror(errno));
201 return 1;
202 }
203 close(fd);
204 }
205 }
206
207 while (1) {
208 if (waitpid(pid, &status, 0) < 0) {
209 fprintf(stderr, "buildrun: wait: %s\n", strerror(errno));
210 return 1;
211 }
212 if (WIFEXITED(status)) {
213 status = WEXITSTATUS(status);
214 break;
215 } else if (WIFSIGNALED(status)) {
216 status = 128 | WTERMSIG(status);
217 break;
218 }
219 }
220
221 if (status != 0) /* something went wrong in child */
222 return status;
223
224 /*
225 * In -r mode, running a command is optional; in the absence of
226 * one, we simply return true, so the user can invoke as 'buildrun
227 * -r file && complex shell command'.
228 */
229 if (!command)
230 return 0;
231
232 execvp(command[0], command);
233 fprintf(stderr, "buildrun: %s: exec: %s\n", command[0], strerror(errno));
234 return 127;
235 }
236
237 const char usagemsg[] =
238 "usage: buildrun -w <control_dir> <command> [<args>...]\n"
239 " or: buildrun -r <control_dir> [<command> [<args>...]]\n"
240 "where: -w write mode: run a build command\n"
241 " -r read mode: wait for a build command to succeed\n"
242 " <control_dir> directory to use for communication; suggest in /tmp\n"
243 " also: buildrun --version report version number\n"
244 " buildrun --help display this help text\n"
245 " buildrun --licence display the (MIT) licence text\n"
246 ;
247
248 void usage(void) {
249 fputs(usagemsg, stdout);
250 }
251
252 const char licencemsg[] =
253 "buildrun is copyright 2012 Simon Tatham.\n"
254 "\n"
255 "Permission is hereby granted, free of charge, to any person\n"
256 "obtaining a copy of this software and associated documentation files\n"
257 "(the \"Software\"), to deal in the Software without restriction,\n"
258 "including without limitation the rights to use, copy, modify, merge,\n"
259 "publish, distribute, sublicense, and/or sell copies of the Software,\n"
260 "and to permit persons to whom the Software is furnished to do so,\n"
261 "subject to the following conditions:\n"
262 "\n"
263 "The above copyright notice and this permission notice shall be\n"
264 "included in all copies or substantial portions of the Software.\n"
265 "\n"
266 "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n"
267 "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n"
268 "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n"
269 "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n"
270 "BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n"
271 "ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n"
272 "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
273 "SOFTWARE.\n"
274 ;
275
276 void licence(void) {
277 fputs(licencemsg, stdout);
278 }
279
280 void version(void) {
281 #define SVN_REV "$Revision$"
282 char rev[sizeof(SVN_REV)];
283 char *p, *q;
284
285 strcpy(rev, SVN_REV);
286
287 for (p = rev; *p && *p != ':'; p++);
288 if (*p) {
289 p++;
290 while (*p && isspace(*p)) p++;
291 for (q = p; *q && *q != '$'; q++);
292 if (*q) *q = '\0';
293 printf("buildrun revision %s\n", p);
294 } else {
295 printf("buildrun: unknown version\n");
296 }
297 }
298
299 int main(int argc, char **argv)
300 {
301 enum { UNSPEC, READ, WRITE } mode = UNSPEC;
302 int doing_opts = 1;
303
304 ctldir = NULL;
305 command = NULL;
306
307 while (--argc > 0) {
308 char *p = *++argv;
309 if (doing_opts && *p == '-') {
310 if (!strcmp(p, "-w")) {
311 mode = WRITE;
312 } else if (!strcmp(p, "-r")) {
313 mode = READ;
314 } else if (!strcmp(p, "--")) {
315 doing_opts = 0;
316 } else if (!strcmp(p, "--version")) {
317 version();
318 return 0;
319 } else if (!strcmp(p, "--help")) {
320 usage();
321 return 0;
322 } else if (!strcmp(p, "--licence") || !strcmp(p, "--license")) {
323 licence();
324 return 0;
325 } else {
326 fprintf(stderr, "buildrun: unrecognised option '%s'\n", p);
327 return 1;
328 }
329 } else {
330 if (!ctldir) {
331 ctldir = p;
332 } else {
333 command = argv;
334 break;
335 }
336 }
337 }
338
339 if (mode == UNSPEC) {
340 fprintf(stderr, "buildrun: expected -w or -r\n");
341 return 1;
342 }
343
344 if (!ctldir) {
345 fprintf(stderr, "buildrun: expected a control directory name\n");
346 return 1;
347 }
348
349 pipepath = malloc(strlen(ctldir) + strlen(PIPEFILE) + 2);
350 if (!pipepath) {
351 fprintf(stderr, "buildrun: out of memory\n");
352 return 1;
353 }
354 sprintf(pipepath, "%s/%s", ctldir, PIPEFILE);
355
356 if (mode == WRITE) {
357 return writemode();
358 } else if (mode == READ) {
359 return readmode();
360 }
361 }