Licorice System – come scrivere una system(3) con timeout in C (e C++)

L

Licorice System come scrivere una system(3) con timeout in C (e C++)

Gary: Signore e signori posso avere la vostra attenzione? Lasciate che vi presenti la futura signora Alana Valentine.
Alana: Idiota.

Questo articolo è la seconda parte (non prevista, devo ammetterlo) di System? No, grazie!, quindi dovrebbe ripetere lo stesso titolo; però poco tempo fa ho visto il bel Licorice Pizza del Maestro Paul Thomas Anderson e non ho resistito alla tentazione di agganciarlo a questo post. E, a maggior ragione, l’ho fatto anche perché questo secondo articolo non è più un “No grazie!” ma è una variazione sul tema con una proposta interessante. Interessante come il film in oggetto, pieno di frasi lapidarie come quella vista sopra. Un piccolo gioiellino che ci ha regalato (grazie Paul!) un P.T.Anderson in veste leggera e spensierata, ma sempre a modo suo (ah, divagando: ne ho scritti altri di “No Grazie!” e vi invito a leggerli o rileggerli, quiqui, qui e qui).

Licorice System
…corri, se no scade il timeout della nuova system…

E allora: immagino che tutti avete ben chiari i difetti della system(3) che ne fanno una funzione anti-pattern (e se non li avete chiari correte a rileggere l’articolo, svelti, prima che scappi!). Come ricorderete avevo elencato una lunga serie di problemi, indicando questo come il più grave:

"Quando si chiama la system(3) il programma principale viene sospeso fino al termine del comando invocato, e non c'è nessuna maniera di controllare efficacemente quello che sta succedendo."

e dopo calcavo la mano in questa maniera:

"Ah, solo come curiosità: nella lista dei problemi qui sopra quello che mi molesta di più è il numero 3: una applicazione che usa system(3) non ha maniera di controllare quello che sta succedendo con i programmi esterni invocati, e deve sospendersi per aspettare che finiscano l'esecuzione: ma questa vi sembra la descrizione di una buona applicazione da mandare in produzione? E vabbè, continuiamo cosi, facciamoci del male..."

Per cui, di cosa parleremo oggi? Ma di una system(3) con timeout, proprio quello che ci serve! In realtà l’idea originale era di fare una versione che risolvesse anche tutti gli altri problemi della lista (e in effetti ne ho scritta una) ma non volevo gettare troppa carne al fuoco, e quindi, per il momento, vi propongo questa che risolve il problema più importante (e scusate se è poco!).

Ok, è venuto il momento di far cantare il codice, che è pieno di commenti (che dovrebbero essere sufficienti a spiegare il tutto), ma comunque aggiungerò anche qualche nota in coda. Vai col codice!

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/wait.h>

// prototipi locali
static int  toutSystem(const char *command, unsigned int timeout_ms);
static void mySleep(unsigned int milliseconds);

#define TOUT_SLEEP  500     // intervallo di sleep per il loop busy wait del timeout

// funzione main()
int main(int argc, char* argv[])
{
    // test con uno shell-script ma funziona con qualsiasi comando disponibile
    printf("main: eseguo toutSystem(\"./script.sh > out.txt\")\n");
    toutSystem("./script.sh > out.txt", 5000);

    return 0;
}

