瀏覽代碼

Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg (#516).

Dan Helfman 3 年之前
父節點
當前提交
0c027a3050
共有 3 個文件被更改,包括 56 次插入4 次删除
  1. 1 0
      NEWS
  2. 15 4
      borgmatic/signals.py
  3. 40 0
      tests/unit/test_signals.py

+ 1 - 0
NEWS

@@ -1,4 +1,5 @@
 1.5.25.dev0
+ * #516: Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg.
  * #517: Fix borgmatic exit code (so it's zero) when initial Borg calls fail but later retries
    succeed.
 

+ 15 - 4
borgmatic/signals.py

@@ -1,23 +1,34 @@
+import logging
 import os
 import signal
+import sys
 
+logger = logging.getLogger(__name__)
 
-def _handle_signal(signal_number, frame):  # pragma: no cover
+
+EXIT_CODE_FROM_SIGNAL = 128
+
+
+def handle_signal(signal_number, frame):
     '''
     Send the signal to all processes in borgmatic's process group, which includes child processes.
     '''
     # Prevent infinite signal handler recursion. If the parent frame is this very same handler
     # function, we know we're recursing.
-    if frame.f_back.f_code.co_name == _handle_signal.__name__:
+    if frame.f_back.f_code.co_name == handle_signal.__name__:
         return
 
     os.killpg(os.getpgrp(), signal_number)
 
+    if signal_number == signal.SIGTERM:
+        logger.critical('Exiting due to TERM signal')
+        sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
+
 
-def configure_signals():  # pragma: no cover
+def configure_signals():
     '''
     Configure borgmatic's signal handlers to pass relevant signals through to any child processes
     like Borg. Note that SIGINT gets passed through even without these changes.
     '''
     for signal_number in (signal.SIGHUP, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2):
-        signal.signal(signal_number, _handle_signal)
+        signal.signal(signal_number, handle_signal)

+ 40 - 0
tests/unit/test_signals.py

@@ -0,0 +1,40 @@
+from flexmock import flexmock
+
+from borgmatic import signals as module
+
+
+def test_handle_signal_forwards_to_subprocesses():
+    signal_number = 100
+    frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='something')))
+    process_group = flexmock()
+    flexmock(module.os).should_receive('getpgrp').and_return(process_group)
+    flexmock(module.os).should_receive('killpg').with_args(process_group, signal_number).once()
+
+    module.handle_signal(signal_number, frame)
+
+
+def test_handle_signal_bails_on_recursion():
+    signal_number = 100
+    frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='handle_signal')))
+    flexmock(module.os).should_receive('getpgrp').never()
+    flexmock(module.os).should_receive('killpg').never()
+
+    module.handle_signal(signal_number, frame)
+
+
+def test_handle_signal_exits_on_sigterm():
+    signal_number = module.signal.SIGTERM
+    frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='something')))
+    flexmock(module.os).should_receive('getpgrp').and_return(flexmock)
+    flexmock(module.os).should_receive('killpg')
+    flexmock(module.sys).should_receive('exit').with_args(
+        module.EXIT_CODE_FROM_SIGNAL + signal_number
+    ).once()
+
+    module.handle_signal(signal_number, frame)
+
+
+def test_configure_signals_installs_signal_handlers():
+    flexmock(module.signal).should_receive('signal').at_least().once()
+
+    module.configure_signals()