Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater

简介: 本文介绍了 Electron 应用在 Windows 中的更新原理,重点分析了 `NsisUpdater` 类的实现。该类利用 NSIS 脚本,通过初始化、检查更新、下载更新、验证签名和安装更新等步骤,确保应用的更新过程安全可靠。核心功能包括差异下载、签名验证和管理员权限处理,确保更新高效且安全。

本文首发微信公众号:前端徐徐。

大家好,我是徐徐。今天我们讲讲 Electron 应用在 windows 中的更新原理。

前言

在 Electron 中 Windows 应用的更新原理其实并不复杂,他巧妙的结合了 NSIS,专门为基于 NSIS (Nullsoft Scriptable Install System) 的 Windows 应用程序设计了相应的更新器,下面我们来详细看看具体是如何实现的吧。

源码位置

https://github1s.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/NsisUpdater.ts

整体流程概述

  1. 初始化: 通过构造函数设置更新器。
  2. 检查更新: (不在这段代码中,但是更新流程的一部分)。
  3. 下载更新: 使用 doDownloadUpdate 方法下载更新文件。
  • 可能使用差异下载 (differentialDownloadWebPackage) 来优化下载过程。
  1. 验证更新: 使用 verifySignature 方法验证下载的文件。
  2. 安装更新: 使用 doInstall 方法执行安装过程。

构造函数和签名验证设置

这里主要是在更新过程开始时,初始化更新器并设置必要的配置,为后续的更新包验证准备签名验证机制。

constructor(options?: AllPublishOptions | null, app?: AppAdapter) {
  super(options, app)
}
protected _verifyUpdateCodeSignature: verifyUpdateCodeSignature = (publisherNames: Array<string>, unescapedTempUpdateFile: string) =>
  verifySignature(publisherNames, unescapedTempUpdateFile, this._logger)
get verifyUpdateCodeSignature(): verifyUpdateCodeSignature {
  return this._verifyUpdateCodeSignature
}
set verifyUpdateCodeSignature(value: verifyUpdateCodeSignature) {
  if (value) {
    this._verifyUpdateCodeSignature = value
  }
}

核心逻辑包括:

  • 构造函数初始化更新器,设置基本配置和应用适配器。
  • _verifyUpdateCodeSignature 方法定义了默认的签名验证逻辑。
  • getter 和 setter 允许自定义签名验证方法。

下载更新

这是更新过程中的核心步骤,负责获取新版本的安装程序,确保下载的文件是完整和安全的,为后续的安装步骤准备必要的文件。

protected doDownloadUpdate(downloadUpdateOptions: DownloadUpdateOptions): Promise<Array<string>> {
  const provider = downloadUpdateOptions.updateInfoAndProvider.provider
  const fileInfo = findFile(provider.resolveFiles(downloadUpdateOptions.updateInfoAndProvider.info), "exe")!
  return this.executeDownload({
    fileExtension: "exe",
    downloadUpdateOptions,
    fileInfo,
    task: async (destinationFile, downloadOptions, packageFile, removeTempDirIfAny) => {
      const packageInfo = fileInfo.packageInfo
      const isWebInstaller = packageInfo != null && packageFile != null
      if (isWebInstaller && downloadUpdateOptions.disableWebInstaller) {
        throw newError("Unable to download new version. Web Installers are disabled", "ERR_UPDATER_WEB_INSTALLER_DISABLED")
      }
      
      // 执行下载逻辑
      if (isWebInstaller || downloadUpdateOptions.disableDifferentialDownload ||
          (await this.differentialDownloadInstaller(fileInfo, downloadUpdateOptions, destinationFile, provider, CURRENT_APP_INSTALLER_FILE_NAME))) {
        await this.httpExecutor.download(fileInfo.url, destinationFile, downloadOptions)
      }
      // 验证签名
      const signatureVerificationStatus = await this.verifySignature(destinationFile)
      if (signatureVerificationStatus != null) {
        await removeTempDirIfAny()
        throw newError(`New version is not signed by the application owner: ${signatureVerificationStatus}`, "ERR_UPDATER_INVALID_SIGNATURE")
      }
      // 处理 Web 安装程序的包下载
      if (isWebInstaller) {
        // ... (Web 安装程序包下载逻辑)
      }
    },
  })
}

核心逻辑包括:

  • 方法首先解析更新信息,找到要下载的 exe 文件。
  • 处理 Web 安装程序和普通安装程序的下载逻辑。
  • 支持差异下载和完整下载。
  • 下载完成后验证文件签名。
  • 对于 Web 安装程序,还需要下载额外的包文件。

差异下载

