Hallo Forum.
Ich möchte gerne einen Prozess aus einem eigenen Thread heraus starten. Dazu habe ich folgende Klasse erstellt. Der Übersichtlichkeit halber habe ich alle Felder, Methoden und Properties weggelassen, die bei dem Problem keine Rolle spielen.
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:
| TProcessThread = class(TThread) private FProcess: Process; FWaitHandles: Array[0..2] of WaitHandle; FTerminateEvent: EventWaitHandle; FKillerEvent: EventWaitHandle; FProcHandle: EventWaitHandle; FStreamWriter: StreamWriter; function CheckExited(ATerminationTimeOut: Integer): Boolean; procedure ProcessExit(Sender: TObject; Args: EventArgs); protected procedure Execute; override; procedure HandleAbort; virtual; procedure HandleExit; virtual; procedure HandleHanging; virtual; procedure HandleTermination; virtual; procedure HandleTimeOut; virtual; procedure InitializeHandles; virtual; procedure InitializeProcess; virtual; public procedure AbortProcess; virtual; procedure TerminateProcess; virtual; end; |
Dieser Thread wird "von außen" erzeugt und gesteuert. Das erfolgt über eine eigene Klasse, deren Kernstück folgendes ist:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13:
| procedure TProcessLauncher.LaunchProcess; begin FProcessThread := TProcessThread.Create(True); with TProcessThread(FProcessThread) do try InitializeHandles; InitializeProcess; Execute; HandleThreadEnd; except raise; end; end; |
Der Thread wird "suspended" erzeugt, um die Initialisierungen einerseits des EventWaitHandle-Arrays und andererseits der Prozess-Klasse zu ermöglichen:
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:
| procedure TProcessThread.InitializeHandles; begin FTerminateEvent := EventWaitHandle.Create(False, EventResetMode.ManualReset); FWaitHandles[0] := FTerminateEvent; FKillerEvent := EventWaitHandle.Create(False, EventResetMode.ManualReset); FWaitHandles[1] := FKillerEvent; FProcHandle := EventWaitHandle.Create(False, EventResetMode.ManualReset); FWaitHandles[2] := FProcHandle; end;
procedure TProcessThread.InitializeProcess; begin FProcess := System.Diagnostics.Process.Create; with FProcess do try with StartInfo do begin FileName := FProgName; WorkingDirectory := FWorkDir; Arguments := FArguments; UseShellExecute := FUseShellExecute; CreateNoWindow := (FWindowStyle = pwsNone); if not CreateNoWindow then case FWindowStyle of pwsHidden: StartInfo.WindowStyle := ProcessWindowStyle.Hidden; pwsMaximized: StartInfo.WindowStyle := ProcessWindowStyle.Maximized; pwsMinimized: StartInfo.WindowStyle := ProcessWindowStyle.Minimized; pwsNormal: StartInfo.WindowStyle := ProcessWindowStyle.Normal; end; RedirectStandardOutput := (not FUseShellExecute) and Assigned(FOnOutput); RedirectStandardError := (not FUseShellExecute) and Assigned(FOnError); RedirectStandardInput := RedirectStandardOutput; EnableRaisingEvents := True; Include(Exited, ProcessExit); if RedirectStandardOutput then Include(OutputDataReceived, FOnOutput); if RedirectStandardError then Include(ErrorDataReceived, FOnError); Start; if RedirectStandardInput then FStreamWriter := FProcess.StandardInput; if RedirectStandardOutput then BeginOutputReadLine; if RedirectStandardError then BeginErrorReadLine; end; except raise; end; end; |
Gestartet wird der Thread dann mit:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
| procedure TProcessThread.Execute; begin Interlocked.Exchange(FHeartBeats, 0); while (not Terminated) do with WaitHandle do begin case WaitAny(FWaitHandles, FPulse, False) of WaitTimeOut: if CheckTimeOut(FProcessTimeOut) then HandleTimeOut; 0: HandleTermination; 1: HandleAbort; 2: HandleExit; end; if not FProcess.Responding then HandleHanging; Interlocked.Increment(FHeartBeats); Application.ProcessMessages; end; end; |
WaitAny kann nun durch vier Ereignisse abgebrochen werden. Zwei davon können von außen erfolgen: AbortProcess und TerminateProcess, das dritte ist der Eventhandler für das Prozessende (Exited-Handler):
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
| procedure TProcessThread.AbortProcess; begin FKillerEvent.&Set; end;
procedure TProcessThread.ProcessExit(Sender: TObject; Args: EventArgs); begin FProcHandle.&Set; end;
procedure TProcessThread.TerminateProcess; begin FTerminateEvent.&Set; end; |
Das vierte Ereignis ist ein TimeOut, der alle FPulse (Standard: 50) msec erfolgt, um vier Dinge zu ermöglichen: Mitzählen der Pulse in FHeartBeats, ProcessMessages, Prüfung auf hängenden Prozess und Prüfung auf ein "ProcessTimeout", also eine angebbare Zeitspanne, die der Prozess nicht überschreiten darf. Die jeweiligen Aktivitäten wurden in entsprechende "Handler" ausgelagert. Die für das Problem wesentlichen sind hier angegeben:
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:
| procedure TProcessThread.HandleTermination; begin FProcess.CloseMainWindow; if CheckExited(FTerminationTimeOut) then Exit; end;
procedure TProcessThread.HandleAbort; begin FProcess.Kill; if not CheckExited(FAbortTimeOut) then raise Exception.Create; end;
function TProcessThread.CheckExited(ATerminationTimeOut: Integer): Boolean; begin while (not FProcess.HasExited) and (ATerminationTimeOut > 0) do begin Dec(ATerminationTimeOut, FPulse); Thread.Sleep(FPulse); Application.ProcessMessages; end; Result := FProcess.HasExited; if Result then Terminate; end; |
Das funktioniert auch alles ganz gut, inklusive ProzessTimeOut. Wenn man z.B. cmd.exe auf diese Weise ausführen lässt, werden die Ausgaben, so die entsprechenden Handler installiert sind, z.B. in TMemos umgeleitet. TerminateProcess, ausgelöst durch einen Button, funkitionert ebenso wie AbortProcess. Auch das "reguläre" Beenden via "exit" oder Schließen des Fensters funktioniert.
Es gibt aber drei Probleme:
1. Sendet man an das so gestartete cmd.exe via
Delphi-Quelltext
1: 2: 3: 4:
| procedure TProcessLauncher.SendInput(Input: String); begin FProcessThread.FStreamWriter.WriteLine(Input); end; |
auch nur einen einzigen Befehl, wird der zwar ausgeführt, TerminateProcess und AbortProcess aber funktionieren nicht mehr. Statt den Prozess kooperativ zu beenden oder abzubrechen, wiederholen sie den zuletzt eingegebenen Befehl. Der mach zwar alles, was man erwartet, inklusive Ausgabeumleitung. cmd.exe lässt sich dann nur noch durch das Senden von exit (regulär) beenden. Haltepunkte an entsprechender Stelle in Execute oder den Handlern zeigen, dass diese in diesem Fall nicht mehr angesprungen werden. Ich vermute, das liegt an FStreamWriter, ich weiß aber nicht, was ich falsch mache. Liegt das daran, dass ich synchron sende, während ich asynchron empfange? Wenn ja: Wie kann man asynchron senden?
2. Startet man GUI-Anwendungen (und zwar ohne Umleitung von StdOutput und StdError), funktioniert zwar AbortProcess, nicht aber TerminateProcess, obwohl dieses doch das MainWindow schließen soll. Ich weiß nicht, warum nicht!
3. Ein kleines GUI-Programm, mit dem ich einen händenden Prozess simulieren will, und das via Button-Druck in eine Endlosschleife ohne ProcessMessages geschickt werden kann, zeigt, dass FProcess.Responding grundsätzlich "True" zurückgibt, obwohl Windows anzeigt "keine Rückmeldung". Die dokumentation sagt zwar, dass True zurückgesendet wird, wenn der Prozess kein MainWindowHandle hat, er hat ja aber eines. Wo ist das Problem?
Danke Euch für alle Tipps.
ASMFreak
Und aus dem Chaos sprach eine Stimme zu mir: Lächle und sei froh, es könnte schlimmer kommen.