Subversion Repositories Code-Repo

Rev

Blame | Last modification | View Log | RSS feed

/*
 * esh - the 'pluggable' shell.
 *
 * Developed by Godmar Back for CS 3214 Fall 2009
 * Virginia Tech.
 */
#include <stdio.h>
#include <readline/readline.h>
#include <unistd.h>
#include <sys/wait.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "esh.h"
#include "esh-sys-utils.h"

// Function declarations
static void usage(char *progname);
static char * build_prompt_from_plugins(void);

static struct list *get_job_list(void);
static struct esh_pipeline *job_from_jid(int jid);
static struct esh_pipeline *job_from_pgrp(pid_t pgrp);
static struct esh_command *cmd_from_pid(pid_t pid);

static void print_jobs(void);
static void purge_jobs(void);
static void wait_for_job(struct esh_pipeline *pipeline,bool exists_in_job_list);
static void send_signal_to_job(int jid, int sig);
static void bring_to_foreground(int jid);
static void give_terminal_to(pid_t pgrp, struct termios *pg_tty_state);

static void sigchld_handler(int sig, siginfo_t *info, void *_ctxt);

static int job_next_id = 1;
static struct termios *sys_tty;
static pid_t shell_pgrp;

// List of all jobs running in the current shell
static struct esh_command_line *job_list;

/* The shell object plugins use.
 * Some methods are set to defaults. */
struct esh_shell shell = {
    .get_jobs = get_job_list,
    .get_job_from_jid = job_from_jid,
    .get_job_from_pgrp = job_from_pgrp,
    .get_cmd_from_pid = cmd_from_pid,
    .build_prompt = build_prompt_from_plugins,
    .readline = readline,       /* GNU readline(3) */ 
    .parse_command_line = esh_parse_command_line /* Default parser */
};

