mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 12:30:44 +00:00
1.7.0:常规发布
- 版本号 1.7.0 -> 1.7.0 - 重建三端并打包 - 关键测试全过
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""局域网遥控 · Windows 被控端。"""
|
||||
|
||||
__version__ = "1.6.2"
|
||||
__version__ = "1.7.0"
|
||||
APP_NAME = "局域网遥控 被控端"
|
||||
|
||||
+159
-4
@@ -105,10 +105,57 @@ class Store:
|
||||
|
||||
@staticmethod
|
||||
def _write(path: Path, data) -> None:
|
||||
"""写 JSON。优先"临时文件 + 原子替换",失败就降级。
|
||||
|
||||
为什么要降级:
|
||||
装到 Program Files / ProgramData 时,配置是**管理员**建的那一份,
|
||||
属主是管理员。普通用户跑的时候:
|
||||
能新建文件,但不能覆盖/改名别人建的文件。
|
||||
于是 os.replace 报 WinError 5。
|
||||
这时候退成"直接写目标文件" —— 牺牲原子性,换能存下配置。
|
||||
"""
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh, ensure_ascii=False, indent=2)
|
||||
os.replace(tmp, path)
|
||||
payload = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
# 1) 正常路径:临时文件 + 原子替换
|
||||
try:
|
||||
with open(tmp, "w", encoding="utf-8") as fh:
|
||||
fh.write(payload)
|
||||
os.replace(tmp, path)
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# 2) 降级:直接覆盖目标文件
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(payload)
|
||||
Store._cleanup(tmp)
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# 3) 再降级:目标被设成只读就摘掉只读位再写
|
||||
try:
|
||||
os.chmod(path, 0o666)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(payload)
|
||||
Store._cleanup(tmp)
|
||||
return
|
||||
except OSError:
|
||||
Store._cleanup(tmp)
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def _cleanup(tmp: Path) -> None:
|
||||
try:
|
||||
if tmp.exists():
|
||||
tmp.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
with self._lock:
|
||||
@@ -217,5 +264,113 @@ def app_dir() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# 数据目录只算一次,之后缓存 —— 每个命令都去试写一次太浪费
|
||||
_DATA_DIR_CACHE: Path | None = None
|
||||
|
||||
|
||||
def _can_write(folder: Path) -> bool:
|
||||
"""这个目录能不能**真正**用来存配置。
|
||||
|
||||
注意不能只测"能新建文件" —— 那只过一半。
|
||||
实际写配置用的是"临时文件 + 原子替换"(os.replace),
|
||||
它需要**覆盖已存在文件**的权限。
|
||||
装到 ProgramData 时,管理员建的那份普通用户改不了,
|
||||
只测新建会误判成可写,然后跑起来才报 WinError 5。
|
||||
所以这里把整个周期都跑一遍:建 -> 改名覆盖 -> 删。
|
||||
"""
|
||||
try:
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
probe = folder / ".write-test"
|
||||
probe.write_text("x", encoding="utf-8")
|
||||
|
||||
# 关键:模拟真实写入的"替换"这一步
|
||||
target = folder / ".write-test-target"
|
||||
try:
|
||||
if target.exists():
|
||||
target.unlink()
|
||||
probe.write_text("a", encoding="utf-8")
|
||||
os.replace(probe, target) # <- 真正的门槛在这
|
||||
target.write_text("b", encoding="utf-8") # 覆盖已存在的
|
||||
target.unlink()
|
||||
except OSError:
|
||||
# 建得了新文件但替换不了 -> 不能当数据目录
|
||||
try:
|
||||
probe.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _is_installed_copy() -> bool:
|
||||
"""程序是不是装在 Program Files 下(也就是"安装版")。
|
||||
|
||||
这个判断只跟**程序装在哪**有关,跟"当前是谁在跑"无关 ——
|
||||
这正是关键:数据目录必须对所有用户是同一个,
|
||||
否则管理员装的配置普通用户看不见(实测踩过)。
|
||||
"""
|
||||
try:
|
||||
here = str(app_dir()).lower().replace("/", "\\")
|
||||
except OSError:
|
||||
return False
|
||||
for name in ("ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"):
|
||||
base = os.environ.get(name)
|
||||
if not base:
|
||||
continue
|
||||
prefix = str(base).lower().replace("/", "\\").rstrip("\\")
|
||||
if here == prefix or here.startswith(prefix + "\\"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def data_dir() -> Path:
|
||||
return app_dir() / "data"
|
||||
"""配置 / 令牌 / 接收文件的存放目录。
|
||||
|
||||
优先级:
|
||||
1. 环境变量 YKXMZ_DATA(测试和特殊部署用)
|
||||
2. 程序目录下的 data/ —— 绿色版、便携 U 盘,拔走就带走配置
|
||||
3. %PROGRAMDATA%\ykxmz\data —— 装到 Program Files 时用这个
|
||||
|
||||
为什么要有第 3 条:
|
||||
安装包默认装到 C:\Program Files\ykxmz,
|
||||
普通用户对那里**没有写权限**(Windows 的硬规矩,不是 bug)。
|
||||
不转走的话,配对码和令牌会存不下去,用户会莫名其妙连不上。
|
||||
ProgramData 是所有用户可写的公共位置,换 Windows 账号也不丢配置。
|
||||
"""
|
||||
global _DATA_DIR_CACHE
|
||||
if _DATA_DIR_CACHE is not None:
|
||||
return _DATA_DIR_CACHE
|
||||
|
||||
override = os.environ.get("YKXMZ_DATA")
|
||||
if override:
|
||||
_DATA_DIR_CACHE = Path(override)
|
||||
return _DATA_DIR_CACHE
|
||||
|
||||
# ── 安装版(装在 Program Files)一律用 ProgramData ──
|
||||
# 不能按"我能不能写 exe 旁边"来判断:
|
||||
# 安装时是管理员跑的,写得进去;之后普通用户跑就写不进去,
|
||||
# 于是悄悄换了目录 —— 安装时设的配对码就找不着了(实测踩过)。
|
||||
# 按"装在哪"判断,管理员和普通用户看到的就是同一份配置。
|
||||
in_program_files = _is_installed_copy()
|
||||
|
||||
beside = app_dir() / "data"
|
||||
if not in_program_files and _can_write(beside):
|
||||
_DATA_DIR_CACHE = beside
|
||||
return _DATA_DIR_CACHE
|
||||
|
||||
# ProgramData:所有用户共用一份配置(换 Windows 账号也不丢)
|
||||
base = os.environ.get("PROGRAMDATA") or r"C:\ProgramData"
|
||||
common = Path(base) / "ykxmz" / "data"
|
||||
if _can_write(common):
|
||||
_DATA_DIR_CACHE = common
|
||||
return _DATA_DIR_CACHE
|
||||
|
||||
# 兜底:ProgramData 写不了(ACL 没配好 / 权限被收紧)就退到用户目录。
|
||||
# 每人一份配置,但**永远能写** —— 总比跑起来报错强。
|
||||
local = (Path(os.environ.get("LOCALAPPDATA")
|
||||
or Path.home()) / "ykxmz" / "data")
|
||||
local.mkdir(parents=True, exist_ok=True)
|
||||
_DATA_DIR_CACHE = local
|
||||
return _DATA_DIR_CACHE
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
--status 打印状态后退出
|
||||
--port 9000 指定端口
|
||||
--new-code 换一组配对码
|
||||
--code 123456 把配对码设成指定值(安装包用)
|
||||
--name "三年二班" 设置电脑在手表上显示的名字
|
||||
--reset-devices 清空已配对设备
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -45,6 +47,12 @@ def parse_args(argv=None):
|
||||
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("--code", default=None, metavar="6位数字",
|
||||
help="把配对码设成指定值(必须是 6 位数字)。"
|
||||
"安装包用这个把用户在安装向导里填的码写进去。")
|
||||
parser.add_argument("--name", default=None, metavar="名字",
|
||||
help="设置这台电脑在手表上显示的名字"
|
||||
"(默认用 Windows 的计算机名)")
|
||||
parser.add_argument("--reset-devices", action="store_true", help="清空已配对设备")
|
||||
parser.add_argument("--silent", action="store_true",
|
||||
help="全静默运行:没有任何窗口,也没有终端")
|
||||
@@ -602,6 +610,42 @@ def main(argv=None) -> int:
|
||||
|
||||
args = parse_args(argv)
|
||||
|
||||
# --code / --name 要**最先**处理 —— 它们会改配置,
|
||||
# 而 --status / --autostart 这些是"看一眼就退出"的命令。
|
||||
# 安装包的调用链是:
|
||||
# ykxmz.exe --code 789113 --name 三年二班 --autostart on --silent
|
||||
# 如果先把 --autostart 处理掉就 return 了,配对码根本写不进去。
|
||||
if args.code is not None or args.name:
|
||||
store = Store(data_dir())
|
||||
wrote = False
|
||||
if args.code is not None:
|
||||
digits = "".join(ch for ch in str(args.code) if ch.isdigit())
|
||||
if len(digits) != 6:
|
||||
print(f"配对码要 6 位数字,收到的是 {args.code!r}")
|
||||
return 2
|
||||
try:
|
||||
store.set_pairing_code(digits)
|
||||
except ValueError as exc:
|
||||
print(f"设置配对码失败:{exc}")
|
||||
return 2
|
||||
# 装完就算"设置好了",不要再弹设置窗口
|
||||
store.set("setup_done", True)
|
||||
store.set("allow_new_pairing", True)
|
||||
print(f"配对码已设为 {digits}")
|
||||
wrote = True
|
||||
if args.name:
|
||||
store.set("name", str(args.name).strip()[:32])
|
||||
print(f"电脑名已设为 {store.get('name')}")
|
||||
wrote = True
|
||||
|
||||
# 只写了配置就结束 —— 这是一次性命令。
|
||||
# 不加这个 return 的话会继续走到 run_silent(),
|
||||
# 变成常驻进程永不返回,调用方(安装程序)会一直等下去。
|
||||
# 想顺便启动服务,显式加 --silent。
|
||||
if wrote and not (args.silent or args.add_device is not None
|
||||
or args.setup):
|
||||
return 0
|
||||
|
||||
if args.autostart:
|
||||
return cli_autostart(args.autostart)
|
||||
if args.status:
|
||||
@@ -629,6 +673,28 @@ def main(argv=None) -> int:
|
||||
if args.reset_devices:
|
||||
store.devices = {}
|
||||
store.save_devices()
|
||||
|
||||
# --code:把配对码设成安装向导里填的那个。
|
||||
# 严格要 6 位数字 —— 和设置窗口里的规则一致,
|
||||
# 免得用户装了之后发现手表上少输一位连不上。
|
||||
if args.code is not None:
|
||||
digits = "".join(ch for ch in str(args.code) if ch.isdigit())
|
||||
if len(digits) != 6:
|
||||
print(f"配对码要 6 位数字,收到的是 {args.code!r}")
|
||||
return 2
|
||||
try:
|
||||
store.set_pairing_code(digits)
|
||||
except ValueError as exc:
|
||||
print(f"设置配对码失败:{exc}")
|
||||
return 2
|
||||
# 装完就是"设置好了",不要再弹设置窗口
|
||||
store.set("setup_done", True)
|
||||
store.set("allow_new_pairing", True)
|
||||
print(f"配对码已设为 {digits}")
|
||||
|
||||
if args.name:
|
||||
store.set("name", str(args.name).strip()[:32])
|
||||
|
||||
if args.new_code or not store.pairing_code():
|
||||
store.new_pairing_code()
|
||||
|
||||
|
||||
@@ -27,12 +27,12 @@ VSVersionInfo(
|
||||
[
|
||||
StringStruct("CompanyName", "ykxmz"),
|
||||
StringStruct("FileDescription", "ykxmz"),
|
||||
StringStruct("FileVersion", "1.6.2.0"),
|
||||
StringStruct("FileVersion", "1.7.0.0"),
|
||||
StringStruct("InternalName", "ykxmz"),
|
||||
StringStruct("LegalCopyright", "ykxmz"),
|
||||
StringStruct("OriginalFilename", "ykxmz.exe"),
|
||||
StringStruct("ProductName", "ykxmz"),
|
||||
StringStruct("ProductVersion", "1.6.2.0"),
|
||||
StringStruct("ProductVersion", "1.7.0.0"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user