removed subproc threadability

This commit is contained in:
Anthony Scopatz 2020-10-17 15:03:36 -05:00
parent 537a29a0ef
commit 438f0c348b
3 changed files with 16 additions and 359 deletions

View file

@ -13,7 +13,7 @@ import argparse
import collections.abc as cabc
from xonsh.platform import ON_WINDOWS, ON_POSIX, pathbasename
from xonsh.tools import executables_in
from xonsh.tools import executables_in, print_warning
from xonsh.lazyasd import lazyobject
@ -30,7 +30,7 @@ class CommandsCache(cabc.Mapping):
self._path_checksum = None
self._alias_checksum = None
self._path_mtime = -1
self.threadable_predictors = default_threadable_predictors()
self._threadable_predictors = {}
def __contains__(self, key):
_ = self.all_commands
@ -55,6 +55,13 @@ class CommandsCache(cabc.Mapping):
"""Returns whether the cache is populated or not."""
return len(self._cmds_cache) == 0
@property
def threadable_predictors(self):
print_warning("CommandsCache.threadable_predictors has been deprecated "
"because it is no longer needed. Please remove downstream "
"references to it. These operations will have no effect.")
return self._threadable_predictors
@staticmethod
def get_possible_names(name):
"""Generates the possible `PATHEXT` extension variants of a given executable
@ -212,311 +219,3 @@ class CommandsCache(cabc.Mapping):
return (
val == (name, True) and self.locate_binary(name, ignore_alias=True) is None
)
def predict_threadable(self, cmd):
"""Predicts whether a command list is able to be run on a background
thread, rather than the main thread.
"""
predictor = self.get_predictor_threadable(cmd[0])
return predictor(cmd[1:])
def get_predictor_threadable(self, cmd0):
"""Return the predictor whether a command list is able to be run on a
background thread, rather than the main thread.
"""
name = self.cached_name(cmd0)
predictors = self.threadable_predictors
if ON_WINDOWS:
# On all names (keys) are stored in upper case so instead
# we get the original cmd or alias name
path, _ = self.lazyget(name, (None, None))
if path is None:
return predict_true
else:
name = pathbasename(path)
if name not in predictors:
pre, ext = os.path.splitext(name)
if pre in predictors:
predictors[name] = predictors[pre]
if name not in predictors:
predictors[name] = self.default_predictor(name, cmd0)
predictor = predictors[name]
return predictor
#
# Background Predictors (as methods)
#
def default_predictor(self, name, cmd0):
"""Default predictor, using predictor from original command if the
command is an alias, elseif build a predictor based on binary analysis
on POSIX, else return predict_true.
"""
# alias stuff
if not os.path.isabs(cmd0) and os.sep not in cmd0:
alss = getattr(builtins, "aliases", dict())
if cmd0 in alss:
return self.default_predictor_alias(cmd0)
# other default stuff
if ON_POSIX:
return self.default_predictor_readbin(
name, cmd0, timeout=0.1, failure=predict_true
)
else:
return predict_true
def default_predictor_alias(self, cmd0):
alias_recursion_limit = (
10 # this limit is se to handle infinite loops in aliases definition
)
first_args = [] # contains in reverse order args passed to the aliased command
alss = getattr(builtins, "aliases", dict())
while cmd0 in alss:
alias_name = alss[cmd0]
if isinstance(alias_name, (str, bytes)) or not isinstance(
alias_name, cabc.Sequence
):
return predict_true
for arg in alias_name[:0:-1]:
first_args.insert(0, arg)
if cmd0 == alias_name[0]:
# it is a self-alias stop recursion immediatly
return predict_true
cmd0 = alias_name[0]
alias_recursion_limit -= 1
if alias_recursion_limit == 0:
return predict_true
predictor_cmd0 = self.get_predictor_threadable(cmd0)
return lambda cmd1: predictor_cmd0(first_args[::-1] + cmd1)
def default_predictor_readbin(self, name, cmd0, timeout, failure):
"""Make a default predictor by
analyzing the content of the binary. Should only works on POSIX.
Return failure if the analysis fails.
"""
fname = cmd0 if os.path.isabs(cmd0) else None
fname = cmd0 if fname is None and os.sep in cmd0 else fname
fname = self.lazy_locate_binary(name) if fname is None else fname
if fname is None:
return failure
if not os.path.isfile(fname):
return failure
try:
fd = os.open(fname, os.O_RDONLY | os.O_NONBLOCK)
except Exception:
return failure # opening error
search_for = {
(b"ncurses",): [False],
(b"libgpm",): [False],
(b"isatty", b"tcgetattr", b"tcsetattr"): [False, False, False],
}
tstart = time.time()
block = b""
while time.time() < tstart + timeout:
previous_block = block
try:
block = os.read(fd, 2048)
except Exception:
# should not occur, except e.g. if a file is deleted a a dir is
# created with the same name between os.path.isfile and os.open
os.close(fd)
return failure
if len(block) == 0:
os.close(fd)
return predict_true # no keys of search_for found
analyzed_block = previous_block + block
for k, v in search_for.items():
for i in range(len(k)):
if v[i]:
continue
if k[i] in analyzed_block:
v[i] = True
if all(v):
os.close(fd)
return predict_false # use one key of search_for
os.close(fd)
return failure # timeout
#
# Background Predictors
#
def predict_true(args):
"""Always say the process is threadable."""
return True
def predict_false(args):
"""Never say the process is threadable."""
return False
@lazyobject
def SHELL_PREDICTOR_PARSER():
p = argparse.ArgumentParser("shell", add_help=False)
p.add_argument("-c", nargs="?", default=None)
p.add_argument("filename", nargs="?", default=None)
return p
def predict_shell(args):
"""Predict the backgroundability of the normal shell interface, which
comes down to whether it is being run in subproc mode.
"""
ns, _ = SHELL_PREDICTOR_PARSER.parse_known_args(args)
if ns.c is None and ns.filename is None:
pred = False
else:
pred = True
return pred
@lazyobject
def HELP_VER_PREDICTOR_PARSER():
p = argparse.ArgumentParser("cmd", add_help=False)
p.add_argument("-h", "--help", dest="help", nargs="?", action="store", default=None)
p.add_argument(
"-v", "-V", "--version", dest="version", nargs="?", action="store", default=None
)
return p
def predict_help_ver(args):
"""Predict the backgroundability of commands that have help & version
switches: -h, --help, -v, -V, --version. If either of these options is
present, the command is assumed to print to stdout normally and is therefore
threadable. Otherwise, the command is assumed to not be threadable.
This is useful for commands, like top, that normally enter alternate mode
but may not in certain circumstances.
"""
ns, _ = HELP_VER_PREDICTOR_PARSER.parse_known_args(args)
pred = ns.help is not None or ns.version is not None
return pred
@lazyobject
def HG_PREDICTOR_PARSER():
p = argparse.ArgumentParser("hg", add_help=False)
p.add_argument("command")
p.add_argument(
"-i", "--interactive", action="store_true", default=False, dest="interactive"
)
return p
def predict_hg(args):
"""Predict if mercurial is about to be run in interactive mode.
If it is interactive, predict False. If it isn't, predict True.
Also predict False for certain commands, such as split.
"""
ns, _ = HG_PREDICTOR_PARSER.parse_known_args(args)
if ns.command == "split":
return False
else:
return not ns.interactive
def predict_env(args):
"""Predict if env is launching a threadable command or not.
The launched command is extracted from env args, and the predictor of
lauched command is used."""
for i in range(len(args)):
if args[i] and args[i][0] != "-" and "=" not in args[i]:
# args[i] is the command and the following is its arguments
# so args[i:] is used to predict if the command is threadable
return builtins.__xonsh__.commands_cache.predict_threadable(args[i:])
return True
def default_threadable_predictors():
"""Generates a new defaultdict for known threadable predictors.
The default is to predict true.
"""
# alphabetical, for what it is worth.
predictors = {
"asciinema": predict_help_ver,
"aurman": predict_false,
"awk": predict_true,
"bash": predict_shell,
"cat": predict_false,
"clear": predict_false,
"cls": predict_false,
"cmd": predict_shell,
"cryptop": predict_false,
"cryptsetup": predict_true,
"csh": predict_shell,
"curl": predict_true,
"elvish": predict_shell,
"emacsclient": predict_false,
"env": predict_env,
"ex": predict_false,
"fish": predict_shell,
"gawk": predict_true,
"ghci": predict_help_ver,
"git": predict_true,
"gvim": predict_help_ver,
"hg": predict_hg,
"htop": predict_help_ver,
"ipython": predict_shell,
"julia": predict_shell,
"ksh": predict_shell,
"less": predict_help_ver,
"ls": predict_true,
"man": predict_help_ver,
"mc": predict_false,
"more": predict_help_ver,
"mutt": predict_help_ver,
"mvim": predict_help_ver,
"nano": predict_help_ver,
"nmcli": predict_true,
"nvim": predict_false,
"percol": predict_false,
"ponysay": predict_help_ver,
"psql": predict_false,
"push": predict_shell,
"pv": predict_false,
"python": predict_shell,
"python2": predict_shell,
"python3": predict_shell,
"ranger": predict_help_ver,
"repo": predict_help_ver,
"rview": predict_false,
"rvim": predict_false,
"rwt": predict_shell,
"scp": predict_false,
"sh": predict_shell,
"ssh": predict_false,
"startx": predict_false,
"sudo": predict_help_ver,
"sudoedit": predict_help_ver,
"systemctl": predict_true,
"tcsh": predict_shell,
"telnet": predict_false,
"top": predict_help_ver,
"tput": predict_false,
"udisksctl": predict_true,
"unzip": predict_true,
"vi": predict_false,
"view": predict_false,
"vim": predict_false,
"vimpager": predict_help_ver,
"weechat": predict_help_ver,
"wget": predict_true,
"xclip": predict_help_ver,
"xdg-open": predict_false,
"xo": predict_help_ver,
"xon.sh": predict_shell,
"xonsh": predict_shell,
"yes": predict_false,
"zip": predict_true,
"zipinfo": predict_true,
"zsh": predict_shell,
}
return predictors

View file

@ -1336,35 +1336,6 @@ def DEFAULT_VARS():
"not always happen.",
doc_configurable=False,
),
"THREAD_SUBPROCS": Var(
is_bool_or_none,
to_bool_or_none,
bool_or_none_to_str,
True if not ON_CYGWIN else False,
"Whether or not to try to run subrocess mode in a Python thread, "
"when applicable. There are various trade-offs, which normally "
"affects only interactive sessions.\n\nWhen True:\n\n"
"* Xonsh is able capture & store the stdin, stdout, and stderr \n"
" of threadable subprocesses.\n"
"* However, stopping threaded subprocs with ^Z (i.e. ``SIGTSTP``)\n"
" is disabled as it causes deadlocked terminals.\n"
" ``SIGTSTP`` may still be issued and only the physical pressing\n"
" of ``Ctrl+Z`` is ignored.\n"
"* Threadable commands are run with ``PopenThread`` and threadable \n"
" aliases are run with ``ProcProxyThread``.\n\n"
"When False:\n\n"
"* Xonsh may not be able to capture stdin, stdout, and stderr streams \n"
" unless explicitly asked to do so.\n"
"* Stopping the thread with ``Ctrl+Z`` yields to job control.\n"
"* Threadable commands are run with ``Popen`` and threadable \n"
" alias are run with ``ProcProxy``.\n\n"
"The desired effect is often up to the command, user, or use case.\n\n"
"None values are for internal use only and are used to turn off "
"threading when loading xonshrc files. This is done because Bash "
"was automatically placing new xonsh instances in the background "
"at startup when threadable subprocs were used. Please see "
"https://github.com/xonsh/xonsh/pull/3705 for more information.\n",
),
"TITLE": Var(
is_string,
ensure_string,
@ -1459,8 +1430,7 @@ def DEFAULT_VARS():
default_xonshrc,
"A list of the locations of run control files, if they exist. User "
"defined run control file will supersede values set in system-wide "
"control file if there is a naming collision. $THREAD_SUBPROCS=None "
"when reading in run control files.",
"control file if there is a naming collision.",
doc_default=(
"On Linux & Mac OSX: ``['/etc/xonshrc', '~/.config/xonsh/rc.xsh', '~/.xonshrc']``\n"
"\nOn Windows: "
@ -2195,8 +2165,6 @@ def xonshrc_context(rcfiles=None, execer=None, ctx=None, env=None, login=True):
ctx = {} if ctx is None else ctx
if rcfiles is None:
return env
orig_thread = env.get("THREAD_SUBPROCS")
env["THREAD_SUBPROCS"] = None
env["XONSHRC"] = tuple(rcfiles)
for rcfile in rcfiles:
if not os.path.isfile(rcfile):
@ -2205,8 +2173,6 @@ def xonshrc_context(rcfiles=None, execer=None, ctx=None, env=None, login=True):
_, ext = os.path.splitext(rcfile)
status = xonsh_script_run_control(rcfile, ctx, env, execer=execer, login=login)
loaded.append(status)
if env["THREAD_SUBPROCS"] is None:
env["THREAD_SUBPROCS"] = orig_thread
return ctx

View file

@ -19,7 +19,6 @@ import xonsh.lazyimps as xli
import xonsh.jobs as xj
from xonsh.procs.readers import ConsoleParallelReader
from xonsh.procs.posix import PopenThread
from xonsh.procs.proxies import ProcProxy, ProcProxyThread
from xonsh.procs.pipelines import (
pause_call_resume,
@ -358,7 +357,7 @@ class SubprocSpec:
self.binary_loc = None
self.is_proxy = False
self.background = False
self.threadable = True
self.threadable = False
self.pipeline_index = None
self.last_in_pipeline = False
self.captured_stdout = None
@ -649,7 +648,7 @@ class SubprocSpec:
return
self.is_proxy = True
env = builtins.__xonsh__.env
thable = env.get("THREAD_SUBPROCS") and getattr(
thable = getattr(
alias, "__xonsh_threadable__", True
)
cls = ProcProxyThread if thable else ProcProxy
@ -712,17 +711,9 @@ def _update_last_spec(last):
if callable_alias:
pass
else:
cmds_cache = builtins.__xonsh__.commands_cache
thable = (
env.get("THREAD_SUBPROCS")
and cmds_cache.predict_threadable(last.args)
and cmds_cache.predict_threadable(last.cmd)
)
if captured and thable:
last.cls = PopenThread
elif not thable:
# foreground processes should use Popen
last.threadable = False
if captured:
# update this when we have PTY server/client
last.cls = subprocess.Popen
if captured == "object" or captured == "hiddenobject":
# CommandPipeline objects should not pipe stdout, stderr
return
@ -870,6 +861,7 @@ def run_subproc(cmds, captured=False, envs=None):
# sure that the shell doesn't hang. This `pause_call_resume` invocation
# does this
pause_call_resume(proc, int)
pause_call_resume(proc, int)
# create command or return if backgrounding.
if background:
return