Download onw/tray.py from ryugyosoft/onwAMD: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/ryugyosoft/onwAMD/resolve/main/onw/tray.py
- Command line
-
hf download hf://ryugyosoft/onwAMD/onw/tray.py
-
curl -L -o tray.py https://huggingface.co/ryugyosoft/onwAMD/resolve/main/onw/tray.py
10 kB
| """onw AMD tray: the resident icon (Windows task tray / Ubuntu top-bar indicator). | |
| onw tray icon + menu: open the onw window (models / server / settings), chat UI, check page, copy the | |
| OpenAI API URL, start / stop the server. With no model downloaded yet it opens the model manager. | |
| The server itself runs as a separate process managed by onw.app (so the window and the tray share it). The tray | |
| also hosts the manager (onw.manager: downloads, model list, settings) that the window talks to, so downloads keep | |
| going while the window is closed. | |
| """ | |
| import json, os, subprocess, sys, threading, time, webbrowser | |
| from . import app, __version__ | |
| def open_window(tab="server", updated=None): | |
| exe = sys.executable | |
| if os.name == "nt" and exe.lower().endswith("python.exe"): | |
| exe = exe[:-10] + "pythonw.exe" | |
| subprocess.Popen([exe, "-m", "onw", "window", "--tab", tab] + (["--updated", updated] if updated else [])) | |
| def icon_image(state): | |
| from . import logo | |
| return logo.draw(64, simple=True, state=state) # the small-size logo + a status dot (grey when stopped) | |
| def copy_text(text): | |
| """Copy to the clipboard: clip (Windows), wl-copy (Wayland) or xclip / xsel (X11). False when none is there.""" | |
| if os.name == "nt": | |
| cmds = [(["clip"], text.encode("utf-16-le"))] | |
| else: | |
| cmds = [(["wl-copy"], text.encode()), (["xclip", "-selection", "clipboard"], text.encode()), | |
| (["xsel", "--clipboard", "--input"], text.encode())] | |
| for cmd, data in cmds: | |
| try: | |
| if subprocess.run(cmd, input=data, check=False, timeout=5).returncode == 0: | |
| return True | |
| except (OSError, subprocess.SubprocessError): | |
| continue | |
| return False | |
| def replace_running(): | |
| """The installer ran again while a tray is running (an update by the one-line installer): ask that tray to restart | |
| into the new files, as the in-app update does (the model that was running is loaded again). False when no | |
| tray answered.""" | |
| import urllib.request | |
| from . import manager as mgr | |
| d = mgr.find_manager() | |
| if not d: | |
| return False | |
| try: | |
| req = urllib.request.Request(f"http://127.0.0.1:{d['port']}/api/restart_tray", b"{}", | |
| {"X-Onw-Token": d["token"], "Content-Type": "application/json"}) | |
| return "error" not in json.loads(urllib.request.urlopen(req, timeout=10).read()) | |
| except Exception: | |
| return False | |
| def take_over(): | |
| """A running tray that cannot restart itself (older than 0.1.3): stop it and its server, remember the model for the | |
| new tray to load again, and wait for its single-instance lock. True once the lock is ours.""" | |
| from . import manager as mgr | |
| info = app.server_info() | |
| with open(os.path.join(app.config_dir(), "resume.json"), "w") as f: | |
| json.dump({"model": info["model"] if info else None}, f) | |
| app.stop_server() | |
| d = mgr.find_manager() | |
| if d: | |
| try: | |
| import psutil | |
| p = psutil.Process(d["pid"]) | |
| procs = p.children(recursive=True) + [p] | |
| for q in procs: | |
| q.terminate() | |
| psutil.wait_procs(procs, timeout=10) | |
| except Exception: | |
| pass | |
| for _ in range(40): | |
| if app.single_instance("tray"): | |
| return True | |
| time.sleep(0.5) | |
| return False | |
| def main(argv=None): | |
| argv = argv or [] | |
| if not app.single_instance("tray"): # already in the tray (e.g. started again from the menu) | |
| if "--replace" in argv: # the installer ran again: the files are new | |
| if replace_running(): # the old tray restarts itself on the new code | |
| return | |
| if not take_over(): # an older tray: stop it and start here instead | |
| open_window() | |
| return | |
| else: | |
| open_window() | |
| return | |
| import pystray | |
| from . import manager as mgr | |
| app.clear_stale_server() # a pid file from before a crash / reboot | |
| manager = mgr.Manager() | |
| mgr.serve(manager) # the window's backend lives here | |
| from .i18n import T | |
| labels = {"stopped": "停止中", "loading": "ロード中…", "running": "動作中", "failed": "ロードに失敗"} | |
| state = {"s": app.server_status()} | |
| def cfg(): | |
| return app.load_config() | |
| def status_text(_=None): | |
| info = app.server_info() | |
| name = (info or {}).get("model") or cfg().get("model") or T("モデル未選択") | |
| return f"onw AMD: {T(labels[state['s']])} — {name.split('/')[-1]}" | |
| def start(_=None, __=None): | |
| c = cfg() | |
| if not app.installed(c): | |
| open_window("models") | |
| return | |
| if c["model"] not in app.installed(c): | |
| c = app.update_config(model=next(iter(app.installed(c)))) | |
| try: | |
| app.start_server(c) | |
| except Exception: # e.g. the port in use: the window says why | |
| open_window("server") | |
| def quit_(icon, __=None): | |
| app.stop_server() | |
| try: # manager.json points at this process: a later | |
| os.remove(mgr.INFO()) # `onw window` then starts its own manager | |
| except OSError: | |
| pass | |
| icon.stop() | |
| def restart_after_update(icon_=None): | |
| """Stop the server, start a fresh tray on the new code, reload the model that was running.""" | |
| info = app.server_info() | |
| with open(os.path.join(app.config_dir(), "resume.json"), "w") as f: | |
| json.dump({"model": info["model"] if info else None}, f) | |
| app.stop_server() | |
| subprocess.Popen([sys.executable, "-m", "onw.relaunch", str(os.getpid())], | |
| cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| **({"creationflags": subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS} if os.name == "nt" | |
| else {"start_new_session": True})) | |
| quit_(icon) | |
| def update_found(v): | |
| try: | |
| icon.notify(T("「更新」を押すと自動で入れ替えて再起動します。"), T("onw v{0} が使えます", v)) | |
| except Exception: # not every indicator backend can notify | |
| pass | |
| try: | |
| icon.update_menu() | |
| except Exception: | |
| pass | |
| def do_update(_=None, __=None): | |
| r = manager.run_update() | |
| if r.get("error"): | |
| open_window("server") | |
| manager.on_updated = lambda: restart_after_update() | |
| manager.on_restart = lambda: restart_after_update() # the installer ran again (onw tray --replace) | |
| manager.on_update_found = update_found | |
| running = lambda _: state["s"] == "running" | |
| menu = pystray.Menu( | |
| pystray.MenuItem(status_text, None, enabled=False), | |
| pystray.MenuItem(lambda _: T("onw を開く(モデル・サーバー・設定)"), lambda *_: open_window(), default=True), | |
| pystray.MenuItem(lambda _: T("チャットを開く"), lambda *_: webbrowser.open(f"http://localhost:{cfg()['port']}/chat"), enabled=running), | |
| pystray.MenuItem(lambda _: T("検証ページを開く"), lambda *_: webbrowser.open(f"http://localhost:{cfg()['port']}/check"), enabled=running), | |
| pystray.MenuItem(lambda _: T("OpenAI API のURLをコピー"), | |
| lambda *_: copy_text(app.api_url(cfg())) or open_window("server")), # no clipboard tool: shown there | |
| pystray.MenuItem(lambda _: T("onw を更新(v{0})", manager.upd["latest"]), do_update, | |
| visible=lambda _: bool(manager.upd["latest"]) and manager.upd["state"] != "updating"), | |
| pystray.MenuItem(lambda _: T("モデルをロード"), start, visible=lambda _: state["s"] in ("stopped", "failed")), | |
| pystray.MenuItem(lambda _: T("アンロード(サーバー停止)"), lambda *_: app.stop_server(), visible=lambda _: state["s"] in ("loading", "running")), | |
| pystray.MenuItem(lambda _: T("終了(サーバーも停止)"), quit_)) | |
| icon = pystray.Icon("onw-amd", icon_image(state["s"]), app.title(), menu) | |
| def refresh(): | |
| last = None | |
| while True: | |
| state["s"] = app.server_status() | |
| if state["s"] != last: | |
| last = state["s"] | |
| icon.icon = icon_image(last) | |
| icon.title = status_text() | |
| try: | |
| icon.update_menu() | |
| except Exception: | |
| pass | |
| time.sleep(2) | |
| c = cfg() | |
| resume = os.path.join(app.config_dir(), "resume.json") | |
| resumed = None | |
| after_update = os.path.exists(resume) | |
| if after_update: # just restarted by an update: reload that model | |
| try: | |
| resumed = json.load(open(resume)).get("model") | |
| except (OSError, ValueError): | |
| pass | |
| os.remove(resume) | |
| if not app.installed(c): | |
| open_window("models") # first run: pick a model to download | |
| elif resumed and state["s"] == "stopped" and resumed in app.installed(c): | |
| try: | |
| app.start_server(app.update_config(model=resumed)) | |
| except Exception: | |
| open_window("server") | |
| elif c["autostart_server"] and state["s"] == "stopped": | |
| start() | |
| if after_update and app.installed(c): # show the new version (an open window reopens itself | |
| open_window("server", updated=__version__) # too; the single-instance lock keeps just one) | |
| threading.Thread(target=refresh, daemon=True).start() | |
| threading.Thread(target=app.refresh_shortcut, daemon=True).start() | |
| icon.run() | |