/* * Copyright 1993, 1995 Christopher Seiwald. * Copyright 2007 Noel Belcourt. * * This file is part of Jam - see jam.c for Copyright information. */ #include "jam.h" #include "lists.h" #include "execcmd.h" #include "output.h" #include #include #include #include #include /* needed for vfork(), _exit() prototypes */ #include #include #include #if defined(sun) || defined(__sun) || defined(linux) #include #endif #ifdef USE_EXECUNIX #include #if defined(__APPLE__) #define NO_VFORK #endif #ifdef NO_VFORK #define vfork() fork() #endif /* * execunix.c - execute a shell script on UNIX/WinNT/OS2/AmigaOS * * If $(JAMSHELL) is defined, uses that to formulate execvp()/spawnvp(). * The default is: * * /bin/sh -c % [ on UNIX/AmigaOS ] * cmd.exe /c % [ on OS2/WinNT ] * * Each word must be an individual element in a jam variable value. * * In $(JAMSHELL), % expands to the command string and ! expands to the slot * number (starting at 1) for multiprocess (-j) invocations. If $(JAMSHELL) does * not include a %, it is tacked on as the last argument. * * Do not just set JAMSHELL to /bin/sh or cmd.exe - it will not work! * * External routines: * exec_cmd() - launch an async command execution. * exec_wait() - wait and drive at most one execution completion. * * Internal routines: * onintr() - bump intr to note command interruption. * * 04/08/94 (seiwald) - Coherent/386 support added. * 05/04/94 (seiwald) - async multiprocess interface * 01/22/95 (seiwald) - $(JAMSHELL) support * 06/02/97 (gsar) - full async multiprocess support for Win32 */ static clock_t tps = 0; static struct timeval tv; static int select_timeout = 0; static int intr = 0; static int cmdsrunning = 0; static struct tms old_time; #define OUT 0 #define ERR 1 static struct { int pid; /* on win32, a real process handle */ int fd[2]; /* file descriptors for stdout and stderr */ FILE *stream[2]; /* child's stdout (0) and stderr (1) file stream */ clock_t start_time; /* start time of child process */ int exit_reason; /* termination status */ int action_length; /* length of action string */ int target_length; /* length of target string */ char *action; /* buffer to hold action and target invoked */ char *target; /* buffer to hold action and target invoked */ char *command; /* buffer to hold command being invoked */ char *buffer[2]; /* buffer to hold stdout and stderr, if any */ void (*func)( void *closure, int status, timing_info*, char *, char * ); void *closure; time_t start_dt; /* start of command timestamp */ } cmdtab[ MAXJOBS ] = {{0}}; /* * onintr() - bump intr to note command interruption */ void onintr( int disp ) { ++intr; printf( "...interrupted\n" ); } /* * exec_cmd() - launch an async command execution. */ void exec_cmd ( char * string, void (*func)( void *closure, int status, timing_info*, char *, char * ), void * closure, LIST * shell, char * action, char * target ) { static int initialized = 0; int out[2]; int err[2]; int slot; int len; char * argv[ MAXARGC + 1 ]; /* +1 for NULL */ /* Find a slot in the running commands table for this one. */ for ( slot = 0; slot < MAXJOBS; ++slot ) if ( !cmdtab[ slot ].pid ) break; if ( slot == MAXJOBS ) { printf( "no slots for child!\n" ); exit( EXITBAD ); } /* Forumulate argv. If shell was defined, be prepared for % and ! subs. * Otherwise, use stock /bin/sh on unix or cmd.exe on NT. */ if ( shell ) { int i; char jobno[4]; int gotpercent = 0; sprintf( jobno, "%d", slot + 1 ); for ( i = 0; shell && i < MAXARGC; ++i, shell = list_next( shell ) ) { switch ( shell->string[0] ) { case '%': argv[ i ] = string; ++gotpercent; break; case '!': argv[ i ] = jobno; break; default : argv[ i ] = shell->string; } if ( DEBUG_EXECCMD ) printf( "argv[%d] = '%s'\n", i, argv[ i ] ); } if ( !gotpercent ) argv[ i++ ] = string; argv[ i ] = 0; } else { argv[ 0 ] = "/bin/sh"; argv[ 1 ] = "-c"; argv[ 2 ] = string; argv[ 3 ] = 0; } /* Increment jobs running. */ ++cmdsrunning; /* Save off actual command string. */ cmdtab[ slot ].command = BJAM_MALLOC_ATOMIC( strlen( string ) + 1 ); strcpy( cmdtab[ slot ].command, string ); /* Initialize only once. */ if ( !initialized ) { times( &old_time ); initialized = 1; } /* Create pipes from child to parent. */ { if ( pipe( out ) < 0 ) exit( EXITBAD ); if ( pipe( err ) < 0 ) exit( EXITBAD ); } /* Start the command */ cmdtab[ slot ].start_dt = time(0); if ( 0 < globs.timeout ) { /* * Handle hung processes by manually tracking elapsed time and signal * process when time limit expires. */ struct tms buf; cmdtab[ slot ].start_time = times( &buf ); /* Make a global, only do this once. */ if ( tps == 0 ) tps = sysconf( _SC_CLK_TCK ); } if ( ( cmdtab[ slot ].pid = vfork() ) == 0 ) { int pid = getpid(); close( out[0] ); close( err[0] ); dup2( out[1], STDOUT_FILENO ); if ( globs.pipe_action == 0 ) dup2( out[1], STDERR_FILENO ); else dup2( err[1], STDERR_FILENO ); close( out[1] ); close( err[1] ); /* Make this process a process group leader so that when we kill it, all * child processes of this process are terminated as well. We use * killpg(pid, SIGKILL) to kill the process group leader and all its * children. */ if ( 0 < globs.timeout ) { struct rlimit r_limit; r_limit.rlim_cur = globs.timeout; r_limit.rlim_max = globs.timeout; setrlimit( RLIMIT_CPU, &r_limit ); } setpgid( pid,pid ); execvp( argv[0], argv ); perror( "execvp" ); _exit( 127 ); } else if ( cmdtab[ slot ].pid == -1 ) { perror( "vfork" ); exit( EXITBAD ); } setpgid( cmdtab[ slot ].pid, cmdtab[ slot ].pid ); /* close write end of pipes */ close( out[1] ); close( err[1] ); /* set both file descriptors to non-blocking */ fcntl(out[0], F_SETFL, O_NONBLOCK); fcntl(err[0], F_SETFL, O_NONBLOCK); /* child writes stdout to out[1], parent reads from out[0] */ cmdtab[ slot ].fd[ OUT ] = out[0]; cmdtab[ slot ].stream[ OUT ] = fdopen( cmdtab[ slot ].fd[ OUT ], "rb" ); if ( cmdtab[ slot ].stream[ OUT ] == NULL ) { perror( "fdopen" ); exit( EXITBAD ); } /* child writes stderr to err[1], parent reads from err[0] */ if (globs.pipe_action == 0) { close(err[0]); } else { cmdtab[ slot ].fd[ ERR ] = err[0]; cmdtab[ slot ].stream[ ERR ] = fdopen( cmdtab[ slot ].fd[ ERR ], "rb" ); if ( cmdtab[ slot ].stream[ ERR ] == NULL ) { perror( "fdopen" ); exit( EXITBAD ); } } /* Ensure enough room for rule and target name. */ if ( action && target ) { len = strlen( action ) + 1; if ( cmdtab[ slot ].action_length < len ) { BJAM_FREE( cmdtab[ slot ].action ); cmdtab[ slot ].action = BJAM_MALLOC_ATOMIC( len ); cmdtab[ slot ].action_length = len; } strcpy( cmdtab[ slot ].action, action ); len = strlen( target ) + 1; if ( cmdtab[ slot ].target_length < len ) { BJAM_FREE( cmdtab[ slot ].target ); cmdtab[ slot ].target = BJAM_MALLOC_ATOMIC( len ); cmdtab[ slot ].target_length = len; } strcpy( cmdtab[ slot ].target, target ); } else { BJAM_FREE( cmdtab[ slot ].action ); BJAM_FREE( cmdtab[ slot ].target ); cmdtab[ slot ].action = 0; cmdtab[ slot ].target = 0; cmdtab[ slot ].action_length = 0; cmdtab[ slot ].target_length = 0; } /* Save the operation for exec_wait() to find. */ cmdtab[ slot ].func = func; cmdtab[ slot ].closure = closure; /* Wait until we are under the limit of concurrent commands. Do not trust * globs.jobs alone. */ while ( ( cmdsrunning >= MAXJOBS ) || ( cmdsrunning >= globs.jobs ) ) if ( !exec_wait() ) break; } /* Returns 1 if file is closed, 0 if descriptor is still live. * * i is index into cmdtab * * s (stream) indexes: * - cmdtab[ i ].stream[ s ] * - cmdtab[ i ].buffer[ s ] * - cmdtab[ i ].fd [ s ] */ int read_descriptor( int i, int s ) { int ret; int len; char buffer[BUFSIZ]; while ( 0 < ( ret = fread( buffer, sizeof(char), BUFSIZ-1, cmdtab[ i ].stream[ s ] ) ) ) { buffer[ret] = 0; if ( !cmdtab[ i ].buffer[ s ] ) { /* Never been allocated. */ cmdtab[ i ].buffer[ s ] = (char*)BJAM_MALLOC_ATOMIC( ret + 1 ); memcpy( cmdtab[ i ].buffer[ s ], buffer, ret + 1 ); } else { /* Previously allocated. */ char * tmp = cmdtab[ i ].buffer[ s ]; len = strlen( tmp ); cmdtab[ i ].buffer[ s ] = (char*)BJAM_MALLOC_ATOMIC( len + ret + 1 ); memcpy( cmdtab[ i ].buffer[ s ], tmp, len ); memcpy( cmdtab[ i ].buffer[ s ] + len, buffer, ret + 1 ); BJAM_FREE( tmp ); } } return feof(cmdtab[ i ].stream[ s ]); } void close_streams( int i, int s ) { /* Close the stream and pipe descriptor. */ fclose(cmdtab[ i ].stream[ s ]); cmdtab[ i ].stream[ s ] = 0; close(cmdtab[ i ].fd[ s ]); cmdtab[ i ].fd[ s ] = 0; } void populate_file_descriptors( int * fmax, fd_set * fds) { int i, fd_max = 0; struct tms buf; clock_t current = times( &buf ); select_timeout = globs.timeout; /* Compute max read file descriptor for use in select. */ FD_ZERO(fds); for ( i = 0; i < globs.jobs; ++i ) { if ( 0 < cmdtab[ i ].fd[ OUT ] ) { fd_max = fd_max < cmdtab[ i ].fd[ OUT ] ? cmdtab[ i ].fd[ OUT ] : fd_max; FD_SET(cmdtab[ i ].fd[ OUT ], fds); } if ( globs.pipe_action != 0 ) { if (0 < cmdtab[ i ].fd[ ERR ]) { fd_max = fd_max < cmdtab[ i ].fd[ ERR ] ? cmdtab[ i ].fd[ ERR ] : fd_max; FD_SET(cmdtab[ i ].fd[ ERR ], fds); } } if (globs.timeout && cmdtab[ i ].pid) { clock_t consumed = (current - cmdtab[ i ].start_time) / tps; clock_t process_timesout = globs.timeout - consumed; if (0 < process_timesout && process_timesout < select_timeout) { select_timeout = process_timesout; } if ( globs.timeout <= consumed ) { killpg( cmdtab[ i ].pid, SIGKILL ); cmdtab[ i ].exit_reason = EXIT_TIMEOUT; } } } *fmax = fd_max; } /* * exec_wait() - wait and drive at most one execution completion. */ int exec_wait() { int i; int ret; int fd_max; int pid; int status; int finished; int rstat; timing_info time_info; fd_set fds; struct tms new_time; /* Handle naive make1() which does not know if commands are running. */ if ( !cmdsrunning ) return 0; /* Process children that signaled. */ finished = 0; while ( !finished && cmdsrunning ) { /* Compute max read file descriptor for use in select(). */ populate_file_descriptors( &fd_max, &fds ); if ( 0 < globs.timeout ) { /* Force select() to timeout so we can terminate expired processes. */ tv.tv_sec = select_timeout; tv.tv_usec = 0; /* select() will wait until: i/o on a descriptor, a signal, or we * time out. */ ret = select( fd_max + 1, &fds, 0, 0, &tv ); } else { /* select() will wait until i/o on a descriptor or a signal. */ ret = select( fd_max + 1, &fds, 0, 0, 0 ); } if ( 0 < ret ) { for ( i = 0; i < globs.jobs; ++i ) { int out = 0; int err = 0; if ( FD_ISSET( cmdtab[ i ].fd[ OUT ], &fds ) ) out = read_descriptor( i, OUT ); if ( ( globs.pipe_action != 0 ) && ( FD_ISSET( cmdtab[ i ].fd[ ERR ], &fds ) ) ) err = read_descriptor( i, ERR ); /* If feof on either descriptor, then we are done. */ if ( out || err ) { /* Close the stream and pipe descriptors. */ close_streams( i, OUT ); if ( globs.pipe_action != 0 ) close_streams( i, ERR ); /* Reap the child and release resources. */ pid = waitpid( cmdtab[ i ].pid, &status, 0 ); if ( pid == cmdtab[ i ].pid ) { finished = 1; pid = 0; cmdtab[ i ].pid = 0; /* Set reason for exit if not timed out. */ if ( WIFEXITED( status ) ) { cmdtab[ i ].exit_reason = 0 == WEXITSTATUS( status ) ? EXIT_OK : EXIT_FAIL; } /* Print out the rule and target name. */ out_action( cmdtab[ i ].action, cmdtab[ i ].target, cmdtab[ i ].command, cmdtab[ i ].buffer[ OUT ], cmdtab[ i ].buffer[ ERR ], cmdtab[ i ].exit_reason ); times( &new_time ); time_info.system = (double)( new_time.tms_cstime - old_time.tms_cstime ) / CLOCKS_PER_SEC; time_info.user = (double)( new_time.tms_cutime - old_time.tms_cutime ) / CLOCKS_PER_SEC; time_info.start = cmdtab[ i ].start_dt; time_info.end = time( 0 ); old_time = new_time; /* Drive the completion. */ --cmdsrunning; if ( intr ) rstat = EXEC_CMD_INTR; else if ( status != 0 ) rstat = EXEC_CMD_FAIL; else rstat = EXEC_CMD_OK; /* Assume -p0 in effect so only pass buffer[ 0 ] * containing merged output. */ (*cmdtab[ i ].func)( cmdtab[ i ].closure, rstat, &time_info, cmdtab[ i ].command, cmdtab[ i ].buffer[ 0 ] ); BJAM_FREE( cmdtab[ i ].buffer[ OUT ] ); cmdtab[ i ].buffer[ OUT ] = 0; BJAM_FREE( cmdtab[ i ].buffer[ ERR ] ); cmdtab[ i ].buffer[ ERR ] = 0; BJAM_FREE( cmdtab[ i ].command ); cmdtab[ i ].command = 0; cmdtab[ i ].func = 0; cmdtab[ i ].closure = 0; cmdtab[ i ].start_time = 0; } else { printf( "unknown pid %d with errno = %d\n", pid, errno ); exit( EXITBAD ); } } } } } return 1; } # endif /* USE_EXECUNIX */