WinForms / WPF 应用的 CI/CD 实践 ── 用 GitHub Actions 实现从构建到签名、发布的自动化

· 更新日期: · · CI/CD, GitHub Actions, WinForms, WPF, C#, .NET, 代码签名, MSIX, 部署, Windows开发, 判断表

「发布版只有那个人的电脑才能构建出来」──在关于 WinForms 或 WPF 业务应用的咨询中,这是一句真的经常听到的话。在本地的 Visual Studio 上做 Release 构建,压缩成 zip,放进共享文件夹。虽然能用,但没有人能回答「那个开发者休假的那天,能不能发布一个修复版本」这个问题。

明明 Web 应用的 CI/CD 资料到处都是,一到桌面应用话题就骤然变少。这固然有其道理——「部署对象不是服务器,而是客户那边的 PC」这一根本差异,确实意味着无法把 Web 那套文章原样照搬。不过,仅就构建与测试的自动化而言,桌面应用几乎可以用和 Web 相同的工作量来搭建。真正的门槛出现在再往后的签名与发布环节,而这里的现实解会因发布方式的不同而不同。

在本博客中,我们已经通过「Windows 应用发布方式怎么选 - MSI / MSIX / ClickOnce / xcopy / 自定义 updater 判断指南」梳理了如何选择发布方式,通过「Windows 出现「Windows 已保护你的电脑」提示的原因」整理了签名方面的思路。本文将以这两篇为前提,从实务角度整理用 GitHub Actions 能把 WinForms / WPF 应用的构建、测试、版本编号、签名、发布物制作自动化到什么程度

1. 先说结论

  • 最大的风险是「只能在某个开发者的 PC 上构建」这种状态。CI/CD 的第一目标并不是让发布全自动化,而是让构建不依赖任何特定的人的电脑就能复现出来。
  • 仅有构建+测试自动化的最小配置,本身就已经很有价值。windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact,一个 YAML 文件就能搭起来。12
  • WinForms / WPF 以 Windows Runner 为前提。由于它们以 net8.0-windows 这类 Windows 专用 TFM 为目标3,若要在 CI 中执行测试,就需要 Windows 环境。
  • 版本编号以标签驱动为落脚点。通过 push v1.2.3 这样的标签来触发发布构建,并把标签的值注入 MSBuild 的 Version 属性。4
  • 签名是自动化中最大的门槛。自 2023 年 6 月起,公开 OV 证书的私钥被要求必须保存在 HSM 中,「把 PFX 放进 Secret、用 signtool 签名」这一以往的标准做法已经无法照旧使用。若想在 CI 中完整闭环,经由云签名服务是现实的做法。5
  • 发布形式不同,接入 CI 的难易度差异很大。顺序依次是:xcopy(zip)最简单,MSIX 必须签名6,MSI 需要在 CLI 中调用配套工具,ClickOnce 则需要 msbuild /target:publish 且颇有一些棱角。7
  • 不要把 UI 自动化测试当成 CI 的必经关卡。把单元测试作为 CI 的必经关卡,UI 测试限定为冒烟测试并放在单独的作业中执行,这是现实可行的做法。

2. 桌面应用的 CI/CD 与 Web 有什么不同

首先梳理一下,为什么不能把 Web 应用的 CI/CD 模式(push → 构建 → 测试 → 部署到服务器)原样搬到桌面应用上。

视角 Web 应用 WinForms / WPF 桌面应用
部署对象 自己管理的服务器 客户・现场的 PC(不受管理)
发布单位 在服务器上统一切换 MSI / MSIX / ClickOnce / zip 等多种多样,展开的时机取决于对方
回滚 可以在服务器端回退 已经发布到的 PC 上很难轻易回退,必须保留旧版安装程序
签名 通常不需要(TLS 由基础设施层负责) 对可执行文件・安装包进行代码签名实质上是必需的
构建环境 容易在 Linux Runner 上闭环 以 Windows Runner 为前提
测试 容易以无头(headless)方式闭环 单元测试相同,UI 测试需要桌面会话
「部署」的含义 直到反映到生产环境为止 CI 的职责范围止于「发布物制作完成」,安装是另一道工序

