sparkle-project / Sparkle

A software update framework for macOS
https://sparkle-project.org
Other
7.46k stars 1.05k forks source link

No popup when running the latest version? #1326

Closed quantonganh closed 2 years ago

quantonganh commented 5 years ago

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, ...

screenshot_2018-12-11_11_01_50

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

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?

kornelski commented 5 years ago

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.

quantonganh commented 5 years ago

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?

kornelski commented 5 years ago

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)

quantonganh commented 5 years ago

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))

quantonganh commented 5 years ago

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.

kornelski commented 5 years ago

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.

quantonganh commented 5 years ago

look for calls dispatch_async and isMainThread

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]) {
quantonganh commented 5 years ago

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?

kornelski commented 5 years ago

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.

quantonganh commented 5 years ago

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

quantonganh commented 5 years ago

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?

kornelski commented 5 years ago

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

quantonganh commented 5 years ago

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 a RunLoop 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?

kornelski commented 5 years ago

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(); });