Skip to content

Commit 98cc6e8

Browse files
authored
Allow subprocess to exit before exiting the parent process (#31)
Fixes #17
1 parent 1e80e9f commit 98cc6e8

5 files changed

Lines changed: 75 additions & 17 deletions

File tree

brainframe/cli/main.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ def main():
3737
args = parser.parse_args(sys.argv[1:2])
3838

3939
# Exit with a clean error when interrupted
40-
def on_sigint(_sig, _frame):
40+
def on_sigint(sig, _frame):
4141
print()
42-
if os_utils.current_command is not None:
43-
os_utils.current_command.send_signal(_sig)
44-
print_utils.fail_translate("portal.interrupted")
42+
if os_utils.current_command.process is None:
43+
print_utils.fail_translate("general.interrupted")
44+
else:
45+
# Let os_utils.run take care of bringing the process down when the current
46+
# command is finished
47+
os_utils.current_command.send_signal(sig)
4548

4649
signal.signal(signal.SIGINT, on_sigint)
4750

brainframe/cli/os_utils.py

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import subprocess
33
import sys
44
from pathlib import Path
5-
from typing import List
5+
from threading import RLock
6+
from typing import List, Optional
67

78
import distro
89
import i18n
@@ -15,7 +16,56 @@
1516
containers agree on it.
1617
"""
1718

18-
current_command = None
19+
20+
class _CurrentCommand:
21+
"""Contains information on the current command being run as a subprocess, if one
22+
exists.
23+
"""
24+
25+
_process: Optional[subprocess.Popen] = None
26+
_lock = RLock()
27+
_interrupted = False
28+
29+
@property
30+
def process(self) -> Optional[subprocess.Popen]:
31+
"""
32+
:return: The currently running subprocess, or None if no subprocess is running
33+
"""
34+
with self._lock:
35+
return self._process
36+
37+
@process.setter
38+
def process(self, value: subprocess.Popen) -> None:
39+
with self._lock:
40+
if self._process is not None and self._process.poll() is None:
41+
# This is never expected to happen, as subprocesses are run in serial
42+
# and in a blocking fashion
43+
raise RuntimeError("Only one process may be run at once")
44+
45+
self._process = value
46+
47+
@property
48+
def interrupted(self) -> bool:
49+
"""
50+
:return: If true, the subprocess was interrupted by a signal
51+
"""
52+
return self._interrupted
53+
54+
def send_signal(self, sig: int):
55+
"""
56+
:param sig: The signal to send to the subprocess
57+
"""
58+
with self._lock:
59+
if self._process is None:
60+
message = (
61+
"Attempted to send a signal when no process was running"
62+
)
63+
raise RuntimeError(message)
64+
self._interrupted = True
65+
self._process.send_signal(sig)
66+
67+
68+
current_command = _CurrentCommand()
1969

2070

2171
def create_group(group_name: str, group_id: int):
@@ -103,13 +153,18 @@ def run(
103153
"""
104154
if print_command:
105155
print_utils.print_color(" ".join(command), print_utils.Color.MAGENTA)
106-
cmd = subprocess.Popen(command, *args, **kwargs)
107-
global current_command
108-
current_command = cmd
109-
cmd.wait()
110-
if cmd.returncode != 0 and exit_on_failure:
111-
sys.exit(cmd.returncode)
112-
return cmd
156+
157+
current_command.process = subprocess.Popen(command, *args, **kwargs)
158+
current_command.process.wait()
159+
160+
if current_command.interrupted:
161+
# A signal was sent to the command before it finished
162+
print_utils.fail_translate("general.interrupted")
163+
elif current_command.process.returncode != 0 and exit_on_failure:
164+
# The command failed during normal execution
165+
sys.exit(current_command.process.returncode)
166+
167+
return current_command.process
113168

114169

115170
_SUPPORTED_DISTROS = {

brainframe/cli/translations/general.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ en:
3333
or try again as root."
3434
missing-defaults-file: "This distribution is missing a defaults file. Please
3535
re-install the latest version of the BrainFrame CLI and try again."
36+
interrupted: "The operation was interrupted"

brainframe/cli/translations/portal.en.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,3 @@ en:
2626
BrainFrame server.\n\n\
2727
To get started with a new BrainFrame instance, run \"brainframe install\". To
2828
see more options, run \"brainframe -h\"."
29-
interrupted: "The operation was interrupted"

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)