Skip to content

Commit ab3a7fa

Browse files
committed
FEATURE-ci-process-cleanup: Preserve unknown Darwin process states without claiming retirement
Actual native capture showed a complete numeric ps row with state ?< in an unrelated group. Recognize Darwin's unknown Mach state while retaining its membership and conservative non-Z semantics. Strict malformed input and owned leader checks remain unchanged. Full captured stdout failed twice through the actual parser before the minimal whitelist repair and now preserves all 412 rows. Add captured-row, unknown owned-group denial, malformed input, and ownership regressions. Local 26 command/49 installed-consumer contracts and 120 affinity trials pass. Restore temporary full native verification for the new source; prior uncaptured parser errors remain only corroborated same-cause inference.
1 parent 38d6678 commit ab3a7fa

3 files changed

Lines changed: 83 additions & 38 deletions

File tree

‎.github/workflows/v3-cleanup-diagnostic.yml‎

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ jobs:
1313
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
1414
- name: Match Python prerequisite
1515
run: brew install python
16-
- name: Bound the unexplained native snapshot observation
16+
- name: Run native command contracts
1717
timeout-minutes: 2
1818
shell: bash
1919
run: |
2020
mkdir -p "$RUNNER_TEMP/v3-cleanup-contracts"
2121
cat > "$RUNNER_TEMP/v3-cleanup-command-contracts.py" <<'PYTHON'
22-
import hashlib, importlib.util, json, os, subprocess, sys, time, unittest
22+
import hashlib, importlib.util, json, os, subprocess, sys, unittest
2323
from pathlib import Path
2424
source = Path.cwd()
2525
output = Path(os.environ['RUNNER_TEMP'], 'v3-cleanup-contracts')
@@ -60,30 +60,15 @@ jobs:
6060
global current_test
6161
current_test = test.id()
6262
super().startTest(test)
63-
started = time.monotonic()
64-
trials = []
65-
for iteration in range(1, 31):
66-
if time.monotonic() - started >= 60:
67-
break
68-
suite = unittest.TestSuite([contracts.CommandsTest(
69-
'test_owned_zombie_permission_denial_preserves_completed_receipts')])
70-
result = unittest.TextTestRunner(verbosity=2, resultclass=Result, failfast=True).run(suite)
71-
trial = dict(iteration=iteration, tests_run=result.testsRun, successful=result.wasSuccessful(),
72-
failures=[test.id() for test, _ in result.failures],
73-
errors=[test.id() for test, _ in result.errors],
74-
skipped=[dict(test=test.id(), reason=reason) for test, reason in result.skipped])
75-
trials.append(trial)
76-
report = dict(mode='bounded-owned-zombie-receipts', max_iterations=30, max_seconds=60,
77-
elapsed_seconds=time.monotonic() - started, trials=trials,
78-
successful=all(item['successful'] for item in trials),
79-
stop='first-failure' if not result.wasSuccessful() else 'running')
80-
(output / 'result.json').write_text(json.dumps(report, indent=2) + '\n')
81-
if not result.wasSuccessful():
82-
break
83-
report['stop'] = ('first-failure' if not report['successful'] else
84-
'iteration-limit' if len(trials) == 30 else 'time-limit')
63+
suite = unittest.defaultTestLoader.loadTestsFromTestCase(contracts.CommandsTest)
64+
expected = [test.id() for test in suite]
65+
result = unittest.TextTestRunner(verbosity=2, resultclass=Result).run(suite)
66+
report = dict(expected=expected, tests_run=result.testsRun, successful=result.wasSuccessful(),
67+
failures=[test.id() for test, _ in result.failures],
68+
errors=[test.id() for test, _ in result.errors],
69+
skipped=[dict(test=test.id(), reason=reason) for test, reason in result.skipped])
8570
(output / 'result.json').write_text(json.dumps(report, indent=2) + '\n')
86-
raise SystemExit(not report['successful'])
71+
raise SystemExit(not result.wasSuccessful())
8772
PYTHON
8873
set +e
8974
python3 "$RUNNER_TEMP/v3-cleanup-command-contracts.py" 2>&1 | tee "$RUNNER_TEMP/v3-cleanup-contracts/commands-test.log"
@@ -225,17 +210,21 @@ jobs:
225210
assert receipt['python'] == binding['python']
226211
assert (contracts / 'exit-status').read_text().strip() == '0'
227212
result = json.loads((contracts / 'result.json').read_text())
228-
assert result['mode'] == 'bounded-owned-zombie-receipts' and result['successful']
229-
assert result['max_iterations'] == 30 and result['max_seconds'] == 60
230-
assert 1 <= len(result['trials']) <= 30
231-
assert result['stop'] in ('iteration-limit', 'time-limit')
232-
for trial in result['trials']:
233-
assert trial['tests_run'] == 1 and trial['successful']
234-
assert not trial['failures'] and not trial['errors'] and not trial['skipped']
235-
# Full 22-contract native GREEN is retained from run 38838310419 for these helper/test hashes.
236-
assert hashes == {
237-
'scripts/lib/package_commands.py': 'e04b86a71ca0fc9085ea2bb527afaba0fc3bf030e0934ef02f78c44d46b344cb',
238-
'scripts/test-v3-installed-consumer.py': 'fa2b3e67ae3f0e5921c98e2dc10fa3653e401f5e0a31b9e4ff219b1d130f3492'}
213+
assert result['successful'] and not result['failures'] and not result['errors']
214+
assert result['tests_run'] == len(result['expected']) == 26
215+
assert len(set(result['expected'])) == 26
216+
assert {
217+
'native_contracts.CommandsTest.test_ps_unknown_state_preserves_complete_numeric_membership',
218+
'native_contracts.CommandsTest.test_ps_unknown_owned_member_cannot_justify_permission_denial',
219+
'native_contracts.CommandsTest.test_ps_unknown_state_does_not_relax_snapshot_validation',
220+
'native_contracts.CommandsTest.test_ps_unknown_state_cannot_hide_missing_or_changed_leader'
221+
} <= set(result['expected'])
222+
expected_skips = {
223+
'native_contracts.CommandsTest.test_cleanup_discovery_failure_retains_original_error',
224+
'native_contracts.CommandsTest.test_new_live_member_delays_retirement_and_zombies_do_not',
225+
'native_contracts.CommandsTest.test_discovery_consumes_cleanup_deadline_without_restarting_wait'}
226+
assert len(result['skipped']) == 3
227+
assert {item['test'] for item in result['skipped']} == expected_skips
239228
assert receipt['uname'][0] == 'Darwin'
240229
assert len(receipt['commands']) == 3
241230
assert all(item['result'] == 'success' for item in receipt['commands'])
@@ -270,7 +259,7 @@ jobs:
270259
assert control['zombie_group_signal_zero']['errno'] == 1
271260
assert control['zombie_helper_sigkill']['result'] == 'success'
272261
(output / 'semantic-proof.json').write_text(json.dumps(dict(status='passed', source_sha=source_sha,
273-
hashes=hashes, command_exits=[row['exit'] for row in records], native_contracts='retained-run-38838310419', bounded_receipt_probe='passed'), indent=2) + '\n')
262+
hashes=hashes, command_exits=[row['exit'] for row in records], native_contracts='passed'), indent=2) + '\n')
274263
PY
275264
- if: always()
276265
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02

