diff --git a/frontends/desktop_pet.pyw b/frontends/desktop_pet.pyw index 0dd22ec..a10cac6 100644 --- a/frontends/desktop_pet.pyw +++ b/frontends/desktop_pet.pyw @@ -1,10 +1,47 @@ """Desktop Pet with HTTP Toast — ~90 lines""" -import tkinter as tk, threading, random, os, sys +import tkinter as tk, threading, random, os, sys, socket from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs -PORT = 51983 -GIF = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'pet.gif') +DEFAULT_PORT = 51983 +SCRIPT_DIR = os.path.dirname(os.path.abspath(sys.argv[0])) +PROJECT_DIR = os.path.dirname(SCRIPT_DIR) +PORT_FILE = os.path.join(PROJECT_DIR, 'temp', 'desktop_pet_port.txt') +PORT = DEFAULT_PORT +GIF = os.path.join(SCRIPT_DIR, 'pet.gif') + + +def _read_saved_port(): + try: + return int(open(PORT_FILE, encoding='utf-8').read().strip()) + except Exception: + return None + + +def _write_saved_port(port): + os.makedirs(os.path.dirname(PORT_FILE), exist_ok=True) + with open(PORT_FILE, 'w', encoding='utf-8') as f: + f.write(str(port)) + + +def _can_bind(port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind(('127.0.0.1', port)) + return True + except OSError: + return False + finally: + sock.close() + + +def _pick_port(preferred=DEFAULT_PORT, lo=45183, hi=45283): + if _can_bind(preferred): + return preferred + for port in range(lo, hi + 1): + if _can_bind(port): + return port + raise OSError('No usable local port found for desktop pet') class Pet: def __init__(self): @@ -85,9 +122,24 @@ class Pet: def log_message(self, *a): pass HTTPServer.allow_reuse_address = False srv = HTTPServer(('127.0.0.1', PORT), H) + _write_saved_port(PORT) t = threading.Thread(target=srv.serve_forever, daemon=True) t.start() print(f'Toast server: http://127.0.0.1:{PORT}/?msg=hello') if __name__ == '__main__': + existing_port = _read_saved_port() + if existing_port: + _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + _s.connect(('127.0.0.1', existing_port)) + _s.close() + print(f'⚠ Pet already running on port {existing_port}, exiting.') + sys.exit(0) + except OSError: + pass + finally: + _s.close() + PORT = _pick_port() + print(f'Using pet port: {PORT}') Pet() diff --git a/frontends/desktop_pet_v2.pyw b/frontends/desktop_pet_v2.pyw index 7b166dc..1afc879 100644 --- a/frontends/desktop_pet_v2.pyw +++ b/frontends/desktop_pet_v2.pyw @@ -1,13 +1,48 @@ """Desktop Pet with Skin System — Cross-platform with True Transparency""" -import os, re, sys, json, threading, io +import os, re, sys, json, threading, io, socket from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs from PIL import Image, ImageDraw, ImageFont, ImageOps -PORT = 51983 +DEFAULT_PORT = 51983 +PORT = DEFAULT_PORT SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(SCRIPT_DIR) SKINS_DIR = os.path.join(SCRIPT_DIR, 'skins') +PORT_FILE = os.path.join(PROJECT_DIR, 'temp', 'desktop_pet_port.txt') + + +def _read_saved_port(): + try: + return int(open(PORT_FILE, encoding='utf-8').read().strip()) + except Exception: + return None + + +def _write_saved_port(port): + os.makedirs(os.path.dirname(PORT_FILE), exist_ok=True) + with open(PORT_FILE, 'w', encoding='utf-8') as f: + f.write(str(port)) + + +def _can_bind(port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.bind(('127.0.0.1', port)) + return True + except OSError: + return False + finally: + sock.close() + + +def _pick_port(preferred=DEFAULT_PORT, lo=45183, hi=45283): + if _can_bind(preferred): + return preferred + for port in range(lo, hi + 1): + if _can_bind(port): + return port + raise OSError('No usable local port found for desktop pet') class SkinLoader: """Load and parse skin configuration""" @@ -277,6 +312,7 @@ class PetBase: try: HTTPServer.allow_reuse_address = True srv = HTTPServer(('127.0.0.1', PORT), Handler) + _write_saved_port(PORT) threading.Thread(target=srv.serve_forever, daemon=True).start() print(f'✓ Server: http://127.0.0.1:{PORT}/?state=walk') except OSError as e: @@ -767,16 +803,22 @@ else: pass if __name__ == '__main__': - # Singleton: if port already in use, another instance is running - import socket - _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - _s.connect(('127.0.0.1', PORT)) - _s.close() - print(f'⚠ Pet already running on port {PORT}, exiting.') - sys.exit(0) - except ConnectionRefusedError: - pass + # Singleton: if saved port already in use, another instance is running + existing_port = _read_saved_port() + if existing_port: + _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + _s.connect(('127.0.0.1', existing_port)) + _s.close() + print(f'⚠ Pet already running on port {existing_port}, exiting.') + sys.exit(0) + except OSError: + pass + finally: + _s.close() + + PORT = _pick_port() + print(f'Using pet port: {PORT}') if sys.platform == 'darwin': pet = MacPet('vita') diff --git a/frontends/stapp.py b/frontends/stapp.py index 6e6210f..1432b25 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -13,6 +13,16 @@ import time, json, re, threading, queue from agentmain import GeneraticAgent +PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +PET_PORT_FILE = os.path.join(PROJECT_DIR, 'temp', 'desktop_pet_port.txt') + + +def read_pet_port(): + try: + return int(open(PET_PORT_FILE, encoding='utf-8').read().strip()) + except Exception: + return None + st.set_page_config(page_title="Cowork", layout="wide") @st.cache_resource @@ -50,7 +60,9 @@ def render_sidebar(): subprocess.Popen([sys.executable, pet_script], **kwargs) def _pet_req(q): def _do(): - try: urlopen(f'http://127.0.0.1:51983/?{q}', timeout=2) + port = read_pet_port() + if not port: return + try: urlopen(f'http://127.0.0.1:{port}/?{q}', timeout=2) except Exception: pass threading.Thread(target=_do, daemon=True).start() agent._pet_req = _pet_req