本文首发同名微信公众号:前端徐徐
大家好,我是徐徐。今天我们讲讲 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 。