Skip to content

Commit 5ee1f15

Browse files
committed
Not handling SIGHUP and SIGTERM on Windows.
1 parent ea60a80 commit 5ee1f15

File tree

6 files changed

+44
-47
lines changed

6 files changed

+44
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Breaking Change
33
* `cmd2` 2.5 supports Python 3.7+ (removed support for Python 3.6)
44
* Bug Fixes
5-
* Fixed issue where persistent history file was not saved upon SIGTERM and SIGHUP signals.
5+
* Fixed issue where persistent history file was not saved upon SIGHUP and SIGTERM signals.
66
* Enhancements
77
* Removed dependency on `attrs` and replaced with [dataclasses](https://docs.python.org/3/library/dataclasses.html)
88
* add `allow_clipboard` initialization parameter and attribute to disable ability to

cmd2/argparse_custom.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,7 @@ class ChoicesProviderFuncBase(Protocol):
352352
Function that returns a list of choices in support of tab completion
353353
"""
354354

355-
def __call__(self) -> List[str]:
356-
... # pragma: no cover
355+
def __call__(self) -> List[str]: ... # pragma: no cover
357356

358357

359358
@runtime_checkable
@@ -362,8 +361,7 @@ class ChoicesProviderFuncWithTokens(Protocol):
362361
Function that returns a list of choices in support of tab completion and accepts a dictionary of prior arguments.
363362
"""
364363

365-
def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]:
366-
... # pragma: no cover
364+
def __call__(self, *, arg_tokens: Dict[str, List[str]] = {}) -> List[str]: ... # pragma: no cover
367365

368366

369367
ChoicesProviderFunc = Union[ChoicesProviderFuncBase, ChoicesProviderFuncWithTokens]
@@ -381,8 +379,7 @@ def __call__(
381379
line: str,
382380
begidx: int,
383381
endidx: int,
384-
) -> List[str]:
385-
... # pragma: no cover
382+
) -> List[str]: ... # pragma: no cover
386383

387384

388385
@runtime_checkable
@@ -400,8 +397,7 @@ def __call__(
400397
endidx: int,
401398
*,
402399
arg_tokens: Dict[str, List[str]] = {},
403-
) -> List[str]:
404-
... # pragma: no cover
400+
) -> List[str]: ... # pragma: no cover
405401

406402

407403
CompleterFunc = Union[CompleterFuncBase, CompleterFuncWithTokens]

cmd2/cmd2.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
854854
)
855855

856856
# iterate through all matching methods
857-
for method_name, method in methods:
857+
for _, method in methods:
858858
subcommand_name: str = getattr(method, constants.SUBCMD_ATTR_NAME)
859859
full_command_name: str = getattr(method, constants.SUBCMD_ATTR_COMMAND)
860860
subcmd_parser_builder = getattr(method, constants.CMD_ATTR_ARGPARSER)
@@ -963,7 +963,7 @@ def _unregister_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
963963
)
964964

965965
# iterate through all matching methods
966-
for method_name, method in methods:
966+
for _, method in methods:
967967
subcommand_name = getattr(method, constants.SUBCMD_ATTR_NAME)
968968
command_name = getattr(method, constants.SUBCMD_ATTR_COMMAND)
969969

@@ -2430,29 +2430,22 @@ def sigint_handler(self, signum: int, _: FrameType) -> None:
24302430
if raise_interrupt:
24312431
self._raise_keyboard_interrupt()
24322432

2433-
def sigterm_handler(self, signum: int, _: FrameType) -> None: # pragma: no cover
2433+
def termination_signal_handler(self, signum: int, _: FrameType) -> None:
24342434
"""
2435-
Signal handler for SIGTERMs which are sent to politely ask this app to terminate.
2435+
Signal handler for SIGHUP and SIGTERM. Only runs on Linux and Mac.
24362436
2437-
If you need custom SIGTERM behavior, then override this method.
2437+
SIGHUP - received when terminal window is closed.
2438+
SIGTERM - received when this process politely asked to terminate.
24382439
2439-
:param signum: signal number
2440-
:param _: required param for signal handlers
2441-
"""
2442-
# Gracefully exit so the persistent history file will be written.
2443-
sys.exit(self.exit_code)
2444-
2445-
def sighup_handler(self, signum: int, _: FrameType) -> None: # pragma: no cover
2446-
"""
2447-
Signal handler for SIGHUPs which are sent when the terminal is closed.
2448-
2449-
If you need custom SIGHUP behavior, then override this method.
2440+
The basic purpose of this method is to call sys.exit() so our atexit 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.
24502443
24512444
:param signum: signal number
24522445
:param _: required param for signal handlers
24532446
"""
2454-
# Gracefully exit so the persistent history file will be written.
2455-
sys.exit(self.exit_code)
2447+
# POSIX systems add 128 to signal numbers for the exit code
2448+
sys.exit(128 + signum)
24562449

