#!/usr/bin/env python3 """LOCAL RUNNER — runs on the operator's own machine (Windows), set up ONCE, to make Altium 365 Part Request submission the automatic tail of a skill run. Why this exists: the library-manager skill runs in the cloud, but the Part Request web form must be driven from the operator's signed-in Chrome on their desktop — the cloud can't reach it. So at the end of a run the skill drops the finished files + a `part_requests.json` manifest into an **inbox folder on this machine** (via the device bridge). This runner watches that inbox and, for each manifest: makes sure Chrome is running with a debugging port on the operator's profile (launching it if needed — the persisted login is reused), runs `altium365_part_request.py`, then moves the manifest to a `processed/` folder. Set it up once (Task Scheduler at logon, or a startup shortcut) and every future skill run auto-submits its Part Requests with no further action — as long as this machine is on and the Chrome profile has been signed into the Workspace at least once. Config: a `runner_config.json` next to this file, e.g. { "base": "https://vecmocon-technologies-pvt-ltd.365.altium.com", "inbox": "D:/User Data/Desktop/altium-library-master/_part_request_inbox", "processed": "D:/User Data/Desktop/altium-library-master/_part_request_inbox/processed", "chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe", "user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data", "debug_port": 9222, "chromedriver": null, "review_first": false, "poll_seconds": 30 } Run: python local_runner.py # watch the inbox forever (use for Task Scheduler / startup) python local_runner.py --once # process whatever is in the inbox now, then exit """ import glob, json, os, shutil, socket, subprocess, sys, time HERE = os.path.dirname(os.path.abspath(__file__)) def load_cfg(): with open(os.path.join(HERE, "runner_config.json"), encoding="utf-8") as f: return json.load(f) def _port_open(port): s = socket.socket(); s.settimeout(1) try: s.connect(("127.0.0.1", int(port))); return True except Exception: return False finally: s.close() def ensure_chrome(cfg): """Make sure a Chrome with the debug port is up (reusing the operator's logged-in profile).""" if _port_open(cfg["debug_port"]): return udd = os.path.expandvars(cfg["user_data_dir"]) subprocess.Popen([cfg["chrome"], f"--remote-debugging-port={cfg['debug_port']}", f"--user-data-dir={udd}"]) for _ in range(40): if _port_open(cfg["debug_port"]): time.sleep(2) # give the profile a moment to restore the session return time.sleep(1) raise RuntimeError("Chrome did not expose a debug port — check 'chrome' path and 'debug_port'.") def process(cfg, manifest): ensure_chrome(cfg) cmd = [sys.executable, os.path.join(HERE, "altium365_part_request.py"), "--manifest", manifest, "--base", cfg["base"], "--debugger-address", f"127.0.0.1:{cfg['debug_port']}", "--out", manifest + ".result.json"] if cfg.get("chromedriver"): cmd += ["--chromedriver", cfg["chromedriver"]] # Default is REVIEW mode: fill the form completely (fields + parameters + attachments) and # leave it on screen WITHOUT saving, so the operator reviews and clicks Save themselves. # This is the review-first gate and it works even when the runner runs unattended (unlike a # console "press Enter"). Set "auto_submit": true in the config only to submit without review. if not cfg.get("auto_submit", False): cmd += ["--no-submit"] subprocess.run(cmd, check=False) def main(): cfg = load_cfg() os.makedirs(cfg["inbox"], exist_ok=True) os.makedirs(cfg["processed"], exist_ok=True) once = "--once" in sys.argv print(f"watching {cfg['inbox']} (base {cfg['base']})") while True: for m in sorted(glob.glob(os.path.join(cfg["inbox"], "*.json"))): if m.endswith(".result.json"): continue print("processing", m) try: process(cfg, m) except Exception as e: print(" error:", e) shutil.move(m, os.path.join(cfg["processed"], os.path.basename(m))) if once: break time.sleep(int(cfg.get("poll_seconds", 30))) if __name__ == "__main__": main()