Windows MSI 安装与维护排查
Windows MSI 安装与维护排查
MSI 安装器返回成功,却仍然运行旧程序;应用文件还在,SYSTEM 修复却说产品不存在;缓存 MSI 没丢,维护又要求原始介质。这些问题都需要把 Windows Installer 的产品实例、安装源和文件覆盖判断分开看。
下面以 Windows 10 Enterprise 21H1(19043.2364)、Windows Installer 5.0 为例,分别检查 Source Report 的安装源、Version Report 的文件覆盖规则和 Report 的用户安装上下文。三个示例是不同产品,执行时必须换成待维护产品的准确代码和路径。
1. 产品身份与版本
| 信息 | 用途 |
|---|---|
| ProductCode | 定位产品,但仍需对应 context/SID |
| PackageCode | 区分具体 MSI 包,不能仅靠文件名 |
| ProductVersion | 产品登记更新级别 |
| EXE 的 FileVersion | 参与文件覆盖决策,与产品版本不是一回事 |
| context 与用户 SID | 定位正确的按用户或按机器实例 |
| InstallSource、LocalPackage | 原源位置与本地缓存数据库,不能互相替代 |
| 实际启动路径、文件哈希、业务输出 | 证明最终运行了什么 |
记录原命令、执行身份、完整日志和业务配置,再决定是修复、更新还是迁移上下文。不要从“程序和功能有名字”直接推断 SYSTEM 能维护它。
2. 修复源缺失
Source Report 由 Report.msi 与外部 payload.cab 组成,业务输出 REPORT_TOTAL=42。只移动原源目录,对已安装程序暂时没有影响;之后再删除目标 EXE 并执行原修复,维护失败:
| 条件 | 修复 | 业务 |
|---|---|---|
| 原源可用,目标 EXE 缺失 | 0 | 文件恢复,输出 42 |
| 原源已移动,已安装 EXE 完好 | 未执行维护 | 仍能输出 42 |
| 原源已移动,EXE 再次缺失 | 1603;内层 1706 | EXE 未恢复 |
| 恢复原字节介质再维护 | 0 | 文件与输出恢复 |
详细日志留下:
SOURCEMGMT: Source is invalid due to missing/inaccessible package.
SOURCEMGMT: Failed to resolve source
Error 1706. An installation package for the product Source Report cannot be found. Try the installation again using a valid copy of the installation package 'Report.msi'.缓存 MSI 保存维护需要的数据库,不保证包含这个包的外部负载。别因此删除 %windir%\Installer,也别只补一个同名 MSI。实验放入另一 ProductCode/PackageCode 的 Report.msi 后,日志改为 invalid package code/product code doesn't match,仍然失败。

提示里的 network resource 是通用措辞,原源可以是本地目录。判断依据应是日志里的实际路径、产品和包身份,不是这一句界面文字。
对这一个机器级实验产品读取维护元数据:
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
$Product='{61FBFAE5-E491-42CC-A4B9-ACF45C6D7702}'
$Source='C:\SourceReport\source'
$Exe='C:\Program Files (x86)\SourceReport\Report.exe'
$Log='C:\SourceReport\fix-repair.log'
$Installer=New-Object -ComObject WindowsInstaller.Installer
$Installer.ProductInfo($Product,'ProductName')
$Installer.ProductInfo($Product,'VersionString')
$Installer.ProductInfo($Product,'InstallSource')
$Installer.ProductInfo($Product,'LocalPackage')
Get-Item -LiteralPath "$Source\Report.msi","$Source\payload.cab"|Select-Object Name,Length原包版本是 1.0.0,源为 C:\SourceReport\source\,MSI 32768 字节、CAB 1412 字节。这些长度只是当次记录,不是验证任意同名源包的标准。恢复可信归档中该版本的完整介质,保留外部 CAB/未压缩文件的相对目录。原路径永久迁移时,用产品支持的选源入口更新源列表;不直接编辑 Installer 内部注册表。Source resiliency
1603 只是事务失败汇总码,需从 Return value 3 附近向前看真实失败动作、SOURCEMGMT、路径和更具体错误。此处 1706 指向缺少合法源,不能把所有 1603 都解释成相同问题。
3. 升级后仍是旧文件
Version Report 原来 ProductVersion=1.0.0、Report.exe FileVersion=1.0.0.0,输出 42。升级包的产品版本变成 1.1.0,包内主程序逻辑已改为 84,却仍标同一个 FileVersion。
用 REINSTALL=ALL REINSTALLMODE=vomus 应用更新后:
| 项目 | 初始 | 有问题的升级 | 修正版 |
|---|---|---|---|
| ProductVersion | 1.0.0 | 1.1.0 | 1.2.0 |
| 主程序 FileVersion | 1.0.0.0 | 1.0.0.0 | 1.2.0.0 |
| 主程序实际输出 | 42 | 42 | 84 |
| 辅助程序 | 1.0.0 | 1.1.0 | 1.2.0 |
| 用户配置 | 初始值 | customer-preserved | customer-preserved |
| 安装返回 | 0 | 0 | 0 |

