Ausgabe von Pseudoterminal lesen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich gaukel einem Prozess mittels `pty` ein Terminal vor und kann so direkt Befehle absenden, die auch ausgeführt werden. Es gibt aber ein Problem mit dem Einlesen. Meine `read()`-Methode gibt die Daten zeilenweise aus. Das Problem entsteht nach Erreichen der letzten Zeile. Wenn `read()` keine neuen Daten bekommen kann, wartet es auf Nutzereingaben. Genau dies möchte ich aber vermeiden. Ich möchte sozusagen vorher wissen, ob noch Zeilen anstehen oder nicht. Am liebsten wäre mir sogar eine Funktion, die den kompletten Ausgabestrom des Prozesses bis zu diesem Zeitpunkt (also nicht erst nach Beendigung) ausgibt.

Hier mein Code:

Code: Alles auswählen

import pty
import os


class ReadWriteProcess(object):

    def __init__(self, *args):
        self.pid, self.fork = pty.fork()
        if self.pid == 0:
            try:
                arguments = args[1:]
            except IndexError:
                arguments = ()
            os.execv(args[0], arguments)

    def read(self, max=100000):
        #line = os.read(self.fork, max)
        #if line:
            #return line
        return os.read(self.fork, max)

    def write(self, s):
        os.write(self.fork, s)

    def sendcommand(self, cmd):
        self.write(cmd + '\n')
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Du könntest [mod]select[/mod] benutzen und damit vor dem Lesen eben schauen, ob Daten vorhanden sind, oder den FD mit [mod]fcntl[/mod] in einen nicht-blockierenden Zustand versetzen (`fcntl.F_SETFL` und `os.O_NONBLOCK` helfen weiter).

Das try-except in Zeile 10 ff. ist recht witzlos, da der "IndexError" nur auftreten kann, wenn die Liste leer ist, was dann aber in Zeile 14 eh einen "IndexError" werfen würde.

Edit: `os.O_NONBLOCK` natürlich.
Zuletzt geändert von Trundle am Dienstag 3. März 2009, 19:22, insgesamt 1-mal geändert.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Danke es klappt anscheinend. :)

Code: Alles auswählen

import os
import pty
import select
import time


class ReadWriteProcess(object):

    def __init__(self, *args):
        "args = An executable path with optional arguments"
        self.pid, self.proc = pty.fork()
        if self.pid == 0:
            os.execv(args[0], args[1:])

    def read(self, max_bytes=100000):
        r, w, exc = select.select([self.proc], [], [], 0)
        if r:
            return os.read(self.proc, max_bytes)

    def read_all(self, delay=0.03):
        all_data = []
        while True:
            data = self.read()
            if data:
                all_data.append(data)
            else:
                break
            time.sleep(delay)
        return ''.join(all_data)

    def write(self, s):
        os.write(self.proc, s)

    def send_command(self, cmd):
        self.write(cmd + '\n')
Ich muss meine oben gemachte Aussage übrigens korrigieren. Es wird sozusagen für jedes Event ein Eintrag gemacht. Wenn ich einen Befehl eingegeben hab, wäre das Prompt+Befehl, Ausgabe des Befehl, Prompt. Wenn die Ausgabe mit einer Statusanzeige arbeitet, bekommt man immer den aktuellen Stand angezeigt (war zumindest beim Testen mit `wget` so). Also z.B. Text des Programms und am Ende Statusanzeige von 2%, beim nächsten Aufruf von `read()` nur die Statuszeile mit diesmal 5% usw bis zum Schluss wieder der Prompt kommt und danach `None`. Übrigens eine prima Möglichkeit, um Statusbalken von anderen Programmen (natürlich nur kommandozeilenbasierende) auf die eigene Anzeige zu übertragen, denke ich. :)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Gibt es etwas vergleichbares eigentlich für Windows? Ich kann in meinem Windows-Python leider kein `pty`-Modul importieren. Und auch die Doku sagt ja, dass es nicht für Windows verfügbar ist. Trotzdem muss es ja eine Möglichkeit geben. Ich habe jetzt schon mehrere Terminal-Emulatoren für Windows gesehen, die in C bzw C++ geschrieben sind. Leider bin ich aber durch den Code absolut nicht durchgestiegen. Ich will ja eigentlich nicht mal ein komplettes Terminal implementieren, sondern möchte für den Anfang nur einem Prozess gegenüber als Terminal auftreten und mit ihm kommunizieren können. Und das eben mindestens auch unter Windows.
Benutzeravatar
HWK
User
Beiträge: 1295
Registriert: Mittwoch 7. Juni 2006, 20:44

Vielleicht hilft Dir das weiter.
MfG
HWK
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Genial. Es klappt jetzt. Besten Dank. :)

