Skip to content

Commit

Permalink
Fixed issue where persistent history file was not saved upon SIGHUP a…
Browse files Browse the repository at this point in the history
…nd SIGTERM signals.
  • Loading branch information
kmvanbrunt committed Sep 12, 2024
1 parent f82ff6e commit 2dbb209
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## 2.5.0 (TBD)
* Breaking Change
* `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7)
* Bug Fixes
* Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals.
* Enhancements
* Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html)
* add `allow_clipboard` initialization parameter and attribute to disable ability to
Expand Down
34 changes: 31 additions & 3 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2411,7 +2411,7 @@ def get_help_topics(self) -> List[str]:
def sigint_handler(self, signum: int, _: FrameType) -> None:
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
If you need custom SIGINT behavior, then override this function.
If you need custom SIGINT behavior, then override this method.
:param signum: signal number
:param _: required param for signal handlers
Expand All @@ -2430,6 +2430,23 @@ def sigint_handler(self, signum: int, _: FrameType) -> None:
if raise_interrupt:
self._raise_keyboard_interrupt()

def termination_signal_handler(self, signum: int, _: FrameType) -> None:
"""
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
SIGHUP - received when terminal window is closed.
SIGTERM - received when this process politely asked to terminate.
The basic purpose of this method is to call sys.exit() so our atexit handler will run
and save the persistent history file. If you need more complex behavior like killing
threads and performing cleanup, then override this method.
:param signum: signal number
:param _: required param for signal handlers
"""
# POSIX systems add 128 to signal numbers for the exit code
sys.exit(128 + signum)

def _raise_keyboard_interrupt(self) -> None:
"""Helper function to raise a KeyboardInterrupt"""
raise KeyboardInterrupt("Got a keyboard interrupt")
Expand Down Expand Up @@ -5434,12 +5451,19 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
if not threading.current_thread() is threading.main_thread():
raise RuntimeError("cmdloop must be run in the main thread")

# Register a SIGINT signal handler for Ctrl+C
# Register signal handlers
import signal

original_sigint_handler = signal.getsignal(signal.SIGINT)
signal.signal(signal.SIGINT, self.sigint_handler) # type: ignore

if not sys.platform.startswith('win'):
original_sighup_handler = signal.getsignal(signal.SIGHUP)
signal.signal(signal.SIGHUP, self.termination_signal_handler) # type: ignore

original_sigterm_handler = signal.getsignal(signal.SIGTERM)
signal.signal(signal.SIGTERM, self.termination_signal_handler) # type: ignore

# Grab terminal lock before the command line prompt has been drawn by readline
self.terminal_lock.acquire()

Expand Down Expand Up @@ -5472,9 +5496,13 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
# This will also zero the lock count in case cmdloop() is called again
self.terminal_lock.release()

# Restore the original signal handler
# Restore original signal handlers
signal.signal(signal.SIGINT, original_sigint_handler)

if not sys.platform.startswith('win'):
signal.signal(signal.SIGHUP, original_sighup_handler)
signal.signal(signal.SIGTERM, original_sigterm_handler)

return self.exit_code

###
Expand Down
11 changes: 11 additions & 0 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,17 @@ def test_raise_keyboard_interrupt(base_app):
assert 'Got a keyboard interrupt' in str(excinfo.value)


@pytest.mark.skipif(sys.platform.startswith('win'), reason="SIGTERM only handeled on Linux/Mac")
def test_termination_signal_handler(base_app):
with pytest.raises(SystemExit) as excinfo:
base_app.termination_signal_handler(signal.SIGHUP, 1)
assert excinfo.value.code == signal.SIGHUP + 128

with pytest.raises(SystemExit) as excinfo:
base_app.termination_signal_handler(signal.SIGTERM, 1)
assert excinfo.value.code == signal.SIGTERM + 128


class HookFailureApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down

0 comments on commit 2dbb209

Please sign in to comment.