Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add support for sparkle:requiredVersion to signify mandatory program updates. #138

Closed
wants to merge 1 commit into from

6 participants

@toddfoster

Our cloud-centric application occasionally requires server-side changes which break older clients. In that case, the application should not be allowed to run without updating.

Check the appcastItem's enclosure for a field labeled
"sparkle:requiredVersion". If found, compare that version to the host's
currently-running version. If the host is not running at least the
required version, do not allow it to launch without an update. This
means hiding the "Skip" and "Later" buttons in the Update alert, and
terminating the program if the user closes the update alert dialog (via
the window control) without launching the update.

Todd Foster Add support for required version.
Check the appcastItem's enclosure for a field labeled
"sparkle:requiredVersion". If found, compare that version to the host's
currently-running version. If the host is not running at least the
required version, do not allow it to launch without an update.  This
means hiding the "Skip" and "Later" buttons in the Update alert, and
terminating the program if the user closes the update alert dialog (via
the window control) without launching the update.
23953ff
@andymatuschak

Todd and I have been talking about the implications of this pull request off-line some, but I'm coming around to the idea. I think we need to be careful how we document the usage of this feature so that it's used in cases of, say, client-server compatibility rather than the directives of marketing.

I have a few suggestions, if you are still interested in working on this patch:

  • This could be integrated nicely with the changes in bf38b0c, codified as one of the <sparkle:tags>. Perhaps <sparkle:requiredUpdate />?
  • I think SUAppcastItem needs an isRequiredUpdate so that SUUpdater's delegate can respond to the discovery of a required update by disabling user interaction in its primary interface.
  • The property of being required needs to be more "contagious": if the user is running 1.4, and 1.5 is marked as "required" because the server API bumps, and then you release 1.5.1, then Sparkle should treat 1.5.1 as a required update too, even though it isn't explicitly marked as such in the feed.
@toddfoster

Those are good suggestions. I'll try to work them in soon.

@mattstevens

The property of being required needs to be more "contagious": if the user is running 1.4, and 1.5 is marked as "required" because the server API bumps, and then you release 1.5.1, then Sparkle should treat 1.5.1 as a required update too, even though it isn't explicitly marked as such in the feed.

I think cascading tags like this need to have the ability to specify a version number in addition to acting as a flag. This would allow developers to continue to use single-update feeds if desired. Otherwise using a tag would mean including the current update and the last one to use each tag. Internally you'd likely always treat the tag as having a version number and in flag mode implicitly set it to the current item's version.

@mattstevens

BTW, if the goal is to force an update I wonder if this could be accomplished through the delegate being discussed in #153 instead of baking the behavior into Sparkle. If the app detects a server version mismatch it could initiate an update check and once the update is downloaded display an application-specific message informing the user that an update needs to be installed due to a change in the service. This strikes me as more user-friendly than displaying a dialog and removing every option except install, then killing the app if the user doesn't click on it.

@andymatuschak

I think cascading tags like this need to have the ability to specify a version number in addition to acting as a flag. This would allow developers to continue to use single-update feeds if desired.

I'm not totally sure what you mean here, Matt. I was thinking that the logic would be: an update is required if any of the present valid updates (i.e. version greater than current installed version, satisfies minimum system requirements, etc) are required. What is the use case you're describing?

If the app detects a server version mismatch it could initiate an update check and once the update is downloaded display an application-specific message informing the user that an update needs to be installed due to a change in the service. This strikes me as more user-friendly than displaying a dialog and removing every option except install, then killing the app if the user doesn't click on it.

This is a good point indeed: this kind of strict and extremely unpleasant behavior requires more specific messaging than this patch's solution provides. The user sees a dialog with a design he's used to from other apps—but now its meaning is different. It says "an update is available," but it means "an update is required," and the user must be told why.

@KenThomases

I'm not sure this is Sparkle's job. The protocol with the server needs a way to check compatibility and the app needs to insist on an update if/when the server tells it it's incompatible.

Remember, Sparkle won't necessarily check on every launch. Or, if the app makes Sparkle check on launch, remember that the user may keep the app running. So, the server and the app may go out of sync spontaneously. This means the app has to have a means of detecting this, even if Sparkle were to implement this feature, which means Sparkle implementing this would be redundant.

@toddfoster

Re: single-update feeds, I do this as well. My build script constructs a new appcast.xml each time, containing only a single version: the current one. So I would indeed prefer to specify the minimum version in the new tag.