http://paste.pocoo.org/show/106599/
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ganz so einfach ist es unter Windows wohl doch nicht. Ich habe vorher immer mit der `cmd.exe` getestet, nun aber mal den Pfad zu Python angegeben. Hier erhalte ich leider nur ein `None`, wenn ich's auslesen will. Unter Linux hingegen geht das ohne Probleme. Hat jemand nen Tipp, woran es liegen könnte?
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Noch ein Problem: Ich möchte, dass mir die `read()`-Methode der `Default`-Variante alle Events ausgibt. Eigentlich habe ich das ja auch schon mal mit `read_all()` gelöst, aber jetzt verstehe ich meinen Denkfehler einfach nicht. Es wird jedes Mal nur das allerletzte Event (also der Prompt) zurückgegeben. Hat da jemand nen Tipp? :)

http://paste.pocoo.org/show/106865/

(Der Docstring am Anfang ist übrigens so gewählt, weil ich noch angefangen habe, eine `Shell()`-Klasse zu implementieren, die ich aber nicht gepasted habe, weil so noch ganz am Anfang steht und für das genannte Problem noch nicht relevant ist.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Ich denke, du hast `all_output` und `output` verwechselt (Zeile 68f.).
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Baaaaaaaaaah, wie dumm! :D Ich such und such...
Besten Dank.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich hab jetzt eine minimale Gui gebaut. Problem ist nun, dass die ganzen Escape-Sequenzen mit angezeigt werden. Hier mal ein Beispiel:

Code: Alles auswählen

Fr Immeo nur bis 10h offen

]0;~~$ cd downloads
]0;~/downloads~/downloads$ ls *.gz
[00m[01;31marora-0.4.tar.gz[00m                      [01;31mpurple-plugin_pack-2.5.1.tar.gz[00m
[01;31mdillo_2.0.orig.tar.gz[00m                 [01;31mpython-xlib-0.14.tar.gz[00m
[01;31mfltk-2.0.x-r6483.tar.gz[00m               [01;31mqt-x11-commercial-src-4.5.0-tp1.tar.gz[00m
[01;31mfltk2_2.0.0~r5917.orig.tar.gz[00m         [01;31mRC_GUI.tar.gz[00m
[01;31minstall_flash_player_10_linux.tar.gz[00m  [01;31mTermEmulator-1.0.tar.gz[00m
[01;31mksh.2008-11-04.linux.i386.gz[00m          [01;31mterminal.0.1.0.1844.tar.gz[00m
[01;31mlogtimes.tar.gz[00m                       [01;31mvmware-any-any-update115.tar.gz[00m
[01;31mPDCurses-3.4.tar.gz[00m                   [01;31mVMware-server-1.0.7-108231.tar.gz[00m
[01;31mpexpect-2.3.tar.gz[00m                    [01;31mX11-WMCtrl-0.01.tar.gz[00m
[01;31mpidgin-guifications-2.16.tar.gz[00m
[m]0;~/downloads~/downloads$ cd ~
]0;~~$
Im Textfeld der Gui sieht man diese komischen Kästchen übrigens nicht. Ich möchte die Escape-Sequenzen nun erstmal rausfiltern. Kann mir jemand auf die Sprünge helfen, wie mein regulärer Ausdruck dafür aussehen müsste? Oder gibt es vielleicht bessere Möglichkeiten?

GUI (Qt) : http://paste.pocoo.org/show/107412/

pseudoterm.py: http://paste.pocoo.org/show/107413/
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Ich habe irgendwann einmal die folgende Regex zusammengestellt bzw. eher zusammenkopiert, ich kann mich allerdings nicht mehr daran erinnern, woher die ursprünglich kommt. Alle Escape-Sequenzen deckt sie jedenfalls nicht ab.

Code: Alles auswählen

ANSI_SEQUENCES = re.compile('\x1b(' + '|'.join([
    # Farben
    '\[[0-9;]*[m]',
    # Cursor Home
    '\[([0-9]+;[0-9]+)?H',
    # Cursor Forward
    '\[[0-9]+C',
    # Erase Down
    '\[J'
]) + ')')
Eine weitere Möglichkeit wäre vielleicht das ``ANSI``-Modul von pexpect. Da reicht es wohl, wenn man die `write_ch`-Methode der `ANSI`-Klasse überschreibt, allerdings habe ich den Code nur überflogen, kann also gut sein, dass ich mich da irre.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Vielen Dank. Das hilft mir sehr. :)

Ich habe das jetzt soweit gekürzt, dass nur die Farb-Sequenzen rausgenommen werden. Allerdings steht noch vor der Pfadangabe etwas "Kauderwelsch":

Code: Alles auswählen

]0;~~$ cd downloads
]0;~/downloads~/downloads$ 
Ich bekomme das irgendwie nicht weg. Kann mir bitte nochmal jemand helfen? :)

Code: Alles auswählen

class PseudoTerminal(ReadWriteProcess):
    COLOR_SEQUENCES = '\x1b(\[[0-9;]*[m])'

    def __init__(self, shell='', save_output=True):
        if not shell:
            if windows:
                shell = 'cmd'
            else:
                shell = os.environ['SHELL']
        ReadWriteProcess.__init__(self, shell)
        self.save_output = save_output
        if self.save_output:
            self.output = self.read()
        else:
            self.output = ''

    def send_command(self, cmd):
        self.write(cmd + '\n')
        if self.save_output:
            self.output += self.remove_color_seqs(self.read())

    def remove_color_seqs(self, s):
        return re.sub(self.COLOR_SEQUENCES, '', s)
fred.reichbier
User
Beiträge: 155
Registriert: Freitag 29. Dezember 2006, 18:27

Hallo,

ich hab mal versucht, ein Terminalemulatormodul zu schreiben und hab daher auch einen Parser für diese Sequenzen geschrieben, den gibts hier. Die `convert`-Funktion könntest du ohne größeren Aufwand so umschreiben, dass sie nicht eine Liste von Instruktionen zurückgibt (wie bis jetzt ;)), sondern einfach die Sequenzen rausschmeißt und den restlichen Text zurückgibt.

Die Sequenzen mit ] sind übrigens Zeug für den xterm. Die in deinem Beispiel sollten wohl den Titel des Fensters setzen. Wenn du deine Regex damit erweitern willst, müsstest du wohl den Text zwischen [ und \x07 ignorieren (wenn das am Schluss wirklich \x07 ist).

Hoffe, das hilft dir weiter.

Gruß,

Fred
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Du solltest den Programmen, die du mit `os.execv` startest, außerdem noch ein ``argv[0]`` spendieren. Sprich `xfile` sollte zu `args` dazu.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

So, ich hab mal wieder ein bißchen was gemacht. Die Regex zum Filtern der angezeigten Sequenzen (zumindest sehe ich keine mehr) lautet:

Code: Alles auswählen

'\x1b(\[[0-9;]*[m]|]0;.*\x07)'
Meine nächste Frage: Gibt es eine Möglichkeit, `QTextEdit()` zu sagen, dass er nicht eine bestimme Cursorposition über-/bzw unterschreiten darf? Das heißt man gäbe ihm im Idealfall die aktuelle Cursorposition und könnte ihn alle Versuche, vor diese Position zu springen, blocken lassen. Das Problem ist nämlich, dass der Benutzer, da er sich in einem beschreibbaren Textfeld befindet, auch die vorhandene Ausgabe löschen könnte. Dann aber würde ich das nächste Kommando falsch interpretieren (siehe dazu auch die Kommentare im Quelltext). Ich versuche gerade, das selbst zu implementieren, aber vielleicht geht es ja einfacher.
Trundle hat geschrieben:Du solltest den Programmen, die du mit `os.execv` startest, außerdem noch ein ``argv[0]`` spendieren. Sprich `xfile` sollte zu `args` dazu.
Aus welchem Grund? Der Aufruf funktioniert doch und `argv[0]` wäre das eigentliche Skript. Ich will aber ja einen zusätzlichen Prozess da reinladen. Oder meinst du was anderes?

pseudoterm.py

gui_term.py
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

snafu hat geschrieben:
Trundle hat geschrieben:Du solltest den Programmen, die du mit `os.execv` startest, außerdem noch ein ``argv[0]`` spendieren. Sprich `xfile` sollte zu `args` dazu.
Aus welchem Grund? Der Aufruf funktioniert doch und `argv[0]` wäre das eigentliche Skript. Ich will aber ja einen zusätzlichen Prozess da reinladen. Oder meinst du was anderes?
Das, was du `os.execv()` als zweiten Parameter übergibst, wird dann das ``argv`` vom neuen Prozess. In der Regel steht in `argv[0]` nun einmal der Name des Programmes, was bei den Prozessen, die mit ``DefaultReadWriteProcess`` gestartet werden, jedoch nicht der Fall ist. Die zsh mag das z.B. nicht und segfaultet.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Nur dass wir nicht aneinander vorbeireden, meinst du so?

Code: Alles auswählen

os.execv(sys.argv[0], [xfile] + [arg for arg in args])
Damit erhalte ich wie gesagt die Ausgabe der Python-Shell (wenn ich's aus dem Interpreter aufrufe). Der Trick ist ja gerade, einen anderen Prozess mitzugeben. Der Segfault (ich nehme an, du meinst diesen Input/Output-Error) muss also IMHO anders umgangen werden.

EDIT: Jetzt weiß ich glaub ich was du meinst:

Code: Alles auswählen

os.execv(xfile, [xfile] + [arg for arg in args])
Das Problem ist aber leider, dass nun jedes Mal die Ausgabe der gestarteten Shell mit angezeigt wird. Weißt du dafür vielleicht auch eine Lösung? :)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Ja, letzteres war gemeint. Und das mit der Ausgabe der Shell verstehe ich nicht ganz. Was genau wird da angezeigt und soll nicht angezeigt werden?
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Wenn ich mit der geänderten Variante bspw die `zsh` als Prozess nehme, also:

Code: Alles auswählen

ReadWriteProcess('/usr/bin/zsh')
...dann wird im Interpreter sofort die Ausgabe der `zsh` angezeigt. Ich kann zwar in den Prozess schreiben, aber es wird eben jedes Mal auch die Ausgabe angezeigt. Vorher war es so, dass ich etwas in den Prozess schreiben konnte und erst nach Aufruf von `read()` die Ausgabe bekam. Genau dies möchte ich beibehalten.
Antworten