2013-05-30 19 views
10

devo codice simile al seguente, utilizzando readline:Readline: ottenere un nuovo prompt SIGINT

#include <errno.h> 
#include <error.h> 
#include <getopt.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <signal.h> 
#include <readline/readline.h> 
#include <readline/history.h> 

void handle_signals(int signo) { 
    if (signo == SIGINT) { 
    printf("You pressed Ctrl+C\n"); 
    } 
} 

int main (int argc, char **argv) 
{ 
    //printf("path is: %s\n", path_string); 
    char * input; 
    char * shell_prompt = "i-shell> "; 
    if (signal(SIGINT, handle_signals) == SIG_ERR) { 
    printf("failed to register interrupts with kernel\n"); 
    } 

    //set up custom completer and associated data strucutres 
    setup_readline(); 

    while (1) 
    { 
    input = readline(shell_prompt); 
    if (!input) 
     break; 
    add_history(input); 

    //do something with the code 
    execute_command(input); 

    } 
    return 0; 
} 

li ho impostato in modo da intercettare SIGINT (cioè utente che preme Ctrl+C), così ho può dire che il gestore di segnale handle_signals() sta funzionando. Tuttavia, quando il controllo ritorna a readline(), sta usando la stessa riga di testo che stava usando prima dell'input. Quello che mi piacerebbe accadesse è che readline "cancelli" l'attuale linea di testo e mi dia una nuova riga, proprio come la shell BASH. Qualcosa del genere:

i-shell> bad_command^C 
i-shell> _ 

Qualche possibilità di farlo funzionare? Qualcosa su una mailing list che ho letto menzionato usando longjmp(2), ma non sembra davvero una buona idea.

risposta

5

Sei corretto nel tuo modo di pensare di usare longjmp. Ma poiché longjmp dovrebbe essere in un gestore di segnale, è necessario utilizzare sigsetjmp/siglongjmp.

Come un esempio veloce utilizzando il codice come base:

#include <setjmp.h> 
#include <errno.h> 
#include <error.h> 
#include <getopt.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <signal.h> 
#include <readline/readline.h> 
#include <readline/history.h> 

sigjmp_buf ctrlc_buf; 

void handle_signals(int signo) { 
    if (signo == SIGINT) { 
    printf("You pressed Ctrl+C\n"); 
    siglongjmp(ctrlc_buf, 1); 
    } 
} 

int main (int argc, char **argv) 
{ 
    //printf("path is: %s\n", path_string); 
    char * input; 
    char * shell_prompt = "i-shell> "; 
    if (signal(SIGINT, handle_signals) == SIG_ERR) { 
    printf("failed to register interrupts with kernel\n"); 
    } 

    //set up custom completer and associated data strucutres 
    setup_readline(); 

    while (sigsetjmp(ctrlc_buf, 1) != 0); 

    while (1) 
    { 
    input = readline(shell_prompt); 
    if (!input) 
     break; 
    add_history(input); 

    //do something with the code 
    execute_command(input); 

    } 
    return 0; 
} 

siglongjmp restituisce un valore diverso da 0 (in questo caso un 1) a sigsetjmp modo le chiamate dal ciclo sigsetjmp di nuovo (un valore di ritorno di successo di sigsetjmp è 0) e chiamerà di nuovo readline.

può anche essere utile per impostare rl_catch_signals = 1 e quindi chiamare rl_set_signals() in modo che la gestione del segnale readline pulisce tutte le variabili di cui ha bisogno per prima di passare il segnale al vostro programma dove si poi tornare a chiamare readline una seconda volta.

+1

Non è possibile chiamare in modo sicuro 'printf' da un gestore di segnale. – pat

4

Inizialmente ero confuso dalla risposta di jancheta, finché non ho scoperto che lo scopo di siglongjmp è quello di sbloccare il segnale ricevuto nella maschera del segnale, prima di fare il salto. Il segnale è bloccato all'ingresso del gestore di segnale in modo che il gestore non si interrompa. Non vogliamo lasciare il segnale bloccato quando riprendiamo la normale esecuzione, ed è per questo che usiamo siglongjmp invece di longjmp. AIUI, questa è solo una scorciatoia, potremmo anche chiamare sigprocmask seguito da longjmp, che sembra essere quello che sta facendo glibc in siglongjmp.