int main(int ac, char *av[]) {
    int opt;
    list_init(&esh_plugin_list);
    int i = 0;

    // Create an empty list of pipelines (jobs)
    job_list = esh_command_line_create_empty();
    // Obtain an empty (sane) terminal
    sys_tty = esh_sys_tty_init();
    // Store the process group of the shell
    shell_pgrp = getpgrp();

    /* Process command-line arguments. See getopt(3) */
    while ((opt = getopt(ac, av, "hp:")) > 0) {
        switch (opt) {
        case 'h':
            usage(av[0]);
            break;

        case 'p':
            esh_plugin_load_from_directory(optarg);
            break;
        }
    }

    esh_plugin_initialize(&shell);

    /* Read/eval loop. */
    for (;;) {
        // Ensure that the shell has access to input/output before prompting
        give_terminal_to(shell_pgrp, sys_tty);

        /* Do not output a prompt unless shell's stdin is a terminal */
        char * prompt = isatty(0) ? shell.build_prompt() : NULL;
        char * cmdline = shell.readline(prompt);
        free (prompt);

        if (cmdline == NULL)  /* User typed EOF */
            break;

        struct esh_command_line * cline = shell.parse_command_line(cmdline);
        free (cmdline);
        if (cline == NULL)                  /* Error in command line */
            continue;

        if (list_empty(&cline->pipes)) {    /* User hit enter */
            esh_command_line_free(cline);
            continue;
        }

        esh_signal_sethandler(SIGCHLD, sigchld_handler);
        esh_signal_block(SIGCHLD);

        // Pop each pipeline, parse it, then execute the commands
        while (!list_empty(&cline->pipes)) {
            struct list_elem *l = list_pop_front(&cline->pipes);
            struct esh_pipeline *pipeline = list_entry(l, struct esh_pipeline, elem);

            // Initialize some variables in the pipeline
            pipeline->jid = 0;
            pipeline->pgrp = 0;
            memset(pipeline->cmd_string, 0, 256);

            // Initialize array of pipes
            int p_pipe_ind = 0;
            int p_pipe_max = list_size(&pipeline->commands) - 1;
            int **pipes = (int**) malloc(p_pipe_max*sizeof(int*));
            if (pipes == NULL) esh_sys_fatal_error("malloc: ");
            for (i = 0; i < p_pipe_max; i++) {
                pipes[i] = (int *) malloc(2);
                if (pipes[i] == NULL) esh_sys_fatal_error("malloc: ");
                if (pipe(pipes[i]) == -1) esh_sys_fatal_error("pipe: ");
            }

            // Concat the command string containing the parsed commands
            struct list_elem *c = list_begin(&pipeline->commands);
            for (; c != list_end(&pipeline->commands); c = list_next(c)) {
                struct esh_command *cmd = list_entry(c, struct esh_command, elem);
                char **p = cmd->argv;
                while (*p) {
                    strcat(pipeline->cmd_string, *p++);
                    if (*p)
                        strcat(pipeline->cmd_string, " ");
                }
                if (list_next(c) != list_tail(&pipeline->commands))
                    strcat(pipeline->cmd_string, " | ");    
            }
            // printf("Command string: %s\n", pipeline->cmd_string);
            
            // Get the first command
            c = list_begin(&pipeline->commands);
            struct esh_command *cmd = list_entry(c, struct esh_command, elem);
            char **p = cmd->argv;

            // Check if the first command is a built in command
            if (strcmp(*p, "jobs") == 0) {
                print_jobs();
                purge_jobs();
                continue;
            } else if (strcmp(*p, "fg") == 0) {
                bring_to_foreground(atoi(cmd->argv[1]));
                continue;
            } else if (strcmp(*p, "bg") == 0) {
                send_signal_to_job(atoi(cmd->argv[1]), SIGCONT);
                continue;
            } else if (strcmp(*p, "kill") == 0) {
                send_signal_to_job(atoi(cmd->argv[1]), SIGKILL);
                continue;
            } else if (strcmp(*p, "stop") == 0) {
                send_signal_to_job(atoi(cmd->argv[1]), SIGSTOP);
                continue;
            } else if (strcmp(*p, "exit") == 0) {
                exit(0);
            }

            // Otherwise parse and execute each command
            for (; c != list_end(&pipeline->commands); c = list_next(c)) {
                cmd = list_entry(c, struct esh_command, elem);
                p = cmd->argv;

                // If it is not a built in command, fork the process and let it run
                if ((cmd->pid = fork()) == 0) {
                    // Set the group process ID to the same value for each pipeline
                    setpgid(0,pipeline->pgrp);
                    esh_signal_unblock(SIGCHLD);
       
                    if (p_pipe_max) {
                        // Set up the pipelines if there are more than one command
                        if (p_pipe_ind == 0) {
                            // First command
                            if (dup2(pipes[p_pipe_ind][1], 1) < 0) 
                                esh_sys_fatal_error("dup2: ");
                            // Set stdin of the first process to the input file if it is specified
                            if (pipeline->iored_input) {
                                if (dup2(open(pipeline->iored_input, O_RDONLY), 0) < 0) 
                                    esh_sys_fatal_error("dup2: ");
                            }
                        } else if (p_pipe_ind == p_pipe_max) {
                            // Last command
                            if (dup2(pipes[p_pipe_ind-1][0], 0) < 0) 
                                esh_sys_fatal_error("dup2: ");
                            // Set stdout of the final process to the output file if it is specified
                            if (pipeline->iored_output) {
                                // Check if we are creating a new file or appending to an existing file
                                if (pipeline->append_to_output) {
                                    if (dup2(open(pipeline->iored_output, O_WRONLY|O_APPEND), 1) < 0) 
                                        esh_sys_fatal_error("dup2: ");
                                } else {
                                    if (dup2(creat(pipeline->iored_output, 0600), 1) < 0) 
                                        esh_sys_fatal_error("dup2: ");
                                }
                            }
                        } else {
                            // All other commands in between
                            if (dup2(pipes[p_pipe_ind-1][0], 0) < 0) 
                                esh_sys_fatal_error("dup2: ");
                            if (dup2(pipes[p_pipe_ind][1], 1) < 0) 
                                esh_sys_fatal_error("dup2: ");
                        }
                        // Close all  pipes
                        for (i = 0; i < p_pipe_max; i++) {
                            if (close(pipes[i][0]) == -1)
                                esh_sys_fatal_error("close: ");
                            if (close(pipes[i][1]) == -1)
                                esh_sys_fatal_error("close: ");
                        }
                    } else {
                        // If there is only one command, set stdin to input file if it is specified
                        if (pipeline->iored_input) {
                            if (dup2(open(pipeline->iored_input, O_RDONLY), 0) < 0) 
                                esh_sys_fatal_error("dup2: ");
                        }
                        // Set stdout of the process to the output file if it is specified
                        if (pipeline->iored_output) {
                            // Check if we are creating a new file or appending to an existing file
                            if (pipeline->append_to_output) {
                                if (dup2(open(pipeline->iored_output, O_WRONLY|O_APPEND), 1) < 0) 
                                    esh_sys_fatal_error("dup2: ");
                            } else {
                                if (dup2(creat(pipeline->iored_output, 0600), 1) < 0) 
                                    esh_sys_fatal_error("dup2: ");
                            }
                        }
                    }
                    
                    if (execvp(cmd->argv[0],&(cmd->argv[0])) < 0)
                        printf("%s: Command not found.\n",cmd->argv[0]);
                    return 1;
                } 
                if (cmd->pid == -1)
                    esh_sys_fatal_error("fork: ");

                // Make sure the terminal is in its own process group
                if (setpgid(0,0) == -1) 
                    esh_sys_error("setpgid: ");

                // Store the process group ID
                if (pipeline->pgrp == 0)
                    pipeline->pgrp = cmd->pid;

                // Increment the pipe counter
                if (p_pipe_max) {
                    p_pipe_ind++;
                }
            }

            // Close all  pipes
            for (i = 0; i < p_pipe_max; i++) {
                if (close(pipes[i][0]) == -1)
                    esh_sys_fatal_error("close: ");
                if (close(pipes[i][1]) == -1)
                    esh_sys_fatal_error("close: ");
            }

            if (!pipeline->bg_job) {
                // If the process is going to be executed in the background, give it the terminal
                pipeline->status = FOREGROUND;
                esh_sys_tty_save(sys_tty);
                give_terminal_to(pipeline->pgrp, NULL);
                wait_for_job(pipeline, false);
            } else {
                // Otherwise save the job in the job list and move on
                pipeline->status = BACKGROUND;
                // Assign the pipeline a job ID
                pipeline->jid = job_next_id;
                job_next_id++;
                list_push_back(shell.get_jobs(),l);
                printf("[%d] %d\n", pipeline->jid, pipeline->pgrp);
            }
        }

        esh_signal_unblock(SIGCHLD);
        esh_command_line_free(cline);
    }
    return 0;
}

