Repository: xonsh/xonsh
Pull request: 👻 Spoooky! Ctrl+Z Fix!
This PR fixes the many job control issues we have had with suspending and presumably closes #1103, #2353, #2370, #2874, #3019, and #3216. I am writing this down so that we don't have to rediscover all of these points in the future.

TL;DR is that threadable subprocesses can't work with job suspension in their current form.

## The Problem

Pressing `Ctrl+Z` (`^Z`, or `b"\x1a"`) at the interactive terminal on posix would cause the subprocess to go into state T (or [stopped](https://linuxjourney.com/lesson/process-states)). This is because [`SIGTSTP`](https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html) is being issued (as it should be).  However, this would cause xonsh / Python to deadlock until an *external* process sent a `SIGCONT` signal, **which is very bad**. 

For example, let's look at the `sleep` command run in a thread (`PopenThread`) before `^Z` is pressed. The process status and heirarchy would look like:

**Before `^Z`:**

```
PID   Status Command
10016 S      |- xonsh   # this is the parent xonsh process
10045 S       |- xonsh  # some xonsh i/o monitoring thread
10044 S       |- xonsh  # some xonsh i/o monitoring thread
10043 S       |- xonsh  # some xonsh i/o monitoring thread
10042 S       |- sleep  # the subprocess being run
```

They are all asleep like they should be.  Oddly though, the threads that are spun up to monitor stdout, stderr, etc. each get their own process id and are children of original xonsh process at the same level as the subprocess we are running.

Now suposse that the user hits `^Z`. The status of the processes becomes:

**After `^Z`:**

```
PID   Status Command
10016 S      |- xonsh   # this is the parent xonsh process
10045 S       |- xonsh  # some xonsh i/o monitoring thread
10044 S       |- xonsh  # some xonsh i/o monitoring thread
10043 S       |- xonsh  # some xonsh i/o monitoring thread
10042 T       |- sleep  # the subprocess being run
```

Because these extra xonsh threads exist that are waiting on the stopped process, xonsh itself is deadlocked and cannot act on any further input until the `SIGCONT` signal is recieved. Note that this situation is very different for subprocs that are not threaded (`Popen`).  For example, let's look at `less` running on some file. **These procs behave normally with job control.**

**Before `^Z`:**

```
PID   Status Command
10016 S      |- xonsh        # this is the parent xonsh process
10042 R       |- less f.txt  # the subprocess being run
```

**After `^Z`:**

```
PID   Status Command
10016 S      |- xonsh        # this is the parent xonsh process
10042 T       |- less f.txt  # the subprocess being run
```

The example with `less` parallels what happens in Bash, where no subprocs are threadable.

## Strategies

There are so many options for how to try to resolve the deadlocking, I named them after character(s) in a [book series](https://www.goodreads.com/book/show/34403860-sufficiently-advanced-magic). Branches have the same name as strategy. The stratgies are outlined here, with sections going into their feasability below.

* **Keras:** Kill the extra threads before suspending.
* **Selyrian:** Kill the extra threads and ignore the suspending signal.
* **Talien:** Ignore the suspend signal and try to reimplement it by reading stdin
* **Salaris:** Ignore the suspend signal entriely and just continue on as if nothing happened.
* **Lydia:** The addition of an environment variable to allow the user to determine if subprocs should be run in threads or not.

The above (except for Lydia) apply only to `PopenThread`.

## Keras

The Keras strategy attempts to kill the threads prior to suspending. This means turning off the teeing of stdin, stdout, and stderr. In principle, this is a safe operations because we will be stopping the subprocess and returning control back to the terminal, and then rendering a new prompt. 

However, this turns out to not be possible because the subproc seems to be stopped before the other processes and thus any signal handlers that have been installed by Python/xonsh to address `SIGTSTP` are never called and we are deadlocked. It may be that the signal handlers are never called just because the subprocess *always seems* to have a lower process id (PID) than the other xonsh threads, and thus is stopped first.  However, even if were were able to manipulate the execution order to ensure that the threads have lower PIDs, this seems like a non-detrministic fix.

It is important to note, though, that SIGTSTP comes in on file descriptor 0 (fd0, stdin), which has been attached to the subprocess by this point. So the signal is first sent to the subprocess. We do not want to, and may not be able to, redirect stdin for all subprocs. Many commands rely on being attched to fd0 for stdin, or change their behavior if they think they are being piped a different input.

One way to get around this might have been to run the subprocess in a Python subprocess, adding a level to the Python process heirarchy.  For example:

**Before `^Z`:**

```
PID   Status Command
10016 S      |- xonsh   # this is the parent xonsh process
10046 S       |- xonsh  # some xonsh i/o monitoring thread
10045 S       |- xonsh  # some xonsh i/o monitoring thread
10044 S       |- xonsh  # some xonsh i/o monitoring thread
10042 S       |- python -c "subprocess.run(['sleep', '2'])" 
10043 S        |- sleep 2 # the subprocess being run
```

You might think this is a reasonble cheat because unthreaded (`Popen`) subprocs have the correct job control behavior in xonsh. However, this tomfoolery doesn't trick anybody and the deadlock remains.

**After `^Z`:**

```
PID   Status Command
10016 S      |- xonsh   # this is the parent xonsh process
10046 S       |- xonsh  # some xonsh i/o monitoring thread
10045 S       |- xonsh  # some xonsh i/o monitoring thread
10044 S       |- xonsh  # some xonsh i/o monitoring thread
10042 T       |- python -c "subprocess.run(['sleep', '2'])" 
10043 T        |- sleep 2 # the subprocess being run
```

Thus, unfortunately, the Keras strategy is unviable.

## Selyrian

The Seylrian strategy is to kill the threads and the ignore any suspending that may happen. As discussed with the Keras strategy, we are not able to kill the threads. Thus, unfortunately, the Selyrian strategy is unviable.

## Taelien

The Taelien strategy is to ignore the `^Z`, kill the other threads, and then suspend the subprocess ourselves. The theory behind this is that it is possible to set suspends signal to be bound to a null key (`b"\x00"`, `stty susp "<undef>"`). In this case, when the user types `^Z`, the kernel does not send SIGTSTP to the subprocess. However, because `^Z` is a control character, it is still echoed to stdin/stdout/stderr. It should be possible to read the `^Z` from the relevant buffer `PopenThread` and hook into the proper next steps.

The good news is that unbinding the suspend action in this way does prevent `^Z` from sending SIGTSTP, and the subprocess continues executing as if nothing had happened.  The bad news is that it is not possible to monitor the I/O buffers for the `^Z` (`b"\x1a"`) character because it written directly to that buffer (by the kernel) and does not go through the capture pipeline setup by `SubprocSpec` and `PopenThread`. Also because of this direct writting, we have no way of realizing that `^Z` has been typed and cannot issue a message to the user that it has been disabled.

Thus, unfortunately, the Taelien strategy is unviable.

## Salaris

The Salaris strategy is to just ignore all `^Z` input completely in `PopenThread`. Given the discussion in the Taelien strategy, this...actually works.

## Lydia

The Lydia strategy yields initial control over whether `Popen` or `PopenThread` is selected to the user. This is presumably done through an environment variable (`$THREAD_SUBPROCS`, because no one else in the world seems to be using it). This allows users to choose the behavior they want, both as a global configuration and in a small context with `${...}.swap(THREAD_SUBPROCS=False)`. This is effectively an easier-to-use alternative to saying,

```python
__xonsh__.commands_cache.threadable_predictors['cmd'] = lambda x: False
```

when testing if threading subprocs is causing problems. It also enables people to always have a working version of `^Z` if that is what they want. The trade off is that when `$THREAD_SUBPROCS = False`, the I/O streams may not be fully captured on the command pipeline in all cases.

The Lydia strategy is also workable.

## Solution

In the end, the only two viable strategies are Salaris + Lydia.  This PR implements these both, `lydia`  being branched off of `salaris`. This is not the ideal combination for all of the reasons above.  However, it is the best that is achievable given the current state of the code base.  

As discussed many other times, we can probably do a ground-up rewrite of this whole system and incorporate all of the lessons learned. This is probably best done in another library (slug) that xonsh then uses. However, it is not clear to me that the Python `subprocess` module is actually able to handle the kind of signal manipulation we want. We might need to move to an async system or write a low-level version of job control for proper handling of `^Z`.

Problem statement:
ctrl-z problem on Linux
Launching a command (like emacs) followed by ctrl-z suspends
the process but doesn't return keyboard control to xonsh. So there
is no way to detach the process with bg and continue typing. The
documentation only mentions that ctrl-z doesn't work on Windows.

xonfig
+------------------+----------+
| xonsh | 0.3.2 |
| Python | 3.4.3 |
| PLY | 3.7 |
| have readline | True |
| prompt toolkit | None |
| shell type | readline |
| pygments | None |
| on posix | True |
| on linux | True |
| distro | Fedora |
| on darwin | False |
| on windows | False |
| is superuser | False |
| default encoding | utf-8 |
+------------------+----------+

Hints from issue discussion:
This one I can't reproduce.  For me, the ctrl-z functionality is working just as I would expect (control goes back to the terminal).

Does this happen for all commands (e.g., `sleep 10`)?  Or just for certain ones like `emacs`?

Okay, wait.  I can reproduce this from within a graphical `emacs` window.  I had been testing with text emacs (`emacs -nw`).  This does indeed look like a bug.

For reference, just tested now...

In Bash, after opening emacs:
- hitting ctrl-z when the terminal has focus _stops the job and returns control to the terminal_ (correct)
- hitting ctrl-z when the emacs window has focus _does nothing_ (correct)

In xonsh, after opening emacs:
- hitting ctrl-z when the terminal has focus _stops the job and returns control to the terminal_ (correct)
- hitting ctrl-z when the emacs window has focus _stops the job and does not return control to the terminal_ (incorrect)

Again, thanks for reporting, @mburo!

Actually, @mburo, I'm not sure I can consider this a bug, as xonsh seems to exhibit the exact same behavior as bash in this situation.  I think if anything, one could consider this to be a problem with emacs.

I tested this as well - it works fine for me, in both cases.

pbr@mongo:~$ emacs
(....text-based emacs takes over screen)
^Z
[1]+  Stopped                 emacs
pbr@mongo:~$ fg
emacs
^X^C
pbr@mongo:~$ emacs
^Z
[1]+  Stopped                 emacs
pbr@mongo:~$ fg
emacs
(...takes over screen again)
^Z
[1]+  Stopped                 emacs
pbr@mongo:~$ jobs
[1]+  Stopped                 emacs
pbr@mongo:~$ ls|cat /dev/tty
^Z
[2]+  Stopped                 ls --color=auto | cat /dev/tty
pbr@mongo:~$ fg
ls --color=auto | cat /dev/tty
^C
pbr@mongo:~$ fg
(...emacs takes over the screen again)

In the above, ^Z is working properly.   When I tested with xonsh it worked exactly the same.

I just tested this again (this time with xonsh 0.3.4 and python 3.5.1). Seems to work now.

Okay!  In that case, I'm going to go ahead and close this issue.  If it crops up again, feel free to reopen.

issue is present again.

xonsh --version
xonsh/0.5.2
python --version
Python 3.5.2
(venv3.5) milez@dev-guy-automerge:~/AutoMerge$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

hey @guyromm, thanks for reporting! We are aware of the issue and it's being addressed on  #2071,
any more information is welcomed!

Commit message: black

Patch to review:
diff --git a/news/ctrlz.rst b/news/ctrlz.rst
new file mode 100644
index 0000000000..f9f43123e3
--- /dev/null
+++ b/news/ctrlz.rst
@@ -0,0 +1,27 @@
+**Added:**
+
+* New ``$TRHEAD_SUBPROCS`` environment variable allows you to
+  specify wheather threadable subprocesses should actually be
+  run in a thread or not.  Default ``True``.
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Pressing ``Ctrl+Z`` no longer deadlocks the terminal,
+  allowing further input from the user, even for threaded
+  subprocesses.
+
+**Security:**
+
+* <news item>
diff --git a/tests/test_builtins.py b/tests/test_builtins.py
index 29950df65d..36dcd2c594 100644
--- a/tests/test_builtins.py
+++ b/tests/test_builtins.py
@@ -6,6 +6,7 @@
 import builtins
 import types
 from ast import AST, Module, Interactive, Expression
+from subprocess import Popen
 
 import pytest
 
@@ -25,8 +26,10 @@
     in_macro_call,
     call_macro,
     enter_macro,
+    cmds_to_specs,
 )
 from xonsh.environ import Env
+from xonsh.proc import PopenThread, ProcProxy, ProcProxyThread
 
 from tools import skip_if_on_windows
 
@@ -388,3 +391,28 @@ def test_enter_macro():
     assert obj.macro_block == "wakka"
     assert obj.macro_globals
     assert obj.macro_locals
+
+
+@skip_if_on_windows
+def test_cmds_to_specs_thread_subproc(xonsh_builtins):
+    env = xonsh_builtins.__xonsh__.env
+    cmds = [["pwd"]]
+    # First check that threadable subprocs become threadable
+    env['THREAD_SUBPROCS'] = True
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is PopenThread
+    # turn off threading and check we use Popen
+    env['THREAD_SUBPROCS'] = False
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is Popen
+
+    # now check the threadbility of callable aliases
+    cmds = [[lambda: "Keras Selyrian"]]
+    # check that threadable alias become threadable
+    env['THREAD_SUBPROCS'] = True
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is ProcProxyThread
+    # turn off threading and check we use ProcProxy
+    env['THREAD_SUBPROCS'] = False
+    specs = cmds_to_specs(cmds, captured='hiddenobject')
+    assert specs[0].cls is ProcProxy
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
index 586c0ad993..2794a2263d 100644
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -738,7 +738,10 @@ def resolve_alias_cls(self):
         if not callable(alias):
             return
         self.is_proxy = True
-        thable = getattr(alias, "__xonsh_threadable__", True)
+        env = builtins.__xonsh__.env
+        thable = env.get("THREAD_SUBPROCS") and getattr(
+            alias, "__xonsh_threadable__", True
+        )
         cls = ProcProxyThread if thable else ProcProxy
         self.cls = cls
         self.threadable = thable
@@ -790,6 +793,7 @@ def _safe_pipe_properties(fd, use_tty=False):
 
 
 def _update_last_spec(last):
+    env = builtins.__xonsh__.env
     captured = last.captured
     last.last_in_pipeline = True
     if not captured:
@@ -799,9 +803,11 @@ def _update_last_spec(last):
         pass
     else:
         cmds_cache = builtins.__xonsh__.commands_cache
-        thable = cmds_cache.predict_threadable(
-            last.args
-        ) and cmds_cache.predict_threadable(last.cmd)
+        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:
diff --git a/xonsh/environ.py b/xonsh/environ.py
index 500ce14ee3..e3dba35e0e 100644
--- a/xonsh/environ.py
+++ b/xonsh/environ.py
@@ -571,6 +571,7 @@ def DEFAULT_ENSURERS():
         "SUGGEST_MAX_NUM": (is_int, int, str),
         "SUGGEST_THRESHOLD": (is_int, int, str),
         "SUPPRESS_BRANCH_TIMEOUT_MESSAGE": (is_bool, to_bool, bool_to_str),
+        "THREAD_SUBPROCS": (is_bool, to_bool, bool_to_str),
         "UPDATE_COMPLETIONS_ON_KEYPRESS": (is_bool, to_bool, bool_to_str),
         "UPDATE_OS_ENVIRON": (is_bool, to_bool, bool_to_str),
         "UPDATE_PROMPT_ON_KEYPRESS": (is_bool, to_bool, bool_to_str),
@@ -755,6 +756,7 @@ def DEFAULT_VALUES():
         "SUGGEST_COMMANDS": True,
         "SUGGEST_MAX_NUM": 5,
         "SUGGEST_THRESHOLD": 3,
+        "THREAD_SUBPROCS": True,
         "TITLE": DEFAULT_TITLE,
         "UPDATE_COMPLETIONS_ON_KEYPRESS": False,
         "UPDATE_OS_ENVIRON": False,
@@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs 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"
+            "* Thredable commands are run with ``PopenThread`` and threadable \n"
+            "  alias 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 yields to job control.\n"
+            "* Thredable 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."
+        ),
         "TITLE": VarDocs(
             "The title text for the window in which xonsh is running. Formatted "
             "in the same manner as ``$PROMPT``, see 'Customizing the Prompt' "
diff --git a/xonsh/jobs.py b/xonsh/jobs.py
index ec984db1c4..7994119fa6 100644
--- a/xonsh/jobs.py
+++ b/xonsh/jobs.py
@@ -202,7 +202,7 @@ def wait_for_active_job(last_task=None, backgrounded=False, return_error=False):
                     last_task=active_task, backgrounded=backgrounded
                 )
         if os.WIFSTOPPED(wcode):
-            print("^Z")
+            print()
             active_task["status"] = "stopped"
             backgrounded = True
         elif os.WIFSIGNALED(wcode):
diff --git a/xonsh/proc.py b/xonsh/proc.py
index 9c778e9cff..3c2899fe59 100644
--- a/xonsh/proc.py
+++ b/xonsh/proc.py
@@ -554,6 +554,8 @@ def __init__(self, *args, stdin=None, stdout=None, stderr=None, **kwargs):
         self.timeout = env.get("XONSH_PROC_FREQUENCY")
         self.in_alt_mode = False
         self.stdin_mode = None
+        self._tc_cc_vsusp = b"\x1a"  # default is usually ^Z
+        self._disable_suspend_keybind()
         # stdout setup
         self.orig_stdout = stdout
         self.stdout_fd = 1 if stdout is None else stdout.fileno()
@@ -820,6 +822,33 @@ def _restore_sigtstp(self, frame=None):
             self.old_tstp_handler = None
         if frame is not None:
             self._disable_cbreak_stdin()
+        self._restore_suspend_keybind()
+
+    def _disable_suspend_keybind(self):
+        if ON_WINDOWS:
+            return
+        try:
+            mode = termios.tcgetattr(0)  # only makes sense for stdin
+        except termios.error:
+            return
+        self._tc_cc_vsusp = mode[CC][termios.VSUSP]
+        mode[CC][termios.VSUSP] = b"\x00"  # set ^Z (ie SIGSTOP) to undefined
+        termios.tcsetattr(0, termios.TCSANOW, mode)
+
+    def _restore_suspend_keybind(self):
+        if ON_WINDOWS:
+            return
+        try:
+            mode = termios.tcgetattr(0)  # only makes sense for stdin
+        except termios.error:
+            return
+        mode[CC][termios.VSUSP] = self._tc_cc_vsusp  # set ^Z (ie SIGSTOP) to original
+        try:
+            # this usually doesn't work in interactive mode,
+            # but we should try it anyway.
+            termios.tcsetattr(0, termios.TCSANOW, mode)
+        except termios.error:
+            pass
 
     #
     # SIGQUIT handler
@@ -1749,6 +1778,7 @@ def SIGNAL_MESSAGES():
         signal.SIGILL: "Illegal instructions",
         signal.SIGTERM: "Terminated",
         signal.SIGSEGV: "Segmentation fault",
+        signal.SIGTSTP: "Stopped",
     }
     if ON_POSIX:
         sm.update(
diff --git a/xonsh/ptk2/key_bindings.py b/xonsh/ptk2/key_bindings.py
index c3e78dc0ad..c68f9acb9c 100644
--- a/xonsh/ptk2/key_bindings.py
+++ b/xonsh/ptk2/key_bindings.py
@@ -350,3 +350,10 @@ def wrap_cursor_forward(event):
     @handle(Keys.ControlJ, filter=IsSearching())
     def accept_search(event):
         search.accept_search()
+
+    @handle(Keys.ControlZ)
+    def skip_control_z(event):
+        """Prevents the writing of ^Z to the prompt, if Ctrl+Z was pressed
+        during the previous command.
+        """
+        pass
diff --git a/xonsh/ptk2/shell.py b/xonsh/ptk2/shell.py
index d8d699f507..ec8b3f26e9 100644
--- a/xonsh/ptk2/shell.py
+++ b/xonsh/ptk2/shell.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 """The prompt_toolkit based xonsh shell."""
+import os
 import sys
 import builtins
 from types import MethodType
@@ -8,7 +9,7 @@
 from xonsh.base_shell import BaseShell
 from xonsh.shell import transform_command
 from xonsh.tools import print_exception, carriage_return
-from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS
+from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS, ON_POSIX
 from xonsh.style_tools import partial_color_tokenize, _TokenType, DEFAULT_STYLE_DICT
 from xonsh.lazyimps import pygments, pyghooks, winutils
 from xonsh.pygments_cache import get_all_styles
@@ -366,3 +367,9 @@ def restore_tty_sanity(self):
         #   if not ON_POSIX:
         #       return
         #   sys.stdout.write('\033[9999999C\n')
+        if not ON_POSIX:
+            return
+        stty, _ = builtins.__xonsh__.commands_cache.lazyget("stty", (None, None))
+        if stty is None:
+            return
+        os.system(stty + " sane")
diff --git a/xontrib/bashisms.py b/xontrib/bashisms.py
index 567602aaca..9f620023aa 100644
--- a/xontrib/bashisms.py
+++ b/xontrib/bashisms.py
@@ -2,6 +2,7 @@
 import shlex
 import sys
 import re
+import builtins
 
 
 __all__ = ()
@@ -62,3 +63,4 @@ def alias(args, stdin=None):
 
 
 aliases["alias"] = alias
+builtins.__xonsh__.env["THREAD_SUBPROCS"] = False


Code review comments:
diff_hunk: @@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs 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"
+            "* Thredable commands are run with ``PopenThread`` and threadable \n"
path: xonsh/environ.py
text: @user1:
```suggestion
            "* Threadable commands are run with ``PopenThread`` and threadable \n"
```

diff_hunk: @@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs 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"
+            "* Thredable commands are run with ``PopenThread`` and threadable \n"
+            "  alias 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 yields to job control.\n"
path: xonsh/environ.py
text: @user1:
There's a word missing here, I think...

diff_hunk: @@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs 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"
+            "* Thredable commands are run with ``PopenThread`` and threadable \n"
+            "  alias 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 yields to job control.\n"
+            "* Thredable commands are run with ``Popen`` and threadable \n"
path: xonsh/environ.py
text: @user1:
```suggestion
            "* Threadable commands are run with ``Popen`` and threadable \n"
```

diff_hunk: @@ -202,7 +202,7 @@ def wait_for_active_job(last_task=None, backgrounded=False, return_error=False):
                     last_task=active_task, backgrounded=backgrounded
                 )
         if os.WIFSTOPPED(wcode):
