Files
kefu/deploy/single-instance-tray-20260917/verify_frozen_instance.py
T
2026-09-21 10:34:06 +08:00

95 lines
4.4 KiB
Python

"""Exercise the actual frozen guard in isolated child processes, without a GUI."""
from pathlib import Path
import json, os, subprocess, sys, tempfile, time, types, uuid
from PyInstaller.archive.readers import CArchiveReader
def load_guard(exe):
pyz = CArchiveReader(str(exe)).open_embedded_archive('PYZ.pyz')
module = types.ModuleType('desktop_instance')
module.__file__ = str(exe)
sys.modules[module.__name__] = module
exec(pyz.extract(module.__name__), module.__dict__)
return module.DesktopInstanceGuard
def worker(exe, folder, namespace, number, action):
assert namespace.startswith('frozen-instance-check-')
folder = Path(folder)
guard = load_guard(exe)(namespace=namespace)
(folder / ('ready-' + number)).touch()
deadline = time.monotonic() + 40
while not (folder / 'go').exists():
if time.monotonic() > deadline: raise RuntimeError('start timed out')
time.sleep(.01)
primary = guard.acquire()
temp = folder / ('role-' + number + '.tmp')
temp.write_text(json.dumps({'primary': primary, 'pid': os.getpid()}))
temp.replace(folder / ('role-' + number))
try:
if action == 'signal':
guard.request_activation()
return
while not (folder / 'stop').exists():
if primary and guard.consume_activation():
(folder / ('activated-' + number)).touch()
if time.monotonic() > deadline: raise RuntimeError('stop timed out')
time.sleep(.01)
finally:
guard.close()
def main():
if len(sys.argv) > 1 and sys.argv[1] == '--worker':
return worker(*sys.argv[2:])
artifact = Path(__file__).parent
exe = Path('C:/wechat_rpa/dist/ZhenAI-WeCom-Assistant-v1.4.21/ZhenAI-WeCom-Assistant-v1.4.21.exe')
namespace = 'frozen-instance-check-' + uuid.uuid4().hex
children = []
with tempfile.TemporaryDirectory(prefix='frozen-instance-check-', dir=artifact) as raw:
folder = Path(raw)
def launch(number, action='hold'):
child = subprocess.Popen([sys.executable, '-B', '-X', 'utf8', str(Path(__file__).resolve()),
'--worker', str(exe), str(folder), namespace, str(number), action],
cwd=folder, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW)
children.append(child)
return child
def wait(predicate, label):
deadline = time.monotonic() + 20
while not predicate():
if time.monotonic() > deadline: raise AssertionError(label + ' timed out')
time.sleep(.02)
try:
for number in range(6): launch(number)
wait(lambda: all((folder / f'ready-{n}').exists() for n in range(6)), 'readiness')
(folder / 'go').touch()
wait(lambda: all((folder / f'role-{n}').exists() for n in range(6)), 'election')
roles = [json.loads((folder / f'role-{n}').read_text()) for n in range(6)]
owners = [n for n, role in enumerate(roles) if role['primary']]
assert len(owners) == 1, roles
owner = owners[0]
signaler = launch(6, 'signal')
signaler.communicate(timeout=15)
assert signaler.returncode == 0
assert not json.loads((folder / 'role-6').read_text())['primary']
wait(lambda: (folder / f'activated-{owner}').exists(), 'existing instance activation')
# Only terminate our own synthetic mutex holder, never a user process.
children[owner].kill()
children[owner].communicate(timeout=5)
launch(7)
wait(lambda: (folder / 'role-7').exists(), 'crash recovery')
assert json.loads((folder / 'role-7').read_text())['primary']
report = {'version':'1.4.21','actualFrozenModule':True,'parallelLaunches':6,
'primaryInstances':1,'activationPassed':True,'crashRecoveryPassed':True,
'realGuiOpened':False,'realMessagesSent':0,'namespaceIsolated':True}
finally:
(folder / 'stop').touch()
for child in children:
try: child.communicate(timeout=5)
except subprocess.TimeoutExpired:
child.kill(); child.communicate(timeout=5)
(artifact / 'frozen-instance-processes.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps(report))
if __name__ == '__main__': main()