最重要的是最后一行。对桌面应用而言,CI/CD 流水线的出口并不是「反映到生产环境」,而是「一份已签名的发布物,被放在随时可以取出的地方」。从这里往后(向客户展开、自动更新)是发布方式设计的话题,属于「发布方式判断表」所涉及的范围。反过来说,只要把出口这样明确地划定下来,桌面应用的 CI/CD 就能用和 Web 相同的工具组合来搭建。

3. 最小配置 ── 用 GitHub Actions 实现构建+测试

一开始应该引入的就只有这些。由于 GitHub 托管 Runner 会为每个作业分配一台全新的虚拟机1,每次 push 都会在一台全新的 Windows 上运行构建与测试,「只有那个人的电脑上才装了的 SDK」这类依赖会当场暴露出来。

name: build-and-test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: windows-latest   # WinForms / WPF 必须使用 Windows Runner
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore
        run: dotnet restore

      - name: Build
        run: dotnet build --configuration Release --no-restore

      - name: Test
        run: dotnet test --configuration Release --no-build

      - name: Publish
        run: dotnet publish src/MyApp/MyApp.csproj -c Release -o publish

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp
          path: publish

补充三个要点。

第一,runs-on: windows-latest 是基本配置。WinForms / WPF 项目的 TargetFrameworknet8.0-windows 这类 Windows 专用 TFM,是启用了 UseWindowsFormsUseWPF 的 .NET 桌面 SDK 项目。3 严格来说,如果只是想编译,启用 EnableWindowsTargeting 后也能在 Linux Runner 上构建,但 dotnet test 这类涉及实际执行的步骤需要 Windows 环境,因此在这种把测试也放进同一个作业里运行的配置中,直接使用 Windows Runner 更为顺理成章。若是 .NET Framework 4.x(旧格式 csproj),就要用 MSBuild 和 NuGet CLI 而不是 dotnet build,但二者都预装在 Windows Runner 上,思路是一样的。

第二,一定要用 actions/upload-artifact 保留发布物。「这次构建的全部产物都能从 GitHub 取出来」,这正是摆脱依赖特定人电脑的实质内容。即使是急需的临时验证版本,也只需从 Actions 页面下载 zip 即可。

第三,在这个阶段还不做签名和发布。仅凭这套最小配置,就能获得「main 分支始终可构建、可测试」「任何人都能取出相同的构建产物」这两项保证,根据笔者的经验,小规模团队的大部分烦恼到这一步就已经解决了。

4. 版本号的自动编号 ── 标签驱动发布

下一个阶段是解决「这个 zip 到底是几版?」的问题。在依赖本地构建的运维方式下,忘记修改 csproj 里的 Version、导致好几代产物都停留在同一个 1.0.0 的事故是常见套路。实务上的落脚点是标签驱动发布:在想要发布的提交上打上 v1.2.3 这样的标签,以此为触发条件运行工作流,把从标签名中取出的版本号注入到构建中。

name: release

on:
  push:
    tags: [ 'v*' ]

jobs:
  release:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      # push 标签不会触发第 3 章中的构建+测试工作流,
      # 所以在生成发布产物之前,这里也要重新跑一次测试
      - name: Test
        run: dotnet test --configuration Release

      - name: Publish with version from tag
        shell: pwsh
        run: |
          $version = $env:GITHUB_REF_NAME.TrimStart('v')   # v1.2.3 -> 1.2.3
          dotnet publish src/MyApp/MyApp.csproj `
            -c Release -o publish `
            -p:Version=$version

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-${{ github.ref_name }}
          path: publish

-p:Version=1.2.3 这样的形式作为 MSBuild 属性传入后,在 .NET SDK 项目中,AssemblyVersionFileVersion 会默认根据 Version 的前缀部分(去掉后缀的部分)生成,InformationalVersion 则默认直接取自 Version 本身。4 csproj 中只放开发用的占位值,发布时的正式版本号则只由标签一处持有,实现了一元化管理。

