Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No popup when running the latest version? #1326

Closed
quantonganh opened this issue Dec 11, 2018 · 14 comments
Closed

No popup when running the latest version? #1326

quantonganh opened this issue Dec 11, 2018 · 14 comments

Comments

@quantonganh
Copy link

quantonganh commented Dec 11, 2018

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
Copy link
Member

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
Copy link
Author

quantonganh commented Dec 12, 2018

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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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
Copy link
Author

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
Copy link
Member

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
Copy link
Author

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:

NSWindow *window = [self.updateAlert window];

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
Copy link
Member

kornelski commented Dec 14, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants