mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 00:00:21 +02:00
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Open Frame Control's assistant as a Chromium panel. Ctrl-C closes it and its SSH tunnel.
|
|
|
|
Start ui/server.py first. Requires the platform Chromium Flatpak and zsh on the
|
|
computer (the existing panel launcher). No model endpoint or key is configured.
|
|
"""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
import signal
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--port', type=int, default=47810, help='local Frame Control port')
|
|
parser.add_argument('--frame-port', type=int, default=47812, help='Frame loopback tunnel port')
|
|
args = parser.parse_args()
|
|
alias = os.environ.get('FRAME_ALIAS', 'frame')
|
|
if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*', alias) or any(not 1 <= p <= 65535 for p in (args.port, args.frame_port)):
|
|
parser.error('Invalid alias or port')
|
|
if not shutil.which('zsh'):
|
|
parser.error('The panel launcher requires zsh on this computer')
|
|
sys.path.insert(0, str(ROOT / 'ui'))
|
|
from frame_mcp import Client
|
|
Client('http://127.0.0.1:' + str(args.port), os.environ.get('FRAME_UI_KEY', '1')).request('/api/host')
|
|
profile = '/tmp/frame-control-assistant-' + uuid.uuid4().hex
|
|
log_path = ''
|
|
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt))
|
|
tunnel = subprocess.Popen(['ssh', '-N', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8',
|
|
'-o', 'ExitOnForwardFailure=yes', '-o', 'ServerAliveInterval=15',
|
|
'-o', 'ServerAliveCountMax=2', '-R',
|
|
f'127.0.0.1:{args.frame_port}:127.0.0.1:{args.port}', alias])
|
|
try:
|
|
# Check the forwarded page before starting a browser; no arbitrary sleeps.
|
|
probe = subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8', alias,
|
|
'curl --retry 5 --retry-connrefused --retry-delay 1 --max-time 10 -fsS ' +
|
|
shlex.quote(f'http://127.0.0.1:{args.frame_port}/assistant')],
|
|
stdout=subprocess.DEVNULL, timeout=30)
|
|
if probe.returncode or tunnel.poll() is not None:
|
|
raise RuntimeError('Could not forward Frame Control to the Frame')
|
|
launched = subprocess.run(['zsh', str(ROOT / 'scripts/panel-on-frame.sh'), '--name', 'Frame Control Assistant',
|
|
'org.chromium.Chromium', '--user-data-dir=' + profile, '--no-first-run',
|
|
'--disable-background-networking', '--disable-sync',
|
|
f'--app=http://127.0.0.1:{args.frame_port}/assistant'], check=True, timeout=45, stdout=subprocess.PIPE, text=True)
|
|
print(launched.stdout, end='', flush=True)
|
|
match = re.search(r'log (/tmp/panel-on-frame\.[A-Za-z0-9]+)', launched.stdout)
|
|
if match:
|
|
log_path = match.group(1)
|
|
print('Assistant panel open. Ctrl-C closes this panel and its tunnel.', flush=True)
|
|
tunnel.wait()
|
|
raise RuntimeError('SSH tunnel ended')
|
|
except KeyboardInterrupt:
|
|
return 0
|
|
finally:
|
|
tunnel.terminate()
|
|
try:
|
|
tunnel.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
tunnel.kill()
|
|
tunnel.wait()
|
|
# Only this unique browser profile, never a shared Chromium instance.
|
|
cleanup = '''import os, pathlib, signal, shutil, sys, time
|
|
profile = sys.argv[1]
|
|
needle = ('--user-data-dir=' + profile).encode()
|
|
owned = []
|
|
for p in pathlib.Path('/proc').iterdir():
|
|
try:
|
|
if p.name.isdigit() and p.stat().st_uid == os.getuid() and needle in (p / 'cmdline').read_bytes().split(b'\\0'):
|
|
owned.append(int(p.name))
|
|
except OSError:
|
|
pass
|
|
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
for pid in owned:
|
|
try: os.kill(pid, sig)
|
|
except ProcessLookupError: pass
|
|
time.sleep(.3)
|
|
shutil.rmtree(profile, ignore_errors=True)
|
|
if sys.argv[2]:
|
|
pathlib.Path(sys.argv[2]).unlink(missing_ok=True)
|
|
'''
|
|
result = subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8', alias,
|
|
'python3 - ' + shlex.quote(profile) + ' ' + shlex.quote(log_path)], input=cleanup, text=True, timeout=20)
|
|
if result.returncode:
|
|
print('Cleanup failed; close the assistant panel and remove ' + profile + ' on the Frame.', file=sys.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
sys.exit(main())
|
|
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
print(str(exc), file=sys.stderr)
|
|
sys.exit(1)
|