C# Mehrere Konsolenbefehle ausführen und zwischendrin den Ausgabestream lesen

Ghost_Rider_R

Lieutenant
Registriert
Nov. 2009
Beiträge
752
Hallo zusammen,

hat zufällig jemand ein Snippet parat, bei dem ich mit einem Process mehrmals abwechselnd einen CMD Befehl absetzten kann und den Returnwert z.B. auf der Konsole ausgeben kann und anschließend wieder einen Befehl absetzen kann & auslesen kann? d.h. mehrmals abwechselnd schreiben, lesen, schreiben, lesen usw. mit einem Prozess.

Wenn ich zum lesen ReadToEnd() verwende, muss ich vorher den InputStream schließen, aber nach dem Lesen ist dann auch der Prozess beendet d.h. ich kann nur einmal schreiben und einmal lesen -> Prozess Beendet. Ich würde aber gerne mit dem selben Prozess mehrmals abwechselnd diese Prozedur ausführen.

Ich habe schon probiert mittels Read() nur den aktuellen Puffer auszulesen, aber da kann es dann halt passieren, dass ich nicht die komplette Ausgabe wie im CMD Fenster erwische, da es ja nur ein Stream ist, welcher kein definiertes Ende in dem Sinne hat.

Hat da jemand eine Idee?

LG Ghost Rider
 
Ein Snippet direkt habe ich nicht, aber zwei Ideen.
  • Du nutzt die Events um die Ausgabe-Daten zu lesen. BeginRead() nicht vergessen! Dadurch bist du aber asynchron und musst eine Queue o. Ä. nutzen.
  • Du liest solange bis du das Trennzeichen z. B. "Neue Zeile" gelesen hast und nimmst dann die Ausgabe des Prozesses als fertig an. (evtl. zusätzlich eine Zeit x warten - aber das ist immer eine Art Code Smell).
Ich hatte letztens auch diese Art von Problem. Bin aber direkt dabei geblieben den Prozess jedes Mal mit anderen Parametern neu zu starten und mit Events zu lesen. Events deshalb weil es mit ReadToEnd zwischendurch immer schön gehangen hat. Ist ein bekanntes Phänomen. Findest dazu auch vieles im Netz.
 
  • Gefällt mir
Reaktionen: Ghost_Rider_R
@marcOcram Vielen Dank für die Rückmeldung. Ja Asynchron wäre eine Möglichkeit, aber da ich hier eine allgemeine Bibliothek schreiben möchte, wäre es mir schon wichtig, dass diese synchron abgearbeitet wird, da ich auch noch nicht weiß, wo diese überall zum Einsatz kommen wird.

Die zweite Variante hat halt auch so einen faden Beigeschmack. Eine ähnliche (unsynchrone) Lösung habe ich aktuell und würde gerne davon wegkommen.

Im Enddefekt soll meine Klasse KonsolenManager nachher in etwa so funktionieren:

Process cmdProzess = xyz.HolenCMDProzess();
KonsolenManager.AusfuehrenBefehl(cmdProzess, "Das ist mein Befehl");
string ausgabe = KonsolenManager.LesenAusgabe(cmdProzess);
KonsolenManager.AusfuehrenBefehl(cmdProzess, "Das ist mein nächster Befehl");
ausgabe = KonsolenManager.LesenAusgabe(cmdProzess);
KonsolenManager.AusfuehrenBefehl(cmdProzess, "Das ist mein letzer Befehl");
ausgabe = KonsolenManager.LesenAusgabe(cmdProzess);

Wenn hier dann asynchrone Prozesse dazu kommen, wird es einfach wieder unleserlich, daher möchte ich die fertige Bibliothek gerne so wie im Beispiel verwenden. Wenn ich dann auf die Abarbeitung des jeweiligen Befehls warten muss, ist das für mich völlig in Ordnung.

Im Enddefekt möchte ich also das Verhalten einer Konsole nachempfinden. Im CMD Fenster gebe ich ja auch einen Befehl ein und muss mit dem nächsten Befehl so lange warten, bis der vorherige Befehl abgearbeitet wurde und die Ausgabe beendet wurde. Erst danach kann ich den nächsten Befehl eingeben und so möchte ich es auch haben.
 
