mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 21:50:44 +00:00
被控端 - 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%) - 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败) - 每分钟回头找局域网,自动切回快 400 倍的路径 - 画面心跳帧:静止时省流量,客户端仍知道流是活的 - 锁屏时给出可操作的提示 手表端 - 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException - 连接错误翻译成人话(证书/时间/解析/超时分别提示) - 经中转连上后用服务器报的地址刷新已配对记录 - 新增局域网连接页面、服务器延迟检测 服务器端 - 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502) - 网络调优脚本 net-tune.sh(开机自动应用) - 管理页:批量删除、网络体检、下载区 - 隧道改 HTTP/2 + 双连接 文档 - 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
653 lines
23 KiB
Python
653 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""被控端启动入口。
|
||
|
||
双击 EXE 的完整流程:
|
||
|
||
第一次运行
|
||
弹出一个窗口「第一次使用」→ 输入 6 位配对码
|
||
可勾选:设置开机自启 / 顺便放行防火墙
|
||
↓ 点确定
|
||
黑终端一闪(提权跑一键修复防火墙,会先弹一次 UAC)
|
||
↓
|
||
窗口消失,程序转入后台静默运行 —— 屏幕上再没有任何东西
|
||
|
||
之后每次开机
|
||
直接后台静默运行,一个窗口都没有
|
||
|
||
手表
|
||
输那 6 位数字就连上
|
||
|
||
要重新设置:ykxmz.exe --setup
|
||
|
||
命令行参数:
|
||
--setup 重新打开设置窗口
|
||
--silent 直接静默运行(开机自启用的就是这个)
|
||
--add-device [分钟] 静默运行 + 临时允许新设备配对
|
||
--autostart on|off 开关开机自启
|
||
--status 打印状态后退出
|
||
--port 9000 指定端口
|
||
--new-code 换一组配对码
|
||
--reset-devices 清空已配对设备
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
|
||
from lanremote import APP_NAME, __version__
|
||
from lanremote.config import Store, app_dir, data_dir
|
||
from lanremote.instance import InstanceLock
|
||
|
||
|
||
def parse_args(argv=None):
|
||
parser = argparse.ArgumentParser(
|
||
prog="lanremote-agent", description=f"{APP_NAME} v{__version__}")
|
||
parser.add_argument("--port", type=int, help="监听端口(默认 8765)")
|
||
parser.add_argument("--new-code", action="store_true", help="换一组配对码")
|
||
parser.add_argument("--reset-devices", action="store_true", help="清空已配对设备")
|
||
parser.add_argument("--silent", action="store_true",
|
||
help="全静默运行:没有任何窗口,也没有终端")
|
||
parser.add_argument("--add-device", nargs="?", type=int, const=10,
|
||
default=None, metavar="分钟",
|
||
help="静默运行,但临时允许新设备配对(默认 10 分钟)")
|
||
parser.add_argument("--autostart", choices=["on", "off"],
|
||
help="开关开机自启")
|
||
parser.add_argument("--status", action="store_true", help="打印状态后退出")
|
||
parser.add_argument("--setup", action="store_true",
|
||
help="重新打开设置窗口")
|
||
parser.add_argument("--netcheck", nargs="?", type=int, const=90,
|
||
default=None, metavar="秒",
|
||
help="网络自检(自开控制台,不需要装 Python)")
|
||
parser.add_argument("--fix-firewall", action="store_true",
|
||
help="提权修复防火墙")
|
||
parser.add_argument("--relay", default=None, metavar="地址",
|
||
help="设置中转服务器地址(跨网络用)。"
|
||
"可用分号分隔多个,按顺序尝试,例如:"
|
||
"http://192.168.2.156:8080;https://relay.xxmxxs.top"
|
||
" —— 局域网优先,断了自动切隧道。"
|
||
"传空字符串关闭")
|
||
parser.add_argument("--relay-status", action="store_true",
|
||
help="查看中转连接状态")
|
||
parser.add_argument("--uninstall", action="store_true",
|
||
help="卸载:停进程、删开机自启、删数据目录")
|
||
return parser.parse_args(argv)
|
||
|
||
|
||
# ======================================================================
|
||
# 命令行小工具
|
||
# ======================================================================
|
||
|
||
def cli_autostart(action: str) -> int:
|
||
from lanremote.autostart import enable, disable, status
|
||
try:
|
||
print(enable() if action == "on" else disable())
|
||
except ValueError as exc:
|
||
print(f"失败: {exc}")
|
||
return 1
|
||
info = status()
|
||
print(f"当前: {'已开启' if info['enabled'] else '未开启'}")
|
||
return 0
|
||
|
||
|
||
def cli_uninstall() -> int:
|
||
"""卸载。会开一个控制台让用户看到过程。"""
|
||
attach_console()
|
||
from lanremote.uninstall import run as do_uninstall
|
||
try:
|
||
print(do_uninstall())
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"卸载出错:{exc}")
|
||
return 1
|
||
print()
|
||
input("按回车关闭…")
|
||
return 0
|
||
|
||
|
||
def attach_console() -> None:
|
||
"""给 --windowed 程序自己开一个控制台窗口。
|
||
|
||
打包成 --windowed 后进程没有控制台,直接 print 什么都看不到。
|
||
AllocConsole 可以临时开一个,并把 stdout/stderr 接过去。
|
||
"""
|
||
if os.name != "nt":
|
||
return
|
||
try:
|
||
import ctypes
|
||
# 从 .bat 双击启动时已经有控制台了,再 AllocConsole 会失败并且输出丢失
|
||
if ctypes.windll.kernel32.GetConsoleWindow():
|
||
return
|
||
ctypes.windll.kernel32.AllocConsole()
|
||
sys.stdout = open("CONOUT$", "w", encoding="utf-8", buffering=1)
|
||
sys.stderr = open("CONOUT$", "w", encoding="utf-8", buffering=1)
|
||
try:
|
||
sys.stdin = open("CONIN$", "r", encoding="utf-8")
|
||
except OSError:
|
||
pass
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
def wait_key() -> None:
|
||
try:
|
||
print()
|
||
input("按回车键关闭这个窗口…")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
class _Tee:
|
||
"""把输出同时送到控制台和文件。"""
|
||
|
||
def __init__(self, *streams):
|
||
self.streams = streams
|
||
|
||
def write(self, text):
|
||
for stream in self.streams:
|
||
try:
|
||
stream.write(text)
|
||
stream.flush()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
def flush(self):
|
||
for stream in self.streams:
|
||
try:
|
||
stream.flush()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
|
||
def cli_netcheck(seconds: int) -> int:
|
||
"""网络自检:监听端口,看手表的包到底有没有到这台电脑。
|
||
|
||
EXE 是 --windowed 打包的,本来没有控制台。这里自己开一个,
|
||
同时把结果写一份到「网络自检结果.txt」——
|
||
控制台万一没显示出来(被杀软拦、权限受限等),
|
||
用户还能从文件里看到结论。wlzj.bat 跑完会自动打开这个文件。
|
||
"""
|
||
attach_console()
|
||
|
||
original = sys.stdout
|
||
path = app_dir() / "网络自检结果.txt"
|
||
handle = None
|
||
try:
|
||
handle = open(path, "w", encoding="utf-8")
|
||
except OSError:
|
||
handle = None
|
||
if handle is not None:
|
||
sys.stdout = _Tee(original, handle)
|
||
|
||
code = 1
|
||
try:
|
||
from lanremote import netcheck
|
||
code = netcheck.run_check(int(seconds or 90))
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f"自检运行失败: {exc}")
|
||
finally:
|
||
if handle is not None:
|
||
try:
|
||
sys.stdout = original
|
||
handle.close()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
try:
|
||
print()
|
||
print(f"结果已保存到:{path}")
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
wait_key()
|
||
return code
|
||
|
||
|
||
def cli_fix_firewall() -> int:
|
||
"""提权修复防火墙(会弹一次 UAC,然后黑终端一闪)。"""
|
||
store = Store(data_dir())
|
||
ok, detail = fix_firewall_with_flash(store)
|
||
attach_console()
|
||
print(("已修复:" if ok else "未完成:") + str(detail))
|
||
wait_key()
|
||
return 0 if ok else 1
|
||
|
||
|
||
def cli_relay(url: str) -> int:
|
||
"""设置中转地址。传空字符串就关掉。"""
|
||
store = Store(data_dir())
|
||
url = (url or "").strip().rstrip("/")
|
||
store.set("relay_url", url)
|
||
attach_console()
|
||
if not url:
|
||
print("已关闭中转(以后只在局域网内可用)")
|
||
wait_key()
|
||
return 0
|
||
if not url.startswith(("http://", "https://")):
|
||
url = "https://" + url
|
||
store.set("relay_url", url)
|
||
print(f"中转地址已设为:{url}")
|
||
print()
|
||
print("正在测试能不能连上…")
|
||
try:
|
||
from lanremote.relay import RelayLink
|
||
link = RelayLink(store)
|
||
if not link.start():
|
||
print(" 启动失败")
|
||
wait_key()
|
||
return 1
|
||
# 等它登记成功
|
||
import time as _t
|
||
for _ in range(30):
|
||
_t.sleep(1)
|
||
status = link.status()
|
||
if status.get("online"):
|
||
print(f" ✓ 已连上中转(设备 {status.get('deviceId')},"
|
||
f"配对码 {status.get('code')})")
|
||
link.stop()
|
||
wait_key()
|
||
return 0
|
||
if status.get("error"):
|
||
print(f" … {status['error']}")
|
||
print(f" ✗ 30 秒内没能连上 最后错误:{link.status().get('error')}")
|
||
link.stop()
|
||
except Exception as exc: # noqa: BLE001
|
||
print(f" ✗ 测试失败:{exc}")
|
||
wait_key()
|
||
return 1
|
||
|
||
|
||
def cli_relay_status() -> int:
|
||
"""看现在的中转状态。"""
|
||
from lanremote.relay import RelayLink
|
||
store = Store(data_dir())
|
||
url = str(store.get("relay_url") or "") or "192.168.2.156:8080;https://relay.xxmxxs.top"
|
||
attach_console()
|
||
if not url:
|
||
print("没有配置中转(只在局域网内可用)")
|
||
print("要跨网络请运行:ykxmz.exe --relay https://你的中转地址")
|
||
wait_key()
|
||
return 0
|
||
print(f"中转地址:{url}")
|
||
print("正在测…")
|
||
link = RelayLink(store)
|
||
link.start()
|
||
import time as _t
|
||
for _ in range(30):
|
||
_t.sleep(1)
|
||
status = link.status()
|
||
if status.get("online"):
|
||
print(f" ✓ 在线")
|
||
print(f" 设备标识 {status.get('deviceId')}")
|
||
print(f" 配对码 {status.get('code')}")
|
||
print(f" 本次已服务 {status.get('sessions')} 个手表会话")
|
||
link.stop()
|
||
wait_key()
|
||
return 0
|
||
print(f" ✗ 不在线 最后错误:{link.status().get('error')}")
|
||
link.stop()
|
||
wait_key()
|
||
return 1
|
||
|
||
|
||
def cli_status() -> int:
|
||
from lanremote.autostart import status as auto_status
|
||
store = Store(data_dir())
|
||
auto = auto_status()
|
||
print(f"{APP_NAME} v{__version__}")
|
||
print(f" 数据目录: {data_dir()}")
|
||
print(f" 监听端口: {store.get('port', 8765)}")
|
||
print(f" 配对码: {store.pairing_code() or '(还没生成)'}")
|
||
print(f" 已配设备: {len(store.device_list())} 台")
|
||
print(f" 允许新配对: {'是' if store.get('allow_new_pairing', True) else '否'}")
|
||
print(f" 开机自启: {'已开启' if auto['enabled'] else '未开启'}")
|
||
if auto["enabled"]:
|
||
print(f" 命令行: {auto['command']}")
|
||
return 0
|
||
|
||
|
||
# 设完配对码之后留多久给手表来配对
|
||
SETUP_PAIR_MINUTES = 60
|
||
# 一台设备都没配对过时,自动开多久的配对窗口(自愈用)
|
||
FIRST_PAIR_MINUTES = 120
|
||
|
||
|
||
# ======================================================================
|
||
# 黑终端一闪:提权修防火墙
|
||
# ======================================================================
|
||
|
||
def fix_firewall_with_flash(store) -> tuple[bool, str]:
|
||
"""提权执行防火墙修复。
|
||
|
||
会先弹一次 UAC;确认之后黑色终端窗口一闪,script 跑完自动关掉。
|
||
这就是设置流程里那个"闪过的黑窗口"。
|
||
"""
|
||
from lanremote import firewall
|
||
from lanremote.discovery import DISCOVERY_PORT
|
||
|
||
tcp_port = int(store.get("port", 8765) or 8765)
|
||
udp_port = int(DISCOVERY_PORT)
|
||
|
||
try:
|
||
return firewall.apply_elevated(tcp_port, udp_port)
|
||
except Exception as exc: # noqa: BLE001
|
||
return False, f"防火墙修复失败: {exc}"
|
||
|
||
|
||
# ======================================================================
|
||
# 静默运行
|
||
# ======================================================================
|
||
|
||
def run_silent(store, allow_pair_minutes: int | None) -> int:
|
||
"""全静默运行:没有窗口,没有终端,只在后台监听。"""
|
||
import time
|
||
|
||
from lanremote.actions import Actions
|
||
from lanremote.headless import HeadlessUI
|
||
from lanremote.server import RemoteServer
|
||
|
||
log_file = data_dir() / "silent.log"
|
||
|
||
# 静默模式下默认**不接受新配对**(防止局域网里任何人接进来)。
|
||
# 需要加设备时用 --add-device 临时开一个时间窗。
|
||
#
|
||
# 自愈:如果一台设备都还没配对过,就必须把配对打开 ——
|
||
# 否则用户设完码会永远连不上(报"被控端已关闭新设备配对")。
|
||
# 配对码仍然是门槛,所以不影响安全;第一台配对成功后自动关闭。
|
||
if not store.device_list():
|
||
allow_pair_minutes = max(allow_pair_minutes or 0,
|
||
FIRST_PAIR_MINUTES)
|
||
store.set("close_pair_after_first", True)
|
||
|
||
if allow_pair_minutes:
|
||
store.set("allow_new_pairing", True)
|
||
deadline = time.time() + max(1, allow_pair_minutes) * 60
|
||
else:
|
||
store.set("allow_new_pairing", False)
|
||
deadline = 0.0
|
||
|
||
ui = HeadlessUI(store, log_file=log_file)
|
||
ui.log(f"{APP_NAME} v{__version__} 静默启动")
|
||
|
||
if not store.get("setup_done", False):
|
||
ui.log("注意:还没有设置过配对码。请先运行:ykxmz.exe --setup")
|
||
ui.log(f"当前临时配对码是 {store.pairing_code()}")
|
||
|
||
lock = InstanceLock()
|
||
if not lock.try_claim():
|
||
ui.log("已有实例在运行,退出")
|
||
return 0
|
||
|
||
server = RemoteServer(store, None, ui=ui)
|
||
server.actions = Actions(ui, store)
|
||
from lanremote.features import Features
|
||
server.features = Features(server.actions)
|
||
server.pairing_enabled = bool(store.get("allow_new_pairing", True))
|
||
ui.server = server
|
||
|
||
try:
|
||
server.start()
|
||
except (OSError, OverflowError, ValueError) as exc:
|
||
ui.log(f"启动失败:{exc}")
|
||
lock.release()
|
||
return 1
|
||
|
||
code = store.pairing_code()
|
||
if allow_pair_minutes and code:
|
||
ui.log(f"配对窗口已开 {allow_pair_minutes} 分钟,配对码 {code}")
|
||
ui.log(f"正在监听 TCP {server.port} / UDP 8766")
|
||
|
||
import threading
|
||
|
||
def watchdog():
|
||
time.sleep(1.0)
|
||
while not ui._stopping:
|
||
time.sleep(5.0)
|
||
if allow_pair_minutes and deadline and time.time() > deadline:
|
||
store.set("allow_new_pairing", False)
|
||
server.pairing_enabled = False
|
||
ui.log("配对窗口已关闭")
|
||
|
||
threading.Thread(target=watchdog, daemon=True, name="pair-window").start()
|
||
|
||
try:
|
||
ui.run_forever()
|
||
except KeyboardInterrupt:
|
||
pass
|
||
finally:
|
||
try:
|
||
server.stop()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
ui.stop()
|
||
lock.release()
|
||
|
||
# 必须用 os._exit 结束。
|
||
# 界面线程拥有 Tk 解释器,正常走解释器退出会报
|
||
# "Tcl_AsyncDelete: async handler deleted by the wrong thread",
|
||
# 而且进程退出码会变成 -2147483645(崩溃码)。
|
||
# os._exit 跳过解释器收尾,干净退出。
|
||
sys.stdout.flush()
|
||
sys.stderr.flush()
|
||
os._exit(0)
|
||
|
||
|
||
# ======================================================================
|
||
# 首次设置
|
||
# ======================================================================
|
||
|
||
def check_code_available(store, code: str):
|
||
"""这个配对码能用吗?
|
||
|
||
能用(或没法判断)返回 None;被别的电脑占了返回一段说明文字。
|
||
|
||
检查走中转的只读接口 /v1/code/check —— 不注册,所以不会误占。
|
||
连不上中转时返回 None(放行):不能因为服务器连不上就装不了,
|
||
真有问题等转静默之后还会再提示一次。
|
||
"""
|
||
if not code or len(code) != 6 or not code.isdigit():
|
||
return "配对码必须是 6 位数字"
|
||
|
||
target = str(store.get("relay_url") or "").strip()
|
||
if not target:
|
||
# 没配过就用内置默认值(定义在 server.py)
|
||
from lanremote.server import DEFAULT_RELAY_URL
|
||
target = DEFAULT_RELAY_URL
|
||
if not target.strip():
|
||
return None # 没配中转,没法检查
|
||
|
||
try:
|
||
from lanremote.relay import RelayLink
|
||
link = RelayLink(store, log=lambda _m: None)
|
||
# start() 会去连中转并登记 —— 但我们只用它的 candidate 解析,
|
||
# 不用它登记(登记会占用配对码)。
|
||
link.candidates = []
|
||
for piece in target.replace(",", ";").split(";"):
|
||
piece = piece.strip().rstrip("/")
|
||
if not piece:
|
||
continue
|
||
if not piece.startswith(("http://", "https://")):
|
||
from lanremote.relay import _guess_scheme
|
||
piece = _guess_scheme(piece) + piece
|
||
link.candidates.append(piece)
|
||
link.url = link.candidates[0] if link.candidates else ""
|
||
reply = link._check_code(code)
|
||
except Exception: # noqa: BLE001
|
||
return None # 检查失败就当没问题
|
||
|
||
if not reply.get("ok"):
|
||
return None # 服务器没回应,放行
|
||
if not reply.get("taken"):
|
||
return None # 没人用,可以用
|
||
|
||
where = reply.get("deviceName") or "另一台电脑"
|
||
return (
|
||
f"配对码 {code} 已经被「{where}」占用了。\n\n"
|
||
f"一个配对码同时只能给一台电脑用。\n"
|
||
f"请换一个别的 6 位数字。\n\n"
|
||
f"(想继续用这个码的话,可以去中转管理页把占用它的那台设备删掉)"
|
||
)
|
||
|
||
|
||
def _tell_firewall_problem(detail: str) -> None:
|
||
"""防火墙没放行成功,说明为什么这次没生效。"""
|
||
message = (
|
||
"防火墙没有放行成功,所以这次**没有**把配对码交给服务器。\n\n"
|
||
f"原因:{detail or '未知'}\n\n"
|
||
"该怎么办:\n"
|
||
" 1. 右键点 ykxmz.exe →「以管理员身份运行」,重新走一遍设置\n"
|
||
" 2. 或者手动放行:控制面板 → Windows Defender 防火墙 →\n"
|
||
" 允许应用通过防火墙 → 找到 ykxmz → 勾上「专用网络」\n\n"
|
||
"放行之后再打开 ykxmz.exe 就行了。"
|
||
)
|
||
try:
|
||
import tkinter as tk
|
||
from tkinter import messagebox
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
root.attributes("-topmost", True)
|
||
messagebox.showerror("还没生效:防火墙没放行", message)
|
||
root.destroy()
|
||
return
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
print("=" * 56)
|
||
print("防火墙没放行,配对码未提交到服务器")
|
||
print(message)
|
||
print("=" * 56)
|
||
|
||
|
||
def _tell_code_problem(message: str) -> None:
|
||
"""把配对码的问题告诉用户。有界面就弹窗,没有就打印。"""
|
||
try:
|
||
import tkinter as tk
|
||
from tkinter import messagebox
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
root.attributes("-topmost", True)
|
||
messagebox.showerror("配对码不能用", message)
|
||
root.destroy()
|
||
return
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
print("=" * 56)
|
||
print("配对码不能用")
|
||
print(message)
|
||
print("=" * 56)
|
||
|
||
|
||
def run_setup(store) -> int:
|
||
"""弹设置窗口 → 存码 → 黑终端一闪修防火墙 → 转静默。"""
|
||
from lanremote.autostart import enable, is_enabled
|
||
from lanremote.ui import SetupWindow
|
||
|
||
# 先检查配对码能不能用,**通过了才去改防火墙**。
|
||
#
|
||
# 踩过的坑:以前是先存码、开自启、修防火墙,最后才转静默——
|
||
# 而"码被别的电脑占了"这件事要等转静默之后才发现。
|
||
# 那时防火墙已经改完了,用户在设置窗口里也没机会换码。
|
||
result = None
|
||
while True:
|
||
window = SetupWindow(store).show()
|
||
if window is None:
|
||
return 0 # 用户关掉了窗口(放弃安装)
|
||
|
||
code = window.get("code", "")
|
||
problem = check_code_available(store, code)
|
||
if problem is None:
|
||
result = window
|
||
break
|
||
|
||
# 被占了:说清楚,让他回去换一个
|
||
_tell_code_problem(problem)
|
||
|
||
store.set_pairing_code(result["code"])
|
||
store.set("setup_done", True)
|
||
|
||
# 开机自启
|
||
if result["autostart"]:
|
||
try:
|
||
if not is_enabled():
|
||
enable()
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
|
||
# 黑终端一闪:修防火墙
|
||
firewall_ok = True
|
||
firewall_note = ""
|
||
if result["firewall"]:
|
||
firewall_ok, firewall_note = fix_firewall_with_flash(store)
|
||
|
||
# 放行防火墙之前,配对码**不加入服务器**。
|
||
#
|
||
# 为什么:注册(= 配对码挂到服务器上)如果发生在防火墙之前,
|
||
# 那么防火墙没放行成功时,服务器上会留着一个谁也连不上的码,
|
||
# 而且它会把码占住,用户重装都换不了。
|
||
# 所以这里卡一道:防火墙没成功就不注册。
|
||
if not firewall_ok:
|
||
_tell_firewall_problem(firewall_note)
|
||
return 3
|
||
|
||
# 关键:设完码必须开一个配对窗口。
|
||
# 之前这里传 None(= 不开窗口 → allow_new_pairing=False),
|
||
# 结果用户刚设完码,被控端立刻就把门锁上,手表第一次配对必然失败,
|
||
# 报"被控端已关闭新设备配对"。
|
||
# 第一台设备配对成功后服务端会自动把窗口关上。
|
||
store.set("close_pair_after_first", True)
|
||
return run_silent(store, SETUP_PAIR_MINUTES)
|
||
|
||
|
||
def main(argv=None) -> int:
|
||
try:
|
||
sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr]
|
||
except Exception:
|
||
pass
|
||
|
||
args = parse_args(argv)
|
||
|
||
if args.autostart:
|
||
return cli_autostart(args.autostart)
|
||
if args.status:
|
||
return cli_status()
|
||
|
||
# 这两个是给 .bat 调的:EXE 自带功能,不需要系统装 Python
|
||
if args.netcheck is not None:
|
||
return cli_netcheck(args.netcheck)
|
||
if args.fix_firewall:
|
||
return cli_fix_firewall()
|
||
if args.relay is not None:
|
||
return cli_relay(args.relay)
|
||
if args.relay_status:
|
||
return cli_relay_status()
|
||
if args.uninstall:
|
||
return cli_uninstall()
|
||
|
||
store = Store(data_dir())
|
||
|
||
if args.port:
|
||
if not (1 <= int(args.port) <= 65535):
|
||
print(f"端口 {args.port} 不合法,应在 1-65535 之间")
|
||
return 2
|
||
store.set("port", int(args.port))
|
||
if args.reset_devices:
|
||
store.devices = {}
|
||
store.save_devices()
|
||
if args.new_code or not store.pairing_code():
|
||
store.new_pairing_code()
|
||
|
||
# 显式要求静默 / 临时开配对窗口
|
||
if args.silent or args.add_device is not None:
|
||
return run_silent(store, args.add_device)
|
||
|
||
# --setup:重新设置
|
||
if args.setup:
|
||
return run_setup(store)
|
||
|
||
# 第一次运行:设置窗口 → 修防火墙 → 静默
|
||
if not store.get("setup_done", False):
|
||
return run_setup(store)
|
||
|
||
# 已经设置过:直接静默,屏幕上什么都不出现
|
||
return run_silent(store, None)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|