-            print("^Z")
+            print()
path: xonsh/jobs.py
text: @user1:
Is this flushing something out?  Or can it be removed altogether?

@author:
I think it could be removed potentially.

diff_hunk: @@ -0,0 +1,27 @@
+**Added:**
+
+* New ``$TRHEAD_SUBPROCS`` environment variable allows you to
path: news/ctrlz.rst
text: @user1:
```suggestion
* New ``$THREAD_SUBPROCS`` environment variable allows you to
```

diff_hunk: @@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs with ^Z (i.e. ``SIGTSTP``)\n"
path: xonsh/environ.py
text: @user1:
```suggestion
            "* However, stopping threaded subprocs with ^Z (i.e. ``SIGTSTP``)\n"
```

diff_hunk: @@ -0,0 +1,27 @@
+**Added:**
+
+* New ``$TRHEAD_SUBPROCS`` environment variable allows you to
+  specify wheather threadable subprocesses should actually be
path: news/ctrlz.rst
text: @user1:
```suggestion
  specify whether threadable subprocesses should actually be
```

diff_hunk: @@ -1159,6 +1161,26 @@ def DEFAULT_DOCS():
             "not always happen.",
             configurable=False,
         ),
+        "THREAD_SUBPROCS": VarDocs(
+            "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 suprocs 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"
+            "* Thredable commands are run with ``PopenThread`` and threadable \n"
+            "  alias are run with ``ProcProxyThread``.\n\n"
path: xonsh/environ.py
text: @user1:
```suggestion
            "  aliases are run with ``ProcProxyThread``.\n\n"
```