Wenn man sein Programm auf mehrere Prozesse aufgeteilt hat, oder andere Programme fernsteuern möchte, ist es oftmals nötig, dafür zu sorgen, dass man in seinem eigenen Programm wartet, bis das Fremdprogramm nicht nur als
Prozess gestartet wurde, sondern vollständig geladen hat und vollständig reagiert.
Da Windows keine Benachrichtigung dieser Art für Fremdprozesse kennt, muss man hierfür eine Reihe von anderen Merkmalen auswerten. Eines der einfachsten Mittel hierfür - und zugleich auch eines der zuverlässigsten - ist die Nutzung einer WM_NULL-Nachricht. Diese bewirkt im Zielprozess nichts (alles andere wäre ein Implementationsfehler), lässt aber in Kombination mit
SendMessageTimeout Rückschlüsse auf die Reaktionsgeschwindigkeit des Zielprozesses zu, da die Verarbeitung einer Nachricht abgebrochen werden kann, sollte dies nicht innerhalb einer gegebenen Zeit geschehen.
Anhand dieses Verhaltens kann man somit ermitteln, ob ein
Prozess in regelmäßigen Abständen seine Message-Queue liest. Dies ist während ein Programm vom Benutzer bedient werden kann und soll in regelmäßigen Abständen der Fall, weshalb hier, außer durch Prozessumschaltzeiten auf Seiten von Windows, keine Verzögerungen auftreten. Anders sieht dies jedoch aus, wenn die Anwendung gerade beschäftigt ist, weil sie z.B. Eingaben des Benutzers in einer längeren Operation verarbeitet, oder eben gerade startet. In diesem Falle wird die Nachrichtenverarbeitung für Nachrichten nur sporadisch aufgerufen (In-
Prozess-Nachrichten werden aber i.d.R. direkt an die Verarbeitungsroutinen weitergeleitet, externe Nachrichten benötigen der Zuarbeit der Anwendung). Diesen Umstand kann man ausnutzen, um zu prüfen ob eine Anwendung bereits geladen ist, da während des Starts auf grund oftmals langer Zeiten zwischen den Aufrufen der Nachrichtenverarbeitung lange Wartezeiten auftreten.
Sendet man somit in regelmäßigen Abständen eine Nachricht an einen gerade startenden
Prozess, so erhält man ein recht gutes Bild darüber, ob sich dieser noch initialisiert. Als Kriterien kann man hierbei wie erwähnt die Reaktionszeit auf eine Windows-Nachricht hernehmen: Wenn eine Nachricht binnen 50ms Bearbeitet wird (was auf einem ausgelasteten System durchaus auch von reagierenden Anwendungen überschritten werden kann), so meldet man die Anwendung als reagierend. Da man jedoch u.U. genau den Timeslot erwischt hatte, in dem die Zielanwendung gerade z.B. den Splashscreen neu gezeichnet hat, ist diese Information nur von geringem Wert für eine Entscheidung darüber, ob sie bereits fertig ist. Dies ändert sich jedoch, wenn man die Reaktionszeit über mehrere Zeitschlitze hinweg beobachtet: Wenn über mehr als eine gegebene Zeitspanne hinweg die Anwendung immer reagiert, kann man davon ausgehen, dass sie nun für Benutzereingaben bereit ist. Um dabei zu vermeiden, dass durch die eigene Warteschleife unnötig CPU-Zeit verbraten wird und dass man aus Versehen alle Kontrollnachrichten in den gleichen Aufruf der Warteschlange schickt, sollten hierbei Wartezeiten mit eingeplant werden. In meinem Fall sind dies 10ms, die Windows Anweisen, meine Anwendung für einen Timeslot, mindestens aber für 10ms schlafen zu legen. Somit gebe ich der fremden Anwendung eine Chance, weiter zu arbeiten, statt nur mit dem Bearbeiten meiner Prüfnachrichten beschäftigt zu sein.
Die fertige Routine sieht somit wie folgt aus:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86:
| Function CreateProcessWaitReady(ProgramFile: String; Commandline: String = ''; CurrDir: String = '.'): Boolean; Var StartInfo: TStartupInfo; ProcInfo: TProcessInformation;
WFSO: DWORD; SMT_Count: Integer; SMT_Done: Boolean; Type TEWPInfo = Packed Record PI: PProcessInformation; SMTD: PBoolean; End; PEWPInfo = ^TEWPInfo; Var EWPI: TEWPInfo;
Function ETWProc(wnd: HWND; Param: PEWPInfo): Boolean; Stdcall; Var Res: DWORD; Begin If SendMessageTimeoutA( wnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, 50, Res) <> 0 Then Begin Param.SMTD^ := True; Sleep(10); End; Result := Not Param.SMTD^; End;
Begin FillChar(StartInfo, SizeOf(TStartupInfo), #0); FillChar(ProcInfo, SizeOf(TProcessInformation), #0); StartInfo.cb := SizeOf(TStartupInfo); StartInfo.dwFlags := STARTF_USESHOWWINDOW Or STARTF_USEPOSITION Or STARTF_USESIZE; StartInfo.wShowWindow := SW_SHOW;
Commandline := Format('"%s" %s', [ProgramFile, Trim(Commandline)]);
Result := CreateProcess( Nil, pChar(Commandline), Nil, Nil, false, NORMAL_PRIORITY_CLASS, Nil, pChar(CurrDir), StartInfo, ProcInfo );
If Result Then Begin SMT_Count := 0; EWPI.PI := @ProcInfo; EWPI.SMTD := @SMT_Done; Repeat
WFSO := WaitForSingleObject(ProcInfo.hProcess, 100);
SMT_Done := False; EnumThreadWindows(ProcInfo.dwThreadId, @ETWProc, Integer(@EWPI)); If SMT_Done Then Inc(SMT_Count) Else SMT_Count := 0; Until (WAIT_OBJECT_0 = WFSO) Or (SMT_Count >= 10);
Result := SMT_Count >= 10; End;
If ProcInfo.hProcess <> 0 Then CloseHandle(ProcInfo.hProcess); End; |
Die Verwendung dieser Routine erfolgt nun analog zu
CreateProcess. Hier mal ein kleines Beispiel:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
| Procedure TForm1.Button1Click(Sender: TObject); Begin Panel1.Caption := 'Starting ...'; Panel1.Color := clYellow; Update; Try If CreateProcessWaitReady(Edit1.Text) Then Begin Panel1.Caption := 'Started !!!'; Panel1.Color := clLime; Update; End Else Begin Panel1.Caption := 'Failed !!!'; Panel1.Color := clRed; Update; Raise EOSError.Create(SysErrorMessage(GetLastError)); End; Finally Sleep(500); Panel1.Caption := 'Bereit'; Panel1.Color := clBtnFace; Update; End;
End; |
Edit1 ist der auszuführende Befehl,
Panel1 zeigt den Status an,
Button1 ist zum
Starten der Anwendung.
Durch Anpassung des Wertes bei WaitForSingleObject kann man die Wartezeit zwischen Prozessabfragen konfigurieren. Dieses Timeout dient gleichzeitig auch dazu, um zu prüfen, dass der
Prozess noch läuft. Die 50 beim Aufruf von SendMessageTimeout gibt die maximale Wartezeit auf eine Antwort auf die versendete WM_NULL-Nachricht an. Dieser Wert ist für reagierende Anwendungen uninteressant, addiert sich für "hängende" Anwendungen auf den Timeout von WaitForSingleObject auf. Der Sleep-Befehl in der lokalen Callback-Routine sorgt für einen gewissen Abstand zwischen den einzelnen Prüfnachrichten. Je kleiner dieser ist, desto schneller wird eine Anwendung als reagierend erkannt; desto unzuverlässiger wird die Erkennung jedoch auch. Die Zykluszeit ist somit mindestens 100ms, maximal jedoch 160ms (wenn eine Nachricht kurz vor dem Timeout erst verarbeitet wird).
Anyone who is capable of being elected president should on no account be allowed to do the job.
Ich code EdgeMonkey - In dubio pro Setting.