mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
按用户规矩升到 1.9.0(有新内容+重构,不是只修 bug)+ 加版本号一致性检查
用户定的规矩:
只修 bug -> 最后一位 +1 1.8.2 -> 1.8.3
加新内容/重构 -> 第二位 +1,最后一位清 0 1.8.2 -> 1.9.0
这一轮既有新东西(设置窗口的中转服务器字段、允许新设备接入开关、
双击开设置窗口)又有重构(手表面板切换收成 showOnly()),
所以是 **1.9.0**,不是我之前说的 1.8.3。
== 顺手加了个「版本号一致性」检查(回归阶段 3a8)==
版本号散落在 8 个地方,靠脚本+人工替换很容易漏。实测漏了两个,
而且**漏了不报错**,只是用户看到一个对不上的版本号:
* windows/version_info.txt 写着 1.7.1
-> 用户在「文件属性 -> 详细信息」里看到的版本,错了三个版本
* docs/LICENSE.txt 写着 1.7.1
-> 安装时许可页上显示 1.7.1
第二个的根因值得记:installer/build_installer.py 会从 docs/ 里三份
文档**重新生成** license-combined.txt,所以光改生成物没用 ——
下次构建又变回去(我第一次就是这么被骗的)。改成在 build_license()
里**统一注入当前版本**,源文档写什么都不影响。
新增:
_tests/check_version_consistency.py 检查 8 处版本号一致
_build/add_verify_stage.py 给套件加阶段(不经 shell,
不会被 PowerShell 吃掉 \$ 变量)
_build/fix_license_version.py 按行精确修 license
== 另外修了两个我自己的坑 ==
1. verify_all.ps1 被 PowerShell heredoc 吃掉了 \ / \,
整个脚本语法错。**同一个坑踩了两次**(3a7 和 3a8)。
以后给 .ps1 加含 \$ 的内容一律写 Python 脚本文件再跑。
2. build_exe.ps1 被 edit 工具吃掉了 UTF-8 BOM
(PowerShell 5.1 会按 GBK 读,中文注释乱码)。
全量回归 43 个阶段全部通过。
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''给 verify_all.ps1 追加阶段 —— **不能用 PowerShell heredoc**。
|
||||
|
||||
== 为什么单独写这个脚本 ==
|
||||
|
||||
我这一轮在 PowerShell 里用 `python -c @"..."@` 写 Python 代码
|
||||
去改 verify_all.ps1,结果 **`$stageOut` 和 `$root` 被 PowerShell
|
||||
当成变量给展开了**(`@"..."@` 是插值字符串),写进去变成:
|
||||
|
||||
= & python (Join-Path "_tests\check_version_consistency.py") 2>&1
|
||||
Show-Fails
|
||||
Record "版本号一致性" (0 -eq 0)
|
||||
|
||||
整个脚本直接语法错。**同一个坑踩了两次**(3a7 和 3a8)。
|
||||
|
||||
教训:凡是给 .ps1 加含 `$` 的内容,一律**写 Python 脚本文件再跑**,
|
||||
不要用 `python -c @"..."@`。
|
||||
|
||||
用法:
|
||||
python _build/add_verify_stage.py <锚点文字> <阶段号> <标题> <脚本相对路径> <Record 名>
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SUITE = ROOT / "_tests" / "verify_all.ps1"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 6:
|
||||
print(__doc__)
|
||||
return 1
|
||||
anchor_text, stage_no, title, script_rel, record_name = sys.argv[1:6]
|
||||
|
||||
raw = SUITE.read_bytes()
|
||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
text = raw.decode("utf-8-sig")
|
||||
|
||||
if script_rel in text:
|
||||
print(" 已经注册过了(%s 里提到了 %s)" % (SUITE.name, script_rel))
|
||||
return 0
|
||||
|
||||
lines = text.splitlines()
|
||||
idx = None
|
||||
for i, ln in enumerate(lines):
|
||||
if ln.startswith("Record") and anchor_text in ln:
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
print(" ✗ 找不到锚点(Record 行含 %r)" % anchor_text)
|
||||
print(" 现有 Record 行:")
|
||||
for i, ln in enumerate(lines):
|
||||
if ln.startswith("Record"):
|
||||
print(" %d: %s" % (i, ln))
|
||||
return 1
|
||||
|
||||
block = [
|
||||
"",
|
||||
"# ---------------------------------------------------------------- %s"
|
||||
% stage_no,
|
||||
'Banner "%s"' % title,
|
||||
"$stageOut = & python (Join-Path $root \"%s\") 2>&1"
|
||||
% script_rel.replace("/", "\\"),
|
||||
" Show-Fails $stageOut",
|
||||
'Record "%s" ($LASTEXITCODE -eq 0)' % record_name,
|
||||
]
|
||||
lines[idx + 1:idx + 1] = block
|
||||
|
||||
out = "\n".join(lines) + "\n"
|
||||
SUITE.write_bytes((b"\xef\xbb\xbf" if had_bom else b"") + out.encode("utf-8"))
|
||||
print(" ✓ 已在「%s」后面插入阶段 %s" % (anchor_text, stage_no))
|
||||
print(" (BOM 保留:%s)" % had_bom)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -83,9 +83,27 @@ def main() -> int:
|
||||
changed += 1
|
||||
|
||||
# ---- Windows EXE 版本信息 ----
|
||||
if swap(ROOT / "windows" / "version_info.txt",
|
||||
[(f'"{OLD}.0"', f'"{NEW}.0"')],
|
||||
f"EXE 文件版本 {NEW}.0"):
|
||||
# ⚠ 原来用的是 f'"{OLD}.0"',一旦 OLD 和文件里对不上就静默跳过
|
||||
# (version_info.txt 就是这么从 1.7.1 一直漏到现在的)。
|
||||
# 改成正则匹配任意四段版本。
|
||||
import re as _re
|
||||
_vi = ROOT / "windows" / "version_info.txt"
|
||||
if _vi.is_file():
|
||||
_raw = _vi.read_bytes()
|
||||
_bom = _raw.startswith(b"\xef\xbb\xbf")
|
||||
_txt = _raw.decode("utf-8-sig")
|
||||
_new = _re.sub(
|
||||
r'(StringStruct\("(?:File|Product)Version",\\s*")\\d+\\.\\d+\\.\\d+\\.\\d+("\))',
|
||||
r'\\g<1>' + NEW + r'.0\\g<2>', _txt)
|
||||
if _new != _txt:
|
||||
_vi.write_bytes((b"\xef\xbb\xbf" if _bom else b"")
|
||||
+ _new.replace("\\r\\n", "\\n").encode("utf-8"))
|
||||
print(f" version_info.txt: EXE 文件版本 {NEW}.0")
|
||||
changed += 1
|
||||
else:
|
||||
print(" [跳过] version_info.txt")
|
||||
|
||||
if False:
|
||||
changed += 1
|
||||
|
||||
# ---- __init__ ----
|
||||
@@ -100,6 +118,22 @@ def main() -> int:
|
||||
f"VERSION={NEW}"):
|
||||
changed += 1
|
||||
|
||||
# ---- 安装包版本 ----
|
||||
# ⚠ 原来漏了这三处,导致 version_info.txt 从 1.7.1 起就一直没跟上
|
||||
# (用户在"文件属性 -> 详细信息"里看到一个错三个版本的号)。
|
||||
if swap(ROOT / "installer" / "ykxmz.iss",
|
||||
[(f'#define MyAppVersion "{OLD}"',
|
||||
f'#define MyAppVersion "{NEW}"'),
|
||||
(f'ykxmz-setup-{OLD}.exe', f'ykxmz-setup-{NEW}.exe')],
|
||||
f"安装包版本 {NEW}"):
|
||||
changed += 1
|
||||
|
||||
# ---- 安装许可页上的版本 ----
|
||||
if swap(ROOT / "installer" / "license-combined.txt",
|
||||
[(f"版本:{OLD}", f"版本:{NEW}")],
|
||||
f"许可页版本 {NEW}"):
|
||||
changed += 1
|
||||
|
||||
# ---- 三份文档 ----
|
||||
for path in sorted((ROOT / "docs").glob("*.txt")):
|
||||
raw = path.read_bytes()
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''修 license-combined.txt 里那份**没过期的**协议版本号。
|
||||
|
||||
这个文件是三份文档拼起来的(许可协议 + 安装前必读 + 版本记录),
|
||||
每份都有自己的头部版本号和落款。实测第一份的头部还写着 1.7.1:
|
||||
|
||||
第 6 行: '版本:1.7.1' <- 头部,要改
|
||||
第 153 行: ' 软件:ykxmz 1.7.1' <- 落款,要改
|
||||
第 540 行: ' 1.7.1' <- **版本记录**(变更日志),
|
||||
历史条目,**不能改**
|
||||
|
||||
前两次我用的整体 replace 都不够精确:
|
||||
* 第一次 `text.replace('版本:1.7.1', ...)` 报了成功,但改完
|
||||
又被后一步覆盖掉了(顺序问题)
|
||||
* 而且整体 replace 会连带把版本记录里的历史条目也改掉
|
||||
|
||||
所以这次**按行号精确改**:只动 6 和 153。
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FILE = ROOT / "installer" / "license-combined.txt"
|
||||
NEW = "1.9.0"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
raw = FILE.read_bytes()
|
||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
text = raw.decode("utf-8-sig")
|
||||
lines = text.splitlines()
|
||||
|
||||
changed = []
|
||||
# 按内容精确匹配,避免行号漂移
|
||||
for i, ln in enumerate(lines):
|
||||
s = ln.strip()
|
||||
# 头部:版本:X.Y.Z
|
||||
if s.startswith("版本:") and s != ("版本:" + NEW):
|
||||
old = s
|
||||
lines[i] = ln.replace(s, "版本:" + NEW)
|
||||
changed.append((i + 1, old, lines[i].strip()))
|
||||
# 落款:软件:ykxmz X.Y.Z
|
||||
elif s.startswith("软件:ykxmz ") and s != ("软件:ykxmz " + NEW):
|
||||
old = s
|
||||
lines[i] = ln.replace(s, "软件:ykxmz " + NEW)
|
||||
changed.append((i + 1, old, lines[i].strip()))
|
||||
|
||||
if not changed:
|
||||
print(" ✓ 没有要改的")
|
||||
return 0
|
||||
|
||||
print(" ── 改了这些行 ──")
|
||||
for n, old, new in changed:
|
||||
print(" 第 %3d 行: %r -> %r" % (n, old, new))
|
||||
|
||||
out = "\n".join(lines) + "\n"
|
||||
FILE.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
|
||||
+ out.encode("utf-8"))
|
||||
|
||||
# 复核:版本记录那一段不许动
|
||||
after = FILE.read_text(encoding="utf-8-sig")
|
||||
if "八、版本记录" not in after:
|
||||
print(" ✗ 版本记录那一段不见了!")
|
||||
return 1
|
||||
seg = after.split("八、版本记录", 1)[1]
|
||||
if " 1.7.1" not in seg:
|
||||
print(" ⚠ 版本记录里的历史条目 1.7.1 被改掉了 —— 那是不该动的")
|
||||
return 1
|
||||
print(" ✓ 版本记录里的历史条目(1.7.1)保持原样")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -17,7 +17,7 @@ import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
VERSION = "1.8.2"
|
||||
VERSION = "1.9.0"
|
||||
NAME = f"ykxmz-{VERSION}"
|
||||
|
||||
AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''检查全仓的产品版本号是不是**一致**的。
|
||||
|
||||
== 为什么需要这个 ==
|
||||
|
||||
版本号散落在至少 8 个地方,靠脚本 + 人工替换,很容易漏。
|
||||
实测就漏了两个:
|
||||
|
||||
windows/version_info.txt 写着 1.7.1(EXE 属性里显示的版本)
|
||||
installer/license-combined.txt 写着 1.7.1
|
||||
|
||||
漏了不会报错 —— 只是用户在"文件属性 → 详细信息"里看到一个
|
||||
对不上的版本号,或者安装时许可页写着旧版本。很难发现。
|
||||
|
||||
== 判定规则(用户定的)==
|
||||
|
||||
只查**产品版本号**,不碰这两类"看着像版本号但其实不是"的:
|
||||
|
||||
* HTTP User-Agent "LanRemote/1.4.0" —— 协议标识,
|
||||
20 多个测试在断言它,动它要同步改一大片
|
||||
* _tests / _build 里当测试数据或历史记录用的版本串
|
||||
|
||||
真正要一致的:
|
||||
windows/lanremote/__init__.py __version__
|
||||
windows/version_info.txt FileVersion
|
||||
android/AndroidManifest.xml versionName
|
||||
android/build_apk.ps1 APK 文件名
|
||||
installer/ykxmz.iss MyAppVersion
|
||||
installer/license-combined.txt 许可页上写的版本
|
||||
relay/relay.py VERSION
|
||||
_build/package_remote.py VERSION
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# (文件, 正则, 说明)
|
||||
SPOTS = [
|
||||
("windows/lanremote/__init__.py", r'__version__\s*=\s*"([^"]+)"',
|
||||
"被控端 __version__"),
|
||||
("windows/version_info.txt", r'StringStruct\("FileVersion",\s*"([^"]+)"\)',
|
||||
"EXE 文件属性里的版本"),
|
||||
("android/AndroidManifest.xml", r'versionName="([^"]+)"',
|
||||
"手表 App 版本"),
|
||||
("android/build_apk.ps1", r'ykxmz-watch-([\d.]+)\.apk',
|
||||
"APK 文件名"),
|
||||
("installer/ykxmz.iss", r'MyAppVersion\s+"([^"]+)"',
|
||||
"安装包版本"),
|
||||
("installer/license-combined.txt", r'版本:\s*([\d.]+)',
|
||||
"安装许可页上的版本"),
|
||||
("relay/relay.py", r'VERSION\s*=\s*"([^"]+)"',
|
||||
"中转服务端版本"),
|
||||
("_build/package_remote.py", r'VERSION\s*=\s*"([^"]+)"',
|
||||
"打包脚本版本"),
|
||||
]
|
||||
|
||||
|
||||
def norm(v: str) -> str:
|
||||
"""把 1.9.0.0 这种四段式归一成三段(Windows 的文件版本要求四段)。"""
|
||||
parts = v.strip().split(".")
|
||||
while len(parts) > 3 and parts[-1] == "0":
|
||||
parts.pop()
|
||||
return ".".join(parts[:3])
|
||||
|
||||
|
||||
def main() -> int:
|
||||
found = {}
|
||||
problems = []
|
||||
|
||||
for rel, pat, note in SPOTS:
|
||||
p = ROOT / rel
|
||||
if not p.is_file():
|
||||
problems.append("%s 不存在(应该改路径或删掉这条)" % rel)
|
||||
continue
|
||||
try:
|
||||
text = p.read_text(encoding="utf-8-sig")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
problems.append("%s 读不了:%s" % (rel, exc))
|
||||
continue
|
||||
m = re.search(pat, text)
|
||||
if not m:
|
||||
problems.append("%s 里找不到版本号(正则:%s)" % (rel, pat))
|
||||
continue
|
||||
found[rel] = (m.group(1), note)
|
||||
|
||||
if not found:
|
||||
print(" 一个版本号都没找到 —— 正则可能全过时了")
|
||||
return 1
|
||||
|
||||
versions = {norm(v): v for v, _n in found.values()}
|
||||
print(" ── 各处的版本号 ──")
|
||||
for rel, (v, note) in found.items():
|
||||
print(" %-40s %-10s %s" % (rel, v, note))
|
||||
|
||||
print()
|
||||
if len(versions) == 1:
|
||||
only = list(versions.values())[0]
|
||||
print(" ✓ 全部一致:%s" % only)
|
||||
else:
|
||||
print(" ✗ **版本号不一致**:")
|
||||
for rel, (v, _n) in found.items():
|
||||
print(" %-40s %s" % (rel, v))
|
||||
print()
|
||||
print(" 版本号散落在多处,用脚本统一改:")
|
||||
print(" python _build\\bump_version_generic.py <旧> <新>")
|
||||
print(" 但那个脚本**漏了** installer/ykxmz.iss 和")
|
||||
print(" installer/license-combined.txt,改完要手动补。")
|
||||
return 1
|
||||
|
||||
if problems:
|
||||
print(" ⚠ 另外还有 %d 个地方要处理:" % len(problems))
|
||||
for x in problems:
|
||||
print(" %s" % x)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
|
||||
(ROOT / "dist").glob("*watch*.apk"),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
return candidates[0] if candidates else \
|
||||
ROOT / "dist" / "ykxmz-watch-1.8.2.apk"
|
||||
ROOT / "dist" / "ykxmz-watch-1.9.0.apk"
|
||||
|
||||
|
||||
APK = _latest_apk()
|
||||
|
||||
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
# 被控端 -> 一个 zip
|
||||
# 服务器端 -> 一个 zip
|
||||
# 控制端 -> 直接给 APK(不打 zip)
|
||||
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-1.8.2.zip"
|
||||
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-1.8.2.zip"
|
||||
WATCH_APK = ROOT / "dist" / "ykxmz-watch-1.8.2.apk"
|
||||
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-1.9.0.zip"
|
||||
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-1.9.0.zip"
|
||||
WATCH_APK = ROOT / "dist" / "ykxmz-watch-1.9.0.apk"
|
||||
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
|
||||
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
|
||||
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
|
||||
@@ -89,7 +89,7 @@ def main() -> int:
|
||||
for label, path in (("被控端", agent_dir / "README-agent.txt"),
|
||||
("服务器端", relay_dir / "README-server.txt"),
|
||||
("控制端(旁边那份)",
|
||||
ROOT / "dist" / "ykxmz-watch-1.8.2-说明.txt")):
|
||||
ROOT / "dist" / "ykxmz-watch-1.9.0-说明.txt")):
|
||||
raw = path.read_bytes() if path.is_file() else b""
|
||||
check(f"{label}说明有 UTF-8 BOM",
|
||||
raw[:3] == b"\xef\xbb\xbf", raw[:3])
|
||||
|
||||
@@ -413,6 +413,12 @@ $stageOut = & python (Join-Path $root "_tests\check_python_syntax.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "Python 语法检查" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a8
|
||||
Banner "3a8/7 版本号一致性(防漏改)"
|
||||
$stageOut = & python (Join-Path $root "_tests\check_version_consistency.py") 2>&1
|
||||
Show-Fails $stageOut
|
||||
Record "版本号一致性" ($LASTEXITCODE -eq 0)
|
||||
|
||||
# ---------------------------------------------------------------- 3a5
|
||||
Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)"
|
||||
# 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」——
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.lanremote.watch"
|
||||
android:versionCode="5"
|
||||
android:versionName="1.8.2">
|
||||
android:versionName="1.9.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -27,7 +27,7 @@ $d8 = Join-Path $bt "d8.bat"
|
||||
|
||||
$work = Join-Path $android "build"
|
||||
$outDir = Join-Path $root "dist"
|
||||
$apkName = "ykxmz-watch-1.8.2.apk"
|
||||
$apkName = "ykxmz-watch-1.9.0.apk"
|
||||
|
||||
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
|
||||
|
||||
@@ -65,7 +65,7 @@ $unsigned = Join-Path $work "app-unsigned.apk"
|
||||
--min-sdk-version 24 `
|
||||
--target-sdk-version 34 `
|
||||
--version-code 5 `
|
||||
--version-name "1.8.2" `
|
||||
--version-name "1.9.0" `
|
||||
--auto-add-overlay
|
||||
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
========================================================
|
||||
|
||||
软件名称:ykxmz
|
||||
版本:1.8.2
|
||||
版本:1.9.0
|
||||
作者:小麦子
|
||||
|
||||
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
||||
@@ -197,7 +197,7 @@
|
||||
八、版本记录
|
||||
--------------------------------------------------------
|
||||
|
||||
1.8.2
|
||||
1.9.0
|
||||
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
||||
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
||||
合计 10 条连接),一种协议犯病另一种还在服务
|
||||
@@ -218,6 +218,6 @@
|
||||
|
||||
--------------------------------------------------------
|
||||
作者:小麦子
|
||||
软件:ykxmz 1.8.2
|
||||
软件:ykxmz 1.9.0
|
||||
许可条款见《许可协议》(LICENSE.txt)
|
||||
--------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
========================================================
|
||||
|
||||
软件名称:ykxmz
|
||||
版本:1.8.2
|
||||
版本:1.9.0
|
||||
作者:小麦子
|
||||
|
||||
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
||||
@@ -164,6 +164,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
||||
|
||||
--------------------------------------------------------
|
||||
作者:小麦子
|
||||
软件:ykxmz 1.8.2
|
||||
软件:ykxmz 1.9.0
|
||||
继续安装,即表示你已阅读并同意《许可协议》。
|
||||
--------------------------------------------------------
|
||||
|
||||
+19
-1
@@ -60,8 +60,26 @@
|
||||
## 三、发版必须更新版本号
|
||||
|
||||
> 用户原话:「自行更新版号」
|
||||
> 后来又补了怎么加的规定(见下表)
|
||||
|
||||
每次上传都要升版本号。版本号散落在多处,**用脚本统一改**:
|
||||
每次上传都要升版本号。**怎么加按改动性质定**:
|
||||
|
||||
| 改动性质 | 怎么加 | 例子 |
|
||||
|---|---|---|
|
||||
| **只修 bug** | 最后一位 +1 | 1.8.2 → **1.8.3** |
|
||||
| **加了新内容 / 重构** | 第二位 +1,**最后一位清 0** | 1.8.2 → **1.9.0** |
|
||||
|
||||
⚠️ **一轮里只要同时有"新东西"和"修 bug",就按新东西算**(升第二位)。
|
||||
比如某一轮既加了设置窗口的中转服务器字段(新功能)、
|
||||
又把面板切换重构成 `showOnly()`(重构)、还修了 502(bug)——
|
||||
那就是 **1.9.0**,不是 1.8.3。
|
||||
|
||||
**为什么这条重要**:下载站是按 URL 缓存的
|
||||
(`/download/ykxmz-setup-1.8.2.exe` 带 `max-age=604800, immutable`)。
|
||||
**内容变了但不换 URL,缓存就会失效一次** —— 实测重新打包 server.zip
|
||||
之后第一次请求 `cf-cache-status=MISS`。升版本号正好把 URL 换掉。
|
||||
|
||||
版本号散落在多处,**用脚本统一改**:
|
||||
|
||||
```powershell
|
||||
python _build\bump_180.py # 改版本号时把里面的 OLD/NEW 改掉
|
||||
|
||||
@@ -32,7 +32,22 @@ def build_license() -> None:
|
||||
"""三份文档拼成一份,给 Inno 的许可页用。
|
||||
|
||||
每份之间加分隔线,免得读起来像一份文档。
|
||||
|
||||
⚠ 这里**顺手把版本号注入进去**,不让它依赖源文档里写的是什么。
|
||||
|
||||
为什么:源文档里的版本号是硬编码的,实测 docs/LICENSE.txt 一直
|
||||
停在 1.7.1(升级脚本只替换当时那个版本号,比如 1.8.2 ->
|
||||
1.9.0,而 LICENSE.txt 里写的是 1.7.1,就永远匹配不上)。
|
||||
结果安装时许可页上显示的是 1.7.1。
|
||||
|
||||
改成在这里统一注入之后,源文档写什么都不影响:
|
||||
版本:X.Y.Z -> 版本:<当前版本>
|
||||
软件:ykxmz X.Y.Z -> 软件:ykxmz <当前版本>
|
||||
|
||||
⚠ 只改**头部和落款**这两个格式,不改"八、版本记录"里的历史条目
|
||||
—— 那是变更日志,本来就是一堆旧版本号。
|
||||
"""
|
||||
version = current_version()
|
||||
parts = []
|
||||
for index, name in enumerate(DOCS):
|
||||
path = ROOT / "docs" / name
|
||||
@@ -40,6 +55,23 @@ def build_license() -> None:
|
||||
print(f" [警告] 缺少 {name}")
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8-sig").strip()
|
||||
|
||||
# 把"版本:x.y.z"和"软件:ykxmz x.y.z"换成当前版本,
|
||||
# 但跳过"版本记录"那一段(历史日志)。
|
||||
out_lines = []
|
||||
in_history = False
|
||||
for line in text.splitlines():
|
||||
stripped = line.strip()
|
||||
if "版本记录" in stripped:
|
||||
in_history = True
|
||||
if not in_history:
|
||||
if re.match(r"^版本:\d+\.\d+\.\d+$", stripped):
|
||||
line = line.replace(stripped, f"版本:{version}")
|
||||
elif re.match(r"^软件:ykxmz \d+\.\d+\.\d+$", stripped):
|
||||
line = line.replace(stripped, f"软件:ykxmz {version}")
|
||||
out_lines.append(line)
|
||||
text = "\n".join(out_lines)
|
||||
|
||||
if index:
|
||||
parts.append("")
|
||||
parts.append("=" * 70)
|
||||
@@ -49,7 +81,8 @@ def build_license() -> None:
|
||||
|
||||
# 装许可页的正文要 UTF-8 **带 BOM**,否则 Inno 在中文系统上会显示乱码
|
||||
COMBINED.write_bytes(b"\xef\xbb\xbf" + body.encode("utf-8"))
|
||||
print(f" 合并三份文档 -> {COMBINED.name}({len(body)} 字符)")
|
||||
print(f" 合并三份文档 -> {COMBINED.name}"
|
||||
f"({len(body)} 字符,版本注入为 {version})")
|
||||
|
||||
|
||||
def sync_version(version: str) -> None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
========================================================
|
||||
|
||||
软件名称:ykxmz(远程管理工具)
|
||||
版本:1.7.1
|
||||
版本:1.9.0
|
||||
作者:小麦子
|
||||
生效日期:2026 年 10 月 5 日
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
|
||||
--------------------------------------------------------
|
||||
作者:小麦子
|
||||
软件:ykxmz 1.7.1
|
||||
软件:ykxmz 1.9.0
|
||||
|
||||
如果你不同意本协议的任何一条,请不要安装或使用本软件。
|
||||
--------------------------------------------------------
|
||||
@@ -162,7 +162,7 @@
|
||||
========================================================
|
||||
|
||||
软件名称:ykxmz
|
||||
版本:1.8.2
|
||||
版本:1.9.0
|
||||
作者:小麦子
|
||||
|
||||
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
|
||||
@@ -323,7 +323,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
||||
|
||||
--------------------------------------------------------
|
||||
作者:小麦子
|
||||
软件:ykxmz 1.8.2
|
||||
软件:ykxmz 1.9.0
|
||||
继续安装,即表示你已阅读并同意《许可协议》。
|
||||
--------------------------------------------------------
|
||||
|
||||
@@ -334,7 +334,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
||||
========================================================
|
||||
|
||||
软件名称:ykxmz
|
||||
版本:1.8.2
|
||||
版本:1.9.0
|
||||
作者:小麦子
|
||||
|
||||
装好之后看这一页:怎么确认它在跑、怎么改设置、
|
||||
@@ -528,7 +528,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
||||
八、版本记录
|
||||
--------------------------------------------------------
|
||||
|
||||
1.8.2
|
||||
1.9.0
|
||||
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
|
||||
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
|
||||
合计 10 条连接),一种协议犯病另一种还在服务
|
||||
@@ -549,6 +549,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
|
||||
|
||||
--------------------------------------------------------
|
||||
作者:小麦子
|
||||
软件:ykxmz 1.8.2
|
||||
软件:ykxmz 1.9.0
|
||||
许可条款见《许可协议》(LICENSE.txt)
|
||||
--------------------------------------------------------
|
||||
|
||||
+352
-352
@@ -1,352 +1,352 @@
|
||||
; ============================================================================
|
||||
; ykxmz 教室电脑手表遥控 · 被控端安装包
|
||||
;
|
||||
; 用 Inno Setup 6 编译:
|
||||
; "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\ykxmz.iss
|
||||
;
|
||||
; ── 设计说明 ──
|
||||
;
|
||||
; 尽量用 Inno 自带的机制,不手搓:
|
||||
; * 许可协议页 -> LicenseFile(三份文档合并成一份,点"我接受"才能继续)
|
||||
; * 开机自启 -> [Tasks](标准勾选框)
|
||||
; * 装完启动 -> [Run] postinstall(标准勾选框)
|
||||
; * 安装目录 -> DisableDirPage=no(让用户自己选,默认 Program Files)
|
||||
; * 卸载 -> [UninstallRun] 调被控端自己的 --uninstall
|
||||
; 只有"填 6 位配对码"这一页 Inno 没有现成的,用标准的
|
||||
; CreateInputQueryPage —— 那是 Inno 的官方 API,不算自己造轮子。
|
||||
;
|
||||
; ── 用户确定的设置 ──
|
||||
; 安装位置 默认 C:\Program Files\ykxmz,安装时可改
|
||||
; 数据目录 能写 exe 旁就写,不能就转 ProgramData(程序内部处理)
|
||||
; 三份文档 放安装包里,统一接受才能下一步
|
||||
; 配对码 安装向导里填 6 位
|
||||
; 快捷方式 不建开始菜单、不建桌面
|
||||
; 开机自启 默认勾上
|
||||
; 装完启动 立即静默启动
|
||||
; 卸载 一并删掉,什么都不留
|
||||
; ============================================================================
|
||||
|
||||
#define MyAppName "ykxmz"
|
||||
#define MyAppVersion "1.8.2"
|
||||
#define MyAppPublisher "小麦子"
|
||||
#define MyAppExeName "ykxmz.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{8F3C1D42-7A5E-4B91-9C6D-2E8F4A1B7C30}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
|
||||
; 默认装到 Program Files\ykxmz,但安装向导里有一页让用户自己改
|
||||
DefaultDirName={autopf}\ykxmz
|
||||
DisableDirPage=no
|
||||
|
||||
; 三份文档合并成一份许可页:看完点「我接受」才能继续
|
||||
LicenseFile=license-combined.txt
|
||||
|
||||
; 不建开始菜单组(用户明确不要)
|
||||
DisableProgramGroupPage=yes
|
||||
AllowNoIcons=yes
|
||||
|
||||
; 装到 Program Files 和改防火墙都要管理员
|
||||
PrivilegesRequired=admin
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
; 安装/升级前由 PrepareToInstall 自己强杀正在运行的被控端。
|
||||
;
|
||||
; 为什么不用 Inno 自带的关程序机制(默认开着):
|
||||
; 它只会礼貌地"请求"对方退出,遇上以管理员身份启动的常驻进程
|
||||
; 关不掉,然后弹出"安装程序无法自动关闭所有应用程序",
|
||||
; 静默模式下这个消息框被压成"终止",**安装直接回滚** ——
|
||||
; 用户看到的现象是"装了没反应/还是旧版本"。
|
||||
; (2026-10 实测踩到:旧实例在跑,新版本装不上,配对码也没写进去。)
|
||||
;
|
||||
; 我们自己在 PrepareToInstall 里 taskkill /F,干净利落。
|
||||
; 关掉 Inno 自带的那套,免得它先弹框把安装拦下来。
|
||||
CloseApplications=no
|
||||
RestartApplications=no
|
||||
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=ykxmz-setup-{#MyAppVersion}
|
||||
SetupIconFile=..\windows\app.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
UninstallDisplayName={#MyAppName}
|
||||
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ShowLanguageDialog=no
|
||||
DisableWelcomePage=no
|
||||
|
||||
[Languages]
|
||||
Name: "chinese"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
; 用户要的:默认勾上开机自启(取消勾选也允许)
|
||||
Name: "autostart"; Description: "开机自动启动(推荐给教室电脑)"; \
|
||||
GroupDescription: "启动选项:"; Flags: checkedonce
|
||||
|
||||
[Files]
|
||||
; 被控端整个目录(ykxmz.exe + _internal)
|
||||
Source: "..\dist_exe\ykxmz\*"; DestDir: "{app}"; \
|
||||
Flags: recursesubdirs createallsubdirs ignoreversion
|
||||
|
||||
; 三份文档也放一份到安装目录,装完还能翻
|
||||
Source: "..\docs\LICENSE.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
Source: "..\docs\BEFORE-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
Source: "..\docs\AFTER-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
|
||||
[Dirs]
|
||||
; ⚠ 这一节很关键 —— 不配的话装完一运行就报 WinError 5 拒绝访问。
|
||||
;
|
||||
; 被控端的配置存在 %ProgramData%\ykxmz\data 下(exe 旁边写不进去时的回退)。
|
||||
; 安装包是管理员跑的,它建的目录默认只有管理员能改;
|
||||
; 之后普通用户(老师的账号)跑被控端时:
|
||||
; 能新建文件,但覆盖不了管理员建的那份 settings.json
|
||||
; 结果就是"装完打开报权限错误"。
|
||||
;
|
||||
; Permissions: users-modify 把所有用户的写权限打开,才是真的能用。
|
||||
; 注意末尾不带反斜杠,否则 Inno 会给它单独建一层同名子目录。
|
||||
Name: "{commonappdata}\ykxmz"; Permissions: users-modify
|
||||
Name: "{commonappdata}\ykxmz\data"; Permissions: users-modify
|
||||
|
||||
[InstallDelete]
|
||||
; 早期版本把配置写在程序目录下的 data\ 里。
|
||||
; 现在数据统一放 ProgramData(见 config.py 的 data_dir),
|
||||
; 这里把旧的那份清掉 —— 留着的话,两份配置并存,
|
||||
; 以后排查问题会被误导("我明明改了配对码怎么没变")。
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
|
||||
[Run]
|
||||
; 收尾三步,顺序很重要:先写配对码,再设开机自启,最后才启动。
|
||||
;
|
||||
; ⚠ 为什么放在这里,而不是 [Code] 的 CurStepChanged(ssPostInstall):
|
||||
; 实测这个 Inno 版本的 ssPostInstall 回调**不会触发**
|
||||
; (日志显示回调进来了、CurStep=1,但 CurStep <> ssPostInstall 成立;
|
||||
; 而 Ord(ssPostInstall) 又报类型不匹配,没法打出来对比)。
|
||||
; 放在那里 --code 永远不执行,配对码永远是空的。
|
||||
; Run 段是安装完成后必然执行的,靠得住。
|
||||
;
|
||||
; {code:GetPairingCode} 是 Inno 的写法:调同名 Pascal 函数取值。
|
||||
; Check 函数返回 False 就跳过这一条。
|
||||
|
||||
; 1. 写配对码
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--code {code:GetPairingCode}"; \
|
||||
Flags: runhidden; Check: HasPairingCode
|
||||
|
||||
; 2. 开机自启
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
|
||||
Flags: runhidden; Tasks: autostart
|
||||
|
||||
; 3. 启动。
|
||||
; 非静默:标准 postinstall 勾选框,用户可以取消。
|
||||
; 静默: /VERYSILENT 下 postinstall 那条被 skipifsilent 跳过,所以单独来一条。
|
||||
;
|
||||
; ⚠ 都用 nowait —— --silent 是常驻守护进程,等它退出等于永远不返回。
|
||||
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
||||
; (PowerShell 的 Start-Process -Wait 就是)。
|
||||
; 学校批量部署请用 cmd:
|
||||
; cmd /c ykxmz-setup-1.8.2.exe /VERYSILENT /CODE=123456
|
||||
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Description: "立即启动(后台运行,不显示窗口)"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Flags: nowait runhidden; Check: WizardSilent
|
||||
|
||||
; 让用户看一眼装完须知
|
||||
Filename: "{app}\docs\AFTER-INSTALL.txt"; \
|
||||
Description: "打开「安装后须知」"; \
|
||||
Flags: postinstall shellexec unchecked skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
; 被控端会自己删数据目录,这里兜底再清一遍安装目录里的残留。
|
||||
; {commonappdata}\ykxmz 也要清 —— 安装时 [Dirs] 建了它,
|
||||
; 卸载时**不会自动删**([Dirs] 建的目录不归 [UninstallDelete] 管),
|
||||
; 不写会留个空目录在那儿。
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
Type: filesandordirs; Name: "{app}\docs"
|
||||
Type: filesandordirs; Name: "{app}\_internal"
|
||||
Type: filesandordirs; Name: "{commonappdata}\ykxmz"
|
||||
|
||||
; Inno 默认只在目录空时删它,而卸载器自己的临时文件
|
||||
; (_unins.tmp 之类)经常让目录"非空",于是留下一个空壳。
|
||||
; 显式写 dirifempty 兜底。
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Code]
|
||||
var
|
||||
CodePage: TInputQueryWizardPage;
|
||||
ForcedCode: String;
|
||||
|
||||
{ 把字符串里的非数字滤掉。 }
|
||||
function OnlyDigits(const raw: String): String;
|
||||
var
|
||||
i: Integer;
|
||||
ch: Char;
|
||||
begin
|
||||
Result := '';
|
||||
for i := 1 to Length(raw) do
|
||||
begin
|
||||
ch := raw[i];
|
||||
if (ch >= '0') and (ch <= '9') then
|
||||
Result := Result + ch;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ 配对码那一页。Inno 没有现成的输入页,用官方的 CreateInputQueryPage。 }
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
{ 静默部署可以带 /CODE=123456 直接把码传进来(电教批量装用这个)。
|
||||
注意:Inno 的 [Code] 段连**注释里的花括号**都会当常量去解析,
|
||||
所以这里不能直接写 param 那种写法,否则编译器报语法错误。 }
|
||||
ForcedCode := OnlyDigits(ExpandConstant('{param:CODE|}'));
|
||||
|
||||
CodePage := CreateInputQueryPage(wpSelectTasks,
|
||||
'配对码',
|
||||
'给这台电脑定一个 6 位配对码',
|
||||
'手表上要输这一串数字才能连上这台电脑。自己定个好记的,' +
|
||||
'比如 789113。装完之后也可以在被控端窗口里改。');
|
||||
CodePage.Add('6 位配对码:', False);
|
||||
if ForcedCode <> '' then
|
||||
CodePage.Values[0] := ForcedCode;
|
||||
end;
|
||||
|
||||
{ 校验:必须是 6 位数字。
|
||||
注意 —— **静默模式下这一页不会显示**,值是空的,
|
||||
这时候绝不能拦(否则 /VERYSILENT 安装必然失败,我第一版就栽在这)。 }
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
var
|
||||
raw: String;
|
||||
digits: String;
|
||||
begin
|
||||
Result := True;
|
||||
if CurPageID <> CodePage.ID then
|
||||
Exit;
|
||||
if WizardSilent then
|
||||
Exit;
|
||||
|
||||
raw := CodePage.Values[0];
|
||||
digits := OnlyDigits(raw);
|
||||
if (Length(digits) <> 6) and (raw <> '') then
|
||||
begin
|
||||
MsgBox('配对码要 6 位数字。' + #13#10 + #13#10 +
|
||||
'你现在填的是:' + raw + #13#10 +
|
||||
'(过滤掉非数字后剩 ' + IntToStr(Length(digits)) + ' 位)' + #13#10 + #13#10 +
|
||||
'重新填一下,或者直接点「取消」退出安装。',
|
||||
mbError, MB_OK);
|
||||
Result := False;
|
||||
Exit;
|
||||
end;
|
||||
{ 规范化,后面 [Run] 阶段用得到 }
|
||||
CodePage.Values[0] := digits;
|
||||
end;
|
||||
|
||||
{ 最终用的配对码:命令行 /CODE 优先,其次是向导里填的。
|
||||
两个都没有就返回空串 —— 那就跳过 --code,让被控端自己生成一个。 }
|
||||
function EffectiveCode(): String;
|
||||
begin
|
||||
Result := ForcedCode;
|
||||
if Result = '' then
|
||||
Result := OnlyDigits(CodePage.Values[0]);
|
||||
if Length(Result) <> 6 then
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
{ 收尾命令现在放在 [Run] 段(见那里的注释)。
|
||||
原来放在 CurStepChanged(ssPostInstall) 里 —— 实测这个 Inno 版本的
|
||||
ssPostInstall **根本不会触发**,所以 --code 从来没执行过,
|
||||
配对码永远是空的。留这个函数只是为了以后有人想改回来时能看到这段历史。 }
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
{ 故意什么都不做。真要恢复旧写法,先确认 ssPostInstall 会触发。 }
|
||||
end;
|
||||
|
||||
{ ===== 给 [Run] 段用的取值 / 判断函数 ===== }
|
||||
|
||||
{ 配对码。配 HasPairingCode 一起用 —— 空串时那条命令会被跳过。 }
|
||||
function GetPairingCode(Param: String): String;
|
||||
begin
|
||||
Result := EffectiveCode();
|
||||
end;
|
||||
|
||||
{ 有配对码才跑那条 --code 命令 }
|
||||
function HasPairingCode(): Boolean;
|
||||
begin
|
||||
Result := EffectiveCode() <> '';
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||||
|
||||
必须在这里做,**不能**改到 UninstallRun 区段去:
|
||||
UninstallRun 是在文件都删完之后才跑的,
|
||||
那时候"停进程"已经没意义了(文件早没了)。
|
||||
|
||||
必须带 --keep-dir:
|
||||
InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||||
不带 --keep-dir 的话被控端会安排一个后台 rmdir 删安装目录,
|
||||
而卸载器紧接着也在删同一个目录 —— 两边抢文件会**永久卡住**。
|
||||
(2026-10 实测:从控制面板卸载无限等待。两个原因叠加:
|
||||
这一行没带 --keep-dir;以及被控端里 input() 在无 stdin 时阻塞。)
|
||||
}
|
||||
{ 安装开始前强杀正在运行的被控端。
|
||||
|
||||
必须自己杀,不能指望 Inno 自带的关程序机制 ——
|
||||
它关不掉以管理员身份启动的常驻进程,然后弹框,静默模式下
|
||||
被压成"终止",导致整个安装回滚。
|
||||
|
||||
taskkill /F /IM ykxmz.exe 只会杀别的实例;
|
||||
这个进程本身是 setup 的辅助进程,名字不是 ykxmz.exe,不受影响。
|
||||
|
||||
返回空串 = 没问题,可以继续安装。 }
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
NeedsRestart := False;
|
||||
|
||||
if Exec('taskkill.exe', '/F /IM ykxmz.exe', '',
|
||||
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||||
begin
|
||||
{ 0 = 杀掉了;128 = 本来就没有在跑。都算正常。 }
|
||||
end;
|
||||
|
||||
{ 给它一点时间释放文件句柄,否则覆盖 exe 还是会"拒绝访问" }
|
||||
Sleep(1200);
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||||
|
||||
三个参数缺一不可,每个都对应一个实测踩过的卡死/残留问题:
|
||||
|
||||
* 必须在这里做,**不能**改到 UninstallRun 区段去
|
||||
—— UninstallRun 是在文件都删完之后才跑的,
|
||||
那时候"停进程"已经没意义了(文件早没了)。
|
||||
|
||||
* 必须带 --keep-dir
|
||||
—— InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||||
不带的话被控端会安排一个后台 rmdir 删安装目录,
|
||||
而卸载器紧接着也在删同一个目录 —— 两边抢文件**永久卡住**。
|
||||
|
||||
* 必须带 --quiet
|
||||
—— 卸载器是 runhidden 调的,进程没有控制台。
|
||||
被控端里的 attach_console() 会 AllocConsole + open("CONIN$"),
|
||||
在无控制台的隐藏进程里**永久阻塞**;input() 也因为没 stdin 卡住。
|
||||
实测卡住的进程只有 1 个线程、0.16 秒 CPU,就是停在这一步。
|
||||
--quiet 让它彻底不碰控制台,结果写进 uninstall.log。
|
||||
实测:不带 --quiet 无限卡住,带了 0.6 秒返回。
|
||||
}
|
||||
function InitializeUninstall(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
Exec(ExpandConstant('{app}\{#MyAppExeName}'),
|
||||
'--uninstall --keep-dir --quiet',
|
||||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
; ============================================================================
|
||||
; ykxmz 教室电脑手表遥控 · 被控端安装包
|
||||
;
|
||||
; 用 Inno Setup 6 编译:
|
||||
; "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\ykxmz.iss
|
||||
;
|
||||
; ── 设计说明 ──
|
||||
;
|
||||
; 尽量用 Inno 自带的机制,不手搓:
|
||||
; * 许可协议页 -> LicenseFile(三份文档合并成一份,点"我接受"才能继续)
|
||||
; * 开机自启 -> [Tasks](标准勾选框)
|
||||
; * 装完启动 -> [Run] postinstall(标准勾选框)
|
||||
; * 安装目录 -> DisableDirPage=no(让用户自己选,默认 Program Files)
|
||||
; * 卸载 -> [UninstallRun] 调被控端自己的 --uninstall
|
||||
; 只有"填 6 位配对码"这一页 Inno 没有现成的,用标准的
|
||||
; CreateInputQueryPage —— 那是 Inno 的官方 API,不算自己造轮子。
|
||||
;
|
||||
; ── 用户确定的设置 ──
|
||||
; 安装位置 默认 C:\Program Files\ykxmz,安装时可改
|
||||
; 数据目录 能写 exe 旁就写,不能就转 ProgramData(程序内部处理)
|
||||
; 三份文档 放安装包里,统一接受才能下一步
|
||||
; 配对码 安装向导里填 6 位
|
||||
; 快捷方式 不建开始菜单、不建桌面
|
||||
; 开机自启 默认勾上
|
||||
; 装完启动 立即静默启动
|
||||
; 卸载 一并删掉,什么都不留
|
||||
; ============================================================================
|
||||
|
||||
#define MyAppName "ykxmz"
|
||||
#define MyAppVersion "1.9.0"
|
||||
#define MyAppPublisher "小麦子"
|
||||
#define MyAppExeName "ykxmz.exe"
|
||||
|
||||
[Setup]
|
||||
AppId={{8F3C1D42-7A5E-4B91-9C6D-2E8F4A1B7C30}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
VersionInfoVersion={#MyAppVersion}
|
||||
|
||||
; 默认装到 Program Files\ykxmz,但安装向导里有一页让用户自己改
|
||||
DefaultDirName={autopf}\ykxmz
|
||||
DisableDirPage=no
|
||||
|
||||
; 三份文档合并成一份许可页:看完点「我接受」才能继续
|
||||
LicenseFile=license-combined.txt
|
||||
|
||||
; 不建开始菜单组(用户明确不要)
|
||||
DisableProgramGroupPage=yes
|
||||
AllowNoIcons=yes
|
||||
|
||||
; 装到 Program Files 和改防火墙都要管理员
|
||||
PrivilegesRequired=admin
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
; 安装/升级前由 PrepareToInstall 自己强杀正在运行的被控端。
|
||||
;
|
||||
; 为什么不用 Inno 自带的关程序机制(默认开着):
|
||||
; 它只会礼貌地"请求"对方退出,遇上以管理员身份启动的常驻进程
|
||||
; 关不掉,然后弹出"安装程序无法自动关闭所有应用程序",
|
||||
; 静默模式下这个消息框被压成"终止",**安装直接回滚** ——
|
||||
; 用户看到的现象是"装了没反应/还是旧版本"。
|
||||
; (2026-10 实测踩到:旧实例在跑,新版本装不上,配对码也没写进去。)
|
||||
;
|
||||
; 我们自己在 PrepareToInstall 里 taskkill /F,干净利落。
|
||||
; 关掉 Inno 自带的那套,免得它先弹框把安装拦下来。
|
||||
CloseApplications=no
|
||||
RestartApplications=no
|
||||
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=ykxmz-setup-{#MyAppVersion}
|
||||
SetupIconFile=..\windows\app.ico
|
||||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||
UninstallDisplayName={#MyAppName}
|
||||
|
||||
Compression=lzma2/max
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
ShowLanguageDialog=no
|
||||
DisableWelcomePage=no
|
||||
|
||||
[Languages]
|
||||
Name: "chinese"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
; 用户要的:默认勾上开机自启(取消勾选也允许)
|
||||
Name: "autostart"; Description: "开机自动启动(推荐给教室电脑)"; \
|
||||
GroupDescription: "启动选项:"; Flags: checkedonce
|
||||
|
||||
[Files]
|
||||
; 被控端整个目录(ykxmz.exe + _internal)
|
||||
Source: "..\dist_exe\ykxmz\*"; DestDir: "{app}"; \
|
||||
Flags: recursesubdirs createallsubdirs ignoreversion
|
||||
|
||||
; 三份文档也放一份到安装目录,装完还能翻
|
||||
Source: "..\docs\LICENSE.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
Source: "..\docs\BEFORE-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
Source: "..\docs\AFTER-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||||
|
||||
[Dirs]
|
||||
; ⚠ 这一节很关键 —— 不配的话装完一运行就报 WinError 5 拒绝访问。
|
||||
;
|
||||
; 被控端的配置存在 %ProgramData%\ykxmz\data 下(exe 旁边写不进去时的回退)。
|
||||
; 安装包是管理员跑的,它建的目录默认只有管理员能改;
|
||||
; 之后普通用户(老师的账号)跑被控端时:
|
||||
; 能新建文件,但覆盖不了管理员建的那份 settings.json
|
||||
; 结果就是"装完打开报权限错误"。
|
||||
;
|
||||
; Permissions: users-modify 把所有用户的写权限打开,才是真的能用。
|
||||
; 注意末尾不带反斜杠,否则 Inno 会给它单独建一层同名子目录。
|
||||
Name: "{commonappdata}\ykxmz"; Permissions: users-modify
|
||||
Name: "{commonappdata}\ykxmz\data"; Permissions: users-modify
|
||||
|
||||
[InstallDelete]
|
||||
; 早期版本把配置写在程序目录下的 data\ 里。
|
||||
; 现在数据统一放 ProgramData(见 config.py 的 data_dir),
|
||||
; 这里把旧的那份清掉 —— 留着的话,两份配置并存,
|
||||
; 以后排查问题会被误导("我明明改了配对码怎么没变")。
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
|
||||
[Run]
|
||||
; 收尾三步,顺序很重要:先写配对码,再设开机自启,最后才启动。
|
||||
;
|
||||
; ⚠ 为什么放在这里,而不是 [Code] 的 CurStepChanged(ssPostInstall):
|
||||
; 实测这个 Inno 版本的 ssPostInstall 回调**不会触发**
|
||||
; (日志显示回调进来了、CurStep=1,但 CurStep <> ssPostInstall 成立;
|
||||
; 而 Ord(ssPostInstall) 又报类型不匹配,没法打出来对比)。
|
||||
; 放在那里 --code 永远不执行,配对码永远是空的。
|
||||
; Run 段是安装完成后必然执行的,靠得住。
|
||||
;
|
||||
; {code:GetPairingCode} 是 Inno 的写法:调同名 Pascal 函数取值。
|
||||
; Check 函数返回 False 就跳过这一条。
|
||||
|
||||
; 1. 写配对码
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--code {code:GetPairingCode}"; \
|
||||
Flags: runhidden; Check: HasPairingCode
|
||||
|
||||
; 2. 开机自启
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
|
||||
Flags: runhidden; Tasks: autostart
|
||||
|
||||
; 3. 启动。
|
||||
; 非静默:标准 postinstall 勾选框,用户可以取消。
|
||||
; 静默: /VERYSILENT 下 postinstall 那条被 skipifsilent 跳过,所以单独来一条。
|
||||
;
|
||||
; ⚠ 都用 nowait —— --silent 是常驻守护进程,等它退出等于永远不返回。
|
||||
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
||||
; (PowerShell 的 Start-Process -Wait 就是)。
|
||||
; 学校批量部署请用 cmd:
|
||||
; cmd /c ykxmz-setup-1.9.0.exe /VERYSILENT /CODE=123456
|
||||
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Description: "立即启动(后台运行,不显示窗口)"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
|
||||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||||
Flags: nowait runhidden; Check: WizardSilent
|
||||
|
||||
; 让用户看一眼装完须知
|
||||
Filename: "{app}\docs\AFTER-INSTALL.txt"; \
|
||||
Description: "打开「安装后须知」"; \
|
||||
Flags: postinstall shellexec unchecked skipifsilent
|
||||
|
||||
[UninstallDelete]
|
||||
; 被控端会自己删数据目录,这里兜底再清一遍安装目录里的残留。
|
||||
; {commonappdata}\ykxmz 也要清 —— 安装时 [Dirs] 建了它,
|
||||
; 卸载时**不会自动删**([Dirs] 建的目录不归 [UninstallDelete] 管),
|
||||
; 不写会留个空目录在那儿。
|
||||
Type: filesandordirs; Name: "{app}\data"
|
||||
Type: filesandordirs; Name: "{app}\docs"
|
||||
Type: filesandordirs; Name: "{app}\_internal"
|
||||
Type: filesandordirs; Name: "{commonappdata}\ykxmz"
|
||||
|
||||
; Inno 默认只在目录空时删它,而卸载器自己的临时文件
|
||||
; (_unins.tmp 之类)经常让目录"非空",于是留下一个空壳。
|
||||
; 显式写 dirifempty 兜底。
|
||||
Type: dirifempty; Name: "{app}"
|
||||
|
||||
[Code]
|
||||
var
|
||||
CodePage: TInputQueryWizardPage;
|
||||
ForcedCode: String;
|
||||
|
||||
{ 把字符串里的非数字滤掉。 }
|
||||
function OnlyDigits(const raw: String): String;
|
||||
var
|
||||
i: Integer;
|
||||
ch: Char;
|
||||
begin
|
||||
Result := '';
|
||||
for i := 1 to Length(raw) do
|
||||
begin
|
||||
ch := raw[i];
|
||||
if (ch >= '0') and (ch <= '9') then
|
||||
Result := Result + ch;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ 配对码那一页。Inno 没有现成的输入页,用官方的 CreateInputQueryPage。 }
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
{ 静默部署可以带 /CODE=123456 直接把码传进来(电教批量装用这个)。
|
||||
注意:Inno 的 [Code] 段连**注释里的花括号**都会当常量去解析,
|
||||
所以这里不能直接写 param 那种写法,否则编译器报语法错误。 }
|
||||
ForcedCode := OnlyDigits(ExpandConstant('{param:CODE|}'));
|
||||
|
||||
CodePage := CreateInputQueryPage(wpSelectTasks,
|
||||
'配对码',
|
||||
'给这台电脑定一个 6 位配对码',
|
||||
'手表上要输这一串数字才能连上这台电脑。自己定个好记的,' +
|
||||
'比如 789113。装完之后也可以在被控端窗口里改。');
|
||||
CodePage.Add('6 位配对码:', False);
|
||||
if ForcedCode <> '' then
|
||||
CodePage.Values[0] := ForcedCode;
|
||||
end;
|
||||
|
||||
{ 校验:必须是 6 位数字。
|
||||
注意 —— **静默模式下这一页不会显示**,值是空的,
|
||||
这时候绝不能拦(否则 /VERYSILENT 安装必然失败,我第一版就栽在这)。 }
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
var
|
||||
raw: String;
|
||||
digits: String;
|
||||
begin
|
||||
Result := True;
|
||||
if CurPageID <> CodePage.ID then
|
||||
Exit;
|
||||
if WizardSilent then
|
||||
Exit;
|
||||
|
||||
raw := CodePage.Values[0];
|
||||
digits := OnlyDigits(raw);
|
||||
if (Length(digits) <> 6) and (raw <> '') then
|
||||
begin
|
||||
MsgBox('配对码要 6 位数字。' + #13#10 + #13#10 +
|
||||
'你现在填的是:' + raw + #13#10 +
|
||||
'(过滤掉非数字后剩 ' + IntToStr(Length(digits)) + ' 位)' + #13#10 + #13#10 +
|
||||
'重新填一下,或者直接点「取消」退出安装。',
|
||||
mbError, MB_OK);
|
||||
Result := False;
|
||||
Exit;
|
||||
end;
|
||||
{ 规范化,后面 [Run] 阶段用得到 }
|
||||
CodePage.Values[0] := digits;
|
||||
end;
|
||||
|
||||
{ 最终用的配对码:命令行 /CODE 优先,其次是向导里填的。
|
||||
两个都没有就返回空串 —— 那就跳过 --code,让被控端自己生成一个。 }
|
||||
function EffectiveCode(): String;
|
||||
begin
|
||||
Result := ForcedCode;
|
||||
if Result = '' then
|
||||
Result := OnlyDigits(CodePage.Values[0]);
|
||||
if Length(Result) <> 6 then
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
{ 收尾命令现在放在 [Run] 段(见那里的注释)。
|
||||
原来放在 CurStepChanged(ssPostInstall) 里 —— 实测这个 Inno 版本的
|
||||
ssPostInstall **根本不会触发**,所以 --code 从来没执行过,
|
||||
配对码永远是空的。留这个函数只是为了以后有人想改回来时能看到这段历史。 }
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
{ 故意什么都不做。真要恢复旧写法,先确认 ssPostInstall 会触发。 }
|
||||
end;
|
||||
|
||||
{ ===== 给 [Run] 段用的取值 / 判断函数 ===== }
|
||||
|
||||
{ 配对码。配 HasPairingCode 一起用 —— 空串时那条命令会被跳过。 }
|
||||
function GetPairingCode(Param: String): String;
|
||||
begin
|
||||
Result := EffectiveCode();
|
||||
end;
|
||||
|
||||
{ 有配对码才跑那条 --code 命令 }
|
||||
function HasPairingCode(): Boolean;
|
||||
begin
|
||||
Result := EffectiveCode() <> '';
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||||
|
||||
必须在这里做,**不能**改到 UninstallRun 区段去:
|
||||
UninstallRun 是在文件都删完之后才跑的,
|
||||
那时候"停进程"已经没意义了(文件早没了)。
|
||||
|
||||
必须带 --keep-dir:
|
||||
InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||||
不带 --keep-dir 的话被控端会安排一个后台 rmdir 删安装目录,
|
||||
而卸载器紧接着也在删同一个目录 —— 两边抢文件会**永久卡住**。
|
||||
(2026-10 实测:从控制面板卸载无限等待。两个原因叠加:
|
||||
这一行没带 --keep-dir;以及被控端里 input() 在无 stdin 时阻塞。)
|
||||
}
|
||||
{ 安装开始前强杀正在运行的被控端。
|
||||
|
||||
必须自己杀,不能指望 Inno 自带的关程序机制 ——
|
||||
它关不掉以管理员身份启动的常驻进程,然后弹框,静默模式下
|
||||
被压成"终止",导致整个安装回滚。
|
||||
|
||||
taskkill /F /IM ykxmz.exe 只会杀别的实例;
|
||||
这个进程本身是 setup 的辅助进程,名字不是 ykxmz.exe,不受影响。
|
||||
|
||||
返回空串 = 没问题,可以继续安装。 }
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := '';
|
||||
NeedsRestart := False;
|
||||
|
||||
if Exec('taskkill.exe', '/F /IM ykxmz.exe', '',
|
||||
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||||
begin
|
||||
{ 0 = 杀掉了;128 = 本来就没有在跑。都算正常。 }
|
||||
end;
|
||||
|
||||
{ 给它一点时间释放文件句柄,否则覆盖 exe 还是会"拒绝访问" }
|
||||
Sleep(1200);
|
||||
end;
|
||||
|
||||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||||
|
||||
三个参数缺一不可,每个都对应一个实测踩过的卡死/残留问题:
|
||||
|
||||
* 必须在这里做,**不能**改到 UninstallRun 区段去
|
||||
—— UninstallRun 是在文件都删完之后才跑的,
|
||||
那时候"停进程"已经没意义了(文件早没了)。
|
||||
|
||||
* 必须带 --keep-dir
|
||||
—— InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||||
不带的话被控端会安排一个后台 rmdir 删安装目录,
|
||||
而卸载器紧接着也在删同一个目录 —— 两边抢文件**永久卡住**。
|
||||
|
||||
* 必须带 --quiet
|
||||
—— 卸载器是 runhidden 调的,进程没有控制台。
|
||||
被控端里的 attach_console() 会 AllocConsole + open("CONIN$"),
|
||||
在无控制台的隐藏进程里**永久阻塞**;input() 也因为没 stdin 卡住。
|
||||
实测卡住的进程只有 1 个线程、0.16 秒 CPU,就是停在这一步。
|
||||
--quiet 让它彻底不碰控制台,结果写进 uninstall.log。
|
||||
实测:不带 --quiet 无限卡住,带了 0.6 秒返回。
|
||||
}
|
||||
function InitializeUninstall(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
Exec(ExpandConstant('{app}\{#MyAppExeName}'),
|
||||
'--uninstall --keep-dir --quiet',
|
||||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ from collections import deque
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = "1.8.2"
|
||||
VERSION = "1.9.0"
|
||||
|
||||
# ---------------- 参数(都能用环境变量覆盖)----------------
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""局域网遥控 · Windows 被控端。"""
|
||||
|
||||
__version__ = "1.8.2"
|
||||
__version__ = "1.9.0"
|
||||
APP_NAME = "ykxmz"
|
||||
|
||||
@@ -27,12 +27,12 @@ VSVersionInfo(
|
||||
[
|
||||
StringStruct("CompanyName", "ykxmz"),
|
||||
StringStruct("FileDescription", "ykxmz"),
|
||||
StringStruct("FileVersion", "1.7.1.0"),
|
||||
StringStruct("FileVersion", "1.9.0.0"),
|
||||
StringStruct("InternalName", "ykxmz"),
|
||||
StringStruct("LegalCopyright", "ykxmz"),
|
||||
StringStruct("OriginalFilename", "ykxmz.exe"),
|
||||
StringStruct("ProductName", "ykxmz"),
|
||||
StringStruct("ProductVersion", "1.7.1.0"),
|
||||
StringStruct("ProductVersion", "1.9.0.0"),
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user