Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

lunedì 22 giugno 2020

C'era una volta un... Allarme
come scrivere un Alarm Manager in C - pt.2

Bruce Lee: C'è un elemento di vero combattimento. Se non lo abbatti... ti uccide.
Cliff Booth: Non se Rick Dalton ha un fucile a pompa.
Ed eccoci alla seconda puntata di C'era una volta a... Hollywood, anzi no: C'era una volta un... Allarme. Cliff"Pitt"Booth, sferzante come suo solito, dice la sua sui metodi di difesa. Beh, noi siamo meno radicali e ci difendiamo con il nostro Alarm Manager (o Alarmer o Gestore di Allarmi, come preferite), la nostra arma vincente contro le situazioni critiche delle applicazioni. Con questo articolo mostreremo l'attesa implementazione delle funzioni di gestione degli allarmi, quelle già intraviste nel header file presentato nella prima parte (a proposito, spero che l'abbiate già letta se no vi faccio punire da Bruce Lee).
...te lo do io l'allarme...
Riepiloghiamo: nella prima parte abbiamo visto le strutture dati implicate nella gestione, e i prototipi delle funzioni implementate. Poi abbiamo visto un semplice esempio d'uso, ossia una applicazione modulare con un main() che lanciava due moduli che correvano in thread separati. I moduli, erano gli utilizzatori finali del Alarmer e, all'avvio, montavano i propri allarmi (cioè li registravano al gestore), per poi usarli in seguito quando necessario. Questa struttura, basata sul montaggio di gruppi di allarmi indipendenti (ogni modulo ha i suoi) è, direi, molto interessante, perché è veramente dinamica: un modulo si registra, si mette al lavoro, usa gli allarmi e, nel caso debba terminare l'attività, può "smontare" i propri allarmi prima di uscire, liberando spazio nella tabella.

Ok, bando alle ciance, so che siete impazienti di vedere l'implementazione delle funzioni... Vai col codice!
#include "alarmer.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>

// prototipi locali
static Alarm *findFirstActiveAlarm(Alarmer *alarmer);
static Alarm *findNextActiveAlarm(Alarmer *alarmer);
static Alarm *findFirstHistAlarm(Alarmer *alarmer);
static Alarm *findNextHistAlarm(Alarmer *alarmer);

// setAlarmer - set valori iniziali alarmer
int setAlarmer(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // init mutex
    int error;
    if (  (error = pthread_mutex_init(&alarmer->onoff_mutex, NULL)) != 0 ||
          (error = pthread_mutex_init(&alarmer->mount_mutex, NULL)) != 0 ||
          (error = pthread_mutex_init(&alarmer->add_mutex,   NULL)) != 0   ) {

        // errore fatale: fermo la inizializzazione
        printf("%s: non posso creare il mutex (%s)\n", __func__, strerror(error));
        return -1;
    }

    // reset pointer della tabella allarmi
    for (int i = 0; i < MAX_ALARMS; i++) {
        // set pointer a 0
        alarmer->alarms_table[i] = 0;
    }

    // reset della tabella allarmi storici
    for (int i = 0; i < MAX_HIST_ALARMS; i++) {
        // reset terminatore di lista
        alarmer->hist_alarms_table[i].id = -1;
    }

    // reset dati per gestione circolare allarmi storici
    alarmer->hist_index     = 0;
    alarmer->num_hist_found = 0;
    alarmer->restart_hist   = false;

    // esco con Ok
    return 0;
}

// mountAlarms - monta un gruppo di allarmi
void mountAlarms(
    Alarmer     *alarmer,   // struttura del Alarmer
    const Alarm *alarms)    // gruppo di allarmi da montare
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->mount_mutex);

    // loop sul gruppo per aggiungere gli allarmi alla tabella
    for (int i = 0; alarms[i].id != -1; i++)
        addAlarm(alarmer, alarms[i].id, alarms[i].owner, alarms[i].fmt);

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->mount_mutex);
}