// toutSystem() - una system(3) con timeout
static int toutSystem(
    const char   *command,      // il comando shell da eseguire (e.g.: cp -v file1 file2)
    unsigned int timeout_ms)    // timeout per waitpid(2) in ms: 0 significa senza timeout
{
    char errmsg_buf[256];

    // fork + exec + wait
    pid_t pid = fork();
    if (pid == 0) {
        // figlio: eseguo il comando
        printf(
               __func__, getpid());
        execl("/bin/sh", "sh", "-c", command, (char *) NULL);

        // questo viene eseguito solo se fallisce exec (exec non ritorna mai)
        printf(
               __func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
        exit(EXIT_FAILURE);
    }
    else if (pid > 0) {
        // padre: attesa uscita del figlio
        printf(
               __func__, getpid());
        int rc_wait;
        int status;
        if (timeout_ms > 0) {   // check timeout
            // busy wait con timeout
            int cnt_wait = 0;
            while ((rc_wait = waitpid(pid, &status, WNOHANG)) == 0) {
                mySleep(TOUT_SLEEP);
                if (++cnt_wait > timeout_ms / TOUT_SLEEP) {
                    // figlio non uscito prima del timeout: return errore
                    printf(
                           __func__, getpid());
                    return -1;
                }
            }
        }
        else {
            // wait senza timeout
            rc_wait = waitpid(pid, &status, 0);
        }

        // figlio uscito: return risultato
        if (rc_wait != pid) {
            // waitpid error
            printf(
                   __func__, getpid(), strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
            return -1;
        }
        else {
            // processo terminato: return risultato
            int result = -1;
            if (WIFEXITED(status)) {
                // questo è l'unico risultato accettato come successo
                result = 0;
                printf(
                       __func__, getpid(), pid, WEXITSTATUS(status));
            }
            else if (WIFSIGNALED(status))
                printf(
                       __func__, getpid(), pid, WTERMSIG(status));
            else if (WIFSTOPPED(status))
                printf(
                       __func__, getpid(), pid, WSTOPSIG(status));
            else
                printf(
                       __func__, getpid(), pid, status);

            return result;
        }
    }
    else {
        // errore fork
        printf(
               __func__, strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
        return -1;
    }
}

// mySleep() - wrapper per nanosleep()
static void mySleep(unsigned int milliseconds)
{
    struct timespec ts;
    ts.tv_sec = milliseconds / 1000;
    ts.tv_nsec = (milliseconds
    nanosleep(&ts, NULL);
}

Come avrete notato la nuova funzione, che ho battezzato toutSystem() (un nome originalissimo…) è scritta sulla falsariga dell’esempio con  fork(2) + exec(3) + wait(2) del precedente articolo, che era una delle soluzioni proposte come buona alternativa alla system(3) e che fa al caso nostro per sviluppare questa nuova versione.

Il funzionamento è (relativamente) semplice: la funzione esegue fork(2) e si sdoppia in padre e figlio. Il figlio esegue il comando <command> esattamente come lo fa, internamente, la system(3), per cui usa execl(3) per creare una sub-shell di esecuzione (e fino a qui siamo abbastanza in linea con la system(3) e con alcuni dei i suoi difetti, sigh). La parte buona, però la esegue il padre che, invece di limitarsi ad aspettare l’uscita del figlio, esegue un loop in stile busy wait  usando in maniera “intelligente” waitpid(3) per un tempo mai superiore ai millisecondi indicati dall’argomento <timeout_ms>. Alla fine del loop si testa il risultato dell’attesa per decidere se ritornare un errore o un esito, e il risultato che ci preme è stato conseguito: la toutSystem() non può bloccare indefinitamente il nostro programma (al contrario di quello che può fare la famigerata system(3)), ma lo blocca al massimo per la durata del timeout. E, comunque, ho lasciato la possibilità di simulare il comportamento “classico” (ossia: attesa indefinita) usando zero come timeout.

L’esempio qui sopra contiene anche un main() di prova, così gli increduli potranno compilare e verificare direttamente il funzionamento. Si può eseguire qualsiasi comando: io nell’esempio ho eseguito uno shell script che ho scritto proprio per verificare l’efficacia del timeout. Lo script (che poteva essere anche un semplice codice C compilato, eh!) è questo:

#!/bin/bash

for i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
do
    echo $i
    sleep 1
done

Lo script scrive per 15 secondi un numero progressivo nel file “out.txt” su cui è rediretto il comando di toutSystem() nel main(): toutSystem(“./script.sh > out.txt”, 5000). Se compilate ed eseguite l’output sarà questo:

main: eseguo toutSystem("./script.sh > out.txt", 5000)
toutSystem: padre: processo 16463: attesa uscita del figlio
toutSystem: figlio: processo 16464: eseguo il comando
toutSystem: padre: processo 16463: waitpid timeout scaduto

che indica che la nostra chiamata a toutSystem() è uscita per timeout dopo 5 secondi (lo script eseguito lavora per 15 secondi, quindi è ancora attivo in quel momento), e difatti noterete che il file “out.txt” si riempirà 10 secondi dopo l’uscita del programma principale. Provare per credere!

Ok, per oggi può bastare: la toutSystem() è già perfettamente utilizzabile nella forma proposta, ed è una ottima alternativa all’uso della system(3) che, non mi stanco mai di dirlo, non si dovrebbe mai usare. Vi consiglio,  come utile esercitazione, di provare a modificare la toutSystem() per ovviare anche agli altri problemi elencati nell’articolo System? No, grazie!. Io l’ho fatto e vi assicuro che non è un lavoro molto complicato (e prossimamente vi mostrerò la mia soluzione, promesso).

Ciao, e al prossimo post!

A proposito di me

Aldo Abate

È un Cinefilo prestato alla Programmazione in C. Per mancanza di tempo ha rinunciato ad aprire un CineBlog (che richiede una attenzione quasi quotidiana), quindi ha deciso di dedicarsi a quello che gli riesce meglio con il poco tempo a disposizione: scrivere articoli sul suo amato C infarcendoli di citazioni Cinematografiche. Il risultato finale è ambiguo e spiazzante, ma lui conta sul fatto che il (si spera) buon contenuto tecnico induca gli appassionati di C a leggere ugualmente gli articoli ignorando la parte Cinefila.
Ma in realtà il suo obiettivo principale è che almeno un lettore su cento scopra il Cinema grazie al C...

Di Aldo Abate

Gli articoli più letti

Articoli recenti

Commenti recenti