日志把三个文件的不同决定直接写了出来:
MSI (s) (6C:30) [07:09:19:029]: File: C:\Program Files (x86)\VersionReport\Helper.exe; Overwrite; Won't patch; Existing file is a lower version
MSI (s) (6C:30) [07:09:19:029]: File: C:\Program Files (x86)\VersionReport\Report.exe; Won't Overwrite; Won't patch; Existing file is of an equal version
MSI (s) (6C:30) [07:09:19:029]: File: C:\Program Files (x86)\VersionReport\settings.ini; Won't Overwrite; Won't patch; Existing file is unversioned but modified事务确实执行了,Helper 文件变新;Report 文件版本相等,按 o 的覆盖规则被保留;无版本且已修改的配置也被保留。已安装主程序哈希仍等于初始文件,包内主程序直接运行却是 84,所以不是快捷方式点错或仍在使用旧进程。文件覆盖规则
对自己的故障先检查实际路径,不能只读产品登记版本:
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
$Product='{D28A1801-E0EE-489A-A3D9-3199148E9D91}'
$Dir='C:\Program Files (x86)\VersionReport'
$Installer=New-Object -ComObject WindowsInstaller.Installer
$Installer.ProductInfo($Product,'VersionString')
Get-Item -LiteralPath "$Dir\Report.exe","$Dir\Helper.exe"|ForEach-Object {
[pscustomobject]@{Name=$_.Name;FileVersion=$_.VersionInfo.FileVersion}
}
& "$Dir\Report.exe"
& "$Dir\Helper.exe"
Get-Content -LiteralPath "$Dir\settings.ini"
$Log='C:\VersionReport\fault-update.log'
Select-String -Path $Log -Pattern 'Report.exe;','Helper.exe;','settings.ini;'|ForEach-Object Line若日志显示组件/功能没有选中,先查功能选择、组件条件和升级类型;若磁盘文件已经更新、进程仍旧,查老进程或另一份副本;若日志显示 FilesInUse、延期替换、需重启,沿相应分支处理。此实验没有文件占用及挂起替换,不能把它的结论直接套给这些分支。
4. 修正文件版本
本例保持 ProductCode、组件 GUID、KeyPath、位置及功能树稳定,按相应 minor upgrade 规则更新 PackageCode、ProductVersion 和文件版本。修正版 Report.exe=1.2.0.0;原维护方式下更新成功,哈希与修正版一致,输出变为 84,客户配置仍保留。
REINSTALLMODE 的 o 不是“覆盖所有文件”,e 允许相等或较旧版本,a 强制重装,v 还涉及源包与重新缓存;REINSTALL 选择需重装的功能。不要为了绕过主程序版本错误把所有文件都强制覆盖,尤其同一包还管理用户配置时。REINSTALLMODE
下面是按该实验路径整理、同时处理需重启结果的启动示例,不是任意 MSI 通用升级参数:
# Example for the recorded machine-level Version Report product.
# Package/log directories must exist; preserve vendor-supported upgrade options.
$Package = 'C:\VersionReport\v3-fixed\Report.msi'
$Log = 'C:\VersionReport\fix-update.log'
$arguments = '/i "{0}" REINSTALL=ALL REINSTALLMODE=vomus /qn /norestart /l*v "{1}"' -f $Package,$Log
$p = Start-Process msiexec.exe -ArgumentList $arguments -PassThru
if (-not $p.WaitForExit(60000)) {
throw "Maintenance is still running: PID=$($p.Id); inspect it and $Log before retrying."
}
switch ($p.ExitCode) {
0 { 'Maintenance succeeded.' }
3010 { 'Maintenance succeeded; restart required before final acceptance.' }
1641 { 'Maintenance succeeded and initiated a restart; check the host state.' }
default { throw "Maintenance failed: $($p.ExitCode); inspect $Log" }
}60 秒只是小测试包的等待上限。超时不意味着进程已经停止,也不是立即重试许可。3010 不能当失败重装,0 也不替代文件、功能和业务验收。真实产品沿用厂商支持的更新入口;组件拆分、路径变化或 major upgrade 不能仅抄本例标识策略。
5. 安装上下文
双用途 Report 产品 {F3E97934-C326-451C-B55C-981B794EAF42} 在原用户身份下正常运行 total=42,SYSTEM 只枚举 MACHINE 范围却没有记录,修复返回 1605(ERROR_UNKNOWN_PRODUCT)。管理员跨用户枚举仍能找到它:
| 检查 | 按用户实例 | 合法迁移机器级之后 |
|---|---|---|
| MACHINE 枚举 | 无匹配项 | Context=4、SID 空 |
| 跨用户枚举 | Context=2、原用户 SID | Context=4 |
| SYSTEM 修复原实例 | 1605 | 机器上下文可维护 |
| 原用户修复 | 0 | 原用户实例已先卸载 |
| 业务 | 原用户 total=42 | 原用户和 SYSTEM 均 total=42 |
Context=2 是 USERUNMANAGED,4 是 MACHINE;还有 USERMANAGED=1。API 的 END=259 是枚举结束,必须看此前有没有成功记录,不能一见 259 就当“没安装”。

