diff options
Diffstat (limited to 'agen5/agShell.c')
-rw-r--r-- | agen5/agShell.c | 695 |
1 files changed, 695 insertions, 0 deletions
diff --git a/agen5/agShell.c b/agen5/agShell.c new file mode 100644 index 0000000..80bcbdd --- /dev/null +++ b/agen5/agShell.c @@ -0,0 +1,695 @@ +/** + * @file agShell.c + * + * Time-stamp: "2012-03-04 19:08:09 bkorb" + * + * Manage a server shell process + * + * This file is part of AutoGen. + * AutoGen Copyright (c) 1992-2012 by Bruce Korb - all rights reserved + * + * AutoGen is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AutoGen is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ +static char * cur_dir = NULL; + +/*=gfunc chdir + * + * what: Change current directory + * + * exparg: dir, new directory name + * + * doc: Sets the current directory for AutoGen. Shell commands will run + * from this directory as well. This is a wrapper around the Guile + * native function. It returns its directory name argument and + * fails the program on failure. +=*/ +SCM +ag_scm_chdir(SCM dir) +{ + static char const zChdirDir[] = "chdir directory"; + + scm_chdir(dir); + + /* + * We're still here, so we have a valid argument. + */ + if (cur_dir != NULL) + free(cur_dir); + { + char const * pz = ag_scm2zchars(dir, zChdirDir); + cur_dir = malloc(AG_SCM_STRLEN(dir) + 1); + strcpy((char*)cur_dir, pz); + } + return dir; +} + +/*=gfunc shell + * + * what: invoke a shell script + * general_use: + * + * exparg: command, shell command - the result value is the stdout output. + * + * doc: + * Generate a string by writing the value to a server shell and reading the + * output back in. The template programmer is responsible for ensuring that + * it completes within 10 seconds. If it does not, the server will be + * killed, the output tossed and a new server started. + * + * Please note: This is the same server process used by the '#shell' + * definitions directive and backquoted @code{`} definitions. There may be + * left over state from previous shell expressions and the @code{`} + * processing in the declarations. However, a @code{cd} to the original + * directory is always issued before the new command is issued. + * + * Also note: When initializing, autogen will set the environment + * variable "AGexe" to the full path of the autogen executable. +=*/ +SCM +ag_scm_shell(SCM cmd) +{ +#ifndef SHELL_ENABLED + return cmd; +#else + if (! AG_SCM_STRING_P(cmd)) + return SCM_UNDEFINED; + { + char* pz = shell_cmd(ag_scm2zchars(cmd, "command")); + cmd = AG_SCM_STR02SCM(pz); + AGFREE((void*)pz); + return cmd; + } +#endif +} + +/*=gfunc shellf + * + * what: format a string, run shell + * general_use: + * + * exparg: format, formatting string + * exparg: format-arg, list of arguments to formatting string, opt, list + * + * doc: Format a string using arguments from the alist, + * then send the result to the shell for interpretation. +=*/ +SCM +ag_scm_shellf(SCM fmt, SCM alist) +{ + int len = scm_ilength(alist); + char* pz; + +#ifdef DEBUG_ENABLED + if (len < 0) + AG_ABEND(SHELLF_BAD_ALIST_MSG); +#endif + + pz = ag_scm2zchars(fmt, "format"); + fmt = run_printf(pz, len, alist); + +#ifdef SHELL_ENABLED + pz = shell_cmd(ag_scm2zchars(fmt, "shell script")); + fmt = AG_SCM_STR02SCM(pz); + AGFREE((void*)pz); +#endif + return fmt; +} + +#ifndef SHELL_ENABLED +HIDE_FN(void closeServer(void) {;}) + +HIDE_FN(char * shell_cmd(char const* pzCmd)) { + char* pz; + AGDUPSTR(pz, pzCmd, "dummy shell command"); + return pz; +} +#else + +/* + * Dual pipe opening of a child process + */ +static fp_pair_t serv_pair = { NULL, NULL }; +static pid_t serv_id = NULLPROCESS; +static bool was_close_err = false; +static int log_ct = 0; +static char const * last_cmd = NULL; + +/* = = = START-STATIC-FORWARD = = = */ +static void +handle_signal(int signo); + +static void +set_orig_dir(void); + +static bool +send_cmd_ok(char const * cmd); + +static void +start_server_cmd_trace(void); + +static void +send_server_init_cmds(void); + +static void +server_setup(void); + +static int +chain_open(int in_fd, char const ** arg_v, pid_t * child_pid); + +static pid_t +server_open(fd_pair_t * fd_pair, char const ** ppArgs); + +static pid_t +server_fp_open(fp_pair_t * fp_pair, char const ** ppArgs); + +static inline void +realloc_text(char ** p_txt, size_t * p_sz, size_t need_len); + +static char* +load_data(void); +/* = = = END-STATIC-FORWARD = = = */ + +LOCAL void +close_server_shell(void) +{ + if (serv_id == NULLPROCESS) + return; + + (void)kill(serv_id, SIGTERM); +#ifdef HAVE_USLEEP + usleep(100000); /* 1/10 of a second */ +#else + sleep(1); +#endif + (void)kill(serv_id, SIGKILL); + serv_id = NULLPROCESS; + + /* + * This guard should not be necessary. However, sometimes someone + * holds an allocation pthread lock when a seg fault occurrs. fclose + * needs that lock, so we hang waiting for it. Oops. So, when we + * are aborting, we just let the OS close these file descriptors. + */ + if (processing_state != PROC_STATE_ABORTING) { + (void)fclose(serv_pair.fp_read); + /* + * This is _completely_ wrong, but sometimes there are data left + * hanging about that gets sucked up by the _next_ server shell + * process. That should never, ever be in any way possible, but + * it is the only explanation for a second server shell picking up + * the initialization string twice. It must be a broken timing + * issue in the Linux stdio code. I have no other explanation. + */ + fflush(serv_pair.fp_write); + (void)fclose(serv_pair.fp_write); + } + + serv_pair.fp_read = serv_pair.fp_write = NULL; +} + +/** + * handle SIGALRM and SIGPIPE signals while waiting for server shell + * responses. + */ +static void +handle_signal(int signo) +{ + static int timeout_limit = 5; + if ((signo == SIGALRM) && (--timeout_limit <= 0)) + AG_ABEND(TOO_MANY_TIMEOUTS_MSG); + + fprintf(trace_fp, SHELL_DIE_ON_SIGNAL_FMT, strsignal(signo), signo); + was_close_err = true; + + (void)fputs(SHELL_LAST_CMD_MSG, trace_fp); + { + char const* pz = (last_cmd == NULL) + ? SHELL_UNK_LAST_CMD_MSG : last_cmd; + fprintf(trace_fp, SHELL_CMD_FMT, cur_dir, pz, SH_DONE_MARK, log_ct); + } + last_cmd = NULL; + close_server_shell(); +} + +/** + * first time starting a server shell, we get our current directory. + * That value is kept, but may be changed via a (chdir "...") scheme call. + */ +static void +set_orig_dir(void) +{ + char * p = malloc(AG_PATH_MAX); + if (p == NULL) + AG_ABEND(SET_ORIG_DIR_NO_MEM_MSG); + + cur_dir = getcwd(p, AG_PATH_MAX); + + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) + fputs(TRACE_SHELL_FIRST_START, trace_fp); +} + +/** + * Send a command string down to the server shell + */ +static bool +send_cmd_ok(char const * cmd) +{ + last_cmd = cmd; + fprintf(serv_pair.fp_write, SHELL_CMD_FMT, cur_dir, last_cmd, + SH_DONE_MARK, ++log_ct); + + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) { + fprintf(trace_fp, LOG_SEP_FMT, log_ct); + fprintf(trace_fp, SHELL_CMD_FMT, cur_dir, last_cmd, + SH_DONE_MARK, log_ct); + } + + (void)fflush(serv_pair.fp_write); + if (was_close_err) + fprintf(trace_fp, CMD_FAIL_FMT, cmd); + return ! was_close_err; +} + +/** + * Tracing level is TRACE_EVERYTHING, so send the server shell + * various commands to start "set -x" tracing and display the + * trap actions. + */ +static void +start_server_cmd_trace(void) +{ + fputs(TRACE_XTRACE_MSG, trace_fp); + if (send_cmd_ok(SHELL_XTRACE_CMDS)) { + char * pz = load_data(); + fputs(SHELL_RES_DISCARD_MSG, trace_fp); + fprintf(trace_fp, TRACE_TRAP_STATE_FMT, pz); + AGFREE((void*)pz); + } +} + +/** + * Send down the initialization string with our PID in it, as well + * as the full path name of the autogen executable. + */ +static void +send_server_init_cmds(void) +{ + was_close_err = false; + + { + char * pzc = AGALOC(SHELL_INIT_STR_LEN + + 11 // log10(1 << 32) + 1 + + strlen(autogenOptions.pzProgPath), + "server init"); + sprintf(pzc, SHELL_INIT_STR, (unsigned int)getpid(), + autogenOptions.pzProgPath, + (dep_fp == NULL) ? "" : dep_file); + + if (send_cmd_ok(pzc)) + AGFREE((void*)load_data()); + AGFREE(pzc); + } + + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) + fputs(SHELL_RES_DISCARD_MSG, trace_fp); + + if (OPT_VALUE_TRACE >= TRACE_EVERYTHING) + start_server_cmd_trace(); +} + +/** + * Perform various initializations required when starting + * a new server shell process. + */ +static void +server_setup(void) +{ + if (cur_dir == NULL) + set_orig_dir(); + else if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) + fputs(SHELL_RESTART_MSG, trace_fp); + + { + struct sigaction new_sa; + new_sa.sa_handler = handle_signal; + new_sa.sa_flags = 0; + (void)sigemptyset(&new_sa.sa_mask); + (void)sigaction(SIGPIPE, &new_sa, NULL); + (void)sigaction(SIGALRM, &new_sa, NULL); + } + + send_server_init_cmds(); + + last_cmd = NULL; +} + +/** + * Given an FD for an inferior process to use as stdin, + * start that process and return a NEW FD that that process + * will use for its stdout. Requires the argument vector + * for the new process and, optionally, a pointer to a place + * to store the child's process id. + * + * @param stdinFd the file descriptor for the process' stdin + * @param ppArgs The program and argument vector + * @param pChild where to stash the child process PID + * + * @returns the read end of a pipe the child process uses for stdout + */ +static int +chain_open(int in_fd, char const ** arg_v, pid_t * child_pid) +{ + fd_pair_t out_pair = { -1, -1 }; + pid_t ch_pid; + char const * shell; + + /* + * If we did not get an arg list, use the default + */ + if (arg_v == NULL) + arg_v = server_args; + + /* + * If the arg list does not have a program, + * assume the zShellProg from the environment, or, failing + * that, then sh. Set argv[0] to whatever we decided on. + */ + if (shell = *arg_v, + (shell == NULL) || (*shell == NUL)) + + *arg_v = shell = shell_program; + + /* + * Create a pipe it will be the child process' stdout, + * and the parent will read from it. + */ + if (pipe((int*)&out_pair) < 0) { + if (child_pid != NULL) + *child_pid = NOPROCESS; + return -1; + } + + /* + * Make sure our standard streams are all flushed out before forking. + * (avoid duplicate output). Call fork() and see which process we become + */ + fflush(stdout); + fflush(stderr); + if (trace_fp != stderr) + fflush(trace_fp); + + ch_pid = fork(); + switch (ch_pid) { + case NOPROCESS: /* parent - error in call */ + close(in_fd); + close(out_pair.fd_read); + close(out_pair.fd_write); + if (child_pid != NULL) + *child_pid = NOPROCESS; + return -1; + + default: /* parent - return opposite FD's */ + if (child_pid != NULL) + *child_pid = ch_pid; + + close(in_fd); + close(out_pair.fd_write); + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) { + fprintf(trace_fp, TRACE_SHELL_PID_FMT, (unsigned int)ch_pid); + fflush(trace_fp); + } + + return out_pair.fd_read; + + case NULLPROCESS: /* child - continue processing */ + break; + } + + /* + * Close the pipe end handed back to the parent process, + * plus stdin and stdout. + */ + close(out_pair.fd_read); + close(STDIN_FILENO); + close(STDOUT_FILENO); + + /* + * Set stdin/out to the fd passed in and the write end of our new pipe. + */ + fcntl(out_pair.fd_write, F_DUPFD, STDOUT_FILENO); + fcntl(in_fd, F_DUPFD, STDIN_FILENO); + + /* + * set stderr to our trace file (if not stderr). + */ + if (trace_fp != stderr) { + close(STDERR_FILENO); + fcntl(fileno(trace_fp), F_DUPFD, STDERR_FILENO); + } + + /* + * Make the output file unbuffered only. + * We do not want to wait for shell output buffering. + */ + setvbuf(stdout, NULL, _IONBF, (size_t)0); + + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) { + fprintf(trace_fp, TRACE_SHELL_STARTS_FMT, shell); + + fflush(trace_fp); + } + + execvp((char*)shell, (char**)arg_v); + AG_CANT("execvp", shell); + /* NOTREACHED */ + return -1; +} + +/** + * Given a pointer to an argument vector, start a process and + * place its stdin and stdout file descriptors into an fd pair + * structure. The "fd_write" connects to the inferior process + * stdin, and the "fd_read" connects to its stdout. The calling + * process should write to "fd_write" and read from "fd_read". + * The return value is the process id of the created process. + */ +static pid_t +server_open(fd_pair_t * fd_pair, char const ** ppArgs) +{ + pid_t chId = NOPROCESS; + + /* + * Create a bi-directional pipe. Writes on 0 arrive on 1 + * and vice versa, so the parent and child processes will + * read and write to opposite FD's. + */ + if (pipe((int*)fd_pair) < 0) + return NOPROCESS; + + fd_pair->fd_read = chain_open(fd_pair->fd_read, ppArgs, &chId); + if (chId == NOPROCESS) + close(fd_pair->fd_write); + + return chId; +} + + +/** + * Identical to "server_open()", except that the "fd"'s are "fdopen(3)"-ed + * into file pointers instead. + */ +static pid_t +server_fp_open(fp_pair_t * fp_pair, char const ** ppArgs) +{ + fd_pair_t fd_pair; + pid_t chId = server_open(&fd_pair, ppArgs); + + if (chId == NOPROCESS) + return chId; + + fp_pair->fp_read = fdopen(fd_pair.fd_read, "r" FOPEN_BINARY_FLAG); + fp_pair->fp_write = fdopen(fd_pair.fd_write, "w" FOPEN_BINARY_FLAG); + return chId; +} + +static inline void +realloc_text(char ** p_txt, size_t * p_sz, size_t need_len) +{ + *p_sz = (*p_sz + need_len + 0xFFF) & ~0xFFF; + *p_txt = AGREALOC((void*)*p_txt, *p_sz, "expand text"); +} + +/** + * Read data from a file pointer (a pipe to a process in this context) + * until we either get EOF or we get a marker line back. + * The read data are stored in a malloc-ed string that is truncated + * to size at the end. Input is assumed to be an ASCII string. + */ +static char* +load_data(void) +{ + char* text; + size_t text_sz = 4096; + size_t used_ct = 0; + char* scan; + char zLine[ 1024 ]; + int retry_ct = 0; +#define LOAD_RETRY_LIMIT 4 + + scan = text = AGALOC(text_sz, "Text Block"); + *text = NUL; + + for (;;) { + char * line_p; + + /* + * Set a timeout so we do not wait forever. Sometimes we don't wait + * at all and we should. Retry in those cases (but not on EOF). + */ + alarm((unsigned int)OPT_VALUE_TIMEOUT); + line_p = fgets(zLine, (int)sizeof(zLine), serv_pair.fp_read); + alarm(0); + + if (line_p == NULL) { + /* + * Guard against a server timeout + */ + if (serv_id == NULLPROCESS) + break; + + if ((OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) || (retry_ct++ > 0)) + fprintf(trace_fp, SHELL_READ_ERR_FMT, errno, strerror(errno)); + + if (feof(serv_pair.fp_read) || (retry_ct > LOAD_RETRY_LIMIT)) + break; + + continue; /* no data - retry */ + } + + /* + * Check for magic character sequence indicating 'DONE' + */ + if (strncmp(zLine, SH_DONE_MARK, SH_DONE_MARK_LEN) == 0) + break; + + { + size_t llen = strlen(zLine); + if (text_sz <= used_ct + llen) { + realloc_text(&text, &text_sz, llen); + scan = text + used_ct; + } + + memcpy(scan, zLine, llen); + used_ct += llen; + scan += llen; + } + + /* + * Stop now if server timed out or if we are at EOF + */ + if ((serv_id == NULLPROCESS) || feof(serv_pair.fp_read)) { + fputs(SHELL_NO_END_MARK_MSG, trace_fp); + break; + } + } + + /* + * Trim off all trailing white space and shorten the buffer + * to the size actually used. + */ + while ( (scan > text) + && IS_WHITESPACE_CHAR(scan[-1])) + scan--; + text_sz = (scan - text) + 1; + *scan = NUL; + + if (OPT_VALUE_TRACE >= TRACE_SERVER_SHELL) + fprintf(trace_fp, TRACE_SHELL_RESULT_MSG, + (int)text_sz, text, zLine); + + return AGREALOC((void*)text, text_sz, "resize output"); +#undef LOAD_RETRY_LIMIT +} + +/** + * Run a semi-permanent server shell. The program will be the + * one named by the environment variable $SHELL, or default to "sh". + * If one of the commands we send to it takes too long or it dies, + * we will shoot it and restart one later. + * + * @param cmd the input command string + * @returns an allocated string, even if it is empty. + */ +LOCAL char * +shell_cmd(char const * cmd) +{ + /* + * IF the shell server process is not running yet, + * THEN try to start it. + */ + if (serv_id == NULLPROCESS) { + putenv((char *)SHELL_SET_PS4_FMT); + serv_id = server_fp_open(&serv_pair, server_args); + if (serv_id > 0) + server_setup(); + } + + /* + * IF it is still not running, + * THEN return the nil string. + */ + if (serv_id <= 0) { + char* pz = (char*)AGALOC(1, "Text Block"); + + *pz = NUL; + return pz; + } + + /* + * Make sure the process will pay attention to us, + * send the supplied command, and then + * have it output a special marker that we can find. + */ + if (! send_cmd_ok(cmd)) + return NULL; + + /* + * Now try to read back all the data. If we fail due to either + * a sigpipe or sigalrm (timeout), we will return the nil string. + */ + { + char* pz = load_data(); + if (pz == NULL) { + fprintf(trace_fp, CMD_FAIL_FMT, cmd); + close_server_shell(); + pz = (char*)AGALOC(1, "Text Block"); + + *pz = NUL; + + } else if (was_close_err) + fprintf(trace_fp, CMD_FAIL_FMT, cmd); + + last_cmd = NULL; + return pz; + } +} + +#endif /* ! SHELL_ENABLED */ +/* + * Local Variables: + * mode: C + * c-file-style: "stroustrup" + * indent-tabs-mode: nil + * End: + * end of agen5/agShell.c */ |