static void usage(char *progname) {
    printf("Usage: %s -h\n"
        " -h            print this help\n"
        " -p  plugindir directory from which to load plug-ins\n",
        progname);

    exit(EXIT_SUCCESS);
}

/* Build a prompt by assembling fragments from loaded plugins that 
 * implement 'make_prompt.'
 *
 * This function demonstrates how to iterate over all loaded plugins.
 */
static char * build_prompt_from_plugins(void) {
    char *prompt = NULL;
    struct list_elem * e = list_begin(&esh_plugin_list);

    for (; e != list_end(&esh_plugin_list); e = list_next(e)) {
        struct esh_plugin *plugin = list_entry(e, struct esh_plugin, elem);

        if (plugin->make_prompt == NULL)
            continue;

        /* append prompt fragment created by plug-in */
        char * p = plugin->make_prompt();
        if (prompt == NULL) {
            prompt = p;
        } else {
            prompt = realloc(prompt, strlen(prompt) + strlen(p) + 1);
            strcat(prompt, p);
            free(p);
        }
    }

    /* default prompt */
    if (prompt == NULL)
        prompt = strdup("esh> ");

    return prompt;
}

/* Shell function to return the list of jobs running in the shell */
static struct list *get_job_list(void) {
    return &job_list->pipes;
}

/* Shell function to return a job given a job ID */
static struct esh_pipeline *job_from_jid(int jid) {
    struct list_elem * p = list_begin (get_job_list()); 
    for (; p != list_end (get_job_list()); p = list_next (p)) {
        struct esh_pipeline *pipe = list_entry(p, struct esh_pipeline, elem);
        if (pipe->jid == jid)
            return pipe;
    }
    return NULL;
}

/* Shell function to return a job given a process group */
static struct esh_pipeline *job_from_pgrp(pid_t pgrp) {
    struct list_elem * p = list_begin (get_job_list()); 
    for (; p != list_end (get_job_list()); p = list_next (p)) {
        struct esh_pipeline *pipe = list_entry(p, struct esh_pipeline, elem);
        if (pipe->pgrp == pgrp)
            return pipe;
    }
    return NULL;
}

/* Shell function to return a command given a PID */
static struct esh_command *cmd_from_pid(pid_t pid) {
    struct list_elem * p = list_begin (get_job_list()); 
    for (; p != list_end (get_job_list()); p = list_next (p)) {
        struct esh_pipeline *pipe = list_entry(p, struct esh_pipeline, elem);
        struct list_elem *c = list_begin(&pipe->commands);
        for (; c != list_end(&pipe->commands); c = list_next(c)) {
            struct esh_command *cmd = list_entry(c, struct esh_command, elem);
            if (cmd->pid == pid)
                return cmd;
        }
    }
    return NULL;
}

