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!
https://italiancoders.it/cera-una-volta-un-allarme-come-scrivere-un-alarm-manager-in-c-pt-2/