// addAlarm - aggiunge un allarme nella tabella allarmi
void addAlarm(
    Alarmer    *alarmer,    // struttura del Alarmer
    int        id,          // ID allarme
    int        owner,       // owner allarme
    const char *fmt)        // testo base allarme (formato printf)
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->add_mutex);

    // loop sopra la tabella allarmi per cercare la prima posizione libera
    for (int i = 0; i < MAX_ALARMS; i++) {
        // verifica se la posizione è libera
        if (alarmer->alarms_table[i] == 0) {
            // aggoiunge l'allarme alla tabella
            alarmer->alarms_table[i] = malloc(sizeof(Alarm));

            // set valori
            alarmer->alarms_table[i]->id       = id;
            alarmer->alarms_table[i]->owner    = owner;
            strcpy(alarmer->alarms_table[i]->fmt, fmt);
            alarmer->alarms_table[i]->active   = false;
            alarmer->alarms_table[i]->on_time  = 0;
            alarmer->alarms_table[i]->off_time = 0;

            // unlock della funzione
            pthread_mutex_unlock(&alarmer->add_mutex);

            // fermo la ricerca ed esco
            return;
        }
    }

    // segnalo errore
    printf("%s: errore: nessuna posizione disponibile\n", __func__);

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->add_mutex);
}

// onAlarm - attiva un allarme
void onAlarm(
    Alarmer *alarmer,       // struttura del Alarmer
    int     id,             // ID allarme
    int     owner,          // owner allarme
    ...)                    // lista variabile di argomenti
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->onoff_mutex);

    // loop sopra la tabella allarmi per cercare l'allarme
    for (int i = 0; i < MAX_ALARMS; i++) {
        // test owner/id (solo per allarmi in uso)
        if (    alarmer->alarms_table[i] &&
                alarmer->alarms_table[i]->owner == owner &&
                alarmer->alarms_table[i]->id == id) {

            // allarme trovato: test se già attivo
            if (alarmer->alarms_table[i]->active == false) {
                // set allarme a ON
                alarmer->alarms_table[i]->active  = true;
                alarmer->alarms_table[i]->on_time = time(NULL);

                // compone il testo con gli argomenti multipli
                va_list arglist;
                va_start(arglist, owner);
                char alarmstr[TEXT_SIZE];
                vsnprintf(alarmstr, sizeof(alarmstr), alarmer->alarms_table[i]->fmt,
                          arglist);
                va_end(arglist);

                // copia il testo e interrompe il loop
                strcpy(alarmer->alarms_table[i]->text, alarmstr);
                break;
            }
            else {
                // allarme trovato ma già attivo: interrompe il loop
                break;
            }
        }
    }

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->onoff_mutex);
}

// offAlarm - disattiva un allarme
void offAlarm(
    Alarmer *alarmer,       // struttura del Alarmer
    int     id,             // ID allarme
    int     owner)          // owner allarme
{
    // lock della funzione per uso thread-safe
    pthread_mutex_lock(&alarmer->onoff_mutex);

    // loop to find alarm
    for (int i = 0; i < MAX_ALARMS; i++) {
        // test owner/id (solo per allarmi in uso)
        if (    alarmer->alarms_table[i] &&
                alarmer->alarms_table[i]->owner == owner &&
                alarmer->alarms_table[i]->id == id) {

            // allarme trovato: test se già attivo
            if (alarmer->alarms_table[i]->active == true) {
                // set allarme a OFF
                alarmer->alarms_table[i]->active   = false;
                alarmer->alarms_table[i]->off_time = time(NULL);

                // restart indice allarmi storici se viene raggiunto MAX_HIST_ALARMS
                if (alarmer->hist_index >= MAX_HIST_ALARMS) {
                    alarmer->hist_index   = 0;
                    alarmer->restart_hist = true;
                }

                // inserisce l'allarme nella tabella allarmi storici
                alarmer->hist_alarms_table[alarmer->hist_index] =
                        *alarmer->alarms_table[i];

                // set indice per il buffer circolare degli allarmi storici e break loop
                alarmer->hist_index++;
                break;
            }
            else {
                // allarme trovato ma già OFF: break loop
                break;
            }
        }
    }

    // unlock della funzione
    pthread_mutex_unlock(&alarmer->onoff_mutex);
}

// getAlarms - compone una stringa con tutti gli allarmi attivi
char* getAlarms(
    Alarmer *alarmer,       // struttura del Alarmer
    char*   dest,           // stringa destinazione
    size_t  size)           // size della stringa destinatione
{
    // loop sopra la tabella allarmi per costruire la stringa destinazione
    dest[0] = 0;
    Alarm* active_alarm = findFirstActiveAlarm(alarmer);
    while (active_alarm != NULL) {
        // ricavo il campo on_time
        tzset();    // questo serve se usiamo localtime_r() invece di localtime()
        struct tm tmp;
        char on_time[32];
        strftime(on_time, sizeof(on_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&active_alarm->on_time, &tmp));

        // aggiunge un allarme alla stringa destinazione
        char* my_dest = malloc(sizeof(char[size]));
        snprintf(my_dest, size, "%s%-2d  %-2d  %-80s  %s\n", dest, active_alarm->id,
                 active_alarm->owner, active_alarm->text, on_time);
        snprintf(dest, size, "%s", my_dest);
        free(my_dest);

        // cerca il prossimo allarme attivo
        active_alarm = findNextActiveAlarm(alarmer);
    }

    // ritorna la stringa composta
    return dest;
}

