UNIX daemon kit és tutorial, C-ben és Pascalban

2019. június 11. 8:36, hup.hu

Nem tudom hány embernek és mennyire lesz ez hasznos, de gondoltam írok egy tutorialt arról, hogy hogyan kell összerakni egy UNIX daemont, meg adok hozzá egy kitet is, hátha jól jön valakinek... Azt feltételezem mindenki tudja, hogy mi a daemon, de azt, hogy ez a gyakorlatban - értsd: programozás szempontjából - hogyan is működik, azt már lehet, hogy kevesebben. Nos, a daemonok kutyaközönséges process-ek, amiket közvetlenül az init futtat, terminálkapcsolat - alapesetben - nincs, a user a különféle signalokon keresztül adhat parancsot nekik. (Legalábbis általában, természetesen lehet egyéb megközelítéssel is, de így szokták.) Ezt kétféleképpen lehet elérni: vagy tényleg az init indítja el őket, vagy indításkor a process a fork() meghívásával készít egy másodpéldányt magából, majd kiszáll, így az init megörökli az elárvult másodpéldányt. Ezen felül persze még pár dolgot meg kell csinálni, amiken most szisztematikusan végig fogunk menni. Kétféle kóddal illusztrálom a folyamatot, egy C-ben és egy Pascalban írttal - bár nem hiszem, hogy túl sokan mérgeznék itt magukat Pascallal rajtam kívül, de ha mégis, hát kutyaharapást macskával... A C-s példákhoz írom az include-okat is, a Pascal-os példákban egyelőre csak a "baseunix" unit kell, hogy benne legyen az uses-ben. Mint azt előbb kitárgyaltuk, a "daemonizált" process-t a fork() segítségével hozzuk létre, aztán kiszállunk. A fork() a parent process-nek a child process process ID-jét adja vissza sikeres forkolás esetén, amúgy meg egy jó nagy -1-et, a child process-nek pedig 0-át, ennek megfelelően a 0-át nem kell lekezelni, csak az attól eltérő eredményeket. C-ben: #include <unistd.h> ... pid_t pid = fork(); if (pid == -1) { // hibakezelés } if (pid != 0) { exit(0); } Pascalban: var pid: tpid; ... pid := fpfork; if (pid = -1) then begin // hibakezelés end; if (pid <> 0) then begin halt(0); end; Ezzel létrehoztuk programunknak a PID1 (init) által futtatott másodpéldányát. Igen ám, csak az init alapesetben a megöröklött child process-eket egy laza SIGHUP-pal az örök folyamatmezőkre küldi, lévén a parent volt a session leader és ha a leader kiszáll, a session maga is befejeződik és az összes foreground tag megy a levesbe (hacsak nem ignorálják a SIGHUP-ot). Ezt preventálandó, létrehozunk egy új session-t a setsid() segítségével, aminek a leaderje már a child process lesz és ezzel, hogy leváltunk a régi munkamenetről, leváltunk a terminálról és visszakaptuk a promptot: a folyamat a háttérbe került. C-ben setsid(), Pascalban fpsetsid, mindkét esetben, ha -1-et kapunk, akkor történt hiba. Ezzel tulajdonképpen már kész is az élő daemon process, legalábbis abban az értelemben, hogy a kötelező köröket lefutottuk: sikerrel leválasztottuk a programot a terminálról és most már a háttérben fut a PID1 égisze alatt, de azért még hátra van egy-két tisztázandó opcionális dolog. De mielőtt ezekbe belemennénk, tisztázzuk le az esedékes double-fork kérdését is. Számos manual és tutorial ezen a ponton előír egy második fork() meghívást - plusz a SIGCHLD és SIGHUP signalok ignorálását - is, amit azzal indokolnak, hogy egyfelől így lehet preventálni a zombie-process létrejöttét, másfelől pedig, hogy a session leader-ré vált child process ne "szerezhessen" ismét vezérlőterminált, hanem helyette a már nem-session leader grandchild fusson tovább, ami nem is szerezhet. Az első indok egyáltalán nem igaz, lévén a zombie-process egy olyan befejeződött child process entry-je a process táblában, aminek a parentje azelőtt kiszállt, hogy a child process visszatérési kódját kiolvasta volna, márpedig a mi child processünk nem fejezte be a futását, a szülője pedig már az init és nem a korábban kiszállt parent process, tehát semmi esetre sem lesz zombi a daemonból, feltéve, hogy a forkoló parent kiszállt. A második indok már lehetne igaz, mert a session leader valóban megnyithat egy új terminal device file-t, ami ha még nincs session-höz rendelve, akkor - a rendszer implementációjától függően - vezérlőterminállá válhat, csakhogy ehhez egyfelől az kell, hogy szándékosan nyissunk egyet, mert nyilván nem magától nyitogat terminal device-okat a daemon, másfelől meg még ha szükség is van egyre, akkor is preventálni lehet a vezérlőterminállá válást, ha a descriptort O_NOCTTY opcióval nyitjuk meg. Ezeknek megfelelően a double-forkra nincs szükség. És akkor most térjünk vissza a daemonizációs folyamat még hátralévő elemeire. Az egyik az az, hogy a child megörökölte az umask-ot a parent-től, márpedig ez egy daemon és azt szeretnénk, hogy ha kiadunk egy permission mask-ot valami parancsnak, akkor az is lenne a beállított mask. Ezt azzal érhetjük el, ha az umask-ot kinullázzuk. Ezt C-ben - a <sys/stat.h> include-olása után - az umask(0), Pascalban pedig az fpumask(0) paranccsal érhetjük el. (A visszatérési érték itt a korábbi umask, hibakezelésre nincs szükség.) A másik, hogy egy daemon azért nem futhat akármilyen munkakönyvtárban (pl. ahonnan elindították), mert azt lecsatolhatják, törölhetik, stb. Ennek megfelelően a munkakönyvtárat át szokták állítani a "/"-re, de ez nincs kőbe vésve; ha egy daemonnak van egy direkt neki fenntartott path-ja, akkor azt is használhatja. Átállítása a chdir()-rel történik. C-ben pl. chdir("/"), Pascalban fpchdir('/'), lekezelendő hiba itt is a -1-es visszatérő érték. A harmadik, hogy egy daemon nem szokott a terminálra firkálni, vagy onnan olvasgatni. Ez sincs kőbevésve, mert pl. develop/debug közben minden további nélkül, de alapvetően egy daemon a stdin, stdout és stderr file descriptorokat (értsd: 0, 1, 2) át szokta irányítani a /dev/null felé. Vagy: egy tetszőleges fájl felé (legalábbis az outputokat), ha valaki így akar loggolni. (Erre még később visszatérünk, mármint a loggoláshoz.) Az átirányítást a dup2() parancs végzi, de ehhez előbb nyitnunk is kell egy új file descriptort az open() segítségével. C-ben: #include <fcntl.h> ... int fd = open("/dev/null", O_RDWR); if (fd == -1) { // hibakezelés } dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); Pascalban: var fd: cint; ... fd := fpopen('/dev/null', O_RdWr); if (fd = -1) then begin // hibakezelés end; fpdup2(fd, 0); fpdup2(fd, 1); fpdup2(fd, 2); A negyedik, hogy ha ezt a process-t signalokkal vezérelni akarjuk, akkor nem ártana mondjuk rendszertszinten tudni, hogy mi a futó daemon process ID-je. Ezt pl. úgy lehet megoldani, hogy kiírjuk egy fájlba. Erre a célra a /var/run/ könyvtár van fenntartva: ide lehet kiírni a getpid(), ill. Pascal alatt a fpgetpid eredményét. (Ezt természetesen a daemonizáció legvégén kell csinálni, amikor már mindennel megvagyunk.) Ötödik, hogy nem ártana, ha a daemonból csak egy futna egyszerre. Ezt az egyel feljebbi részben tulajdonképpen már megoldottuk, csak azt kell lekérdezni, hogy létezik-e a fájl és ha igen, akkor kiszállni daemonizáció helyett. (Ezt értelemszerűen a legelején kell megcsinálni, minden egyéb előtt.) Illetve ez az alap, de lehet ennél jobban is csinálni, mert pl. ha - ne adja az ég - a daemon összedőlt valamiért, mint teszi azt a windóz nap-mint-nap fél disznó az ólban, akkor a PID file ott marad és a daemon nem fog elindulni többet. Ennek megfelelően ki kell olvasni belőle a letárolt PID-et és lekérdezni, hogy fut-e még a process. Ezt a legegyszerűbben úgy lehet abszolválni, ha küldünk neki egy 0-ás signalt (ld. később), amit maga a process nem fog megkapni és lekezelni, viszont az OS igen és ha a visszatérési érték nem 0, akkor a process már nem létezik, lehet törölni a fájlt és daemonizálni. A hatodik, hogy a dupla daemonizációt úgy lehet levédeni, hogy ellenőrizzük, hogy a parent process process ID-je 1-e. Lekérdezése C-ben getppid(), Pascalban fpgetppid; a visszatérési érték mindkét esetben 1-nél nagyobb kell, hogy legyen; ha 1, akkor a parent az init, tehát ki kell szállni. (Nyilván ezt is az elején kell végrehajtani.) Nagyjából ennyi lenne, amennyit egy process daemonizálásához meg kell csinálni. A nehezén már túl vagyunk, most jön a neheze, egy háromsoros levél megírása outlookban, mielőtt elfogy a .NET miatt a memória, vagy az idő a következő erőszakos frissítésig a vezérlés. Mint azt az elején sikerült körülírni: a vezérlés - általában - signalokkal történik. A linkelt wikicikk megemlíti, hogy a signalok az interruptokra hasonlítanak; a formális lekezelés szempontjából ez mindenképpen igaz, mert van egy jelünk, meg van egy vektorunk (függvényünk), amire a CPU a jel megkapásakor odaugrik és végrehajtja. (Mármint amikor a process kapja meg a jelet, nem a CPU.) A signalra triggerelődő függvényeket (handlereket), vagy a signalok figyelmen kívül hagyását, default handlerre állítását (SIG_IGN, SIG_DFL) a signal() függvénnyel lehet beállítani, legalábbis elméletben. A gyakorlatban az implementáció UNIX-onként eltér, sőt Linux esetén még disztribúciónként is, ennek megfelelően nem kimondottan ajánlott használni, ha cross-platform megoldást akarunk: az igazi cross-platform és POSIX-compliant megoldás a sigaction() használata. Ennek a használata az azonos nevű struct-ot használva történik, amiben egy signal handlerének beállításához először be kell állítani a sa_handler-t a callback függvény címére, kinullázni a sa_mask-ot a sigemptyset() segítségével (ez a maszk jelöli meg, hogy mely signalokat blokkolja a rendszer miközben a handler fut), kinullázni a sa_flags opcióit (egyik sem kell, ld. a manualt) és végül csak Linux alatt NULL-ra állítani a POSIX szabványban nem létező - és ebben az esetben amúgy sem használt - sa_restorer-t. Azaz, hogy ne csak rizsa, hanem kód is legyen; az implementáció C-ben: #include <stdbool.h> #include <signal.h> ... bool assign_signal(int signal, void (*callback)(int)) { struct sigaction act; act.sa_handler = callback; sigemptyset(&act.sa_mask); act.sa_flags = 0; #ifdef __linux__ act.sa_restorer = NULL; #endif return sigaction(signal, &act, NULL) != 0; } és Pascalban: type unix_signal = cint; unix_signal_callback = procedure (signal: unix_signal); cdecl; punix_signal_callback = ^unix_signal_callback; ... function assign_signal(signal: unix_signal; callback: punix_signal_callback): boolean; var act: sigactionrec; begin act.sa_handler := sigactionhandler(callback); fpsigemptyset(act.sa_mask); act.sa_flags := 0; {$ifdef linux} act.sa_restorer := nil; {$endif} result := fpsigaction(signal, @act, nil) <> 0; end; És akkor térjünk rá, hogy mely signalokkal van is dolgunk a gyakorlatban. A legtöbb idevágó leírás felsorolja a SIGTTIN, SIGTTOU és SIGTSTP signalok SIG_IGN-re állítását. Az első kettőt egy process akkor kapja, ha a háttérből írni vagy olvasni akarja a vezérlőterminált, a harmadik pedig akkor keletkezik, ha a vezérlőterminálon a ^Z-re tenyerel a felhasználó, hogy megállítsa a process-t. Mivel mint azt fentebb tisztáztuk, egy daemonnak nincs vezérlőterminálja, ezért ezeket a signalokat normál esetben soha nem kaphatja meg a process. (Nyilván lehet küldeni neki ilyet kill-lel, de azzal bármit lehet...) Az a két signal, amit ténylegesen használni szoktak ilyenkor, az a SIGHUP (a konfigok újratöltésére) és a SIGTERM (a daemon szabályszerű megállítására). Persze lehet mást, de ezek a konvencionális signalok. Ennek megfelelően az assign C-ben: void handle_signal(int sig) { if (sig == SIGTERM) { // a daemon megállítása } else { // a daemon konfigjainak újratöltése } } ... assign_signal(SIGTERM, &handle_signal); assign_signal(SIGHUP, &handle_signal); És Pascalban: procedure handle_signal(sig: unix_signal); cdecl; begin if (sig = SIGTERM) then begin // a daemon megállítása end else begin // a daemon konfigjainak újratöltése end; end; ... assign_signal(SIGTERM, punix_signal_callback(@handle_signal)); assign_signal(SIGHUP, punix_signal_callback(@handle_signal)); Értelemszerűen lehet akár külön handlert is adni nekik, meg lehet egyéb signalokat is használni, pl. SIGUSR1, SIGUSR2, stb... Ez volna a fogadó része a dolognak, most jön a küldő. Signalt küldeni a kill() paranccsal lehet, illetve Pascalban fpkill. A használat pedig úgy történik, hogy a program ugye kap(hat) argumentumokat (C-ben argv[x], Pascalban ParamStr(x)) és mindenki a maga ízlése szerint összerakhatja a maga parancsait úgy, hogy az egyikre daemonizál és futtatja a lényegi részt, a másik kettőre meg lekéri a PID-et és SIGTERM-et, vagy SIGHUP-ot küld neki (stop vagy reload végett), aztán várakozik pl. 5 másodpercig, vagy addig amíg a célprocess ki nem száll/vissza nem jelez, pl. egy a /var/lock/-ban, vagy a /tmp/-ben létrehozott semaphore file segítségével. Nos, alapjában véve ennyi egy UNIX daemon. Alapjában véve. Hogy egy klasszikust idézzek, "kb. így fest a dolog gatyában". Most jön a kit része, amit ígértem az elején. Tehát akkor van egy daemonizerünk; C-ben http://oscomp.hu/daemon-kit/c/daemon.c és Pascalban http://oscomp.hu/daemon-kit/pascal/daemon.pas amik a következő függvényeket tartalmazzák: C: void remove_daemon_pidfile(char *daemon_name) Pascal: procedure remove_daemon_pidfile(daemon_name: string); Nomen est omen, arra való, hogy leszedje a daemon process lock-ért felelős PID fájlt a /var/run/ mélyéről. Akkor "célszerű" használni, amikor a daemon szabályszerűen áll le. (Amikor nem szabályszerűen teszi, akkor a drink() függvényt célszerű, lehetőleg uint64_t-re castolt -1-gyel...) C: pid_t pidof_daemon(char *daemon_name) Pascal: function pidof_daemon(daemon_name: string): tpid; Másik beszédes nevű függvény, de ez nem csak lekéri a daemon PID-jét, hanem, amennyiben a lock létezik, de kidöglött mögüle a process, akkor feloldja. Ha a daemon nem fut, akkor nullát ad vissza. C: int daemonize_process(char *daemon_name, bool redirect_fds, char *workdir) Pascal: function daemonize_process(daemon_name: string; redirect_fds: boolean = true; workdir: string = '/'): integer; Ez felel a daemonizálásért. (DOH!) Átadja neki az ember a daemon böcsületes nevét és - a signal redirect kivételével (ld. később) - mindent megcsinál. A név bármi lehet, de értelemszerűen ugyanazt kell használni mind a négy függvénynél. A redirect_fds paraméter felel azért, hogy ráhúzza-e a daemonizer a stdxyz descriptorokra a dup2()-t vagy ne. Normál esetben igen, debugnál jól jön, hogy ki lehet kapcsolni. A workdir értelemszerűen a munkakönyvtár. (Mint kiveséztük, daemonoknál általában ez a gyökérkönyvtár.) Visszatérési értéke 0, ha minden rendben volt, vagy egy hibakód. (Ld. a forrásban, nem sorolom fel, mert sok is, meg nem is érdekes.) C: int stop_daemon(char *daemon_name, int stopsig, int32_t timeout) Pascal: function stop_daemon(daemon_name: string; stopsig: cint; timeout: longint): integer; Ezt lehet meghívni, ha le akarjuk állítani a daemont. A stopsig-et direkt nem drótoztam be a SIGTERM-re, mert hátha valaki máshogy akarja. Ami viszont fontos, hogy ha timeout mikroszekundumig nem száll ki a célprocess, akkor viszont garantáltan SIGKILL-lel honorálja a dolgot. Visszatérési értéke lehet 0, ha a process rendben leállt, 1, ha ki kellett nyírni és 2, ha nem is futott. Na most, azt meg kell jegyezni, hogy a GLibC-ben van már daemonizer, de az minden, csak nem cross-platform (lévén GLibC dependens). Ez a megközelítés viszont az. Aztán, van nekünk egy signal handlerünk is; C-ben: http://oscomp.hu/daemon-kit/c/signals.c és Pascalban: http://oscomp.hu/daemon-kit/pascal/signals.pas a következő függvényekkel: C: bool assign_signal(int signal, void (*callback)(int)) Pascal: function assign_signal(signal: unix_signal; callback: punix_signal_callback): boolean; Ez a fentebb már kivesézett segédfüggvény, amit a nem-POSIX-compliant signal() helyett használhatunk. Több szót nem vesztegetnék rá, csak annyit, hogy ha a SIG_DFL, ill. SIG_IGN konstansokat (0 és 1) akarjuk hozzárendelni valamelyik signalhoz, akkor azt a Pascal verzióban ugyanúgy typecastolva lehet, mintha egy függvény címét akarjuk odaadni neki. Hiba esetén ad vissza igazat. C: bool signal_w_fsemaphore(pid_t proc, int sig, int timeout, char *semaphore) Pascal: function signal_w_fsemaphore(proc: tpid; sig: unix_signal; timeout: integer; semaphore: string): boolean; Ez sig signalt küld a proc processnek és timeout mikroszekundumig várakozik, vagy addig, amíg a semaphore fájlt valaki létrehozza. (Meghíváskor értelemszerűen magától törli először.) Ezzel lehet pl. ütemezni a reloadot, meghívjuk, a signal handler meg újratölti a konfigot és létrehozza a semaphore-t. Ez is hiba (azaz timeout) esetén ad vissza igazat. Nos, nagyjából így lehetett összefoglalni és "kitesíteni" a fentebb leírtakat. Mondjuk egyvalamiről nem sikerült szót ejteni, hogy maga a daemon főciklusa hogyan működik. Pofonegyszerűen: a váza egy mezei while ciklus, ami egy feltételhez kötve vár: C: bool run_daemon = true; while (run_daemon) { // főciklus } Pascal: var run_daemon: boolean = true; while (run_daemon) do begin // főciklus end; És akkor a fentebbi signal handleres példa úgy módosul, hogy a leállítós ágba bekerül, hogy run_daemon = false; Mi hiányzik még ebből? Mondjuk az, hogy ne őrölje fel a CPU-t ez a szerencsétlen ciklus: kell bele várakoztatás. Ezt a nanosleep() (Pascal: fpnanosleep) függvénnyel lehet, de annak körülményes és kényelmetlen a használata. Úgyhogy itt egy erre alapuló microsleep (ennél precízebb amúgy sem valószínű, hogy kellene) C-ben: http://oscomp.hu/daemon-kit/c/microsleep.c és Pascalban: http://oscomp.hu/daemon-kit/pascal/microsleep.pas Egy db. függvény van benne: C: void _usleep(uint64_t us) Pascal: usleep(us: qword); Használata triviális. A C-s függvény esetében az underscore prefixum azért van, hogy ne akadjon össze a GLibC-ben lévővel és azért nem arra wrappel csak simán, mert egyrészt nem biztos, hogy GLibC van a rendszerben, másrészt pedig azért, mert a GLibC-s verzió nem kezeli le a nanosleep() visszatérési értékét és a remainder timespec tartalmát is levesbe küldi, ahelyett, hogy ha van maradék, akkor azt is levárná. (De, ahogy nézem, a musl sem foglalkozik vele.) Nos, a daemon innentől kezdve tulajdonképpen készen van. Viszont én még belinkelnék három másik libet is. (Amiknek a használata teljesen opcionális, komfortnak van.) Egyfelől: ami - majdnem - minden daemonban lesz úgyis: konfigurációs állományok parsingjához itt egy lib C-ben: http://oscomp.hu/daemon-kit/c/confparse.c és Pascalban: http://oscomp.hu/daemon-kit/pascal/confparse.pas A következő két függvénnyel: C: void parse_conf_file(char *filename, int items_c, struct conf_item items[], int *error_code, int *error_line) Pascal: procedure parse_conf_file(filename: string; items: conf_items; error_code: pinteger; error_line: pinteger); C: void scan_conf_dir(char *dirname, int items_c, struct conf_item items[], char **error_file, int *error_code, int *error_line) Pascal: procedure scan_conf_dir(dirname: string; items: conf_items; error_file: pstring; error_code: pinteger; error_line: pinteger); Az első egy közönséges név = érték # komment jellegű konfigparseoló, a második pedig ezt hívja meg (nem rekurzívan) egy könyvtár összes fájljára, ha valaki szeparálni akarná a konfigokat. Az error_code, error_line és a dirscanner esetében az error_file egyértelműek, hogy mire valók (hibakódokért ld. a forrást), az viszont fontos, hogy hiba esetén az egész megáll és kiszáll, nem próbálja meg parse-olni a hátralevőket. Ami viszont magyarázatra szorul, az az items. C: struct conf_item { char *ci_name; int ci_type; void *ci_var; }; Pascal: type conf_item = record ci_name: string; ci_type: integer; ci_var: pointer; end; conf_items = array of conf_item; Lényegében arról beszélünk, hogy van egy ilyen elemekből álló tömbünk, amikben meghatározzuk, hogy milyen változókat írjon a parser (ci_var), azoknak mi a típusa (ci_type, ld. a forrást) és milyen néven hivatkozunk rájuk a konfig fájlokban (ci_name). Példa C-ben: A változók: char *ezegystring; float ezegyfloat; uint32_t ezegyint; bool ezegybool; A tömb: struct conf_item items[4] = { { "ezegystring", CONFVAR_TYPE_STRING, &ezegystring, }, { "ezegyfloat", CONFVAR_TYPE_FLOAT32, &ezegyfloat, }, { "ezegyint", CONFVAR_TYPE_INT32, &ezegyint, }, { "ezegybool", CONFVAR_TYPE_BOOL, &ezegybool, } }; És a tényleges kód: char *ef; int ec, el; scan_conf_dir( "/etc/teszt_daemon_conf_dir/", 4, items, &ef, &ec, &el ); Példa Pascalban: A változók: var ezegystring: string; ezegyfloat: single; ezegyint: integer; ezegybool: boolean; A tömb: const items: array[0..3] of conf_item = ( ( ci_name: 'ezegystring'; ci_type: CONFVAR_TYPE_STRING; ci_var: @ezegystring; ), ( ci_name: 'ezegyfloat'; ci_type: CONFVAR_TYPE_FLOAT32; ci_var: @ezegyfloat; ), ( ci_name: 'ezegyint'; ci_type: CONFVAR_TYPE_INT32; ci_var: @ezegyint; ), ( ci_name: 'ezegybool'; ci_type: CONFVAR_TYPE_BOOL; ci_var: @ezegybool; ) ); És a tényleges kód: var ef: string; ec, el: integer; scan_conf_dir( '/etc/teszt_daemon_conf_dir/', items, @ef, @ec, @el ); Mivel a célváltozókat pointerrel adjuk meg, így nem korlátozódik semmilyen scopre-ra, (nyugodtan lehet egy struct belsejében, vagy egy tömbben, vagy akárhol), vagy namespace-re (csak a tömb deklarációjánál kell, hogy "lássa" a pointerek célját). Így sokkal kényelmesebb a konfigok kezelése és - ha szükséges - a változókészlet bővítése. A másik a daemon argumentumainak lekezeléséhez adna egy mankót; C-ben: http://oscomp.hu/daemon-kit/c/argforward.c és Pascalban: http://oscomp.hu/daemon-kit/pascal/argforward.pas C: int forward_args(int args_c, struct fw_arg args[], int def_arg, int argc, char *argv[]) Pascal: function forward_args(args: fw_args; def_arg: integer): integer; C: char *implode_arg_names(int args_c, struct fw_arg args[], char *sep) Pascal: function implode_arg_names(args: fw_args; sep: string): string; Hogy mi ez? A daemonnak átadott argumentumokat normál esetben egy jókora case "toronyban" kezeli le az ember, amit egy idő után elég kényelmetlen bővíteni, vagy csak keresgetni benne, ha sok parancsot tud és/vagy az egyes parancsok implementációi hosszúra nyúlnak; persze azokat ki lehet pakolni külön-külön függvényekbe, de akkor már majdnem ugyanott van vele az ember, mint ezzel, csak mégis hátrább, ld. mindjárt. A felállás hasonló, mint az előbb: átadunk egy struct halmot, ahol az egyes elemek tartalmazzák a parancs nevét és a meghívandó eljárást. Az első végigiterálja a tömböt és összehasonlítja az 1. argumentumot a nevekkel és ha egyezést talál, akkor meghívja azt a függvényt, átadván neki a bemeneti argumentumokat is (mintha az lenne a főprogram). Ha nem talál egyezést, akkor azt az függvényt hívja meg, amelyik a def_arg indexe alatt van. A második - a nevéből sejthetően - az ismert parancsneveket fűzi össze egy tetszőleges szeparátorral. Ennek az az értelme, hogy pl. ha van help függvényünk, abban ezt meghívhatjuk és bővítéskor nem kell külön mindenütt bővíteni, csak hozzá kell adni az új nevet. A definíciók C-ben: struct fw_arg { char *arg_name; int (*arg_func)(int argc, char *argv[]); }; és Pascalban: type fw_func = function (argv: array of string): integer; fw_arg = record arg_name: string; arg_func: fw_func; end; fw_args = array of fw_arg; És akkor egy példa C-ben: ... int stop(int argc, char *argv[]); int reload(int argc, char *argv[]); int start(int argc, char *argv[]); int help(int argc, char *argv[]); char *rl_sem = "/var/run/teszt_reload"; struct fw_arg args[4] = { { "start", &start }, { "stop", &stop }, { "reload", &reload }, { "help", &help } }; ... int stop(int argc, char *argv[]) { int result; result = stop_daemon("teszt", SIGTERM, 5000); fprintf(stderr, "Stopped with code: %d\n", result); return result; } int reload(int argc, char *argv[]) { return signal_w_fsemaphore(pidof_daemon("teszt"), SIGHUP, 5000, rl_sem); } int start(int argc, char *argv[]) { ... } int help(int argc, char *argv[]) { char *names; names = implode_arg_names(4, args, "/"); fprintf(stderr, "teszt <%s>\n", names); free(names); return 0; } int main(int argc, char *argv[]) { forward_args(4, args, 3, argc, argv); } És Pascalban: ... function stop(argv: array of string): integer; forward; function reload(argv: array of string): integer; forward; function start(argv: array of string): integer; forward; function help(argv: array of string): integer; forward; const rl_sem = '/var/run/teszt_reload'; args: array[0..3] of fw_arg = ( ( arg_name: 'start'; arg_func: @start; ), ( arg_name: 'stop'; arg_func: @stop; ), ( arg_name: 'reload'; arg_func: @reload; ), ( arg_name: 'help'; arg_func: @help; ) ); ... function stop(argv: array of string): integer; begin result := stop_daemon('teszt', SIGTERM, 5000); writeln('Stopped with code: ', result); end; function reload(argv: array of string): integer; begin result := integer(signal_w_fsemaphore(pidof_daemon('teszt'), SIGHUP, 5000, rl_sem)); end; function start(argv: array of string): integer; begin ... end; function help(argv: array of string): integer; begin writeln('teszt <' + implode_arg_names(args, '/') + '>'); result := 0; end; begin forward_args(args, 3); end. És végül, amire ígértem, hogy még visszatérünk: a loggoláshoz egy segédlib.; C-ben: http://oscomp.hu/daemon-kit/c/logging.c és Pascalban: http://oscomp.hu/daemon-kit/pascal/logging.pas A következő függvényekkel: C: set_logging_path(int index, char *path) C: clear_logging() C: void write_log(char *line, int logging_channel) Pascal: procedure write_log(line: string; logging_channel: integer = 0); És változókkal: C: bool logging_echo = false; Pascal: logging_echo: boolean = false; Pascal: logging_paths: array of string; Amit korábban nem írtam le: nem az a célszerű, ha az ember a stderr-t és stdout-ot fájlokba irányítja át a /dev/null helyett, hanem az, ha alapvetően logfájlokba írkál. Amint a függvények definícióiból kiderül, több naplófájlt is lehet vele kezelni, ha esetleg szükséges lenne. Két közös pont van a két implementációban: az egyik maga a write_log(), ami a naplófájlokat írja (automatikusan "[yyyy-mm-dd HH:ii:ss.TTT]: " formátumú időbélyeggel prefixálva), a másik a logging_echo változó, ami ha igaz, akkor a kiirandó sor a terminálon is meg fog jelenni (időbélyeg nélkül). Nyilván, ehhez az kell, hogy ne legyenek a stdxyz descriptorok átirányítva a /dev/null-ra. (Egyszóval ez is debugra való.) A többi eltérés a stringtömbök kezelésének eltérése miatt van. Pascalban csak simán setlength(logging_paths, 1); logging_paths[0] := '/var/log/teszt.log'; a beállítás, törlésre meg nincs szükség, mert a stringtömböt felszabadítja a program kilépéskor. C-ben viszont set_logging_path(0, "/var/log/teszt.log"); a beállítás és a program befejezésekor kell a clear_logging() is. Ami még fontos, hogy a logfájlok nincsenek folyamatosan nyitva, aminek oka, hogy ha a program megborulna, akkor is intakt állapotban maradjon a logfájl, meg az is, hogy ha valami jótét lélek menet közben törölné a nyitott fájlt, az "vicces" lenne, így viszont egyszerűen létrehozza megint és írja, előről. A kitet teszteltem Linux, Solaris 10, FreeBSD és OpenBSD alatt (ez utóbbi alatt - Pascal fordító híján - csak a C-s edisönt) és ment mindenütt. (Egyéb BSD-ket már nem volt kedvem felrakni.) OSX alatt sajnos nem volt érkezésem tesztelni, mert szét van szedve minden (mármint a KVM), az OSX meg nem akar menni VM-ből, de ha valaki megpróbálja lefordítani OSX alatt, akkor nagyon szívesen veszem a visszajelzést, hogy mire jutott vele. AIX alatt sem tudtam tesztelni, mert nincs vasam hozzá, de ugyanazt tudom mondani, mint az OSX esetében. Buildeléséhez elméletileg nem kell semmi, viszont Solaris 10 alatt az -lposix4 kelleni fog a forgatáskor. Nos, azt hiszem, több marhaságot már nem tudok összehordani, de azt hiszem, hogy ennyi is bőven elég volt. Mint az elején mondtam, nem tudom, hogy hány embernek lesznek újdonságok ezek a dolgok, vagy hasznos a daemon-kit, amit publikáltam, de remélem nem írtam hiába ezt a posztot.

Tovább a teljes cikkre...

Keresés