2026/11/1 1:07:18

2012 R2安装.NET 3.5:SXS源与0x800f0906排错

2012 R2安装.NET 3.5:SXS源与0x800f0906排错 简介面向服务器运维、系统集成与桌面支持人员的 Windows Server 2012 R2 离线安装 .NET Framework 3.5 所需 SXS 组件包。资源汇集启用 NetFX3 功能时依赖的并行程序集与配套文件可用于无外网或带宽受限环境下的服务器部署、旧版业务系统迁移与测试环境搭建。压缩包共 1568 个文件约 97.18MB以 dll 动态库、exe 可执行文件、resx 资源、config 配置、aspx 与 ascx 页面控件、sql 脚本等为主涵盖运行时类库、配置模板与安装向导相关组件文件层级与系统组件存放结构基本对应便于按需检索。已有 2033 人学习下载具备较高的参考与复用价值。读者可据此结合 DISM 命令完成离线功能启用快速定位所需程序集减少在线下载等待与版本不匹配问题也能加深对 Side-by-Side 并存机制的理解适合需要批量部署或维护旧版 .NET 应用的管理员使用。1. 内网那台 2012 R2 报 0x800f0906SxS 到底补的是哪一块缺口内网里一台 Windows Server 2012 R2业务软件装到一半提示缺 .NET Framework 3.5。你在服务器管理器里勾上功能一路下一步最后停在「指定备用源路径」或者 PowerShell 直接甩出 0x800f0906。界面没有下载入口也没有任何联网按钮看起来像系统坏了。实际情况是 2012 R2 把 .NET 3.5 的安装载荷从默认路径里摘了出去单独放在安装介质的 sources\sxs 目录也就是常说的 Windows Server 2012 R2 SXS 文件。它属于按需功能Features on Demand的离线源不联网也能装前提是这份 sxs 的版本、语言、补丁分支和当前系统对得上。隔离网、批量交付、不想开更新服务这三类场景都靠它代价是把「对版本」这件事做在前面后面几章基本都是围绕这个展开的。2. 取源之前先对版本install.wim、sxs 目录与语言分支的三种取法很多人拿到一份 sxs 就开始敲 dism报错了再回头找原因来回折腾半天。真正稳的顺序是先确认这台机器是什么版本、什么语言、什么补丁分支再决定从哪种介质里取源。2012 R2 的 sxs 不是「通用补丁包」它和系统版本是绑定的Standard 的载荷给 Datacenter 用、英文的载荷给中文系统用都会被 CBS 层直接拒掉返回的还是一句含糊的「源文件找不到」。2.1 三条命令把版本、语言、分支钉死动手取源之前先在这台目标机上跑三条命令把三个维度的信息记下来。这三条都是系统自带不需要额外装工具也不会改动任何配置。# 1. 当前系统版本Standard / Datacenter / Essentials / Foundation DISM /Online /Get-CurrentEdition # 2. 系统界面语言与已安装语言包 DISM /Online /Get-Intl # 3. 当前构建号用于判断补丁分支 [Environment]::OSVersion.Version.ToString()第一条命令输出的是Current Edition : ServerStandard这类字符串它决定了你后面应该在 install.wim 里选哪个索引而不是随便挂一个就用。第二条第 3 行一般是Default System UI Language : zh-CN这个值必须和你手上 sxs 所在介质的语言一致中文系统配英文介质是 0x800f0906 的高发组合。第三条给出主版本号和构建号2012 R2 的构建号固定为 9600但后面的修订号不同代表介质是否已经集成过累积更新装载旧介质给新系统补功能大概率还能过反过来则容易在 CBS 阶段被版本比较拦下。提示版本、语言、构建号这三项建议直接抄进变更单或部署记录里后面不管换介质还是换机器比对时只看这三个值比翻日志快得多。2.2 取法一挂载 ISO直接指向 install.wim 的索引这是最省磁盘空间的做法不用把 sxs 解出来dism 支持直接从 wim 的第 N 个映像里读取载荷。先把 ISO 挂载成盘符假设挂到了D:接着用一条命令列出 wim 里的索引清单。:: 列出 install.wim 里所有映像的索引、名称和描述 dism /Get-WimInfo /WimFile:D:\sources\install.wim输出里每个索引会带Name : Windows Server 2012 R2 Standard这样的字段把 2.1 步里记下的版本名对上去就得到了索引号。拿到索引号后执行启用命令/Source指向 wim 文件本身冒号后面紧跟索引。:: /Source 指向 wim 文件 索引号其他参数含义见下方说明 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sources\install.wim:2 /LimitAccess ^ /LogPath:D:\log\netfx3_install.log/Online表示作用于当前运行的系统不是离线映像/Enable-Feature是动作/FeatureName:NetFx3是 .NET 3.5 在 2012 R2 里的功能名大小写不敏感但拼写必须准确/All会连带启用该功能依赖的父功能漏掉它有时会在重启后才发现状态不完整/Source指定备用源/LimitAccess是离线场景的关键它阻止 dism 回退去联系更新服务没有这个参数即使给了源系统仍可能尝试联网并把整个流程拖到超时。/LogPath单独落一份日志排错时比翻全局 dism.log 舒服。这种做法的短板是 ISO 必须一直挂着服务器重启、盘符变化、维护窗口里卸载了虚拟光驱源就断了。第二次再装别的功能时又得重新挂载长期看不如把目录抽出来。2.3 取法二把 sxs 目录抽到本地磁盘长期留用抽目录的意义不只是「省得每次挂 ISO」更重要的是把源固定在一个 SYSTEM 账户稳定可读的路径上。部署脚本、组策略、后期补装功能都指向同一个位置路径不变排错范围就小。# 挂载 ISO拿到盘符把 sxs 整体复制到本地再卸载 $iso D:\ISO\WS2012R2_zh-cn.iso $mount Mount-DiskImage -ImagePath $iso -PassThru $drive ($mount | Get-Volume).DriveLetter Copy-Item -Path ${drive}:\sources\sxs -Destination D:\sxs_src -Recurse -Force Dismount-DiskImage -ImagePath $isoMount-DiskImage返回的是磁盘对象Get-Volume取出对应卷才能拿到盘符直接拼${drive}:是因为卷对象里的 DriveLetter 不带冒号。Copy-Item -Recurse -Force会覆盖同名文件如果目标目录之前有过半成品这一步相当于重置。卸载用Dismount-DiskImage比手动在磁盘管理里弹出干净。复制完检查一下体积和文件构成正常的 sxs 目录在两三百 MB 量级里面是一批.cab载荷文件和.mum清单文件其中和 .NET 3.5 直接相关的那个 cab 名字里带 netfx3 字样。如果目录只有几 MB说明复制被中断了后面对应就是 0x800f081f。# 核对体积与关键载荷是否齐全 $sxs D:\sxs_src {0:N1} MB -f ((Get-ChildItem $sxs -Recurse | Measure-Object Length -Sum).Sum / 1MB) Get-ChildItem $sxs -Filter *netfx3* -Recurse | Select-Object Name, Length第一行算总大小第二行筛出名字含 netfx3 的载荷。只要这里能列出文件且体积正常源这一侧基本就过关了剩下的问题都出在调用方式和系统策略上。2.4 取法三从其他介质或已装机器上抠大多数时候别这么干网上流传的做法还有两种一是从一台已经装好 .NET 3.5 的同版本机器上拷WinSxS目录下的相关文件夹二是从别的版本 ISO 里借一份 sxs。前者的问题在于WinSxS是硬链接存储文件之间互相引用直接复制出去会破坏链接关系看起来文件都在实际校验对不上后者的问题在于版本分支不同Standard 与 Datacenter 的功能载荷虽然高度相似但清单里的版本标识不一样CBS 会拒绝加载。确实需要从介质里提取而不想挂载的场合可以用解包工具直接读 wim。命令行方式如下适合在 PE 或 Linux 环境下操作。# 用 wimlib 直接从 install.wim 抽出 sources\sxs不用挂载 wimlib-imagex extract install.wim 2 \sources\sxs --dest-dir./sxs_src2是映像索引和 2.2 步查出来的一致--dest-dir指定输出目录路径里的反斜杠是 wim 内部路径分隔符在 bash 下需要用引号包住避免被转义。图形界面下用压缩软件打开 wim 也能直接拖出 sxs 目录效果一样。但无论哪种方式抽出来的东西必须来自与目标系统同版本、同语言的介质这一条没有捷径。2.5 一份源能不能用先做三项静态核对在真正执行安装之前花两分钟做静态核对能省掉后面一大半的排错时间。核对项不多但每一项都直接对应一个高频错误码。核对项命令或方法期望结果版本一致DISM /Get-WimInfo /WimFile:...对比Get-CurrentEdition名称中的版本字样相同语言一致DISM /Online /Get-Intl对比介质语言默认 UI 语言与介质语言相同载荷齐全统计 sxs 体积、筛 netfx3 文件体积正常、关键 cab 存在三项都过再进入安装环节。任何一项对不上先换介质不要试图靠改参数绕过CBS 的版本比较发生在很底层参数层面绕不过去。3. 双路径安装DISM 六个参数和 PowerShell 两条命令的对照写法源备好之后安装本身只有一条命令的事但参数怎么摆、失败后看哪里、装完怎么证明它真的能用这三件事决定了你是十分钟收工还是耗一下午。命令行和 PowerShell 两条路径底层调的是同一套 CBS 接口差别在于返回值的处理方式和能否嵌进脚本。3.1 DISM 命令逐参数拆解完整形态如下六个参数一个都不能省每个都对应一类失败场景。:: 在线启用 .NET 3.5源指向本地 sxs 目录禁止回退联网 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sxs_src /LimitAccess ^ /LogPath:D:\log\netfx3_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log/Online与/Image是互斥的前者操作正在运行的系统后者操作挂载的离线映像混用会直接报参数错误。/FeatureName后面跟的功能名可以先用/Get-FeatureInfo查一次状态确认它当前是 Disabled 而不是 Enable Pending如果处于待重启状态先把重启做掉再装否则新命令会被挂起。:: 安装前先确认功能当前状态避免在 Pending 状态下重复操作 dism /Online /Get-FeatureInfo /FeatureName:NetFx3/All的作用是连同父功能一起启用。NetFx3 在功能树里挂在 .NET Framework 3.5 这个节点下/All会把它依赖的上层一并打开省掉装完发现 WCF 相关子项还是关闭的尴尬。/Source既可以指向目录也可以指向wim:index形式前者要求目录里直接就是 cab 和 mum 文件不能多套一层文件夹D:\sxs_src\sxs这种结构是错的。/LimitAccess前面提过它的作用是在找不到源时不去联系更新服务而是直接失败返回对隔离网来说这是必须的对能联网的机器来说它也能把安装时间从不确定的几分钟压到几十秒。/LogPath指定的目录必须预先存在dism 不会帮你创建父目录。3.2 PowerShell 两条路径的写法与差异脚本化部署时更常用 PowerShell2012 R2 上可用的有两条路子一条是服务器管理模块的Install-WindowsFeature一条是 DISM 模块的Enable-WindowsOptionalFeature。# 路径 A服务器管理模块返回结构化的 Success / RestartNeeded $r Install-WindowsFeature -Name NET-Framework-Core -Source D:\sxs_src -Restart:$false $r | Select-Object FeatureResult, Success, RestartNeeded, ExitCode # 路径 BDISM 模块参数与命令行一一对应 Enable-WindowsOptionalFeature -Online -FeatureName NetFx3 -All -Source D:\sxs_src -LimitAccess -NoRestart路径 A 的功能名是NET-Framework-Core和命令行的NetFx3不是一个命名体系这是最容易写错的地方脚本里混用会直接提示功能不存在。它的好处是返回对象里有Success和ExitCode判断分支写起来干净不需要解析文本输出。-Restart:$false表示不自动重启生产机上建议一律关掉自动重启把重启窗口交给变更流程。路径 B 的Enable-WindowsOptionalFeature参数名和 dism 命令行几乎一一对应-LimitAccess、-NoRestart、-All都在返回值里State字段会是Enabled或EnablePending。注意这条命令在部分精简过的系统上不可用因为它依赖 DISM 模块完整加载遇到模块缺失就退回路径 A 或直接用命令行。两条路径执行完都可以用一条命令复核状态不用等重启。# 两种模块的查询方式输出结果应显示已安装 Get-WindowsFeature -Name NET-Framework-Core | Select-Object Name, InstallState (Get-WindowsOptionalFeature -Online -FeatureName NetFx3).StateInstallState期望值是InstalledState期望值是Enabled。如果显示InstallPending或EnablePending说明载荷已经写入但需要重启才能生效这时候不要再重复执行安装命令直接安排重启。3.3 返回码对照与日志定位失败时先看返回码再看日志不要一上来就翻 CBS.log那玩意儿几千行没目标地看纯属浪费时间。常见返回码和对应的方向整理如下。返回码含义优先排查方向0x800f0906源文件不可用或版本不匹配版本、语言、索引号0x800f081f源路径中缺少所需文件sxs 目录是否完整0x800f0954被策略导向更新服务器组策略、注册表策略项0x800f0922系统分区或保留分区空间不足磁盘剩余空间0x80073712组件存储损坏先做组件存储修复0x800f0906 是最常见的一个字面意思是「找不到源文件」实际八成是版本对不上比如 Datacenter 系统拿了 Standard 的索引或者中文系统配了英文介质。0x800f081f 更直接路径通了但目录里少了关键 cab通常是复制中断或只拷了部分文件。0x800f0954 比较隐蔽即使加了/LimitAccess也会出现因为它是策略层在功能安装前就把请求重定向走了参数拦不住。日志分两处dism 自己的日志记录命令行解析和整体流程CBS 日志记录组件级的加载细节。# 从 CBS 日志里捞出与 NetFx3 相关的最后若干条记录 Select-String -Path $env:WINDIR\Logs\CBS\CBS.log -Pattern NetFx3|0x800f | Select-Object -Last 40-Pattern里用竖线做或匹配NetFx3抓功能名0x800f抓错误码-Last 40限制输出行数避免刷屏。真正有用的信息往往是错误码前面那几行会写明「期望的版本是 X实际提供的是 Y」这类比对结果对照 2.1 步记录的三个值一看就知道差在哪。3.4 装完不算完跑一个 3.5 编译的探针功能状态显示已安装只能说明载荷写进去了不能说明运行时真的可用。线上遇到过状态正常但程序一加载就报「未能加载文件或程序集」的情况原因是安装过程中有过中断部分文件虽然落地但注册信息不全。最省事的验证方式是拿 .NET 3.5 自带的编译器现场编一个小程序跑一遍。# 写一个最小探针程序用 3.5 的编译器编出来再执行 $cs using System; class Probe { static void Main() { Console.WriteLine(CLR Environment.Version); } } $src Join-Path $env:TEMP fx35_probe.cs Set-Content -Path $src -Value $cs -Encoding ASCII $csc Join-Path $env:WINDIR Microsoft.NET\Framework\v3.5\csc.exe $csc /nologo /out:$env:TEMP\fx35_probe.exe $src $env:TEMP\fx35_probe.execsc.exe在 v3.5 目录下它编译出来的程序目标运行时是 CLR 2.0这正是 .NET 3.5 的底座。程序能打印出版本号说明编译器、运行时、程序集解析三条链路都通了比看功能状态可靠得多。如果csc.exe路径不存在说明 3.5 的编译器组件没装全功能状态可能只覆盖了运行时部分这种情况在手动拷文件「修复」过的机器上并不少见。4. 排查清单0x800f0906、0x800f081f、0x800f0954 背后的五类现场前面三章讲的是顺利路径这一章全是失败的现场。每一条都按「看到什么、为什么、怎么处理」写顺序按出现频率排。这里只写实际撞过的理论上的可能性不列。4.1 0x800f0906源和系统不是同一份东西现象很典型命令执行到一半停住进度条走了一点又退回来最后一行是The source files could not be found错误码 0x800f0906。日志里会有一行比对结果写明期望的版本标识和实际提供的版本标识不一致。原因几乎都是版本或语言错配。三种常见组合Datacenter 系统拿了 Standard 索引中文系统配了英文介质带累积更新的系统拿了 RTM 原始介质。前两种是清单层面的版本标识不匹配第三种是 CBS 在做版本比较时认为源比系统旧。第三种有时候能过有时候过不了取决于具体功能是否在更新中被改动过。处理方式是回到 2.1 步把三个值重新核一遍然后换一份对应的介质。这里有个容易被忽略的点介质文件名里带的语言标记不一定准有些整合版 ISO 的文件名是中文但内部只集成了英文语言包唯一可靠的判断方式是挂载后看sources\lang.ini或直接跑/Get-WimInfo看描述里的语言字段。换介质之后建议先清一次临时状态避免上一次的残留影响判断。:: 清理挂起的操作后再重试避免上一次失败的残留干扰 dism /Online /Cleanup-Image /RevertPendingActions这条命令会回滚处于挂起状态的操作执行后通常需要重启一次。它不是万能药只在确认有挂起操作时才用滥用会把正常进行中的安装也回滚掉。4.2 0x800f081f路径对了文件不全现象是命令很快失败比 0x800f0906 还快错误提示同样是源文件找不到但错误码是 0x800f081f。这个码的特点是不做版本比较就失败了因为它在更早的阶段就发现目标路径里没有它要找的文件。原因通常是三种/Source指向的层级错了多套了一层目录复制过程被中断目录里只有一部分 cab用了网络共享路径但执行账户对共享没有读权限。第一种最常见D:\sxs_src\sxs\*.cab这种结构 dism 是不认的它要求 cab 直接躺在/Source指定的目录下。第二种在复制大目录时容易发生尤其是从慢速 U 盘或跨网段共享复制看起来完成了实际少了文件。第三种在脚本以 SYSTEM 身份运行时特别隐蔽手动执行没问题计划任务里就失败。处理方式先用 2.3 步的两条核对命令确认目录体积和关键文件体积明显偏小就重新复制路径层级用Get-ChildItem看一眼确认 cab 在第一层网络路径统一改成先复制到本地再装不要直接指向 UNC。注意以 SYSTEM 身份执行的脚本访问网络共享时走的是计算机账户不是当前登录用户权限模型完全不同。这类问题在手动测试阶段永远复现不出来。4.3 0x800f0954被策略拽回了更新服务器现象是命令执行时间明显偏长像是在等什么超时最后返回 0x800f0954日志里能看到它尝试联系某个内部更新服务器的记录。即使加了/LimitAccess也一样因为拦截发生在更早的一层。原因是组策略里配置了「指定 Intranet 更新服务位置」或者「为功能安装指定备用源」相关的策略项系统在功能安装流程开始前就把源重定向到了更新服务器。/LimitAccess只管 dism 这一层不主动联网管不了策略层已经改写的源地址。处理方式是查策略并临时调整。定位到计算机配置下的管理模板系统节点里和功能安装、更新位置相关的几项确认「始终尝试从更新中检索」是否被启用以及内部更新服务器地址是否被指定。生产环境不建议直接改全域策略更稳妥的做法是在单机上用本地策略临时覆盖装完再恢复或者把 sxs 源直接发布到那台内部更新服务器上让策略指向的地址本身就有载荷。# 查看当前生效的更新源相关策略键值 Get-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate -ErrorAction SilentlyContinue输出里如果出现WUServer或UseWUServer这类键基本可以确认是策略接管了。改之前先导出备份改完执行gpupdate /force让策略立即生效再重试安装。4.4 0x800f0922卡在磁盘和保留分区现象是安装进展到较后阶段才失败错误码 0x800f0922前面一切正常让人误以为是源的问题。日志里会出现写入失败或空间不足的字样。原因有两类。一类是系统盘剩余空间不足.NET 3.5 安装过程中需要在系统盘展开临时文件空间不够就在最后阶段崩掉。另一类是系统保留分区空间不足这个分区通常只有几百 MB某些情况下功能安装需要往里面写引导相关的内容空间不够同样报这个码。第二类经常被误判成源的问题因为错误码看起来和源没关系但排查方向完全错。处理方式先看系统盘剩余空间低于几个 GB 就先清理再看保留分区用磁盘管理确认它的剩余空间如果已经接近满需要评估是否扩容。保留分区扩容涉及分区表操作风险较高生产机上建议走维护窗口并做好备份不要在现场临时动手。4.5 路径与权限带空格、超长、UNC 这三类隐形坑现象是命令手动敲能过写进脚本就失败或者在某些机器上过、换台机器就不过。错误码不固定有时是 0x800f081f有时直接提示路径无效。原因是路径本身的形态。/Source后面带空格的路径必须用引号包住这是最基础的一条但脚本里拼接变量时经常忘。超长路径在深层目录嵌套时也会触发问题Windows 的路径长度限制在部分 API 上依然是 260 字符。UNC 路径除了前面说的 SYSTEM 权限问题还有凭据问题脚本运行时不会弹出认证窗口连不上就是连不上。处理方式是统一规范源目录固定放在本地磁盘的短路径下比如D:\sxs_src不带空格不带中文脚本里所有路径变量都加引号跨机器部署时先复制再安装不要直接引用共享。这套规范写进部署脚本的注释里比事后排查省事得多。# 部署脚本里对源路径做一次前置校验不通过就直接退出 $src D:\sxs_src if (-not (Test-Path $src)) { throw 源目录不存在: $src } if ($src -match \s) { throw 源路径含空格改用短路径: $src } if (-not (Get-ChildItem $src -Filter *.cab)) { throw 源目录中未找到 cab 载荷: $src }三段判断分别对应路径不存在、含空格、载荷缺失三种情况放在脚本开头执行能在真正调用安装命令之前就把问题暴露出来报错信息也比 dism 返回的十六进制码直观。5. 批量落地把 sxs 源固化进 install.wim 与部署脚本单台机器装完事情只做了一半。真正省事的是把这一步前移到镜像制作阶段或者固化成一份可重复执行的部署脚本。前者让新装出来的机器天生带 .NET 3.5后者让已有的存量机器能批量补齐两条路可以并行。5.1 离线给 install.wim 预置 .NET 3.5这条路适合新交付的机器。核心是用 dism 挂载 wim在离线状态下启用功能再提交卸载。整个过程不依赖目标机做一次镜像后面装多少台都省这一步。:: 1. 挂载 install.wim 的第 2 个索引到 C:\mnt目录需预先创建 dism /Mount-Image /ImageFile:D:\ISO\sources\install.wim /Index:2 /MountDir:C:\mnt :: 2. 在离线映像里启用 NetFx3源用本地 sxs 目录 dism /Image:C:\mnt /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:D:\sxs_src /LimitAccess :: 3. 提交更改并卸载 dism /Unmount-Image /MountDir:C:\mnt /Commit第一步的/MountDir必须是一个空目录且不能位于待修改的 wim 所在磁盘的根目录否则挂载会失败。挂载是读写模式这一步会实际修改 wim 文件操作前先复制一份原始 wim 做备份这是血泪经验挂载中途出错导致 wim 损坏的情况并不罕见。第二步的/Image换成了离线映像路径其余参数和在线安装完全一致/Source依然指向 sxs 目录因为离线映像里同样没有载荷。第三步的/Commit是关键忘了它等于白做卸载时不带/Commit会丢弃所有修改。索引号要和目标机器的版本对应。如果一份 wim 里同时有 Standard 和 Datacenter 两个索引而你只改了其中一个装到机器上发现功能还是没有就是因为装的是另一个索引。稳妥做法是每个索引都处理一遍或者干脆用/Get-WimInfo确认后只保留需要的那一个再制作介质。提示离线注入前先确认 wim 的版本与后续要部署的机器一致注入完成后可以用/Get-FeatureInfo针对离线映像查一次状态确认是 Enabled 再卸载。5.2 部署脚本检测、暂存、安装、留痕四段式存量机器的批量补齐靠脚本。脚本的结构不用复杂四段足够检测当前状态决定是否跳过把源暂存到本地执行安装并捕获返回码最后把结果写成一份可追溯的记录。param( [string]$SourcePath \\fileserver\deploy\sxs_2012r2_zh-cn, [string]$StagePath C:\_stage\sxs, [string]$LogRoot C:\_stage\log ) # 第一段已在安装状态或挂起状态则直接跳过 $feat Get-WindowsFeature -Name NET-Framework-Core if ($feat.InstallState -eq Installed) { $env:COMPUTERNAME 已安装跳过 | Out-File $LogRoot\skip.log -Append return } # 第二段把源复制到本地避免 SYSTEM 账户访问共享的权限问题 if (-not (Test-Path $StagePath)) { New-Item -ItemType Directory -Path $StagePath -Force | Out-Null } Copy-Item -Path $SourcePath\* -Destination $StagePath -Recurse -Force # 第三段执行安装并捕获退出码 $r Install-WindowsFeature -Name NET-Framework-Core -Source $StagePath -Restart:$false # 第四段把结果落到按机器名命名的日志里 [pscustomobject]{ Host $env:COMPUTERNAME Success $r.Success ExitCode $r.ExitCode Restart $r.RestartNeeded Time (Get-Date).ToString(s) } | Export-Csv -Path $LogRoot\$env:COMPUTERNAME.csv -NoTypeInformation -Encoding UTF8第一段的判断放在最前面避免对已安装的机器重复操作重复安装虽然通常无害但会浪费时间并且可能在日志里留下误导性的记录。第二段用Copy-Item把共享内容拉到本地这一步是解决权限问题的关键SYSTEM 账户访问 UNC 走的是计算机账户在域环境里通常没有共享读权限先复制再装能绕开整个问题。第三段用-Restart:$false关掉自动重启批量执行时自动重启会把并行的机器搞得一团乱。第四段按机器名落 CSV后面汇总时直接合并所有文件就能出一张表。脚本里没有做重启后的二次校验因为重启会中断脚本执行。二次校验单独放在开机任务或者运维平台的下一个流程里用 3.4 步的探针程序跑一遍结果回写到同一张表里形成闭环。5.3 用组策略和应答文件接管备用源脚本适合存量机器新交付的机器更适合在应答文件阶段就把源指定好。2012 R2 的无人值守安装里可以在SetupComplete.cmd里挂一条安装命令这个脚本在系统装完、登录界面出现之前执行权限足够。:: SetupComplete.cmd 放置在 %WINDIR%\Setup\Scripts\ 下安装收尾时自动执行 dism /Online /Enable-Feature /FeatureName:NetFx3 /All ^ /Source:%SystemDrive%\_stage\sxs /LimitAccess ^ /LogPath:%SystemDrive%\_stage\log\netfx3_setup.log%SystemDrive%\_stage\sxs这个目录需要在镜像制作阶段就放进去也就是 5.1 步的做法之外再额外拷一份 sxs 到镜像的对应位置。这样新机器装完第一次启动时功能已经在收尾阶段装好了用户侧完全无感。日志路径同理需要预先存在可以在脚本开头加一行mkdir或者干脆把载荷和日志放在同一个目录下。组策略那条路适合已经上线、不方便重装的环境。前面 4.3 节提过策略会拦截源反过来也能用策略把源固定下来具体做法是在管理模板里找到功能安装相关的策略项把「备用源路径」配置成本地暂存目录同时关闭「始终尝试从更新中检索」。这样即使有人手动在服务器管理器里点安装也会自动用上本地源不需要每次都交代一遍。5.4 介质升级之后旧 sxs 还能不能接着用这个问题在补丁更新之后一定会遇到。结论是分情况如果只是常规的累积更新系统版本标识没有变旧的 sxs 通常还能用因为功能载荷本身没被改动如果是服务分支变化或者介质是从早期整合版升上来的旧 sxs 里缺少新版清单安装时会以 0x800f0906 失败。判断方法很简单拿新装好的系统跑一次 2.1 步的三条命令把三个值和旧介质对比。构建号差异较大时直接换用同批次的新介质不要试图用旧源硬装。维护窗口里顺手把新的 sxs 抽出来覆盖暂存目录这个动作花不了几分钟能避免下一次部署时的返工。注意覆盖暂存目录前先确认没有正在执行的安装任务中途替换文件会导致 CBS 读到半新半旧的清单产生更难排查的错误。6. 收在自检上一份 sxs 源核对脚本和三条长期习惯部署做多了会发现真正拖时间的从来不是安装命令本身而是「这份源到底能不能用」这个前置判断。我现在的习惯是拿到任何一份 sxs 目录先跑一遍核对脚本通过了再往下走。# sxs 源核对脚本版本、语言、载荷、体积四项一次过 param([string]$Sxs D:\sxs_src) $cur (DISM /Online /Get-CurrentEdition | Select-String Current Edition) -replace .*:\s*, $lang (DISM /Online /Get-Intl | Select-String Default System UI Language) -replace .*:\s*, $sum (Get-ChildItem $Sxs -Recurse -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum / 1MB $cab Get-ChildItem $Sxs -Filter *netfx3* -Recurse -ErrorAction SilentlyContinue [pscustomobject]{ Edition $cur Language $lang SxsSizeMB [math]::Round($sum, 1) NetFx3Cab if ($cab) { $cab.Count } else { 0 } SourcePath $Sxs } | Format-List前两行取系统版本和默认语言第三行算目录总大小第四行筛关键载荷。输出一个对象一眼能看到四项。判定标准很简单NetFx3Cab为 0 直接换源SxsSizeMB低于三位数基本就是复制不全版本和语言这两项则要人工对照变更记录。脚本不判断对错只把信息摊开判断交给人这样更可控。自检项判定标准不通过时的动作NetFx3Cab 数量大于 0重新从介质复制目录体积三位数 MB 量级检查复制是否中断版本字符串与目标机一致换对应索引或介质默认语言与介质语言一致换同语言介质三条长期习惯也一并写在这里。第一源目录固定使用短路径不带空格不带中文所有脚本和文档里统一这个路径不给「临时换个位置」留口子。第二每次部署前先跑核对脚本通过了再动目标机把判断前置比事后翻 CBS 日志便宜太多。第三介质一更新就把新的 sxs 覆盖到暂存目录并把版本、语言、构建号记进部署记录下次遇到 0x800f0906 时可以直接比对不用重新查一遍。从那以后我每次拿到一份 sxs不管多急都强制先把核对脚本跑一遍再碰目标机这个习惯至少帮我省下了三个通宵。希望帮到你。本文还有配套的精品资源点击获取