diff --git a/xonsh/commands_cache.py b/xonsh/commands_cache.py index 416a1ab59..fab705c26 100644 --- a/xonsh/commands_cache.py +++ b/xonsh/commands_cache.py @@ -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 diff --git a/xonsh/environ.py b/xonsh/environ.py index 8eb80a935..217bb51c4 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -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 diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py index d2865d19a..73d6dcf2d 100644 --- a/xonsh/procs/specs.py +++ b/xonsh/procs/specs.py @@ -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