Sparkle
Sparkle copied to clipboard
No popup when running the latest version?
I'm trying to use Sparkle with Qt (binding for Go) app.
sparkle.m:
#import <Headers/SUUpdater.h>
static SUUpdater* updater = nil;
void sparkle_checkUpdates()
{
if (!updater) {
updater = [[SUUpdater sharedUpdater] retain];
}
[updater setUpdateCheckInterval:3600];
[updater checkForUpdatesInBackground];
}
sparke.go:
// +build darwin windows
package main
/*
#cgo CFLAGS: -I ${SRCDIR}/Sparkle.framework
#cgo LDFLAGS: -F ${SRCDIR} -framework Sparkle
void sparkle_checkUpdates();
*/
import "C"
func sparkle_checkUpdates() {
C.sparkle_checkUpdates()
}
main.go:
func main() {
widgets.NewQApplication(len(os.Args), os.Args)
action := widgets.NewQMenuBar(nil).AddMenu2("").AddAction("Check for Updates...")
// http://doc.qt.io/qt-5/qaction.html#MenuRole-enum
action.SetMenuRole(widgets.QAction__ApplicationSpecificRole)
action.ConnectTriggered(func(bool) { sparkle_checkUpdates() })
widgets.QApplication_Exec()
}
It is working fine when there is an update: download, extract, install, relaunch, ...
But when running the latest version, click "Check for Updates..." menu and nothing happens. There is no popup said that we are up-to-date, something like this:
![screenshot 2018-12-11 11 01 34](https://user-images.githubusercontent.com/1568504/49777946-34755980-fd35-11e8-947c-07a2a186b2b2.png)
In Console, I only see this:
[3 <private> stream, pid: 90977, url: https://example.com/appcast.xml, traffic class: 200, tls] cancelled
[3.1 70A1F65B-7E7A-4ED2-AB8B-A21621ED7658 <private>.58040<-><private>]
Connected Path: satisfied (Path is satisfied), interface: en0, ipv4, dns
Duration: 0.497s, DNS @0.000s took 0.001s, TCP @0.003s took 0.051s, TLS took 0.113s
bytes in/out: 4481/675, packets in/out: 6/3, rtt: 0.053s, retransmitted packets: 0, out-of-order packets: 0
appcast.xml:
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>x</title>
<item>
<title>1.0.0.2905</title>
<pubDate>Tue, 11 Dec 2018 11:09:10 +0800</pubDate>
<sparkle:minimumSystemVersion>10.7</sparkle:minimumSystemVersion>
<enclosure url="https://example.com/x.zip" sparkle:version="1.0.0.2905" sparkle:shortVersionString="1.0.0.2905" sparkle:edSignature="x" length="104408678" type="application/octet-stream"/>
</item>
</channel>
</rss>
Did I miss something?
Is your application running a Cocoa RunLoop
? It's usually set up by NSApplicationMain
in C apps, but in Go you'd be responsible for running it or having the event loop set up yourself.
Without the runloop all our async code may be subtly broken and events get queued to /dev/null
.
Our main.go
is something like this:
package main
import (
"github.com/therecipe/qt/widgets"
)
func main() {
widgets.NewQApplication(len(os.Args), os.Args)
action := widgets.NewQMenuBar(nil).AddMenu2("").AddAction("Check for Updates...")
// http://doc.qt.io/qt-5/qaction.html#MenuRole-enum
action.SetMenuRole(widgets.QAction__ApplicationSpecificRole)
action.ConnectTriggered(func(bool) { sparkle_checkUpdates() })
widgets.QApplication_Exec()
}
AFAIK, widgets.QApplication_Exec()
make the app enter the main event loop.
Without the runloop all our async code may be subtly broken and events get queued to
/dev/null
.
Why "Check for Updates..." always works if there is a new version?
Why "Check for Updates..." always works if there is a new version?
This is a bit puzzling. Do you get alerts when there's an error? (e.g. try making invalid XML in the appcast)
Do you get alerts when there's an error?
Yes:
Error: An error occurred in retrieving update information. Please try again later. An error occurred while parsing the update feed. (URL (null)) Error: An error occurred while parsing the update feed. (null) (URL (null)) Error: Line 13: expected '>' (null) (URL (null))
The logs is pretty the same when there is a new version:
Task <9E0431E4-0E4B-4E71-9BAD-8ABCF7524E76>.<1> now using Connection 41
Task <9E0431E4-0E4B-4E71-9BAD-8ABCF7524E76>.<1> sent request, body N
Task <9E0431E4-0E4B-4E71-9BAD-8ABCF7524E76>.<1> received response, status 200 content K
Task <9E0431E4-0E4B-4E71-9BAD-8ABCF7524E76>.<1> response ended
TIC TCP Conn Cancel [41:0x6000001744c0]
[41 <private> stream, pid: 98435, url: https://example.com/appcast.xml, traffic class: 200, tls] cancelled
[41.1 47D2946B-D605-4648-8489-900272B37BBD <private>.50696<-><private>]
Connected Path: satisfied (Path is satisfied), interface: en0, ipv4, dns
Duration: 0.295s, DNS @0.000s took 0.002s, TCP @0.003s took 0.062s, TLS took 0.115s
bytes in/out: 4637/675, packets in/out: 6/3, rtt: 0.062s, retransmitted packets: 0, out-of-order packets: 0
except that there is no pop up.
Compile your copy of Sparkle.
In Sparkle's code, look for calls dispatch_async
and isMainThread
. I suspect there's something fishy there and async calls sent to the main thread get lost. Add NSLog around them and see how far the code reaches. Alternatively, if you're more of a debugger person, make a debug build of Sparkle and step through that code.
look for calls
dispatch_async
andisMainThread
I've taken a look at all below files. Looks like these are used for doing something after the popup dialog appear: download, extract, delta update, ...
Where is the code that show the popup dialog?
Sparkle/SPUDownloaderSession.m
43: dispatch_async(dispatch_get_main_queue(), ^{
60: dispatch_async(dispatch_get_main_queue(), ^{
Sparkle/Autoupdate/Autoupdate.m
143: dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
147: dispatch_async(dispatch_get_main_queue(), ^{
155: dispatch_async(dispatch_get_main_queue(), ^(){
163: dispatch_async(dispatch_get_main_queue(), ^{
175: dispatch_async(dispatch_get_main_queue(), ^{
Sparkle/SUPipedUnarchiver.m
85: dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Sparkle/SPUDownloaderDeprecated.m
32: dispatch_async(dispatch_get_main_queue(), ^{
49: dispatch_async(dispatch_get_main_queue(), ^{
Sparkle/SUDiskImageUnarchiver.m
50: dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Sparkle/SUBinaryDeltaUnarchiver.m
88: dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Sparkle/SUUIBasedUpdateDriver.m
174: dispatch_async(dispatch_get_main_queue(), ^{
212: dispatch_async(dispatch_get_main_queue(), ^{
260: dispatch_async(dispatch_get_main_queue(), ^{
Sparkle/SUUnarchiverNotifier.m
40: dispatch_async(dispatch_get_main_queue(), ^{
54: dispatch_async(dispatch_get_main_queue(), ^{
62: dispatch_async(dispatch_get_main_queue(), ^{
Sparkle/SUUpdater.m
461: if (![NSThread isMainThread])
469: if (![NSThread isMainThread])
Sparkle/SUUserInitiatedUpdateDriver.m
30: if (![NSThread isMainThread]) {
Sparkle/SUUIBasedUpdateDriver.m
323: if ([NSThread isMainThread]) {
I asked my co-worker to test on 10.14 and I saw something in the logs:
default 12:23:06.968371 +0800 lsd Non-fatal error enumerating at
, continuing: Error Domain=NSCocoaErrorDomain Code=260 "The file “PlugIns” couldn’t be opened because there is no such file." UserInfo={NSURL=PlugIns/ -- file:///Applications/x.app/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/, NSFilePath=/Applications/x.app/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PlugIns, NSUnderlyingError=0x7fd2797e4490 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Why PlugIns
is missing?
ls -l Contents/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/
total 16
-rw-r--r-- 1 root wheel 1524 Dec 13 08:30 Info.plist
drwxr-xr-x 4 root wheel 128 Dec 13 08:30 MacOS
-rw-r--r-- 1 root wheel 8 Dec 13 08:30 PkgInfo
drwxr-xr-x 36 root wheel 1152 Dec 13 08:30 Resources
non-fatal error but does it relate to this problem?
We don't use plugins, so it's expected that this doesn't exist. I'm not sure why macOS is even trying to access it.
Where is the code that show the popup dialog?
OK. I found this: https://github.com/sparkle-project/Sparkle/blob/master/Sparkle/SUUIBasedUpdateDriver.m#L104
I added some logs:
diff --git a/Sparkle/SUBasicUpdateDriver.m b/Sparkle/SUBasicUpdateDriver.m
index 2349f636..a5f6fe4f 100644
--- a/Sparkle/SUBasicUpdateDriver.m
+++ b/Sparkle/SUBasicUpdateDriver.m
@@ -71,8 +71,10 @@
[appcast setHttpHeaders:[updater httpHeaders]];
[appcast fetchAppcastFromURL:URL inBackground:self.downloadsAppcastInBackground completionBlock:^(NSError *error) {
if (error) {
+ NSLog(@"checkForUpdatesAtURL: abortUpdateWithError");
[self abortUpdateWithError:error];
} else {
+ NSLog(@"checkForUpdatesAtURL: appcastDidFinishLoading");
[self appcastDidFinishLoading:appcast];
}
}];
@@ -196,8 +198,10 @@
if ([self itemContainsValidUpdate:item]) {
self.updateItem = item;
+ NSLog(@"appcastDidFinishLoading: didFindValidUpdate");
[self performSelectorOnMainThread:@selector(didFindValidUpdate) withObject:nil waitUntilDone:NO];
} else {
+ NSLog(@"appcastDidFinishLoading: didNotFindUpdate");
self.updateItem = nil;
[self performSelectorOnMainThread:@selector(didNotFindUpdate) withObject:nil waitUntilDone:NO];
}
run make release
, copy Sparkle.framework
, and rebuild my app. Then I tested for both cases: run an old/latest version -> click "Check for Updates...", but I didn't see any added log in the Console. Why?
Were you using an older version of Sparkle before? We've switched our network back-end couple of versions ago. The new backend, like the alerts, doesn't work without a RunLoop
. NSURLSession
depends on having a RunLoop
to schedule the callbacks, so if check for appcast never completes, that may be why.
I don't know how Qt is integrated, but I wouldn't be surprised if it had its own event loop, instead of running "competitor's" event loop.
It's also important for Cocoa to have "main thread" which is the same as the thread of main()
. Your Qt/Go program has to live in a background thread, and leave the original thread of execution to be blocked by Cocoa.
Try moving your program to a thread, and block main()
with NSRunLoop
.
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
Were you using an older version of Sparkle before?
No, I just started using version 1.21.0 some days ago.
The new backend, like the alerts, doesn't work without a
RunLoop
.NSURLSession
depends on having aRunLoop
to schedule the callbacks, so if check for appcast never completes, that may be why.
Maybe that's the reason, as I saw in the source code, NSWindow
is used if Sparkle find a valid update: https://github.com/sparkle-project/Sparkle/blob/d430c33bf49e1ab7fcb4430b01f3ca2122af9005/Sparkle/SUUIBasedUpdateDriver.m#L91
but I wouldn't be surprised if it had its own event loop, instead of running "competitor's" event loop.
I think so: https://wiki.qt.io/Threads_Events_QObjects#Events_and_the_event_loop
It's also important for Cocoa to have "main thread" which is the same as the thread of
main()
. Your Qt/Go program has to live in a background thread, and leave the original thread of execution to be blocked by Cocoa.
I found something:
You also will need to to instantiate your custom NSApplication before creating a QApplication.
https://developer.apple.com/documentation/appkit/nsapplication
void NSApplicationMain(int argc, char *argv[]) {
[NSApplication sharedApplication];
[NSBundle loadNibNamed:@"myMain" owner:NSApp];
[NSApp run];
}
but the thing is I don't know how can I call Go's main function from within NSApplicationMain
event loop?
Yes, if you could run NSApplicationMain
that would be great.
By default it also wants to launch Cocoa GUI. I never tried not having one, but maybe if you don't have the main nib file, it won't be a problem? or you could try [NSApp run]
or just [NSRunLoop run]
.
Because Cocoa wants to have the main thread for itself, you need to spawn rest of your app on another thread. That may do it:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ run_go_and_qt(); });