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: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Dienstag 3. März 2009, 15:08

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')
shcol (Repo | Doc | PyPi)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Dienstag 3. März 2009, 16:03

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: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Dienstag 3. März 2009, 19:13

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. :)
shcol (Repo | Doc | PyPi)
Benutzeravatar
snafu
User
Beiträge: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mittwoch 4. März 2009, 01:03

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.
shcol (Repo | Doc | PyPi)
Benutzeravatar
HWK
User
Beiträge: 1295
Registriert: Mittwoch 7. Juni 2006, 20:44

Mittwoch 4. März 2009, 09:26

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

Donnerstag 5. März 2009, 16:42

Genial. Es klappt jetzt. Besten Dank. :)

http://paste.pocoo.org/show/106599/
shcol (Repo | Doc | PyPi)
Benutzeravatar
snafu
User
Beiträge: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Donnerstag 5. März 2009, 19:18

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?
shcol (Repo | Doc | PyPi)
Benutzeravatar
snafu
User
Beiträge: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 7. März 2009, 19:26

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.
shcol (Repo | Doc | PyPi)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Samstag 7. März 2009, 19:32

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: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 7. März 2009, 20:04

Baaaaaaaaaah, wie dumm! :D Ich such und such...
Besten Dank.
shcol (Repo | Doc | PyPi)
Benutzeravatar
snafu
User
Beiträge: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Mittwoch 11. März 2009, 15:53

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/
shcol (Repo | Doc | PyPi)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Donnerstag 12. März 2009, 17:07

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: 5651
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Donnerstag 12. März 2009, 18:14

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)
shcol (Repo | Doc | PyPi)
fred.reichbier
User
Beiträge: 155
Registriert: Freitag 29. Dezember 2006, 18:27

Donnerstag 12. März 2009, 19:54

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

Donnerstag 12. März 2009, 21:18

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)
Antworten