Retain aircraft through native landing and qualify bounded input diagnostics
This commit is contained in:
+237
-19
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user