2017-01-09 3 views
6

Ich muss eine interaktive Bash-Instanz in einem separaten Prozess in Python mit eigenen dedizierten TTY (ich kann nicht pexpect verwenden). ich diesen Code-Snippet verwendete ich in ähnlichen Programmen häufig gebraucht:Interaktive Bash mit Popen und eine dedizierte TTY Python

master, slave = pty.openpty() 

p = subprocess.Popen(["/bin/bash", "-i"], stdin=slave, stdout=slave, stderr=slave) 

os.close(slave) 

x = os.read(master, 1026) 

print x 

subprocess.Popen.kill(p) 
os.close(master) 

Aber wenn ich es laufen lasse, bekomme ich die folgende Ausgabe:

$ ./pty_try.py 
bash: cannot set terminal process group (10790): Inappropriate ioctl for device 
bash: no job control in this shell 

Strace des Laufs zeigt einige Fehler:

... 
readlink("/usr/bin/python2.7", 0x7ffc8db02510, 4096) = -1 EINVAL (Invalid argument) 
... 
ioctl(3, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffc8db03590) = -1 ENOTTY (Inappropriate ioctl for device) 
... 
readlink("./pty_try.py", 0x7ffc8db00610, 4096) = -1 EINVAL (Invalid argument) 

Das Code-Snippet scheint ziemlich einfach, ist Bash nicht etwas, was es braucht? Was könnte das Problem hier sein?

+2

Das ist ganz normal - Sie haben eine ** interaktive Shell ** ohne ** Jobkontrolle **. –

+1

Wenn Sie auch die Jobsteuerung wünschen, müssen Sie Ihre Shell zum Prozessleader machen - das ist die neue "Sitzung", sie wird mit 'start_new_session = True' Schlüsselwortargument zu' Popen' (seit Python 3.2) erreicht. Wenn Sie mehr Kontrolle brauchen, verwenden Sie 'preexec_fn = ...' –

+0

Ok, das klingt vernünftig. Ich verstehe, dass die 'start_new_session = True 'nur für> 3.2 relevant ist. Gibt es eine Entsprechung in 2.7? Tut mir leid, sollte wahrscheinlich die Python-Version in der Frage erwähnt haben. – TKKS

Antwort

2

Dies ist die Lösung, die für mich am Ende gearbeitet (wie von Qarma vorgeschlagen):

libc = ctypes.CDLL('libc.so.6') 

master, slave = pty.openpty() 
p = subprocess.Popen(["/bin/bash", "-i"], preexec_fn=libc.setsid, stdin=slave, stdout=slave, stderr=slave) 
os.close(slave) 

... do stuff here ... 

x = os.read(master, 1026) 
print x 
+1

Ich bezweifle, du brauchst '-i' mehr mit all den anderen Dingen, bash sollte seine Umgebung erkennen. –

-3

müssen Sie Eingabe übergeben, um zu lesen und dann x zu drucken.

+0

Also besteht Ihre Antwort aus einem Satz, den ich nicht entziffern kann, und Sie erwarten, dass jeder dankbar ist? :) –

4

Dies ist eine Lösung, die einen interaktiven Befehl in subprocess auszuführen. Es benutzt Pseudo-Terminal, um stdout nicht blockierend zu machen (auch ein Befehl benötigt ein tty-Gerät, zB bash). Es verwendet Select, um die Eingabe und die Ausgabe an den Subprozess zu verarbeiten.

#!/usr/bin/env python 
# -*- coding: utf-8 -*- 

import os 
import sys 
import select 
import termios 
import tty 
import pty 
from subprocess import Popen 

command = 'bash' 
# command = 'docker run -it --rm centos /bin/bash'.split() 

# save original tty setting then set it to raw mode 
old_tty = termios.tcgetattr(sys.stdin) 
tty.setraw(sys.stdin.fileno()) 

# open pseudo-terminal to interact with subprocess 
master_fd, slave_fd = pty.openpty() 

# use os.setsid() make it run in a new process group, or bash job control will not be enabled 
p = Popen(command, 
      preexec_fn=os.setsid, 
      stdin=slave_fd, 
      stdout=slave_fd, 
      stderr=slave_fd, 
      universal_newlines=True) 

while p.poll() is None: 
    r, w, e = select.select([sys.stdin, master_fd], [], []) 
    if sys.stdin in r: 
     d = os.read(sys.stdin.fileno(), 10240) 
     os.write(master_fd, d) 
    elif master_fd in r: 
     o = os.read(master_fd, 10240) 
     if o: 
      os.write(sys.stdout.fileno(), o) 

# restore tty settings back 
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)