有一点需要注意。actions/upload-artifact 的产物存在仓库层面的保留期限(默认 90 天),过期就会被删除。由于桌面应用需要长期保存旧版本安装程序以便回滚,应把标签构建的产物发布到 GitHub Release 之类的永久存放位置,把 Actions 的 Artifact 当作纯粹的临时中转来看待。

好处是运维完全收拢在 Git 之内。「客户那边的 1.2.3 对应哪个提交」由标签确定,EXE 属性中显示的文件版本与 Git 标签机械地保持一致。此外,从 .NET 8 SDK 起,InformationalVersion 默认还会附加 Git 提交哈希(SourceRevisionId),4 只要把它显示在版本信息界面上,就可以直接从发布物追溯到具体提交。

5. 把代码签名接入 CI ── 这是最大的门槛

那些顺利把构建和版本号自动化的团队,几乎无一例外都会在签名这一步卡住。关于为什么 Store 之外分发的 Windows 应用实质上必须做代码签名(SmartScreen、企业安全产品、篡改检测),我们已经在「Windows 出现「Windows 已保护你的电脑」提示的原因」中整理过,这里只专注于在 CI 的哪个环节、以什么方式执行签名

5.1 signtool 的基本形式

签名操作本身只是一条命令。signtool 包含在 Windows SDK 中,GitHub 的 Windows Runner 上也可以使用。当前版本的 SDK 要求必须指定 /fd(文件摘要)和 /td(时间戳摘要),推荐使用 SHA256。8

