Skip to content

Commit 175bf00

Browse files
committed
FEATURE-ci-process-cleanup: observe native macOS cleanup failure
1 parent b2657b1 commit 175bf00

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: v3 cleanup diagnostic
2+
on:
3+
pull_request:
4+
branches: [v3]
5+
paths: ['.github/workflows/v3-cleanup-diagnostic.yml']
6+
permissions:
7+
contents: read
8+
jobs:
9+
macos-cleanup:
10+
runs-on: macos-14
11+
timeout-minutes: 5
12+
steps:
13+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
14+
- name: Match Python prerequisite
15+
run: brew install python
16+
- name: Observe actual helper and native zombie signal semantics
17+
timeout-minutes: 2
18+
shell: bash
19+
run: |
20+
cat > "$RUNNER_TEMP/v3-cleanup-native-probe.py" <<'PY'
21+
#!/usr/bin/env python3
22+
"""Bounded observational probe: execute the checked-out helper unchanged."""
23+
import argparse
24+
import importlib.util
25+
import json
26+
import os
27+
from pathlib import Path
28+
import platform
29+
import select
30+
import signal
31+
import sys
32+
import time
33+
import traceback
34+
35+
parser = argparse.ArgumentParser()
36+
parser.add_argument('--source', type=Path, default=Path.cwd())
37+
parser.add_argument('--output', type=Path, required=True)
38+
args = parser.parse_args()
39+
source = args.source.resolve()
40+
args.output.mkdir(parents=True, exist_ok=False)
41+
sys.path.insert(0, str(source / 'scripts/lib'))
42+
import package_commands as commands
43+
spec = importlib.util.spec_from_file_location('audit', source / 'scripts/audit-v3-installed-package.py')
44+
audit = importlib.util.module_from_spec(spec)
45+
spec.loader.exec_module(audit)
46+
47+
receipt = dict(platform=platform.platform(), uname=list(platform.uname()), python=sys.version,
48+
waitid=callable(getattr(os, 'waitid', None)), wnowait=hasattr(os, 'WNOWAIT'),
49+
source=str(source), uid=os.getuid(), observations=[], commands=[], controls=[])
50+
51+
def state(tree, deadline):
52+
members = commands._process_snapshot(deadline)
53+
tree._check_leader(members.get(tree.child.pid))
54+
return dict(pid=tree.child.pid, parent=os.getpid(), returncode=tree.child.returncode,
55+
anchor_exited=tree._anchor(deadline),
56+
group={str(pid): value for pid, value in members.items() if value[1] == tree.child.pid})
57+
58+
original_kill_group = commands._CommandTree._kill_group
59+
60+
def observed_kill_group(tree, deadline):
61+
item = state(tree, deadline)
62+
receipt['observations'].append(item)
63+
try:
64+
result = original_kill_group(tree, deadline)
65+
item['signal'] = 'success'
66+
return result
67+
except BaseException as error:
68+
item.update(signal=type(error).__name__, errno=getattr(error, 'errno', None), error=str(error))
69+
item['after'] = state(tree, deadline)
70+
raise
71+
72+
# Observation only; the original helper performs every signal and cleanup.
73+
commands._CommandTree._kill_group = observed_kill_group
74+
runner = commands.Commands(args.output)
75+
for argv in (['git', 'branch', '--show-current'], [sys.executable, '-c', 'pass'],
76+
[sys.executable, '-c', 'raise SystemExit(7)']):
77+
item = dict(argv=argv)
78+
try:
79+
item['output'] = runner(argv, source, audit.clean_environment(), timeout=3, allow_failure=True)
80+
item['result'] = 'success'
81+
except Exception as error:
82+
item.update(result=type(error).__name__, error=str(error), traceback=traceback.format_exc())
83+
receipt['commands'].append(item)
84+
85+
# A fresh owned live leader is the contrasting permission control.
86+
tree = commands._CommandTree([sys.executable, '-u', '-c', "import time; print('ready', flush=True); time.sleep(30)"], source, os.environ)
87+
deadline = time.monotonic() + 5
88+
control = dict(kind='live-to-zombie')
89+
try:
90+
if not select.select([tree.child.stdout], [], [], 3)[0]:
91+
raise RuntimeError('live control readiness deadline exceeded')
92+
if tree.child.stdout.readline() != b'ready\n':
93+
raise RuntimeError('live control did not become ready')
94+
control['live'] = state(tree, deadline)
95+
original_kill_group(tree, deadline)
96+
control['live_sigkill'] = 'success'
97+
while not tree.exited(deadline):
98+
commands._remaining(deadline)
99+
time.sleep(.001)
100+
control['zombie'] = state(tree, deadline)
101+
for label, operation in (
102+
('zombie_pid_signal_zero', lambda: os.kill(tree.child.pid, 0)),
103+
('zombie_group_signal_zero', lambda: os.killpg(tree.child.pid, 0)),
104+
('zombie_helper_sigkill', lambda: original_kill_group(tree, deadline))):
105+
tree._anchor(deadline)
106+
try:
107+
operation()
108+
control[label] = dict(result='success')
109+
except Exception as error:
110+
control[label] = dict(result=type(error).__name__, errno=getattr(error, 'errno', None), error=str(error))
111+
control['after'] = state(tree, deadline)
112+
except Exception as error:
113+
control.update(error=str(error), traceback=traceback.format_exc())
114+
finally:
115+
# Only our unreaped direct child; never signal after wait/reap.
116+
if tree.child.returncode is None:
117+
tree.child.kill()
118+
tree.child.wait(timeout=2)
119+
tree.child.stdout.close()
120+
receipt['controls'].append(control)
121+
(args.output / 'probe.json').write_text(json.dumps(receipt, indent=2) + '\n')
122+
print(json.dumps(receipt, indent=2), flush=True)
123+
PY
124+
python3 "$RUNNER_TEMP/v3-cleanup-native-probe.py" --source "$GITHUB_WORKSPACE" --output "$RUNNER_TEMP/v3-cleanup-diagnostic"
125+
- if: always()
126+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
127+
with:
128+
name: v3-cleanup-diagnostic-${{ github.sha }}
129+
path: ${{ runner.temp }}/v3-cleanup-diagnostic
130+
if-no-files-found: error

0 commit comments

Comments
 (0)