在下载更新时,尝试使用差异下载来减少带宽使用和下载时间,通过只下载变更的部分,提高更新效率,为大型更新包提供优化的下载方式

private async differentialDownloadWebPackage(
  downloadUpdateOptions: DownloadUpdateOptions,
  packageInfo: PackageFileInfo,
  packagePath: string,
  provider: Provider<any>
): Promise<boolean> {
  if (packageInfo.blockMapSize == null) {
    return true
  }
  try {
    const downloadOptions: DifferentialDownloaderOptions = {
      newUrl: new URL(packageInfo.path),
      oldFile: path.join(this.downloadedUpdateHelper!.cacheDir, CURRENT_APP_PACKAGE_FILE_NAME),
      logger: this._logger,
      newFile: packagePath,
      requestHeaders: this.requestHeaders,
      isUseMultipleRangeRequest: provider.isUseMultipleRangeRequest,
      cancellationToken: downloadUpdateOptions.cancellationToken,
    }
    if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) {
      downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it)
    }
    await new FileWithEmbeddedBlockMapDifferentialDownloader(packageInfo, this.httpExecutor, downloadOptions).download()
  } catch (e: any) {
    this._logger.error(`Cannot download differentially, fallback to full download: ${e.stack || e}`)
    return process.platform === "win32"
  }
  return false
}

核心逻辑包括:

  • 检查是否支持差异下载(通过 blockMapSize)。
  • 设置差异下载的选项,包括新旧文件路径、请求头等。
  • 使用 FileWithEmbeddedBlockMapDifferentialDownloader 执行差异下载。
  • 如果差异下载失败,在 Windows 平台上回退到完整下载。

签名验证

在下载完成后,验证更新文件的真实性和完整性,防止安装未经授权或被篡改的更新。

private async verifySignature(tempUpdateFile: string): Promise<string | null> {
  let publisherName: Array<string> | string | null
  try {
    publisherName = (await this.configOnDisk.value).publisherName
    if (publisherName == null) {
      return null
    }
  } catch (e: any) {
    if (e.code === "ENOENT") {
      // no app-update.yml
      return null
    }
    throw e
  }
  return await this._verifyUpdateCodeSignature(Array.isArray(publisherName) ? publisherName : [publisherName], tempUpdateFile)
}

核心逻辑包括:

  • 从磁盘配置中读取发布者名称。
  • 如果没有配置文件或发布者名称,则跳过验证。
  • 调用 _verifyUpdateCodeSignature 方法进行实际的签名验证。

安装

这是更新过程的最后一步,负责执行实际的安装,确保安装程序以正确的权限和参数运行,处理安装过程中可能出现的各种情况和错误。

protected doInstall(options: InstallOptions): boolean {
  const args = ["--updated"]
  if (options.isSilent) {
    args.push("/S")
  }
  if (options.isForceRunAfter) {
    args.push("--force-run")
  }
  if (this.installDirectory) {
    args.push(`/D=${this.installDirectory}`)
  }
  const packagePath = this.downloadedUpdateHelper == null ? null : this.downloadedUpdateHelper.packageFile
  if (packagePath != null) {
    args.push(`--package-file=${packagePath}`)
  }
  const callUsingElevation = (): void => {
    this.spawnLog(path.join(process.resourcesPath, "elevate.exe"), [options.installerPath].concat(args)).catch(e => this.dispatchError(e))
  }
  if (options.isAdminRightsRequired) {
    this._logger.info("isAdminRightsRequired is set to true, run installer using elevate.exe")
    callUsingElevation()
    return true
  }
  this.spawnLog(options.installerPath, args).catch((e: Error) => {
    // 错误处理逻辑
    if (errorCode === "UNKNOWN" || errorCode === "EACCES") {
      callUsingElevation()
    } else if (errorCode === "ENOENT") {
      require("electron").shell.openPath(options.installerPath).catch((err: Error) => this.dispatchError(err))
    } else {
      this.dispatchError(e)
    }
  })
  return true
}

核心逻辑包括:

  • 根据提供的选项构建安装参数。
  • 支持静默安装、强制运行、自定义安装目录等选项。
  • 处理需要管理员权限的情况,使用 elevate.exe 提升权限。
  • 处理各种安装错误,包括权限问题和文件不存在的情况。

结语

通过简单的分析源码发现,Electron 应用在 windows 上的更新其实也不难,这个NsisUpdater 类不仅仅是一个简单的文件下载和替换工具,而是一个精心设计的系统,它考虑到了软件更新过程中的诸多方面,当然底层的 NSIS 程序帮助了我们不少的忙,如果在往下探的话,可以研究一下 NSIS,具体可以参考:https://nsis.sourceforge.io/Main_Page

