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

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

Vielleicht solltest du mal den genauen Code zeigen, bei dem das auftritt. Weil irgendwie komme ich mit meinem Vorstellungsvermögen gerade nicht weiter.
"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

Ach, keine Ahnung. Jetzt klappt es nicht mal mehr mit der Bash, selbst wenn ich den Code von davor benutze. Vielleicht ja irgendwas systeminternes. Ich hatte nämlich einige Zeit das Problem, dass in allen Terminals (also generell, nicht nur das von mir) die Umlaute nicht richtig angezeigt wurden. Neuerdings geht das auf einmal wieder. Ich vermute, dass irgendein Update (Debian) da was verändert hat. Bin jedenfalls gerade etwas genervt, weil ich wirklich denke, dass es nicht an meinem Code liegen kann.

Aber hier mal der aktuelle Code mit der älteren execv-Variante: http://paste.pocoo.org/show/108845/

Ergebnis:

Code: Alles auswählen

In [1]: import pseudoterm

In [2]: pseudoterm.PseudoTerminal()
Mama + Papa: Do 14h (einkaufen)

Anmeldefrist für WM-Module endet am 19.03.!!!
http://www.ruhr-uni-bochum.de/philosophy/lectures/ss09.pdf

~$ 
Out[2]: <pseudoterm.PseudoTerminal object at 0x923cdac>
Und diese direkte Ausgabe hatte ich früher nie.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Naja, in `get_output()` steht ``print output``, von daher ist es nicht unbedingt verwunderlich, dass etwas ausgegeben wird.

Und du solltest in `get_output()` außerdem berücksichtigen, dass deine `read()`-Methode auch `None` zurückgeben kann.
"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

Es gibt für fast jedes Mysterium eine ganz einfache Erklärung. ;P Es klappt jetzt auch mit der `zsh`. Danke. :)

Eine unschöne Sache gibt es aber noch:

Code: Alles auswählen

In [9]: test = pseudoterm.PseudoTerminal(shell='test.py')

In [10]: print test.output
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)

/home/sebastian/<ipython console> in <module>()

/home/sebastian/pseudoterm.py in __init__(self, shell, save_output)
    140         if not shell:
    141             shell = 'cmd' if windows else os.environ['SHELL']
--> 142         ReadWriteProcess.__init__(self, shell)
    143         self.save_output = save_output
    144         self.output = self.get_output() if save_output else ''

/home/sebastian/pseudoterm.py in __init__(self, xfile, *args)
     41         pid, self.terminal = pty.fork()
     42         if pid == 0:
---> 43             os.execv(xfile, [xfile] + [arg for arg in args])
     44 
     45 

OSError: [Errno 8] Exec format error
`test.py` ist hierbei eine ausführbare Datei mit Python-Code (besteht also meinen Test am Anfang der ReadWriteProcess-Klasse). Der Traceback wird dann *in* das erzeugte `test`-Objekt geschrieben. Es wird also auch keine Exception ausgelöst. Weißt du was man da machen könnte?
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Das ist klar. Da wird schon eine Ausnahme geworfen, nur eben im Kindprozess, und davon bekommt der Elternprozess ja nichts mit (außer eben, dass der Traceback in ``output`` landet).

Mit Pipes könnte man da etwas machen. Dazu erstellt man einfach eine Pipe (mit `os.pipe()`) und erhält dann ein Ende zum Lesen, eines zum Schreiben. `os.fork()` dupliziert die Enden. Der Kindprozess schreibt in das beschreibbare Ende, der Elternprozess liest das lesbare Ende. Mit dem `execv()`-Aufruf wird dann das beschreibbare Ende geschlossen und der `os.read()`-Aufruf des Elternprozesses kehrt zurück. Der Kindprozess umschließt den `execv()`-Aufruf einfach mit ``try: ... except: ...`` und schreibt dann in sein Ende der Pipe, dass etwas fehlgeschlagen ist. Folglich bekommt der Elternprozess entweder einen leeren String zurück (kein Fehler), oder eben einen String mit einem Wert. Wenn man lustig ist, könnte man so z.B. eine gepickelte Ausnahme rumschicken.