Jeder Konsolenbefehl ist doch ein Prozess. Wenn du cmd öffnest und da "ping XYZ" eintippst, dann wird ping.exe als neuer Prozess ausgeführt und cmd.exe wartet darauf dass dieser sich beendet. Dann kannst du einen neuen Befehl eingeben.

Wenn du keine CMD-Syntax brauchst kannst du die entsprechenden Programme/Befehle auch direkt ausführen, ich sehe nicht welchen Mehrwert der Umweg über CMD bringt. In CMD eingebaute Befehle wie mkdir kannst du besser in C# Code selbst implementieren.

Ghost_Rider_R schrieb:
Ich habe schon probiert mittels Read() nur den aktuellen Puffer auszulesen, aber da kann es dann halt passieren, dass ich nicht die komplette Ausgabe wie im CMD Fenster erwische, da es ja nur ein Stream ist, welcher kein definiertes Ende in dem Sinne hat.
Dieses Problem lässt sich auch grundsätzlich nicht lösen. Solange der Prozess noch läuft kannst du nie wissen, wann er mit seiner Ausgabe wirklich fertig ist. Wenn du sicher weißt, dass das Programm z.B. genau eine Zeile ausgibt könntest du solange lesen bis du einen Zeilenumbruch gefunden hast. Ansonsten brauchst du ein Signal dass der Prozess fertig ist (i.d.R. dass er sich beendet) und dieses Signal nimmt CMD dir aber weg.
 
  • Gefällt mir
Reaktionen: Ghost_Rider_R
Also ich würde ja eine existierende Bibliothek verwenden, statt eine zu schreiben, wenns nicht nur um den Lerneffekt geht... ich verwende für sowas CliWrap. Dies ist ganz hervorragend.

Gibt sogar ein Youtube-Video zur Einführung, wenn man keine Text-Tutorials / Doku mag:

Wenns um den Lerneffekt geht, kann ich nur empfehlen, dir den Code der Library mal anzuschauen, der ist ziemlich gut ausgedacht.

C#:
var result = await Cli.Wrap("ls")
                // .WithWorkingDirectory(_fs.Directory.GetCurrentDirectory())
                .WithArguments("/tmp")
                .WithValidation(CommandResultValidation.None)
                .ExecuteBufferedAsync();
          
return (result.ExitCode, result.StandardError, result.StandardOutput);

Du könntest für deinen Fall auch mit dem await foreach Konstrukt arbeiten, dann kannst du auf jedes Event entsprechend reagieren:

C#:
using CliWrap;
using CliWrap.EventStream;

var cmd = Cli.Wrap("foo").WithArguments("bar");

await foreach (var cmdEvent in cmd.ListenAsync())
{
    switch (cmdEvent)
    {
        case StartedCommandEvent started:
            _output.WriteLine($"Process started; ID: {started.ProcessId}");
            break;
        case StandardOutputCommandEvent stdOut:
            _output.WriteLine($"Out> {stdOut.Text}");
            break;
        case StandardErrorCommandEvent stdErr:
            _output.WriteLine($"Err> {stdErr.Text}");
            break;
        case ExitedCommandEvent exited:
            _output.WriteLine($"Process exited; Code: {exited.ExitCode}");
            break;
    }
}
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Ghost_Rider_R
Wenn du das selber machen willst, hast du aus Sicht der WinAPI zwei Möglichkeiten den Output von Konsolenapplikationen über den Parent-Prozess auszulesen:
  1. Per Anonymous Pipe
  2. Über den Console Screen Buffer
Beide Varianten haben Vor- und Nachteile, wobei Nr. 2 die eigentlich "sauberere" aber komplexere Variante ist.

Es gibt mittlerweile noch eine dritte Variante: "Pseudoconsole Sessions".
Das war zur damaligen Zeit, zu der ich mich mit diesem Thema befasst habe, aber noch nicht Spruchreif und ist zudem "nur" ab Windows 10 verfügbar.
(Beim Überfliegen liest sich das ähnlich zur Variante Nr. 2, nur hoffentlich mit weniger Bullshit...:heul:)

