Come promesso in un mio recente webcast, su questo blog troverete il codice sorgente per poter fornire il vostro dispositivo di un servizio, o se volete, di un'attività che parta all'avvio del dispositivo stesso. Per chi volesse comunque avere solo un riassunto delle "puntate precedenti"...allora accomodatevi :-).
Allora, si comincia parlando dei servizi in genere. Come sapranno in molti, i servizi nei dispositivi desktop sono quei programmi che vanno in esecuzione all'avvio della macchina, senza che nessuno abbia ancora effettuato il logon. Sono dei processi che vengono lanciati con specifici contesti di sicurezza e, dopo aver caricato un certo numero di DLL, eseguono dei compiti ben precisi. Alcuni di questi processi si chiamano svchost (che qualcuno guardando in task manager si sarà chiesto..ma questi svchost, a cosa servono?) ed analizzando la nostra macchina con process explorer possiamo anche scoprire a quali compiti ciascuno di essi assolve.
Tutti i processi evidenziati in rosa sono servizi. Il processo Svchost selezionato, ha caricato al suo interno rpcss.dll (dll che troviamo in Windows\System32) e di conseguenza ne deduciamo che implementa le funzionalità per la gestione delle remote procedure calls. Analizzando il resto dei processi troveremo che altri svchost ospitano anche più di una dll per implementare i servizi di "ora di Windows", Crittografia, Telefonia, Windows firewall e altri ancora.
E nel mondo Mobile che cosa accade? Fortunatamente ci viene in contro un processo che ritroviamo dalla versione PPC 2002 in poi (anche se per i dispositivi un pò più datati è possibile aggirare questo problema) che si chiama services.exe, il quale è preposto alla esecuzione di alcune DLL poste in locazioni ben precise, non apena il dispositivo effettua uno start (o restart). Il funzionamento di Services.exe è molto simile al processo Devices.exe, anche nella gestione come vedremo. Una differenza sostanziale è che Services.exe viene utilizzato se non abbiamo bisogno di accedere ad un hardware ben preciso ovvero se non dobbiamo implementare delle tecniche di IPC (inter Process Comunication). Ecco graficamente come appare il processo Services.exe
Attivazione, Gestione e Deattivazione
Per scrivere un servizio, è necessario scrivere una dll che sarà poi caricata nel processo che vediamo sopra in figura. Questa DLL deve necessariamente esporre delle funzioni con nomi ben precisi. due di queste funzioni le vediamo subito analizzandone il prototipo:
DWORD xxx_Init( DWORD dwData); DWORD xxx_Deinit(DWORD dwData);
Naturalmente xxx deve essere sostituito dal nome del nostro servizio, oppure da un prefisso che noi abbiamo scelto. Questa funzione viene utilizzata da Service.exe dopo che un'altra primitiva RegisterService avrà caricato la nostra DLL e avrà verificato che quest'ultima esporti le corrette funzioni, proprio come quando una classe implementa un'interfaccia. Se tutto è ok, Services.exe si vedrà ritornare un valore da xxx_Init che sarà poi utilizzato in altre eventuali successive chiamate. RegisterService ottiene un Handle del servizio. Possiamo immaginarci come la funzione Deinit venga chiamata da Serices.exe dopo che la primitiva DeregisterService ha controllato e deattivato il nostro servizio. Per poter controllare in corsa il servizio invece, è necessario procurarci prima di tutto un handle della nostra dll tramite la primitiva
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDispostion, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
Esatto, proprio CreateFile. Dopo che la funzione avrà avuto esito positivo si potranno inviare alla DLL dei messaggi di tipo IOCTL (Input Output Controls) tramite la primitiva DeviceIOControl la quale provoca l'invocazone della primitiva esposta nella DLL di nome xxx_IOControl. Questa ultima si aspetta di ricevere dei messaggi IOCTL tramite i quali intraprendere le corrette azioni. Solo per dare un'idea vediamo alcuni dei messaggi IOCTL più comuni che possono essere gestiti dalla DLL del servizio:
|
IOCTL
|
Descrizione |
|
IOCTL_SERVICE_STOP
|
ferma il servizo
|
|
IOCTL_SERVICE_START
|
Fa partire il servizio
|
|
IOCTL_SERVICE_STATUS
|
Ritorna lo stato del servizio
|
|
IOCTL_SERVICE_CALLBACK_STATUS
|
Funzione di callback da invocare ed eseguire in Services.exe
|
I messaggi IOCTL possono essere inviati sia dalle applicazioni che da Services.exe. Una cosa molto interessante è che si possono mandare in esecuzione all'interno di Services.exe, più istanze dello stesso servizio, senza cioè far girare più istanze dello stesso processo. Questo va tenuto in grande considerazione, visto che ancora abbiamo il problema del limite dei 32 processi (superato con la versione 6.0 di windows CE e non di Windows Mobile 6 che equipaggia ancora il Windows Mobile 5.x!!!!!). Oltre alla possibilità di avere più istanze è possibile comunque far girare il nostro servizio in un processo Services.exe assolutamente dedicato come indicato in figura:
Per poter far partire il servizio in un processo separato, è necessario impostare la chiave di registro HKEY_LOCAL_MACHINE\Services\<Service Name>\Context di tipo DWORD e assegnarle il valore 0x02 (SERVICE_INIT_STANDALONE), dove naturalmente <Service Name> è un placeholder per il nome che noi abbiamo assegnato al servizio. Questo valore verrà poi passato ala funzione xxx_Init nel parametro dwData, la quale ritornerà FALSE se la DLL non supporta l'avvio stand-alone
Super Service Sockets
Una caratteristica fondamentale che non dobbiamo trascurare se il servizio che stiamo costruendo deve utilizzare funzionalità di rete, è proprio Super Service Sokets. Chi è "avvezzo" all'utilizzo di sistemi Open Source non sarà per niente sorpreso da questa funzionalità che si avvicina molto per esempio al servizio Inetd di FreeBSD. In pratica Super Service rimane in ascolto solo lui delle richieste di connessione per conto dei vari servizi attivati smistando le richieste dei clients alle varie DLL. Questo migliora l'overhead di sistema ed una migliore gestione delle socket (esiste un solo thread in accept() per tutti i servizi e non un thread in accept() per ogni servizio). Il compito di Super Service sarà quindi quello di mettersi in ascolto su una certa porta in nome e per conto di una certa DLL e all'arrivo di una richiesta di connessione passargli la socket che verrà a quel punto gestita direttamente dallo specifico servizio. Il sistema di bindng può essere impostato all'avvio di Services.exe oppure gestito dinamicamente. Per l'avvio di un Super Services è necessario impostare il flag SERVICE_INIT_STOPPED nella chiave di registro Context (vedi prima). Conseguentemente Services.exe chiamerà xxx_IOControl e passerà il valore IOCTL_SERVICE_REGISTER_SOCKADDR. Se tutto ok allora xxx_IOControl ritornerà TRUE e Super Service si troverà le porte su cui attendere i client nelle seguenti chiavi di registro
[HKEY_LOCAL_MACHINE\Services\TELNETD]
"Context"=0x00000001 //SERVICE_INIT_STOPPED, use super server
[HKEY_LOCAL_MACHINE\Services\TELNETD\Accept\TCP-23] ; TCP port 23, default Telnet Port
"SockAddr"=hex:02,00,00,17,00,00,00,00,00,00,00,00,00,00,00,00
[HKEY_LOCAL_MACHINE\Services\TELNETD\Accept\TCP-24] ; Also listen on TCP port 24
"SockAddr"=hex:02,00,00,18,00,00,00,00,00,00,00,00,00,00,00,00
In questo caso (dopo aver impostato il flag context) si indicano due porte di ascolto TCP sulla porta 23 e 24 per il servizio Telnet. (che potrete trovare come esempio nei file del SDK).
Progetto Service
E veniamo adesso ad un progetto vero e proprio che vogliamo implementare sul nostro Device. La solution si basa su tre progetti (in codice nativo C++), rispettivamente:
- La DLL che implementa il servizio vero e proprio
- Un file CAB per fare il deploy del servizio
- Una DLL di Setup (Setup.dll) che servirà proprio per avviare il servizio
In pratica la nostra DLL verrà inserita nel CAB insieme ad un file setup.dll contenete le invocazioni delle primitive per registrare e avviare il servizio, proprio come spiegavamo in precedenza. Il concetto è che non appena abbiamo fatto il deploy della DLL nella directory \Windows (poichè è li che Services.exe si aspetta di trovare tutte le DLL che implementano dei servizi), dobbiamo invocare la primitiva RegisterService per causare l'invocazione di xxx_Init(). Quale miglior luogo per inserire il RegisterService se non in una Setup.dll che verrà utilizzata dal .CAB quando lanceremo quest'ultimo. Infatti, quando il .CAB viaggia con una DLL chiamata Setup.dll, la prima cosa che fa, dopo aver messo a posto i file nelle directory giuste ed inizializzato le chiavi di registro, è quello di chiamare la funzione Install_Init. Solitamente in questa funzione viene inserita la classica domanda tipo "sei sicuro di voler installare il servizio?". Se l'utente digita OK allora il .CAB chiama la funzione Install_Exit dentro la quale si invoca effettivamente RegisterService. Tanto per fare chiarezza vediamo in questa immagine una illustrazione sistematica delle fasi di cui abbiamo parlato:
Il progetto servizio viene realizzato da una dll MyService.dll dentro la quale vengono implementate le funzioni xxx_Init, xxx_Deinit e così via, dove il prefisso scelto è naturalmente CTS. Dobbiamo poi fornire un file .DEF in cui pubblichiamo le funzioni esportate. Il Working-thread (il thread che efettua il lavoro vero e proprio del servizio) non è altro che un thread che ogni 10 secondi lancia una messagebox.
La dll Setup.dll contiene le implementazioni delle funzioni Install_Init e Install_Exit dove rispettivamente vengono invocate le primitive RegisterService e DeregisterService. Ricordo che queste primitive vengono invocate direttamente (e per default) dal .CAB al momento dell'avvio di quest'ultimo.
Il progetto forse più interessante è quello del CAB per device. In questo progetto infatti dobbiamo indicare per prima cosa che si intende utilizzare un file setup.dll e questo lo diciamo nelle proprietà del progetto come indicato:
Dopodichè, dopo aver indicato che il file Myservice.dll dovrà essere inserito nella directory /Windows del target device e la dll Setup.dll nella directory Program Files, ecco come si da indicazione al file .CAB di come dovranno essere impostate le chiavi di registro necessarie all'installazione. Come vediamo i valori sono autoesplicativi:
A questo punto sarà necessario compilare e simulare un deploy del .CAB copiando manualmente (tramite i remote tools di VS2005) il .CAB in una directory qualsiasi del dispositivo target. Un'ultima notazione va fatta per il debug. Inserire dei break point non porta a nulla in quanto la nostra DLL viene eseguita nello spazio di indirizzamento del processo Services.exe. Questo ci obbliga all'inserimento di un'istruzione DebugBreak() nel nostro sorgente come in figura:
Questo significa che il procedimento per effettuare il debug sarà il seguente (dopo aver ricompilato il sorgente ovviamente):
- Ricompilare la solution
- Fare il deploy del .CAB
- Fare un attach al processo Services.exe utilizzando il tool di VS2005
- Lanciare il CAB
- Attendere l'arrivo all'istruzione DebugBreak().
L'attach è illustrato in figura:
Con questo mi sembra di aver detto tutto, tranne dove potete scaricarvi i sorgenti e cioè da qui.
Il progetto servizio viene realizzato da una dll MyService.dll dentro la quale vengono implementate le funzioni xxx_Init, xxx_Deinit e così via, dove il prefisso scelto è naturalmente CTS. Dobbiamo poi fornire un file .DEF in cui pubblichiamo le funzioni esportate. Il Working-thread (il thread che efettua il lavoro vero e proprio del servizio) non è altro che un thread che ogni 10 secondi lancia una messagebox.
La dll Setup.dll contiene le implementazioni delle funzioni Install_Init e Install_Exit dove rispettivamente vengono invocate le primitive RegisterService e DeregisterService. Ricordo che queste primitive vengono invocate direttamente (e per default) dal .CAB al momento dell'avvio di quest'ultimo.
Il progetto forse più interessante è quello del CAB per device. In questo progetto infatti dobbiamo indicare per prima cosa che si intende utilizzare un file setup.dll e questo lo diciamo nelle proprietà del progetto come indicato:
Dopodichè, dopo aver indicato che il file Myservice.dll dovrà essere inserito nella directory /Windows del target device e la dll Setup.dll nella directory Program Files, ecco come si da indicazione al file .CAB di come dovranno essere impostate le chiavi di registro necessarie all'installazione. Come vediamo i valori sono autoesplicativi:
A questo punto sarà necessario compilare e simulare un deploy del .CAB copiando manualmente (tramite i remote tools di VS2005) il .CAB in una directory qualsiasi del dispositivo target. Un'ultima notazione va fatta per il debug. Inserire dei break point non porta a nulla in quanto la nostra DLL viene eseguita nello spazio di indirizzamento del processo Services.exe. Questo ci obbliga all'inserimento di un'istruzione DebugBreak() nel nostro sorgente come in figura:
Questo significa che il procedimento per effettuare il debug sarà il seguente (dopo aver ricompilato il sorgente ovviamente):
- Ricompilare la solution
- Fare il deploy del .CAB
- Fare un attach al processo Services.exe utilizzando il tool di VS2005
- Lanciare il CAB
- Attendere l'arrivo all'istruzione DebugBreak().
L'attach è illustrato in figura:
Con questo mi sembra di aver detto tutto, tranne dove potete scaricarvi i sorgenti e cioè da qui.