Skip to content

Commit 2cc4eac

Browse files
committed
Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals.
1 parent a9fd1bf commit 2cc4eac

File tree

3 files changed

+44
-3
lines changed

3 files changed

+44
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## 2.5.0 (TBD)
22
* Breaking Change
33
* `cmd2` 2.5 supports Python 3.8+ (removed support for Python 3.6 and 3.7)
4+
* Bug Fixes
5+
* Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals.
46
* Enhancements
57
* Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html)
68
* add `allow_clipboard` initialization parameter and attribute to disable ability to

cmd2/cmd2.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,7 +2411,7 @@ def get_help_topics(self) -> List[str]:
24112411
def sigint_handler(self, signum: int, _: FrameType) -> None:
24122412
"""Signal handler for SIGINTs which typically come from Ctrl-C events.
24132413
2414-
If you need custom SIGINT behavior, then override this function.
2414+
If you need custom SIGINT behavior, then override this method.
24152415
24162416
:param signum: signal number
24172417
:param _: required param for signal handlers
@@ -2430,6 +2430,23 @@ def sigint_handler(self, signum: int, _: FrameType) -> None:
24302430
if raise_interrupt:
24312431
self._raise_keyboard_interrupt()
24322432

2433+
def termination_signal_handler(self, signum: int, _: FrameType) -> None:
2434+
"""
2435+
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
2436+
2437+
SIGHUP - received when terminal window is closed
2438+
SIGTERM - received when this app has been requested to terminate
2439+
2440+
The basic purpose of this method is to call sys.exit() so our exit handler will run
2441+
and save the persistent history file. If you need more complex behavior like killing
2442+
threads and performing cleanup, then override this method.
2443+
2444+
:param signum: signal number
2445+
:param _: required param for signal handlers
2446+
"""
2447+
# POSIX systems add 128 to signal numbers for the exit code
2448+
sys.exit(128 + signum)
2449+
24332450
def _raise_keyboard_interrupt(self) -> None:
24342451
"""Helper function to raise a KeyboardInterrupt"""
24352452
raise KeyboardInterrupt("Got a keyboard interrupt")
@@ -5434,12 +5451,19 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
54345451
if not threading.current_thread() is threading.main_thread():
54355452
raise RuntimeError("cmdloop must be run in the main thread")
54365453

5437-
# Register a SIGINT signal handler for Ctrl+C
5454+
# Register signal handlers
54385455
import signal
54395456

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

5460+
if not sys.platform.startswith('win'):
5461+
original_sighup_handler = signal.getsignal(signal.SIGHUP)
5462+
signal.signal(signal.SIGHUP, self.termination_signal_handler) # type: ignore
5463+
5464+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
5465+
signal.signal(signal.SIGTERM, self.termination_signal_handler) # type: ignore
5466+
54435467
# Grab terminal lock before the command line prompt has been drawn by readline
54445468
self.terminal_lock.acquire()
54455469

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

5475-
# Restore the original signal handler
5499+
# Restore original signal handlers
54765500
signal.signal(signal.SIGINT, original_sigint_handler)
54775501

5502+
if not sys.platform.startswith('win'):
5503+
signal.signal(signal.SIGHUP, original_sighup_handler)
5504+
signal.signal(signal.SIGTERM, original_sigterm_handler)
5505+
54785506
return self.exit_code
54795507

54805508
###

tests/test_cmd2.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,17 @@ def test_raise_keyboard_interrupt(base_app):
10381038
assert 'Got a keyboard interrupt' in str(excinfo.value)
10391039

10401040

1041+
@pytest.mark.skipif(sys.platform.startswith('win'), reason="SIGTERM only handeled on Linux/Mac")
1042+
def test_termination_signal_handler(base_app):
1043+
with pytest.raises(SystemExit) as excinfo:
1044+
base_app.termination_signal_handler(signal.SIGHUP, 1)
1045+
assert excinfo.value.code == signal.SIGHUP + 128
1046+
1047+
with pytest.raises(SystemExit) as excinfo:
1048+
base_app.termination_signal_handler(signal.SIGTERM, 1)
1049+
assert excinfo.value.code == signal.SIGTERM + 128
1050+
1051+
10411052
class HookFailureApp(cmd2.Cmd):
10421053
def __init__(self, *args, **kwargs):
10431054
super().__init__(*args, **kwargs)

0 commit comments

Comments
 (0)