mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-05 17:00:58 +01:00
update controlling terminal and process group
This commit is contained in:
parent
ede2340999
commit
bebfc7fab4
3 changed files with 91 additions and 58 deletions
|
@ -27,7 +27,7 @@ from xonsh.inspectors import Inspector
|
||||||
from xonsh.aliases import Aliases, make_default_aliases
|
from xonsh.aliases import Aliases, make_default_aliases
|
||||||
from xonsh.environ import Env, default_env, locate_binary
|
from xonsh.environ import Env, default_env, locate_binary
|
||||||
from xonsh.foreign_shells import load_foreign_aliases
|
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.platform import ON_POSIX, ON_WINDOWS
|
||||||
from xonsh.proc import (
|
from xonsh.proc import (
|
||||||
PopenThread, ProcProxyThread, ProcProxy, ConsoleParallelReader,
|
PopenThread, ProcProxyThread, ProcProxy, ConsoleParallelReader,
|
||||||
|
@ -496,6 +496,8 @@ class SubprocSpec:
|
||||||
self.prep_env(kwargs)
|
self.prep_env(kwargs)
|
||||||
self.prep_preexec_fn(kwargs, pipeline_group=pipeline_group)
|
self.prep_preexec_fn(kwargs, pipeline_group=pipeline_group)
|
||||||
if callable(self.alias):
|
if callable(self.alias):
|
||||||
|
if 'preexec_fn' in kwargs:
|
||||||
|
kwargs.pop('preexec_fn')
|
||||||
p = self.cls(self.alias, self.cmd, **kwargs)
|
p = self.cls(self.alias, self.cmd, **kwargs)
|
||||||
else:
|
else:
|
||||||
p = self._run_binary(kwargs)
|
p = self._run_binary(kwargs)
|
||||||
|
@ -533,7 +535,7 @@ class SubprocSpec:
|
||||||
|
|
||||||
def prep_preexec_fn(self, kwargs, pipeline_group=None):
|
def prep_preexec_fn(self, kwargs, pipeline_group=None):
|
||||||
"""Prepares the 'preexec_fn' keyword argument"""
|
"""Prepares the 'preexec_fn' keyword argument"""
|
||||||
if not (ON_POSIX and self.cls is subprocess.Popen):
|
if not ON_POSIX:
|
||||||
return
|
return
|
||||||
if not builtins.__xonsh_env__.get('XONSH_INTERACTIVE'):
|
if not builtins.__xonsh_env__.get('XONSH_INTERACTIVE'):
|
||||||
return
|
return
|
||||||
|
@ -778,6 +780,17 @@ def _should_set_title(captured=False):
|
||||||
hasattr(builtins, '__xonsh_shell__'))
|
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):
|
def run_subproc(cmds, captured=False):
|
||||||
"""Runs a subprocess, in its many forms. This takes a list of 'commands,'
|
"""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
|
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)
|
specs = cmds_to_specs(cmds, captured=captured)
|
||||||
captured = specs[-1].captured
|
captured = specs[-1].captured
|
||||||
|
background = specs[-1].background
|
||||||
procs = []
|
procs = []
|
||||||
proc = pipeline_group = None
|
proc = pipeline_group = None
|
||||||
|
term_pgid = None
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
proc = spec.run(pipeline_group=pipeline_group)
|
proc = spec.run(pipeline_group=pipeline_group)
|
||||||
procs.append(proc)
|
if captured != 'object' and proc.pid and pipeline_group is None:
|
||||||
if ON_POSIX and pipeline_group is None and \
|
|
||||||
spec.cls is subprocess.Popen:
|
|
||||||
pipeline_group = proc.pid
|
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({
|
add_job({
|
||||||
'cmds': cmds,
|
'cmds': cmds,
|
||||||
'pids': [i.pid for i in procs],
|
'pids': [i.pid for i in procs],
|
||||||
'obj': proc,
|
'obj': proc,
|
||||||
'bg': spec.background,
|
'bg': background,
|
||||||
})
|
})
|
||||||
if _should_set_title(captured=captured):
|
if _should_set_title(captured=captured):
|
||||||
# set title here to get currently executing command
|
# set title here to get currently executing command
|
||||||
pause_call_resume(proc, builtins.__xonsh_shell__.settitle)
|
pause_call_resume(proc, builtins.__xonsh_shell__.settitle)
|
||||||
# create command or return if backgrounding.
|
# create command or return if backgrounding.
|
||||||
if spec.background:
|
if background:
|
||||||
return
|
return
|
||||||
if captured == 'hiddenobject':
|
if captured == 'hiddenobject':
|
||||||
command = HiddenCommandPipeline(specs, procs, starttime=starttime,
|
command = HiddenCommandPipeline(specs, procs, starttime=starttime,
|
||||||
captured=captured)
|
captured=captured, term_pgid=term_pgid)
|
||||||
else:
|
else:
|
||||||
command = CommandPipeline(specs, procs, starttime=starttime,
|
command = CommandPipeline(specs, procs, starttime=starttime,
|
||||||
captured=captured)
|
captured=captured, term_pgid=term_pgid)
|
||||||
# now figure out what we should return.
|
# now figure out what we should return.
|
||||||
if captured == 'stdout':
|
if captured == 'stdout':
|
||||||
command.end()
|
command.end()
|
||||||
|
|
|
@ -28,7 +28,10 @@ if ON_DARWIN:
|
||||||
for pid in job['pids']:
|
for pid in job['pids']:
|
||||||
if pid is None: # the pid of an aliased proc is None
|
if pid is None: # the pid of an aliased proc is None
|
||||||
continue
|
continue
|
||||||
os.kill(pid, signal)
|
try:
|
||||||
|
os.kill(pid, signal)
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
elif ON_WINDOWS:
|
elif ON_WINDOWS:
|
||||||
pass
|
pass
|
||||||
elif ON_CYGWIN:
|
elif ON_CYGWIN:
|
||||||
|
@ -70,6 +73,9 @@ if ON_WINDOWS:
|
||||||
def _set_pgrp(info):
|
def _set_pgrp(info):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def give_terminal_to(pgid):
|
||||||
|
pass
|
||||||
|
|
||||||
def wait_for_active_job(last_task=None, backgrounded=False):
|
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
|
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)
|
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
||||||
|
|
||||||
def _set_pgrp(info):
|
def _set_pgrp(info):
|
||||||
pid = info['pids'][0]
|
for pid in info['pids']:
|
||||||
if pid is None:
|
if pid is None: # occurs if first process is an alias
|
||||||
# occurs if first process is an alias
|
continue
|
||||||
info['pgrp'] = None
|
try:
|
||||||
return
|
info['pgrp'] = os.getpgid(pid)
|
||||||
try:
|
return
|
||||||
info['pgrp'] = os.getpgid(pid)
|
except ProcessLookupError:
|
||||||
except ProcessLookupError:
|
continue
|
||||||
info['pgrp'] = None
|
info['pgrp'] = None
|
||||||
|
|
||||||
_shell_pgrp = os.getpgrp()
|
_shell_pgrp = os.getpgrp()
|
||||||
|
|
||||||
|
@ -123,21 +129,21 @@ else:
|
||||||
def _shell_tty():
|
def _shell_tty():
|
||||||
try:
|
try:
|
||||||
_st = sys.stderr.fileno()
|
_st = sys.stderr.fileno()
|
||||||
if os.tcgetpgrp(_st) != os.getpgid(os.getpid()):
|
if os.tcgetpgrp(_st) != os.getpgid(0):
|
||||||
# we don't own it
|
# we don't own it
|
||||||
_st = None
|
_st = None
|
||||||
except OSError:
|
except OSError:
|
||||||
_st = None
|
_st = None
|
||||||
return _st
|
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
|
# give_terminal_to from bash 4.3 source, jobs.c, line 4030
|
||||||
# this will give the terminal to the process group pgid
|
# this will give the terminal to the process group pgid
|
||||||
if ON_CYGWIN:
|
if ON_CYGWIN:
|
||||||
# on cygwin, signal.pthread_sigmask does not exist in Python, even
|
# on cygwin, signal.pthread_sigmask does not exist in Python, even
|
||||||
# though pthread_sigmask is defined in the kernel. thus, we use
|
# though pthread_sigmask is defined in the kernel. thus, we use
|
||||||
# ctypes to mimic the calls in the "normal" version below.
|
# ctypes to mimic the calls in the "normal" version below.
|
||||||
def _give_terminal_to(pgid):
|
def give_terminal_to(pgid):
|
||||||
st = _shell_tty()
|
st = _shell_tty()
|
||||||
if st is not None and os.isatty(st):
|
if st is not None and os.isatty(st):
|
||||||
omask = ctypes.c_ulong()
|
omask = ctypes.c_ulong()
|
||||||
|
@ -153,12 +159,16 @@ else:
|
||||||
LIBC.sigprocmask(ctypes.c_int(signal.SIG_SETMASK),
|
LIBC.sigprocmask(ctypes.c_int(signal.SIG_SETMASK),
|
||||||
ctypes.byref(omask), None)
|
ctypes.byref(omask), None)
|
||||||
else:
|
else:
|
||||||
def _give_terminal_to(pgid):
|
def give_terminal_to(pgid):
|
||||||
st = _shell_tty()
|
oldmask = signal.pthread_sigmask(signal.SIG_BLOCK, _block_when_giving)
|
||||||
if st is not None and os.isatty(st):
|
try:
|
||||||
oldmask = signal.pthread_sigmask(signal.SIG_BLOCK,
|
os.tcsetpgrp(sys.stderr.fileno(), pgid)
|
||||||
_block_when_giving)
|
return True
|
||||||
os.tcsetpgrp(st, pgid)
|
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)
|
signal.pthread_sigmask(signal.SIG_SETMASK, oldmask)
|
||||||
|
|
||||||
def wait_for_active_job(last_task=None, backgrounded=False):
|
def wait_for_active_job(last_task=None, backgrounded=False):
|
||||||
|
@ -170,22 +180,14 @@ else:
|
||||||
active_task = get_next_task()
|
active_task = get_next_task()
|
||||||
# Return when there are no foreground active task
|
# Return when there are no foreground active task
|
||||||
if active_task is None:
|
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
|
return last_task
|
||||||
pgrp = active_task.get('pgrp', None)
|
|
||||||
obj = active_task['obj']
|
obj = active_task['obj']
|
||||||
backgrounded = False
|
backgrounded = False
|
||||||
# give the terminal over to the fg process
|
try:
|
||||||
if pgrp is not None:
|
_, wcode = os.waitpid(obj.pid, os.WUNTRACED)
|
||||||
_give_terminal_to(pgrp)
|
except ChildProcessError: # No child processes
|
||||||
_continue(active_task)
|
return wait_for_active_job(last_task=active_task,
|
||||||
_, wcode = os.waitpid(obj.pid, os.WUNTRACED)
|
backgrounded=backgrounded)
|
||||||
if os.WIFSTOPPED(wcode):
|
if os.WIFSTOPPED(wcode):
|
||||||
print('^Z')
|
print('^Z')
|
||||||
active_task['status'] = "stopped"
|
active_task['status'] = "stopped"
|
||||||
|
@ -307,9 +309,9 @@ def clean_jobs():
|
||||||
# newline
|
# newline
|
||||||
print()
|
print()
|
||||||
print('xonsh: {}'.format(msg), file=sys.stderr)
|
print('xonsh: {}'.format(msg), file=sys.stderr)
|
||||||
print('-'*5, file=sys.stderr)
|
print('-' * 5, file=sys.stderr)
|
||||||
jobs([], stdout=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.',
|
print('Type "exit" or press "ctrl-d" again to force quit.',
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
jobs_clean = False
|
jobs_clean = False
|
||||||
|
@ -354,31 +356,31 @@ def fg(args, stdin=None):
|
||||||
return '', 'Cannot bring nonexistent job to foreground.\n'
|
return '', 'Cannot bring nonexistent job to foreground.\n'
|
||||||
|
|
||||||
if len(args) == 0:
|
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:
|
elif len(args) == 1:
|
||||||
try:
|
try:
|
||||||
if args[0] == '+': # take the last manipulated task
|
if args[0] == '+': # take the last manipulated task
|
||||||
act = tasks[0]
|
tid = tasks[0]
|
||||||
elif args[0] == '-': # take the second to last manipulated task
|
elif args[0] == '-': # take the second to last manipulated task
|
||||||
act = tasks[1]
|
tid = tasks[1]
|
||||||
else:
|
else:
|
||||||
act = int(args[0])
|
tid = int(args[0])
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
return '', 'Invalid job: {}\n'.format(args[0])
|
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])
|
return '', 'Invalid job: {}\n'.format(args[0])
|
||||||
else:
|
else:
|
||||||
return '', 'fg expects 0 or 1 arguments, not {}\n'.format(len(args))
|
return '', 'fg expects 0 or 1 arguments, not {}\n'.format(len(args))
|
||||||
|
|
||||||
# Put this one on top of the queue
|
# Put this one on top of the queue
|
||||||
tasks.remove(act)
|
tasks.remove(tid)
|
||||||
tasks.appendleft(act)
|
tasks.appendleft(tid)
|
||||||
|
|
||||||
job = get_task(act)
|
job = get_task(tid)
|
||||||
job['bg'] = False
|
job['bg'] = False
|
||||||
job['status'] = "running"
|
job['status'] = "running"
|
||||||
print_one_job(act)
|
print_one_job(tid)
|
||||||
wait_for_active_job()
|
wait_for_active_job()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ from xonsh.tools import (redirect_stdout, redirect_stderr, print_exception,
|
||||||
XonshCalledProcessError, findfirst, on_main_thread,
|
XonshCalledProcessError, findfirst, on_main_thread,
|
||||||
XonshError, format_std_prepost)
|
XonshError, format_std_prepost)
|
||||||
from xonsh.lazyasd import lazyobject, LazyObject
|
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
|
from xonsh.lazyimps import fcntl, termios, _winapi, msvcrt, winutils
|
||||||
|
|
||||||
|
|
||||||
|
@ -1654,7 +1654,8 @@ class CommandPipeline:
|
||||||
|
|
||||||
nonblocking = (io.BytesIO, NonBlockingFDReader, ConsoleParallelReader)
|
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
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
@ -1695,6 +1696,7 @@ class CommandPipeline:
|
||||||
self._closed_handle_cache = {}
|
self._closed_handle_cache = {}
|
||||||
self.lines = []
|
self.lines = []
|
||||||
self._stderr_prefix = self._stderr_postfix = None
|
self._stderr_prefix = self._stderr_postfix = None
|
||||||
|
self._term_pgid = term_pgid
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
s = self.__class__.__name__ + '('
|
s = self.__class__.__name__ + '('
|
||||||
|
@ -1912,12 +1914,10 @@ class CommandPipeline:
|
||||||
# Ending methods
|
# 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
|
"""Waits for the command to complete and then runs any closing and
|
||||||
cleanup procedures that need to be run.
|
cleanup procedures that need to be run.
|
||||||
"""
|
"""
|
||||||
if self.ended:
|
|
||||||
return
|
|
||||||
if tee_output:
|
if tee_output:
|
||||||
for _ in self.tee_stdout():
|
for _ in self.tee_stdout():
|
||||||
pass
|
pass
|
||||||
|
@ -1932,6 +1932,21 @@ class CommandPipeline:
|
||||||
self.ended = True
|
self.ended = True
|
||||||
self._raise_subproc_error()
|
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):
|
def _endtime(self):
|
||||||
"""Sets the closing timestamp if it hasn't been already."""
|
"""Sets the closing timestamp if it hasn't been already."""
|
||||||
if self.endtime is None:
|
if self.endtime is None:
|
||||||
|
|
Loading…
Add table
Reference in a new issue