Zu Nr. 1:
Grober Ablauf:
  • Security-Attributes mit Vererbung definieren.
  • Pipe-Handels per CreatePipe und den definierten Security-Attributes erzeugen.
  • Startup-Info mit dem Write-Handle der Pipe als StdOutput und StdError definieren.
  • Konsolenapplikationen über CreateProcess mit den definierten Security-Attributes und der Startup-Info ausführen.
  • In einer Schleife auf den Child-Prozess mit geringem Timeout warten.
  • Nach Ablauf des Timers oder nach Ende des Child-Prozesses die Pipe per ReadPipe mit dem Read-Handle auslesen.
Theoretisch ohne Threading machbar: Erstmal alle Prozesse starten und dann in einer Schleife die einzelnen Pipes auslesen.
Führt dann aber dazu, dass der Hauptprozess so lange beschäftigt ist, bis alle Child-Prozesse zum Ende gekommen sind.

Vorteile:
  • Relativ simpel.
  • Funktioniert auch mit crappy, schnell gehacktem Code.
  • Kein Output geht verloren, da der Child-Prozess blockiert, wenn der Buffer von StdOuput oder StdError voll ist.
Nachteile:
  • Der Child-Prozess blockiert, wenn der Buffer von StdOuput oder StdError voll ist.
    • Auf Seiten des Hauptprozesses macht das vor allem bei viel schreibenden Anwendungen ein Auslesen und leeren des Pipe-Buffers in sehr kurzen Zeitabständen notwendig, um die Ausführung des Child-Prozess nicht unnötig aufzuhalten.
    • Auf Seiten des Child-Prozesses kann es bei komplexen Anwendungen durch Blockierungen zu Fehlern kommen, wenn z.B. Timing-relevante oder/und Timeout-behaftete Dinge getan werden.
  • Liefert nur den reinen Text des Outputs. Farbinformationen gehen verloren.
  • Die Wahrscheinlichkeit, dass der Child-Prozess vom Windows-Scheduler auf seinem "eigenen" CPU-Core/Thread laufen gelassen werden kann, ist, wegen den häufigen Interaktion mit dem Hauptprozess, eher gering.

Zu Nr. 2:
Grober Ablauf:
  • Security-Attributes mit Vererbung definieren.
  • Les- und Schreibbaren Console Screen Buffer per CreateConsoleScreenBuffer und den definierten Security-Attributes erzeugen.
  • Per GetConsoleScreenBufferInfo und SetConsoleScreenBufferInfo die Größe des Console Screen Buffers so groß wie möglich machen.
  • Per FillConsoleOutputCharacter den Console Screen Buffer vorbelegen.
  • Startup-Info mit dem Console-Screen-Buffer-Handle als StdOutput und StdError definieren.
  • Konsolenapplikationen über CreateProcess mit den definierten Security-Attributes und der Startup-Info ausführen.
  • Ab hier wirds jetzt u.U. kompliziert:
    • Wenn man mit Sicherheit sagen kann, dass die Konsolenapplikationen nicht den kompletten Console Screen Buffer vollschreibt, ist es das Beste und einfachste, auf das Ende des Child-Prozesses zu warten, um dann per ReadConsoleOutput den Console Screen Buffer auszulesen.
    • Steht es im Raum des Möglichen, dass die Konsolenapplikationen den kompletten Console Screen Buffer vollschreibt, wirds kompliziert (denn während dem Auslesen kann die Konsolenapplikationen ja weiterhin schreiben):
      • Per GetConsoleScreenBufferInfo die Y-Position der Konsolenapplikationen beziehen. (Und hoffen, dass sie dem Ende noch nicht zu nah ist...)
      • Per ReadConsoleOutput den Console Screen Buffer bis Y-1 auslesen.
      • Per FillConsoleOutputCharacter bis Y-1 erneut vorbelegen (also quasi Leeren).
      • Per SetConsoleScreenBufferInfo die Position der Konsolenapplikationen ganz auf Anfang setzen.
      • Per ReadConsoleOutput den Console Screen Buffer ab Y bis Ende auslesen.
      • Per FillConsoleOutputCharacter von Y bis Ende ebenso erneut vorbelegen (also quasi Leeren).
      • Die beiden ausgelesenen Outputs so zusammenführen, wie sie zusammengehören.
      • Rinse and Repeate bis der Child-Prozess zum Ende gekommen ist.
      • Während des gesamten Vorgangs nicht vergessen immer schön zu Beten!
Theoretisch ohne Threading machbar, wenn davon ausgegangen werden kann, dass die Konsolenapplikationen nicht den gesamten Buffer mehr als vollschreiben: Erstmal alle Prozesse starten und dann in einer Schleife immer mal wieder die einzelnen Child-Prozesse abklappern, um zu schauen, ob sie schon fertig sind. Wenn ja, dann einfach Output auslesen.
Threading in wärmstens empfohlen, wenn möglichst garantiert werden soll, dass der gesamte Output einer jeder beliebigen Konsolenapplikationen erfasst wird, vor allem wenn diese viel schreiben und lange laufen.
Hintergrund ist der, dass der Konsolenapplikation ein alternativer Console Screen Buffer zur Verfügung gestellt wird, auf dem der Hauptprozess Lese- und Schreibrechte besitzt.
Dieser Console Screen Buffer funktioniert aber genauso, wie der Console Screen Buffer der Command Line. D.h. er kann maximal 32766 Zeilen lang sein und wird mehr hineingeschrieben, so läuft er über, indem die ersten Zeilen aus dem Buffer wieder entfernt werden.

Vorteile:
  • Der Output kann 1-zu-1, also inkl. Farbinformationen, ausgelesen werden.
  • Der Child-Prozess wird zu keiner Zeit blockiert.
  • Wenn bis zum Ende des Child-Prozesses gewartet werden kann, ist die Wahrscheinlichkeit hoch, dass der Scheduler von Windows den Child-Prozess auf seinem "eigenen" CPU-Core/Thread laufen lassen wird.
Nachteile:
  • Recht komplex in der Implementierung, es gibt viele kleine Fallstricke und viel zu beachten.
  • Bei sehr schreiblustigen Anwendungen oder Verzögerungen im Hauptprozess keine 100%ige Garantie, dass lückenlos der komplette Output erfasst wird.
  • Der Output ist entsprechend der 2-dimensionalen Ausmaße des Console Screen Buffers formatiert (genauso, wie man es aus der Command Line auch kennt). Das muss beachtet und behandelt werden, wenn der Inhalt wo anders sauber dargestellt werden soll.
  • Es gibt Konsolenapplikationen (z.B. Ping), die "scheißen" auf den übergebenen Console Screen Buffer und schreiben stattdessen einfach in den Console Screen Buffer des Hauptprozesses durch. :freak: (Kack Windows... mein Ernst! Was für eine Scheiße!!! :grr:)
 
  • Gefällt mir
Reaktionen: Ghost_Rider_R und PHuV
👍 Klasse und lobenswert beschriebene Lösungen.
AW4 schrieb:
  • Es gibt Konsolenapplikationen (z.B. Ping), die "scheißen" auf den übergebenen Console Screen Buffer und schreiben stattdessen einfach in den Console Screen Buffer des Hauptprozesses durch. :freak: (Kack Windows... mein Ernst! Was für eine Scheiße!!! :grr:)
Kommt vielleicht daher, daß dieses Programm einfach von einem Unix/BSD Code 1:1 übernommen wurde? Aber da war ich zuwenig Windows Entwickler, um das beurteilen zu können.
 
Ich habe es nun nochmals anders gelöst. Ich verwende für jeden Befehl einen eigenen Prozess im Hintergrund von AusfuerenBefehl() und gebe die Antwort auch gleich aus. Dann habe ich künftig bei der Verwendung eine schlanke Methoden:

KonsolenManager konsolenmanger = new KonsolenManager();
string ausgabe = konsolenmanger .AusfuehrenBefehl("Das ist mein erster Befehl");
ausgabe = konsolenmanger .AusfuehrenBefehl("Das ist mein zweiter Befehl");
ausgabe = konsolenmanger .AusfuehrenBefehl("Das ist mein letzer Befehl");

Ich denke so ist es für mich sehr praktikabel.

Vielen Dank für eure Hilfe!
 
  • Gefällt mir
Reaktionen: Ghost_Rider_R
Zurück
Oben