Also könnte das alles in allem so irgendwie aussehen:

Code: Alles auswählen

import os
import pickle
import pty
import sys


class DefaultReadWriteProcess(object):
    # ...
    def __init__(self, xfile, *args):
        # ...

        # Pipe for communication with parent
        err_r, err_w = os.pipe()

        pid, self.terminal = pty.fork()
        if pid == 0:
            # Child
            # Close parent's end of the pipe
            os.close(err_r)
            try:
                os.execv(xfile, [xfile] + list(args))
            except BaseException as exc_value:
                os.write(err_w, pickle.dumps(exc_value))
            # Nobody should ever see the return code
            os._exit(255)
        # Parent
        # Close child's end of the pipe
        os.close(err_w)
        # Read (1 MiB) whether an exception occurred or not
        data = os.read(err_r, 2**20)
        if data:
            # Wait for child to terminate
            os.waitpid(pid, 0)
            # Raise child's exception
            raise pickle.loads(data)
Das sieht jetzt alles komplizierter aus, als es letztlich ist.
"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

Okay. Es funktioniert jetzt. Firma dankt. :)

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

Warum eigentlich 255 als Exitcode? Der bedeutet doch eigentlich sowas wie "Datei nicht gefunden", oder? So gesehen war das Beenden aber IMHO erfolgreich, daher doch im Grunde 0.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

IMHO funktioniert der Code nicht. Es ist eine Race Condition drin: Wenn vom Elternprozess überprüft wird, ob Daten vorliegen, muss das Kind noch nichts gesendet haben, was aber nicht heißt, dass es prinzipiell nichts sendet. Um genau zu sein, muss `select()` auf jeden Fall zurückgeben, dass von der Pipe gelesen werden kann, denn die das eine Pipe-Ende wird durch den `execv()`-Aufruf geschlossen. Also völlig unabhängig davon, ob das Kind etwas sendet oder nicht, muss ``self.hascontent(parents_end)`` wahr sein. Das hat zur Folge, dass `exc` eventuell ein leerer String ist, was wiederum einen `EOFError` beim Unpickeln wirft. Ist `exc` kein leerer String, ist es eine gepickelte Ausnahme. Sollte der Code also ohne Ausnahme durchlaufen, ist das lediglich ein Beweis für die Existenz der Race-Condition.

Und zum Exitcode: Es sollte niemals die Stelle erreicht werden, wo der zurückgegeben wird. Die Stelle wird nur erreicht, wenn `execv()` fehlgeschlagen ist, was ich jetzt aber wirklich nicht gerade als Erfolg werten würde. Abgesehen davon ist das ein Kindprozess, und der Elternprozess ist am Exit-Code nicht interessiert, und jemand anderes sieht ihn schlicht und ergreifend nicht.
"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

Okay. Das mit dem Exitcode leuchtet mir nun ein, der Absatz davor jedoch nicht. In der Pipe kann doch nur etwas drin sein, wenn `pickle` reinschreibt. Pickle schreibt aber erst im Falle einer Exception in die Pipe. Du sagst selber, dass die Stelle nach `os.execv()` gar nicht erst erreicht wird, wenn die Ausführung erfolgreich ist. Wenn man so überlegt, müsste daher doch ohnehin keine Prüfung auf `content` stattfinden, oder nicht? Könnte der Teil nicht sogar eigentlich ganz weg? Also, dass die Bedingung für den if-Block rausgenommen und der Fehler in jedem Fall geschmissen wird, da man davon ausgehen kann, dass diese Stelle nur im Falle einer Exception erreicht werden kann?