‎scripts/lib/package_commands.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,9 @@ def _process_snapshot(deadline):
261261
for line in output.getvalue().decode('ascii').splitlines():
262262
_remaining(deadline)
263263
fields = line.split()
264+
# Darwin can report unknown Mach state '?'; retain it as non-retired.
264265
if (len(fields) != 4 or any(not item.isdigit() for item in fields[:3])
265-
or not fields[3] or fields[3][0] not in 'RSDZTWtXxKIPLU'
266+
or not fields[3] or fields[3][0] not in 'RSDZTWtXxKIPLU?'
266267
or any(flag not in '+<>ACEJLNSsVWX' for flag in fields[3][1:])):
267268
raise RuntimeError('malformed command process snapshot')
268269
pid, parent, group = map(int, fields[:3])

‎scripts/test-v3-installed-consumer.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,61 @@ def launch(argv, *args, **kwargs):
507507
with self.assertRaisesRegex(RuntimeError, 'cleanup.*snapshot'):
508508
self.runner(self.command('pass'), self.root, os.environ, timeout=2)
509509

510+
def ps_output(self, data):
511+
popen = subprocess.Popen
512+
def launch(argv, *args, **kwargs):
513+
if argv[0] in ('/bin/ps', '/usr/bin/ps'):
514+
argv = self.command(f'import os; os.write(1, {data.encode("ascii")!r})')
515+
return popen(argv, *args, **kwargs)
516+
return patch.object(subprocess, 'Popen', launch)
517+
518+
@unittest.skipUnless(os.name == 'posix', 'Darwin unknown scheduling state')
519+
def test_ps_unknown_state_preserves_complete_numeric_membership(self):
520+
# Captured native rows: the unknown process belongs to a different group.
521+
data = ' 2255 2178 2255 Z< \n 2258 910 892 ?< \n'
522+
with patch.object(sys, 'platform', 'darwin'), self.ps_output(data):
523+
members = commands._process_snapshot(time.monotonic() + 2)
524+
self.assertEqual({2255: (2178, 2255, 'Z'), 2258: (910, 892, '?')}, members)
525+
526+
@unittest.skipUnless(os.name == 'posix', 'unknown owned process cannot prove retirement')
527+
def test_ps_unknown_owned_member_cannot_justify_permission_denial(self):
528+
for unknown in ('leader', 'descendant'):
529+
with self.subTest(unknown=unknown):
530+
tree = self.zombie_tree()
531+
pid = tree.child.pid
532+
data = f'{pid} {os.getpid()} {pid} ' + ('?<' if unknown == 'leader' else 'Z') + '\n'
533+
if unknown == 'descendant':
534+
data += f'{pid + 1} {pid} {pid} ?<\n'
535+
with patch.object(sys, 'platform', 'darwin'), patch.object(os, 'waitid', None, create=True), self.ps_output(data):
536+
with patch.object(os, 'killpg', side_effect=PermissionError(1, 'unknown group denied')):
537+
with self.assertRaisesRegex(PermissionError, 'unknown group denied'):
538+
tree._kill_group(time.monotonic() + 2)
539+
self.assertIsNone(tree.child.returncode, 'unknown state cannot reap the ownership anchor')
540+
541+
@unittest.skipUnless(os.name == 'posix', 'strict ps snapshot structure')
542+
def test_ps_unknown_state_does_not_relax_snapshot_validation(self):
543+
for data in ('pid 910 892 ?<', '2258 parent 892 ?<', '2258 910 group ?<',
544+
'-1 910 892 ?<', '0 910 892 ?<', '2258 -1 892 ?<', '2258 910 -1 ?<',
545+
'2258 910 ?<', '2258 910 892 ?< command', '2258 910 892 ?Q',
546+
'2258 910 892 !<', '2258 910 892 ?<\n2258 910 892 Z'):
547+
with self.subTest(data=data):
548+
with patch.object(sys, 'platform', 'darwin'), self.ps_output(data + '\n'):
549+
with self.assertRaisesRegex(RuntimeError, 'snapshot'):
550+
commands._process_snapshot(time.monotonic() + 2)
551+
552+
@unittest.skipUnless(os.name == 'posix', 'strict ps ownership anchor')
553+
def test_ps_unknown_state_cannot_hide_missing_or_changed_leader(self):
554+
tree = self.zombie_tree()
555+
pid = tree.child.pid
556+
for data in (f'{pid + 1} {pid} {pid} ?<',
557+
f'{pid} {os.getpid() + 1} {pid} ?<', f'{pid} {os.getpid()} 0 ?<'):
558+
with self.subTest(data=data):
559+
with patch.object(sys, 'platform', 'darwin'), patch.object(os, 'waitid', None, create=True), self.ps_output(data + '\n'):
560+
with patch.object(os, 'killpg') as kill:
561+
with self.assertRaisesRegex(RuntimeError, 'ownership'):
562+
tree._kill_group(time.monotonic() + 2)
563+
kill.assert_not_called()
564+
510565
@unittest.skipUnless(os.name == 'posix', 'documented BSD/macOS state modifiers')
511566
def test_ps_fallback_accepts_documented_state_modifiers(self):
512567
popen = subprocess.Popen

0 commit comments

Comments
 (0)