Een halt aan het ongebreideld starten van kopieën
Een tijdje geleden vond ik op internet een VO-module, 'RunningInstances' genaamd (met dank aan Lars-Eric Gisslén, lars.gisslen@chello.se). Doel van deze functie was om te ontdekken hoeveel kopieën er van hetzelfde programma draaien. Dit leek me wel wat en ik heb het programma daarom goed bestudeerd. Tenslotte ben ik er achter gekomen dat het eigenlijk veel eenvoudiger kan, terwijl een nieuwe aanpak ook veel meer mogelijkheden geeft.
Deze aanpak is ook een voorbeeld van hoe je vanuit een VO-programma met andere programma's kunt communiceren.
RunningInstances
Het hart van het programma bestond uit drie Windows-API functies:
- CreateToolhelp32Snapshot()
- Process32First()
- Process32Next()
(Zie voor alle technische details de Windows SDK en de source van RunningInstances.) CreateToolhelp32Snapshot() maakt een soort 'foto' van het systeem. Dit is nodig, omdat er natuurlijk continu processen opgestart en beëindigd worden. Vervolgens wordt met de combinatie van Process32First() en Process32Next() door deze lijst gewandeld en een structure gevuld. In deze structure zitten een tweetal interessante gegevens:
- het pad en naam van het exe-bestand
- de process-handle
Het programma werkt door de naam van het exe-bestand te vergelijken met de naam van het programma dat opgestart wordt. Komt dit vaker dan 1 keer voor - het opgestarte programma komt zelf namelijk ook in de lijst voor! -, dan kun je de conclusie trekken dat een tweede instantie van het programma is gestart … een melding geven dus en het programma beëindigen.
Problemen en tekortkomingen
De auteur meldde een probleem: onder Windows 98 werd in de structure de volledige padnaam teruggegeven. Onder Windows 2000 kwamen slechts de laatste 15 tekens te voorschijn (met dank aan Bill). Als workaround werd geopperd om de exe-naam niet langer dan 15 tekens te maken. Exe-bestanden kunnen echter dezelfde naam hebben, als ze in verschillende directories zijn opgeslagen, terwijl het verschillende programma’s kunnen zijn. Wat te denken b.v van een exe-bestand dat SETUP.EXE heet. Tien tegen één dat al deze setups een verschillende functionaliteit bezitten, terwijl RunningInstances ze als kopieën van hetzelfde programma zal zien. (Zelfs als setup.exe vanuit een script werkt is dit nog waar: verschillende fabrikanten noemen het programma hetzelfde).
RunningInstances heeft echter een nog groter probleem. Als de gebruiker een kopie van het programma in een andere directory installeert en de exe-naam hernoemt, merkt RunningInstances dit niet. Het zou fijn zijn als onze beveiliging iets snuggerder was.
Daarnaast zou het prettig zijn als het programma dat niet opgestart wordt - omdat er immers al een kopie draait - het reeds draaiende programma op de voorgrond plaatst in plaats van alleen een melding te produceren.
Daarnaast zou het prettig zijn als reeds draaiende programma op de voorgrond geplaatst wordt
Vooral met dat laatste heb ik stevig zitten stoeien. Mijn eerste gedachte was dat ik vanuit de process-handle wel het top-window van dat programma zou kunnen vinden. Wellicht dat er een mogelijkheid is, maar ik heb hem nog niet gevonden. De Windows-API heeft van alles aan boord maar het is niet eenvoudig om vanuit een process-handle bij een venster te komen.
Alternatieven
Toen ik RunningInstances bestudeerde, viel het me op dat inderdaad alle processen worden gecheckt, dus ook alle programma's in de systemtray (rechts onderin het scherm) en nog een heel stel andere. Je verbaast je er over hoeveel er eigenlijk draait. Ik ben in ieder geval mijn systeem maar eens stevig gaan schoonmaken.
Wat ik eigenlijk wilde was dat ik alleen maar de programma's kreeg die in de taakbalk te zien zijn, en vervolgens wilde ik de teksten die daar getoond worden voor ieder programma met mijn programma vergelijken. In de taakbalk wordt de titel van het top-window van een programma getoond. In VO is deze titel bekend als de Caption van een window. Dit bracht me naar de Windows-API functie EnumWindows(). Deze functie loopt door alle top-windows in het systeem en roept voor ieder window een functie aan welke we met EnumWindows mogen meegeven. Deze functie mogen we zelf schrijven en ziet er b.v. als volgt uit:
define Apptitle:="Mijn programmaatje"
define Appmaxcopies:=1
global appcopies:=0 as dword
function EnumWindowsProc(hwnd as ptr,lParam as dword) as logic callback
local x as int
local p as ptr
local c as string
x:=GetWindowTextLength(hwnd)
if x>0
p:=memalloc(x+30)
GetWindowText(hwnd,p,x+30)
c:=psz2string(p)
memfree(p)
if c==Apptitle
if applcopies>=Appmaxcopies
setforegroundwindow(hwnd)
PostQuitMessage(0)
return False
endif
appcopies+=1
endif
endif
return True
De Windows-API stelt een aantal eisen aan onze functie. Ten eerste moet de calling convention CallBack zijn. Ten tweede heeft de functie 2 parameters: de eerste parameter is een handle naar het window dat onderzocht moet worden en de tweede parameter is een waarde die overgenomen wordt vanuit EnumWindows(). Deze laatste parameter kun je voor eigen doeleinden gebruiken. Ik had hem niet nodig, dus is de waarde altijd nul. De functie moet true of false teruggeven. Bij false stopt EnumWindows met het aanbieden van top-windows.
Ik heb twee defines nodig:
- Apptitle is de tekst welke als caption in het top-window van mijn programma moet komen. Meestal is het top-window Shellwindow dus Shellwindow:caption:=Apptitle moet de klus kunnen klaren.
- Appmaxcopies bepaalt het aantal kopieën dat ik toesta van mijn programma. Zetten we hier 3 neer, mag de gebruiker het programma drie keer starten, maar daarna is het afgelopen.
Tenslotte heb ik nog een Global nodig om het aantal top-windows te tellen dat dezelfde titel heeft als mijn programma.
De functie mag een zelf gekozen naam hebben, maar de SDK stelde de naam EnumWindowsProc voor.
In de functie vraag ik de lengte op van de caption (titel) van het top-window, alloceer dit aantal bytes (plus 30; hier kom ik later op terug) en haal met GetWindowText() de caption op. Deze wordt vergeleken met de titel van mijn programma. Komen ze overeen, wordt de teller verhoogd. Is de teller groter of gelijk aan het maximaal toegestane aantal kopieën, dan wordt de laatst gevonden kopie in de voorgrond gezet d.m.v. SetForegroundWindow() en met PostQuitMessage() wordt het (lopende) programma beëindigd. PostQuitMessage() lijkt veel op het commando Quit, maar gaat iets netter om met de beëindiging van het programma.
Bijft over de effectuering vanuit ons programma. Neem daarvoor in het programma de volgende regel op:
enumwindows(@enumwindowsproc(),0)
De functie EnumWindows() heeft twee parameters: de eerste parameter is een pointer naar de eigengemaakte functie. Daarom staat er een apestaartje voor. De tweede parameter is een getal dat je zelf mag bepalen. EnumWindows brengt dit getal over naar EnumWindowProc() in de tweede parameter. Voor mijn doeleinden had de parameter geen nut, dus ik heb er maar 0 ingezet.
De aanroep naar EnumWindows moet komen nadat App:start() is uitgevoerd, omdat App:exec(), die de verwerking van vensterberichten verzorgt, gestart moet zijn. Je kunt de aanroep bijvoorbeeld plaatsen in de init-methode van je hoofdvenster. Stel nu dat een niet toegestane kopie wordt opgestart. Wellicht dat er al wat vensterberichten ter verwerking aan Windows zijn aangeboden. Onze functie stopt hier vervolgens een Quit-message bij. De vensterberichten worden afgehandeld en tenslotte ook onze Quit-message: direct in de init-methode wordt het programma dan geëindigd.
Conclusies en aandachtspunten
Er zit een aantal voordelen aan deze manier van beveiligen:
- Ten eerste kan de gebruiker de titel van het top-window niet wijzigen; alles wordt door de programmeur bepaald. Het is dus wel van belang om op een of andere manier rekening te houden met het evt. wijzigen van de titel van een top-window.
- Ten tweede heeft eenvoud zijn charme. De functie EnumWindowsProc is precies 20 regels lang.
- Ten derde hebben we hiermee een mechanisme om top-windows in andere applicaties te wijzigen. Wat we nu gedaan hebben met SetForegroundWindow() is het venster in de voorgrond brengen, maar er is natuurlijk niets op tegen om een venster van een andere applicatie te verplaatsen of te resizen, of muisclicks te simuleren (dit alles met de Windows-API functies Sendmessage() en/of Postmessage()) en dit geldt niet alleen voor kopieën van eigen gemaakte programma's!
Je kunt het nog bonter maken. Op basis van EnumWindows kun je er voor zorgen dat verkenner niet opgestart kan worden zolang jouw programma draait, of slechts één exemplaar ervan. Omdat je een handle hebt naar het top-window kun je met API functies ook alle child-windows achterhalen en alle controls op een window. Vervolgens kun je deze windows en controls allerlei berichten sturen. De mogelijkheden zijn dan echt onbegrensd.
De mogelijkheden van het gebruik van EnumWindows zijn echt onbegrensd
Overigens checkt EnumWindows() alle processen in de systemtray. Ik heb geen moeite genomen om ze uit te sluiten. Bij nader inzien is het ook niet zo'n probleem: ook die programma's zou je willen/kunnen beveiligen.
In eerste instantie haalde ik de lengte op met GetWindowTextLenght() en alloceerde ik precies die hoeveelheid bytes, maar dan bleken alle titels te kort te zijn. Ik heb niet echt goed gekeken wat hiervan de oorzaak is, maar domweg 30 bytes meer gealloceerd. In mijn geval werkt het prima, maar misschien dat iemand hier de oorzaak van weet of er eens naar kan kijken.
Een ander punt van verbetering is het feit dat er niet goed wordt gereageerd als MemAlloc() mislukt.
Zoals veel van ons ben ik blij met codevoorbeelden van anderen. Dat was voor mij de reden om dit artikel te schrijven.
Niets uit deze uitgave mag gepubliceerd of hergebruikt worden, in welke vorm dan ook, zonder de schriftelijke toestemming van de auteur