// getHistAlarms - compone una stringa con tutti gli allarmi storici
char* getHistAlarms(
    Alarmer *alarmer,       // struttura del Alarmer
    char*   dest,           // destination string
    size_t  size)           // size of destination string
{
    // loop sopra la tabella allarmi per costruire la stringa destinazione
    dest[0] = 0;
    Alarm* hist_alarm = findFirstHistAlarm(alarmer);
    while (hist_alarm != NULL) {
        // ricavo i campi on_time e off_time
        tzset();    // questo serve se usiamo localtime_r() invece di localtime()
        struct tm tmp;
        char on_time[32];
        strftime(on_time, sizeof(on_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&hist_alarm->on_time, &tmp));
        char off_time[32];
        strftime(off_time, sizeof(off_time), "%m-%d-%Y %H:%M:%S",
                 localtime_r(&hist_alarm->off_time, &tmp));

        // aggiunge un allarme alla stringa destinazione
        char* my_dest = malloc(sizeof(char[size]));
        snprintf(my_dest, size, "%s%-2d  %-2d  %-80s  %s  %s\n", dest,
                 hist_alarm->id, hist_alarm->owner, hist_alarm->text, on_time, off_time);
        snprintf(dest, size, "%s", my_dest);
        free(my_dest);

        // cerca il prossimo allarme storico
        hist_alarm = findNextHistAlarm(alarmer);
    }

    // ritorna la stringa composta
    return dest;
}
Il codice è, come al solito, super-commentato, quindi se lo avete letto con attenzione dovreste già conoscerne tutti i segreti. Come avrete notato abbiamo una funzione di set del Alarmer, la setAlarmer() che è quella che si usa nel main() per inizializzare la struttura base. Poi abbiamo la funzione di mount che serve a montare gli allarmi e che dovrebbe essere la prima attività eseguita da un modulo. Poi abbiamo la funzione di add che aggiunge allarmi alla Alarm Table: la usa internamente solo la funzione di mount nell'implementazione, quindi potrebbe non essere resa pubblica ma, in realtà, usi più sofisticati del nostro Alarm Manager prevedono che si possano aggiungere e togliere allarmi dinamicamente, esternamente alle operazioni di mount, quindi ha senso lasciarla pubblica.

E poi abbiamo le due funzioni che consentono di alzare e abbassare un allarme (ossia: attivare e disattivare) e che sono la onAlarm() e la offAlarm(). Queste funzioni sono quelle che permettono ai moduli utilizzatori di comunicare che c'è un problema o che è stato risolto un problema.

E, infine, abbiamo due funzioni "estetiche", getAlarms() e getHistAlarms(), che ci permettono di leggere, in forma di testo,  la situazione corrente degli allarmi, attivi e storici. Nell'esempio proposto nello scorso articolo venivano usate nel main() per presentare graficamente (a tempo) la situazione dell'esecuzione.

Sicuramente avrete notato la natura  thread-safe dell'implementazione: abbiamo un unico Alarmer, creato nel main thread, e vari moduli utilizzatori in thread separati: le varie attività devono essere quindi protette per evitare situazioni incoerenti. Per cui abbiamo alcuni mutex (creati dalla setAlarmer()) che vengono usati opportunamente per sincronizzare le operazioni.

Uhm... manca qualcosa... ah, si! Le funzioni statiche! I più attenti avranno notato quattro prototipi di funzioni statiche (ossia: che non vengono esportate) che si usano solo internamente all'implementazione del Alarmer: sono le funzioni usate da getAlarms() e getHistAlarms() per scorrere le tabelle e estrarre gli allarmi. Le quattro funzioni usano il classico metodo findfirst + findnext per eseguire il compito, e sono veramente molto semplici. Vai col codice!
// findFirstActiveAlarm - trova il primo allarme attivo nella tabella allarmi
static Alarm* findFirstActiveAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // reset indice
    alarmer->find_active_index = 0;

    // cerca il prossimo allarme attivo
    return findNextActiveAlarm(alarmer);
}

