Sparkle: Fully silent update in C# (Xamarin Forms on Mac)

Created on 6 Apr 2018  路  27Comments  路  Source: sparkle-project/Sparkle

I've been trying to get silent updates working on my Xamarin Forms app. I'm using Sparkle-Sharp for my C# bindings to Sparkle, and using the information in issues 1047 and 787 to get me to where I am now.

The CheckForUpdatesInBackground doesn't seem to be working for me, so I've been keeping my own timer and occasionally calling InstallUpdatesIfAvailable. This has worked well to download and install updates without any user intervention (my requirement). The progress bar on the download is not desirable, but can be overlooked. However, every time my loop invokes InstallUpdatesIfAvailable, there's a popup showing the progress of the update check (even if there's no new updates). This isn't a good user experience, and I'm trying to figure out how to not show any popups (completely silent and automatic, without user intervention.)

My setup looks like this in C#:
NSUserDefaults.StandardUserDefaults.SetBool(true, "SUAutomaticallyUpdate");
NSUserDefaults.StandardUserDefaults.Synchronize();
SUUpdater.SharedUpdater.AutomaticallyDownloadsUpdates = true;
SUUpdater.SharedUpdater.AutomaticallyChecksForUpdates = true;

And this in my info.plist:
SUAutomaticallyUpdate Boolean YES
SUEnableAutomaticChecks Boolean YES
SUScheduleCheckInterval Number 3600
SUPublicDSAKeyFile String xxx.pem
SUFeedURL String https://xxx/macosclient.xml
SUShowReleaseNotes Boolean NO

Then I have a background worker that makes this call every hour:
Device.BeginInvokeOnMainThread(SUUpdater.SharedUpdater.InstallUpdatesIfAvailable);

I've tried a few combinations of settings, but this gets me as close to the behavior as I'm looking for. Any ideas on how to hide the popup that occurs while Sparkle is checking for updates? Bonus points for also hiding the download popup to make this truly silent.

Most helpful comment

For posterity, I'll mention that in my solution mentioned above, my C# delegate was getting garbage collected, causing null exceptions in release mode. I needed to hang on to a reference of my SparkleDelegate in C# land so that it doesn't disappear.

private SparkleDelegate _updaterDelegates = new SparkleDelegate();

SUUpdater.SharedUpdater.Delegate = _updaterDelegates;

All 27 comments

Sounds like the same problem as Qt. We depend on Cocoa to listen for app termination to perform install. If you quit the app any other way we don't install anything.

Thanks for the quick response! I tried a few things out just now, and it looks like if I use CheckForUpdatesInBackground instead, the update is downloaded silently. When I restart, it's the new version, and I've seen no popups. So that's good, except I need to be able to force the update immediately after it downloads (restarting the app) instead of waiting for the user to close the app. Is there a way to do this?

There's a delegate method that gives you a callback to force restart.

@kornelski, a delegate that let's you know when an update has downloaded and is ready for the app to restart? Which one is that? willInstallUpdate?

Yes, SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationInvocation and invoke the callback it gives.

It sounds like this will initiate the restart. However, how do I know when to call it? How do I know when the update is ready for the restart?

It restarts, because we can't install anything while the app is running, so installation can begin only after the app quits.

I understand that a restart is required. Is there a boolean that I can check to see if Sparkle is waiting for a restart?

The mere fact that you've got that callback is the information saying Sparkle is waiting for restart.

Oh, okay. I misunderstood. Is there a method then that I can invoke to cause my application to restart?

Is invocation of the callback not causing a restart?

Use this delegate callback

public override void UpdaterWillInstallUpdateOnQuit(SUUpdater updater, SUAppcastItem item, NSInvocation invocation)
 {
     invocation.Invoke();
 }

Awesome, thanks @gaocan1992. I hadn't had a chance to get back to playing with my updater code yet, but this seems to answer my question clearly. Thanks!

If you are curious, this is the source code I found.

if ([updaterDelegate respondsToSelector:@selector(updater:willInstallUpdateOnQuit:immediateInstallationInvocation:)])
    {
        BOOL relaunch = YES;
        BOOL showUI = NO;
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:@selector(installWithToolAndRelaunch:displayingUserInterface:)]];
        [invocation setSelector:@selector(installWithToolAndRelaunch:displayingUserInterface:)];
        [invocation setArgument:&relaunch atIndex:2];
        [invocation setArgument:&showUI atIndex:3];
        [invocation setTarget:self];

        [updaterDelegate updater:self.updater willInstallUpdateOnQuit:self.updateItem immediateInstallationInvocation:invocation];
    }

@ech0matrix Can I ask you if you are running into this problem https://github.com/sparkle-project/Sparkle/issues/1203

@gaocan1992 I don't think so. I'm verifying automaticallyDownloadsUpdates is true before running the background check.

@ech0matrix May I ask how do you verify this value and what if this value is not true.

- (BOOL)automaticallyDownloadsUpdates
{
    // If we aren't allowed automatic updates, don't ever let them happen
    if (![SUSystemUpdateInfo systemAllowsAutomaticUpdatesForHost:self.host]) {
        return NO;
    }

    // Otherwise, automatically downloading updates is allowed. Does the user want it?
    return [self.host boolForUserDefaultsKey:SUAutomaticallyUpdateKey];
}

