Skip to content

Commit c5a69fe

Browse files
committed
fix(installer): only kill QMT Gateway python procs (precise WMI filter)
1 parent 5ccb67d commit c5a69fe

3 files changed

Lines changed: 95 additions & 13 deletions

File tree

installer/installer.nsi

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,23 @@ Function .onInit
210210
; used directly; NSIS will not pop a language picker because no picker
211211
; macro is invoked here.
212212
;
213-
; 升级安装前先把已注册的开机自启计划任务停掉——避免它在我们解压
214-
; embedded Python 时拉起 start-silent.bat → python.exe 持有
215-
; python313.dll 等文件,导致 tar 报 "Can't unlink already-existing
216-
; object" 整个安装失败。停掉后计划任务的定义还在,install 完成
217-
; 后用户可以重新启用"开机自启"组件(它会 register-task.ps1 重新注册)。
218-
nsExec::ExecToLog 'schtasks /end /tn "QMT Gateway" >nul 2>&1 & exit 0'
213+
; 升级安装前必须杀掉本应用持有的 python.exe 进程——否则后续 tar -xf
214+
; 解压 python-embed.zip 会因为 .pyd/.dll 被进程持有而报
215+
; "Can't unlink already-existing object" 并段错误退出(HEAP_CORRUPTION
216+
; -1073740940)。
217+
;
218+
; 精确匹配:用 PowerShell + WMI 查询 CommandLine 含 "qmt_gateway" 或
219+
; "quantide-gateway" 的 python.exe,**只杀这些**。不动用户的其它
220+
; Python IDE / 脚本 / notebook / jupyter / venv 等。
221+
; 计划任务 schtasks /end 也在并行做——避免它在我们 kill 后立刻重新拉
222+
; 起 start-silent.bat → python.exe。停掉后计划任务定义还在,install
223+
; 完成后用户可以重新启用"开机自启"组件(它会 register-task.ps1 重建)。
224+
;
225+
; PowerShell 逻辑放到独立 .ps1 文件里——NSIS -Command 内联复杂 PowerShell
226+
; 会因为嵌套引号、CJK、$^ 转义等问题变得不可读且易错。
227+
InitPluginsDir
228+
File /oname=$PLUGINSDIR\kill-qmt-gateway-procs.ps1 "kill-qmt-gateway-procs.ps1"
229+
nsExec::ExecToLog 'cmd.exe /c schtasks /end /tn "QMT Gateway" >nul 2>&1 & powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$PLUGINSDIR\kill-qmt-gateway-procs.ps1" >nul 2>&1 & exit 0'
219230
FunctionEnd
220231

221232
Function .onVerifyInstDir
@@ -250,13 +261,16 @@ Section "-Core" SEC_CORE
250261
; 上一次 install 中途崩溃后可能留下半套残留文件(python.exe / .pyd / .dll
251262
; 仍被进程持有,或被 TrustedInstaller / AV 接管所有者),NSIS 自带的
252263
; RMDir /r 拿不动这些文件——结果后续 tar -xf 报 "Can't unlink
253-
; already-existing object" 整个安装中止。先用 takeown + icacls + cmd
254-
; rmdir 强制清掉 $INSTDIR\python 子目录(不能清整个 $INSTDIR,否则会
255-
; 把接下来要复制的 install-python.ps1 也一起干掉)。如果 cmd rmdir
256-
; 因为文件锁失败也没关系——后续 SetOverwrite on 会覆盖大部分文件,
257-
; 残留的旧文件不会阻塞 tar 解压(tar -xf 会报 warning 但失败的文件
258-
; 在下一轮重装时再清)。
259-
nsExec::ExecToLog 'cmd.exe /c takeown /f "$INSTDIR\python" /a /r /d y >nul 2>&1 & icacls "$INSTDIR\python" /grant Administrators:F /t /c /q >nul 2>&1 & cd /d "%TEMP%" & rmdir /s /q "$INSTDIR\python" >nul 2>&1 & exit 0'
264+
; already-existing object" 并以 STATUS_HEAP_CORRUPTION (-1073740940)
265+
; 段错误退出,导致整个安装中止。先用 takeown + icacls + cmd rmdir 强
266+
; 制清掉 $INSTDIR\python 子目录(不能清整个 $INSTDIR,否则会把接下
267+
; 来要复制的 install-python.ps1 也一起干掉)。如果 cmd rmdir 因为文
268+
; 件锁失败也没关系——后续 SetOverwrite on 会覆盖大部分文件,残留的
269+
; 旧文件不会阻塞 tar 解压(tar -xf 会报 warning 但失败的文件在下一
270+
; 轮重装时再清)。
271+
;
272+
; 重试 3 次:第一次可能因为某个文件刚被释放需要等 1s。
273+
nsExec::ExecToLog 'cmd.exe /c for /l %i in (1,1,3) do (takeown /f "$INSTDIR\python" /a /r /d y >nul 2>&1 & icacls "$INSTDIR\python" /grant Administrators:F /t /c /q >nul 2>&1 & cd /d "%TEMP%" & rmdir /s /q "$INSTDIR\python" >nul 2>&1 & if not exist "$INSTDIR\python" exit 0 & ping -n 2 127.0.0.1 >nul) & exit 0'
260274

