如何为electron应用实现一个简易的更新功能(代码片段)

BennuCTech BennuCTech     2023-02-24     733

关键词:

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

autoUpdater

我们用electron-forge进行打包,其实这就自带了更新功能,即autoUpdater。使用也很简单只需要几步,如下:

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。

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

674802FE0AE3B272F5182E4626893FDB2D8D2107 xxxxxx-0.1.0-full.nupkg 76560314

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

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

问题

其实并没有这么顺利,下面总结了中间遇到的几个问题。

出错弹窗乱码,查看详细日志

如果electron运行时出错,那么就会弹窗提示,但是在实际运行中发现,如果错误信息中有中文,那么就会导致错误信息乱码。这样就无法看到准确的信息。

如何处理呢?

在应用的根目录(安装目录,一般在c:/用户/[用户名]/AppData/Local/[应用名])会生成一个SquirrelSetup.log的日志文件,这里面就记录着错误的详细信息。

System.Exception: Couldn’t acquire lock, is another instance running

查看SquirrelSetup.log看到这个错误的详细信息如下:

2021-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中还会记录相关错误,而且更新中断。

所以这并不是解决办法,这样处理后会导致第一次启动更新大概率失败,不过再次启动的时候就会正常更新了,所以暂时可以接受。

服务器403

查看SquirrelSetup.log看到这个错误的详细信息如下:

2021-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()
2021-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()
2021-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抓包发现,服务器返回的是:


    "code": "40310011",
    "msg": "invalid User-Agent header"

在charles中查看这个请求的header发现没有User-Agent,所以应该就是这里出现的问题。

通过postman我们模拟请求,发现当删除User-Agent就会出现上面的错误,随便添加一个就可以正常访问。

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

替换成功七牛后就可以正常访问了。

完善

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

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

关注微信公众号:BennuCTech,发送“Electron更新源码”获取完整源码

本地更新

官方还提供了一个方案,手动下载更新包到本地,然后通过本地更新,但是没有上面的简单,但是因为一起调研了一下,所以也简单记录一下。

下载这部分就不说了,参考网上的文档即可。主要说一下本地文件位置和更新。
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解压覆盖来实现更新。

关注公众号:BennuCTech,获取更多干货!

如何为 Electron 重建原生 npm 模块?

】如何为Electron重建原生npm模块?【英文标题】:HowtorebuildnativenpmmodulesforElectron?【发布时间】:2017-08-0715:35:18【问题描述】:请帮助我了解如何为Electron1.6.2重建本机npm模块?我使用https://electron.atom.io/docs/tutorial/quick-start中的步骤... 查看详情

如何为 Electron / Atom Shell App 设置应用程序图标

