本文首发微信公众号:前端徐徐。
大家好,我是徐徐。今天我们讲讲 Electron 应用在 windows 中的更新原理。
前言
在 Electron 中 Windows 应用的更新原理其实并不复杂,他巧妙的结合了 NSIS,专门为基于 NSIS (Nullsoft Scriptable Install System) 的 Windows 应用程序设计了相应的更新器,下面我们来详细看看具体是如何实现的吧。
源码位置
整体流程概述
- 初始化: 通过构造函数设置更新器。
- 检查更新: (不在这段代码中,但是更新流程的一部分)。
- 下载更新: 使用
doDownloadUpdate
方法下载更新文件。
- 可能使用差异下载 (
differentialDownloadWebPackage
) 来优化下载过程。
- 验证更新: 使用
verifySignature
方法验证下载的文件。 - 安装更新: 使用
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 。