Ho pensato che potrebbe essere pericoloso fare un salto perché readline() chiamate malloc e free. Se il segnale viene ricevuto mentre una funzione non sicura del segnale asincrono come malloc o free sta modificando lo stato globale, potrebbe verificarsi un danneggiamento se dovessimo saltare fuori dal gestore del segnale. Ma Readline installa i propri gestori di segnali che sono attenti a questo. Hanno appena impostato una bandiera ed escono; quando la libreria Readline riprende il controllo (di solito dopo una chiamata "read()" interrotta) chiama RL_CHECK_SIGNALS() che poi inoltra qualsiasi segnale in sospeso all'applicazione client utilizzando kill(). Quindi è sicuro usare siglongjmp() per uscire da un gestore di segnale per un segnale che ha interrotto una chiamata a readline() - il segnale è garantito che non è stato ricevuto durante una funzione non sicura del segnale asincrono.

In realtà, non è del tutto vero, perché ci sono un paio di chiamate a malloc() e free() all'interno rl_set_prompt(), che readline() chiamate subito prima rl_set_signals(). Mi chiedo se questo ordine chiamante debba essere cambiato. In ogni caso la probabilità di condizioni di gara è molto ridotta.

Ho guardato il codice sorgente Bash e sembra saltare fuori dal suo gestore SIGINT.

Un'altra interfaccia di Readline che è possibile utilizzare è l'interfaccia di callback. Questo è usato da applicazioni come Python o R che hanno bisogno di ascoltare su più descrittori di file contemporaneamente, ad esempio per dire se una finestra di trama viene ridimensionata mentre l'interfaccia della riga di comando è attiva. Lo faranno in un ciclo select().

Ecco un messaggio da Chet Ramey che dà qualche idea di che cosa fare per ottenere un comportamento Bash simile dopo aver ricevuto SIGINT nell'interfaccia di callback:

https://lists.gnu.org/archive/html/bug-readline/2016-04/msg00071.html

I messaggi suggerisce che si fa qualcosa di simile questo:

rl_free_line_state(); 
    rl_cleanup_after_signal(); 
    RL_UNSETSTATE(RL_STATE_ISEARCH|RL_STATE_NSEARCH|RL_STATE_VIMOTION|RL_STATE_NUMERICARG|RL_STATE_MULTIKEY); 
    rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0; 
    printf("\n"); 

Quando si riceve la vostra SIGINT, è possibile impostare una bandiera, e poi controllare il flag nella tua select() ciclo - dal momento che la chiamata select() otterrà interrotto dal segnale con errno==EINTR. Se trovi che il flag è stato impostato, esegui il codice sopra.

La mia opinione è che Readline dovrebbe eseguire qualcosa come il frammento sopra nel proprio codice di gestione SIGINT. Attualmente esegue più o meno solo le prime due righe, motivo per cui roba simile alla ricerca incrementale e ai macro della tastiera sono cancellate da^C, ma la riga non viene cancellata.

Un altro poster diceva "Chiama rl_clear_signals()", che ancora mi confonde. Non l'ho provato, ma non vedo come potrebbe ottenere nulla dato che (1) I gestori del segnale di Readline inoltrano il segnale comunque, e (2) readline() installa i gestori di segnale all'entrata (e li cancella quando esce), quindi normalmente non saranno attivi al di fuori del codice Readline.

1

La creazione di un salto sembra a me intrusa e soggetta a errori. L'implementazione della shell a cui aggiungevo questo supporto non consentiva questa modifica.

Fortunatamente, readline ha una soluzione alternativa più chiara. Il mio SIGINT gestore si presenta così:

static void 
int_handler(int status) { 
    printf("\n"); // Move to a new line 
    rl_on_new_line(); // Regenerate the prompt on a newline 
    rl_replace_line("", 0); // Clear the previous text 
    rl_redisplay(); 
} 

Questo ha avuto nessun altro codice aggiuntivo altrove per ottenere questo lavoro - nessuna variabile globale, nessuna impostazione salti.