@gaocan1992
My use case is that we want the application to check for updates, download, install, and restart, with 0 user interaction. We're using C# (Xamarin Forms on Mac). I'm using Sparkle-Sharp for C# bindings. The updater object doesn't seem to do anything unless I invoke it manually (since I didn't make an updater object in Objective C).

Right now, I have a background worker that periodically invokes the updater to check for updates. It gets initialized partly in C#, and partly in the info.plist. You can see my setup and code in my original post at the top of this thread. I'm forcing "AutomaticallyDownloadsUpdates = true;" before my worker calls InstallUpdatesIfAvailable.

Unfortunately, InstallUpdatesIfAvailable still generates popups for progress bars on the check and install, so the whole point of this thread was to try to find a quiet way. It sounds like my solution will be to use CheckForUpdatesInBackground, and setup a delegate to trigger the restart. I'm hoping this will solve my problem, but I won't be able to try it out until next week.

@ech0matrix Seems like I have the same setup. @kornelski Suggest me don't force AutomaticallyDownloadsUpdates = true instead setting it in Info.plist.

And I have set this values in my Info.plist already

        <key>SUEnableAutomaticChecks</key>
    <true/>
    <key>SUAllowsAutomaticUpdates</key>
    <true/>
    <key>SUAutomaticallyUpdate</key>
    <true/>

I use CheckForUpdatesInBackground and use the delegate callback to trigger the restart.
However, I still see the popup sometimes which is weird.

By reading the source code I noticed that before Sparkle chooses SUAutomaticUpdateDriver which is the UI less update driver, it will check "automaticallyDownloadsUpdates" which is read from SUHost (return [self.host boolForUserDefaultsKey:SUAutomaticallyUpdateKey];) and SUHost will read the settings from Info.plist.

So I think even if you force AutomaticallyDownloadsUpdates = true it will try to modify the Info.plist
for the value of SUAutomaticallyUpdateKey but when CheckForUpdatesInBackground tries to decide the value of this AutomaticallyDownloadsUpdates it still have to read from the value of SUAutomaticallyUpdateKey.

Could you mention me if you also see UI even you use CheckForUpdatesInBackground.
I doubt there is a threading issue in Sparkle.

@gaocan1992 I do have a call in my C# to set SUAutomaticallyUpdate as well, although I was just trying everything I could to get it to work:
NSUserDefaults.StandardUserDefaults.SetBool(true, "SUAutomaticallyUpdate"); NSUserDefaults.StandardUserDefaults.Synchronize();

As I mentioned, I'll be able to play around more with the CheckForUpdatesInBackground next week. But so far, I have not seen the UI when using that function. In fact, I did not even know it was doing anything at all, but I manually restarted the app and noticed the version number had bumped.

@ech0matrix This should be the right behavior. I may look into this to make sure that the bool value is true before actually call CheckForUpdatesInBackground, thanks
NSUserDefaults.StandardUserDefaults.SetBool(true, "SUAutomaticallyUpdate"); NSUserDefaults.StandardUserDefaults.Synchronize();

@gaocan1992, I finally got back around to making this change, and it works beautifully. I even dynamically set my SUFeedURL at runtime.

My delegate:

        private class SparkleDelegate : SUUpdaterDelegate
        {
            // Callback for when update is ready to install
            public override void UpdaterWillInstallUpdateOnQuit(SUUpdater updater, SUAppcastItem item, NSInvocation invocation)
            {
                // Force restart of application in order to install update
                invocation.Invoke();
            }
       }

My initialization:

            NSUserDefaults.StandardUserDefaults.SetString(UpdaterWebServiceURL, "SUFeedURL");
            NSUserDefaults.StandardUserDefaults.Synchronize();
            SUUpdater.SharedUpdater.Delegate = new SparkleDelegate();

In a loop, I call once an hour:
SUUpdater.SharedUpdater.CheckForUpdatesInBackground();

With all the other settings I mentioned previously, it all works as expected: automatically and silently.

@ech0matrix Everything looks good, but why do you need put CheckForUpdatesInBackground in a loop?
I believe you can set CheckInterval to 1 hour.

@gaocan1992 I do have SUScheduledCheckInterval set to 1 hour. I tried letting Sparkle check for updates on its own, but after waiting for a while, I didn't see anything happening. I even tried setting it to something low, like 30 seconds. So I'm manually invoking the check in order to poll for updates. Maybe I'm missing something in my setup steps, but that's how I've gotten it to work.

@ech0matrix I assume that you are missing sth. Looks like it works for me.

For posterity, I'll mention that in my solution mentioned above, my C# delegate was getting garbage collected, causing null exceptions in release mode. I needed to hang on to a reference of my SparkleDelegate in C# land so that it doesn't disappear.

private SparkleDelegate _updaterDelegates = new SparkleDelegate();

SUUpdater.SharedUpdater.Delegate = _updaterDelegates;

Was this page helpful?
0 / 5 - 0 ratings

Related issues

tghs picture tghs  路  7Comments

mikepulaski picture mikepulaski  路  8Comments

kornelski picture kornelski  路  9Comments

ianbytchek picture ianbytchek  路  8Comments

bdenckla-ua picture bdenckla-ua  路  5Comments