Skip to content
This repository was archived by the owner on Oct 30, 2026. It is now read-only.
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
23 changes: 21 additions & 2 deletions src/orchard_cli/apple/boot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Shared checkm8/Pongo boot sequence; adapters supply device identity constraints."""
import os
import signal
import subprocess
import shutil
import time
Expand Down Expand Up @@ -45,6 +47,23 @@ def wait_mode(device, mode, *, chip, board, product, seconds=30):
time.sleep(0.25)


def run_process_group(argv, *, timeout, **kwargs):
"""subprocess.run(check=True) that also stops the command's children.

palera1n posix_spawns checkra1n and only waits for it. Killing palera1n
alone leaves checkra1n running, and it exploits the next DFU device it
sees, so give up on the whole process group instead.
"""
with subprocess.Popen(argv, start_new_session=True, **kwargs) as process:
try:
returncode = process.wait(timeout=timeout)
except BaseException:
os.killpg(process.pid, signal.SIGKILL)
raise
if returncode:
raise subprocess.CalledProcessError(returncode, argv)


def boot(device, assets, manifest, progress, *, profile, chip, board, product):
if device['mode'] == 'linux':
progress.phase('Using running recovery; no reboot or upload (timing starts at resume)')
Expand All @@ -61,8 +80,8 @@ def boot(device, assets, manifest, progress, *, profile, chip, board, product):
with tempfile.NamedTemporaryFile(prefix='palera1n-', suffix='.log', dir=logs,
delete=False) as log:
try:
subprocess.run([executable, '-p', '-k', str(assets['pongo'])],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=120)
run_process_group([executable, '-p', '-k', str(assets['pongo'])],
stdout=log, stderr=subprocess.STDOUT, timeout=120)
except (OSError, subprocess.SubprocessError) as error:
raise RuntimeError('Pongo bootstrap failed or outcome unknown; inspect USB before any retry. '
f'Log: {log.name}') from error
Expand Down
30 changes: 29 additions & 1 deletion tests/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import subprocess
import sys
import tempfile
import time
import unittest
from unittest.mock import Mock, patch
import urllib.request
Expand Down Expand Up @@ -46,7 +47,7 @@ def test_adapter_boot_sequence_timeouts_and_no_retry_are_preserved(self):
patch.object(boot.shutil, 'which', return_value='/tools/palera1n'), \
patch.object(adapter_module.usb, 'selected') as selected, \
patch.object(boot, 'wait_mode') as wait, \
patch.object(boot.subprocess, 'run') as bootstrap, \
patch.object(boot, 'run_process_group') as bootstrap, \
patch.object(boot.pongo, 'upload') as upload:
adapter.boot(device, assets, record, Mock())
self.assertEqual(bootstrap.call_count, 1)
Expand All @@ -62,6 +63,33 @@ def test_adapter_boot_sequence_timeouts_and_no_retry_are_preserved(self):
self.assertEqual(bootstrap.call_count, 2) # one success, one failed call; no retry.
upload.assert_not_called()

def test_bootstrap_timeout_stops_spawned_children(self):
# palera1n posix_spawns checkra1n; a surviving checkra1n would exploit
# the next DFU device behind a later command's back.
from orchard_cli.apple import boot
pidfile = self.root / 'child.pid'
script = f'sleep 300 & echo $! > {pidfile}; wait'
with self.assertRaises(subprocess.TimeoutExpired):
boot.run_process_group(['sh', '-c', script], timeout=1)
child = int(pidfile.read_text())
for _ in range(50):
try:
state = Path(f'/proc/{child}/stat').read_text().rsplit(')', 1)[1].split()[0]
except (FileNotFoundError, ProcessLookupError): # gone mid-read
break
if state == 'Z':
break
time.sleep(0.05)
else:
os.kill(child, 9)
self.fail('bootstrap child outlived the timeout')

def test_bootstrap_failure_is_reported(self):
from orchard_cli.apple import boot
with self.assertRaises(subprocess.CalledProcessError):
boot.run_process_group(['sh', '-c', 'exit 3'], timeout=5)
boot.run_process_group(['true'], timeout=5)

def test_manifest_becomes_existing_workflow_inputs(self):
record, assets = releases.resolve(FIXTURE / 'index.toml', 'j71b')
self.assertEqual(record['release'], 'example-1')
Expand Down