Gerade getestet. Letzteres geht nicht. Ich glaube, ich habe auch noch nicht ganz verstanden wie das mit der inneren Schleife genau ist. Das Skript scheint ja weiterzulaufen, aber die innere Schleife wird dabei trotzdem nicht verlassen(?)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Welche Bedingung im if? Das Kind hat doch gar keine if-Anweisung. Und du solltest dir einmal klar machen, wer hier was ausführt. Du forkst zunächst einmal. Das Kind führt dann den if-Block mit der Bedingung ``pid == 0`` aus. *Gleichzeitig* läuft aber der Elternprozess weiter, der aber nicht in den if-Block springt. Im Elternprozess überprüfst du jetzt, ob man vom einen Ende der Pipe lesen kann. Wohlgemerkt: ob man davon lesen kann. Wenn `select()` dir etwas zurückgibt, heißt das keinesfalls, dass auch Daten vorliegen, sondern lediglich, dass man von dem FD lesen kann. Zu dem Zeitpunkt muss aber das Kind noch nicht einmal zwingend beim `execv()`-Aufruf angekommen sein, wovon der Elternprozess jedoch ausgeht, eben indem er schaut, ob man von der Pipe lesen kann. Und das ist die Race Condition.

Jetzt habe ich eben geschrieben, dass `select()` dir zurückgibt, ob man von der Pipe lesen kann, nicht aber, ob Daten vorliegen. Liest man jetzt von der Pipe, gibt es zwei Möglichkeiten: Man bekommt Daten zurück, oder eben einen Bytestring der Länge Null. Letzteres bedeutet, dass das andere Ende der Pipe (also das, in das der Kindpozess schreibt), geschlossen wurde. Das andere Ende der Pipe wird aber auf jeden Fall geschlossen: Entweder durch `execv()` oder eben dadurch, dass der Prozess durch das `os._exit()` beendet wird. Folglich müsste, nachdem das Kind seinen `execv()`-Aufruf ausgeführt hat, `select()` in jedem Fall zurückgeben, dass vom lesbaren Ende der Pipe gelesen werden kann. Und genau deshalb steht bei mir einfach ein `read()` und es wird danach überprüft, ob tatsächlich Daten gelesen wurden, was einen Fehler bedeuten würde, oder nicht. Und die Race Condition ist damit auch weg.
"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

Genau das funktioniert aber IMHO nicht. Bei einem "gültigen" Dateinamen bleibt der Ablauf bei `os.read()` stecken, da er auf Daten zum Lesen wartet:

Code: Alles auswählen

In [1]: import pseudoterm

In [2]: pseudoterm.ReadWriteProcess('/bin/bash')

^C---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)

/home/sebastian/<ipython console> in <module>()

/home/sebastian/pseudoterm.py in __init__(self, xfile, *args)
     57             os._exit(255)
     58         os.close(childs_end)
---> 59         data = os.read(parents_end, 2**20)
     60         if data:
     61             os.waitpid(pid, 0)

KeyboardInterrupt: 

In [3]: pseudoterm.ReadWriteProcess('/bin/bas')
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)

/home/sebastian/<ipython console> in <module>()

/home/sebastian/pseudoterm.py in __init__(self, xfile, *args)
     60         if data:
     61             os.waitpid(pid, 0)
---> 62             raise pickle.loads(data)
     63 
     64 

OSError: [Errno 2] No such file or directory

Code: Alles auswählen

def __init__(self, xfile, *args):
        [...]

        parents_end, childs_end = os.pipe()
        pid, self.terminal = pty.fork()
        if pid == 0:
            os.close(parents_end)
            try:
                os.execv(xfile, [xfile] + list(args))
            except BaseException:
                exc = sys.exc_info()[1]
                os.write(childs_end, pickle.dumps(exc))
            os._exit(255)
        os.close(childs_end)
        data = os.read(parents_end, 2**20)
        if data:
            os.waitpid(pid, 0)
            raise pickle.loads(data)
Trundle hat geschrieben:Welche Bedingung im if?
Ich meinte die Bedingung im Elternprozess, ob Daten vorhanden sind. Das hat sich aber nach deinen Erklärungen erledigt, weil ich jetzt verstanden habe, dass der Elternprozess einfach weiterläuft und der `if pid == 0`-Block nur für den geforkten Prozess gilt.

