mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 00:14:41 +01:00
fix(signals): fix processing exit signals and exit exception (#5399)
### Before Case 1: Handler catches the exit signal and do not pass it forward. As result xonsh could be suspended by OS or crash. Also exit code is wrong. See repeatable examples with SIGHUP in https://github.com/xonsh/xonsh/issues/5381#issuecomment-2097961804. Case 2: From bash/zsh as login shell run xonsh. Then send quit signal to xonsh. The terminal state will be broken: disabled SIGINT, mouse pointer produces mouse state codes. ### After Case 1: Xonsh exit normally with right exit code. Fixed #5381 #5304 #5371. Case 2: After exiting with right exit code the state of the terminal is good. ## For community ⬇️ **Please click the 👍 reaction instead of leaving a `+1` or 👍 comment** --------- Co-authored-by: a <1@1.1> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
bb394a8e84
commit
fd5304fb87
7 changed files with 81 additions and 12 deletions
23
news/sig_exit_fix.rst
Normal file
23
news/sig_exit_fix.rst
Normal file
|
@ -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>
|
|
@ -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)"]]
|
||||
|
|
|
@ -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
|
||||
|
@ -1225,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
|
||||
|
|
|
@ -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=""):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue