"""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()