Re: Sparkle's job, this is a valid argument. I am proposing to make it Sparkle's job, chiefly because I use Sparkle in a cross-platform application and its counterpart (Microsoft's ClickOnce) does precisely this. It seems a reasonable task to assign to the updater. One of the great strengths of Sparkle is how it contains all the update logic in itself.

We do have Sparkle run a check on every launch. And an app which is running when an updated server is deployed may in fact become crippled. Users may choose to run in a crippled state to finish their current session, but (in our case) there is no sense in allowing them to continue to launch the older version on subsequent runs.

Perhaps the text displayed in the default dialog should be modified when isRequiredUpdate?

@mattstevens

I'm not totally sure what you mean here, Matt. I was thinking that the logic would be: an update is required if any of the present valid updates (i.e. version greater than current installed version, satisfies minimum system requirements, etc) are required. What is the use case you're describing?

We're in agreement on the behavior of the tag, I was referring to how it is included in the appcast. If I understand correctly the idea is to do this:

<item>
  <title>AwesomeApp 1.5.1</title>
  ...
</item>
<item>
  <title>AwesomeApp 1.5</title>
  <sparkle:tags>
    <sparkle:requiredVersion />
  </sparkle:tags>
  ...
</item>

The requiredVersion tag cascades so if I'm on 1.4 then 1.5.1 is presented as required. But if I'm on 1.5 then 1.5.1 is not required and this is where the wrinkle comes in. Some developers only include the most recent version in their appcast, either to keep the file small or because it is easy to generate as Todd is doing. Here are some examples:

http://www.haystacksoftware.com/arq/arq2.xml
http://hibariapp.com/appcast.xml
http://notational.net/nvupdates.xml

Because requiredVersion is a flag these single-update feeds are no longer possible for anyone wishing to use a tag. They must be expanded to include at least the current version and the last version for which the tag was specified. If requiredVersion could optionally specify a version number then both styles of appcast are supported.

<item>
  <title>AwesomeApp 1.5.1</title>
  <sparkle:tags>
    <sparkle:requiredVersion>1.5</sparkle:requiredVersion>
  </sparkle:tags>
  ...
</item>

This probably breaks the idea of requiredVersion being just a tag, but I do see the value in single-update feeds. It's a frequently requested resource and if the current version is the only one that matters then there's not much value in transmitting data for other versions

@andymatuschak

Regarding single-update feeds: perhaps, then, we need a feed-level element which states the minimum required version. I don't think it would make sense to make the required-version-with-a-version-number tag a child of an <item>.

The thing that continues to trouble me about this patch is: we can talk about this potentially being Sparkle's job, but there's still so much that a client must do himself in order to correctly opt into this behavior. He must:

  • ensure that Sparkle checks for updates on each app launch
  • ensure that either:
    • he doesn't show any UI until Sparkle finishes checking (which sucks for the user!)
    • or he can handle showing the UI with a potentially-out-of-date server version while Sparkle checks and deal with disabling all his UI if Sparkle finds a required update. This seems very hard to code in the face of.
  • provides some sane error-handling messaging and behavior

Can we mitigate any/all of these requirements? I'm not sure Sparkle itself is providing enough utility for this use case.

A custom update driver with custom UI might be the better experience here: why give the user an option? Simply inform him that the application must update to continue. Heck, why show any UI at all? Maybe it should just do the whole thing silently, if the rest of the app's UI is going to be unusable or not showing anyway.

@toddfoster: What's the user experience for this like on Windows? Does the app just not show any UI until the check is complete?

All this discussion aside it occurs to me that this patch also doesn't account for the case when the user agrees to download and install the update, but encounters an error along the way.

@toddfoster

Yes, this is definitely a specialized use-case for Sparkle. We don't show any UI until Sparkle finishes checking (or gives up). With a reasonable connection, this takes hardly any time at all. Without a reasonable connection... a cloud-centric application (for whom staying current is critical) is going to be hurting all over.

ClickOnce (as we use it, at least) operates in this way. Our application does not start (no UI is displayed) until the check has been made and we're either up-to-date or else the user downloaded or skipped an optional update. If ClickOnce finds a required update, the user is not even asked: the download begins immediately. The download doesn't happen silently, though: the same dialog (with progress bar) is displayed either way. For an optional update one can choose "Skip" or "Cancel." For a required update the "Skip" but is removed and the caption is modified to include the word "Required."

Having the auto-updater run synchronously actually makes coding the rest of the client much simpler: we don't have to include any version synchronization code elsewhere beyond making sure that a long-running application doesn't fall behind when the services update.

I realize there may not be a widespread desire for Sparkle to conform to the ways of ClickOnce. But it's not a bad workflow. Nice in its simplicity.

@dovy dovy referenced this pull request in toddfoster/toddfoster.github.com
Closed

Query #1

@jakepetroules

@pornel @MaddTheSane Shall we merge this?

@pornel
Owner

I'm a bit uneasy about having UI without skip button. That's just taunting users - showing dialog that's for letting user make a choice and not giving them a choice.

Perhaps we should have a per-feed-item option for silent automatic update instead? If app won't work without an update, then just update it.

I also remember somebody ask for silent updates for minor releases (and full UI for major ones).

@jakepetroules

[...] showing dialog that's for letting user make a choice and not giving them a choice.

Yes, good point. Weren't you working on silent automatic updates in your fork some time ago?

@pornel
Owner

I think silent updates are a better user experience and achieve the same goal.

@pornel pornel closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 6, 2012
  1. Add support for required version.

    Todd Foster authored
    Check the appcastItem's enclosure for a field labeled
    "sparkle:requiredVersion". If found, compare that version to the host's
    currently-running version. If the host is not running at least the
    required version, do not allow it to launch without an update.  This
    means hiding the "Skip" and "Later" buttons in the Update alert, and
    terminating the program if the user closes the update alert dialog (via
    the window control) without launching the update.
This page is out of date. Refresh to see the latest.
View
1  SUBasicUpdateDriver.h
@@ -33,6 +33,7 @@
- (BOOL)hostSupportsItem:(SUAppcastItem *)ui;
- (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui;
- (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui;
+- (BOOL)itemContainsRequiredUpdate:(SUAppcastItem *)ui;
- (void)didFindValidUpdate;
- (void)didNotFindUpdate;
View
6 SUBasicUpdateDriver.m
@@ -81,6 +81,12 @@ - (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui
return [self hostSupportsItem:ui] && [self isItemNewer:ui] && ![self itemContainsSkippedVersion:ui];
}
+- (BOOL)itemContainsRequiredUpdate:(SUAppcastItem *)ui
+{
+ NSString *minimumHostVersion = [[[ui propertiesDictionary] objectForKey:@"enclosure"] objectForKey:@"sparkle:requiredVersion"];
+ return [[self versionComparator] compareVersion:[host version] toVersion:minimumHostVersion] == NSOrderedAscending;
+}
+
- (void)appcastDidFinishLoading:(SUAppcast *)ac
{
if ([[updater delegate] respondsToSelector:@selector(updater:didFinishLoadingAppcast:)])
View
2  SUUIBasedUpdateDriver.m
@@ -18,7 +18,7 @@ @implementation SUUIBasedUpdateDriver
- (void)didFindValidUpdate
{
- updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem host:host];
+ updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem isRequired:[self itemContainsRequiredUpdate:updateItem] host:host];
[updateAlert setDelegate:self];
id<SUVersionDisplay> versDisp = nil;
View
3  SUUpdateAlert.h
@@ -35,9 +35,10 @@ typedef enum
IBOutlet NSButton *laterButton;
NSProgressIndicator *releaseNotesSpinner;
BOOL webViewFinishedLoading;
+ BOOL updateRequired;
}
-- (id)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)host;
+- (id)initWithAppcastItem:(SUAppcastItem *)item isRequired:(BOOL)updateRequired host:(SUHost *)aHost;
- (void)setDelegate:delegate;
- (IBAction)installUpdate:sender;
View
11 SUUpdateAlert.m
@@ -27,13 +27,14 @@ -(void) setDrawsBackground: (BOOL)state;
@implementation SUUpdateAlert
-- (id)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)aHost
+- (id)initWithAppcastItem:(SUAppcastItem *)item isRequired:(BOOL)required host:(SUHost *)aHost
{
self = [super initWithHost:host windowNibName:@"SUUpdateAlert"];
if (self)
{
host = [aHost retain];
updateItem = [item retain];
+ updateRequired = required;
[self setShouldCascadeWindows:NO];
// Alex: This dummy line makes sure that the binary is linked against WebKit.
@@ -153,6 +154,10 @@ - (BOOL)allowsAutomaticUpdates
- (void)awakeFromNib
{
+ // Hide Skip/Later buttons if a minimum version is specified and the host version is not at least that version
+ [skipButton setHidden:updateRequired];
+ [laterButton setHidden:updateRequired];
+
NSString* sizeStr = [host objectForInfoDictionaryKey:SUFixedHTMLDisplaySizeKey];
if( [host isBackgroundApplication] )
@@ -264,6 +269,10 @@ -(BOOL)showsReleaseNotesText
- (BOOL)windowShouldClose:note
{
+ // Quit on close window if update is required
+ if (updateRequired)
+ [[NSApplication sharedApplication] terminate:self];
+
[self endWithSelection:SURemindMeLaterChoice];
return YES;
}
Something went wrong with that request. Please try again.