Autor Beitrag
ASMFreak
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 53
Erhaltene Danke: 9



BeitragVerfasst: Fr 09.12.11 17:59 
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.
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:
  TProcessThread = class(TThread)
  private
    FProcess: Process;                           // zu startender Prozess
    FWaitHandles: Array[0..2of WaitHandle;     // Feld von Strukturen, die eine Wait-Anweisung beenden können
    FTerminateEvent: EventWaitHandle;            // Zum Unterbrechen der Wait-Anweisung bei Terminierung
    FKillerEvent: EventWaitHandle;               // Zum Unterbrechen der Wait-Anweisung bei Abort
    FProcHandle: EventWaitHandle;                // Zum Feststellen des Endes des Prozesses
    FStreamWriter: StreamWriter;
    function  CheckExited(ATerminationTimeOut: Integer): Boolean;  // verwaltet TimeOut-Zählung
    procedure ProcessExit(Sender: TObject; Args: EventArgs);       // Exited-Handler des Prozesses
  protected
    procedure Execute; override;                 // startet Prozess und wartet auf Ereignisse
    procedure HandleAbort; virtual;              // Aktionen, die im Falle des unbedingten Abbruchs erforderlich sind
    procedure HandleExit; virtual;               // Aktionen, die im Falle des regulären Endes erforderlich sind
    procedure HandleHanging; virtual;            // Aktionen, die im Falle des Hängens erforderlich sind
    procedure HandleTermination; virtual;        // Aktionen, die im Falle des kontrollierten Abbruchs erforderlich sind
    procedure HandleTimeOut; virtual;            // Aktionen, die im Falle des Timeouts erforderlich sind.
    procedure InitializeHandles; virtual;        // erzeugt und befüllt FWaitHandles
    procedure InitializeProcess; virtual;        // erzeugt die StartInfo-Struktur und FProcess
  public
    procedure AbortProcess; virtual;             // versucht, den Prozess zu killen
    procedure TerminateProcess; virtual;         // versucht, den Prozess "anständig" zu beenden
  end;

Dieser Thread wird "von außen" erzeugt und gesteuert. Das erfolgt über eine eigene Klasse, deren Kernstück folgendes ist:
ausblenden 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:
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:
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;     // Input umgeleitet, wenn Output umgeleitet!
      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:
ausblenden 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):
ausblenden 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:
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:
28:
29:
procedure TProcessThread.HandleTermination;
begin
  FProcess.CloseMainWindow;
  if CheckExited(FTerminationTimeOut) then
    Exit;
  // Weiterer Code, der mit der Terminierung nichts weiter zu tun hat.
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 > 0do
  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
ausblenden 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.
Und ich lächelte und ich war froh – und es kam schlimmer!
ASMFreak Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 53
Erhaltene Danke: 9



BeitragVerfasst: Fr 09.12.11 18:12 
Hallo,

manchmal hilft es, sein Hirn einzuschalten. Das Problem mit dem Hängen habe ich gelsöt, es liegt daran, dass ich vor dem Prüfen auf FProcess.Responding FProcess.Refresh aufrufen muss. Für die beiden andern Probleme habe ich noch keine Lösung, wäre also für Hilfe dankbar.

Gruß,
ASMFreak.

_________________
Und aus dem Chaos sprach eine Stimme zu mir: Lächle und sei froh, es könnte schlimmer kommen.
Und ich lächelte und ich war froh – und es kam schlimmer!