Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/seedsigner/views/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,24 +293,41 @@ def run(self):
from seedsigner.gui.screens.screen import ResetScreen
thread = RestartView.DoResetThread()
thread.start()
self.run_screen(ResetScreen)
try:
self.run_screen(ResetScreen)
except Exception:
# Stop the reset thread if the screen exits abnormally (e.g.
# ScreenshotComplete during screenshot generation). Broad catch
# is intentional: whatever caused the exit, we must prevent the
# background thread from killing the process.
thread.stop()
raise


class DoResetThread(BaseThread):
def run(self):
import os
import shlex
import sys
import time
from subprocess import call

# Give the screen just enough time to display the reset message before
# exiting.
time.sleep(0.25)

# Kill the SeedSigner process; Running the process again.
# `.*` is a wildcard to detect either `python`` or `python3`.
if not self.keep_running:
return

# Kill the current process by its PID (reliable across all
# Python binary names). The shell subprocess survives the
# parent being killed and can then start the new process.
pid = os.getpid()
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
call("kill $(pidof python*) & python /opt/src/main.py", shell=True)
python = shlex.quote(sys.executable)
call(f"kill {pid}; exec {python} /opt/src/main.py", shell=True)
else:
call("kill $(ps aux | grep '[p]ython.*main.py' | awk '{print $2}')", shell=True)
call(f"kill {pid}", shell=True)



Expand Down
60 changes: 60 additions & 0 deletions tests/test_flows_view.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os
import shlex
import sys
from unittest.mock import patch

# Must import test base before the Controller
Expand Down Expand Up @@ -64,3 +67,60 @@ def test_unhandled_exception_flow(self):
FlowStep(UnhandledExceptionView),
FlowStep(MainMenuView),
])


def test_restart_thread_command_seedsigner_os(self):
"""
Verify DoResetThread uses os.getpid() and sys.executable on seedsigner-os.
"""
original_hostname = Settings.HOSTNAME
try:
Settings.HOSTNAME = Settings.SEEDSIGNER_OS
thread = RestartView.DoResetThread()
thread.keep_running = True
with patch('subprocess.call') as mock_subprocess_call, \
patch('time.sleep'):
thread.run()
mock_subprocess_call.assert_called_once()
cmd = mock_subprocess_call.call_args[0][0]
pid = os.getpid()
python = shlex.quote(sys.executable)
assert cmd == f"kill {pid}; exec {python} /opt/src/main.py"
assert mock_subprocess_call.call_args[1] == {"shell": True}
finally:
Settings.HOSTNAME = original_hostname


def test_restart_thread_command_desktop(self):
"""
Verify DoResetThread uses os.getpid() on non-seedsigner-os (desktop).
"""
original_hostname = Settings.HOSTNAME
try:
Settings.HOSTNAME = "desktop-host"
thread = RestartView.DoResetThread()
thread.keep_running = True
with patch('subprocess.call') as mock_subprocess_call, \
patch('time.sleep'):
thread.run()
mock_subprocess_call.assert_called_once()
cmd = mock_subprocess_call.call_args[0][0]
pid = os.getpid()
assert cmd == f"kill {pid}"
assert mock_subprocess_call.call_args[1] == {"shell": True}
finally:
Settings.HOSTNAME = original_hostname


def test_restart_thread_skips_kill_when_stopped(self):
"""
Verify DoResetThread does not execute kill when keep_running is False.
This simulates the screenshot generator scenario where ScreenshotComplete
is raised during screen render, causing RestartView to stop the thread.
"""
thread = RestartView.DoResetThread()
thread.keep_running = False
with patch('subprocess.call') as mock_subprocess_call, \
patch('time.sleep'):
thread.run()
mock_subprocess_call.assert_not_called()
Loading