signtool sign /f MyCert.pfx /p $env:PFX_PASSWORD `
  /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
  publish\MyApp.exe

时间戳(/tr)虽然可以省略,但一定要加上。有了时间戳,即使证书过期之后,也能验证「签名当时是有效的」,从而让已发布文件上的签名持续保持有效。86

5.2 证书类型与接入 CI 的现实情况

问题不在于命令本身,而在于私钥放在哪里。证书的获取形态不同,接入 CI 的方式也会有根本性的差异。

证书形态 私钥所在位置 接入 CI 的难易度 备注
云签名服务(Azure Artifact Signing = 原 Trusted Signing 等) 云端 容易接入(首选方案)。设计上就以对接 GitHub Actions 等为前提 可用的国家/地区有限制(法人为美国、加拿大、欧盟、英国等)5
OV 证书(2023 年 6 月以后新申请) 必须使用 HSM / USB Token 由于 Token 无法插入 Runner,原样无法使用。若 CA 提供云端 HSM 选项则可以 依据 CA/Browser Forum 的要求5
EV 证书 HSM / USB Token 同上 SmartScreen 的即时信任效果已于 2024 年废除,在签名运维层面可以与 OV 同等看待5
旧式 PFX 文件(以往发放、内部 CA、自签名) 文件 以 Base64 存入 Secret 后再还原(见下文) 面向公开发布、新申请的证书原则上已经无法再获得这种形态

也就是说,搜索时经常见到的「把 PFX 放进 GitHub 的 Secret、用 signtool 签名」这套配置,对内部 CA 或已有的 PFX 仍然有效,但对今后新申请公开证书的情况,前提已经不成立了。如果是从零搭建,现实的做法是围绕从一开始就支持 CI 对接的云签名服务来考虑。5 若仍在用 USB Token 方式运维,就会变成一种折衷配置:只把签名这一环节留在本地电脑,或留在插着 Token 的自建(self-hosted)Runner 上。

5.3 Secret 管理注意事项

以下是把 PFX 方式(内部 CA、已有证书)接入 CI 时的标准做法。

  • 把 PFX 转成 Base64 字符串存入 GitHub 的 Secret,再在作业中还原成文件。这是 GitHub Docs 中针对如何在 Secret 中处理二进制数据所给出的做法。9
  • 把密码放进另一个 Secret。Secret 的值会在日志中自动打码,9 但这种保护不会延伸到经过加工的派生值。除签名步骤之外,不要把它作为环境变量传给其他步骤。
  • 来自 Fork 的 Pull Request 不会拿到 Secret(GITHUB_TOKEN 除外)。9 不过,作业本身仍然会以空 Secret 的方式执行,因此上面的还原步骤会因为对空字符串做 Base64 解码而失败。应该把签名步骤放进像第 4 章那样由标签触发的发布工作流中(它不会在 Fork PR 上触发),或者加上 if: github.event_name != 'pull_request' 这样的条件来明确跳过。
      - name: Restore signing certificate
        shell: pwsh
        run: |
          $bytes = [Convert]::FromBase64String($env:PFX_BASE64)
          [IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", $bytes)
        env:
          PFX_BASE64: ${{ secrets.SIGNING_PFX_BASE64 }}

还有一个关键点:收窄能够访问签名密钥的工作流范围。拥有仓库写权限的人可以改写工作流,因此应该把签名用的 Secret 放进带审批者(Environment)中,让只有发布工作流才能引用到它。已签名的二进制文件本身就是「这是我们公司制作的」这一事实的证明,因此对密钥的处理,应该按照与自动更新分发基础设施相同等级的信任边界来设计(这一点也可以参考「自动更新的安全设计——为什么仅靠 HTTPS 还不够」)。

6. 按发布形式划分的 CI/CD 集成判断表

走到签名这一步之后,最后剩下的就是发布物的形态。发布方式本身的选型交给「发布方式判断表」去处理,这里只从 CI/CD 的角度做比较。

发布形式 在 CI 中的易用程度 在 CI 中的制作手段 签名要求 自动更新
xcopy(zip 发布) 最简单 仅需 dotnet publish+压缩 对 EXE/DLL 签名(推荐) 无(手动展开)
xcopy+自制 updater 简单(仅就构建而言)。更新分发的设计另外是一项重活 dotnet publish+生成清单 必须做 EXE 签名+更新文件的校验设计 自制(需要设计信任边界)
MSI 中等 在 CLI 中运行 WiX 等工具 对 MSI 文件签名(推荐~实质必需) 无(需要另外的发布机制)
MSIX 中等 MSBuild / MakeAppx+signtool 必须对包签名(未签名无法安装)6 可通过 App Installer 等实现
ClickOnce 棱角很多 msbuild /target:publish+发布配置文件(dotnet CLI 不支持)7 清单签名+EXE 签名 内置(该方式的主要卖点)

补充说明如下。

  • xcopy(zip):第 3、4 章的工作流几乎就是完成形态。无论最终采用哪种发布形式,先把这种形态跑通都是最近的路径。
  • MSI:把安装程序定义(WiX 等)放进仓库,通过 CLI 构建。比起生成本身,「MSI 中要包含什么(服务注册、per-machine/per-user)」的设计才是重点所在。
  • MSIX:由于 Windows 不允许安装未签名的 MSIX,如果不把签名的自动化一并做好,CI 化就无法真正完成6 反过来说,只要签名基础设施到位,这是一种容易接入 CI 的形式。若通过 Microsoft Store 发布,还有另一种解法——Store 一侧会重新签名,因此不需要自备证书。5
  • ClickOnce:无法通过 dotnet CLI 发布,需要使用指定了发布配置文件(.pubxml)的 msbuild /target:publish /p:PublishProfile=...。IDE 中每次发布都会自动递增的修订号(ApplicationRevision),在命令行构建中不会自动递增,7 这使得第 4 章介绍的标签驱动、显式传入版本号的设计成为必需。这里还有一个要注意的地方:ClickOnce 的更新判定所依据的不是 -p:Version(程序集信息),而是部署侧的版本号(ApplicationVersion / ApplicationRevision),因此如果不额外以 /p:ApplicationVersion=1.2.3.0 这样的形式,把从标签生成的四段式版本号单独传入,新发布的版本就不会被识别为更新。相关机制以及适用与否,在「ClickOnce 是什么 - 从实务角度整理其原理、更新机制以及适用与不适用的场景」中有解说。

单从 CI/CD 的角度来说,「先从 zip 开始,等发布需求定下来之后再把 MSIX 或 MSI 作为一个作业加进去」是增量成本最小的推进方式。前段(构建、测试、版本号)对所有形式都是通用的,因此之后替换发布形式相关的步骤,也不会浪费之前的投入。

7. 测试自动化要做到什么程度

最后,来划定一下作为 CI 关卡(必需检查)应该要求做到什么程度的测试。

测试层级 在 CI 中的处理 理由
单元测试(逻辑) 必经关卡,每次 Pull Request 都执行 快、稳定,在 Windows Runner 上直接就能跑
无界面集成测试(数据库、文件 I/O) 原则上必需,如果耗时就分离到夜间执行 外部依赖的初始化需要一些功夫,但自动化的价值很高
UI 自动化测试(冒烟) 放在单独作业中,只跑少量用例 需要桌面会话,不稳定因素较多,仅限于启动~主要界面跳转这类程度
UI 自动化测试(全面覆盖) 不作为 CI 的关卡 维护成本容易超过收益

对桌面应用而言,自动化价值最高的并不是 UI,而是 UI 之下的那一层。如果业务逻辑埋在代码后置(code-behind)里,就写不了单元测试,因此把逻辑从界面中分离出来,本身就是 CI/CD 的前提投入。UI 自动化测试应限定在「启动、登录、主要界面能打开」这类冒烟测试上,放到夜间等单独的触发条件下执行,是现实可行的做法。Runner 上的 UI 测试在界面会话、分辨率、时序方面陷阱很多,这方面的内容(包括 CI、无人值守执行的坑)在「Windows 桌面应用的 UI 自动化测试 ── UI Automation 原理与用 FlaUI 打造不易损坏的测试」中有详细处理。

8. 总结

  • 只要把出口定义为「已签名发布物的完成」,桌面应用的 CI/CD 就能用与 Web 相同的工具组合来搭建。
  • 最小配置是 windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact。仅此一项就能消除「只能在某个开发者的 PC 上构建」的风险。12
  • 由于 WinForms / WPF 以 net8.0-windows 这类 Windows 专用 TFM 为目标,因此以 Windows Runner 为前提。3
  • 版本号通过 v1.2.3 标签 → 注入 -p:Version 的标签驱动方式实现一元化管理。4
  • 签名是 CI 自动化中最大的门槛。如今 OV 证书也必须使用 HSM 保管,若想在 CI 中完整闭环,云签名服务是首选方案;PFX+Secret 方式则更适合内部 CA、已有证书的场景。59
  • 发布形式接入 CI 的难易度依次为:xcopy(zip)→MSI / MSIX→ClickOnce。MSIX 必须签名,6 ClickOnce 需要留意 msbuild /target:publish 以及修订号不会自动递增这两点。7
  • 把单元测试作为 CI 的必经关卡,UI 测试限定为冒烟测试并放在单独的作业中执行,把逻辑从界面中分离出来是前提投入。

相关文章

相关咨询领域

合同会社小村软件(KomuraSoft)除了承接 WinForms / WPF 应用开发之外,也承接从本地构建运维向 CI/CD 迁移、用 GitHub Actions 设计构建・签名・发布流水线、以及为现有桌面应用实现可测试化(逻辑分离)等业务。

参考链接

  1. GitHub Docs, GitHub-hosted runners reference。关于 windows-latest 等 Runner 标签、每个作业都会分配一台全新虚拟机,以及公共仓库中标准 Runner 免费这几点的说明。  2 3

  2. Microsoft Learn, GitHub Actions and .NET。关于通过 GitHub Actions 实现 .NET 的 CI/CD、actions/checkout 与 actions/setup-dotnet 的作用,以及在工作流中使用 dotnet restore / build / test / publish 的说明。  2

  3. Microsoft Learn, MSBuild reference for .NET Desktop SDK projects。关于 WinForms / WPF 项目会指定 net8.0-windows 这类 Windows 专用 TFM,以及通过 UseWindowsForms / UseWPF 启用 .NET 桌面 SDK 的说明。  2 3

  4. Microsoft Learn, Set assembly attributes in a project file。关于 AssemblyVersion / FileVersion(去掉后缀)与 InformationalVersion 默认从 Version 属性生成,以及 .NET 8 SDK 及以后版本会把 SourceRevisionId(提交哈希)附加到 InformationalVersion 中的说明。  2 3 4

  5. Microsoft Learn, Code signing options for Windows app developers。关于自 2023 年 6 月起 CA/Browser Forum 要求 OV 证书的私钥必须保存在 HSM/硬件 Token 中、EV 证书的首次 SmartScreen 免信任效果已于 2024 年废除、Azure Artifact Signing(原 Trusted Signing)无需 Token 即可与 GitHub Actions 等集成但可用地区有限,以及 Store 端的 MSIX 发布会由 Microsoft 重新签名的说明。  2 3 4 5 6 7

  6. Microsoft Learn, Sign an MSIX package。关于 Windows 要求 MSIX 包必须具备有效代码签名,以及时间戳能让签名验证在证书过期后依然保持有效的说明。  2 3 4 5

  7. Microsoft Learn, Build .NET ClickOnce applications from the command line。关于 .NET 的 ClickOnce 发布需要使用指定了发布配置文件的 msbuild /target:publish,以及 ApplicationRevision 在命令行构建中不会自动递增的说明。  2 3 4

  8. Microsoft Learn, SignTool。关于 SignTool 包含在 Windows SDK 中、当前版本要求指定 /fd/td 并推荐 SHA256,以及通过 /tr 指定 RFC 3161 时间戳的说明。  2

  9. GitHub Docs, Using secrets in GitHub Actions。关于 Secret 值会在日志中自动隐藏、把证书等二进制数据以 Base64 存入 Secret 并在作业中还原的步骤,以及来自 Fork 触发的工作流不会拿到 Secret 的说明。  2 3 4

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

WinForms / WPF 应用的构建应该在 GitHub Actions 的哪种 Runner 上运行?
应使用 windows-latest 等 Windows Runner。WinForms / WPF 项目以 net8.0-windows 这类 Windows 专用目标框架为对象,无论是验证构建产物的运行情况,还是通过 dotnet test 执行测试,都需要 Windows 环境。由于 GitHub 托管 Runner 会为每个作业分配一台全新的虚拟机,因此可以获得不依赖开发者本人 PC 的、可复现的构建。公共仓库使用标准 Runner 是免费的,私有仓库则按分钟计费。
代码签名能在 CI 中完全自动化吗?
这取决于证书的持有方式。以往那种把 PFX 文件放进 Secret、用 signtool 签名的方式,由于 2023 年 6 月以后 CA/Browser Forum 的要求规定公开 OV 证书的私钥必须保存在 HSM(硬件)中,对于新申请的证书,原则上已经无法再使用这种方式。USB Token 形态的证书无法插入云端 Runner,因此如果想在 CI 中实现完全自动化,现实的做法是经由 Azure Artifact Signing(原 Trusted Signing)这类云签名服务,或 CA 提供的云端 HSM。如果继续沿用 Token 运维方式,就只能把签名这一环节留在本地机器或自建(self-hosted)Runner 上。
应该从哪里开始自动化?
首先应该只引入构建+测试的自动化。仅仅做到每次 push 都会在 windows-latest Runner 上运行 dotnet build / dotnet test,就能消除「只能在某个开发者的 PC 上构建」「合并导致构建损坏却要到发布前夕才发现」这两个最大的风险。签名、安装程序制作、发布的自动化可以之后再逐步添加,如果一开始就想把所有环节都搭好,往往会卡在签名这一步。
发布形式(MSI / MSIX / ClickOnce / xcopy)不同,CI/CD 的搭建难易度也会不同吗?
会有很大不同。xcopy 发布(zip)只需压缩 dotnet publish 的输出,是最简单的方式。MSIX 可以通过 MSBuild 与 signtool 接入 CI,但对包进行签名是必需的。MSI 可以通过在 CI 中调用 WiX 等工具来实现自动化。ClickOnce 无法通过 dotnet CLI 发布,需要组合使用 msbuild /target:publish 与发布配置文件(publish profile),并且还要注意命令行方式下修订号(revision)不会自动递增这一点。在决定发布方式的阶段,就应该把是否容易接入 CI 也纳入判断依据。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表