• Electron应用如何实现自动更新的功能?
  • 发布于 1周前
  • 36 热度
    0 评论

问题:如何为Electron应用实现一个简易的自动更新功能

官方其实已经提供了几种很便捷的方案:https://www.electronjs.org/docs/tutorial/updates 但是不是需要github,就是需要搭建一个服务端,因为我们的场景很小,electron只是一个壳,所以更新的需求不强烈,只是一个以防万一的功能,所以我们想寻求一个简单的方式来处理。


autoUpdater
我们用electron-forge进行打包,其实这就自带了更新功能,即autoUpdater。使用也很简单只需要几步,如下:
// 堆代码 duidaima.com
const { autoUpdater } = require('electron')
//先设置更新的url
autoUpdater.setFeedURL({url: "https://xxxxxx"});
//在合适的时机检查更新
autoUpdater.checkForUpdates();
其实这样就可以了,checkForUpdates会检查更新并自动下载安装,全程无感知。当重启应用的时候就会是新版本的了。当然这是最简单的步骤,我们后面会丰富一下功能。

这里有几个问题。

首先,mac上如果想更新,那么必须是签名的应用,目前我们的mac应用未签名,所以不能使用,会提示。
Error: Could not get code signature for running application

其次,就是更新url,这地址对应的是什么?我们如何方便快捷的构建出一个更新服务?在官方文档中没有详细的描述这个地址对应的是什么,因为如果使用官方提供的几种服务后台,可以通过后台界面直接添加一个更新即可,其他的无需关心。但是我们又不打算使用官方提供的方案,那么我们就必须自己研究出这个url对应的是什么?是文件?配置数据?

更新服务
经过我几天的摸索,查阅相关文档和源码,最终确定了url背后的东西。因为我们目前只考虑windows,所以下面都是以windows为准。我们用forge通过squirrel-maker来创建windows安装包,创建后文件路径是项目根目录/out/make/squirrel.windows/x64/xxxx.exe。

但是同目录下还同时生成了另外两个文件RELEASES和xxx.nupkg,这就是我们更新所需要的文件,其中RELEASES相当于配置文件,里面记录着nupkg文件的完整名称、SHA512(用于校验)和文件大小,如下:
674802FE0AE3B272F5182E4626893FDB2D8D2107 xxxxxx-0.1.0-full.nupkg 76560314

所以我们将这两个文件上传到文件服务器上,放在同一个目录下(或虚拟目录),然后将目录的地址设置为feedUrl即可。这样autoUpdater会自动下载该目录下的RELEASES文件并读取配置,然后通过拿到的文件名下载更新文件并校验,成功后即自动后台安装。

如果我们观察应用的根目录就会发现,实际上在应用根目录有以不同版本号命名的目录,后台安装实际上就是将新版本下载后解压到根目录中新版本号的目录中,然后重启的时候,执行文件exe就会使用新版本号的目录中的文件运行,这样就完成了更新。而旧版本的文件实际上还存在根目录中。所以才会无感知的进行安装,因为不需要删除修改文件(需要修改很少的配置文件)。

问题
其实并没有这么顺利,下面总结了中间遇到的几个问题。
1.出错弹窗乱码,查看详细日志
如果electron运行时出错,那么就会弹窗提示,但是在实际运行中发现,如果错误信息中有中文,那么就会导致错误信息乱码。这样就无法看到准确的信息。

如何处理呢?
在应用的根目录(安装目录,一般在c:/用户/[用户名]/AppData/Local/[应用名])会生成一个SquirrelSetup.log的日志文件,这里面就记录着错误的详细信息。
System.Exception: Couldn't acquire lock, is another instance running
查看SquirrelSetup.log看到这个错误的详细信息如下:
2023-04-25 15:09:13> SingleGlobalInstance: Failed to grab lockfile, will retry: C:\Users\guozh\AppData\Local\Temp\.squirrel-lock-68CEC12091756AFBF3BF0445D48359FFDABDAB12: System.IO.IOException: 文件“C:\Users\guozh\AppData\Local\Temp\.squirrel-lock-68CEC12091756AFBF3BF0445D48359FFDABDAB12”正由另一进程使用,因此该进程无法访问此文件。
   在 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   在 System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   在 System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   在 Squirrel.SingleGlobalInstance..ctor(String key, TimeSpan timeOut)
2021-04-25 15:09:14> Unhandled exception: System.AggregateException: 发生一个或多个错误。---> System.Exception: Couldn't acquire lock, is another instance running
   在 Squirrel.SingleGlobalInstance..ctor(String key, TimeSpan timeOut)
   在 Squirrel.UpdateManager.<acquireUpdateLock>b__32_0()
   在 System.Threading.Tasks.Task`1.InnerInvoke()
   在 System.Threading.Tasks.Task.Execute()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.UpdateManager.<CheckForUpdate>d__7.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Update.Program.<CheckForUpdate>d__8.MoveNext()
   --- 内部异常堆栈跟踪的结尾 ---
   在 System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   在 System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   在 System.Threading.Tasks.Task`1.get_Result()
   在 Squirrel.Update.Program.executeCommandLine(String[] args)
   在 Squirrel.Update.Program.main(String[] args)