Also ich verstehe schon, dass es etwas heikel ist, aus der Lesbarkeit der Pipe zu schliessen, dass der Kindprozess einen Fehler geworfen haben muss. Mir fällt aber keine bessere Umsetzung dafür ein. Man könnte IMHO höchstens wieder ein `delay` einbauen, um dem Kind etwas Zeit zu geben. Aber bisher funktioniert es in der Praxis auch ohne Delay.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

D'oh. Man sollte dem Schreibende der Pipe natürlich noch sagen, dass es bei einem `execv()` geschlossen werden sollte.

Code: Alles auswählen

from fcntl import fcntl, FD_CLOEXEC, F_GETFD, F_SETFD

# Das gleich nach ``parents_end, childs_end = os.pipe()``
fcntl(childs_end, F_SETFD, fcntl(childs_end, F_GETFD) | FD_CLOEXEC)
Jetzt sollte es aber funktionieren. Und das mit der Lesbarkeit überprüfen ist nicht heikel, das ist zum Scheitern verurteilt, IMHO.
"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

Das funktioniert jetzt. Verstehe ich das richtig:

- Erzeuge die Pipe und setze danach die Flags des Pipeendes `childs_end`, welches ja ein Filedescriptor ist, neu
- Verwende hierfür die bisherigen Flags + das neue Flag `FD_CLOEXEC`, welches den fd schliessen soll, wenn eine exec-Operation *erfolgreich* aufgerufen wurde
- Bei erfolgreichem `exec` wird also die Pipe geschlossen und hat nur einen leereren String im nun lesbaren Pipeende des Kindes
- Bei Misserfolg schreibt meine `os.read()`-Anweisung die Exception rein
- `os._exit()` würde dann das Kind schliessen und wird auch nur dann erreicht, wenn `os.execv()` abbrechen musste
- Jetzt schliesst der Elternprozess das Pipeende des Kindes: Warum eigentlich? Sollte das nicht besser im Block des Kindes kurz vor `os._exit()` stehen? Denn im Erfolgsfall hat man doch das Flag, das sich um's Schliessen kümmert. Außerdem könnte der Elternprozess dem Kind doch dann die Pipe vor der Nase zumachen, bevor dieses etwas reinschreiben kann, oder nicht?
- Danach wird jedenfalls geprüft, ob Daten da sind. Ich nehme an, `os.read()` wartet ab, bis ein leerer String kommt oder eben die Exception
- Wenn der String befüllt ist, wartet man auf noch die Beendigung des Kindes (das ja dann einen Fehler gehabt haben muss), weil man - so denke ich mal - sicher sein will, dass keine Daten mehr nachkommen und holt sich schließlich die Daten mit `pickle`.

Ich bohre deshalb so nach, weil ich den Ablauf meines Programms halt schon verstehen will. ;)
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Ich nehme an, du hast dich verschrieben und meintest natürlich `os.write()` anstatt `os.read()`.

Wichtig ist, dass du dir klar machst, was beim Fork passiert: Danach hast du zwei identische Prozesse. Das heißt, die haben auch dieselben Dateideskriptoren. Ergo hat deine Pipe vier Dateideskriptoren: Zwei zum Lesen (eine im Elternprozess, eine im Kindprozess), zwei zum Schreiben (dito). Dass die Prozesse also das Ende schließen, das sie nicht benutzen, hat also schon seine Richtigkeit.

Darauf zu warten, dass das Kind beendet ist, darauf kann wohl auch verzichtet werden. In eine Pipe kann eh nicht endlos geschrieben werden, wenn man nicht zwischendurch davon liest, also kann das in dem Fall hier sogar dazu führen, dass beide Prozesse niemals fortfahren (wenn die gepickelte Ausnahme größer als 1 MiB sein sollte). Auf jeden Fall sollte das lesbare Ende der Pipe im Elternprozess nach dem Lesen geschlossen werden, was ich in meinem Code leider vergessen habe (aus mehreren Gründen sollte das geschehen: damit keine Dateideskriptoren leaken und damit der Kindprozess nicht endlos hängt, weil er unter Umständen noch etwas in die Pipe schreiben mag, aber niemand mehr davon liest).
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Antworten