】如何为Electron/AtomShellApp设置应用程序图标【英文标题】:HowtosetappiconforElectron/AtomShellApp【发布时间】:2015-10-1008:37:25【问题描述】:如何为Electron应用设置应用图标?我正在尝试BrowserWindow(icon:\'path/to/image.png\');,但它不起作用... 查看详情

Electron.js:如何为一个窗口创建一个单独的下载 webContents.session?

】Electron.js:如何为一个窗口创建一个单独的下载webContents.session?【英文标题】:Electron.js:howtocreateaseparatedownloadwebContents.sessionforawindow?【发布时间】:2021-08-2516:16:11【问题描述】:我有一个Electronfilemanagerapp,它为不同的目的创... 查看详情

如何为纯文本实现 Open In 应用程序

】如何为纯文本实现OpenIn应用程序【英文标题】:HowtoimplementOpenInappsforplaintext【发布时间】:2014-04-2602:49:50【问题描述】:我想为用户提供点击“操作”按钮并弹出常用共享表的功能,其中应包括消息、Facebook等图标右侧的其他... 查看详情

如何为 electron + react js + next 进行生产构建

】如何为electron+reactjs+next进行生产构建【英文标题】:Howtomakeproductionbuildforelectron+reactjs+next【发布时间】:2020-05-2902:12:44【问题描述】:我正在使用带有reactjs和下一个的电子。我找不到如何制作它的生产版本。任何人都可以帮... 查看详情

Android:如何为 Android ListView 实现这样的操作?

】Android:如何为AndroidListView实现这样的操作?【英文标题】:Android:HowtoImplementsuchoperationforAndroidListView?【发布时间】:2012-01-2719:51:51【问题描述】:我的应用程序具有以下功能:我已经完成了使用自定义适配器设置值。但现在... 查看详情

如何为winform C#应用程序实现密码恢复[关闭]

】如何为winformC#应用程序实现密码恢复[关闭]【英文标题】:howtoimplementpasswordrecoveryforawinformC#application[closed]【发布时间】:2013-02-1614:41:41【问题描述】:我有一个完全基于Windows的C#编写的Winform应用程序。它没有任何网络模块,... 查看详情

如何为 iOS 和 Android 开发应用内更新?

】如何为iOS和Android开发应用内更新?【英文标题】:Howtodevelopanin-appupdateforiOSandAndroid?【发布时间】:2017-01-0613:56:01【问题描述】:supercell的皇室战争如何在不进入应用商店的情况下在应用本身内更新其应用(功能、内容等)?... 查看详情

Flutter - 如何为消息应用程序实现通知?

】Flutter-如何为消息应用程序实现通知?【英文标题】:Flutter-Howtoimplementnotificationsforamessagingapp?【发布时间】:2019-12-3000:58:40【问题描述】:我正在Flutter上创建一个消息应用程序,它使用CloudFirestore来接收和存储消息。一切正常... 查看详情

Electron builder - 如何为生产 Windows 可执行文件构建 loadURL

】Electronbuilder-如何为生产Windows可执行文件构建loadURL【英文标题】:Electronbuilder-howtoloadURLforproductionwindowsexecutablebuild【发布时间】:2020-04-0217:23:56【问题描述】:我很高兴地创建了一个Angular应用程序,并通过loadURL将其加载到Elec... 查看详情

如何为 SQL Server 实现类似 TADSevent 的功能

】如何为SQLServer实现类似TADSevent的功能【英文标题】:HowtoimplementTADSeventlikefunctionalityforSQLServer【发布时间】:2020-05-2503:06:05【问题描述】:我正在使用Delphi10.3和Advantage数据库。在这里,我使用了sp_SignalEvent方法,然后使用了TADSE... 查看详情

如何为离子输入实现货币输入指令

】如何为离子输入实现货币输入指令【英文标题】:Howtoimplementacurrencyinputdirectiveforion-input【发布时间】:2017-09-1900:55:05【问题描述】:更新:最初虽然问题出在ControlValueAccessor的实现中,但随后确定问题在于将ControlValueAccessor应... 查看详情

如何为 ExtJS GridPanel 实现自定义行排序

】如何为ExtJSGridPanel实现自定义行排序【英文标题】:HowtoimplementcustomrowsortingforaExtJSGridPanel【发布时间】:2011-11-0813:00:45【问题描述】:我已经实现了一个Web-应用程序,它具有一个可以分组或取消分组的GridPanel,并且行应该按字... 查看详情

如何为 Windows 7 编写进度条以在任务栏上进行自我更新?

】如何为Windows7编写进度条以在任务栏上进行自我更新?【英文标题】:HowdoIcodeaprogressbarforWindows7toalsoupdateitselfonthetaskbar?【发布时间】:2010-11-1121:45:35【问题描述】:Windows7有一个很棒的新功能,应用程序可以通过状态栏报告当... 查看详情

如何为反应原生应用程序实现屏幕锁定[关闭]

】如何为反应原生应用程序实现屏幕锁定[关闭]【英文标题】:Howtoimplementscreenlocktoareactnativeapp[closed]【发布时间】:2021-02-0508:39:16【问题描述】:我希望为我的移动应用程序添加一项安全功能,每次用户打开应用程序时,它都会... 查看详情

使用rxjs实现一个简易的仿elm架构应用

使用RxJS实现一个简易的仿Elm架构应用标签(空格分隔):前端什么是Elm架构Elm架构是一种使用Elm语言编写Web前端应用的简单架构,在代码模块化、代码重用以及测试方面都有较好的优势。使用Elm架构,可以非常轻松的构建复杂... 查看详情

如何为 Parse 实现电子邮件适配器以允许密码重置功能

】如何为Parse实现电子邮件适配器以允许密码重置功能【英文标题】:HowtoimplementemailadapterforParsetoallowpasswordresetfeature【发布时间】:2018-06-1110:31:27【问题描述】:问题描述我正在使用AWSEC2来托管我的bitnami解析服务器,该服务器... 查看详情

electron-updater实现electron应用程序更新

使用electron开发桌面应用对于前端来说是比较新的领域。通常web端实现应用的更新比较简单,因为用户访问web端的网页都是通过浏览器访问,输入网址后,找到对应服务器的资源然后返回给用户,所以我们更新应用只需要替换服... 查看详情