24572450
def _raise_keyboard_interrupt(self) -> None:
24582451
"""Helper function to raise a KeyboardInterrupt"""
@@ -5458,17 +5451,18 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
54585451
if not threading.current_thread() is threading.main_thread():
54595452
raise RuntimeError("cmdloop must be run in the main thread")
54605453

5461-
# Register a SIGINT signal handler for Ctrl+C
5454+
# Register signal handlers
54625455
import signal
54635456

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

5467-
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
5468-
signal.signal(signal.SIGTERM, self.sigterm_handler) # type: ignore
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
54695463

5470-
original_sighup_handler = signal.getsignal(signal.SIGHUP)
5471-
signal.signal(signal.SIGHUP, self.sighup_handler) # type: ignore
5464+
original_sigterm_handler = signal.getsignal(signal.SIGTERM)
5465+
signal.signal(signal.SIGTERM, self.termination_signal_handler) # type: ignore
54725466

54735467
# Grab terminal lock before the command line prompt has been drawn by readline
54745468
self.terminal_lock.acquire()
@@ -5502,10 +5496,12 @@ def cmdloop(self, intro: Optional[str] = None) -> int: # type: ignore[override]
55025496
# This will also zero the lock count in case cmdloop() is called again
55035497
self.terminal_lock.release()
55045498

5505-
# Restore the original signal handlers
5499+
# Restore original signal handlers
55065500
signal.signal(signal.SIGINT, original_sigint_handler)
5507-
signal.signal(signal.SIGTERM, original_sigterm_handler)
5508-
signal.signal(signal.SIGHUP, original_sighup_handler)
5501+
5502+
if not sys.platform.startswith('win'):
5503+
signal.signal(signal.SIGHUP, original_sighup_handler)
5504+
signal.signal(signal.SIGTERM, original_sigterm_handler)
55095505

55105506
return self.exit_code
55115507

cmd2/decorators.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,7 @@ def with_argparser(
266266
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
267267
preserve_quotes: bool = False,
268268
with_unknown_args: bool = False,
269-
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]:
270-
... # pragma: no cover
269+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover
271270

272271

273272
@overload
@@ -277,8 +276,7 @@ def with_argparser(
277276
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
278277
preserve_quotes: bool = False,
279278
with_unknown_args: bool = False,
280-
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]:
281-
... # pragma: no cover
279+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]: ... # pragma: no cover
282280

283281

284282
def with_argparser(
@@ -418,8 +416,7 @@ def as_subcommand_to(
418416
*,
419417
help: Optional[str] = None,
420418
aliases: Optional[List[str]] = None,
421-
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]:
422-
... # pragma: no cover
419+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover
423420

424421

425422
@overload
@@ -430,8 +427,7 @@ def as_subcommand_to(
430427
*,
431428
help: Optional[str] = None,
432429
aliases: Optional[List[str]] = None,
433-
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]:
434-
... # pragma: no cover
430+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]: ... # pragma: no cover
435431

436432

437433
def as_subcommand_to(

cmd2/history.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,10 @@ def _zero_based_index(self, onebased: Union[int, str]) -> int:
154154
return result
155155

156156
@overload
157-
def append(self, new: HistoryItem) -> None:
158-
... # pragma: no cover
157+
def append(self, new: HistoryItem) -> None: ... # pragma: no cover
159158

160159
@overload
161-
def append(self, new: Statement) -> None:
162-
... # pragma: no cover
160+
def append(self, new: Statement) -> None: ... # pragma: no cover
163161

164162
def append(self, new: Union[Statement, HistoryItem]) -> None:
165163
"""Append a new statement to the end of the History list.

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)