electron-updater 核心源码解析
插件的源码实现我们根据业务调用的流程去看相对来说好理解,本文档主要根据更新流程来梳理,有兴趣的同学可以扩展其他方面
问题1: 如何实现跨系统兼容
问题2: 为什么本地无法调试
问题3: 如何判断是否需要升级及下载
问题4: 如果进行更新
1..插件各核心模块流程图

2.根据业务层调用解析实现思路
代码中标红色为核心的钩子和配置项
1. 插件入口main.ts
用来区分不同系统的升级流程,此次解析主要以window系统流程为主
ini
if (process.platform === "win32") {
_autoUpdater = new (require("./NsisUpdater").NsisUpdater)()
} else if (process.platform === "darwin") {
_autoUpdater = new (require("./MacUpdater").MacUpdater)()
} else {
_autoUpdater = new (require("./AppImageUpdater").AppImageUpdater)()
......
}
2.1 setFeedURL
此函数的主要作用是将业务层传入的url处理之后生成createClient, 此函数的主要作用是读取文件夹下的latest.yml 文件来获取更新程序的版本信息
typescript
setFeedURL(options: PublishConfiguration | AllPublishOptions | string) {
const runtimeOptions = this.createProviderRuntimeOptions()
// https://github.com/electron-userland/electron-builder/issues/1105
let provider: Provider<any>
if (typeof options === "string") {
provider = new GenericProvider({ provider: "generic", url: options }, this, {
...runtimeOptions,
isUseMultipleRangeRequest: isUrlProbablySupportMultiRangeRequests(options),
})
} else {
provider = createClient(options, this, runtimeOptions)
}
this.clientPromise = Promise.resolve(provider)
}
2.2 checkForUpdates
此函数的核心逻辑是:通过下载latest.yml 文件,根据yml的版本信息和本地的版本信息做比对,来确定是否需要升级
kotlin
private async doCheckForUpdates(): Promise<UpdateCheckResult> {
this.emit("checking-for-update")
const result = await this.getUpdateInfoAndProvider()
const updateInfo = result.info
if (!(await this.isUpdateAvailable(updateInfo))) {
this._logger.info(
`Update for version ${this.currentVersion.format()} is not available (latest version: ${updateInfo.version}, downgrade is ${
this.allowDowngrade ? "allowed" : "disallowed"
}).`
)
this.emit("update-not-available", updateInfo)
return {
versionInfo: updateInfo,
updateInfo,
}
}
this.updateInfoAndProvider = result
this.onUpdateAvailable(updateInfo) // 钩子函数 update-available
const cancellationToken = new CancellationToken()
//noinspection ES6MissingAwait
return {
versionInfo: updateInfo,
updateInfo,
cancellationToken,
// 此时如果配置 autoDownload 则会自动进行下载
downloadPromise: this.autoDownload ? this.downloadUpdate(cancellationToken) : null,
}
}
private async isUpdateAvailable(updateInfo: UpdateInfo): Promise<boolean> {
const latestVersion = parseVersion(updateInfo.version)
if (latestVersion == null) {
throw newError(
`This file could not be downloaded, or the latest version (from update server) does not have a valid semver version: "${updateInfo.version}"`,
"ERR_UPDATER_INVALID_VERSION"
)
}
const currentVersion = this.currentVersion
// 判断当前版本和云端配置文件yml 中的版本
if (isVersionsEqual(latestVersion, currentVersion)) {
return false
}
// 判断支持的系统版本范围
const minimumSystemVersion = updateInfo?.minimumSystemVersion
const currentOSVersion = release()
if (minimumSystemVersion) {
try {
if (isVersionLessThan(currentOSVersion, minimumSystemVersion)) {
this._logger.info(`Current OS version ${currentOSVersion} is less than the minimum OS version required ${minimumSystemVersion} for version ${currentOSVersion}`)
return false
}
} catch (e: any) {
this._logger.warn(`Failed to compare current OS version(${currentOSVersion}) with minimum OS version(${minimumSystemVersion}): ${(e.message || e).toString()}`)
}
}
const isStagingMatch = await this.isStagingMatch(updateInfo)
if (!isStagingMatch) {
return false
}
// https://github.com/electron-userland/electron-builder/pull/3111#issuecomment-405033227
// https://github.com/electron-userland/electron-builder/pull/3111#issuecomment-405030797
const isLatestVersionNewer = isVersionGreaterThan(latestVersion, currentVersion)
const isLatestVersionOlder = isVersionLessThan(latestVersion, currentVersion)
if (isLatestVersionNewer) {
return true
}
return this.allowDowngrade && isLatestVersionOlder
}
// 这里如果不打包 this.app.isPackaged 且不配置 forceDevUpdateConfig(在json的nsis中进行配置)时,本地是无法进行调试的
public isUpdaterActive(): boolean {
const isEnabled = this.app.isPackaged || this.forceDevUpdateConfig
if (!isEnabled) {
this._logger.info("Skip checkForUpdates because application is not packed and dev update config is not forced")
return false
}
return true
}
调用此函数会首先触发钩子 "checking-for-update",
如果没有要更新的则触发 "update-not-available",
有需要更新时触发"update-available",如果用户设置的 autoDownload为true 时会自动下载,否则需要手动调用 downloadUpdate 函数
2.3 downloadUpdate
此函数的核心逻辑是:通过云端yml 文件和本地update-info.json 文件来判断比对是否需要下载升级包,如果本地存在,则直接进行升级,如果本地不存在,则先进行下载
kotlin
downloadUpdate(cancellationToken: CancellationToken = new CancellationToken()): Promise<Array<string>> {
const updateInfoAndProvider = this.updateInfoAndProvider
if (updateInfoAndProvider == null) {
const error = new Error("Please check update first")
this.dispatchError(error)
return Promise.reject(error)
}
if (this.downloadPromise != null) {
this._logger.info("Downloading update (already in progress)")
return this.downloadPromise
}
this._logger.info(
`Downloading update from ${asArray(updateInfoAndProvider.info.files)
.map(it => it.url)
.join(", ")}`
)
const errorHandler = (e: Error): Error => {
// https://github.com/electron-userland/electron-builder/issues/1150#issuecomment-436891159
if (!(e instanceof CancellationError)) {
try {
this.dispatchError(e)
} catch (nestedError: any) {
this._logger.warn(`Cannot dispatch error event: ${nestedError.stack || nestedError}`)
}
}
return e
}
this.downloadPromise = this.doDownloadUpdate({
updateInfoAndProvider,
requestHeaders: this.computeRequestHeaders(updateInfoAndProvider.provider),
cancellationToken,
disableWebInstaller: this.disableWebInstaller,
disableDifferentialDownload: this.disableDifferentialDownload,
})
.catch((e: any) => {
throw errorHandler(e)
})
.finally(() => {
this.downloadPromise = null
})
return this.downloadPromise
}
...
关键函数
// 处理下载路径
export function findFile(files: Array<ResolvedUpdateFileInfo>, extension: string, not?: Array<string>): ResolvedUpdateFileInfo | null | undefined {
if (files.length === 0) {
throw newError("No files provided", "ERR_UPDATER_NO_FILES_PROVIDED")
}
const result = files.find(it => it.url.pathname.toLowerCase().endsWith(`.${extension}`))
if (result != null) {
return result
} else if (not == null) {
return files[0]
} else {
return files.find(fileInfo => !not.some(ext => fileInfo.url.pathname.toLowerCase().endsWith(`.${ext}`)))
}
}
// 下载文件
protected async executeDownload(taskOptions: DownloadExecutorTask): Promise<Array<string>> {
const fileInfo = taskOptions.fileInfo
const downloadOptions: DownloadOptions = {
headers: taskOptions.downloadUpdateOptions.requestHeaders,
cancellationToken: taskOptions.downloadUpdateOptions.cancellationToken,
sha2: (fileInfo.info as any).sha2,
sha512: fileInfo.info.sha512,
}
// 钩子函数 download-progress
if (this.listenerCount(DOWNLOAD_PROGRESS) > 0) {
downloadOptions.onProgress = it => this.emit(DOWNLOAD_PROGRESS, it)
}
const updateInfo = taskOptions.downloadUpdateOptions.updateInfoAndProvider.info
const version = updateInfo.version
const packageInfo = fileInfo.packageInfo
function getCacheUpdateFileName(): string {
// NodeJS URL doesn't decode automatically
const urlPath = decodeURIComponent(taskOptions.fileInfo.url.pathname)
if (urlPath.endsWith(`.${taskOptions.fileExtension}`)) {
return path.basename(urlPath)
} else {
// url like /latest, generate name
return taskOptions.fileInfo.info.url
}
}
const downloadedUpdateHelper = await this.getOrCreateDownloadHelper()
const cacheDir = downloadedUpdateHelper.cacheDirForPendingUpdate
await mkdir(cacheDir, { recursive: true })
const updateFileName = getCacheUpdateFileName()
let updateFile = path.join(cacheDir, updateFileName)
const packageFile = packageInfo == null ? null : path.join(cacheDir, `package-${version}${path.extname(packageInfo.path) || ".7z"}`)
// 此函数用来写入本地update-info.json
const done = async (isSaveCache: boolean) => {
await downloadedUpdateHelper.setDownloadedFile(updateFile, packageFile, updateInfo, fileInfo, updateFileName, isSaveCache)
await taskOptions.done!({
...updateInfo,
downloadedFile: updateFile,
})
return packageFile == null ? [updateFile] : [updateFile, packageFile]
}
const log = this._logger
// 此函数是云端yml文件和update-info.json 进行判断比较,用来确定是否需要下载
const cachedUpdateFile = await downloadedUpdateHelper.validateDownloadedPath(updateFile, updateInfo, fileInfo, log)
if (cachedUpdateFile != null) {
updateFile = cachedUpdateFile
return await done(false)
}
const removeFileIfAny = async () => {
await downloadedUpdateHelper.clear().catch(() => {
// ignore
})
return await unlink(updateFile).catch(() => {
// ignore
})
}
const tempUpdateFile = await createTempUpdateFile(`temp-${updateFileName}`, cacheDir, log)
try {
await taskOptions.task(tempUpdateFile, downloadOptions, packageFile, removeFileIfAny)
await retry(
() => rename(tempUpdateFile, updateFile),
60,
500,
0,
0,
error => error instanceof Error && /^EBUSY:/.test(error.message)
)
} catch (e: any) {
await removeFileIfAny()
if (e instanceof CancellationError) {
log.info("cancelled")
this.emit("update-cancelled", updateInfo)
}
throw e
}
log.info(`New version ${version} has been downloaded to ${updateFile}`)
return await done(true)
}
// 下载成功
protected executeDownload(taskOptions: DownloadExecutorTask): Promise<Array<string>> {
return super.executeDownload({
...taskOptions,
done: event => {
this.dispatchUpdateDownloaded(event) // 钩子函数 update-downloaded
this.addQuitHandler()
return Promise.resolve()
},
})
}
// 下载完整之后的更新逻辑
// 处理退出逻辑
autoUpdater.autoInstallOnAppQuit = false
protected addQuitHandler(): void {
// 根据 autoInstallOnAppQuit 来确定是否自动退出升级
if (this.quitHandlerAdded || !this.autoInstallOnAppQuit) {
return
}
this.quitHandlerAdded = true
this.app.onQuit(exitCode => {
if (this.quitAndInstallCalled) {
this._logger.info("Update installer has already been triggered. Quitting application.")
return
}
if (!this.autoInstallOnAppQuit) {
this._logger.info("Update will not be installed on quit because autoInstallOnAppQuit is set to false.")
return
}
if (exitCode !== 0) {
this._logger.info(`Update will be not installed on quit because application is quitting with exit code ${exitCode}`)
return
}
this._logger.info("Auto install update on quit")
this.install(true, false)
})
}
2.4 quitAndInstall
此函数的核心逻辑是:通过不同的策略生成不同的启动参数传给exe, exe安装时nsis根据不同的参数执行不同的安装流程, 要说明的是,此流程参数为electron-builder 自带的打包生成的exe,云盘客户端为自定义nsis 打包,此传参不适用,目前只集成了 /S 静默升级流程
kotlin
quitAndInstall(isSilent = false, isForceRunAfter = false): void {
this._logger.info(`Install on explicit quitAndInstall`)
const isInstalled = this.install(isSilent, isSilent ? isForceRunAfter : this.autoRunAppAfterInstall)
if (isInstalled) {
setImmediate(() => {
// this event is normally emitted when calling quitAndInstall, this emulates that
require("electron").autoUpdater.emit("before-quit-for-update")
this.app.quit()
})
} else {
this.quitAndInstallCalled = false
}
}
// 设置exe 启动参数
protected doInstall(options: InstallOptions): boolean {
const args = ["--updated"]
if (options.isSilent) { // 是否静默
args.push("/S")
}
if (options.isForceRunAfter) { // 是否强制启动
args.push("--force-run")
}
if (this.installDirectory) { // 默认路径,设置在nsis - installDirectory
// maybe check if folder exists
args.push(`/D=${this.installDirectory}`)
}
const packagePath = this.downloadedUpdateHelper == null ? null : this.downloadedUpdateHelper.packageFile
if (packagePath != null) {
// only = form is supported
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) => {
// https://github.com/electron-userland/electron-builder/issues/1129
// Node 8 sends errors: https://nodejs.org/dist/latest-v8.x/docs/api/errors.html#errors_common_system_errors
const errorCode = (e as NodeJS.ErrnoException).code
this._logger.info(
`Cannot run installer: error code: ${errorCode}, error message: "${e.message}", will be executed again using elevate if EACCES, and will try to use electron.shell.openItem if ENOENT`
)
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
}
注:启动参数为electron-builder 打包之后的安装流程,云盘客户端安装卸载使用nsis自定义开发,上述配置只有 静默安装 /S 有效,其余参数无效
3.问题回溯
问题1: 如何实现跨系统兼容
同一入口文件,通过不同的环境变量来区分加载不同系统的升级文件
问题2: 为什么本地无法调试
代码内容做了拦截判断,本地需要配置 forceDevUpdateConfig
问题3: 如何判断是否需要升级及下载
通过读取升级文件夹下的yml 来获取最新版本的相关信息
问题4: 如果进行更新
通过各种配置策略生成启动命令参数,然后直接启动exe 进行升级 ,nsis脚本通过参数的不同执行不同的安装流程