261275
File /oname=${INSTALLER_SCRIPT_NAME} "install-python.ps1"
262276
File "scrub-stale-installs.ps1"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""杀进程 helper:只杀 QMT Gateway 相关的 python.exe。
2+
3+
由 installer.nsi 在 .onInit 阶段调用,避免 tar -xf 解压 python-embed.zip
4+
时遇到被进程持有的 .pyd/.dll 报 "Can't unlink already-existing object"
5+
并段错误退出(HEAP_CORRUPTION -1073740940)。
6+
7+
精确匹配:CommandLine 含 "qmt_gateway" 或 "quantide-gateway"(install
8+
路径)——不动用户的其它 Python IDE / 脚本 / notebook / jupyter 等。
9+
"""
10+
$ErrorActionPreference = 'SilentlyContinue'
11+
12+
$pidsToKill = @()
13+
Get-CimInstance Win32_Process -Filter "Name='python.exe'" | ForEach-Object {
14+
$cmd = [string]$_.CommandLine
15+
if ($cmd -match 'qmt_gateway|quantide-gateway') {
16+
$pidsToKill += $_.ProcessId
17+
}
18+
}
19+
20+
foreach ($pid in $pidsToKill) {
21+
try {
22+
Stop-Process -Id $pid -Force -ErrorAction Stop
23+
} catch {
24+
# 进程可能在我们检查后退出;忽略
25+
}
26+
}
27+
28+
# 等 2 秒让 Windows 释放文件句柄
29+
Start-Sleep -Seconds 2
30+
exit 0

tests/test_installer_nsi.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,3 +754,41 @@ def test_installer_nsi_no_ansi_create_shortcut():
754754
"installer.nsi must invoke create-shortcuts.ps1 to build start menu"
755755
)
756756

757+
758+
def test_installer_kills_only_qmt_gateway_python_processes():
759+
"""installer.nsi 必须只杀 QMT Gateway 相关的 python.exe 进程,不能误杀
760+
用户其它的 Python IDE / 脚本 / notebook / jupyter / venv 等。
761+
762+
历史 bug:曾用 taskkill /F /IM python.exe /T 杀掉全部 python 进程。
763+
"""
764+
text = INSTALLER_NSI.read_text(encoding="utf-8-sig")
765+
kill_ps1 = ROOT / "installer" / "kill-qmt-gateway-procs.ps1"
766+
767+
assert kill_ps1.is_file(), "Missing installer/kill-qmt-gateway-procs.ps1"
768+
ps_text = kill_ps1.read_text(encoding="utf-8-sig")
769+
770+
# NSIS 必须把脚本放到 $PLUGINSDIR 然后用 -File 调用(不能用内联 -Command)
771+
assert "kill-qmt-gateway-procs.ps1" in text, (
772+
"installer.nsi must invoke kill-qmt-gateway-procs.ps1"
773+
)
774+
assert "InitPluginsDir" in text, (
775+
"installer.nsi must call InitPluginsDir before File /oname=$PLUGINSDIR\\..."
776+
)
777+
assert "taskkill" not in text or "taskkill /F /IM python.exe" not in text, (
778+
"installer.nsi must NOT use `taskkill /F /IM python.exe` — would kill "
779+
"user's other Python processes (IDEs, notebooks, venvs)"
780+
)
781+
782+
# kill-qmt-gateway-procs.ps1 必须精确过滤 CommandLine
783+
assert "qmt_gateway" in ps_text and "quantide-gateway" in ps_text, (
784+
"kill-procs.ps1 must filter CommandLine on both 'qmt_gateway' and "
785+
"'quantide-gateway' to identify our processes"
786+
)
787+
assert "Get-CimInstance Win32_Process" in ps_text, (
788+
"kill-procs.ps1 must use WMI (Get-CimInstance Win32_Process) to inspect "
789+
"CommandLine before killing — taskkill /IM python.exe is too broad"
790+
)
791+
assert "Stop-Process" in ps_text, (
792+
"kill-procs.ps1 must call Stop-Process on the filtered PIDs"
793+
)
794+

0 commit comments

Comments
 (0)