/* Prints out the current list of jobs in the shell and their statuses */
static void print_jobs(void) {
    struct list_elem * p = list_begin (shell.get_jobs()); 
    for (; p != list_end (shell.get_jobs()); p = list_next (p)) {
        struct esh_pipeline *pipe = list_entry(p, struct esh_pipeline, elem);

        // For each job, print out [JobID] Status (Command String)
        printf("[%d] ", pipe->jid);
        if (pipe->status == FOREGROUND || pipe->status == BACKGROUND)
            printf("Running  ");
        else if (pipe->status == COMPLETED)
            printf("Done  ");
        else if (pipe->status == TERMINATED)
            printf("Terminated  ");
        else if (pipe->status == STOPPED)
            printf("Stopped  ");
        else
            printf("Unknown State  ");

        printf("(%s)\n", pipe->cmd_string);
    }
}

/* Removes completed or terminated jobs from the job list */
static void purge_jobs(void) {
    struct list_elem * p = list_begin (shell.get_jobs()); 
    for (; p != list_end (shell.get_jobs()); p = list_next (p)) {
        struct esh_pipeline *pipe = list_entry(p, struct esh_pipeline, elem);
        if (pipe->status == COMPLETED || pipe->status == TERMINATED) {
            list_remove(p);
        }
    }
    // Also reset the job ID if the job list is empty
    if (list_empty(shell.get_jobs())) {
        job_next_id = 1;
    }
}

/* Waits for the specified job to finish */
static void wait_for_job(struct esh_pipeline *pipeline, bool exists_in_job_list) {
    assert(esh_signal_is_blocked(SIGCHLD));

    // Loop till all commands in the pipeline is done executing
    while (pipeline->status == FOREGROUND && !list_empty(&pipeline->commands)) {
        int status;

        // Wait till a child process exits
        pid_t child = waitpid(-1, &status, WUNTRACED);
        if (child != -1) {
            // If child has exited or terminated, remove the child from the pipeline
            if (WIFEXITED(status) || WIFSIGNALED(status)) {
                struct list_elem * e = list_begin (&pipeline->commands); 
                for (; e != list_end (&pipeline->commands); e = list_next (e)) {
                    struct esh_command *cmd = list_entry(e, struct esh_command, elem);
                    if (child == cmd->pid)
                        list_remove(e);
                }
            }
            // If child was stopped (INTPSTP), put it in the job list and print out status
            if (WIFSTOPPED(status)) {
                pipeline->status = STOPPED;
                if (pipeline->jid == 0) {
                    pipeline->jid = job_next_id;
                    job_next_id++;
                }
                if (!exists_in_job_list)
                    list_push_back(shell.get_jobs(), &pipeline->elem);
                printf("\n[%d]  Stopped  (%s)\n", pipeline->jid, pipeline->cmd_string);
                return;
            }
        }
    }
    pipeline->status = COMPLETED;
}

/* Sends the specified signal to the specified job ID */
static void send_signal_to_job(int jid, int sig) {
    struct esh_pipeline *pipe = job_from_jid(jid);
    if (killpg(pipe->pgrp, sig) == -1)
        esh_sys_fatal_error("killpg: ");
}

/* Brings a job to the foreground */
static void bring_to_foreground(int jid) {
    struct esh_pipeline *pipe = job_from_jid(jid);
    printf("%s\r\n", pipe->cmd_string);
    pipe->status = FOREGROUND;

    // Give the foreground process the terminal
    give_terminal_to(pipe->pgrp, NULL);
    // Resume the process
    send_signal_to_job(jid, SIGCONT);
    // Wait for process to finish
    wait_for_job(pipe, true);
}

/* Gives the terminal to the specified process group */
static void give_terminal_to(pid_t pgrp, struct termios *pg_tty_state) {
    esh_signal_block(SIGTTOU);
    // printf("Terminal to process group %d\n", pgrp);
    int rc = tcsetpgrp(esh_sys_tty_getfd(), pgrp);
    if (rc == -1)
        esh_sys_fatal_error("tcsetpgrp: ");

    if (pg_tty_state)
        esh_sys_tty_restore(pg_tty_state);
    esh_signal_unblock(SIGTTOU);
}

/* Handler for SIGCHLD */
static void sigchld_handler(int sig, siginfo_t *info, void *_ctxt) {
    pid_t child;
    int status;

    assert(sig == SIGCHLD);

    while ((child = waitpid(-1, &status, WUNTRACED|WNOHANG|WCONTINUED)) > 0) {
        // printf("Received signal %d from process %d\n",sig, child);
        struct esh_command *cmd = shell.get_cmd_from_pid(child);
        // Check the status of the child and update status accordingly
        if (WIFEXITED(status)) {
            cmd->pipeline->status = COMPLETED;
        } else if (WIFSIGNALED(status)) {
            cmd->pipeline->status = TERMINATED;
        } else if (WIFCONTINUED(status)) {
            cmd->pipeline->status = BACKGROUND;
        } else if (WIFSTOPPED(status)) {
            cmd->pipeline->status = STOPPED;
        }
    }
}