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