---> (内部异常 #0) System.Exception: Couldn't acquire lock, is another instance running
   在 Squirrel.SingleGlobalInstance..ctor(String key, TimeSpan timeOut)
   在 Squirrel.UpdateManager.<acquireUpdateLock>b__32_0()
   在 System.Threading.Tasks.Task`1.InnerInvoke()
   在 System.Threading.Tasks.Task.Execute()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.UpdateManager.<CheckForUpdate>d__7.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Update.Program.<CheckForUpdate>d__8.MoveNext()<---
出现这个错误怀疑是与electron编译的安装包有关,运行安装包的时候会展示安装动画,但是安装完成已经打开应用了,动画还没有消失,有时候甚至持续几分钟。

应用一打开的时候就会进行更新,所以这时候有可能应用进程和安装器进程有冲突,导致上面的问题。目前这个问题还没有很好的规避,但是可以通过注册处理autoUpdater的error事件进行规避,如下:
autoUpdater.on('error', (error) => {
    //dialog.showMessageBox({message:"error:" + error.name + "," + error.message + "," + error.stack})
    console.log("error:" + error.name + "," + error.message + "," + error.stack)
  });
在添加了这样的代码后,就不会再弹窗提示了。但是实际问题还存在,在SquirrelSetup.log中还会记录相关错误,而且更新中断。所以这并不是解决办法,这样处理后会导致第一次启动更新大概率失败,不过再次启动的时候就会正常更新了,所以暂时可以接受。

2. 服务器403
查看SquirrelSetup.log看到这个错误的详细信息如下:
2023-04-25 14:51:42> IEnableLogger: Failed to download url: https://appd.knowbox.cn/aiclass-pc-update/dev/RELEASES?id=aiclass&localVersion=0.1.0&arch=amd64: System.Net.WebException: 远程服务器返回错误: (403) 已禁止。
   在 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   在 System.Net.WebClient.GetWebResponse(WebRequest request, IAsyncResult result)
   在 System.Net.WebClient.DownloadBitsResponseCallback(IAsyncResult result)
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Utility.<LogIfThrows>d__43`1.MoveNext()
2023-04-25 14:51:42> FileDownloader: Downloading url: https://appd.knowbox.cn/aiclass-pc-update/dev/releases?id=aiclass&localversion=0.1.0&arch=amd64
2021-04-25 14:51:43> IEnableLogger: Failed to download url: https://appd.knowbox.cn/aiclass-pc-update/dev/releases?id=aiclass&localversion=0.1.0&arch=amd64: System.Net.WebException: 远程服务器返回错误: (403) 已禁止。
   在 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   在 System.Net.WebClient.GetWebResponse(WebRequest request, IAsyncResult result)
   在 System.Net.WebClient.DownloadBitsResponseCallback(IAsyncResult result)
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Utility.<LogIfThrows>d__43`1.MoveNext()
2023-04-25 14:51:43> CheckForUpdateImpl: Download resulted in WebException (returning blank release list): System.Net.WebException: 远程服务器返回错误: (403) 已禁止。
   在 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   在 System.Net.WebClient.GetWebResponse(WebRequest request, IAsyncResult result)
   在 System.Net.WebClient.DownloadBitsResponseCallback(IAsyncResult result)
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Utility.<LogIfThrows>d__43`1.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.FileDownloader.<DownloadUrl>d__3.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   在 Squirrel.UpdateManager.CheckForUpdateImpl.<CheckForUpdate>d__2.MoveNext()
其实上面只是告诉我们服务端返回了403,至于为什么并没有说明。url是没问题的,文件也存在,在浏览器中也可以访问,为什么会出现403。最后通过charles抓包发现,服务器返回的是:
{
    // 堆代码 duidaima.com
    "code": "40310011",
    "msg": "invalid User-Agent header"
}
在charles中查看这个请求的header发现没有User-Agent,所以应该就是这里出现的问题。通过postman我们模拟请求,发现当删除User-Agent就会出现上面的错误,随便添加一个就可以正常访问。

因为应用用的是electron自带的更新,所以无法干预这个请求,那么就从服务器这边入手。经过测试发现七牛没有这样的问题,即使没有User-Agent也可以正常访问,所以应该是upyun有什么配置。替换成功七牛后就可以正常访问了。

完善
上面只是最简单的步骤,打开应用后就会自动检测更新,又更新就自动下载安装。用户无感知,所以不知道何时更新,只有用户关闭重启应用后才会使用新版本。所以我们需要通知用户。

autoUpdater有很多事件回调,我们上面提到了error,我们就通过监听这些事件来通知用户,这样就实现了更新功能,相对于官方的方案更简单轻量,后续只要更新服务器上的两个文件即可。

本地更新
官方还提供了一个方案,手动下载更新包到本地,然后通过本地更新,但是没有上面的简单,但是因为一起调研了一下,所以也简单记录一下。下载这部分就不说了,参考网上的文档即可。主要说一下本地文件位置和更新。electron如何保存一些临时文件,在哪里保存比较好?官网的给了一个很好的例子,代码如下:
var path = require('path');
var fs = require('fs');

global.tmpPath = path.join( app.getPath("temp"), "AICLASS");
if( !fs.existsSync(global.tmpPath)){
    fs.mkdirSync(global.tmpPath);
}
这样我们得到了一个临时目录tmpPath,那么这个目录在哪里呢?它的位置在c:/用户/[用户名]/AppData/Local/Temp/AICLASS,其实就是浏览器的缓存目录,其中AICLASS是我们自己定义的目录。

我们将文件下载到这个目录中,就可以通过autoUpdater进行本地更新了,与网络更新一样,只不过feedUrl变成了本地目录而已,如下:
autoUpdater.setFeedURL({url: global.tmpPath});

通过zip解压的应用
因为win7可能缺少某些必要的库,所以electron的安装包实际上并不能成功运行。这部分用户我们提供的是zip包,自行解压即可。

但是这部分用户就不能使用autoUpdater了,因为这个是依赖于squirrel安装器的,如果是通过zip解压的则没有,所以无法使用。这部分用户目前只能通过手动下载新的zip解压覆盖来实现更新。
用户评论