| /****************************************************************************** | |
| * Copyright (c) 2005 The Regents of the University of California. | |
| * This material was produced under U.S. Government contract W-7405-ENG-36 | |
| * for Los Alamos National Laboratory, which is operated by the University | |
| * of California for the U.S. Department of Energy. The U.S. Government has | |
| * rights to use, reproduce, and distribute this software. NEITHER THE | |
| * GOVERNMENT NOR THE UNIVERSITY MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR | |
| * ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is modified | |
| * to produce derivative works, such modified software should be clearly | |
| * marked, so as not to confuse it with the version available from LANL. | |
| * | |
| * Additionally, this program and the accompanying materials | |
| * are made available under the terms of the Eclipse Public License v1.0 | |
| * which accompanies this distribution, and is available at | |
| * http://www.eclipse.org/legal/epl-v10.html | |
| * | |
| * LA-CC 04-115 | |
| ******************************************************************************/ | |
| #include <sys/types.h> | |
| #include <sys/wait.h> | |
| #include <sys/errno.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <signal.h> | |
| #include <unistd.h> | |
| #include <fcntl.h> | |
| #include "MIList.h" | |
| #include "MISession.h" | |
| #include "MICommand.h" | |
| #include "MIError.h" | |
| #include "MIOOBRecord.h" | |
| #include "MIValue.h" | |
| #include "MIResult.h" | |
| static MIList * MISessionList = NULL; | |
| static struct timeval MISessionDefaultSelectTimeout = {0, 1000}; | |
| static int MISessionDebug = 1; | |
| static void DoOOBCallbacks(MISession *sess, MIList *oobs); | |
| static void HandleChild(int sig); | |
| static int WriteCommand(int fd, char *cmd); | |
| static char *ReadResponse(int fd); | |
| #ifdef __gnu_linux__ | |
| static int IsExecAsyncStopped(MISession *sess, MIList *oobs); | |
| #endif /* __gnu_linux__ */ | |
| extern int get_master_pty(char **); | |
| extern int get_slave_pty(char *); | |
| MISession * | |
| MISessionNew(void) | |
| { | |
| MISession * sess = (MISession *)malloc(sizeof(MISession)); | |
| sess->in_fd = -1; | |
| sess->out_fd = -1; | |
| sess->pty_fd = -1; | |
| sess->pid = -1; | |
| sess->exited = 1; | |
| sess->exit_status = 0; | |
| sess->command = NULL; | |
| sess->send_queue = MIListNew(); | |
| sess->gdb_path = strdup("gdb"); | |
| sess->event_callback = NULL; | |
| sess->cmd_callback = NULL; | |
| sess->exec_callback = NULL; | |
| sess->status_callback = NULL; | |
| sess->notify_callback = NULL; | |
| sess->console_callback = NULL; | |
| sess->log_callback = NULL; | |
| sess->target_callback = NULL; | |
| sess->select_timeout = MISessionDefaultSelectTimeout; | |
| if (MISessionList == NULL) | |
| MISessionList = MIListNew(); | |
| MIListAdd(MISessionList, (void *)sess); | |
| return sess; | |
| } | |
| void | |
| MISessionFree(MISession *sess) | |
| { | |
| MIListRemove(MISessionList, (void *)sess); | |
| free(sess); | |
| } | |
| static void | |
| HandleChild(int sig) | |
| { | |
| int stat; | |
| pid_t pid; | |
| MISession * sess; | |
| pid = wait(&stat); | |
| if (MISessionList != NULL) { | |
| for (MIListSet(MISessionList); (sess = (MISession *)MIListGet(MISessionList)) != NULL; ) { | |
| if (sess->pid == pid) { | |
| sess->exited = 1; | |
| sess->exit_status = stat; | |
| } | |
| } | |
| } | |
| } | |
| void | |
| MISessionSetTimeout(MISession *sess, long sec, long usec) | |
| { | |
| sess->select_timeout.tv_sec = sec; | |
| sess->select_timeout.tv_usec = usec; | |
| } | |
| void | |
| MISessionSetDebug(int debug) | |
| { | |
| MISessionDebug = debug; | |
| } | |
| int | |
| MISessionStartLocal(MISession *sess, char *prog) | |
| { | |
| int p1[2]; | |
| int p2[2]; | |
| char * name; | |
| if (pipe(p1) < 0 || pipe(p2) < 0) { | |
| MISetError(MI_ERROR_SYSTEM, strerror(errno)); | |
| return -1; | |
| } | |
| sess->in_fd = p2[1]; | |
| sess->out_fd = p1[0]; | |
| sess->exited = 0; | |
| signal(SIGCHLD, HandleChild); | |
| signal(SIGPIPE, SIG_IGN); | |
| if ((sess->pty_fd = get_master_pty(&name)) < 0 ) { | |
| name = strdup("/dev/null"); | |
| } | |
| switch (sess->pid = fork()) | |
| { | |
| case 0: | |
| dup2(p2[0], 0); | |
| dup2(p1[1], 1); | |
| close(p1[0]); | |
| close(p1[1]); | |
| close(p2[0]); | |
| close(p2[1]); | |
| if (prog == NULL) | |
| execlp(sess->gdb_path, "gdb", "-q", "-tty", name, "-i", "mi", NULL); | |
| else | |
| execlp(sess->gdb_path, "gdb", "-q", "-tty", name, "-i", "mi", prog, NULL); | |
| exit(1); | |
| case -1: | |
| MISetError(MI_ERROR_SYSTEM, strerror(errno)); | |
| return -1; | |
| default: | |
| break; | |
| } | |
| free(name); | |
| close(p1[1]); | |
| close(p2[0]); | |
| return 0; | |
| } | |
| void | |
| MISessionRegisterEventCallback(MISession *sess, void (*callback)(MIEvent *)) | |
| { | |
| sess->event_callback = callback; | |
| } | |
| void | |
| MISessionRegisterCommandCallback(MISession *sess, void (*callback)(MIResultRecord *)) | |
| { | |
| sess->cmd_callback = callback; | |
| } | |
| void | |
| MISessionRegisterExecCallback(MISession *sess, void (*callback)(char *, MIList *)) | |
| { | |
| sess->exec_callback = callback; | |
| } | |
| void | |
| MISessionRegisterStatusCallback(MISession *sess, void (*callback)(char *, MIList *)) | |
| { | |
| sess->status_callback = callback; | |
| } | |
| void | |
| MISessionRegisterNotifyCallback(MISession *sess, void (*callback)(char *, MIList *)) | |
| { | |
| sess->notify_callback = callback; | |
| } | |
| void | |
| MISessionRegisterConsoleCallback(MISession *sess, void (*callback)(char *)) | |
| { | |
| sess->console_callback = callback; | |
| } | |
| void | |
| MISessionRegisterLogCallback(MISession *sess, void (*callback)(char *)) | |
| { | |
| sess->log_callback = callback; | |
| } | |
| void | |
| MISessionRegisterTargetCallback(MISession *sess, void (*callback)(char *)) | |
| { | |
| sess->target_callback = callback; | |
| } | |
| void | |
| MISessionSetGDBPath(MISession *sess, char *path) | |
| { | |
| if (sess->gdb_path != NULL) | |
| free(sess->gdb_path); | |
| sess->gdb_path = strdup(path); | |
| } | |
| /* | |
| * Send command to debugger. | |
| */ | |
| int | |
| MISessionSendCommand(MISession *sess, MICommand *cmd) | |
| { | |
| if (sess->pid == -1) { | |
| MISetError(MI_ERROR_SESSION, ""); | |
| return -1; | |
| } | |
| MIListAdd(sess->send_queue, (void *)cmd); | |
| return 0; | |
| } | |
| /* | |
| * Send command to GDB. We keep writing | |
| * until the whole command has been sent. | |
| * | |
| * There is a chance this could block. | |
| */ | |
| static int | |
| WriteCommand(int fd, char *cmd) | |
| { | |
| int n; | |
| int len = strlen(cmd); | |
| if (MISessionDebug) { | |
| printf("MI: SEND %s", cmd); | |
| if (cmd[len-1] != '\n') | |
| printf("\n"); | |
| fflush(stdout); | |
| } | |
| while (len > 0) { | |
| n = write(fd, cmd, len); | |
| if (n <= 0) { | |
| if (n < 0) { | |
| if (errno == EINTR) | |
| continue; | |
| MISetError(MI_ERROR_SYSTEM, strerror(errno)); | |
| } | |
| return -1; | |
| } | |
| cmd += n; | |
| len -= n; | |
| } | |
| return 0; | |
| } | |
| /* | |
| * Read BUFSIZ chunks of data until we have read | |
| * everything available. | |
| * | |
| * Everything has been read if: | |
| * 1) the read returns less than BUFSIZ | |
| * 2) the read would block (errno == EAGAIN) | |
| * | |
| * The assumption is that O_NONBLOCK has been set | |
| * of the file descriptor. | |
| * | |
| * The buffer is static, so we just reuse it for each | |
| * call and increase the size if necessary. | |
| */ | |
| #define MI_BUFSIZ 1024 //BUFSIZ | |
| static char * | |
| ReadResponse(int fd) | |
| { | |
| int n; | |
| int len = 0; | |
| char * p; | |
| static int res_buf_len = MI_BUFSIZ; | |
| static char * res_buf = NULL; | |
| if (res_buf == NULL) | |
| res_buf = (char *)malloc(MI_BUFSIZ); | |
| p = res_buf; | |
| for (;;) { | |
| n = read(fd, p, MI_BUFSIZ); | |
| if (n <= 0) { | |
| if (n < 0) { | |
| if (errno == EAGAIN) | |
| break; | |
| if (errno == EINTR) | |
| continue; | |
| MISetError(MI_ERROR_SYSTEM, strerror(errno)); | |
| } | |
| return NULL; | |
| } | |
| if (n < MI_BUFSIZ) | |
| break; | |
| len += MI_BUFSIZ; | |
| if (len == res_buf_len) { | |
| res_buf_len += MI_BUFSIZ; | |
| res_buf = (char *)realloc(res_buf, res_buf_len); | |
| } | |
| p = &res_buf[len]; | |
| } | |
| if (n > 0) | |
| p[n] = '\0'; | |
| if (MISessionDebug) { | |
| printf("MI: RECV %s", res_buf); | |
| if (res_buf[strlen(res_buf)-1] != '\n') | |
| printf("\n"); | |
| fflush(stdout); | |
| } | |
| return res_buf; | |
| } | |
| /* | |
| * Send first pending command to GDB for each session. | |
| * | |
| * For each session, read response from GDB and | |
| * parse the result. | |
| * | |
| * Assumes that fds contains file descriptors ready | |
| * for writing. | |
| */ | |
| void | |
| MISessionProcessCommandsAndResponses(MISession *sess, fd_set *rfds, fd_set *wfds) | |
| { | |
| char * str; | |
| MIOutput * output; | |
| if (sess->pid == -1) { | |
| return; | |
| } | |
| if (sess->in_fd != -1 | |
| && !MIListIsEmpty(sess->send_queue) | |
| && sess->command == NULL | |
| && (wfds == NULL || FD_ISSET(sess->in_fd, wfds)) | |
| ) | |
| { | |
| sess->command = (MICommand *)MIListRemoveFirst(sess->send_queue); | |
| #ifdef __gnu_linux__ | |
| /* | |
| * NOTE: this hack only works if gdb is started with the '-tty' argument (or | |
| * presumably if the 'tty' command is issued.) Without this, the only way to | |
| * interrupt a running process seems to be from the command line. | |
| */ | |
| if (strcmp(sess->command->command, "-exec-interrupt") == 0) { | |
| if (MISessionDebug) { | |
| printf("MI: sending SIGINT to %d\n", sess->pid); | |
| fflush(stdout); | |
| } | |
| kill(sess->pid, SIGINT); | |
| } else if (WriteCommand(sess->in_fd, MICommandToString(sess->command)) < 0) { | |
| sess->in_fd = -1; | |
| } | |
| #else /* __gnu_linux__ */ | |
| if (WriteCommand(sess->in_fd, MICommandToString(sess->command)) < 0) { | |
| sess->in_fd = -1; | |
| } | |
| #endif /* __gnu_linux */ | |
| } | |
| if (sess->out_fd != -1 && FD_ISSET(sess->out_fd, rfds)) { | |
| if ((str = ReadResponse(sess->out_fd)) == NULL) { | |
| sess->out_fd = -1; | |
| return; | |
| } | |
| /* | |
| * If there's a command in progress, use the MIOutput saved with the command | |
| * to process any output. Otherwise create a new one. | |
| */ | |
| if (sess->command != NULL) { | |
| output = sess->command->output; | |
| } else { | |
| output = MIOutputNew(); | |
| } | |
| MIParse(str, output); | |
| /* | |
| * The output can consist of: | |
| * async oob records that are not necessarily the result of a command | |
| * stream oob records that always result from a command | |
| * result records from a command | |
| * | |
| * Async and stream oob records are processed immediately and removed. | |
| * | |
| * If there are result records, then the output *should* have resulted | |
| * from the execution of a command. Mark the command as completed an | |
| * invoke its callback. | |
| * | |
| * The stream oob and result records are freed when the command is freed. | |
| */ | |
| if (output->oobs != NULL) { | |
| #ifdef __gnu_linux__ | |
| if (sess->command != NULL && | |
| strcmp(sess->command->command, "-exec-interrupt") == 0 && | |
| IsExecAsyncStopped(sess, output->oobs)) { | |
| sess->command->completed = 1; | |
| } | |
| #endif /* __gnu_linux__ */ | |
| DoOOBCallbacks(sess, output->oobs); | |
| } | |
| /* | |
| * If there's a command in progress, process it. | |
| */ | |
| if (sess->command != NULL) { | |
| if (output->rr != NULL) { | |
| if (MISessionDebug) { | |
| printf("MI: PROCESS COMMAND CALLBACK\n"); | |
| fflush(stdout); | |
| } | |
| if (sess->command->callback != NULL) { | |
| sess->command->callback(output->rr, sess->command->cb_data); | |
| } | |
| sess->command->completed = 1; | |
| } | |
| } else { | |
| MIOutputFree(output); | |
| } | |
| if (sess->command != NULL && sess->command->completed) { | |
| sess->command = NULL; | |
| } | |
| } | |
| /* process application output */ | |
| if (sess->pty_fd != -1 && FD_ISSET(sess->pty_fd, rfds)) | |
| { | |
| if ((str = ReadResponse(sess->pty_fd)) != NULL) { | |
| if (sess->target_callback != NULL) { | |
| sess->target_callback(str); | |
| } | |
| } else { | |
| sess->pty_fd = -1; | |
| } | |
| } | |
| } | |
| /* | |
| * Used to process a result record after a CLI command has been | |
| * issued if an MIEvent needs to be generated. | |
| * | |
| * In MI mode, GDB CLI commands append result information (after | |
| * the "done" result class) to the result record. This function can be | |
| * passed to MICommandRegisterCallback() in order to automatically generate | |
| * an MIEvent when the CLI command completes. | |
| * | |
| * NOTE: CLI commands DO NOT operate asychronously, so some commands will | |
| * block until the command is complete (e.g. "run"). This may cause a user | |
| * interface to block. | |
| */ | |
| void | |
| ProcessCLIResultRecord(MIResultRecord *rr, void *data) | |
| { | |
| MISession *sess = (MISession *)data; | |
| MIResult *res; | |
| MIValue *val; | |
| if (rr->resultClass == MIResultRecordDONE) { | |
| for (MIListSet(rr->results); (res = (MIResult *)MIListGet(rr->results)); ) { | |
| if (strcmp(res->variable, "reason") == 0) { | |
| val = res->value; | |
| if (val->type == MIValueTypeConst) { | |
| if (sess->event_callback) { | |
| sess->event_callback(MIEventCreateStoppedEvent(val->cstring, rr->results)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /* | |
| * Process OOB callbacks. | |
| * | |
| * Async OOB's are removed from oobs list. | |
| * Stream OOB's are left on the oobs list so they are available | |
| * as part of the command result. | |
| */ | |
| static void | |
| DoOOBCallbacks(MISession *sess, MIList *oobs) | |
| { | |
| MIOOBRecord * oob; | |
| MIResult * res; | |
| MIValue * val; | |
| for (MIListSet(oobs); (oob = (MIOOBRecord *)MIListGet(oobs)) != NULL; ) { | |
| switch (oob->type) { | |
| case MIOOBRecordTypeAsync: | |
| switch (oob->sub_type) { | |
| case MIOOBRecordExecAsync: | |
| if (sess->exec_callback != NULL) { | |
| sess->exec_callback(oob->class, oob->results); | |
| } | |
| if (strcmp(oob->class, "stopped") == 0) { | |
| int seen_reason = 0; | |
| for (MIListSet(oob->results); (res = (MIResult *)MIListGet(oob->results)); ) { | |
| if (strcmp(res->variable, "reason") == 0) { | |
| seen_reason = 1; | |
| val = res->value; | |
| if (val->type == MIValueTypeConst) { | |
| if (sess->event_callback) { | |
| sess->event_callback(MIEventCreateStoppedEvent(val->cstring, oob->results)); | |
| } | |
| } | |
| } | |
| } | |
| /* | |
| * Temporary breakpoints under Linux don't have a "reason". If we receive | |
| * a stopped event with no reason, then we created a StoppedEvent anyway | |
| */ | |
| if (seen_reason == 0) { | |
| if (sess->event_callback) { | |
| sess->event_callback(MIEventCreateStoppedEvent("temporary-breakpoint-hit", oob->results)); | |
| } | |
| } | |
| } | |
| break; | |
| case MIOOBRecordStatusAsync: | |
| if (sess->status_callback != NULL) { | |
| sess->status_callback(oob->class, oob->results); | |
| } | |
| break; | |
| case MIOOBRecordNotifyAsync: | |
| if (sess->notify_callback != NULL) { | |
| sess->notify_callback(oob->class, oob->results); | |
| } | |
| break; | |
| } | |
| MIListRemove(oobs, (void *)oob); | |
| MIOOBRecordFree(oob); | |
| break; | |
| case MIOOBRecordTypeStream: | |
| switch (oob->sub_type) { | |
| case MIOOBRecordConsoleStream: | |
| if (sess->console_callback != NULL) { | |
| sess->console_callback(oob->cstring); | |
| } | |
| break; | |
| case MIOOBRecordLogStream: | |
| if (sess->log_callback != NULL) { | |
| sess->log_callback(oob->cstring); | |
| } | |
| break; | |
| case MIOOBRecordTargetStream: | |
| if (sess->target_callback != NULL) { | |
| sess->target_callback(oob->cstring); | |
| } | |
| break; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| #ifdef __gnu_linux__ | |
| /* | |
| * Check if there is an oob that is the result of an -exec-interrupt | |
| */ | |
| static int | |
| IsExecAsyncStopped(MISession *sess, MIList *oobs) | |
| { | |
| MIOOBRecord * oob; | |
| for (MIListSet(oobs); (oob = (MIOOBRecord *)MIListGet(oobs)) != NULL; ) { | |
| if (oob->type == MIOOBRecordTypeAsync && | |
| oob->sub_type == MIOOBRecordExecAsync && | |
| strcmp(oob->class, "stopped") == 0) { | |
| return 1; | |
| } | |
| } | |
| return 0; | |
| } | |
| #endif /* __gnu_linux__ */ | |
| void | |
| MISessionGetFds(MISession *sess, int *nfds, fd_set *rfds, fd_set *wfds, fd_set *efds) | |
| { | |
| int n = 0; | |
| if (rfds != NULL) | |
| FD_ZERO(rfds); | |
| if (wfds != NULL) | |
| FD_ZERO(wfds); | |
| if (efds != NULL) | |
| FD_ZERO(efds); | |
| if (sess->pid != -1) { | |
| if (wfds != NULL && sess->in_fd != -1) { | |
| FD_SET(sess->in_fd, wfds); | |
| if (sess->in_fd > n) | |
| n = sess->in_fd; | |
| } | |
| if (rfds != NULL && sess->out_fd != -1) { | |
| FD_SET(sess->out_fd, rfds); | |
| if (sess->out_fd > n) | |
| n = sess->out_fd; | |
| } | |
| /* check if data on pty */ | |
| if (rfds != NULL && sess->pty_fd != -1) { | |
| FD_SET(sess->pty_fd, rfds); | |
| if (sess->pty_fd > n) | |
| n = sess->pty_fd; | |
| } | |
| } | |
| if (nfds != NULL) | |
| *nfds = n + 1; | |
| } | |
| /* | |
| * Default progress command if none supplied. | |
| * | |
| * Don't select on write fds. This will almost always return immediately, | |
| * so the timeout is never used. The end result is a busy wait that | |
| * consumes gobs of CPU. | |
| * | |
| * Also, we make a copy of sess->select_timeout as some select() | |
| * functions will modify it. | |
| */ | |
| int | |
| MISessionProgress(MISession *sess) | |
| { | |
| int n; | |
| int nfds; | |
| fd_set rfds; | |
| struct timeval tv = sess->select_timeout; | |
| MISessionGetFds(sess, &nfds, &rfds, NULL, NULL); | |
| for ( ;; ) { | |
| n = select(nfds, &rfds, NULL, NULL, &tv); | |
| if (n < 0) { | |
| if (errno == EINTR) | |
| continue; | |
| MISetError(MI_ERROR_SYSTEM, strerror(errno)); | |
| return -1; | |
| } | |
| break; | |
| } | |
| /* | |
| * Return if there is nothing to do | |
| */ | |
| if (n == 0) { | |
| if (sess->command != NULL && !sess->command->completed) { | |
| if (sess->command->timeout != 0) { | |
| sess->command->timeout -= (sess->select_timeout.tv_sec * 1000 + sess->select_timeout.tv_usec / 1000); | |
| if (sess->command->timeout <= 0) { | |
| sess->command->timeout = -1; | |
| sess->command->completed = 1; | |
| sess->command = NULL; | |
| return 0; | |
| } | |
| } | |
| } | |
| if (MIListIsEmpty(sess->send_queue)) { | |
| return 0; | |
| } | |
| } | |
| MISessionProcessCommandsAndResponses(sess, &rfds, NULL); | |
| return n; | |
| } |