使用定向 Installer API,而不调用可能触发产品一致性检查的 Win32_Product:
$ErrorActionPreference='Stop'
Add-Type -TypeDefinition @'
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.ComponentModel;
public class ProductContext {
[DllImport("msi.dll",CharSet=CharSet.Unicode)]
static extern uint MsiEnumProductsEx(string product,string sid,uint context,uint index,StringBuilder code,out uint found,StringBuilder foundSid,ref uint length);
public static string Find(string product,string sid,uint context) {
if(String.IsNullOrEmpty(sid)) sid=null;
var text=new StringBuilder();
for(uint i=0;;) {
uint found,n=8;
var code=new StringBuilder(39);
var owner=new StringBuilder((int)n);
uint rc=MsiEnumProductsEx(product,sid,context,i,code,out found,owner,ref n);
if(rc==234) {
owner=new StringBuilder((int)n+1);n=(uint)owner.Capacity;
rc=MsiEnumProductsEx(product,sid,context,i,code,out found,owner,ref n);
}
if(rc==259) {text.Append("END=259");break;}
if(rc!=0) throw new Win32Exception((int)rc);
text.AppendLine("Product="+code+" Context="+found+" SID="+owner);
i++;
}
return text.ToString();
}
}
'@
$Product='{F3E97934-C326-451C-B55C-981B794EAF42}'
[ProductContext]::Find($Product,$null,4)
[ProductContext]::Find($Product,'S-1-1-0',7)机器查询使用原生 NULL SID,不能传 SYSTEM SID。跨所有用户用 S-1-1-0,并需要相应管理员权限;代码先将空字符串转回 NULL,避免 PowerShell 到 .NET 字符串绑定后误传空值。234 表示缓冲不足,重试同一索引,0 才推进,259 才结束;其他错误保留,不能吞成“未安装”。MsiEnumProductsExW
6. 用户与机器级部署
继续按用户部署时,检测清单保留 ProductCode、context、SID,原用户/受控的原用户执行环境发起维护:
msiexec.exe /fomus {F3E97934-C326-451C-B55C-981B794EAF42} /qn /norestart /l*v C:\Logs\owner-repair.log本次原用户修复为 0,SYSTEM 的同一目标为 1605。复制用户目录、在 SYSTEM 的 HKCU 写同名键,不会建立原实例维护上下文。
需要机器级部署时,先确认该包支持双用途编写。ALLUSERS=2 配 MSIINSTALLPERUSER=1 请求按用户,配空值请求按机器;这不是能把任何第三方 MSI 硬转机器级的开关。安装上下文、MSIINSTALLPERUSER
原实例维护沿用其原上下文。迁移先保存业务配置,在原身份合法卸载并确认旧 SID/context 不再枚举,再按支持方式重新安装:
msiexec.exe /i C:\Packages\Report.msi ALLUSERS=2 MSIINSTALLPERUSER="" /qn /norestart /l*v C:\Logs\machine-install.log上面是 cmd 语法,空字符串引号需要按实际 shell 保留,别直接套进 PowerShell 5.1 的原生参数传递。该次原用户修复、卸载、机器级安装都返回 0,最终 ALLUSERS 规范化为 1,API 只剩 Context=4,原用户和 SYSTEM 功能都通过。
迁移会改变安装位置、注册与配置范围。不能只以安装退出 0 宣告完成,更不能手工移动负载、改 Installer 产品注册表或删除缓存来制造“机器级已安装”。
7. 维护与回退
验收至少核对正确实例、安装器结果与重启要求、准确 EXE 路径/版本/哈希、业务请求及客户配置。升级包还应归档正确发行介质和必要的外部负载,确保未来修复能找到源。
回退按产品支持的卸载/重新安装或厂商回滚路径执行,先备份业务数据和配置。MSI 事务内回滚与“安装完几天后恢复旧版本”不是同一个能力;不要仅把旧 EXE 复制回去而让登记版本和组件状态保持新版本。对照实验只清理专用产品和目录,保留安装日志与原介质证据。
