xref: /illumos-gate/usr/src/cmd/make/lib/mksh/dosys.cc (revision f2b348930f2f8a7df01ddf5e144f784d4ce31828)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 /*
22  * Copyright 2005 Sun Microsystems, Inc. All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 /*
27  * Copyright 2015, Joyent, Inc.
28  * Copyright 2019 RackTop Systems.
29  * Copyright 2026 Oxide Computer Company
30  */
31 
32 
33 /*
34  *	dosys.cc
35  *
36  *	Execute one commandline
37  */
38 
39 /*
40  * Included files
41  */
42 #include <sys/wait.h>			/* WIFEXITED(status) */
43 #include <alloca.h>		/* alloca() */
44 
45 #include <stdio.h>		/* errno */
46 #include <errno.h>		/* errno */
47 #include <fcntl.h>		/* open() */
48 #include <mksh/dosys.h>
49 #include <mksh/macro.h>		/* getvar() */
50 #include <mksh/misc.h>		/* getmem(), fatal_mksh(), errmsg() */
51 #include <sched.h>		/* sched_getparam() */
52 #include <spawn.h>		/* posix_spawn() */
53 #include <sys/stat.h>		/* open() */
54 #include <sys/wait.h>		/* wait() */
55 #include <ulimit.h>		/* ulimit() */
56 #include <unistd.h>		/* close(), dup2() */
57 #include <stdlib.h>		/* closefrom() */
58 #include <libintl.h>
59 
60 /*
61  * typedefs & structs
62  */
63 
64 /*
65  * Static variables
66  */
67 
68 /*
69  * File table of contents
70  */
71 static posix_spawnattr_t *nice_attr(posix_spawnattr_t *attr, int nice_prio);
72 static pid_t	spawn_vp(char *name, char **argv, char **envp, posix_spawnattr_t *attr, Boolean ignore_error, pathpt vroot_path);
73 
74 /*
75  * Workaround for NFS bug. Sometimes, when running 'open' on a remote
76  * dmake server, it fails with "Stale NFS file handle" error.
77  * The second attempt seems to work.
78  */
79 int
80 my_open(const char *path, int oflag, mode_t mode) {
81 	int res = open(path, oflag, mode);
82 	if (res < 0 && (errno == ESTALE || errno == EAGAIN)) {
83 		/* Stale NFS file handle. Try again */
84 		res = open(path, oflag, mode);
85 	}
86 	return res;
87 }
88 
89 /*
90  *	void
91  *	redirect_io(char *stdout_file, char *stderr_file)
92  *
93  *	Redirects stdout and stderr for a child mksh process.
94  */
95 void
96 redirect_io(char *stdout_file, char *stderr_file)
97 {
98 	int	i;
99 
100 	(void) closefrom(3);
101 	if ((i = my_open(stdout_file,
102 	         O_WRONLY | O_CREAT | O_TRUNC | O_DSYNC,
103 	         S_IREAD | S_IWRITE)) < 0) {
104 		fatal_mksh(gettext("Couldn't open standard out temp file `%s': %s"),
105 		      stdout_file,
106 		      errmsg(errno));
107 	} else {
108 		if (dup2(i, 1) == -1) {
109 			fatal_mksh("*** Error: dup2(3, 1) failed: %s",
110 				errmsg(errno));
111 		}
112 		close(i);
113 	}
114 	if (stderr_file == NULL) {
115 		if (dup2(1, 2) == -1) {
116 			fatal_mksh("*** Error: dup2(1, 2) failed: %s",
117 				errmsg(errno));
118 		}
119 	} else if ((i = my_open(stderr_file,
120 	                O_WRONLY | O_CREAT | O_TRUNC | O_DSYNC,
121 	                S_IREAD | S_IWRITE)) < 0) {
122 		fatal_mksh(gettext("Couldn't open standard error temp file `%s': %s"),
123 		      stderr_file,
124 		      errmsg(errno));
125 	} else {
126 		if (dup2(i, 2) == -1) {
127 			fatal_mksh("*** Error: dup2(3, 2) failed: %s",
128 				errmsg(errno));
129 		}
130 		close(i);
131 	}
132 }
133 
134 /*
135  *	nice_attr(attr, nice_prio)
136  *
137  *	Initialise spawn attributes that have the same effect as running
138  *	the child under "nice -<nice_prio>". The child starts at a
139  *	scheduling priority nice_prio below this process's current
140  *	priority, clamped to the range of its scheduling class.
141  *
142  *	Return value:
143  *				attr, initialised, or NULL if the current
144  *				scheduling parameters could not be determined,
145  *				in which case the child is spawned with the
146  *				priority it would inherit anyway.
147  *
148  *	Parameters:
149  *		attr		The spawn attributes to initialise
150  *		nice_prio	The priority decrement to apply
151  */
152 static posix_spawnattr_t *
153 nice_attr(posix_spawnattr_t *attr, int nice_prio)
154 {
155 	struct sched_param param;
156 	int policy, prio_min, prio_max;
157 
158 	if ((policy = sched_getscheduler(0)) == -1 ||
159 	    sched_getparam(0, &param) != 0) {
160 		return (NULL);
161 	}
162 
163 	prio_min = sched_get_priority_min(policy);
164 	prio_max = sched_get_priority_max(policy);
165 	if (prio_min == -1 || prio_max == -1)
166 		return (NULL);
167 
168 	param.sched_priority -= nice_prio;
169 	if (param.sched_priority < prio_min)
170 		param.sched_priority = prio_min;
171 	else if (param.sched_priority > prio_max)
172 		param.sched_priority = prio_max;
173 
174 	if (posix_spawnattr_init(attr) != 0)
175 		return (NULL);
176 	if (posix_spawnattr_setflags(attr, POSIX_SPAWN_SETSCHEDPARAM) != 0 ||
177 	    posix_spawnattr_setschedparam(attr, &param) != 0) {
178 		(void) posix_spawnattr_destroy(attr);
179 		return (NULL);
180 	}
181 
182 	return (attr);
183 }
184 
185 /*
186  *	doshell(command, ignore_error)
187  *
188  *	Used to run command lines that include shell meta-characters.
189  *	The make macro SHELL is supposed to contain a path to the shell.
190  *
191  *	Return value:
192  *				The pid of the process we started
193  *
194  *	Parameters:
195  *		command		The command to run
196  *		ignore_error	Should we abort on error?
197  *
198  *	Global variables used:
199  *		filter_stderr	If -X is on we redirect stderr
200  *		shell_name	The Name "SHELL", used to get the path to shell
201  */
202 pid_t
203 doshell(wchar_t *command, Boolean ignore_error, char *stdout_file, char *stderr_file, int nice_prio)
204 {
205 	char			*argv[4];
206 	int			argv_index = 0;
207 	posix_spawnattr_t	attr;
208 	posix_spawnattr_t	*attrp = NULL;
209 	int			cmd_argv_index;
210 	int			err;
211 	int			length;
212 	Name			shell = getvar(shell_name);
213 	char			*shellname;
214 	char			*tmp_mbs_buffer;
215 
216 
217 	if (IS_EQUAL(shell->string_mb, "")) {
218 		shell = shell_name;
219 	}
220 	if ((shellname = strrchr(shell->string_mb, (int) slash_char)) == NULL) {
221 		shellname = shell->string_mb;
222 	} else {
223 		shellname++;
224 	}
225 
226 	argv[argv_index++] = shellname;
227 	argv[argv_index++] = (char*)(ignore_error ? "-c" : "-ce");
228 	if ((length = wcslen(command)) >= MAXPATHLEN) {
229 		tmp_mbs_buffer = getmem((length * MB_LEN_MAX) + 1);
230                 (void) wcstombs(tmp_mbs_buffer, command, (length * MB_LEN_MAX) + 1);
231 		cmd_argv_index = argv_index;
232                 argv[argv_index++] = strdup(tmp_mbs_buffer);
233                 retmem_mb(tmp_mbs_buffer);
234 	} else {
235 		WCSTOMBS(mbs_buffer, command);
236 		cmd_argv_index = argv_index;
237 		argv[argv_index++] = strdup(mbs_buffer);
238 	}
239 	argv[argv_index] = NULL;
240 	(void) fflush(stdout);
241 
242 	if (nice_prio != 0)
243 		attrp = nice_attr(&attr, nice_prio);
244 
245 	/*
246 	 * The child needs no special signal handling. exec() resets any
247 	 * signals that make catches (such as its interrupt handlers) to
248 	 * the default, and dispositions that were ignored when make
249 	 * started have been left untouched.
250 	 */
251 	err = posix_spawn(&childPid, shell->string_mb, NULL, attrp,
252 	    argv, environ);
253 	if (err != 0) {
254 		(void) fprintf(stderr,
255 		    gettext("Could not load Shell from `%s': %s\n"),
256 		    shell->string_mb,
257 		    errmsg(err));
258 		childPid = -1;
259 	}
260 	if (attrp != NULL)
261 		(void) posix_spawnattr_destroy(attrp);
262 	retmem_mb(argv[cmd_argv_index]);
263 	return (childPid);
264 }
265 
266 /*
267  *	spawn_vp(name, argv, envp, attr, ignore_error)
268  *
269  *	Like posix_spawn, but does path search.
270  *	This starts command when make invokes it directly (without a shell).
271  *
272  *	Return value:
273  *				The pid of the process, or -1 on failure
274  *
275  *	Parameters:
276  *		name		The name of the command to run
277  *		argv		Arguments for the command
278  *		envp		The environment for it
279  *		attr		Spawn attributes for it, may be NULL
280  *		ignore_error	Should we abort on error?
281  *
282  *	Global variables used:
283  *		shell_name	The Name "SHELL", used to get the path to shell
284  *		vroot_path	The path used by the vroot package
285  */
286 static pid_t
287 spawn_vp(char *name, char **argv, char **envp, posix_spawnattr_t *attr, Boolean ignore_error, pathpt vroot_path)
288 {
289 	Name			shell = getvar(shell_name);
290 	char			*shellname;
291 	char			*shargv[4];
292 	Name			tmp_shell;
293 	pid_t			pid;
294 
295 	if (IS_EQUAL(shell->string_mb, "")) {
296 		shell = shell_name;
297 	}
298 
299 	for (int i = 0; i < 5; i++) {
300 		pid = spawn_vroot(name,
301 				  argv + 1,
302 				  envp,
303 				  attr,
304 				  vroot_path,
305 				  VROOT_DEFAULT);
306 		if (pid >= 0)
307 			return (pid);
308 		switch (errno) {
309 		case ENOEXEC:
310 		case ENOENT:
311 			/* That failed. Let the shell handle it */
312 			shellname = strrchr(shell->string_mb, (int) slash_char);
313 			if (shellname == NULL) {
314 				shellname = shell->string_mb;
315 			} else {
316 				shellname++;
317 			}
318 			shargv[0] = shellname;
319 			shargv[1] = (char*)(ignore_error ? "-c" : "-ce");
320 			shargv[2] = argv[0];
321 			shargv[3] = NULL;
322 			tmp_shell = getvar(shell_name);
323 			if (IS_EQUAL(tmp_shell->string_mb, "")) {
324 				tmp_shell = shell_name;
325 			}
326 			return (spawn_vroot(tmp_shell->string_mb,
327 					    shargv,
328 					    envp,
329 					    attr,
330 					    vroot_path,
331 					    VROOT_DEFAULT));
332 		case ETXTBSY:
333 			/*
334 			 * The program is busy (debugged?).
335 			 * Wait and then try again.
336 			 */
337 			(void) sleep((unsigned) i);
338 		case EAGAIN:
339 			break;
340 		default:
341 			return (-1);
342 		}
343 	}
344 	return (-1);
345 }
346 
347 /*
348  *	doexec(command, ignore_error)
349  *
350  *	Will scan an argument string and split it into words
351  *	thus building an argument list that can be passed to exec_ve()
352  *
353  *	Return value:
354  *				The pid of the process started here
355  *
356  *	Parameters:
357  *		command		The command to run
358  *		ignore_error	Should we abort on error?
359  *
360  *	Global variables used:
361  *		filter_stderr	If -X is on we redirect stderr
362  */
363 pid_t
364 doexec(wchar_t *command, Boolean ignore_error, char *stdout_file, char *stderr_file, pathpt vroot_path, int nice_prio)
365 {
366 	int			arg_count = 5;
367 	char			**argv;
368 	posix_spawnattr_t	attr;
369 	posix_spawnattr_t	*attrp = NULL;
370 	int			length;
371 	char			**p;
372 	wchar_t			*q;
373 	wchar_t			*t;
374 	char			*tmp_mbs_buffer;
375 
376 	for (t = command; *t != (int) nul_char; t++) {
377 		if (iswspace(*t)) {
378 			arg_count++;
379 		}
380 	}
381 	argv = (char **)alloca(arg_count * (sizeof(char *)));
382 	/* Reserve argv[0] for sh in case of spawn_vp failure */
383 	p = &argv[1];
384 	if ((length = wcslen(command)) >= MAXPATHLEN) {
385 		tmp_mbs_buffer = getmem((length * MB_LEN_MAX) + 1);
386 		(void) wcstombs(tmp_mbs_buffer, command, (length * MB_LEN_MAX) + 1);
387 		argv[0] = strdup(tmp_mbs_buffer);
388 		retmem_mb(tmp_mbs_buffer);
389         } else {
390 		WCSTOMBS(mbs_buffer, command);
391 		argv[0] = strdup(mbs_buffer);
392 	}
393 
394 	/* Build list of argument words. */
395 	for (t = command; *t;) {
396 		if (p >= &argv[arg_count]) {
397 			/* This should never happen, right? */
398 			WCSTOMBS(mbs_buffer, command);
399 			fatal_mksh(gettext("Command `%s' has more than %d arguments"),
400 			      mbs_buffer,
401 			      arg_count);
402 		}
403 		q = t;
404 		while (!iswspace(*t) && (*t != (int) nul_char)) {
405 			t++;
406 		}
407 		if (*t) {
408 			for (*t++ = (int) nul_char; iswspace(*t); t++);
409 		}
410 		if ((length = wcslen(q)) >= MAXPATHLEN) {
411 			tmp_mbs_buffer = getmem((length * MB_LEN_MAX) + 1);
412 			(void) wcstombs(tmp_mbs_buffer, q, (length * MB_LEN_MAX) + 1);
413 			*p++ = strdup(tmp_mbs_buffer);
414 			retmem_mb(tmp_mbs_buffer);
415 		} else {
416 			WCSTOMBS(mbs_buffer, q);
417 			*p++ = strdup(mbs_buffer);
418 		}
419 	}
420 	*p = NULL;
421 
422 	/* Then spawn the command with that argument list. */
423 	(void) fflush(stdout);
424 	if (nice_prio != 0)
425 		attrp = nice_attr(&attr, nice_prio);
426 	childPid = spawn_vp(argv[1], argv, environ, attrp, ignore_error,
427 	    vroot_path);
428 	if (childPid == -1) {
429 		(void) fprintf(stderr,
430 		    gettext("Cannot load command `%s': %s\n"),
431 		    argv[1], errmsg(errno));
432 	}
433 	if (attrp != NULL)
434 		(void) posix_spawnattr_destroy(attrp);
435 	for (int i = 0; argv[i] != NULL; i++) {
436 		retmem_mb(argv[i]);
437 	}
438 	return (childPid);
439 }
440 
441 /*
442  *	await(ignore_error, silent_error, target, command, running_pid)
443  *
444  *	Wait for one child process and analyzes
445  *	the returned status when the child process terminates.
446  *
447  *	Return value:
448  *				Returns true if commands ran OK
449  *
450  *	Parameters:
451  *		ignore_error	Should we abort on error?
452  *		silent_error	Should error messages be suppressed for dmake?
453  *		target		The target we are building, for error msgs
454  *		command		The command we ran, for error msgs
455  *		running_pid	The pid of the process we are waiting for
456  *
457  *	Static variables used:
458  *		filter_file	The fd for the filter file
459  *		filter_file_name The name of the filter file
460  *
461  *	Global variables used:
462  *		filter_stderr	Set if -X is on
463  */
464 Boolean
465 await(Boolean ignore_error, Boolean silent_error, Name target, wchar_t *command, pid_t running_pid, void *xdrs_p, int job_msg_id)
466 {
467         int                     status;
468 	char			*buffer;
469 	int			core_dumped;
470 	int			exit_status;
471 	FILE			*outfp;
472 	pid_t			pid;
473 	struct stat		stat_buff;
474 	int			termination_signal;
475 
476 	if (running_pid < 0) {
477 		/*
478 		 * The command could not be spawned and an error will
479 		 * already have been printed. Report it as a command that
480 		 * failed.
481 		 */
482 		exit_status = 1;
483 		termination_signal = 0;
484 		core_dumped = 0;
485 	} else {
486 		while ((pid = wait(&status)) != running_pid) {
487 			if (pid == -1) {
488 				fatal_mksh(gettext("wait() failed: %s"),
489 				    errmsg(errno));
490 			}
491 		}
492 		(void) fflush(stdout);
493 		(void) fflush(stderr);
494 
495 		if (status == 0) {
496 
497 #ifdef PRINT_EXIT_STATUS
498 			warning_mksh("I'm in await(), and status is 0.");
499 #endif
500 
501 			return (succeeded);
502 		}
503 
504 #ifdef PRINT_EXIT_STATUS
505 		warning_mksh("I'm in await(), and status is *NOT* 0.");
506 #endif
507 
508 
509 		exit_status = WEXITSTATUS(status);
510 
511 #ifdef PRINT_EXIT_STATUS
512 		warning_mksh("I'm in await(), and exit_status is %d.",
513 		    exit_status);
514 #endif
515 
516 		termination_signal = WTERMSIG(status);
517 		core_dumped = WCOREDUMP(status);
518 	}
519 
520 	/*
521 	 * If the child returned an error, we now try to print a
522 	 * nice message about it.
523 	 */
524 
525 	if (!silent_error) {
526 		if (exit_status != 0) {
527 			(void) fprintf(stdout,
528 				       gettext("*** Error code %d"),
529 				       exit_status);
530 		} else {
531 				(void) fprintf(stdout,
532 					       gettext("*** Signal %d"),
533 					       termination_signal);
534 			if (core_dumped) {
535 				(void) fprintf(stdout,
536 					       gettext(" - core dumped"));
537 			}
538 		}
539 		if (ignore_error) {
540 			(void) fprintf(stdout,
541 				       gettext(" (ignored)"));
542 		}
543 		(void) fprintf(stdout, "\n");
544 		(void) fflush(stdout);
545 	}
546 
547 #ifdef PRINT_EXIT_STATUS
548 	warning_mksh("I'm in await(), returning failed.");
549 #endif
550 
551 	return failed;
552 }
553 
554 /*
555  *	sh_command2string(command, destination)
556  *
557  *	Run one sh command and capture the output from it.
558  *
559  *	Return value:
560  *
561  *	Parameters:
562  *		command		The command to run
563  *		destination	Where to deposit the output from the command
564  *
565  *	Static variables used:
566  *
567  *	Global variables used:
568  */
569 void
570 sh_command2string(String command, String destination)
571 {
572 	FILE		*fd;
573 	int		chr;
574 	int		status;
575 	Boolean		command_generated_output = false;
576 
577 	command->text.p = NULL;
578 	WCSTOMBS(mbs_buffer, command->buffer.start);
579 	if ((fd = popen(mbs_buffer, "r")) == NULL) {
580 		WCSTOMBS(mbs_buffer, command->buffer.start);
581 		fatal_mksh(gettext("Could not run command `%s' for :sh transformation"),
582 		      mbs_buffer);
583 	}
584 	while ((chr = getc(fd)) != EOF) {
585 		if (chr == (int) newline_char) {
586 			chr = (int) space_char;
587 		}
588 		command_generated_output = true;
589 		append_char(chr, destination);
590 	}
591 
592 	/*
593 	 * We don't want to keep the last LINE_FEED since usually
594 	 * the output of the 'sh:' command is used to evaluate
595 	 * some MACRO. ( /bin/sh and other shell add a line feed
596 	 * to the output so that the prompt appear in the right place.
597 	 * We don't need that
598 	 */
599 	if (command_generated_output){
600 		if ( *(destination->text.p-1) == (int) space_char) {
601 			* (-- destination->text.p) = '\0';
602 		}
603 	} else {
604 		/*
605 		 * If the command didn't generate any output,
606 		 * set the buffer to a null string.
607 		 */
608 		*(destination->text.p) = '\0';
609 	}
610 
611 	status = pclose(fd);
612 	if (status != 0) {
613 		WCSTOMBS(mbs_buffer, command->buffer.start);
614 		fatal_mksh(gettext("The command `%s' returned status `%d'"),
615 		      mbs_buffer,
616 		      WEXITSTATUS(status));
617 	}
618 }
619 
620 
621