diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index 40eea606b..91c7be653 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -27,7 +27,7 @@ from xonsh.inspectors import Inspector from xonsh.aliases import Aliases, make_default_aliases from xonsh.environ import Env, default_env, locate_binary from xonsh.foreign_shells import load_foreign_aliases -from xonsh.jobs import add_job +from xonsh.jobs import add_job, give_terminal_to from xonsh.platform import ON_POSIX, ON_WINDOWS from xonsh.proc import ( PopenThread, ProcProxyThread, ProcProxy, ConsoleParallelReader, @@ -496,6 +496,8 @@ class SubprocSpec: self.prep_env(kwargs) self.prep_preexec_fn(kwargs, pipeline_group=pipeline_group) if callable(self.alias): + if 'preexec_fn' in kwargs: + kwargs.pop('preexec_fn') p = self.cls(self.alias, self.cmd, **kwargs) else: p = self._run_binary(kwargs) @@ -533,7 +535,7 @@ class SubprocSpec: def prep_preexec_fn(self, kwargs, pipeline_group=None): """Prepares the 'preexec_fn' keyword argument""" - if not (ON_POSIX and self.cls is subprocess.Popen): + if not ON_POSIX: return if not builtins.__xonsh_env__.get('XONSH_INTERACTIVE'): return @@ -778,6 +780,17 @@ def _should_set_title(captured=False): hasattr(builtins, '__xonsh_shell__')) +def update_fg_process_group(pipeline_group, background): + if background: + return False + if not ON_POSIX: + return False + env = builtins.__xonsh_env__ + if not env.get('XONSH_INTERACTIVE'): + return False + return give_terminal_to(pipeline_group) + + def run_subproc(cmds, captured=False): """Runs a subprocess, in its many forms. This takes a list of 'commands,' which may be a list of command line arguments or a string, representing @@ -793,34 +806,37 @@ def run_subproc(cmds, captured=False): """ specs = cmds_to_specs(cmds, captured=captured) captured = specs[-1].captured + background = specs[-1].background procs = [] proc = pipeline_group = None + term_pgid = None for spec in specs: starttime = time.time() proc = spec.run(pipeline_group=pipeline_group) - procs.append(proc) - if ON_POSIX and pipeline_group is None and \ - spec.cls is subprocess.Popen: + if captured != 'object' and proc.pid and pipeline_group is None: pipeline_group = proc.pid - if not spec.is_proxy: + if update_fg_process_group(pipeline_group, background): + term_pgid = pipeline_group + procs.append(proc) + if not all(x.is_proxy for x in specs): add_job({ 'cmds': cmds, 'pids': [i.pid for i in procs], 'obj': proc, - 'bg': spec.background, + 'bg': background, }) if _should_set_title(captured=captured): # set title here to get currently executing command pause_call_resume(proc, builtins.__xonsh_shell__.settitle) # create command or return if backgrounding. - if spec.background: + if background: return if captured == 'hiddenobject': command = HiddenCommandPipeline(specs, procs, starttime=starttime, - captured=captured) + captured=captured, term_pgid=term_pgid) else: command = CommandPipeline(specs, procs, starttime=starttime, - captured=captured) + captured=captured, term_pgid=term_pgid) # now figure out what we should return. if captured == 'stdout': command.end() diff --git a/xonsh/jobs.py b/xonsh/jobs.py index 54ce56b17..1610086eb 100644 --- a/xonsh/jobs.py +++ b/xonsh/jobs.py @@ -28,7 +28,10 @@ if ON_DARWIN: for pid in job['pids']: if pid is None: # the pid of an aliased proc is None continue - os.kill(pid, signal) + try: + os.kill(pid, signal) + except ProcessLookupError: + pass elif ON_WINDOWS: pass elif ON_CYGWIN: @@ -70,6 +73,9 @@ if ON_WINDOWS: def _set_pgrp(info): pass + def give_terminal_to(pgid): + pass + def wait_for_active_job(last_task=None, backgrounded=False): """ Wait for the active job to finish, to be killed by SIGINT, or to be @@ -102,15 +108,15 @@ else: signal.signal(signal.SIGTSTP, signal.SIG_IGN) def _set_pgrp(info): - pid = info['pids'][0] - if pid is None: - # occurs if first process is an alias - info['pgrp'] = None - return - try: - info['pgrp'] = os.getpgid(pid) - except ProcessLookupError: - info['pgrp'] = None + for pid in info['pids']: + if pid is None: # occurs if first process is an alias + continue + try: + info['pgrp'] = os.getpgid(pid) + return + except ProcessLookupError: + continue + info['pgrp'] = None _shell_pgrp = os.getpgrp() @@ -123,21 +129,21 @@ else: def _shell_tty(): try: _st = sys.stderr.fileno() - if os.tcgetpgrp(_st) != os.getpgid(os.getpid()): + if os.tcgetpgrp(_st) != os.getpgid(0): # we don't own it _st = None except OSError: _st = None return _st - # _give_terminal_to is a simplified version of: + # give_terminal_to is a simplified version of: # give_terminal_to from bash 4.3 source, jobs.c, line 4030 # this will give the terminal to the process group pgid if ON_CYGWIN: # on cygwin, signal.pthread_sigmask does not exist in Python, even # though pthread_sigmask is defined in the kernel. thus, we use # ctypes to mimic the calls in the "normal" version below. - def _give_terminal_to(pgid): + def give_terminal_to(pgid): st = _shell_tty() if st is not None and os.isatty(st): omask = ctypes.c_ulong() @@ -153,12 +159,16 @@ else: LIBC.sigprocmask(ctypes.c_int(signal.SIG_SETMASK), ctypes.byref(omask), None) else: - def _give_terminal_to(pgid): - st = _shell_tty() - if st is not None and os.isatty(st): - oldmask = signal.pthread_sigmask(signal.SIG_BLOCK, - _block_when_giving) - os.tcsetpgrp(st, pgid) + def give_terminal_to(pgid): + oldmask = signal.pthread_sigmask(signal.SIG_BLOCK, _block_when_giving) + try: + os.tcsetpgrp(sys.stderr.fileno(), pgid) + return True + except Exception as e: + print('tcsetpgrp error {}: {}'.format(e.__class__.__name__, e), + file=sys.stderr) + return False + finally: signal.pthread_sigmask(signal.SIG_SETMASK, oldmask) def wait_for_active_job(last_task=None, backgrounded=False): @@ -170,22 +180,14 @@ else: active_task = get_next_task() # Return when there are no foreground active task if active_task is None: - _give_terminal_to(_shell_pgrp) # give terminal back to the shell - if backgrounded and hasattr(builtins, '__xonsh_shell__'): - # restoring sanity could probably be called whenever we return - # control to the shell. But it only seems to matter after a - # ^Z event. This *has* to be called after we give the terminal - # back to the shell. - builtins.__xonsh_shell__.shell.restore_tty_sanity() return last_task - pgrp = active_task.get('pgrp', None) obj = active_task['obj'] backgrounded = False - # give the terminal over to the fg process - if pgrp is not None: - _give_terminal_to(pgrp) - _continue(active_task) - _, wcode = os.waitpid(obj.pid, os.WUNTRACED) + try: + _, wcode = os.waitpid(obj.pid, os.WUNTRACED) + except ChildProcessError: # No child processes + return wait_for_active_job(last_task=active_task, + backgrounded=backgrounded) if os.WIFSTOPPED(wcode): print('^Z') active_task['status'] = "stopped" @@ -307,9 +309,9 @@ def clean_jobs(): # newline print() print('xonsh: {}'.format(msg), file=sys.stderr) - print('-'*5, file=sys.stderr) + print('-' * 5, file=sys.stderr) jobs([], stdout=sys.stderr) - print('-'*5, file=sys.stderr) + print('-' * 5, file=sys.stderr) print('Type "exit" or press "ctrl-d" again to force quit.', file=sys.stderr) jobs_clean = False @@ -354,31 +356,31 @@ def fg(args, stdin=None): return '', 'Cannot bring nonexistent job to foreground.\n' if len(args) == 0: - act = tasks[0] # take the last manipulated task by default + tid = tasks[0] # take the last manipulated task by default elif len(args) == 1: try: if args[0] == '+': # take the last manipulated task - act = tasks[0] + tid = tasks[0] elif args[0] == '-': # take the second to last manipulated task - act = tasks[1] + tid = tasks[1] else: - act = int(args[0]) + tid = int(args[0]) except (ValueError, IndexError): return '', 'Invalid job: {}\n'.format(args[0]) - if act not in builtins.__xonsh_all_jobs__: + if tid not in builtins.__xonsh_all_jobs__: return '', 'Invalid job: {}\n'.format(args[0]) else: return '', 'fg expects 0 or 1 arguments, not {}\n'.format(len(args)) # Put this one on top of the queue - tasks.remove(act) - tasks.appendleft(act) + tasks.remove(tid) + tasks.appendleft(tid) - job = get_task(act) + job = get_task(tid) job['bg'] = False job['status'] = "running" - print_one_job(act) + print_one_job(tid) wait_for_active_job() diff --git a/xonsh/proc.py b/xonsh/proc.py index 697d321b8..8aef824ce 100644 --- a/xonsh/proc.py +++ b/xonsh/proc.py @@ -29,7 +29,7 @@ from xonsh.tools import (redirect_stdout, redirect_stderr, print_exception, XonshCalledProcessError, findfirst, on_main_thread, XonshError, format_std_prepost) from xonsh.lazyasd import lazyobject, LazyObject -from xonsh.jobs import wait_for_active_job +from xonsh.jobs import wait_for_active_job, give_terminal_to from xonsh.lazyimps import fcntl, termios, _winapi, msvcrt, winutils @@ -1654,7 +1654,8 @@ class CommandPipeline: nonblocking = (io.BytesIO, NonBlockingFDReader, ConsoleParallelReader) - def __init__(self, specs, procs, starttime=None, captured=False): + def __init__(self, specs, procs, starttime=None, captured=False, + term_pgid=None): """ Parameters ---------- @@ -1695,6 +1696,7 @@ class CommandPipeline: self._closed_handle_cache = {} self.lines = [] self._stderr_prefix = self._stderr_postfix = None + self._term_pgid = term_pgid def __repr__(self): s = self.__class__.__name__ + '(' @@ -1912,12 +1914,10 @@ class CommandPipeline: # Ending methods # - def end(self, tee_output=True): + def end_internal(self, tee_output): """Waits for the command to complete and then runs any closing and cleanup procedures that need to be run. """ - if self.ended: - return if tee_output: for _ in self.tee_stdout(): pass @@ -1932,6 +1932,21 @@ class CommandPipeline: self.ended = True self._raise_subproc_error() + def end(self, tee_output=True): + """ + End the pipeline, return the controlling terminal if needed. + + Main things done in end_internal(). + """ + if self.ended: + return + self.end_internal(tee_output=tee_output) + pgid = os.getpgid(0) + if self._term_pgid is None or pgid == self._term_pgid: + return + if give_terminal_to(pgid): # if gave term succeed + self._term_pgid = pgid + def _endtime(self): """Sets the closing timestamp if it hasn't been already.""" if self.endtime is None: