Skip to content

Commit 5ef7cac

Browse files
authored
Use shutdown-first detached reboot wrappers
* Use shutdown-first detached reboot wrappers * Share detached reboot wrapper * Simplify shutdown reboot wrapper * Address reboot wrapper review comments * Share reboot progress message * Remove redundant shutdown reboot wrapper
1 parent 77e8e1c commit 5ef7cac

6 files changed

Lines changed: 24 additions & 43 deletions

File tree

src/timecapsulesmb/cli/flows.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from timecapsulesmb.cli.runtime import LogCallback, emit_progress
88
from timecapsulesmb.core.net import extract_host
99
from timecapsulesmb.core.errors import system_exit_message
10-
from timecapsulesmb.deploy.executor import remote_request_reboot, remote_request_shutdown_reboot
10+
from timecapsulesmb.deploy.executor import remote_request_reboot
1111
from timecapsulesmb.deploy.verify import (
1212
managed_runtime_ready,
1313
render_managed_runtime_verification,
@@ -26,6 +26,7 @@
2626

2727
REBOOT_UP_TIMEOUT_MESSAGE = "Timed out waiting for SSH after reboot."
2828
ACP_REBOOT_REQUEST_TIMEOUT_SECONDS = 10
29+
SSH_SHUTDOWN_REBOOT_PROGRESS_MESSAGE = "SSH: /bin/sync; /sbin/shutdown -r now (fallback /sbin/reboot)"
2930

3031

3132
def wait_for_tcp_port_state(
@@ -167,8 +168,8 @@ def _request_reboot_via_ssh_shutdown(
167168
connection,
168169
command_context,
169170
log=log,
170-
request_reboot=remote_request_shutdown_reboot,
171-
progress_message="SSH: /sbin/shutdown -r now (fallback /sbin/reboot)",
171+
request_reboot=remote_request_reboot,
172+
progress_message=SSH_SHUTDOWN_REBOOT_PROGRESS_MESSAGE,
172173
)
173174

174175

@@ -178,7 +179,7 @@ def _request_reboot_via_ssh(
178179
*,
179180
log: LogCallback = None,
180181
request_reboot: Callable[[SshConnection], None] | None = None,
181-
progress_message: str = "SSH: /sbin/reboot",
182+
progress_message: str = SSH_SHUTDOWN_REBOOT_PROGRESS_MESSAGE,
182183
) -> None:
183184
command_context.add_debug_fields(ssh_reboot_attempted=True)
184185
emit_progress(log, progress_message)

src/timecapsulesmb/cli/fsck.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from timecapsulesmb.cli.context import CommandContext
99
from timecapsulesmb.cli.flows import observe_reboot_cycle
1010
from timecapsulesmb.cli.runtime import add_config_argument, load_env_config
11+
from timecapsulesmb.deploy.executor import DETACHED_SHUTDOWN_REBOOT_COMMAND
1112
from timecapsulesmb.deploy.planner import DEFAULT_APPLE_MOUNT_WAIT_SECONDS
1213
from timecapsulesmb.device.processes import render_direct_pkill9_by_ucomm, render_direct_pkill9_watchdog
1314
from timecapsulesmb.identity import ensure_install_id
@@ -89,7 +90,7 @@ def build_remote_fsck_script(device: str, mountpoint: str, *, reboot: bool) -> s
8990
lines.extend(
9091
[
9192
"echo '--- reboot ---'",
92-
"/sbin/reboot >/dev/null 2>&1 || true",
93+
DETACHED_SHUTDOWN_REBOOT_COMMAND,
9394
]
9495
)
9596
return "\n".join(lines)

src/timecapsulesmb/deploy/executor.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010
from timecapsulesmb.transport.ssh import SshConnection, run_scp, run_ssh
1111

1212

13-
DETACHED_REBOOT_COMMAND = "/bin/sh -c 'exec </dev/null >/dev/null 2>&1; (/bin/sleep 1; /sbin/reboot) & exit 0'"
1413
DETACHED_SHUTDOWN_REBOOT_COMMAND = (
1514
"/bin/sh -c 'exec </dev/null >/dev/null 2>&1; "
16-
"(/bin/sleep 1; "
17-
"if [ -x /sbin/shutdown ]; then /sbin/shutdown -r now || /sbin/reboot; else /sbin/reboot; fi"
15+
"(/bin/sync; /bin/sleep 1; "
16+
"/sbin/shutdown -r now || /sbin/reboot"
1817
") & exit 0'"
1918
)
2019
REBOOT_REQUEST_TIMEOUT_SECONDS = 30
@@ -120,10 +119,6 @@ def run_remote_actions(connection: SshConnection, actions: Iterable[RemoteAction
120119

121120

122121
def remote_request_reboot(connection: SshConnection) -> None:
123-
run_ssh(connection, DETACHED_REBOOT_COMMAND, check=False, timeout=REBOOT_REQUEST_TIMEOUT_SECONDS)
124-
125-
126-
def remote_request_shutdown_reboot(connection: SshConnection) -> None:
127122
run_ssh(connection, DETACHED_SHUTDOWN_REBOOT_COMMAND, check=False, timeout=REBOOT_REQUEST_TIMEOUT_SECONDS)
128123

129124

tests/test_cli.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -902,14 +902,8 @@ def run_deploy_cli(
902902
mocks.verify_managed_runtime = stack.enter_context(
903903
mock.patch("timecapsulesmb.cli.flows.verify_managed_runtime", return_value=verify_runtime)
904904
)
905-
mocks.remote_request_shutdown_reboot = stack.enter_context(
906-
mock.patch("timecapsulesmb.cli.flows.remote_request_shutdown_reboot", side_effect=reboot_side_effect)
907-
)
908905
mocks.remote_request_reboot = stack.enter_context(
909-
mock.patch(
910-
"timecapsulesmb.cli.flows.remote_request_reboot",
911-
side_effect=AssertionError("deploy should not request legacy SSH reboot directly"),
912-
)
906+
mock.patch("timecapsulesmb.cli.flows.remote_request_reboot", side_effect=reboot_side_effect)
913907
)
914908
mocks.acp_reboot = stack.enter_context(
915909
mock.patch(
@@ -4529,7 +4523,6 @@ def test_deploy_no_reboot_stops_after_upload_phase(self) -> None:
45294523
)
45304524

45314525
self.assertEqual(result.rc, 0)
4532-
result.mocks.remote_request_shutdown_reboot.assert_not_called()
45334526
result.mocks.remote_request_reboot.assert_not_called()
45344527
self.assertEqual(result.mocks.verify_payload_home_conn.call_count, 2)
45354528
result.mocks.flush_remote_filesystem_writes.assert_called_once()
@@ -4546,7 +4539,6 @@ def test_deploy_payload_verification_failure_aborts_before_reboot(self) -> None:
45464539
)
45474540

45484541
self.assertEqual(str(result.exception), "managed payload verification failed at /Volumes/dk2/.samba4: missing smbd")
4549-
result.mocks.remote_request_shutdown_reboot.assert_not_called()
45504542
result.mocks.remote_request_reboot.assert_not_called()
45514543
result.mocks.verify_payload_home_conn.assert_called_once()
45524544
result.mocks.flush_remote_filesystem_writes.assert_not_called()
@@ -4572,7 +4564,6 @@ def test_deploy_post_sync_payload_verification_failure_aborts_before_reboot(self
45724564
"managed payload verification failed at /Volumes/dk2/.samba4: missing payload directory",
45734565
)
45744566
result.mocks.flush_remote_filesystem_writes.assert_called_once()
4575-
result.mocks.remote_request_shutdown_reboot.assert_not_called()
45764567
result.mocks.remote_request_reboot.assert_not_called()
45774568
self.assertEqual(result.mocks.verify_payload_home_conn.call_count, 2)
45784569
telemetry_error = self.telemetry_payload("deploy_finished")["error"]
@@ -4591,7 +4582,6 @@ def test_deploy_declined_reboot_returns_without_rebooting(self) -> None:
45914582

45924583
self.assertEqual(result.rc, 0)
45934584
self.assertIn("Deployment complete without reboot.", result.text)
4594-
result.mocks.remote_request_shutdown_reboot.assert_not_called()
45954585
result.mocks.remote_request_reboot.assert_not_called()
45964586
self.assertEqual(result.mocks.verify_payload_home_conn.call_count, 2)
45974587
result.mocks.flush_remote_filesystem_writes.assert_called_once()
@@ -4610,8 +4600,7 @@ def test_deploy_reboot_timeout_returns_failure(self) -> None:
46104600
self.assertEqual(result.rc, 1)
46114601
self.assertIn("SSH reboot request timed out; checking whether the device is rebooting...", result.text)
46124602
self.assertIn(deploy.REBOOT_NO_DOWN_MESSAGE, result.text)
4613-
result.mocks.remote_request_shutdown_reboot.assert_called_once()
4614-
result.mocks.remote_request_reboot.assert_not_called()
4603+
result.mocks.remote_request_reboot.assert_called_once()
46154604
result.mocks.acp_reboot.assert_not_called()
46164605
result.mocks.verify_managed_runtime.assert_not_called()
46174606

@@ -4673,7 +4662,6 @@ def test_deploy_netbsd4_yes_runs_activation_and_skips_reboot(self) -> None:
46734662
self.assertEqual(result.mocks.run_remote_actions.call_count, 3)
46744663
self.assertEqual(result.mocks.verify_payload_home_conn.call_count, 2)
46754664
result.mocks.flush_remote_filesystem_writes.assert_called_once()
4676-
result.mocks.remote_request_shutdown_reboot.assert_not_called()
46774665
result.mocks.remote_request_reboot.assert_not_called()
46784666
self.assertIn("Activating NetBSD4 payload without reboot.", result.text)
46794667
self.assertIn("NetBSD4 activation complete.", result.text)
@@ -7324,7 +7312,11 @@ def test_fsck_yes_reboots_and_waits_by_default(self) -> None:
73247312
self.assertIn("^wcifsfs$", remote_cmd)
73257313
self.assertIn("umount -f /Volumes/dk2", remote_cmd)
73267314
self.assertIn("fsck_hfs -fy /dev/dk2", remote_cmd)
7315+
self.assertIn("exec </dev/null >/dev/null 2>&1", remote_cmd)
7316+
self.assertIn("/bin/sync; /bin/sleep 1;", remote_cmd)
7317+
self.assertIn("/sbin/shutdown -r now", remote_cmd)
73277318
self.assertIn("/sbin/reboot", remote_cmd)
7319+
self.assertIn(") & exit 0", remote_cmd)
73287320
self.assertEqual(wait_mock.call_args_list[0].kwargs, {"expected_up": False, "timeout_seconds": 90})
73297321
self.assertEqual(wait_mock.call_args_list[1].kwargs, {"expected_up": True, "timeout_seconds": 420})
73307322
text = output.getvalue()

tests/test_cli_flows.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from timecapsulesmb.cli.flows import (
1717
ACP_REBOOT_REQUEST_TIMEOUT_SECONDS,
1818
REBOOT_UP_TIMEOUT_MESSAGE,
19+
SSH_SHUTDOWN_REBOOT_PROGRESS_MESSAGE,
1920
observe_reboot_cycle,
2021
request_deploy_reboot_and_wait,
2122
request_reboot_and_wait,
@@ -152,10 +153,10 @@ def test_request_reboot_and_wait_succeeds_after_acp_reboot_request(self) -> None
152153
self.assertIn("ACP reboot requested.", output.getvalue())
153154
self.assertIn("Waiting for the device to go down...", output.getvalue())
154155

155-
def test_request_deploy_reboot_and_wait_uses_ssh_shutdown_request(self) -> None:
156+
def test_request_deploy_reboot_and_wait_uses_ssh_reboot_request(self) -> None:
156157
command_context = FakeCommandContext()
157158
output = io.StringIO()
158-
with mock.patch("timecapsulesmb.cli.flows.remote_request_shutdown_reboot") as shutdown_reboot_mock:
159+
with mock.patch("timecapsulesmb.cli.flows.remote_request_reboot") as reboot_mock:
159160
with mock.patch("timecapsulesmb.cli.flows.acp_reboot", side_effect=AssertionError("deploy should not use ACP reboot")):
160161
with mock.patch("timecapsulesmb.cli.flows.wait_for_ssh_state_conn", side_effect=[True, True]) as wait_mock:
161162
with redirect_stdout(output):
@@ -166,7 +167,7 @@ def test_request_deploy_reboot_and_wait_uses_ssh_shutdown_request(self) -> None:
166167
)
167168

168169
self.assertTrue(ok)
169-
shutdown_reboot_mock.assert_called_once()
170+
reboot_mock.assert_called_once()
170171
self.assertEqual(wait_mock.call_args_list[0].kwargs, {"expected_up": False, "timeout_seconds": 60})
171172
self.assertEqual(wait_mock.call_args_list[1].kwargs, {"expected_up": True, "timeout_seconds": 240})
172173
self.assertEqual(command_context.finish_fields["reboot_was_attempted"], True)
@@ -268,7 +269,7 @@ def test_request_ssh_reboot_uses_ssh_only_strategy_and_progress_log(self) -> Non
268269
self.assertEqual(command_context.debug_fields["reboot_request_strategy"], "ssh")
269270
self.assertEqual(command_context.debug_fields["ssh_reboot_attempted"], True)
270271
self.assertEqual(command_context.debug_fields["ssh_reboot_succeeded"], True)
271-
self.assertEqual(messages, ["SSH: /sbin/reboot"])
272+
self.assertEqual(messages, [SSH_SHUTDOWN_REBOOT_PROGRESS_MESSAGE])
272273
self.assertIn("SSH reboot requested.", output.getvalue())
273274

274275
def test_request_ssh_reboot_records_timeout_without_raising(self) -> None:

tests/test_deploy_modules.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,12 @@
4040
)
4141
from timecapsulesmb.deploy.dry_run import format_deployment_plan
4242
from timecapsulesmb.deploy.executor import (
43-
DETACHED_REBOOT_COMMAND,
4443
DETACHED_SHUTDOWN_REBOOT_COMMAND,
4544
FLUSH_REMOTE_FILESYSTEMS_COMMAND,
4645
FLUSH_REMOTE_FILESYSTEMS_TIMEOUT_SECONDS,
4746
REBOOT_REQUEST_TIMEOUT_SECONDS,
4847
flush_remote_filesystem_writes,
4948
remote_request_reboot,
50-
remote_request_shutdown_reboot,
5149
remote_uninstall_payload,
5250
upload_deployment_payload,
5351
upload_flash_file,
@@ -297,25 +295,18 @@ def test_remote_request_reboot_uses_explicit_reboot_timeout(self) -> None:
297295
connection = SshConnection("root@10.0.0.2", "pw", "-o foo")
298296
with mock.patch("timecapsulesmb.deploy.executor.run_ssh") as run_ssh_mock:
299297
remote_request_reboot(connection)
300-
run_ssh_mock.assert_called_once_with(
301-
connection,
302-
DETACHED_REBOOT_COMMAND,
303-
check=False,
304-
timeout=REBOOT_REQUEST_TIMEOUT_SECONDS,
305-
)
306-
307-
def test_remote_request_shutdown_reboot_uses_shutdown_with_reboot_fallback(self) -> None:
308-
connection = SshConnection("root@10.0.0.2", "pw", "-o foo")
309-
with mock.patch("timecapsulesmb.deploy.executor.run_ssh") as run_ssh_mock:
310-
remote_request_shutdown_reboot(connection)
311298
run_ssh_mock.assert_called_once_with(
312299
connection,
313300
DETACHED_SHUTDOWN_REBOOT_COMMAND,
314301
check=False,
315302
timeout=REBOOT_REQUEST_TIMEOUT_SECONDS,
316303
)
304+
self.assertIn("exec </dev/null >/dev/null 2>&1", DETACHED_SHUTDOWN_REBOOT_COMMAND)
305+
self.assertIn("/bin/sync; /bin/sleep 1;", DETACHED_SHUTDOWN_REBOOT_COMMAND)
317306
self.assertIn("/sbin/shutdown -r now", DETACHED_SHUTDOWN_REBOOT_COMMAND)
318307
self.assertIn("|| /sbin/reboot", DETACHED_SHUTDOWN_REBOOT_COMMAND)
308+
self.assertNotIn("[ -x /sbin/shutdown ]", DETACHED_SHUTDOWN_REBOOT_COMMAND)
309+
self.assertIn(") & exit 0", DETACHED_SHUTDOWN_REBOOT_COMMAND)
319310

320311
def test_flush_remote_filesystem_writes_syncs_and_waits(self) -> None:
321312
connection = SshConnection("root@10.0.0.2", "pw", "-o foo")

0 commit comments

Comments
 (0)