Repository: xonsh/xonsh

Issue:
SIGHUP signal not raises exit immediately
<!--- Provide a general summary of the issue in the Title above -->
<!--- If you have a question along the lines of "How do I do this Bash command in xonsh"
please first look over the Bash to Xonsh translation guide: https://xon.sh/bash_to_xsh.html
If you don't find an answer there, please do open an issue! -->

I'm not sure if this is an alacritty issue, or a xonsh issue, so I'm starting here.
When using alacritty in single-instance mode, xonsh processes remain if their parent window is closed by the window manager (e.g. Meta+Q for Sway). I've only been able to find this issue when using both alacritty in single-instance mode and xonsh. Alacritty single-instance with Zsh works as expected, alacritty individual instances works with xonsh, and xonsh with foot's single-instance mode works. The issue also does not occur if xonsh has a parent process other than alacritty (e.g. alacritty -> zsh -> xonsh)

## Expected Behavior
<!--- Tell us what should happen -->
Xonsh processes should exit when parent window closes.

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- If part of your bug report is a traceback, please first enter debug mode before triggering the error
To enter debug mode, set the environment variable `XONSH_DEBUG=1` _before_ starting `xonsh`.
On Linux and OSX, an easy way to to do this is to run `env XONSH_DEBUG=1 xonsh` -->

Xonsh process remains when parent window is closed.

## Steps to Reproduce
<!--- Please try to write out a minimal reproducible snippet to trigger the bug, it will help us fix it! -->

1. `export SHELL=$(which xonsh)` if xonsh isn't your default shell.
2. Start an alacritty instance with `alacritty --socket /tmp/alacritty.sock`
3. Create a second alacritty window with `alacritty msg --socket /tmp/alacritty.sock create-window`. This creates a new terminal attached to the same instance of alacritty. Notice now there are two xonsh processes attached to the same alacritty process.
4. (optional) run some command in the second terminal window to help identify it (e.g. man bash).
5. Close the second terminal window using the window manager instead of exiting xonsh directly. Notice there are still two xonsh processes attached to the alacritty process.


## xonfig

<details>

```
+------------------+----------------------------------+
| xonsh            | 0.16.0                           |
| Python           | 3.11.9                           |
| PLY              | 3.11                             |
| have readline    | True                             |
| prompt toolkit   | 3.0.40                           |
| shell type       | prompt_toolkit                   |
| history backend  | sqlite                           |
| pygments         | 2.17.2                           |
| on posix         | True                             |
| on linux         | True                             |
| distro           | unknown                          |
| on wsl           | False                            |
| on darwin        | False                            |
| on windows       | False                            |
| on cygwin        | False                            |
| on msys2         | False                            |
| is superuser     | False                            |
| default encoding | utf-8                            |
| xonsh encoding   | utf-8                            |
| encoding errors  | surrogateescape                  |
| xontrib 1        | vox                              |
| xontrib 2        | coreutils                        |
| xontrib 3        | voxapi                           |
| RC file 1        | /etc/xonsh/xonshrc               |
| RC file 2        | /home/wombat/.config/xonsh/rc.xsh |
+------------------+----------------------------------+
```

</details>

### System
Distro: Nixos
WM: Sway 1.9
Alacritty: 0.13.2

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**


Discussion:
FlyingWombat: Tried this out, and while it is exiting when sent SIGHUP now, it doesn't always exit properly. What I noticed is, after xonsh exits, zsh no longer recognizes Ctrl+C.

When `kill -SIGHUP 1234` is run from xonsh itself, it exists correctly (zsh works fine after). But running `kill -SIGHUP 1234` from another terminal running e.g. zsh, goes like this:
```
# running zsh, launch xonsh
xonsh on  sig_exit_fix [?] via 🐍 v3.12.3 (.venv)
❯ xonsh

# send SIGHUP
(.venv) wombat@wombat-pc ~/builds/dev/xonsh sig_exit_fix @ 
%

xonsh on  sig_exit_fix [?] via 🐍 v3.12.3 (.venv) took 14s
❯ ^C^C
# zsh no longer recognizes Ctrl+C

```
If I do the same thing with bash, zsh is working properly afterwards.
FlyingWombat: The new revision is working for me. Xonsh is now accepting exit signals and exiting properly.

