Autor Beitrag
BenBE
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 8721
Erhaltene Danke: 191

Win95, Win98SE, Win2K, WinXP
D1S, D3S, D4S, D5E, D6E, D7E, D9PE, D10E, D12P, DXEP, L0.9\FPC2.0
BeitragVerfasst: Mi 15.07.09 08:44 
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:

ausblenden volle Höhe 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:
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:
// Wait for a process to be fully started and responding.
// (C) 2007-2009 by Benny Baumann (BenBE).
// Use in freeware or with written permission granted as long as copyright and licence remain intact.
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

            //At first check if the process is still running (and lower CPU load) ...
            WFSO := WaitForSingleObject(ProcInfo.hProcess, 100);

            //Now check for at least one window that responds in time
            SMT_Done := False;
            EnumThreadWindows(ProcInfo.dwThreadId, @ETWProc, Integer(@EWPI));
            If SMT_Done Then
                Inc(SMT_Count) //Program responded ...
            Else
                SMT_Count := 0//No reaction, reset counter
        Until (WAIT_OBJECT_0 = WFSO) Or (SMT_Count >= 10);

        Result := SMT_Count >= 10//Check if the program was responding for a given time
    End;

    If ProcInfo.hProcess <> 0 Then
        CloseHandle(ProcInfo.hProcess);
End;


Die Verwendung dieser Routine erfolgt nun analog zu CreateProcess. Hier mal ein kleines Beispiel:

ausblenden 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.