31f0fd76 |
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) { |
6f739499 |
281 | #define SVN_REV "$Revision$" |
31f0fd76 |
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 | } |