Retain aircraft through native landing and qualify bounded input diagnostics

This commit is contained in:
2026-09-19 20:34:18 +02:00
parent f870443b9b
commit 2fba12f987
129 changed files with 50369 additions and 88 deletions
+237 -19
View File
@@ -50,6 +50,8 @@ def gate_options(native, args):
if any(value not in {'-errOrdinaryGateObserve', '-errOrdinaryGateChoices'} for value in tokens):
raise Refused('Unknown native gate observer option')
approach = getattr(args, 'enable_private_approach_proof', False)
if getattr(args, 'enable_private_selection_proof', False) and '-errOrdinaryGateChoices' not in tokens:
raise Refused('Selection requires matching native gate choices')
if approach and '-errOrdinaryGateObserve' not in tokens:
raise Refused('Missing native gate observer nonce')
if '-errOrdinaryGateChoices' in tokens and not approach:
@@ -152,10 +154,10 @@ class RelativeMouse:
raise Refused('Private pointer is unavailable or on another screen')
return {'root': root.value, 'child': child.value, 'root_x': rx.value, 'root_y': ry.value, 'mask': mask.value}
def validate_pointer(self, target, dx, dy):
def validate_pointer(self, target, dx, dy, allowed_mask=0):
position = self.pointer()
geometry = target['geometry']; left, top, right, bottom = target['focused_clipped_bounds']
if position['root'] != geometry['root'] or position['mask'] != 0:
if position['root'] != geometry['root'] or position['mask'] & ~allowed_mask != 0:
raise Refused('Private pointer root or button/modifier state invalid')
for x, y in ((position['root_x'], position['root_y']), (position['root_x']+dx, position['root_y']+dy)):
if not left <= x < right or not top <= y < bottom:
@@ -163,6 +165,7 @@ class RelativeMouse:
child = position['child']
if child and self.query.server_pid(child) != self.owner['pid']:
raise Refused('Pointer child belongs to a foreign native client')
return position
def move(self, dx, dy, guard):
if type(dx) is not int or dx not in (-24, 24) or dy != 0 or type(dy) is not int:
@@ -296,7 +299,7 @@ class Observer:
"""Only records appended after this exact owned console was opened count."""
pattern = re.compile(r'ERR INPUT OBSERVE: run=([A-Za-z0-9]{1,32}) sample=(\d+) real=(\d+) world=(\d+(?:\.\d+)?) (.+)$')
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False, enable_approach=False):
def __init__(self, path, nonce, clock=time.monotonic, enable_look=False, enable_approach=False, enable_selection=False):
self.path, self.nonce, self.clock = path, nonce, clock
self.fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
info = os.fstat(self.fd)
@@ -313,11 +316,16 @@ class Observer:
self.raw = []
self.bytes = 0
self.terminal = False
if enable_selection and not (enable_approach and enable_look):
os.close(self.fd)
raise Refused("Selection observation requires approach and look")
if enable_approach and not enable_look:
os.close(self.fd)
raise Refused("Approach observation requires native look")
self.enable_look = enable_look
self.enable_approach = enable_approach
self.enable_selection = enable_selection
self.choice_previous = 0; self.choice_real = -1; self.selection_previous = None
self.approach_lines = []; self.gate_records = []; self.gate_received = {}
self.gate_complete_end = 0; self.approach_previous = None; self.alignment_previous = None
self.ready_at = self.ready_real = self.queued_real = None
@@ -340,17 +348,28 @@ class Observer:
lines = self.pending.split(b'\n'); self.pending = lines.pop()
for raw in lines:
line = raw.decode('utf-8', 'strict').rstrip('\r')
if self.enable_approach and any(prefix in line for prefix in ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:')):
if len(self.approach_lines) >= 1900:
if self.enable_approach and any(prefix in line for prefix in (('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:', 'ERR GATE CHOICE:') if self.enable_selection else ('ERR INPUT OBSERVE:', 'ERR LOOK OBSERVE:', 'ERR GATE OBSERVE:'))):
if len(self.approach_lines) >= (2400 if self.enable_selection else 1900):
raise Refused('Native approach record bound exceeded')
self.approach_lines.append(line)
if self.enable_selection and 'ERR GATE CHOICE:' in line:
from ordinary_gate_choices import parse_choice_record
record = parse_choice_record(line, self.nonce)
if record['sample'] != self.choice_previous + 1 or record['real_ms'] <= self.choice_real:
raise Refused('Native choice records missing, repeated or reversed')
if not self.gate_records or self.gate_records[-1]['kind'] != 'geometry' or any(record[k] != self.gate_records[-1][k] for k in ('sample', 'real_ms')):
raise Refused('Native choices lack exact preceding gate geometry')
self.choice_previous = record['sample']; self.choice_real = record['real_ms']
self.gate_complete_end = len(self.approach_lines)
self.gate_received.setdefault(record['sample'], self.clock())
continue
if self.enable_approach and 'ERR GATE OBSERVE:' in line:
from ordinary_gate_evidence import parse_gate_record
record = parse_gate_record(line, self.nonce)
self.gate_records.append(record)
if len(self.gate_records) > 902:
raise Refused('Native gate record bound exceeded')
if record['kind'] == 'geometry':
if record['kind'] == 'geometry' and not self.enable_selection:
self.gate_complete_end = len(self.approach_lines)
self.gate_received.setdefault(record['sample'], self.clock())
continue
@@ -425,7 +444,11 @@ class Observer:
any(r['kind'] == 'cancelled' for r in self.gate_records + self.look_records)):
raise Refused('Native approach context is unavailable or ended')
content = '\n'.join(self.approach_lines[:self.gate_complete_end])
data = join_gate_snapshots(content, self.nonce)
if self.enable_selection:
from ordinary_gate_choices import join_choice_snapshots
data = join_choice_snapshots(content, self.nonce)
else:
data = join_gate_snapshots(content, self.nonce)
index = max(data['snapshots'])
geometry = validate_gate_geometry(content, self.nonce, data['ready']['sample'], index)
if not geometry['verified']:
@@ -445,6 +468,9 @@ class Observer:
def alignment_snapshots(self):
return self._snapshot_batch('alignment_previous')
def selection_snapshots(self):
return self._snapshot_batch('selection_previous')
def _snapshot_batch(self, cursor):
_, data = self.approach_data()
snapshots = data['snapshots']; latest = max(snapshots)
@@ -462,6 +488,12 @@ class Observer:
'inventory': native['inventory'], 'phase': action['phase'], 'door_state': geo['door_state'], 'door_control': geo['door_control'],
'geometry': {'nonce': self.nonce, 'basis': 'registered-prison-lock-native-interaction',
**{key: geo[key] for key in ('prison', 'gate', 'gate_forward', 'context', 'radius')}}})
if self.enable_selection:
choice = item['choices']
if cursor != 'approach_previous' and action['context_selected']:
from ordinary_gate_choices import exact_prison_choices
choice = exact_prison_choices(item)
result[-1]['choices'] = {key: choice[key] for key in ('nonce','sample','real_ms','selected_id','selected_class','actions')}
setattr(self, cursor, latest)
return result
@@ -558,10 +590,12 @@ def confirm_release(guard, keyboard, report, key, clock=time.monotonic, sleep=ti
observation['end'] = clock()
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False, alignment=False):
def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time.sleep, mouse=None, approach=False, alignment=False, selection=False):
deadline = clock() + 70
held = None
approach_completed = False
if selection and (not alignment or not approach or mouse is None or not getattr(observer, "enable_selection", False)):
raise Refused("Selection requires explicit alignment and choice capabilities")
if alignment and (not approach or mouse is None):
raise Refused("Alignment requires approach and look capabilities")
report.update(events=[], paired_release_errors=[], sequence_completed=False)
@@ -636,7 +670,10 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
approach_completed = True
if alignment:
notify({'kind': 'approach-finished', 'successful': True})
run_alignment(guard, keyboard, observer, mouse, report, clock, sleep)
if selection:
run_selection(guard, keyboard, observer, mouse, report, clock, sleep)
else:
run_alignment(guard, keyboard, observer, mouse, report, clock, sleep)
report["sequence_completed"] = True
return
pulse('W', 0.30)
@@ -672,7 +709,7 @@ def sequence(guard, keyboard, observer, report, clock=time.monotonic, sleep=time
report['paired_release_errors'].extend(route.get('paired_release_errors', []))
report['unconfirmed_held_key'] = route.get('unconfirmed_held_key')
notify({'kind': 'approach-finished', 'successful': approach_completed if alignment else report['sequence_completed']})
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors']
report['all_attempted_presses_released'] = not report.get('unconfirmed_held_key') and not report['paired_release_errors'] and not report.get('unconfirmed_held_button') and not report.get('wheel_release_errors')
class ApproachKeyboard:
@@ -744,6 +781,92 @@ class AlignmentMouse:
self.report['input_sent'] = True
class SelectionWheel:
"""One button5 pair on the existing private connection; no other input API."""
def __init__(self, mouse, report, clock, sleep, deadline, enabled=False):
self.mouse, self.report, self.clock, self.sleep = mouse, report, clock, sleep
self.deadline, self.enabled, self.attempted = deadline, enabled, False
if enabled is not True:
raise Refused('Selection wheel capability is not enabled')
self.xt = mouse.xt
self.xt.XTestFakeButtonEvent.restype = C.c_int
self.xt.XTestFakeButtonEvent.argtypes = [C.c_void_p, C.c_uint, C.c_int, C.c_ulong]
def pulse(self, button, guard, *, deadline):
if (self.enabled is not True or self.attempted or type(button) is not int or button != 5
or not math.isfinite(deadline) or deadline > self.deadline or self.clock() >= deadline):
raise Refused('Unsupported, repeated or expired wheel trial')
self.attempted = True
report = self.report
report.setdefault('wheel_events', []); report.setdefault('wheel_release_errors', [])
up_attempted = False; pending = False; down_at = up_at = None
mask = 1 << 12 # X11 Button5Mask; no other modifier/button may be held.
def owned(releasing=False):
target = guard.check(); self.mouse.keyboard.require_idle()
self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask if releasing else 0)
target = guard.check()
self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask if releasing else 0)
def send(down, cleanup=False):
nonlocal up_attempted, pending, down_at, up_at
owned(not down)
if not cleanup and self.clock() >= deadline:
raise Refused('Original facing lease expired before wheel request')
if down:
pending = True; report['unconfirmed_held_button'] = button
notify({'kind':'button-pending','button':button})
else:
up_attempted = True
event = {'button':button,'down':down,'monotonic':self.clock(),'sent':False}
if cleanup: event['finally'] = True
report['wheel_events'].append(event)
report['input_attempted'] = True
if not self.xt.XTestFakeButtonEvent(self.mouse.query.d, button, int(down), 0):
raise Refused('Native wheel request refused')
self.mouse.query.x.XFlush(self.mouse.query.d)
event['sent'] = True; report['input_sent'] = True
if down: down_at = event['monotonic']
else: up_at = event['monotonic']
def confirm():
nonlocal pending
end = self.clock() + .35
while True:
target = guard.check(); self.mouse.keyboard.require_idle()
position = self.mouse.validate_pointer(target, 0, 0, allowed_mask=mask)
if self.clock() >= end:
raise Refused('Private wheel release confirmation exceeded bound')
if position['mask'] == 0:
pending = False; report['unconfirmed_held_button'] = None
notify({'kind':'button-release-confirmed','button':button})
return
if position['mask'] != mask or self.clock() >= end:
raise Refused('Private wheel release was not confirmed')
self.sleep(min(.025, end-self.clock()))
try:
send(True)
send(False)
confirm()
if self.clock() >= deadline:
raise Refused('Original facing lease expired before wheel confirmation')
return {'button':button,'down_monotonic':down_at,'up_monotonic':up_at,'released':True}
except Exception as error:
report['wheel_release_errors'].append(str(error))
raise
finally:
if pending and not up_attempted:
try:
# Expiry permits only cleanup of our already attempted button;
# current private ownership remains mandatory, without native state.
send(False, cleanup=True); confirm()
except Exception as error:
report['wheel_release_errors'].append(str(error))
if pending:
report['unconfirmed_held_button'] = button
def run_alignment(guard, keyboard, observer, mouse, report, clock, sleep):
from prison_alignment_proof import perform_alignment
from prison_approach_evidence import validate_approach_evidence
@@ -792,9 +915,75 @@ def run_alignment(guard, keyboard, observer, mouse, report, clock, sleep):
notify({'kind': 'alignment-finished', 'successful': successful})
def run_selection(guard, keyboard, observer, mouse, report, clock, sleep):
from prison_alignment_proof import perform_facing
from prison_alignment_evidence import validate_facing_evidence
from prison_action_selection_proof import perform_selection
from prison_approach_evidence import validate_approach_evidence
if observer.ready_at is None or observer.ready_real is None or observer.queued_real is None:
raise Refused('Original native alignment clocks missing')
original_end = observer.ready_at + (45000 - (observer.ready_real - observer.queued_real))/1000 - .5
started = clock()
deadline = min(started + 8, original_end)
if deadline <= started:
raise Refused('Original observation lifetime unavailable for alignment')
notify({'kind': 'alignment-pending', 'deadline_monotonic': deadline})
successful = False
try:
content, data = observer.approach_data()
route = report.get('approach_proof')
evidence = validate_approach_evidence(content, observer.nonce, report['look_proof'], route)
if not evidence['verified']:
raise Refused('Native approach prerequisite refused: ' + '; '.join(evidence['errors']))
if clock() >= deadline:
raise Refused('Original alignment deadline exceeded during prerequisite validation')
poses = {r['sample']: r for r in observer.look_records if r['kind'] == 'sample'}
look = report['look_proof']
calibration = {'nonce': observer.nonce, 'completed': True, 'pixels': 24,
'baseline_direction': poses[look['baseline_samples'][-1]]['direction'],
'response_direction': poses[look['response_sample']]['direction'],
'ready_real_ms': observer.ready_real, 'ready_monotonic': observer.ready_at,
'observer_start_real_ms': observer.queued_real, 'observer_deadline_monotonic': original_end,
'alignment_deadline_monotonic': deadline}
observer.alignment_previous = evidence['last_sample'] - 1
def alignment_notify(event):
if event.get('kind') in ('facing-pending', 'action-selection-pending'):
notify({'kind':'alignment-pending','deadline_monotonic':event['deadline_monotonic']})
# Native ownership checks remain live, but no keyboard-sending capability
# is handed to this stage. It may only assert that all keys are idle.
perform_facing(guard, keyboard.require_idle, AlignmentMouse(mouse, report, clock, deadline),
observer.alignment_snapshots, route['geometry'], calibration, evidence, report,
clock=clock, sleep=sleep, notify=alignment_notify)
facing = report.get('facing_proof', {})
if not facing.get('completed'):
raise Refused('Facing module did not complete')
deadline = min(deadline, facing['deadline_monotonic'])
content, data = observer.approach_data()
facing_evidence = validate_facing_evidence(content, observer.nonce, look, route, facing)
if not facing_evidence.get('verified'):
raise Refused('Independent native facing prerequisite refused')
if clock() >= deadline:
raise Refused('Original facing lease expired during validation')
observer.selection_previous = facing_evidence['last_sample'] - 1
wheel = SelectionWheel(mouse, report, clock, sleep, deadline, enabled=True)
perform_selection(guard, keyboard.require_idle, wheel, observer.selection_snapshots,
route['geometry'], calibration, facing_evidence, report,
clock=clock, sleep=sleep, notify=alignment_notify)
if not report.get('action_selection_proof', {}).get('completed'):
raise Refused('Selection module did not complete')
guard.check(); keyboard.require_idle()
observer.approach_data()
if clock() >= deadline:
raise Refused('Original alignment deadline exceeded at final observation')
successful = True
finally:
notify({'kind': 'alignment-finished', 'successful': successful})
def arguments(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--enable-private-keyboard-proof', action='store_true', required=True)
p.add_argument('--enable-private-selection-proof', action='store_true', help='After facing, try one button5 wheel selection; never perform the action')
p.add_argument('--enable-private-look-proof', action='store_true')
p.add_argument('--enable-private-alignment-proof', action='store_true', help='After successful approach, attempt stationary native lock selection using horizontal relative mouse only')
p.add_argument('--enable-private-approach-proof', action='store_true', help='After look calibration, replace W/Tab cycle with bounded interior approach only')
@@ -805,6 +994,8 @@ def arguments(argv=None):
p.add_argument('--nonce', required=True)
p.add_argument('command', nargs=argparse.REMAINDER)
args = p.parse_args(argv)
if args.enable_private_selection_proof and not (args.enable_private_alignment_proof and args.enable_private_approach_proof and args.enable_private_look_proof):
p.error('Selection proof requires explicit alignment, approach and look proof')
if args.enable_private_alignment_proof and not (args.enable_private_approach_proof and args.enable_private_look_proof):
p.error("Alignment proof requires explicit approach and look proof")
if args.enable_private_approach_proof and not args.enable_private_look_proof:
@@ -865,6 +1056,7 @@ APPROACH_MODULES = ('prison_approach_proof.py', 'ordinary_gate_evidence.py',
ALIGNMENT_MODULES = ('prison_alignment_proof.py', 'prison_alignment_evidence.py')
SELECTION_MODULES = ('ordinary_gate_choices.py', 'prison_action_selection_proof.py', 'prison_action_selection_evidence.py')
def approach_module_hashes():
@@ -884,6 +1076,8 @@ def report_for_write(report):
names = ['approach_proof']
if report.get('alignment_requested') is True:
names.append('alignment_proof')
if report.get('selection_requested') is True:
names += ['facing_proof', 'action_selection_proof']
for name in names:
route = report.get(name)
if isinstance(route, dict):
@@ -910,6 +1104,7 @@ def main(argv=None):
'look_requested': args.enable_private_look_proof,
'approach_requested': getattr(args, 'enable_private_approach_proof', False),
'alignment_requested': getattr(args, 'enable_private_alignment_proof', False),
'selection_requested': getattr(args, 'enable_private_selection_proof', False),
'sequence_completed': False, 'release_qualified': False, 'gameplay_qualified': False,
'hard_supervision': NOTIFY is not None, 'nonce': args.nonce, 'binding': bound, 'command': command, 'report': str(args.report), 'input_attempted': False,
'helper_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
@@ -919,6 +1114,9 @@ def main(argv=None):
report['approach_module_sha256'] = approach_module_hashes()
if report['alignment_requested']:
report['alignment_module_sha256'] = module_hashes(ALIGNMENT_MODULES)
if report['selection_requested']:
report['selection_module_sha256'] = module_hashes(SELECTION_MODULES)
report.update(wheel_events=[], wheel_release_errors=[], unconfirmed_held_button=None)
fd = os.open(args.report, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
info = os.fstat(fd)
notify({'kind': 'report', 'path': str(args.report), 'device': info.st_dev, 'inode': info.st_ino,
@@ -956,7 +1154,7 @@ def main(argv=None):
raise Refused('New native console deadline exceeded')
report['console'] = str(args.console)
observer = Observer(args.console, args.nonce, enable_look=args.enable_private_look_proof,
enable_approach=getattr(args, "enable_private_approach_proof", False))
enable_approach=getattr(args, "enable_private_approach_proof", False), enable_selection=getattr(args, "enable_private_selection_proof", False))
query = display.X11(bound['display'])
while time.monotonic() < deadline:
if child is not None and child.poll() is not None:
@@ -964,6 +1162,12 @@ def main(argv=None):
observer.poll()
if display.binding(args.host_display, args.prefix) != bound:
raise Refused('Display binding changed during window startup')
# Bind only after the native mission's original player is ready.
# Startup/splash windows may legitimately disappear before then;
# after binding, the existing guard still forbids any replacement.
if observer.events != ['READY']:
time.sleep(.1)
continue
query.query_budget = display.MAX_WINDOWS
owners = [owner for window in query.windows()
if (owner := display.owned_window(window, bound, args.prefix, args.game_executable))]
@@ -984,7 +1188,7 @@ def main(argv=None):
keyboard = Keyboard(query)
keyboard.require_idle()
mouse = RelativeMouse(query, keyboard, owner, enable_approach=getattr(args, "enable_private_approach_proof", False), enable_alignment=getattr(args, "enable_private_alignment_proof", False)) if args.enable_private_look_proof else None
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False), alignment=getattr(args, "enable_private_alignment_proof", False))
sequence(guard, keyboard, observer, report, mouse=mouse, approach=getattr(args, "enable_private_approach_proof", False), alignment=getattr(args, "enable_private_alignment_proof", False), selection=getattr(args, "enable_private_selection_proof", False))
except (OSError, ValueError, UnicodeError, subprocess.SubprocessError) as error:
report['error'] = str(error)
finally:
@@ -1047,7 +1251,7 @@ def stop_exact_child(record, read=display.process, kill=os.kill):
return False
def supervision_failure(record, key, reason):
def supervision_failure(record, key, reason, button=None):
"""Amend only the exact exclusive report created by our now-stopped worker."""
if record is None:
return
@@ -1063,13 +1267,18 @@ def supervision_failure(record, key, reason):
report = dict(record['metadata'])
# Keep the worker's actual refusal; supervisor lifecycle is separate evidence.
key = key or report.get('unconfirmed_held_key')
button = button or report.get('unconfirmed_held_button')
report.setdefault('error', reason)
report.update(supervision_failed=True, supervision_error=reason, sequence_completed=False,
all_attempted_presses_released=key is None and report.get('all_attempted_presses_released', True),
all_attempted_presses_released=key is None and button is None and report.get('all_attempted_presses_released', True),
unconfirmed_held_key=key)
if key is not None:
report['input_attempted'] = True
report.setdefault('paired_release_errors', []).append('Supervisor could not confirm paired key release')
if button is not None:
report['unconfirmed_held_button'] = button
report['input_attempted'] = True
report.setdefault('wheel_release_errors', []).append('Supervisor could not confirm wheel button release')
payload = (json.dumps(report, indent=2) + '\n').encode()
os.lseek(fd, 0, os.SEEK_SET); os.write(fd, payload); os.ftruncate(fd, len(payload)); os.fsync(fd)
finally:
@@ -1091,6 +1300,7 @@ def supervise(argv=None):
previous[signum] = signal.signal(signum, forward)
alignment_deadline = None; alignment_finished = False
approach_deadline = None; approach_finished = False
button_at = None; held_button = None
look_at = None; held_at = None; held_key = None; owned_child = None; report_record = None; proof_finished = False; expired = False
deadline = time.monotonic() + 190
try:
@@ -1121,6 +1331,12 @@ def supervise(argv=None):
look_at = time.monotonic()
elif event and event['kind'] == 'look-finished':
look_at = None
elif event and event['kind'] == 'button-pending':
if button_at is None: button_at = time.monotonic()
held_button = event['button']
elif event and event['kind'] == 'button-release-confirmed':
if event['button'] == held_button:
button_at = None; held_button = None
elif event and event['kind'] == 'press-pending':
held_at = time.monotonic(); held_key = event['key']
elif event and event['kind'] == 'release-confirmed':
@@ -1132,7 +1348,7 @@ def supervise(argv=None):
elif event and event['kind'] == 'proof-finished':
proof_finished = event['successful']
now = time.monotonic()
if received or now >= deadline or (alignment_deadline is not None and not alignment_finished and now >= alignment_deadline) or (approach_deadline is not None and not approach_finished and now >= approach_deadline) or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
if received or now >= deadline or (alignment_deadline is not None and not alignment_finished and now >= alignment_deadline) or (approach_deadline is not None and not approach_finished and now >= approach_deadline) or (button_at is not None and now - button_at > 1.5) or (held_at is not None and now - held_at > 1.5) or (look_at is not None and now - look_at > 4):
expired = True
child.terminate() # asks worker finally to release if still provable
child.join(1)
@@ -1147,18 +1363,20 @@ def supervise(argv=None):
break
if last['kind'] == 'release-confirmed':
held_key = None
elif last['kind'] == 'button-release-confirmed' and last['button'] == held_button:
held_button = None
elif last['kind'] == 'report':
report_record = last
elif last['kind'] == 'proof-finished':
proof_finished = last['successful']
if not (received and proof_finished and held_key is None):
supervision_failure(report_record, held_key, 'Private input worker timed out or was interrupted')
if not (received and proof_finished and held_key is None and held_button is None):
supervision_failure(report_record, held_key, 'Private input worker timed out or was interrupted', button=held_button)
break
child.join(2)
if child.exitcode != 0 and not expired:
stop_exact_child(owned_child)
if not (received and proof_finished and held_key is None):
supervision_failure(report_record, held_key, 'Private input worker exited unsuccessfully')
if not (received and proof_finished and held_key is None and held_button is None):
supervision_failure(report_record, held_key, 'Private input worker exited unsuccessfully', button=held_button)
return 1 if expired or child.exitcode != 0 else 0
finally:
if child.pid and child.is_alive():