1 /*
2 * Copyright (C) 2006-2019, Thomas Maier-Komor
3 *
4 * This is the source code of xjobs.
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include <errno.h>
21 #include <limits.h>
22 #include <math.h>
23 #include <signal.h>
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <sys/stat.h>
28 #include <sys/types.h>
29 #include <unistd.h>
30
31 #include "config.h"
32 #include "colortty.h"
33 #include "log.h"
34 #include "settings.h"
35 #include "support.h"
36 #include "version.h"
37
38 #ifndef SIGPOLL
39 #define SIGPOLL SIGIO
40 #endif
41
42 #ifdef __sun
43 #include <sys/stropts.h>
44 #endif
45
46 #ifdef __OpenBSD__
47 #include <sys/param.h>
48 #include <sys/sysctl.h>
49 #endif
50
51 /* SUSv3 does not have PATH_MAX */
52 #ifndef PATH_MAX
53 #define PATH_MAX _XOPEN_PATH_MAX
54 #endif
55
56 extern int yylex(void);
57
58 int (*gettoken)(void) = yylex;
59
60 flag_t ShowPID = flag_on, Echo = flag_on;
61 int Stdout = -1, Stderr = -1, Stdin = -1, Prompt = 0, ExitOnError = 0, RsrcUsage = 1
62 , Input = STDIN_FILENO, InFlags = 0;
63 unsigned QLen = 0, Lines = 1;
64 long Limit = 0, Pagesize;
65 const char *Script = 0;
66 char *Path = 0;
67
68 RETSIGTYPE processSignal(int sig);
69
70
parse_flag(const char * arg)71 static flag_t parse_flag(const char *arg)
72 {
73 if (0 == strcasecmp(arg,"true"))
74 return flag_on;
75 if (0 == strcasecmp(arg,"yes"))
76 return flag_on;
77 if (0 == strcasecmp(arg,"on"))
78 return flag_on;
79 if (0 == strcmp(arg,"1"))
80 return flag_on;
81 if (0 == strcasecmp(arg,"false"))
82 return flag_off;
83 if (0 == strcasecmp(arg,"no"))
84 return flag_off;
85 if (0 == strcasecmp(arg,"off"))
86 return flag_off;
87 if (0 == strcmp(arg,"0"))
88 return flag_off;
89 return flag_invalid;
90 }
91
92
set_color_mode(const char * m)93 static void set_color_mode(const char *m)
94 {
95 if (m == 0)
96 TtyMode = tty_auto;
97 else if (!strcasecmp(m,"auto"))
98 TtyMode = tty_auto;
99 else if (!strcasecmp(m,"pipe"))
100 TtyMode = tty_pipe;
101 else if (!strcasecmp(m,"ansi"))
102 TtyMode = tty_ansi;
103 else if (!strcasecmp(m,"off"))
104 TtyMode = tty_none;
105 else if (!strcasecmp(m,"none"))
106 TtyMode = tty_none;
107 else {
108 warn("invalid argument for color mode: '%s'; disabling color support\n",m);
109 TtyMode = tty_none;
110 return;
111 }
112 dbug("color mode set to '%s'\n",m);
113 }
114
115
parse_option_j(const char * arg)116 void parse_option_j(const char *arg)
117 {
118 double lf;
119 char f;
120 switch (sscanf(arg,"%lg%c",&lf,&f)) {
121 case 2:
122 if (f == 'x') {
123 #ifdef _SC_NPROCESSORS_ONLN
124 lf *= sysconf(_SC_NPROCESSORS_ONLN);
125 #elif defined(_SC_NPROCESSORS_CONF)
126 lf *= sysconf(_SC_NPROCESSORS_CONF);
127 #elif defined(__OpenBSD__)
128 int mib[2], nproc;
129 size_t len = sizeof(nproc);
130 mib[0] = CTL_HW;
131 mib[1] = HW_NCPU;
132 if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
133 warn("unable to determine number of processors: %s\nassuming 1 processors\n",strerror(errno));
134 nproc = 1;
135 }
136 lf *= nproc;
137 #else
138 warn("unable to determine number of processors, assuming 1\n");
139 lf *= 1;
140 #endif
141 } else
142 error("invalid suffix '%c' in argument for job limit setting\n",f);
143 /*FALLTHROUGH*/
144 case 1:
145 Limit = (long)ceil(lf);
146 if (Limit <= 0)
147 error("invalid argument for option -j\n");
148 dbug("maximum number of jobs set to %d\n",Limit);
149 break;
150 default:
151 error("missing argument to option -j\n");
152 }
153 }
154
155
read_config(const char * cf)156 void read_config(const char *cf)
157 {
158 struct stat st;
159 dbug("looking for config file %s\n",cf);
160 int fd = open(cf,O_RDONLY);
161 if (fd == -1) {
162 if (errno == ENOENT)
163 dbug("no config file %s\n",cf);
164 else
165 warn("unable to open config file %s: %s\n",cf,strerror(errno));
166 return;
167 }
168 if (-1 == fstat(fd,&st)) {
169 close(fd);
170 warn("unable to stat config file %s: %s\n",cf,strerror(errno));
171 return;
172 }
173 if (st.st_uid && (getuid() != st.st_uid)) {
174 close(fd);
175 warn("ignoring config file %s from different user\n",cf);
176 return;
177 }
178 if (st.st_size == 0) {
179 close(fd);
180 dbug("ignoring empty config file %s\n",cf);
181 return;
182 }
183 char *buf = Malloc(st.st_size);
184 if (-1 == read(fd,buf,st.st_size)) {
185 close(fd);
186 free(buf);
187 warn("unable to read config file %s: %s\n",cf,strerror(errno));
188 return;
189 }
190 close(fd);
191 dbug("parsing config file %s\n",cf);
192 char *line = buf;
193 while (line && (line-buf < st.st_size)) {
194 char key[64],value[256];
195 char *nl = strchr(line,'\n');
196 if (nl) {
197 *nl = 0;
198 ++nl;
199 }
200 char *pound = strchr(line,'#');
201 if (pound)
202 *pound = 0;
203 while ((*line == ' ') || (*line == '\t'))
204 ++line;
205 if (2 != sscanf(line,"%63[A-Za-z_.]%*[ \t=:]%255[0-9A-Za-z _/:$()-]",key,value)) {
206 line = nl;
207 continue;
208 }
209 dbug("parsing key/value pair %s=%s\n",key,value);
210 char *valuestr = value;
211 if (strchr(value,'$')) {
212 dbug("resolving %s\n",value);
213 valuestr = resolve_env(value);
214 }
215
216 if (strcasecmp(key,"showpid") == 0) {
217 flag_t f = parse_flag(valuestr);
218 if (f != flag_invalid)
219 ShowPID = f;
220 else
221 warn("ignoring invalid argument for key %s: %s\n",key,valuestr);
222 } else if (strcasecmp(key,"path") == 0) {
223 Path = valuestr;
224 dbug("PATH set to %s\n",Path);
225 line = nl;
226 continue; // needed so that valuestr will not be free()'ed
227 } else if (strcasecmp(key,"echo") == 0) {
228 flag_t f = parse_flag(valuestr);
229 if (f != flag_invalid)
230 Echo = f;
231 else
232 warn("ignoring invalid argument for key %s: %s\n",key,valuestr);
233 } else if (strcasecmp(key,"color.mode") == 0) {
234 set_color_mode(valuestr);
235 } else if (strcasecmp(key,"color.fail") == 0) {
236 color_t c = str2color(valuestr);
237 if (c != invalid_color)
238 ColorFail = c;
239 else
240 warn("ignoring invalid color %s\n",valuestr);
241 } else if (strcasecmp(key,"color.done") == 0) {
242 color_t c = str2color(valuestr);
243 if (c != invalid_color)
244 ColorDone = c;
245 else
246 warn("ignoring invalid color %s\n",valuestr);
247 } else if (strcasecmp(key,"color.debug") == 0) {
248 color_t c = str2color(valuestr);
249 if (c != invalid_color)
250 ColorDebug = c;
251 else
252 warn("ignoring invalid color %s\n",valuestr);
253 } else if (strcasecmp(key,"color.info") == 0) {
254 color_t c = str2color(valuestr);
255 if (c != invalid_color)
256 ColorInfo = c;
257 else
258 warn("ignoring invalid color %s\n",valuestr);
259 } else if (strcasecmp(key,"color.warn") == 0) {
260 color_t c = str2color(valuestr);
261 if (c != invalid_color)
262 ColorWarn = c;
263 else
264 warn("ignoring invalid color %s\n",valuestr);
265 } else if (strcasecmp(key,"color.out") == 0) {
266 color_t c = str2color(valuestr);
267 if (c != invalid_color)
268 ColorOut = c;
269 else
270 warn("ignoring invalid color %s\n",valuestr);
271 } else if (strcasecmp(key,"color.error") == 0) {
272 color_t c = str2color(valuestr);
273 if (c != invalid_color)
274 ColorError = c;
275 else
276 warn("ignoring invalid color %s\n",valuestr);
277 } else if (strcasecmp(key,"color.start") == 0) {
278 color_t c = str2color(valuestr);
279 if (c != invalid_color)
280 ColorStart = c;
281 else
282 warn("ignoring invalid color %s\n",valuestr);
283 } else if (strcasecmp(key,"jobs") == 0) {
284 parse_option_j(valuestr);
285 } else if (strcasecmp(key,"verbose") == 0) {
286 if ((valuestr[1] == 0) && (valuestr[0] >= '0') && (valuestr[0] <= '5'))
287 Verbose = (verbose_t)(valuestr[0] - '0');
288 else if (strcasecmp(valuestr,"silent") == 0)
289 Verbose = Silent;
290 else if (strcasecmp(valuestr,"error") == 0)
291 Verbose = Error;
292 else if (strcasecmp(valuestr,"warning") == 0)
293 Verbose = Warning;
294 else if (strcasecmp(valuestr,"status") == 0)
295 Verbose = Status;
296 else if (strcasecmp(valuestr,"info") == 0)
297 Verbose = Info;
298 else if (strcasecmp(valuestr,"debug") == 0)
299 Verbose = Debug;
300 else
301 warn("invalid argument '%s' to setting 'verbose'\n",valuestr);
302 } else {
303 warn("unknown key %s\n",key);
304 }
305 if (valuestr != value)
306 free(valuestr);
307 line = nl;
308 }
309 }
310
311
init_limit(void)312 void init_limit(void)
313 {
314 #ifdef _SC_NPROCESSORS_ONLN
315 Limit = sysconf(_SC_NPROCESSORS_ONLN);
316 #elif defined(_SC_NPROCESSORS_CONF)
317 Limit = sysconf(_SC_NPROCESSORS_CONF);
318 #elif defined(__OpenBSD__)
319 int mib[2], nproc;
320 size_t len = sizeof(nproc);
321 mib[0] = CTL_HW;
322 mib[1] = HW_NCPU;
323 if (-1 == sysctl(mib,2,&nproc,&len,0,0)) {
324 warn("unable to determine number of processors: %s\nassuming 1 processor\n",strerror(errno));
325 nproc = 1;
326 }
327 Limit = nproc;
328 #else
329 warn("unable to determine number of available processors - assuming 1 processor\n");
330 Limit = 1;
331 #endif
332 dbug("%d processors currently online (default job limit)\n",Limit);
333 }
334
335
init_defaults(const char * exe)336 void init_defaults(const char *exe)
337 {
338 long ps = sysconf(_SC_PAGESIZE);
339 if (ps == -1) {
340 warn("unable to determine page size: %s\n",strerror(errno));
341 Pagesize = 4096;
342 } else
343 Pagesize = ps;
344 char cfname[PATH_MAX+1];
345 if (0 != getcwd(cfname,sizeof(cfname))) {
346 size_t cl = strlen(cfname);
347 assert(cl < sizeof(cfname)-1);
348 if (cfname[cl-1] != '/') {
349 cfname[cl++] = '/';
350 cfname[cl] = 0;
351 }
352 if ((exe[0] == '.') && (exe[1] == '/'))
353 exe += 2;
354 size_t el = strlen(exe);
355 assert(cl+el < sizeof(cfname));
356 memcpy(cfname+cl,exe,el+1);
357 char *sl = strrchr(cfname,'/');
358 assert((sl-cfname)+17<sizeof(cfname));
359 memcpy(sl+1,"../etc/xjobs.rc",16);
360 read_config(cfname);
361 }
362 const char *home = getenv("HOME");
363 if (home) {
364 size_t hl = strlen(home);
365 assert(hl+11 <= sizeof(cfname));
366 memcpy(cfname,home,hl);
367 memcpy(cfname+hl,"/.xjobs.rc",11);
368 read_config(cfname);
369 }
370 }
371
372
open_script(const char * a)373 static void open_script(const char *a)
374 {
375 int ret;
376 struct stat st;
377
378 Input = open(a,O_RDONLY);
379 if (Input == -1)
380 error("could not open input file %s: %s\n",optarg,strerror(errno));
381 dbug("opening input script %s\n",optarg);
382 ret = fstat(Input, &st);
383 assert(ret != -1);
384 if (S_ISFIFO(st.st_mode)) {
385 struct sigaction sig;
386 dbug("input script is a named pipe\n");
387 sig.sa_handler = processSignal;
388 sigemptyset(&sig.sa_mask);
389 sigaddset(&sig.sa_mask,SIGPOLL);
390 sig.sa_flags = SA_RESTART;
391 ret = sigaction(SIGPOLL,&sig,0);
392 assert(ret == 0);
393 #ifndef __CYGWIN__
394 (void) open(optarg,O_WRONLY);
395 #endif
396 #ifndef __sun
397 ret = fcntl(Input,F_SETOWN,getpid());
398 if (ret != 0)
399 warn("unable to set owning process for SIGPIPE of named pipe %s: %s\n",optarg,strerror(errno));
400 #endif
401 ret = fcntl(Input,F_SETFL,O_RDONLY|O_NONBLOCK
402 #ifdef FASYNC
403 | FASYNC
404 #endif
405 );
406 assert(ret == 0);
407 #ifndef FASYNC
408 ret = ioctl(Input,I_SETSIG,S_RDNORM);
409 if (ret == -1)
410 error("unable to setup SIGPOLL: %s\n",strerror(errno));
411 #endif
412 InFlags = fcntl(Input,F_GETFL);
413 Script = optarg;
414 }
415 close_onexec(Input);
416 dbug("input file set to %s\n",optarg);
417 }
418
419
parse_options(int argc,char ** argv)420 void parse_options(int argc, char **argv)
421 {
422 int i;
423 while ((i = getopt(argc,argv,"01c:dehj:L:l:nNpq:rs:tv::V")) != -1) {
424 switch (i) {
425 default:
426 abort();
427 break;
428 case '0':
429 if (gettoken != yylex)
430 error("conflicting options -0 and -1");
431 dbug("set scanning mode to 1 argument ending on \\0\n");
432 gettoken = read_to_0;
433 break;
434 case '1':
435 if (gettoken != yylex)
436 error("conflicting options -0 and -1");
437 dbug("set scanning mode to 1 argument ending on \\n\n");
438 gettoken = read_to_nl;
439 break;
440 case 'c':
441 set_color_mode(optarg);
442 break;
443 case 'd':
444 dbug("stdout and stderr will be unbuffered\n");
445 Stdout = dup(STDOUT_FILENO);
446 Stderr = dup(STDERR_FILENO);
447 break;
448 case 'e':
449 dbug("user requests to exit on error\n");
450 ExitOnError = 1;
451 break;
452 case 'h':
453 usage();
454 break;
455 case 'j':
456 parse_option_j(optarg);
457 break;
458 case 'L':
459 set_log(optarg);
460 break;
461 case 'l':
462 if (1 == sscanf(optarg,"%u",&Lines) && (Lines > 0))
463 dbug("combining %u lines to a single command\n",Lines);
464 else
465 error("error in argument to option -l\n");
466 break;
467 case 'N':
468 Stdout = open("/dev/null",O_WRONLY);
469 if (Stdout == -1)
470 error("could not open /dev/null: %s\n",strerror(errno));
471 else
472 dbug("stdout and stderr redirected to /dev/null\n");
473 Stderr = Stdout;
474 break;
475 case 'n':
476 Stdout = open("/dev/null",O_WRONLY);
477 if (Stdout == -1)
478 error("could not open /dev/null: %s\n",strerror(errno));
479 else
480 dbug("stdout redirected to /dev/null\n");
481 break;
482 case 'p':
483 {
484 Prompt = 1;
485 dbug("enabling prompt mode\n");
486 } break;
487 case 'q':
488 {
489 unsigned tmp;
490 if (1 == sscanf(optarg,"%u",&tmp) && (tmp > 0)) {
491 QLen = tmp;
492 dbug("limiting queue length to %lu elements\n",QLen);
493 } else
494 error("error in argument to option -q\n");
495 }
496 break;
497 case 'r':
498 RsrcUsage = 0;
499 dbug("disabling display of resource usage\n");
500 break;
501 case 's':
502 open_script(optarg);
503 break;
504 case 't':
505 /* ignored for backward compatibility */
506 dbug("ignoring option -t for backward compatibility\n");
507 break;
508 case 'v':
509 /* must be done again after config file, so that
510 * the command line overrides the config file
511 */
512 if ((optarg != 0) && (optarg[0] >= '0') && (optarg[0] <= '5') && (optarg[1] == 0))
513 Verbose = optarg[0] - '0';
514 else
515 error("missing or invalid argument for option -v\n");
516 break;
517 case 'V':
518 version();
519 exit(0);
520 break;
521 case '?':
522 error("unknown option -%c\n",optopt);
523 break;
524 case ':':
525 error("option -%c requires an operand\n",optopt);
526 break;
527 }
528 }
529 }
530
531
version(void)532 void version(void)
533 {
534 (void) printf(
535 "xjobs version " VERSION "\n"
536 "Copyright 2006-2019, Thomas Maier-Komor\n"
537 "License: GPLv2\n"
538 "\n"
539 );
540 }
541
542
usage(void)543 void usage(void)
544 {
545 version();
546 (void) printf(
547 "synopsis:\n"
548 "xjobs [options] [command [option|argument ...]]\n"
549 "xjobs [options] -- [command [option|argument ...]]\n"
550 "\n"
551 "valid options are:\n"
552 "-h : print this usage information and exit\n"
553 "-L <log> : set log file to <log> (default: stderr)\n"
554 "-j <num> : maximum number of jobs to execute in parallel (default %ld)\n"
555 "-j <times>x : set maximum number based on available processors (e.g. 0.5x)\n"
556 "-s <file> : script to execute (default: read from stdin)\n"
557 "-l <num> : combine <num> lines to a single job\n"
558 "-n : redirect stdout of childs to /dev/null\n"
559 "-N : redirect stdout and stderr of childs to /dev/null\n"
560 "-d : direct unbuffered output of stdout and stderr\n"
561 "-v <level> : set verbosity to level\n"
562 " (0=silent,1=error,2=warning,3=info,4=status,5=debug)\n"
563 "-c <color> : set color mode (none/auto/pipe/ansi)\n"
564 "-p : prompt user, whether job should be started\n"
565 "-q <num> : limit queue to <num> entries\n"
566 "-t : print total time before exiting\n"
567 "-e : exit if a job terminates with an error\n"
568 "-0 : one argument per job terminated by a null-character\n"
569 "-1 : one argument per job terminated by a new-line\n"
570 #ifdef HAVE_WAIT4
571 "-r : omit display of resource usage\n"
572 #endif
573 "-- : last option to xjobs, following options are passed to jobs\n"
574 "-V : print version and exit\n"
575 , Limit
576 );
577 exit(0);
578 }
579
580
581