Files
users/scripts/launch_progress.py

288 lines
8.4 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
神射手 桌面启动器 - 带进度条
- 本地版Ollama Docker MongoDB pnpm dev 打开浏览器
- NAS版(--nas)检查 NAS 可达性 打开 http://192.168.1.201:3117
"""
import os
import sys
import time
import subprocess
import signal
import threading
USE_GUI = True
try:
import tkinter as tk
from tkinter import ttk
except ImportError:
USE_GUI = False
PROJECT = "/Users/karuo/Documents/开发/2、私域银行/神射手"
import glob
PATH_DIRS = [
"/opt/homebrew/bin", "/usr/local/bin",
os.path.expanduser("~/.volta/bin"),
]
for d in PATH_DIRS:
if os.path.isdir(d):
os.environ["PATH"] = d + os.pathsep + os.environ.get("PATH", "")
for p in glob.glob(os.path.expanduser("~/.fnm/*/installation/bin")):
if os.path.isdir(p):
os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "")
for p in glob.glob(os.path.expanduser("~/.nvm/versions/node/*/bin")):
if os.path.isdir(p):
os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "")
server_pid = None
_widgets = {} # 存放控件引用
NAS_MODE = "--nas" in sys.argv or "-n" in sys.argv
NAS_URL = "http://192.168.1.201:3117"
def notify(msg):
"""发送 macOS 通知(无 GUI 时使用)"""
subprocess.run(
["osascript", "-e", f'display notification "{msg}" with title "神射手"'],
capture_output=True, timeout=2
)
def set_progress(pct, step, msg):
"""线程安全更新进度"""
if not USE_GUI:
return
w = _widgets
if "pct" in w:
w["pct"]["text"] = f"{pct}%"
if "step" in w:
w["step"]["text"] = step
if "msg" in w:
w["msg"]["text"] = msg
if "progress" in w:
w["progress"]["value"] = pct
if "root" in w:
try:
w["root"].update_idletasks()
except Exception:
pass
def port_listen(port):
"""检测端口是否在监听"""
try:
r = subprocess.run(
["lsof", "-i", f":{port}", "-sTCP:LISTEN"],
capture_output=True, timeout=2
)
return r.returncode == 0
except Exception:
return False
def http_ok(url, timeout=2):
"""检测 HTTP 是否可访问"""
try:
import urllib.request
req = urllib.request.Request(url)
urllib.request.urlopen(req, timeout=timeout)
return True
except Exception:
return False
def run_steps_nas(root, start_time):
"""NAS 版:检查 NAS 可达性 → 打开浏览器"""
def elapsed():
return int(time.time() - start_time)
def upd(pct, step, msg):
full = f"{msg} (已用时 {elapsed()} 秒)"
set_progress(pct, step, full)
if not USE_GUI:
notify(f"{pct}% | {step}: {msg}")
upd(0, "神射手 (NAS)", "检查 NAS 连通性...")
if http_ok(NAS_URL, timeout=5):
upd(90, "神射手 (NAS)", "NAS 服务可用")
else:
upd(50, "神射手 (NAS)", "NAS 可能未启动,仍尝试打开...")
time.sleep(1)
upd(100, "完成", "正在打开浏览器...")
subprocess.run(["open", NAS_URL], timeout=3)
time.sleep(2)
# NAS 版无后台进程2 秒后自动关闭窗口
if root is not None and "root" in _widgets:
try:
r = _widgets["root"]
r.after(0, r.destroy)
except Exception:
pass
def run_steps(root, start_time):
"""root 为 None 时表示无 GUI 模式"""
global server_pid
if NAS_MODE:
run_steps_nas(root, start_time)
return
def elapsed():
return int(time.time() - start_time)
def upd(pct, step, msg):
full = f"{msg} (已用时 {elapsed()} 秒)"
set_progress(pct, step, full)
if not USE_GUI:
notify(f"{pct}% | {step}: {msg}")
try:
os.chdir(PROJECT)
except Exception as e:
upd(0, "错误", str(e))
return
# 1. Ollama 本地大模型 (0-15%)
upd(0, "本地大模型", "检查 Ollama (localhost:11434)...")
if not http_ok("http://localhost:11434/api/tags"):
upd(2, "本地大模型", "正在启动 Ollama...")
subprocess.run(["open", "-a", "Ollama"], capture_output=True, timeout=5)
for i in range(45):
if http_ok("http://localhost:11434/api/tags"):
break
upd(2 + i // 3, "本地大模型", f"等待 Ollama 就绪... ({i+1}s)")
time.sleep(1)
upd(15, "本地大模型", "Ollama 已就绪")
# 2. Docker (15-35%)
upd(15, "Docker", "检查 Docker Engine...")
subprocess.run(["open", "-a", "Docker"], capture_output=True, timeout=5)
for i in range(60):
r = subprocess.run(["docker", "info"], capture_output=True, timeout=5)
if r.returncode == 0:
break
upd(15 + i // 3, "Docker", f"等待 Docker Engine... ({i*2}s)")
time.sleep(2)
upd(35, "Docker", "Docker 已就绪")
# 3. MongoDB (35-45%)
upd(35, "MongoDB", "启动 MongoDB 容器...")
subprocess.run(["docker", "start", "datacenter_mongodb"], capture_output=True, timeout=10)
time.sleep(3)
upd(45, "MongoDB", "MongoDB 已就绪")
# 4. 若 3117 已监听,直接完成
if port_listen(3117):
upd(95, "神射手", "服务已在运行")
time.sleep(1)
subprocess.run(["open", "http://localhost:3117"], timeout=3)
upd(100, "完成", "已打开浏览器")
return
# 5. 启动 pnpm dev (45-90%)
upd(45, "神射手", "启动开发服务 (pnpm dev)...")
proc = subprocess.Popen(
["pnpm", "dev"],
cwd=PROJECT,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
server_pid = proc.pid
# 6. 等待 3117 (90-100%)
for i in range(90):
if port_listen(3117):
break
pct = 45 + min(44, i * 2)
upd(pct, "神射手", f"等待端口 3117 就绪... ({i+1}s)")
time.sleep(1)
upd(95, "神射手", "正在打开浏览器...")
subprocess.run(["open", "http://localhost:3117"], timeout=3)
upd(100, "完成", "神射手已启动")
# 有 GUI 时保持进程直到窗口关闭;无 GUI 时直接退出pnpm 在后台继续运行)
if root is not None:
try:
proc.wait()
except Exception:
pass
def main():
global server_pid
if not USE_GUI:
run_steps(None, time.time())
return
try:
root = tk.Tk()
root.title("神射手 (NAS) - 启动中" if NAS_MODE else "神射手 - 启动中")
root.geometry("420x180")
root.resizable(False, False)
f = ttk.Frame(root, padding=16)
f.pack(fill=tk.BOTH, expand=True)
title = "神射手 (NAS) 数据中台" if NAS_MODE else "神射手 数据中台"
subtitle = "NAS 192.168.1.201:3117" if NAS_MODE else "Ollama → Docker → MongoDB → 神射手"
ttk.Label(f, text=title, font=("", 14, "bold")).pack(anchor=tk.W)
ttk.Label(f, text=subtitle, font=("", 9)).pack(anchor=tk.W)
f2 = ttk.Frame(f)
f2.pack(fill=tk.X, pady=(12, 0))
lb_pct = ttk.Label(f2, text="0%", font=("", 11, "bold"))
lb_pct.pack(side=tk.RIGHT)
lb_step = ttk.Label(f2, text="初始化", font=("", 10))
lb_step.pack(anchor=tk.W)
lb_msg = ttk.Label(f2, text="准备启动...", font=("", 9))
lb_msg.pack(anchor=tk.W)
p = ttk.Progressbar(f2, length=380, mode="determinate")
p.pack(fill=tk.X, pady=(4, 0))
p["value"] = 0
_widgets["root"] = root
_widgets["pct"] = lb_pct
_widgets["step"] = lb_step
_widgets["msg"] = lb_msg
_widgets["progress"] = p
def on_close():
global server_pid
if server_pid and not NAS_MODE:
try:
os.kill(server_pid, signal.SIGTERM)
except Exception:
pass
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_close)
root.update_idletasks()
except Exception:
run_steps(None, time.time())
return
t = threading.Thread(target=lambda: run_steps(root, time.time()))
t.daemon = True
t.start()
try:
root.mainloop()
except Exception:
pass
if server_pid and t.is_alive():
try:
os.kill(server_pid, signal.SIGTERM)
except Exception:
pass
if __name__ == "__main__":
main()