├── Makefile ├── README.md └── timebox.c /Makefile: -------------------------------------------------------------------------------- 1 | PROGRAM = timebox 2 | 3 | 4 | $(PROGRAM) : $(PROGRAM).c 5 | $(CC) $(CFLAGS) -o $@ $^ 6 | 7 | clean : 8 | rm -f $(PROGRAM) 9 | 10 | 11 | .PHONY : clean 12 | 13 | .DELETE_ON_ERROR : 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # timebox 2 | ## Politely timebox unix programs 3 | 4 | [Timeboxing](https://en.wikipedia.org/wiki/Timeboxing) is a time management technique. Some humans [find it beneficial](https://spin.atomicobject.com/2014/05/03/timeboxing-mitigate-risk/) to start with a fixed time period and size their work to fit. Can machines practice timeboxing too? Sure! That's kind of what an [RTOS does](https://en.wikipedia.org/wiki/Real-time_operating_system). If you want to timebox a regular old unix program though, you'll need something else... 5 | 6 | ## Silly Example 7 | 8 | See how many files can be counted under your home directory in 5-6 seconds: 9 | 10 | ```sh 11 | time timebox 5.0 1.0 find ~ | wc -l 12 | ``` 13 | 14 | The two numbers `5.0` and `1.0` are the run period and grace period, respectively. If the program doesn't exit before 5 seconds is up, `timebox` sends it `SIGTERM`. If it still hasn't exited after 6 seconds, `timebox` sends it `SIGKILL`. This is not unlike what `init` does with running processes at shutdown. 15 | 16 | ## Other Applications 17 | 18 | - Problems that have "best effort" solutions, like search. Exhaustive searches can take a long time. Perhaps you always want to respond with _something_ within a second. In your search program, print the best result you've found so far when you receive `SIGTERM`. You can then run it under `timebox`. 19 | 20 | - Suppose you're a cost-constrained cloud user being charged for cpu time. (I mean, who isn't these days?) You want to ~crack a hash~ solve a problem, but can't spend more than $X, even if that means you might not get a solution at all. Timebox it! 21 | 22 | - College programming assignments where you a submit source code solution. A grading program compiles, runs, and checks the ouput of your program. But what if your program is naughty and never exits? Or what if you submit a small C++ program which [generates GBs of compiler errors](https://tgceec.tumblr.com/post/74534916370/results-of-the-grand-c-error-explosion?is_related_post=1) that take forever to format and print? Timebox it! 23 | 24 | - You want to expose a server, but only for a limited, short period of time. Forgetting and leaving it open indefinitely would be risky. Timebox it! 25 | 26 | ## Okay But Why 27 | 28 | This was inspired by ["Unix System Call Timeouts"](https://eklitzke.org/unix-system-call-timeouts). Specifically, the ["waitpid equivalent with timeout?"](http://stackoverflow.com/questions/282176/waitpid-equivalent-with-timeout/290025) question on Stack Overflow. There are all kinds of ways to do this... 29 | 30 | ### SIGALRM 31 | 32 | Set an `alarm` and wait for `SIGALRM`. This is hard to get right, and you need to watch out for [race conditions](http://docstore.mik.ua/orelly/perl4/cook/ch16_22.htm). 33 | 34 | ### signalfd 35 | 36 | Turn `SIGCHLD` into a selectable event via `signalfd`. Only works on Linux. 37 | 38 | ### self-pipe 39 | 40 | Use [djb's self-pipe trick](https://cr.yp.to/docs/selfpipe.html) to turn `SIGCHLD` into a selectable event. This is sort of the poor man's `signalfd`, and is probably the most reliable and broadly portable way to do it. Although I wonder if the "can't safely mix select with signals" reason still applies on modern unixes. 41 | 42 | ### sigtimedwait 43 | 44 | Use `sigtimedwait` with a signal set of `SIGCHLD`. This is a nice, simple approach, but wouldn't mix well with an event loop doing other things. Not that we're doing that here, but it might be relevant in _your_ program. 45 | 46 | ### EOF on child-to-parent pipe 47 | 48 | `timebox` uses yet another way: wait for `EOF` on a child-to-parent pipe, which happens when the pipe's write side is closed at child exit. 49 | 50 | -------------------------------------------------------------------------------- /timebox.c: -------------------------------------------------------------------------------- 1 | /* 2 | * timebox: run a program for at most M + N seconds. 3 | * 4 | * M = running period 5 | * N = grace period (responding to SIGTERM) 6 | * 7 | * Copyright Alan Grow 2017. 8 | * This is free and unencumbered software released into the public domain. 9 | * 10 | */ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | void sig_catch(int sig, void (*f)()); 24 | void sig_uncatch(int sig); 25 | void die(int code, const char *message); 26 | void sysdie(const char *message); 27 | 28 | int done_running = 0; 29 | int done_gracing = 0; 30 | 31 | void sigint() { done_gracing = done_running = 1; } 32 | void sigchld() { done_gracing = 1; } 33 | 34 | int main(int argc, char **argv) 35 | { 36 | // Process arguments. 37 | 38 | if (argc < 4) die(1, "usage: timebox TIMEOUT GRACE program arg1 arg2 ..."); 39 | 40 | char *argv0 = *argv++; argc--; 41 | double timeout_argument = atof(*argv++); argc--; 42 | double grace_argument = atof(*argv++); argc--; 43 | 44 | if (timeout_argument <= 0) die(1, "invalid TIMEOUT"); 45 | if (grace_argument < 0) die(1, "invalid GRACE"); 46 | 47 | // We work with timevals throughout, convert from doubles. 48 | 49 | struct timeval timeout; 50 | struct timeval grace; 51 | 52 | timeout.tv_sec = (time_t)timeout_argument; 53 | timeout.tv_usec = (suseconds_t)(1e6 * (timeout_argument - timeout.tv_sec)); 54 | grace.tv_sec = (time_t)grace_argument; 55 | grace.tv_usec = (suseconds_t)(1e6 * (grace_argument - grace.tv_sec)); 56 | 57 | // Create the child-to-parent pipe. 58 | 59 | int pipefds[2]; 60 | if (pipe(pipefds) < 0) sysdie("pipe error"); 61 | 62 | // SIGINT: initiate shutdown without any more delays. 63 | // SIGCHLD: child has exited, no grace period delay needed. 64 | 65 | sig_catch(SIGINT, sigint); 66 | sig_catch(SIGCHLD, sigchld); 67 | 68 | // Fork and run the child program. 69 | 70 | pid_t pid; 71 | switch ((pid = fork())) { 72 | case -1: 73 | sysdie("fork error"); break; 74 | case 0: // Child, close pipe read side and run tail program. 75 | sig_uncatch(SIGINT); 76 | sig_uncatch(SIGCHLD); 77 | if (close(pipefds[0]) < 0) sysdie("close error"); 78 | if (execvp(*argv, argv) < 0) sysdie("exec error"); 79 | // Never reached. 80 | break; 81 | default: // Parent, continue with this program. 82 | break; 83 | } 84 | 85 | // Close pipe write side. 86 | 87 | if (close(pipefds[1]) < 0) sysdie("close error"); 88 | 89 | // Calculate absolute deadlines for running and grace periods. 90 | 91 | struct timeval started; 92 | struct timeval deadline_running; 93 | struct timeval deadline_gracing; 94 | 95 | if (gettimeofday(&started, 0) < 0) sysdie("gettimeofday error"); 96 | timeradd(&started, &timeout, &deadline_running); 97 | timeradd(&deadline_running, &grace, &deadline_gracing); 98 | 99 | // Running period. Wait until one of these occurs: 100 | // 1. The deadline has passed. 101 | // 2. The deadline timeout elapses. 102 | // 3. The child exits, triggering a readable zero byte at eof. 103 | 104 | while (!done_running) 105 | { 106 | // Recalculate remaining time. 107 | 108 | struct timeval now; 109 | struct timeval remaining; 110 | if (gettimeofday(&now, 0) < 0) sysdie("gettimeofday error"); 111 | timersub(&deadline_running, &now, &remaining); 112 | 113 | // Done if deadline has passed (1). 114 | 115 | if (remaining.tv_sec < 0) { 116 | done_running = 1; 117 | break; 118 | } 119 | 120 | // Wait for (2) or (3). 121 | 122 | fd_set readable; 123 | FD_ZERO(&readable); 124 | FD_SET(pipefds[0], &readable); 125 | int rc = select(pipefds[0]+1, &readable, 0, 0, &remaining); 126 | // Done if timeout elapses (2). 127 | if (rc == 0) done_running = 1; 128 | if (done_running) break; 129 | if (rc < 0 && (errno == EINTR || errno == EAGAIN)) continue; 130 | if (rc < 0) sysdie("select error"); 131 | 132 | // The child-to-parent pipe is readable, so try to read a byte. 133 | 134 | char c; 135 | ssize_t bytes = read(pipefds[0], &c, 1); 136 | // Prevent child keeping parent busy by writing to pipe. 137 | if (bytes > 0) die(101, "child wrote to pipe"); 138 | // Done if we read zero bytes at eof (3). 139 | if (!bytes) done_running = 1; 140 | if (done_running) break; 141 | if (bytes < 0) sysdie("read error"); 142 | } 143 | 144 | // Grace period. Send TERM to child and wait for deadline to elapse. 145 | 146 | kill(pid, SIGTERM); 147 | kill(pid, SIGCONT); 148 | 149 | while (!done_gracing) 150 | { 151 | // Recalculate remaining time. 152 | 153 | struct timeval now; 154 | struct timeval remaining; 155 | if (gettimeofday(&now, 0) < 0) sysdie("gettimeofday error"); 156 | timersub(&deadline_gracing, &now, &remaining); 157 | 158 | // Done if deadline has passed. 159 | 160 | if (remaining.tv_sec < 0) { 161 | done_gracing = 1; 162 | break; 163 | } 164 | 165 | // Sleep until deadline. 166 | 167 | int rc = select(0, 0, 0, 0, &remaining); 168 | if (rc == 0) done_gracing = 1; 169 | if (done_gracing) break; 170 | if (rc < 0 && (errno == EINTR || errno == EAGAIN)) continue; 171 | if (rc < 0) sysdie("select error"); 172 | } 173 | 174 | // Finally, force kill child in case it's not responding. 175 | 176 | kill(pid, SIGKILL); 177 | 178 | // Collect child status and bubble up its exit code. 179 | 180 | int status; 181 | if (waitpid(pid, &status, 0) < 0) sysdie("waitpid error"); 182 | return WEXITSTATUS(status); 183 | } 184 | 185 | void sig_catch(int sig, void (*f)()) 186 | { 187 | struct sigaction sa; 188 | sa.sa_handler = f; 189 | sa.sa_flags = 0; 190 | sigemptyset(&sa.sa_mask); 191 | sigaction(sig,&sa,(struct sigaction *) 0); 192 | } 193 | 194 | void sig_uncatch(int sig) 195 | { 196 | struct sigaction sa; 197 | sa.sa_handler = SIG_IGN; 198 | sa.sa_flags = 0; 199 | sigemptyset(&sa.sa_mask); 200 | sigaction(sig,&sa,(struct sigaction *) 0); 201 | } 202 | 203 | void die(int code, const char *message) 204 | { 205 | fprintf(stderr, "%s\n", message); 206 | _exit(100); 207 | } 208 | 209 | void sysdie(const char *message) 210 | { 211 | fprintf(stderr, "%s: %s\n", message, strerror(errno)); 212 | _exit(errno & 0x7f); 213 | } 214 | 215 | --------------------------------------------------------------------------------