相关文章
|
2月前
|
前端开发 开发者 UED
你真的了解 Electron 的自动更新吗?揭秘AppUpdater 类的内部工作原理
本文由前端徐徐首发,深入探讨了 Electron 的自动更新工作原理,特别是 `electron-builder` 中 `AppUpdater` 类的源码分析,涵盖配置更新源、检查更新、下载更新、安装更新及事件通知等核心功能,帮助开发者更好地理解和使用 Electron 的自动更新机制。
180 0
你真的了解 Electron 的自动更新吗?揭秘AppUpdater 类的内部工作原理
|
2月前
|
安全 前端开发 iOS开发
揭秘 electron-builder:macOS 应用打包背后到底发生了什么?
本文详细介绍了 Electron 应用在 macOS 平台上的打包流程,涵盖配置文件、打包步骤、签名及 notarization 等关键环节。通过剖析 `electron-builder` 的源码,展示了如何处理多架构应用、执行签名,并解决常见问题。适合希望深入了解 macOS 打包细节的开发者。
111 2
|
2月前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
233 2
|
2月前
|
监控 前端开发 安全
谈谈我做 Electron 应用的这一两年
本文首发于微信公众号“前端徐徐”,作者徐徐分享了过去一两年间开发Electron桌面应用的经验与心得。文章详细介绍了从项目启动、技术选型到具体实施的过程,并探讨了桌面端开发面临的挑战及解决方案,如软件更新、任务队列设计、性能优化等。此外,还列举了一些特殊需求的实现方法,如静默安装、进程禁用等。作者认为,尽管桌面端开发有其独特性,但通过不断探索与实践,仍能显著提升用户体验和技术水平。
185 0
谈谈我做 Electron 应用的这一两年
|
2月前
|
XML 缓存 前端开发
Electron-builder 是如何打包 Windows 应用的?
本文首发于微信公众号“前端徐徐”,作者徐徐深入解析了 electron-builder 在 Windows 平台上的打包流程。文章详细介绍了 `winPackager.ts`、`AppxTarget.ts`、`MsiTarget.ts` 和 `NsisTarget.ts` 等核心文件,涵盖了目标创建、图标处理、代码签名、资源编辑、应用签名、性能优化等内容,并分别讲解了 AppX/MSIX、MSI 和 NSIS 安装程序的生成过程。通过这些内容,读者可以更好地理解和使用 electron-builder 进行 Windows 应用的打包和发布。
202 0
|
2月前
|
API Windows
Windows之窗口原理
这篇文章主要介绍了Windows窗口原理和如何使用Windows API创建和管理窗口。
65 0
|
2月前
|
数据可视化 程序员 C#
C#中windows应用窗体程序的输入输出方法实例
C#中windows应用窗体程序的输入输出方法实例
56 0
|
3月前
|
存储 安全 程序员
Windows任务管理器开发原理与实现
Windows任务管理器开发原理与实现
|
4月前
|
vr&ar C# 图形学
WPF与AR/VR的激情碰撞:解锁Windows Presentation Foundation应用新维度,探索增强现实与虚拟现实技术在现代UI设计中的无限可能与实战应用详解
【8月更文挑战第31天】增强现实(AR)与虚拟现实(VR)技术正迅速改变生活和工作方式,在游戏、教育及工业等领域展现出广泛应用前景。本文探讨如何在Windows Presentation Foundation(WPF)环境中实现AR/VR功能,通过具体示例代码展示整合过程。尽管WPF本身不直接支持AR/VR,但借助第三方库如Unity、Vuforia或OpenVR,可实现沉浸式体验。例如,通过Unity和Vuforia在WPF中创建AR应用,或利用OpenVR在WPF中集成VR功能,从而提升用户体验并拓展应用功能边界。
91 0
|
4月前
|
存储 开发者 C#
WPF与邮件发送:教你如何在Windows Presentation Foundation应用中无缝集成电子邮件功能——从界面设计到代码实现,全面解析邮件发送的每一个细节密武器!
【8月更文挑战第31天】本文探讨了如何在Windows Presentation Foundation(WPF)应用中集成电子邮件发送功能,详细介绍了从创建WPF项目到设计用户界面的全过程,并通过具体示例代码展示了如何使用`System.Net.Mail`命名空间中的`SmtpClient`和`MailMessage`类来实现邮件发送逻辑。文章还强调了安全性和错误处理的重要性,提供了实用的异常捕获代码片段,旨在帮助WPF开发者更好地掌握邮件发送技术,提升应用程序的功能性与用户体验。
78 0