Patch:
diff --git a/news/funcalias.rst b/news/funcalias.rst
new file mode 100644
index 00000000..895388b7
--- /dev/null
+++ b/news/funcalias.rst
@@ -0,0 +1,24 @@
+**Added:**
+
+* Added FuncAlias to process callable aliases.
+* Added alias name printing in case of exception in alias.
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Fixed showing alias description using superhelp e.g. ``which?``.
+
+**Security:**
+
+* <news item>
diff --git a/news/sig_exit_fix.rst b/news/sig_exit_fix.rst
new file mode 100644
index 00000000..db9c7ee9
--- /dev/null
+++ b/news/sig_exit_fix.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Fixed processing exit signals and exceptions (e.g. SIGHUP in #5381) to provide careful exiting with right exit code and TTY cleaning.
+
+**Security:**
+
+* <news item>
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
index e1b59c9f..1ebdf31a 100644
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -54,6 +54,37 @@ def EXEC_ALIAS_RE():
     return re.compile(r"@\(|\$\(|!\(|\$\[|!\[|\&\&|\|\||\s+and\s+|\s+or\s+|[>|<]")
 
 
+class FuncAlias:
+    """Provides a callable alias for xonsh commands."""
+
+    attributes_show = ["__xonsh_threadable__", "__xonsh_capturable__"]
+    attributes_inherit = attributes_show + ["__doc__"]
+
+    def __init__(self, name, func):
+        self.__name__ = self.name = name
+        self.func = func
+        for attr in self.attributes_inherit:
+            if (val := getattr(func, attr, None)) is not None:
+                self.__setattr__(attr, val)
+
+    def __repr__(self):
+        r = {"name": self.name, "func": self.func.__name__}
+        r |= {
+            attr: val
+            for attr in self.attributes_show
+            if (val := getattr(self, attr, None)) is not None
+        }
+        return f"FuncAlias({repr(r)})"
+
+    def __call__(
+        self, args=None, stdin=None, stdout=None, stderr=None, spec=None, stack=None
+    ):
+        func_args = [args, stdin, stdout, stderr, spec, stack][
+            : len(inspect.signature(self.func).parameters)
+        ]
+        return self.func(*func_args)
+
+
 class Aliases(cabc.MutableMapping):
     """Represents a location to hold and look up aliases."""
 
@@ -182,6 +213,8 @@ class Aliases(cabc.MutableMapping):
             else:
                 # need to exec alias
                 self._raw[key] = ExecAlias(val, filename=f)
+        elif isinstance(val, types.FunctionType):
+            self._raw[key] = FuncAlias(key, val)
         else:
             self._raw[key] = val
 
@@ -225,7 +258,7 @@ class Aliases(cabc.MutableMapping):
 
 
 class ExecAlias:
-    """Provides a callable alias for xonsh source code."""
+    """Provides an exec alias for xonsh source code."""
 
     def __init__(self, src, filename="<exec-alias>"):
         """
diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py
index 2dec238b..0ad7e93c 100644
--- a/xonsh/built_ins.py
+++ b/xonsh/built_ins.py
@@ -53,15 +53,20 @@ def resetting_signal_handle(sig, f):
     """Sets a new signal handle that will automatically restore the old value
     once the new handle is finished.
     """
-    oldh = signal.getsignal(sig)
+    prev_signal_handler = signal.getsignal(sig)
 
-    def newh(s=None, frame=None):
+    def new_signal_handler(s=None, frame=None):
         f(s, frame)
-        signal.signal(sig, oldh)
+        signal.signal(sig, prev_signal_handler)
         if sig != 0:
+            """
+            There is no immediate exiting here.
+            The ``sys.exit()`` function raises a ``SystemExit`` exception.
+            This exception must be caught and processed in the upstream code.
+            """
             sys.exit(sig)
 
-    signal.signal(sig, newh)
+    signal.signal(sig, new_signal_handler)
 
 
 def helper(x, name=""):
diff --git a/xonsh/main.py b/xonsh/main.py
index f914db04..00ee5e5e 100644
--- a/xonsh/main.py
+++ b/xonsh/main.py
@@ -577,13 +577,18 @@ def main_xonsh(args):
         else:
             # pass error to finally clause
             exc_info = sys.exc_info()
+    except SystemExit:
+        exc_info = sys.exc_info()
     finally:
         if exc_info != (None, None, None):
             err_type, err, _ = exc_info
             if err_type is SystemExit:
-                raise err
-            print_exception(None, exc_info)
-            exit_code = 1
+                XSH.exit = True
+                code = getattr(exc_info[1], "code", 0)
+                exit_code = int(code) if code is not None else 0
+            else:
+                exit_code = 1
+                print_exception(None, exc_info)
         events.on_exit.fire()
         postmain(args)
     return exit_code
diff --git a/xonsh/procs/proxies.py b/xonsh/procs/proxies.py
index bc9b0767..dcb1d0e8 100644
--- a/xonsh/procs/proxies.py
+++ b/xonsh/procs/proxies.py
@@ -9,7 +9,6 @@ licensed to the Python Software foundation under a Contributor Agreement.
 
 import collections.abc as cabc
 import functools
-import inspect
 import io
 import os
 import signal
@@ -274,7 +273,7 @@ def parse_proxy_return(r, stdout, stderr):
             stdout.write(str(r[0]))
             stdout.flush()
         if rlen > 1 and r[1] is not None:
-            stderr.write(str(r[1]))
+            stderr.write(xt.endswith_newline(str(r[1])))
             stderr.flush()
         if rlen > 2 and isinstance(r[2], int):
             cmd_result = r[2]
@@ -285,69 +284,6 @@ def parse_proxy_return(r, stdout, stderr):
     return cmd_result
 
 
-def proxy_zero(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes no parameters."""
-    return f()
-
-
-def proxy_one(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes one parameter: args"""
-    return f(args)
-
-
-def proxy_two(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes two parameter: args and stdin."""
-    return f(args, stdin)
-
-
-def proxy_three(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes three parameter: args, stdin, stdout."""
-    return f(args, stdin, stdout)
-
-
-def proxy_four(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes four parameter: args, stdin, stdout,
-    and stderr.
-    """
-    return f(args, stdin, stdout, stderr)
-
-
-def proxy_five(f, args, stdin, stdout, stderr, spec, stack):
-    """Calls a proxy function which takes four parameter: args, stdin, stdout,
-    stderr, and spec.
-    """
-    return f(args, stdin, stdout, stderr, spec)
-
-
-PROXIES = (proxy_zero, proxy_one, proxy_two, proxy_three, proxy_four, proxy_five)
-
-
-def partial_proxy(f):
-    """Dispatches the appropriate proxy function based on the number of args."""
-    numargs = 0
-
-    for name, param in inspect.signature(f).parameters.items():
-        # handle *args/**kwargs signature
-        if param.kind in {param.VAR_KEYWORD, param.VAR_POSITIONAL}:
-            numargs = 6
-            break
-        if (
-            param.kind == param.POSITIONAL_ONLY
-            or param.kind == param.POSITIONAL_OR_KEYWORD
-        ):
-            numargs += 1
-        elif name in xt.ALIAS_KWARG_NAMES and param.kind == param.KEYWORD_ONLY:
-            numargs += 1
-    if numargs < 6:
-        return functools.partial(PROXIES[numargs], f)
-    elif numargs == 6:
-        # don't need to partial.
-        return f
-    else:
-        e = "Expected proxy with 6 or fewer arguments for {}, not {}"
-        raise xt.XonshError(e.format(", ".join(xt.ALIAS_KWARG_NAMES), numargs))
-
-
 def get_proc_proxy_name(cls):
     func_name = cls.f
     if type(cls.f) is functools.partial:
@@ -409,8 +345,7 @@ class ProcProxyThread(threading.Thread):
         env : Mapping, optional
             Environment mapping.
         """
-        self.orig_f = f
-        self.f = partial_proxy(f)
+        self.f = f
         self.args = args
         self.pid = None
         self.returncode = None
@@ -799,8 +734,7 @@ class ProcProxy:
         close_fds=False,
         env=None,
     ):
-        self.orig_f = f
-        self.f = partial_proxy(f)
+        self.f = f
         self.args = args
         self.pid = os.getpid()
         self.returncode = None
diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py
index 4751c14e..c0d01d5d 100644
--- a/xonsh/procs/specs.py
+++ b/xonsh/procs/specs.py
@@ -488,10 +488,18 @@ class SubprocSpec:
         except FileNotFoundError as ex:
             cmd0 = self.cmd[0]
             if len(self.cmd) == 1 and cmd0.endswith("?"):
-                with contextlib.suppress(OSError):
-                    return self.cls(
-                        ["man", cmd0.rstrip("?")], bufsize=bufsize, **kwargs
+                cmdq = cmd0.rstrip("?")
+                if cmdq in XSH.aliases:
+                    alias = XSH.aliases[cmdq]
+                    descr = (
+                        repr(alias) + (":\n" + doc)
+                        if (doc := getattr(alias, "__doc__", ""))
+                        else ""
                     )
+                    return self.cls(["echo", descr], bufsize=bufsize, **kwargs)
+                else:
+                    with contextlib.suppress(OSError):
+                        return self.cls(["man", cmdq], bufsize=bufsize, **kwargs)
             e = f"xonsh: subprocess mode: command not found: {repr(cmd0)}"
             env = XSH.env
             sug = xt.suggest_commands(cmd0, env)
@@ -701,7 +709,7 @@ class SubprocSpec:
         if not callable(self.alias):
             return
         # check that we actual need the stack
-        sig = inspect.signature(self.alias)
+        sig = inspect.signature(getattr(self.alias, "func", self.alias))
         if len(sig.parameters) <= 5 and "stack" not in sig.parameters:
             return
         # compute the stack, and filter out these build methods
diff --git a/xonsh/ptk_shell/shell.py b/xonsh/ptk_shell/shell.py
index a14838bd..22b63348 100644
--- a/xonsh/ptk_shell/shell.py
+++ b/xonsh/ptk_shell/shell.py
@@ -7,6 +7,7 @@ from functools import wraps
 from types import MethodType
 
 from prompt_toolkit import ANSI
+from prompt_toolkit.application.current import get_app
 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
 from prompt_toolkit.clipboard import InMemoryClipboard
 from prompt_toolkit.enums import EditingMode
@@ -410,8 +411,12 @@ class PromptToolkitShell(BaseShell):
                     raw_line = line
                     line = self.precmd(line)
                     self.default(line, raw_line)
-            except (KeyboardInterrupt, SystemExit):
+            except (KeyboardInterrupt, SystemExit) as e:
                 self.reset_buffer()
+                if isinstance(e, SystemExit):
+                    get_app().reset()  # Reset TTY mouse and keys handlers.
+                    self.restore_tty_sanity()  # Reset TTY SIGINT handlers.
+                    raise
             except EOFError:
                 if XSH.env.get("IGNOREEOF"):
                     print('Use "exit" to leave the shell.', file=sys.stderr)
@@ -583,8 +588,9 @@ class PromptToolkitShell(BaseShell):
 
     def restore_tty_sanity(self):
         """An interface for resetting the TTY stdin mode. This is highly
-        dependent on the shell backend. Also it is mostly optional since
-        it only affects ^Z backgrounding behaviour.
+        dependent on the shell backend.
+        For prompt-toolkit it allows to fix case when terminal lost
+        SIGINT catching and Ctrl+C is not working after abnormal exiting.
         """
         # PTK does not seem to need any specialization here. However,
         # if it does for some reason in the future...
diff --git a/xonsh/readline_shell.py b/xonsh/readline_shell.py
index e98d4bcf..4cb15d47 100644
--- a/xonsh/readline_shell.py
+++ b/xonsh/readline_shell.py
@@ -618,11 +618,13 @@ class ReadlineShell(BaseShell, cmd.Cmd):
         while not XSH.exit:
             try:
                 self._cmdloop(intro=intro)
-            except (KeyboardInterrupt, SystemExit):
+            except (KeyboardInterrupt, SystemExit) as e:
                 print(file=self.stdout)  # Gives a newline
                 fix_readline_state_after_ctrl_c()
                 self.reset_buffer()
                 intro = None
+                if isinstance(e, SystemExit):
+                    raise
 
     @property
     def prompt(self):
diff --git a/xonsh/tools.py b/xonsh/tools.py
index 6662400e..d65950e3 100644
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -1116,7 +1116,7 @@ def display_colored_error_message(exc_info, strip_xonsh_error_types=True, limit=
     content = traceback.format_exception(*exc_info, limit=limit)
 
     if no_trace_and_raise_subproc_error and "Error:" in content[-1]:
-        content = [content[-1].rstrip()]
+        content = [content[-1]]
 
     traceback_str = "".join([v for v in content])
 
@@ -1133,7 +1133,7 @@ def display_colored_error_message(exc_info, strip_xonsh_error_types=True, limit=
     lexer = pygments.lexers.python.PythonTracebackLexer()
     tokens = list(pygments.lex(traceback_str, lexer=lexer))
     # this goes to stdout, but since we are interactive it doesn't matter
-    print_color(tokens, end="\n", file=sys.stderr)
+    print_color(tokens, end="", file=sys.stderr)
     return
 
 
@@ -2858,3 +2858,8 @@ def unquote(s: str, chars="'\""):
     if len(s) >= 2 and s[0] == s[-1] and s[0] in chars:
         return s[1:-1]
     return s
+
+
+def endswith_newline(s: str):
+    """Force one new line character end to string."""
+    return s.rstrip("\n") + "\n"


Test patch:
diff --git a/tests/procs/test_specs.py b/tests/procs/test_specs.py
index 402707e8..a02bf88c 100644
--- a/tests/procs/test_specs.py
+++ b/tests/procs/test_specs.py
@@ -141,6 +141,7 @@ def test_capture_always(
 
 
 @skip_if_on_windows
+@pytest.mark.flaky(reruns=3, reruns_delay=1)
 def test_interrupted_process_returncode(xonsh_session):
     xonsh_session.env["RAISE_SUBPROC_ERROR"] = False
     cmd = [["python", "-c", "import os, signal; os.kill(os.getpid(), signal.SIGINT)"]]
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
index da2b1eac..a2c0e45a 100644
--- a/tests/test_aliases.py
+++ b/tests/test_aliases.py
@@ -9,7 +9,7 @@ import pytest
 from xonsh.aliases import Aliases, ExecAlias
 
 
-def cd(args, stdin=None, **kwargs):
+def cd(args, stdin=None):
     return args
 
 
@@ -30,10 +30,11 @@ def test_imports(xession):
         "o": ["omg", "lala"],
         "ls": ["ls", "-  -"],
         "color_ls": ["ls", "--color=true"],
-        "cd": cd,
+        "cd": "FuncAlias",
         "indirect_cd": ["cd", ".."],
     }
     raw = ales._raw
+    raw["cd"] = type(ales["cd"]).__name__
     assert raw == expected
 
 
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
index deccff8b..bab7ae4a 100644
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -44,12 +44,14 @@ skip_if_no_sleep = pytest.mark.skipif(
 def run_xonsh(
     cmd,
     stdin=sp.PIPE,
+    stdin_cmd=None,
     stdout=sp.PIPE,
     stderr=sp.STDOUT,
     single_command=False,
     interactive=False,
     path=None,
     add_args: list = None,
+    timeout=20,
 ):
     env = dict(os.environ)
     if path is None:
@@ -72,6 +74,7 @@ def run_xonsh(
         input = cmd
     if add_args:
         args += add_args
+
     proc = sp.Popen(
         args,
         env=env,
@@ -81,8 +84,12 @@ def run_xonsh(
         universal_newlines=True,
     )
 
+    if stdin_cmd:
+        proc.stdin.write(stdin_cmd)
+        proc.stdin.flush()
+
     try:
-        out, err = proc.communicate(input=input, timeout=20)
+        out, err = proc.communicate(input=input, timeout=timeout)
     except sp.TimeoutExpired:
         proc.kill()
         raise
@@ -1000,6 +1007,177 @@ def test_run_fail_not_on_path():
     assert out != "Hello world"
 
 
+ALIASES_THREADABLE_PRINT_CASES = [
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: 1/0
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "^f1f1f1\nException in thread.*FuncAlias.*\nZeroDivisionError.*\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: 1/0
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in thread.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: 1/0
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in thread.*\nTraceback.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: 1/0
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in thread.*FuncAlias.*\nTraceback.*\nZeroDivisionError.*\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: (None, "I failed", 2)
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "^f1f1f1\nI failed\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: (None, "I failed", 2)
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: (None, "I failed", 2)
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed.*\nTraceback.*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: (None, "I failed", 2)
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed\nf2f2f2\n$",
+    ),
+]
+
+ALIASES_UNTHREADABLE_PRINT_CASES = [
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: 1/0
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "^f1f1f1\nException in.*FuncAlias.*\nZeroDivisionError.*\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: 1/0
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: 1/0
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in.*\nTraceback.*\nZeroDivisionError: .*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: 1/0
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nException in.*FuncAlias.*\nTraceback.*\nZeroDivisionError.*\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: (None, "I failed", 2)
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "^f1f1f1\nI failed\nf2f2f2\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = False
+aliases['f'] = lambda: (None, "I failed", 2)
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = True
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: (None, "I failed", 2)
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed.*\nTraceback.*\nsubprocess.CalledProcessError.*\n$",
+    ),
+    (
+        """
+$RAISE_SUBPROC_ERROR = False
+$XONSH_SHOW_TRACEBACK = True
+aliases['f'] = lambda: (None, "I failed", 2)
+aliases['f'].__xonsh_threadable__ = False
+echo f1f1f1 ; f ; echo f2f2f2
+""",
+        "f1f1f1\nI failed\nf2f2f2\n$",
+    ),
+]
+
+
+@skip_if_on_windows
+@pytest.mark.parametrize(
+    "case", ALIASES_THREADABLE_PRINT_CASES + ALIASES_UNTHREADABLE_PRINT_CASES
+)
+def test_aliases_print(case):
+    cmd, match = case
+    out, err, ret = run_xonsh(cmd=cmd, single_command=False)
+    assert re.match(
+        match, out, re.MULTILINE | re.DOTALL
+    ), f"\nFailed:\n```\n{cmd.strip()}\n```,\nresult: {out!r}\nexpected: {match!r}."
+
+
 @skip_if_on_windows
 @pytest.mark.parametrize("interactive", [True, False])
 def test_raise_subproc_error_with_show_traceback(monkeypatch, interactive):
@@ -1054,3 +1232,23 @@ def test_main_d():
         single_command=True,
     )
     assert out == "dummy\n"
+
+
+def test_catching_system_exit():
+    stdin_cmd = "__import__('sys').exit(2)\n"
+    out, err, ret = run_xonsh(
+        cmd=None, stdin_cmd=stdin_cmd, interactive=True, single_command=False, timeout=3
+    )
+    if ON_WINDOWS:
+        assert ret == 1
+    else:
+        assert ret == 2
+
+
+@skip_if_on_windows
+def test_catching_exit_signal():
+    stdin_cmd = "kill -SIGHUP @(__import__('os').getpid())\n"
+    out, err, ret = run_xonsh(
+        cmd=None, stdin_cmd=stdin_cmd, interactive=True, single_command=False, timeout=3
+    )
+    assert ret > 0
diff --git a/tests/test_tools.py b/tests/test_tools.py
index 56e9baea..eee08cd6 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -2122,7 +2122,7 @@ def test_print_exception_error(xession, capsys):
         match,
         cap.err,
         re.MULTILINE | re.DOTALL,
-    ), f"Assert: {cap.err!r} not matched with {match!r}"
+    ), f"\nAssert: {cap.err!r},\nexpected: {match!r}"
 
     with xession.env.swap(XONSH_SHOW_TRACEBACK=True):
         try:
@@ -2130,9 +2130,9 @@ def test_print_exception_error(xession, capsys):
         except subprocess.CalledProcessError:
             print_exception(msg="MSG")
     cap = capsys.readouterr()
-    match = ".*Traceback.*subprocess.CalledProcessError: Command .* returned non-zero exit status .*\nMSG\n"
+    match = ".*Traceback.*subprocess.CalledProcessError: Command .* returned non-zero exit status .*MSG\n"
     assert re.match(
         match,
         cap.err,
         re.MULTILINE | re.DOTALL,
-    ), f"Assert: {cap.err!r} not matched with {match!r}"
+    ), f"\nAssert: {cap.err!r},\nexpected {match!r}"