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

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

大家好,我是徐徐。今天我们讲讲 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) 来优化下载过程。
  4. 验证更新: 使用 verifySignature 方法验证下载的文件。
  5. 安装更新: 使用 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值