From 42e9ac4b3e8f34b054e45bfb61c3d6e4153be8b1 Mon Sep 17 00:00:00 2001 From: Zac Medico Date: Thu, 7 Jun 2007 12:02:59 +0000 Subject: When using a pty for logging, use setsid() to create a new session and make the pty into the controlling terminal of the new session. This makes interactive ebuild behave properly in interactive cases like check_license() where ${PAGER:-less} is invoked. svn path=/main/trunk/; revision=6747 --- pym/portage/__init__.py | 51 +++++++++++++++++++++++++++++++++++++++++-------- pym/portage/output.py | 9 +++++++++ pym/portage/process.py | 23 +++++++++++++++++++--- 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/pym/portage/__init__.py b/pym/portage/__init__.py index c931ed32a..b6fbf2cad 100644 --- a/pym/portage/__init__.py +++ b/pym/portage/__init__.py @@ -2290,6 +2290,8 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, **keyw slave_fd = None output_pid = None input_pid = None + stdin_termios = None + stdin_fd = None if logfile: del keywords["logfile"] fd_pipes = keywords.get("fd_pipes") @@ -2299,12 +2301,41 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, **keyw raise ValueError(fd_pipes) from pty import openpty master_fd, slave_fd = openpty() - # Disable the ECHO attribute so the terminal behaves properly - # if the subprocess needs to read input from stdin. - import termios - term_attr = termios.tcgetattr(slave_fd) - term_attr[3] &= ~termios.ECHO - termios.tcsetattr(slave_fd, termios.TCSAFLUSH, term_attr) + fd_pipes.setdefault(0, sys.stdin.fileno()) + stdin_fd = fd_pipes[0] + if os.isatty(stdin_fd): + # Copy the termios attributes from stdin_fd to the slave_fd and put + # the stdin_fd into raw mode with ECHO disabled. The stdin + # termios attributes are reverted before returning, or via the + # atexit hook in portage.process when killed by a signal. + import termios, tty + stdin_termios = termios.tcgetattr(stdin_fd) + tty.setraw(stdin_fd) + term_attr = termios.tcgetattr(stdin_fd) + term_attr[3] &= ~termios.ECHO + termios.tcsetattr(stdin_fd, termios.TCSAFLUSH, term_attr) + termios.tcsetattr(slave_fd, termios.TCSAFLUSH, stdin_termios) + from output import get_term_size, set_term_size + rows, columns = get_term_size() + set_term_size(rows, columns, slave_fd) + pre_exec = keywords.get("pre_exec") + def setup_ctty(): + os.setsid() + # Make it into the "controlling terminal". + import termios + if hasattr(termios, "TIOCSCTTY"): + # BSD 4.3 approach + import fcntl + fcntl.ioctl(0, termios.TIOCSCTTY) + else: + # SVR4 approach + fd = os.open(os.ttyname(0), os.O_RDWR) + for x in 0, 1, 2: + os.dup2(fd, x) + os.close(fd) + if pre_exec: + pre_exec() + keywords["pre_exec"] = setup_ctty # tee will always exit with an IO error, so ignore it's stderr. null_file = open('/dev/null', 'w') mypids.extend(portage.process.spawn(['tee', '-i', '-a', logfile], @@ -2374,8 +2405,12 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, **keyw os.waitpid(input_pid, 0) portage.process.spawned_pids.remove(input_pid) pid = mypids[-1] - retval = os.waitpid(pid, 0)[1] - portage.process.spawned_pids.remove(pid) + try: + retval = os.waitpid(pid, 0)[1] + portage.process.spawned_pids.remove(pid) + finally: + if stdin_termios: + termios.tcsetattr(stdin_fd, termios.TCSAFLUSH, stdin_termios) if retval != os.EX_OK: if retval & 0xff: return (retval & 0xff) << 8 diff --git a/pym/portage/output.py b/pym/portage/output.py index f5e261c82..b4877b30d 100644 --- a/pym/portage/output.py +++ b/pym/portage/output.py @@ -260,6 +260,15 @@ def get_term_size(): pass return -1, -1 +def set_term_size(lines, columns, fd): + """ + Set the number of lines and columns for the tty that is connected to fd. + For portability, this simply calls `stty rows $lines columns $columns`. + """ + from portage.process import spawn + cmd = ["stty", "rows", str(lines), "columns", str(columns)] + spawn(cmd, env=os.environ, fd_pipes={0:fd}) + class EOutput: """ Performs fancy terminal formatting for status and informational messages. diff --git a/pym/portage/process.py b/pym/portage/process.py index dfc106e6b..0cb175375 100644 --- a/pym/portage/process.py +++ b/pym/portage/process.py @@ -111,9 +111,19 @@ def cleanup(): atexit_register(cleanup) +# Make sure the original terminal attributes are reverted at exit. +if sys.stdin.isatty(): + import termios + _stdin_termios = termios.tcgetattr(sys.stdin.fileno()) + def _reset_stdin_termios(stdin_termios): + import termios + termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, stdin_termios) + atexit_register(_reset_stdin_termios, _stdin_termios) + del termios, _stdin_termios, _reset_stdin_termios + def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, uid=None, gid=None, groups=None, umask=None, logfile=None, - path_lookup=True): + path_lookup=True, pre_exec=None): """ Spawns a given command. @@ -140,6 +150,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, @type logfile: String @param path_lookup: If the binary is not fully specified then look for it in PATH @type path_lookup: Boolean + @param pre_exec: A function to be called with no arguments just prior to the exec call. + @type pre_exec: callable logfile requires stdout and stderr to be assigned to this process (ie not pointed somewhere else.) @@ -194,7 +206,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, if not pid: try: _exec(binary, mycommand, opt_name, fd_pipes, - env, gid, groups, uid, umask) + env, gid, groups, uid, umask, pre_exec) except Exception, e: # We need to catch _any_ exception so that it doesn't # propogate out of this function and cause exiting @@ -251,7 +263,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, # Everything succeeded return 0 -def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask): +def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, + pre_exec): """ Execute a given binary with options @@ -274,6 +287,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask): @type uid: Integer @param umask: an int representing a unix umask (see man chmod for umask details) @type umask: Integer + @param pre_exec: A function to be called with no arguments just prior to the exec call. + @type pre_exec: callable @rtype: None @returns: Never returns (calls os.execve) """ @@ -315,6 +330,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask): os.setuid(uid) if umask: os.umask(umask) + if pre_exec: + pre_exec() # And switch to the new process. os.execve(binary, myargs, env) -- cgit v1.2.3-1-g7c22