0

I am trying to write a minimal implementation of a shell for a school project. To write such an implementation I use the readline function and I have to handle SIGINT so that I display a new prompt on a new line when it occurs within the program life (which could be at two key moments that are just after the user input has been read (beginning of the while loop) or after the line has been executed (end of the loop). First a minimal reproducible example of my problem (with some prints for me to better understand what happens. I left the comments explaining what I tried and will explain them right after the chunk of code you need to see to understand.

#include <signal.h>
#include <sys/wait.h>
#include <stdio.h>
#include <readline/readline.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#define RED "\e[0;31m"
#define GRN "\e[0;32m"
#define CRESET "\e[0m"

volatile sig_atomic_t   g_termsig;

extern char **environ;

int event_hook(void)
{
    return (0);
}

int init_sig(void (*handler)(int, siginfo_t *, void *), const int signum)
{
    struct sigaction    sa;

    bzero(&sa, sizeof sa);
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;
    //if (sigemptyset(&sa.sa_mask) || sigaddset(&sa.sa_mask, signum))
    //  return (1);
    return (sigaction(signum, &sa, NULL));
}

_Noreturn void  exit_message(const char *message, const int status)
{
    const char  *color;

    color = RED;
    if (! status)
        color = GRN;
    fprintf(stderr, "%s%s%s\n", color, message, CRESET);
    exit(status);
}

void    interrupt_handler(int sig, siginfo_t *info, void *ucontext)
{
    (void)info;
    (void)ucontext;
    g_termsig = 128 + sig;
    rl_done = 1;
    /*
        if (! rl_done)
            ioctl(0, TCSTI, "\n");
    */
    //fprintf(stdout, "\nThis is readline_buffer : %s\n", rl_line_buffer);
}

void    restore_display(void)
{
    fprintf(stdout, "Here is yo signal : %d\n", g_termsig);
    g_termsig = 0;
    /*
    fprintf(stdout, "In function : %s\nThis is readline_buffer : %s\n", \
    __func__, rl_line_buffer);
    */
//  rl_replace_line("", 0);
    //fprintf(stdout, "This is the new line buffer : %s\n", rl_line_buffer);
//  rl_on_new_line();
//  rl_redisplay();
}

int     execute_line(const char *line)
{
    pid_t   child_pid;
    char    *args[2] = {(char *)line, NULL};
    int     status;

    child_pid = fork();
    /*Here we set status to 0x100 so that WIFEXITED(status) is true 
    and WEXITSTATUS(status) is -1*/
    status = 0x100; 
    if (child_pid < 0)
        exit_message("No fork my G, gotta spoon dis time\n", 1);
    if (! child_pid)
    {
        signal(SIGQUIT, SIG_DFL);
        signal(SIGINT, SIG_DFL);
        execve(line, args, environ);
        exit_message("Dude wtf?! Is that even a real command you typed?\n", 1);
    }
    wait(&status);
    return (status);
}

int main(void)
{
    char    *line;

    signal(SIGQUIT, SIG_IGN);
    if (init_sig(interrupt_handler, SIGINT))
        exit_message("Yo, can't setup handler my G\n", 1);
    /*
    printf("This is readline output stream : %p\n", rl_outstream);
    printf("This is the address of rl_startup_hook : %p\n", rl_startup_hook);
    printf("This is the address of rl_pre_input_hook : %p\n", rl_pre_input_hook);
    printf("This is the address of rl_event_hook : %p\n", rl_event_hook);
    */
    rl_event_hook = event_hook;
    while (1)
    {
        line = readline("Enter yo commands> ");
        if (g_termsig)
        {
            free(line);
            restore_display();
            continue ;
        }
        if (! line)
            break ;
        execute_line(line);
        //It doesn't happen with that example but in the rest of my code
        //g_termsig could be set in the execute_line function
        if (g_termsig)
            restore_display();
        free(line);
        line = NULL;
    }
    exit_message("Everything good\n", 0);
}

So the thing is that I wanted the readline function to quit when SIGINT was received. I first settled for using ioctl with the TCSTI request (which is detailed in man 2 ioctl_tty) to fake a user input in the handler. I finally (after reading this question and reading a good amount of this documentation settled for an event hook that seems to work better.

So far so good you'll tell me (in fact I hope not and that you'll be able to correct me from here). The matter is that when I input for example /usr/bin/cat and kill it with ^C then the prompts get all messed up with the commands output. For example when I run /usr/bin/cat and then /usr/bin/ls or some non working command, it doesn't matter, the prompt get messed up with the ouput. I try a combination of rl_redisplay, rl_on_new_line and rl_replace_line functions (the only ones from readline library I am allowed to use in this project) but nothing seems to do the trick so I am a bit lost. Thanks in advance for reading me and taking time to share your knowledge with me

lazydev
  • 154
  • 1
  • 1
  • 8
  • have you read [this question](https://stackoverflow.com/questions/16828378/readline-get-a-new-prompt-on-sigint)? – Barmar Aug 19 '23 at 22:00
  • A line editing library should not make you deal with the SIGINT signal. Find a better one. Line editing puts the TTY in raw mode, so that Ctrl-C is received as an ordinary character. While line editing is going on, there is no SIGINT; it is used only for interrupting the execution of the commands in your own code. – Kaz Aug 20 '23 at 03:04
  • @Barmar Yes I did but I am unfortunately not allowed to use such jumps. I noticed while reading bash source code for signal handling that bash doesn't do it either so I don't think it is necessary to handle such events – lazydev Aug 20 '23 at 07:58
  • @Kaz there is indeed a SIGINT when readline is waiting for user input and my handler executes properly. The thing is that I must be resetting variables in the wrong order because my prompt get messed up when I interrupt an executable launched with execve (and not when I don't launch an executable nor when my executable finishes without being interrupted) – lazydev Aug 20 '23 at 08:00
  • @lazydev What if you just put the TTY into raw mode yourself around calls to readline. I wonder would readline keep that, part of the state or would it reset those states, putting the TTY in a mode where it performs Ctrl_C SIGINT? – Kaz Aug 20 '23 at 18:47

1 Answers1

1

So I finally got the answer. The thing is that when the process was forked (let's say we ran /usr/bin/cat) and then interrupted with SIGINT the wait called failed and the parent was unwaited for (thus creating a zombie and messing will all the other wait calls). If we ran after that command, a command such as /usr/bin/ls the process encountered the previous zombie that had been unwaited for, returned early, printed the prompt and only after the ls command printed its output. The solution is to set sa.sa_flags = SA_SIGINFO | SA_RESTART (in function init_sig) so that the wait call will be restarted when SIGINT is received.

I hope my troubles will help some other people, thanks to those who tried to come up with hints and workarounds

lazydev
  • 154
  • 1
  • 1
  • 8