// findNextActiveAlarm - trova il prossimo allarme attivo nella tabella allarmi
static Alarm* findNextActiveAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // loop per cercare il prossimo allarme attivo
    while (alarmer->find_active_index < MAX_ALARMS) {
        // test se attivo (solo sugli allarmi in usos)
        if (    alarmer->alarms_table[alarmer->find_active_index] &&
                alarmer->alarms_table[alarmer->find_active_index]->active) {

            // incrementa indice e ritorna l'allarme trovato
            alarmer->find_active_index++;
            return alarmer->alarms_table[alarmer->find_active_index - 1];
        }

        alarmer->find_active_index++;
    }

    // ritorna allarme non trovato
    return NULL;
}

// findFirstHistAlarm - trova il primo allarme nella tabella allarmi storici
static Alarm* findFirstHistAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // reset contatore e indice
    alarmer->num_hist_found = 0;
    if (!alarmer->restart_hist)
        alarmer->find_hist_index = 0;
    else
        alarmer->find_hist_index = alarmer->hist_index;

    // cerca il prossimo allarme storico
    return findNextHistAlarm(alarmer);
}

// findNextHistAlarm - trova il prossimo allarme nella tabella allarmi storici
static Alarm* findNextHistAlarm(
    Alarmer *alarmer)       // struttura del Alarmer
{
    // restart indice allarmi storici se viene raggiunto MAX_HIST_ALARMS
    if (alarmer->find_hist_index >= MAX_HIST_ALARMS)
        alarmer->find_hist_index = 0;

    // cerca il prossimo allarme storico
    if (    alarmer->num_hist_found < MAX_HIST_ALARMS &&
            alarmer->hist_alarms_table[alarmer->find_hist_index].id != -1) {

        // incrementa contatore e indice e ritorna l'allarme storico trovato
        alarmer->num_hist_found++;
        alarmer->find_hist_index++;
        return &alarmer->hist_alarms_table[alarmer->find_hist_index - 1];
    }

    // ritorna allarme storico non trovato
    return NULL;
}
Come giá anticipato nella prima parte del post, ho omesso alcune funzioni e funzionalità, per snellire un po' la presentazione del codice, quindi mancano la delAlarmer(), la umountAlarm() e la delAlarm(): sono funzioni speculari, rispettivamente, della setAlarmer(), della mountAlarm() e della addAlarm(), e non sono molto complicate da scrivere: fanno esattamente il contrario delle funzioni corrispondenti.

Un capitolo a parte merita il discorso offset e instance che sono commentati nella mountAlarms(), quindi vi ripeto quello che scrissi nella prima parte:

...l'operazione di mount inserisce gli allarmi nelle posizioni opportune della tabella interna, e può gestire anche "gli stessi allarmi" più volte: ad esempio una applicazione che contiene due istanze dello stesso tipo di utilizzatore, avrà due gruppi identici di allarmi montati che saranno differenziati usando i campi instance e offset...

pertanto, questa è una funzionalità molto interessante ma non implementata in questa versione light del Alarm Manager: c'è da aggiungere un po' di codice per farla funzionare e, prima o poi, scriverò una terza parte dell'articolo proprio per completare questo discorso, magari aggiungendo anche le funzioni mancanti citate sopra. Prima o poi, eh! Non siate impazienti...

Tornando al pezzo: vi ripropongo la ricetta finale della prima parte dell'articolo, che ora è, finalmente, possibile realizzare completamente, visto che abbiamo l'implementazione delle funzioni:

...ricetta finale: prendere il main e il tcpServer, aggiungere il modulo udpServer (ispirato al tcpServer), compilare bene, aggiungere un pizzico di sale ed eseguire. Vedrete che ogni 7 secondi si mostreranno gli allarmi attivi e storici, in maniera dinamica. Tra l'altro gli allarmi storici (che hanno, nel demo, un buffer di 8) quando raggiungono il limite cominciano a "circolare", cioè i più vecchi vengono cancellati per far posto ai nuovi. Provare per credere...

Ebbene si, sarà pure una versione light, con qualche funzione (e funzionalità) in meno, ma va liscia come l'olio! Compilate, gente, compilate...

Ciao, e al prossimo post!

Nessun commento:

Posta un commento