Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Touched practically every line of code in a super-monster-awesome ref…

…actoring. Please read:

- Broke out SUUpdater functionality into update drivers. There's a basic one from which everything else inherits, then a user-initiated one, an automatic one, and a scheduled one. It's super-clean-and-shiny.
- Destroyed the abomination that was SUStatusChecker. In its place is SUProbingUpdateDriver, which is like 10 lines long.
- Made automatic installation less stupid. It used to install, THEN offer to relaunch. That's dumb, beacuse if the user says no, the app is running from the trash. Now it offers to install and relaunch or to install on quit.
- Renamed like every method and symbol. I hope you didn't branch anything.
- Reorganized the project hierarchy to be much clearer and easier to navigate.
- Reworked the error system to no use NSError instead of exceptions; extra technical information is now logged to the console so that we can find problems.
- A bunch of other small bugfixes in things I noticed along the way but no longer remember.
- Probably a ton of other stuff.
Read over the code and see what I've done. Then PLEASE test this with your app internally and let me know how it goes. This revision is hereby NOT YET DECLARED SAFE FOR PUBLIC RELEASE. But because I'm still using SVN, this is how things have to be.
  • Loading branch information...
commit bc3be9a1f24deb5436684f2f59d7f0379e3bc915 1 parent 0e99700
andym authored
Showing with 1,317 additions and 904 deletions.
  1. +5 −0 NSFileManager+Aliases.h
  2. +16 −19 NSFileManager+Authentication.m
  3. +5 −0 NSFileManager+ExtendedAttributes.h
  4. +18 −0 NSURL+Parameters.h
  5. +27 −0 NSURL+Parameters.m
  6. +18 −0 NSWorkspace+SystemVersion.h
  7. +36 −0 NSWorkspace+SystemVersion.m
  8. +1 −3 RSS.h
  9. +14 −24 RSS.m
  10. +4 −3 SUAppcast.h
  11. +46 −55 SUAppcast.m
  12. +5 −27 SUAppcastItem.h
  13. +64 −85 SUAppcastItem.m
  14. +15 −3 SUAutomaticUpdateAlert.h
  15. +16 −8 SUAutomaticUpdateAlert.m
  16. +23 −0 SUAutomaticUpdateDriver.h
  17. +76 −0 SUAutomaticUpdateDriver.m
  18. +56 −0 SUBasicUpdateDriver.h
  19. +208 −0 SUBasicUpdateDriver.m
  20. +17 −0 SUConstants.h
  21. +17 −4 SUConstants.m
  22. +7 −6 SUInstaller.h
  23. +2 −3 SUInstaller.m
  24. +5 −0 SUPackageInstaller.h
  25. +6 −1 SUPlainInstaller.h
  26. +14 −2 SUPlainInstaller.m
  27. +24 −0 SUProbingUpdateDriver.h
  28. +28 −0 SUProbingUpdateDriver.m
  29. +21 −0 SUScheduledUpdateDriver.h
  30. +39 −0 SUScheduledUpdateDriver.m
  31. +0 −30 SUStatusChecker.h
  32. +0 −78 SUStatusChecker.m
  33. +1 −1  SUSystemProfiler.m
  34. +27 −0 SUUpdateDriver.h
  35. +26 −0 SUUpdateDriver.m
  36. +2 −2 SUUpdatePermissionPrompt.h
  37. +1 −1  SUUpdatePermissionPrompt.m
  38. +6 −33 SUUpdater.h
  39. +51 −453 SUUpdater.m
  40. +23 −0 SUUserInitiatedUpdateDriver.h
  41. +151 −0 SUUserInitiatedUpdateDriver.m
  42. +8 −1 Sparkle.h
  43. +118 −62 Sparkle.xcodeproj/project.pbxproj
  44. +50 −0 en.lproj/SUAutomaticUpdateAlert.nib/classes.nib
  45. +20 −0 en.lproj/SUAutomaticUpdateAlert.nib/info.nib
  46. BIN  en.lproj/SUAutomaticUpdateAlert.nib/keyedobjects.nib
View
5 NSFileManager+Aliases.h
@@ -6,8 +6,13 @@
// Copyright 2008 Andy Matuschak. All rights reserved.
//
+#ifndef NSFILEMANAGER_PLUS_ALIASES_H
+#define NSFILEMANAGER_PLUS_ALIASES_H
+
#import <Cocoa/Cocoa.h>
@interface NSFileManager (Aliases)
- (BOOL)isAliasFolderAtPath:(NSString *)path;
@end
+
+#endif
View
35 NSFileManager+Authentication.m
@@ -88,7 +88,13 @@ - (NSString *)_temporaryCopyNameForPath:(NSString *)path
}
else
postFix = @"old";
- return [[[path stringByDeletingPathExtension] stringByAppendingFormat:@" (%@)", postFix] stringByAppendingPathExtension:[path pathExtension]];
+ NSString *prefix = [[path stringByDeletingPathExtension] stringByAppendingFormat:@" (%@)", postFix];
+ NSString *tempDir = [prefix stringByAppendingPathExtension:[path pathExtension]];
+ // Now let's make sure we get a unique path.
+ int cnt=2;
+ while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999)
+ tempDir = [NSString stringWithFormat:@"%@ %d.%@", prefix, cnt++, [path pathExtension]];
+ return tempDir;
}
- (BOOL)_copyPathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst error:(NSError **)error
@@ -206,27 +212,18 @@ - (BOOL)copyPathWithAuthentication:(NSString *)src overPath:(NSString *)dst erro
return [self _copyPathWithForcedAuthentication:src toPath:dst error:error];
NSString *tmpPath = [self _temporaryCopyNameForPath:dst];
-
- // We get more error information if we're running on Leopard, so let's use that if we can.
- if ([[NSFileManager defaultManager] respondsToSelector:@selector(moveItemAtPath:toPath:error:)])
+
+ if (![[NSFileManager defaultManager] movePath:dst toPath:tmpPath handler:self])
{
- if (![[NSFileManager defaultManager] moveItemAtPath:dst toPath:tmpPath error:error]) { return NO; }
- if (![[NSFileManager defaultManager] copyItemAtPath:src toPath:dst error:error]) { return NO; }
+ if (error != NULL)
+ *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] forKey:NSLocalizedDescriptionKey]];
+ return NO;
}
- else // We just get generic error messages.
+ if (![[NSFileManager defaultManager] copyPath:src toPath:dst handler:self])
{
- if (![[NSFileManager defaultManager] movePath:dst toPath:tmpPath handler:self])
- {
- if (error != NULL)
- *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] forKey:NSLocalizedDescriptionKey]];
- return NO;
- }
- if (![[NSFileManager defaultManager] copyPath:src toPath:dst handler:self])
- {
- if (error != NULL)
- *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't copy %@ to %@.", src, dst] forKey:NSLocalizedDescriptionKey]];
- return NO;
- }
+ if (error != NULL)
+ *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Couldn't copy %@ to %@.", src, dst] forKey:NSLocalizedDescriptionKey]];
+ return NO;
}
// Trash the old copy of the app.
View
5 NSFileManager+ExtendedAttributes.h
@@ -6,6 +6,9 @@
// Copyright 2008 Mark Mentovai. All rights reserved.
//
+#ifndef NSFILEMANAGER_PLUS_EXTENDEDATTRIBUTES
+#define NSFILEMANAGER_PLUS_EXTENDEDATTRIBUTES
+
#import <Cocoa/Cocoa.h>
@interface NSFileManager (ExtendedAttributes)
@@ -46,3 +49,5 @@
- (void)releaseFromQuarantine:(NSString*)root;
@end
+
+#endif
View
18 NSURL+Parameters.h
@@ -0,0 +1,18 @@
+//
+// NSURL+Parameters.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef NSURL_PLUS_PARAMETERS_H
+#define NSURL_PLUS_PARAMETERS_H
+
+#import <Cocoa/Cocoa.h>
+
+@interface NSURL (SUParameterAdditions)
+- (NSURL *)URLWithParameters:(NSArray *)parameters;
+@end
+
+#endif
View
27 NSURL+Parameters.m
@@ -0,0 +1,27 @@
+//
+// NSURL+Parameters.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "NSURL+Parameters.h"
+
+@implementation NSURL (SUParameterAdditions)
+- (NSURL *)URLWithParameters:(NSArray *)parameters;
+{
+ if (parameters == nil || [parameters count] == 0) { return self; }
+ NSMutableArray *profileInfo = [NSMutableArray array];
+ NSEnumerator *profileInfoEnumerator = [parameters objectEnumerator];
+ NSDictionary *currentProfileInfo;
+ while ((currentProfileInfo = [profileInfoEnumerator nextObject])) {
+ [profileInfo addObject:[NSString stringWithFormat:@"%@=%@", [currentProfileInfo objectForKey:@"key"], [currentProfileInfo objectForKey:@"value"]]];
+ }
+
+ NSString *appcastStringWithProfile = [NSString stringWithFormat:@"%@?%@", [self absoluteString], [profileInfo componentsJoinedByString:@"&"]];
+
+ // Clean it up so it's a valid URL
+ return [NSURL URLWithString:[appcastStringWithProfile stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
+}
+@end
View
18 NSWorkspace+SystemVersion.h
@@ -0,0 +1,18 @@
+//
+// NSWorkspace+SystemVersion.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef NSWORKSPACE_PLUS_SYSTEMVERSION_H
+#define NSWORKSPACE_PLUS_SYSTEMVERSION_H
+
+#import <Cocoa/Cocoa.h>
+
+@interface NSWorkspace (SystemVersion)
++ (NSString *)systemVersionString;
+@end
+
+#endif
View
36 NSWorkspace+SystemVersion.m
@@ -0,0 +1,36 @@
+//
+// NSWorkspace+SystemVersion.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "NSWorkspace+SystemVersion.h"
+
+
+@implementation NSWorkspace (SystemVersion)
++ (NSString *)systemVersionString
+{
+ // This returns a version string of the form X.Y.Z
+ // There may be a better way to deal with the problem that gestaltSystemVersionMajor
+ // et al. are not defined in 10.3, but this is probably good enough.
+ NSString* verStr = nil;
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4
+ SInt32 major, minor, bugfix;
+ OSErr err1 = Gestalt(gestaltSystemVersionMajor, &major);
+ OSErr err2 = Gestalt(gestaltSystemVersionMinor, &minor);
+ OSErr err3 = Gestalt(gestaltSystemVersionBugFix, &bugfix);
+ if (!err1 && !err2 && !err3)
+ {
+ verStr = [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix];
+ }
+ else
+#endif
+ {
+ NSString *versionPlistPath = @"/System/Library/CoreServices/SystemVersion.plist";
+ verStr = [[[NSDictionary dictionaryWithContentsOfFile:versionPlistPath] objectForKey:@"ProductVersion"] retain];
+ }
+ return verStr;
+}
+@end
View
4 RSS.h
@@ -61,14 +61,12 @@ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMA
/*Public*/
+- (RSS *)initWithURL:(NSURL *) url normalize:(BOOL) fl userAgent:(NSString*)userAgent error:(NSError **)error;
- (RSS *) initWithTitle: (NSString *) title andDescription: (NSString *) description;
- (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl;
-- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl;
-- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent:(NSString *)userAgent;
-
- (NSDictionary *) headerItems;
- (NSMutableArray *) newsItems;
View
38 RSS.m
@@ -138,41 +138,31 @@ - (RSS *) initWithData: (NSData *) rssData normalize: (BOOL) fl {
} /*initWithData*/
-- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl
-{
- return [self initWithURL: url normalize: fl userAgent: nil];
-}
-
-- (RSS *) initWithURL: (NSURL *) url normalize: (BOOL) fl userAgent: (NSString*)userAgent
+- (RSS *)initWithURL:(NSURL *)url normalize:(BOOL)fl userAgent:(NSString*)userAgent error:(NSError **)error
{
- NSData *rssData;
-
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: url cachePolicy: NSURLRequestReloadIgnoringCacheData
timeoutInterval: 30.0];
if (userAgent)
[request setValue: userAgent forHTTPHeaderField: @"User-Agent"];
- NSURLResponse *response=0;
- NSError *error=0;
-
- rssData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];
+ NSURLResponse *response = nil;
+ NSData *rssData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error];
+ if (rssData == nil) { return nil; }
- if (rssData == nil)
+ @try
{
- NSString *failureReason;
- if ([error respondsToSelector:@selector(localizedFailureReason)])
- failureReason = [error localizedFailureReason];
- else
- failureReason = [error localizedDescription];
- NSException *exception = [NSException exceptionWithName: @"RSSDownloadFailed"
- reason: failureReason userInfo: [error userInfo] ];
- [exception raise];
+ [self initWithData:rssData normalize:fl];
}
-
- return [self initWithData: rssData normalize: fl];
-} /*initWithUrl*/
+ @catch (NSException *parseException)
+ {
+ if (error)
+ *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while parsing the update feed.", nil), NSLocalizedDescriptionKey, [parseException reason], NSLocalizedFailureReasonErrorKey, nil]];
+ return nil;
+ }
+ return self;
+}
- (NSDictionary *) headerItems {
View
7 SUAppcast.h
@@ -12,11 +12,13 @@
@class RSS, SUAppcastItem;
@interface SUAppcast : NSObject {
NSArray *items;
+ NSString *userAgentString;
id delegate;
}
-- (void)fetchAppcastFromURL:(NSURL *)url parameters:(NSArray *)parameters;
+- (void)fetchAppcastFromURL:(NSURL *)url;
- (void)setDelegate:delegate;
+- (void)setUserAgentString:(NSString *)userAgentString;
- (SUAppcastItem *)newestItem;
- (NSArray *)items;
@@ -25,8 +27,7 @@
@interface NSObject (SUAppcastDelegate)
- (void)appcastDidFinishLoading:(SUAppcast *)appcast;
-- (void)appcastDidFailToLoad:(SUAppcast *)appcast;
-- (NSString *)userAgentForAppcast:(SUAppcast *)appcast;
+- (void)appcast:(SUAppcast *)appcast failedToLoadWithError:(NSError *)error;
@end
#endif
View
101 SUAppcast.m
@@ -9,39 +9,11 @@
#import "Sparkle.h"
#import "SUAppcast.h"
-@interface NSURL (SUParameterAdditions)
-- (NSURL *)URLWithParameters:(NSArray *)parameters;
-@end
-
-@implementation NSURL (SUParameterAdditions)
-- (NSURL *)URLWithParameters:(NSArray *)parameters;
-{
- if (parameters == nil || [parameters count] == 0) { return self; }
- NSMutableArray *profileInfo = [NSMutableArray array];
- NSEnumerator *profileInfoEnumerator = [parameters objectEnumerator];
- NSDictionary *currentProfileInfo;
- while ((currentProfileInfo = [profileInfoEnumerator nextObject])) {
- [profileInfo addObject:[NSString stringWithFormat:@"%@=%@", [currentProfileInfo objectForKey:@"key"], [currentProfileInfo objectForKey:@"value"]]];
- }
-
- NSString *appcastStringWithProfile = [NSString stringWithFormat:@"%@?%@", [self absoluteString], [profileInfo componentsJoinedByString:@"&"]];
-
- // Clean it up so it's a valid URL
- return [NSURL URLWithString:[appcastStringWithProfile stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
-}
-@end
-
-
@implementation SUAppcast
-- (void)fetchAppcastFromURL:(NSURL *)url parameters:(NSArray *)parameters
+- (void)fetchAppcastFromURL:(NSURL *)url
{
- [NSThread detachNewThreadSelector:@selector(_fetchAppcastFromURL:) toTarget:self withObject:[url URLWithParameters:parameters]]; // let's not block the main thread
-}
-
-- (void)setDelegate:del
-{
- delegate = del;
+ [NSThread detachNewThreadSelector:@selector(_fetchAppcastFromURL:) toTarget:self withObject:url];
}
- (void)dealloc
@@ -52,7 +24,10 @@ - (void)dealloc
- (SUAppcastItem *)newestItem
{
- return [items objectAtIndex:0]; // the RSS class takes care of sorting by published date, descending.
+ if ([items count] > 0)
+ return [items objectAtIndex:0]; // the RSS class takes care of sorting by published date, descending.
+ else
+ return nil;
}
- (NSArray *)items
@@ -64,40 +39,56 @@ - (void)_fetchAppcastFromURL:(NSURL *)url
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
- RSS *feed = [RSS alloc];
- @try
+ NSError *error = nil;
+ RSS *feed = [[RSS alloc] initWithURL:url normalize:YES userAgent:userAgentString error:&error];
+ if (!feed)
{
- NSString *userAgent = nil;
- if ([delegate respondsToSelector:@selector(userAgentForAppcast:)])
- userAgent = [delegate userAgentForAppcast:self];
-
- feed = [feed initWithURL:url normalize:YES userAgent:userAgent];
- if (!feed)
- [NSException raise:@"SUFeedException" format:@"Couldn't fetch feed from server."];
+ [self performSelectorOnMainThread:@selector(reportError:) withObject:error waitUntilDone:NO];
+ return;
+ }
- // Set up all the appcast items
- NSMutableArray *tempItems = [NSMutableArray array];
- id enumerator = [[feed newsItems] objectEnumerator], current;
+ // Set up all the appcast items:
+ items = [NSMutableArray array];
+ id enumerator = [[feed newsItems] objectEnumerator], current;
+ @try
+ {
while ((current = [enumerator nextObject]))
{
- [tempItems addObject:[[[SUAppcastItem alloc] initWithDictionary:current] autorelease]];
+ [(NSMutableArray *)items addObject:[[[SUAppcastItem alloc] initWithDictionary:current] autorelease]];
}
- items = [[NSArray arrayWithArray:tempItems] retain];
-
- if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:)])
- [delegate performSelectorOnMainThread:@selector(appcastDidFinishLoading:) withObject:self waitUntilDone:NO];
-
}
- @catch (NSException *e)
+ @catch (NSException *parseException)
{
- if ([delegate respondsToSelector:@selector(appcastDidFailToLoad:)])
- [delegate performSelectorOnMainThread:@selector(appcastDidFailToLoad:) withObject:self waitUntilDone:NO];
+ error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while parsing the update feed.", nil), NSLocalizedDescriptionKey, [parseException reason], SUTechnicalErrorInformationKey, nil]];
+ [self performSelectorOnMainThread:@selector(reportError:) withObject:error waitUntilDone:NO];
+ return;
}
- @finally
+ items = [[NSArray arrayWithArray:items] retain]; // Make the items list immutable.
+
+ if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:)])
+ [delegate performSelectorOnMainThread:@selector(appcastDidFinishLoading:) withObject:self waitUntilDone:NO];
+
+ [feed release];
+ [pool release];
+}
+
+- (void)reportError:(NSError *)error
+{
+ if ([delegate respondsToSelector:@selector(appcast:failedToLoadWithError:)])
{
- [feed release];
- [pool release];
+ [delegate appcast:self failedToLoadWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]];
}
}
+- (void)setUserAgentString:(NSString *)uas
+{
+ [userAgentString release];
+ userAgentString = [uas copy];
+}
+
+- (void)setDelegate:del
+{
+ delegate = del;
+}
+
@end
View
32 SUAppcastItem.h
@@ -17,48 +17,26 @@
NSURL *releaseNotesURL;
- NSString *DSASignature;
- NSString *MD5Sum;
-
+ NSString *DSASignature;
NSString *minimumSystemVersion;
NSURL *fileURL;
- NSString *fileVersion;
NSString *versionString;
+ NSString *displayVersionString;
}
// Initializes with data from a dictionary provided by the RSS class.
- initWithDictionary:(NSDictionary *)dict;
- (NSString *)title;
-- (void)setTitle:(NSString *)aTitle;
-
+- (NSString *)versionString;
+- (NSString *)displayVersionString;
- (NSDate *)date;
-- (void)setDate:(NSDate *)aDate;
-
- (NSString *)description;
-- (void)setDescription:(NSString *)aDescription;
-
- (NSURL *)releaseNotesURL;
-- (void)setReleaseNotesURL:(NSURL *)aReleaseNotesURL;
-
-- (NSString *)DSASignature;
-- (void)setDSASignature:(NSString *)aDSASignature;
-
-- (NSString *)MD5Sum;
-- (void)setMD5Sum:(NSString *)aMd5Sum;
-
- (NSURL *)fileURL;
-- (void)setFileURL:(NSURL *)aFileURL;
-
-- (NSString *)fileVersion;
-- (void)setFileVersion:(NSString *)aFileVersion;
-
-- (NSString *)versionString;
-- (void)setVersionString:(NSString *)versionString;
-
+- (NSString *)DSASignature;
- (NSString *)minimumSystemVersion;
-- (void)setMinimumSystemVersion:(NSString *)systemVersionString;
@end
View
149 SUAppcastItem.m
@@ -11,70 +11,6 @@
@implementation SUAppcastItem
-- initWithDictionary:(NSDictionary *)dict
-{
- self = [super init];
- if (self)
- {
- [self setTitle:[dict objectForKey:@"title"]];
- [self setDate:[dict objectForKey:@"pubDate"]];
- [self setDescription:[dict objectForKey:@"description"]];
-
- id enclosure = [dict objectForKey:@"enclosure"];
- [self setDSASignature:[enclosure objectForKey:@"sparkle:dsaSignature"]];
- [self setMD5Sum:[enclosure objectForKey:@"sparkle:md5Sum"]];
-
- [self setFileURL:[NSURL URLWithString:[[enclosure objectForKey:@"url"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
-
- // Find the appropriate release notes URL.
- if ([dict objectForKey:@"sparkle:releaseNotesLink"])
- {
- [self setReleaseNotesURL:[NSURL URLWithString:[dict objectForKey:@"sparkle:releaseNotesLink"]]];
- }
- else if ([[self description] hasPrefix:@"http://"]) // if the description starts with http://, use that.
- {
- [self setReleaseNotesURL:[NSURL URLWithString:[self description]]];
- }
- else
- {
- [self setReleaseNotesURL:nil];
- }
-
- NSString *minVersion = [dict objectForKey:@"sparkle:minimumSystemVersion"];
- if(minVersion)
- [self setMinimumSystemVersion:minVersion];
- else
- [self setMinimumSystemVersion:@"10.3.0"];//sparkle doesn't run on 10.2-, so we don't have to worry about it
-
- // Try to find a version string.
- // Finding the new version number from the RSS feed is a little bit hacky. There are two ways:
- // 1. A "sparkle:version" attribute on the enclosure tag, an extension from the RSS spec.
- // 2. If there isn't a version attribute, Sparkle will parse the path in the enclosure, expecting
- // that it will look like this: http://something.com/YourApp_0.5.zip. It'll read whatever's between the last
- // underscore and the last period as the version number. So name your packages like this: APPNAME_VERSION.extension.
- // The big caveat with this is that you can't have underscores in your version strings, as that'll confuse Sparkle.
- // Feel free to change the separator string to a hyphen or something more suited to your needs if you like.
- NSString *newVersion = [enclosure objectForKey:@"sparkle:version"];
- if (!newVersion) // no sparkle:version attribute
- {
- // Separate the url by underscores and take the last component, as that'll be closest to the end,
- // then we remove the extension. Hopefully, this will be the version.
- NSArray *fileComponents = [[enclosure objectForKey:@"url"] componentsSeparatedByString:@"_"];
- if ([fileComponents count] > 1)
- newVersion = [[fileComponents lastObject] stringByDeletingPathExtension];
- }
- [self setFileVersion:newVersion];
-
- NSString *shortVersionString = [enclosure objectForKey:@"sparkle:shortVersionString"];
- if (shortVersionString)
- [self setVersionString:shortVersionString];
- else
- [self setVersionString:[self fileVersion]];
- }
-
- return self;
-}
-
// Attack of accessors!
- (NSString *)title { return [[title retain] autorelease]; }
@@ -120,16 +56,7 @@ - (void)setDSASignature:(NSString *)aDSASignature
[DSASignature release];
DSASignature = [aDSASignature copy];
}
-
-
-- (NSString *)MD5Sum { return [[MD5Sum retain] autorelease]; }
-
-- (void)setMD5Sum:(NSString *)aMD5Sum
-{
- [MD5Sum release];
- MD5Sum = [aMD5Sum copy];
-}
-
+
- (NSURL *)fileURL { return [[fileURL retain] autorelease]; }
@@ -140,21 +67,21 @@ - (void)setFileURL:(NSURL *)aFileURL
}
-- (NSString *)fileVersion { return [[fileVersion retain] autorelease]; }
+- (NSString *)versionString { return [[versionString retain] autorelease]; }
-- (void)setFileVersion:(NSString *)aFileVersion
+- (void)setVersionString:(NSString *)s
{
- [fileVersion release];
- fileVersion = [aFileVersion copy];
+ [versionString release];
+ versionString = [s copy];
}
-- (NSString *)versionString { return [[versionString retain] autorelease]; }
+- (NSString *)displayVersionString { return [[displayVersionString retain] autorelease]; }
-- (void)setVersionString:(NSString *)aVersionString
+- (void)setDisplayVersionString:(NSString *)s
{
- [versionString release];
- versionString = [aVersionString copy];
+ [displayVersionString release];
+ displayVersionString = [s copy];
}
@@ -165,6 +92,59 @@ - (void)setMinimumSystemVersion:(NSString *)systemVersionString
minimumSystemVersion = [systemVersionString copy];
}
+- initWithDictionary:(NSDictionary *)dict
+{
+ self = [super init];
+ if (self)
+ {
+ [self setTitle:[dict objectForKey:@"title"]];
+ [self setDate:[dict objectForKey:@"pubDate"]];
+ [self setDescription:[dict objectForKey:@"description"]];
+
+ id enclosure = [dict objectForKey:@"enclosure"];
+ if (enclosure == nil || [enclosure objectForKey:@"url"] == nil)
+ [NSException raise:@"SUAppcastException" format:@"Couldn't find an download URL for feed entry %@!", [self title]];
+ [self setFileURL:[NSURL URLWithString:[[enclosure objectForKey:@"url"] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
+ [self setDSASignature:[enclosure objectForKey:@"sparkle:dsaSignature"]];
+
+ // Try to find a version string.
+ // Finding the new version number from the RSS feed is a little bit hacky. There are two ways:
+ // 1. A "sparkle:version" attribute on the enclosure tag, an extension from the RSS spec.
+ // 2. If there isn't a version attribute, Sparkle will parse the path in the enclosure, expecting
+ // that it will look like this: http://something.com/YourApp_0.5.zip. It'll read whatever's between the last
+ // underscore and the last period as the version number. So name your packages like this: APPNAME_VERSION.extension.
+ // The big caveat with this is that you can't have underscores in your version strings, as that'll confuse Sparkle.
+ // Feel free to change the separator string to a hyphen or something more suited to your needs if you like.
+ NSString *newVersion = [enclosure objectForKey:@"sparkle:version"];
+ if (!newVersion) // no sparkle:version attribute
+ {
+ // Separate the url by underscores and take the last component, as that'll be closest to the end,
+ // then we remove the extension. Hopefully, this will be the version.
+ NSArray *fileComponents = [[enclosure objectForKey:@"url"] componentsSeparatedByString:@"_"];
+ if ([fileComponents count] > 1)
+ newVersion = [[fileComponents lastObject] stringByDeletingPathExtension];
+ else
+ [NSException raise:@"SUAppcastException" format:@"Couldn't find a version string for %@! You need a sparkle:version attribute.", [enclosure objectForKey:@"url"]];
+ }
+ [self setVersionString:newVersion];
+ [self setMinimumSystemVersion:[dict objectForKey:@"sparkle:minimumSystemVersion"]];
+
+ NSString *shortVersionString = [enclosure objectForKey:@"sparkle:shortVersionString"];
+ if (shortVersionString)
+ [self setDisplayVersionString:shortVersionString];
+ else
+ [self setDisplayVersionString:[self versionString]];
+
+ // Find the appropriate release notes URL.
+ if ([dict objectForKey:@"sparkle:releaseNotesLink"])
+ [self setReleaseNotesURL:[NSURL URLWithString:[dict objectForKey:@"sparkle:releaseNotesLink"]]];
+ else if ([[self description] hasPrefix:@"http://"]) // if the description starts with http://, use that.
+ [self setReleaseNotesURL:[NSURL URLWithString:[self description]]];
+ else
+ [self setReleaseNotesURL:nil];
+ }
+ return self;
+}
- (void)dealloc
{
@@ -173,10 +153,9 @@ - (void)dealloc
[self setDescription:nil];
[self setReleaseNotesURL:nil];
[self setDSASignature:nil];
- [self setMD5Sum:nil];
[self setFileURL:nil];
- [self setFileVersion:nil];
- [self setVersionString:nil];
+ [self setVersionString:nil];
+ [self setDisplayVersionString:nil];
[super dealloc];
}
View
18 SUAutomaticUpdateAlert.h
@@ -11,17 +11,29 @@
#import "SUWindowController.h"
+typedef enum
+{
+ SUInstallNowChoice,
+ SUInstallLaterChoice,
+ SUDoNotInstallChoice
+} SUAutomaticInstallationChoice;
+
@class SUAppcastItem;
@interface SUAutomaticUpdateAlert : SUWindowController {
SUAppcastItem *updateItem;
+ id delegate;
NSBundle *hostBundle;
}
-- (id)initWithAppcastItem:(SUAppcastItem *)item hostBundle:(NSBundle *)hostBundle;
+- (id)initWithAppcastItem:(SUAppcastItem *)item hostBundle:(NSBundle *)hostBundle delegate:delegate;
+- (IBAction)installNow:sender;
+- (IBAction)installLater:sender;
+- (IBAction)doNotInstall:sender;
-- (IBAction)relaunchNow:sender;
-- (IBAction)relaunchLater:sender;
+@end
+@interface NSObject (SUAutomaticUpdateAlertDelegateProtocol)
+- (void)automaticUpdateAlert:(SUAutomaticUpdateAlert *)aua finishedWithChoice:(SUAutomaticInstallationChoice)choice;
@end
#endif
View
24 SUAutomaticUpdateAlert.m
@@ -11,19 +11,21 @@
@implementation SUAutomaticUpdateAlert
-- (id)initWithAppcastItem:(SUAppcastItem *)item hostBundle:(NSBundle *)hb;
+- (id)initWithAppcastItem:(SUAppcastItem *)item hostBundle:(NSBundle *)hb delegate:del;
{
self = [super initWithHostBundle:hb windowNibName:@"SUAutomaticUpdateAlert"];
if (self)
{
updateItem = [item retain];
+ delegate = del;
hostBundle = [hb retain];
[self setShouldCascadeWindows:NO];
+ [[self window] center];
}
return self;
}
-- (void) dealloc
+- (void)dealloc
{
[hostBundle release];
[updateItem release];
@@ -31,16 +33,22 @@ - (void) dealloc
}
-- (IBAction)relaunchNow:sender
+- (IBAction)installNow:sender
{
[self close];
- [NSApp stopModalWithCode:NSAlertDefaultReturn];
+ [delegate automaticUpdateAlert:self finishedWithChoice:SUInstallNowChoice];
}
-- (IBAction)relaunchLater:sender
+- (IBAction)installLater:sender
{
[self close];
- [NSApp stopModalWithCode:NSAlertAlternateReturn];
+ [delegate automaticUpdateAlert:self finishedWithChoice:SUInstallLaterChoice];
+}
+
+- (IBAction)doNotInstall:sender
+{
+ [self close];
+ [delegate automaticUpdateAlert:self finishedWithChoice:SUDoNotInstallChoice];
}
- (NSImage *)applicationIcon
@@ -50,12 +58,12 @@ - (NSImage *)applicationIcon
- (NSString *)titleText
{
- return [NSString stringWithFormat:SULocalizedString(@"A new version of %@ has been installed!", nil), [hostBundle name]];
+ return [NSString stringWithFormat:SULocalizedString(@"A new version of %@ is ready to install!", nil), [hostBundle name]];
}
- (NSString *)descriptionText
{
- return [NSString stringWithFormat:SULocalizedString(@"%1$@ %2$@ has been installed and will be ready to use next time %1$@ starts! Would you like to relaunch now?", nil), [hostBundle name], [hostBundle displayVersion]];
+ return [NSString stringWithFormat:SULocalizedString(@"%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?", nil), [hostBundle name], [hostBundle displayVersion]];
}
@end
View
23 SUAutomaticUpdateDriver.h
@@ -0,0 +1,23 @@
+//
+// SUAutomaticUpdateDriver.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef SUAUTOMATICUPDATEDRIVER_H
+#define SUAUTOMATICUPDATEDRIVER_H
+
+#import <Cocoa/Cocoa.h>
+#import "SUBasicUpdateDriver.h"
+
+@class SUAutomaticUpdateAlert;
+@interface SUAutomaticUpdateDriver : SUBasicUpdateDriver {
+ BOOL postponingInstallation, showErrors;
+ SUAutomaticUpdateAlert *alert;
+}
+
+@end
+
+#endif
View
76 SUAutomaticUpdateDriver.m
@@ -0,0 +1,76 @@
+//
+// SUAutomaticUpdateDriver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "SUAutomaticUpdateDriver.h"
+#import "Sparkle.h"
+
+@implementation SUAutomaticUpdateDriver
+
+- (void)unarchiverDidFinish:(SUUnarchiver *)ua
+{
+ alert = [[SUAutomaticUpdateAlert alloc] initWithAppcastItem:updateItem hostBundle:hostBundle delegate:self];
+ if ([NSApp isActive])
+ [alert showWindow:self];
+ else
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:NSApp];
+}
+
+- (void)applicationDidBecomeActive:(NSNotification *)aNotification
+{
+ [alert showWindow:self];
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:@"NSApplicationDidBecomeActiveNotification" object:NSApp];
+}
+
+- (void)automaticUpdateAlert:(SUAutomaticUpdateAlert *)aua finishedWithChoice:(SUAutomaticInstallationChoice)choice;
+{
+ switch (choice)
+ {
+ case SUInstallNowChoice:
+ [self installUpdate];
+ break;
+
+ case SUInstallLaterChoice:
+ postponingInstallation = YES;
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil];
+ break;
+
+ case SUDoNotInstallChoice:
+ [[SUUserDefaults standardUserDefaults] setObject:[updateItem versionString] forKey:SUSkippedVersionKey];
+ [self abortUpdate];
+ break;
+ }
+}
+
+- (void)installUpdate
+{
+ showErrors = YES;
+ [SUInstaller installFromUpdateFolder:[downloadPath stringByDeletingLastPathComponent] overHostBundle:hostBundle delegate:self synchronously:postponingInstallation];
+}
+
+- (void)applicationWillTerminate:(NSNotification *)note
+{
+ [self installUpdate];
+}
+
+- (void)installerFinishedForHostBundle:(NSBundle *)hb
+{
+ if (hb != hostBundle) { return; }
+ [unarchiver cleanUp];
+ if (!postponingInstallation)
+ [self relaunchHostApp];
+}
+
+- (void)abortUpdateWithError:(NSError *)error
+{
+ if (showErrors)
+ [super abortUpdateWithError:error];
+ else
+ [self abortUpdate];
+}
+
+@end
View
56 SUBasicUpdateDriver.h
@@ -0,0 +1,56 @@
+//
+// SUBasicUpdateDriver.h
+// Sparkle,
+//
+// Created by Andy Matuschak on 4/23/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef SUBASICUPDATEDRIVER_H
+#define SUBASICUPDATEDRIVER_H
+
+#import <Cocoa/Cocoa.h>
+#import "SUUpdateDriver.h"
+
+@class SUAppcastItem, SUUnarchiver, SUAppcast, SUUnarchiver;
+@interface SUBasicUpdateDriver : SUUpdateDriver {
+ NSBundle *hostBundle;
+ SUAppcastItem *updateItem;
+
+ NSURLDownload *download;
+ NSString *downloadPath;
+ SUUnarchiver *unarchiver;
+}
+
+- (void)checkForUpdatesAtURL:(NSURL *)appcastURL hostBundle:(NSBundle *)hb;
+
+- (void)appcastDidFinishLoading:(SUAppcast *)ac;
+- (void)appcast:(SUAppcast *)ac failedToLoadWithError:(NSError *)error;
+
+- (BOOL)isItemNewer:(SUAppcastItem *)ui;
+- (BOOL)hostSupportsItem:(SUAppcastItem *)ui;
+- (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui;
+- (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui;
+- (void)didFindValidUpdate;
+- (void)didNotFindUpdate;
+
+- (void)downloadUpdate;
+- (void)download:(NSURLDownload *)d decideDestinationWithSuggestedFilename:(NSString *)name;
+- (void)downloadDidFinish:(NSURLDownload *)d;
+- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error;
+
+- (void)extractUpdate;
+- (void)unarchiverDidFinish:(SUUnarchiver *)ua;
+- (void)unarchiverDidFail:(SUUnarchiver *)ua;
+
+- (void)installUpdate;
+- (void)installerFinishedForHostBundle:(NSBundle *)hb;
+- (void)installerForHostBundle:(NSBundle *)hb failedWithError:(NSError *)error;
+- (void)relaunchHostApp;
+
+- (void)abortUpdate;
+- (void)abortUpdateWithError:(NSError *)error;
+
+@end
+
+#endif
View
208 SUBasicUpdateDriver.m
@@ -0,0 +1,208 @@
+//
+// SUBasicUpdateDriver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 4/23/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "SUBasicUpdateDriver.h"
+#import "Sparkle.h"
+
+@implementation SUBasicUpdateDriver
+
+- (void)checkForUpdatesAtURL:(NSURL *)appcastURL hostBundle:(NSBundle *)hb
+{
+ hostBundle = [hb retain];
+ SUAppcast *appcast = [[SUAppcast alloc] init];
+ CFRetain(appcast); // We'll manage the appcast's memory ourselves so we don't have to make it an IV to support GC.
+ [appcast release];
+
+ [appcast setDelegate:self];
+ [appcast setUserAgentString:[NSString stringWithFormat: @"%@/%@ Sparkle/1.5b1", [hostBundle name], [hostBundle displayVersion]]];
+ [appcast fetchAppcastFromURL:appcastURL];
+}
+
+- (BOOL)isItemNewer:(SUAppcastItem *)ui
+{
+ return [[SUStandardVersionComparator defaultComparator] compareVersion:[hostBundle version]
+ toVersion:[ui versionString]] == NSOrderedAscending;
+}
+
+- (BOOL)hostSupportsItem:(SUAppcastItem *)ui
+{
+ if ([ui minimumSystemVersion] == nil || [[ui minimumSystemVersion] isEqualToString:@""]) { return YES; }
+ return [[SUStandardVersionComparator defaultComparator] compareVersion:[ui minimumSystemVersion]
+ toVersion:[NSWorkspace systemVersionString]] != NSOrderedDescending;
+}
+
+- (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui
+{
+ return [[[SUUserDefaults standardUserDefaults] objectForKey:SUSkippedVersionKey] isEqualToString:[ui versionString]];
+}
+
+- (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui
+{
+ return [self hostSupportsItem:ui] && [self isItemNewer:ui] && ![self itemContainsSkippedVersion:ui];
+}
+
+- (void)appcastDidFinishLoading:(SUAppcast *)ac
+{
+ updateItem = [[ac newestItem] retain];
+ CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
+ if (updateItem == nil) { [self didNotFindUpdate]; return; }
+
+ [[SUUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:SULastCheckTimeKey];
+
+ if ([self itemContainsValidUpdate:updateItem])
+ [self didFindValidUpdate];
+ else
+ [self didNotFindUpdate];
+}
+
+- (void)appcast:(SUAppcast *)ac failedToLoadWithError:(NSError *)error
+{
+ CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
+ [self abortUpdateWithError:error];
+}
+
+- (void)didFindValidUpdate
+{
+ [self downloadUpdate];
+}
+
+- (void)didNotFindUpdate
+{
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUNoUpdateError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:SULocalizedString(@"You already have the newest version of %@.", nil), [hostBundle name]] forKey:NSLocalizedDescriptionKey]]];
+}
+
+- (void)downloadUpdate
+{
+ download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self];
+}
+
+- (void)download:(NSURLDownload *)d decideDestinationWithSuggestedFilename:(NSString *)name
+{
+ // If name ends in .txt, the server probably has a stupid MIME configuration. We'll give the developer the benefit of the doubt and chop that off.
+ if ([[name pathExtension] isEqualToString:@"txt"])
+ name = [name stringByDeletingPathExtension];
+
+ // We create a temporary directory in /tmp and stick the file there.
+ // Not using a GUID here because hdiutil for some reason chokes on GUIDs. Too long? I really have no idea.
+ NSString *prefix = [NSString stringWithFormat:@"%@ %@ Update", [hostBundle name], [hostBundle version]];
+ NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:prefix];
+ int cnt=1;
+ while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999)
+ tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d", prefix, cnt++]];
+ BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil];
+ if (!success)
+ {
+ // Okay, something's really broken with /tmp
+ [download cancel];
+ [download release]; download = nil;
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUTemporaryDirectoryError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Can't make a temporary directory for the update download at %@.",tempDir] forKey:NSLocalizedDescriptionKey]]];
+ }
+
+ downloadPath = [[tempDir stringByAppendingPathComponent:name] retain];
+ [download setDestination:downloadPath allowOverwrite:YES];
+}
+
+- (void)downloadDidFinish:(NSURLDownload *)d
+{
+ [download release];
+ download = nil;
+ [self extractUpdate];
+}
+
+- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
+{
+ [self abortUpdateWithError:error];
+}
+
+- (void)extractUpdate
+{
+ // DSA verification, if activated by the developer
+ if ([[hostBundle objectForInfoDictionaryKey:SUExpectsDSASignatureKey] boolValue])
+ {
+ NSString *pkeyString = [hostBundle objectForInfoDictionaryKey:SUPublicDSAKeyKey];
+ if (![[NSFileManager defaultManager] validatePath:downloadPath withEncodedDSASignature:[updateItem DSASignature] withPublicDSAKey:pkeyString])
+ {
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUSignatureError userInfo:[NSDictionary dictionaryWithObject:@"The update is improperly signed." forKey:NSLocalizedDescriptionKey]]];
+ return;
+ }
+ }
+
+ unarchiver = [[SUUnarchiver alloc] init];
+ [unarchiver setDelegate:self];
+ [unarchiver unarchivePath:downloadPath];
+}
+
+- (void)unarchiverDidFinish:(SUUnarchiver *)ua
+{
+ [self installUpdate];
+}
+
+- (void)unarchiverDidFail:(SUUnarchiver *)ua
+{
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:[NSDictionary dictionaryWithObject:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil) forKey:NSLocalizedDescriptionKey]]];
+}
+
+- (void)installUpdate
+{
+ [SUInstaller installFromUpdateFolder:[downloadPath stringByDeletingLastPathComponent] overHostBundle:hostBundle delegate:self synchronously:NO];
+}
+
+- (void)installerFinishedForHostBundle:(NSBundle *)hb
+{
+ if (hb != hostBundle) { return; }
+ [unarchiver cleanUp];
+ [self relaunchHostApp];
+}
+
+- (void)relaunchHostApp
+{
+ [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self];
+ NSString *relaunchPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"relaunch" ofType:nil];
+ @try
+ {
+ [NSTask launchedTaskWithLaunchPath:relaunchPath arguments:[NSArray arrayWithObjects:[hostBundle bundlePath], [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]], nil]];
+ }
+ @catch (NSException *e)
+ {
+ // Note that we explicitly use the host app's name here, since updating plugin for Mail relaunches Mail, not just the plugin.
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:SULocalizedString(@"An error occurred while relaunching %1$@, but the new version will be available next time you run %1$@.", nil), [NSApp name]], NSLocalizedDescriptionKey, [e reason], NSLocalizedFailureReasonErrorKey, nil]]];
+ // We intentionally don't abandon the update here so that the host won't initiate another.
+ }
+ [NSApp terminate:self];
+}
+
+- (void)installerForHostBundle:(NSBundle *)hb failedWithError:(NSError *)error
+{
+ if (hb != hostBundle) { return; }
+ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while installing the update. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]];
+}
+
+- (void)abortUpdate
+{
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super abortUpdate];
+}
+
+- (void)abortUpdateWithError:(NSError *)error
+{
+ if ([error code] != SUNoUpdateError) // Let's not bother logging this.
+ NSLog(@"Sparkle Error: %@", [error localizedDescription]);
+ if ([error localizedFailureReason])
+ NSLog(@"Sparkle Error (continued): %@", [error localizedFailureReason]);
+ [self abortUpdate];
+}
+
+- (void)dealloc
+{
+ [hostBundle release];
+ [downloadPath release];
+ [unarchiver release];
+ [super dealloc];
+}
+
+@end
View
17 SUConstants.h
@@ -12,6 +12,7 @@
extern NSString *SUUpdaterWillRestartNotification;
+extern NSString *SUTechnicalErrorInformationKey;
extern NSString *SUFeedURLKey;
extern NSString *SUHasLaunchedBeforeKey;
@@ -28,10 +29,26 @@ extern NSString *SUEnableSystemProfilingKey;
extern NSString *SUSendProfileInfoKey;
extern NSString *SUSparkleErrorDomain;
+// Appcast phase errors.
+extern OSStatus SUAppcastParseError;
+extern OSStatus SUNoUpdateError;
+extern OSStatus SUAppcastError;
+extern OSStatus SURunningFromDiskImageError;
+
+// Downlaod phase errors.
+extern OSStatus SUTemporaryDirectoryError;
+
+// Extraction phase errors.
+extern OSStatus SUUnarchivingError;
+extern OSStatus SUSignatureError;
+
+// Installation phase errors.
extern OSStatus SUFileCopyFailure;
extern OSStatus SUAuthenticationFailure;
extern OSStatus SUMissingUpdateError;
extern OSStatus SUMissingInstallerToolError;
+extern OSStatus SURelaunchError;
+extern OSStatus SUInstallationError;
// NSInteger is a type that was added to Leopard.
// Here is some glue to ensure that NSInteger will work with pre-10.5 SDKs:
View
21 SUConstants.m
@@ -10,6 +10,7 @@
#import "SUConstants.h"
NSString *SUUpdaterWillRestartNotification = @"SUUpdaterWillRestartNotificationName";
+NSString *SUTechnicalErrorInformationKey = @"SUTechnicalErrorInformation";
NSString *SUHasLaunchedBeforeKey = @"SUHasLaunchedBefore";
NSString *SUFeedURLKey = @"SUFeedURL";
@@ -26,7 +27,19 @@
NSString *SUSendProfileInfoKey = @"SUSendProfileInfo";
NSString *SUSparkleErrorDomain = @"SUSparkleErrorDomain";
-OSStatus SUFileCopyFailure = 9000;
-OSStatus SUAuthenticationFailure = 9001;
-OSStatus SUMissingUpdateError = 9002;
-OSStatus SUMissingInstallerToolError = 9003;
+OSStatus SUAppcastParseError = 1000;
+OSStatus SUNoUpdateError = 1001;
+OSStatus SUAppcastError = 1002;
+OSStatus SURunningFromDiskImageError = 1003;
+
+OSStatus SUTemporaryDirectoryError = 2000;
+
+OSStatus SUUnarchivingError = 3000;
+OSStatus SUSignatureError = 3001;
+
+OSStatus SUFileCopyFailure = 4000;
+OSStatus SUAuthenticationFailure = 4001;
+OSStatus SUMissingUpdateError = 4002;
+OSStatus SUMissingInstallerToolError = 4003;
+OSStatus SURelaunchError = 4004;
+OSStatus SUInstallationError = 4005;
View
13 SUInstaller.h
@@ -6,18 +6,19 @@
// Copyright 2008 Andy Matuschak. All rights reserved.
//
+#ifndef SUINSTALLER_H
+#define SUINSTALLER_H
+
#import <Cocoa/Cocoa.h>
@interface SUInstaller : NSObject { }
-+ (void)installFromUpdateFolder:(NSString *)updateFolder overHostBundle:(NSBundle *)hostBundle delegate:delegate;
++ (void)installFromUpdateFolder:(NSString *)updateFolder overHostBundle:(NSBundle *)hostBundle delegate:delegate synchronously:(BOOL)synchronously;
+ (void)_finishInstallationWithResult:(BOOL)result hostBundle:(NSBundle *)hostBundle error:(NSError *)error delegate:delegate;
@end
@interface NSObject (SUInstallerDelegateInformalProtocol)
-- installerFinishedForHostBundle:(NSBundle *)hostBundle;
-- installerForHostBundle:(NSBundle *)hostBundle failedWithError:(NSError *)error;
+- (void)installerFinishedForHostBundle:(NSBundle *)hostBundle;
+- (void)installerForHostBundle:(NSBundle *)hostBundle failedWithError:(NSError *)error;
@end
-extern NSString *SUInstallerPathKey;
-extern NSString *SUInstallerHostBundleKey;
-extern NSString *SUInstallerDelegateKey;
+#endif
View
5 SUInstaller.m
@@ -16,7 +16,7 @@
@implementation SUInstaller
-+ (void)installFromUpdateFolder:(NSString *)updateFolder overHostBundle:(NSBundle *)hostBundle delegate:delegate;
++ (void)installFromUpdateFolder:(NSString *)updateFolder overHostBundle:(NSBundle *)hostBundle delegate:delegate synchronously:(BOOL)synchronously;
{
// Search subdirectories for the application
NSString *currentFile, *newAppDownloadPath = nil, *bundleFileName = [[hostBundle bundlePath] lastPathComponent], *alternateBundleFileName = [[hostBundle name] stringByAppendingPathExtension:[[hostBundle bundlePath] pathExtension]];
@@ -56,8 +56,7 @@ + (void)installFromUpdateFolder:(NSString *)updateFolder overHostBundle:(NSBundl
}
else
{
- NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:newAppDownloadPath, SUInstallerPathKey, hostBundle, SUInstallerHostBundleKey, delegate, SUInstallerDelegateKey, nil];
- [NSThread detachNewThreadSelector:@selector(performInstallationWithInfo:) toTarget:(isPackage ? [SUPackageInstaller class] : [SUPlainInstaller class]) withObject:info];
+ [(isPackage ? [SUPackageInstaller class] : [SUPlainInstaller class]) performInstallationWithPath:newAppDownloadPath hostBundle:hostBundle delegate:delegate synchronously:synchronously];
}
}
View
5 SUPackageInstaller.h
@@ -6,9 +6,14 @@
// Copyright 2008 Andy Matuschak. All rights reserved.
//
+#ifndef SUPACKAGEINSTALLER_H
+#define SUPACKAGEINSTALLER_H
+
#import "Sparkle.h"
#import "SUPlainInstaller.h"
@interface SUPackageInstaller : SUPlainInstaller { }
+ (void)installPath:(NSString *)path overHostBundle:(NSBundle *)bundle delegate:delegate;
@end
+
+#endif
View
7 SUPlainInstaller.h
@@ -6,8 +6,13 @@
// Copyright 2008 Andy Matuschak. All rights reserved.
//
+#ifndef SUPLAININSTALLER_H
+#define SUPLAININSTALLER_H
+
#import "Sparkle.h"
@interface SUPlainInstaller : SUInstaller { }
-+ (void)installPath:(NSString *)path overHostBundle:(NSBundle *)bundle delegate:delegate;
++ (void)performInstallationWithPath:(NSString *)path hostBundle:(NSBundle *)hostBundle delegate:delegate synchronously:(BOOL)synchronously;
@end
+
+#endif
View
16 SUPlainInstaller.m
@@ -8,6 +8,9 @@
#import "SUPlainInstaller.h"
+extern NSString *SUInstallerPathKey;
+extern NSString *SUInstallerHostBundleKey;
+extern NSString *SUInstallerDelegateKey;
@implementation SUPlainInstaller
@@ -18,13 +21,22 @@ + (void)installPath:(NSString *)path overHostBundle:(NSBundle *)bundle delegate:
[self _finishInstallationWithResult:result hostBundle:bundle error:error delegate:delegate];
}
-+ (void)performInstallationWithInfo:(NSDictionary *)info
++ (void)_performInstallationWithInfo:(NSDictionary *)info
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-
+
[self installPath:[info objectForKey:SUInstallerPathKey] overHostBundle:[info objectForKey:SUInstallerHostBundleKey] delegate:[info objectForKey:SUInstallerDelegateKey]];
[pool release];
}
++ (void)performInstallationWithPath:(NSString *)path hostBundle:(NSBundle *)hostBundle delegate:delegate synchronously:(BOOL)synchronously;
+{
+ NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:path, SUInstallerPathKey, hostBundle, SUInstallerHostBundleKey, delegate, SUInstallerDelegateKey, nil];
+ if (synchronously)
+ [self _performInstallationWithInfo:info];
+ else
+ [NSThread detachNewThreadSelector:@selector(_performInstallationWithInfo:) toTarget:self withObject:info];
+}
+
@end
View
24 SUProbingUpdateDriver.h
@@ -0,0 +1,24 @@
+//
+// SUProbingUpdateDriver.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef SUPROBINGUPDATEDRIVER_H
+#define SUPROBINGUPDATEDRIVER_H
+
+#import <Cocoa/Cocoa.h>
+#import "SUBasicUpdateDriver.h"
+
+// This replaces the old SUStatusChecker.
+@interface SUProbingUpdateDriver : SUBasicUpdateDriver { }
+@end
+
+@interface NSObject (SUProbeDriverDelegateProtocol)
+- (void)didFindValidUpdate:(SUAppcastItem *)item toHostBundle:(NSBundle *)hb;
+- (void)didNotFindUpdateToHostBundle:(NSBundle *)hb;
+@end
+
+#endif
View
28 SUProbingUpdateDriver.m
@@ -0,0 +1,28 @@
+//
+// SUProbingUpdateDriver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "SUProbingUpdateDriver.h"
+
+
+@implementation SUProbingUpdateDriver
+
+- (void)didFindValidUpdate
+{
+ if ([delegate respondsToSelector:@selector(didFindValidUpdate:toHostBundle:)])
+ [delegate didFindValidUpdate:updateItem toHostBundle:hostBundle];
+ [self abortUpdate];
+}
+
+- (void)didNotFindUpdate
+{
+ if ([delegate respondsToSelector:@selector(didNotFindUpdateToHostBundle:)])
+ [delegate didNotFindUpdateToHostBundle:hostBundle];
+ [self abortUpdate];
+}
+
+@end
View
21 SUScheduledUpdateDriver.h
@@ -0,0 +1,21 @@
+//
+// SUScheduledUpdateDriver.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef SUSCHEDULEDUPDATEDRIVER_H
+#define SUSCHEDULEDUPDATEDRIVER_H
+
+#import <Cocoa/Cocoa.h>
+#import "SUUserInitiatedUpdateDriver.h"
+
+@interface SUScheduledUpdateDriver : SUUserInitiatedUpdateDriver {
+ BOOL showErrors;
+}
+
+@end
+
+#endif
View
39 SUScheduledUpdateDriver.m
@@ -0,0 +1,39 @@
+//
+// SUScheduledUpdateDriver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/6/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "SUScheduledUpdateDriver.h"
+#import "Sparkle.h"
+
+@implementation SUScheduledUpdateDriver
+
+- (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui
+{
+ // I wish I could think of a good way to not duplicate this code from SUBasicUpdateDriver, but inheritance makes it tricky.
+ return [self hostSupportsItem:ui] && [self isItemNewer:ui] && ![self itemContainsSkippedVersion:ui];
+}
+
+- (void)didFindValidUpdate
+{
+ showErrors = YES; // We only start showing errors after we present the UI for the first time.
+ [super didFindValidUpdate];
+}
+
+- (void)didNotFindUpdate
+{
+ [self abortUpdate]; // Don't tell the user that no update was found; this was a scheduled update.
+}
+
+- (void)abortUpdateWithError:(NSError *)error
+{
+ if (showErrors)
+ [super abortUpdateWithError:error];
+ else
+ [self abortUpdate];
+}
+
+@end
View
30 SUStatusChecker.h
@@ -1,30 +0,0 @@
-//
-// SUStatusChecker.h
-// Sparkle
-//
-// Created by Evan Schoenberg on 7/6/06.
-//
-
-#ifndef SUSTATUSCHECKER_H
-#define SUSTATUSCHECKER_H
-
-#import "SUUpdater.h"
-
-@class SUStatusChecker;
-
-@protocol SUStatusCheckerDelegate <NSObject>
-//versionString will be nil and isNewVersion will be NO if version checking fails.
-- (void)statusChecker:(SUStatusChecker *)statusChecker foundVersion:(NSString *)versionString isNewVersion:(BOOL)isNewVersion;
-@end
-
-@interface SUStatusChecker : SUUpdater {
- id<SUStatusCheckerDelegate> scDelegate;
-}
-
-// Create a status checker which will notifiy delegate once the appcast version is determined.
-// Notification occurs via the method defined in the SUStatusCheckerDelegate informal protocol.
-+ (SUStatusChecker *)statusCheckerForDelegate:(id<SUStatusCheckerDelegate>)delegate;
-
-@end
-
-#endif
View
78 SUStatusChecker.m
@@ -1,78 +0,0 @@
-//
-// SUStatusChecker.m
-// Sparkle
-//
-// Created by Evan Schoenberg on 7/6/06.
-//
-
-#import "Sparkle.h"
-#import "SUStatusChecker.h"
-
-@interface SUStatusChecker (Private)
-- (id)initForDelegate:(id<SUStatusCheckerDelegate>)inDelegate;
-- (BOOL)newVersionAvailable;
-@end;
-
-@implementation SUStatusChecker
-
-+ (SUStatusChecker *)statusCheckerForDelegate:(id<SUStatusCheckerDelegate>)inDelegate
-{
- SUStatusChecker *statusChecker = [[self alloc] initForDelegate:inDelegate];
-
- return [statusChecker autorelease];
-}
-
-- (id)initForDelegate:(id<SUStatusCheckerDelegate>)inDelegate
-{
- self = [super init];
- if (self)
- {
- scDelegate = [inDelegate retain];
-
- [self checkForUpdatesInBackground];
- }
-
- return self;
-}
-
-- (void)dealloc
-{
- [scDelegate release]; scDelegate = nil;
-
- [super dealloc];
-}
-
-- (void)applicationDidFinishLaunching:(NSNotification *)note
-{
- //Take no action when the application finishes launching
-}
-
-- (void)appcastDidFinishLoading:(SUAppcast *)ac
-{
- @try
- {
- if (!ac) { [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."]; }
-
- updateItem = [[ac newestItem] retain];
- [ac autorelease];
-
- if (![updateItem fileVersion])
- {
- [NSException raise:@"SUAppcastException" format:@"Can't extract a version string from the appcast feed. The filenames should look like YourApp_1.5.tgz, where 1.5 is the version number."];
- }
-
- [scDelegate statusChecker:self
- foundVersion:[updateItem fileVersion]
- isNewVersion:[self newVersionAvailable]];
- }
- @catch (NSException *e)
- {
- NSLog([e reason]);
-
- [scDelegate statusChecker:self foundVersion:nil isNewVersion:NO];
- }
-
- updateInProgress = NO;
-}
-
-@end
View
2  SUSystemProfiler.m
@@ -39,7 +39,7 @@ - (NSMutableArray *)systemProfileArrayForHostBundle:(NSBundle *)hostBundle
unsigned long length = sizeof(value) ;
// OS version
- NSString *currentSystemVersion = [SUUpdater systemVersionString];
+ NSString *currentSystemVersion = [NSWorkspace systemVersionString];
if (currentSystemVersion != nil)
[profileArray addObject:[NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:@"osVersion",@"OS Version",currentSystemVersion,currentSystemVersion,nil] forKeys:profileDictKeys]];
View
27 SUUpdateDriver.h
@@ -0,0 +1,27 @@
+//
+// SUUpdateDriver.h
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#ifndef SUUPDATEDRIVER_H
+#define SUUPDATEDRIVER_H
+
+#import <Cocoa/Cocoa.h>
+
+@interface SUUpdateDriver : NSObject
+{
+ BOOL finished;
+ id delegate;
+}
+- (void)checkForUpdatesAtURL:(NSURL *)appcastURL hostBundle:(NSBundle *)hb;
+- (void)abortUpdate;
+- (BOOL)finished;
+
+- delegate;
+- (void)setDelegate:delegate;
+@end
+
+#endif
View
26 SUUpdateDriver.m
@@ -0,0 +1,26 @@
+//
+// SUUpdateDriver.m
+// Sparkle
+//
+// Created by Andy Matuschak on 5/7/08.
+// Copyright 2008 Andy Matuschak. All rights reserved.
+//
+
+#import "SUUpdateDriver.h"
+
+
+@implementation SUUpdateDriver
+- (void)checkForUpdatesAtURL:(NSURL *)appcastURL hostBundle:(NSBundle *)hb
+{
+ [NSException raise:@"SUAbstractDriverError" format:@"Don't use SUUpdateDriver directly; use a subclass."];
+}
+
+- (void)abortUpdate
+{
+ [self setValue:[NSNumber numberWithBool:YES] forKey:@"finished"];
+}
+
+- (BOOL)finished { return finished; }
+- delegate { return delegate; }
+- (void)setDelegate:del { delegate = del; }
+@end
View
4 SUUpdatePermissionPrompt.h
@@ -12,8 +12,8 @@
#import "Sparkle.h"
typedef enum {
- SUAutomaticallyUpdate,
- SUDoNotAutomaticallyUpdate
+ SUAutomaticallyCheck,
+ SUDoNotAutomaticallyCheck
} SUPermissionPromptResult;
@interface SUUpdatePermissionPrompt : SUWindowController {
View
2  SUUpdatePermissionPrompt.m
@@ -117,7 +117,7 @@ - (IBAction)finishPrompt:(id)sender
if (![delegate respondsToSelector:@selector(updatePermissionPromptFinishedWithResult:)])
[NSException raise:@"SUInvalidDelegate" format:@"SUUpdatePermissionPrompt's delegate (%@) doesn't respond to updatePermissionPromptFinishedWithResult:!", delegate];
[[SUUserDefaults standardUserDefaults] setBool:shouldSendProfile forKey:SUSendProfileInfoKey];
- [delegate updatePermissionPromptFinishedWithResult:([sender tag] == 1 ? SUAutomaticallyUpdate : SUDoNotAutomaticallyUpdate)];
+ [delegate updatePermissionPromptFinishedWithResult:([sender tag] == 1 ? SUAutomaticallyCheck : SUDoNotAutomaticallyCheck)];
[[self window] close];
[NSApp stopModal];
[self autorelease];
View
39 SUUpdater.h
@@ -9,32 +9,14 @@
#ifndef SUUPDATER_H
#define SUUPDATER_H
-// Before you use Sparkle in your app, you must set SUFeedURL in Info.plist to the
-// address of the appcast on your webserver. If you don't already have an
-// appcast, please see the Sparkle documentation to learn about how to set one up.
-
-// .zip, .dmg, .tar, .tbz, .tgz archives are supported at this time.
-
-@class SUAppcastItem, SUUpdateAlert, SUStatusController, SUUnarchiver;
+@class SUUpdateDriver;
@interface SUUpdater : NSObject {
- SUAppcastItem *updateItem;
-
- SUStatusController *statusController;
- SUUpdateAlert *updateAlert;
-
- NSURLDownload *downloader;
- NSString *downloadPath;
-
NSTimer *checkTimer;
- NSTimeInterval checkInterval;
-
- BOOL userInitiated;
- BOOL updateInProgress;
+ NSTimeInterval checkInterval;
+ SUUpdateDriver *driver;
NSBundle *hostBundle;
id delegate;
-
- SUUnarchiver *unarchiver;
}
- (void)setHostBundle:(NSBundle *)hostBundle;
@@ -44,19 +26,10 @@
// and Sparkle will check for updates and report back its findings verbosely.
- (IBAction)checkForUpdates:sender;
-// This method is similar to the above, but it's intended for updates initiated by
-// the computer instead of by the user. It does not alert the user when he is up to date,
-// and it remains silent about network errors in fetching the feed. This is what you
-// want to call to update programmatically; only use checkForUpdates: with buttons and menu items.
-- (void)checkForUpdatesInBackground;
-
-// This method allows you to schedule a check to run every time interval. You probably don't want
-// to call this directly: if you set a SUScheduledCheckInterval key in Info.plist or
-// the user defaults, Sparkle will set this up for you automatically on startup. You might
-// just want to call this every time the user changes the setting in the preferences.
-- (void)scheduleCheckWithInterval:(NSTimeInterval)interval;
+// This forces an update to begin with a particular driver (see SU*UpdateDriver.h)
+- (void)checkForUpdatesWithDriver:(SUUpdateDriver *)driver;
-+ (NSString *)systemVersionString; // Returns the current system's version.
+- (BOOL)updateInProgress;
@end
View
504 SUUpdater.m
@@ -9,21 +9,10 @@
#import "Sparkle.h"
#import "SUUpdater.h"
-#import <stdio.h>
-#import <sys/stat.h>
-#import <unistd.h>
-#import <signal.h>
-#import <dirent.h>
-
@interface SUUpdater (Private)
-- (void)beginUpdateCheck;
- (void)beginUpdateCycle;
-- (void)showUpdateAlert;
-- (void)beginDownload;
-- (void)extractUpdate;
-- (void)showUpdateErrorAlertWithInfo:(NSString *)info;
-- (void)abandonUpdate;
-- (IBAction)installAndRestart:sender;
+- (NSArray *)feedParameters;
+- (BOOL)automaticallyUpdates;
@end
@implementation SUUpdater
@@ -58,15 +47,6 @@ - (id)init
return self;
}
-- (void)setHostBundle:(NSBundle *)hb
-{
- [hostBundle release];
- hostBundle = [hb retain];
- [[SUUserDefaults standardUserDefaults] setIdentifier:[hostBundle bundleIdentifier]];
-}
-
-#pragma mark Automatic check support
-
- (void)applicationDidFinishLaunching:(NSNotification *)note
{
// If the user has been asked about automatic checks and said no, get out of here.
@@ -79,12 +59,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)note
// Now, we don't want to ask the user for permission to do a weird thing on the first launch.
// We wait until the second launch.
if ([[SUUserDefaults standardUserDefaults] boolForKey:SUHasLaunchedBeforeKey] == NO)
- {
[[SUUserDefaults standardUserDefaults] setBool:YES forKey:SUHasLaunchedBeforeKey];
- return;
- }
-
- [SUUpdatePermissionPrompt promptWithHostBundle:hostBundle delegate:self];
+ else
+ [SUUpdatePermissionPrompt promptWithHostBundle:hostBundle delegate:self];
}
if ([[SUUserDefaults standardUserDefaults] boolForKey:SUEnableAutomaticChecksKey] == YES)
@@ -93,9 +70,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)note
- (void)updatePermissionPromptFinishedWithResult:(SUPermissionPromptResult)result
{
- BOOL automaticallyUpdate = (result == SUAutomaticallyUpdate);
- [[SUUserDefaults standardUserDefaults] setBool:(result == SUAutomaticallyUpdate) forKey:SUEnableAutomaticChecksKey];
- if (automaticallyUpdate)
+ BOOL automaticallyCheck = (result == SUAutomaticallyCheck);
+ [[SUUserDefaults standardUserDefaults] setBool:automaticallyCheck forKey:SUEnableAutomaticChecksKey];
+ if ([self automaticallyUpdates])
[self beginUpdateCycle];
}
@@ -122,57 +99,51 @@ - (void)beginUpdateCycle
else
delayUntilCheck = 0; // We're overdue! Run one now.
- [self performSelector:@selector(checkForUpdatesInBackground) withObject:nil afterDelay:delayUntilCheck];
- [self performSelector:@selector(scheduleCheckWithIntervalObject:) withObject:[NSNumber numberWithDouble:checkInterval] afterDelay:delayUntilCheck];
-}
-
-- (void)scheduleCheckWithInterval:(NSTimeInterval)interval
-{
- if (checkTimer)
- {
- [checkTimer invalidate];
- checkTimer = nil;
- }
-
- checkInterval = interval;
- if (interval > 0)
- checkTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:YES];
+ checkTimer = [NSTimer scheduledTimerWithTimeInterval:delayUntilCheck target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:NO];
}
-// An unfortunate necessity in order to use performSelector:withObject:afterDelay:
-- (void)scheduleCheckWithIntervalObject:(NSNumber *)interval
+- (void)checkForUpdatesInBackground
{
- [self scheduleCheckWithInterval:[interval doubleValue]];
+ [self checkForUpdatesWithDriver:[[[([self automaticallyUpdates] ? [SUAutomaticUpdateDriver class] : [SUScheduledUpdateDriver class]) alloc] init] autorelease]];
}
-- (void)checkForUpdatesInBackground
+- (IBAction)checkForUpdates:sender
{
- userInitiated = NO;
- [self beginUpdateCheck];
+ [self checkForUpdatesWithDriver:[[[SUUserInitiatedUpdateDriver alloc] init] autorelease]];
}
-- (IBAction)checkForUpdates:sender
+- (void)checkForUpdatesWithDriver:(SUUpdateDriver *)d
{
- userInitiated = YES;
- [self beginUpdateCheck];
+ if ([self updateInProgress]) { return; }
+ if (checkTimer) { [checkTimer invalidate]; checkTimer = nil; }
+
+ // A value in the user defaults overrides one in the Info.plist (so preferences panels can be created wherein users choose between beta / release feeds).
+ NSString *appcastString = [[SUUserDefaults standardUserDefaults] objectForKey:SUFeedURLKey];
+ if (!appcastString)
+ appcastString = [hostBundle objectForInfoDictionaryKey:SUFeedURLKey];
+ if (!appcastString)
+ [NSException raise:@"SUNoFeedURL" format:@"You must specify the URL of the appcast as the SUFeedURLKey in either the Info.plist or the user defaults!"];
+ NSCharacterSet* quoteSet = [NSCharacterSet characterSetWithCharactersInString: @"\"\'"]; // Some feed publishers add quotes; strip 'em.
+ NSURL *feedURL = [[NSURL URLWithString:[appcastString stringByTrimmingCharactersInSet:quoteSet]] URLWithParameters:[self feedParameters]];
+
+ driver = [d retain];
+ if ([driver delegate] == nil) { [driver setDelegate:delegate]; }
+ [driver addObserver:self forKeyPath:@"finished" options:0 context:NULL];
+ [driver checkForUpdatesAtURL:feedURL hostBundle:hostBundle];
}
-- (BOOL)validateMenuItem:(NSMenuItem *)item
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
- if ([item action] == @selector(checkForUpdates:))
- {
- if (updateInProgress)
- return NO;
- else
- return YES;
- }
- return YES;
+ if (object != driver) { return; }
+ [driver release]; driver = nil;
+ [NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:NO];
}
- (BOOL)automaticallyUpdates
{
// If the SUAllowsAutomaticUpdatesKey exists and is set to NO, return NO.
- if ([[hostBundle objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] boolValue] == NO)
+ if ([hostBundle objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] &&
+ [[hostBundle objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey] boolValue] == NO)
return NO;
// If we're not using DSA signatures, we aren't going to trust any updates automatically.
@@ -186,13 +157,6 @@ - (BOOL)automaticallyUpdates
return YES; // Otherwise, we're good to go.
}
-- (BOOL)isAutomaticallyUpdating
-{
- return [self automaticallyUpdates] && !userInitiated;
-}
-
-#pragma mark Appcast-fetching phase
-
- (NSArray *)feedParameters
{
BOOL sendingSystemProfile = ([[SUUserDefaults standardUserDefaults] boolForKey:SUSendProfileInfoKey] == YES);
@@ -204,397 +168,19 @@ - (NSArray *)feedParameters
return parameters;
}
-- (void)beginUpdateCheck
-{
- if ([hostBundle isRunningFromDiskImage])
- {
- if (userInitiated)
- [self showUpdateErrorAlertWithInfo:[NSString stringWithFormat:SULocalizedString(@"%1$@ can't be updated when it's running from a disk image. Move %1$@ to your Applications folder, relaunch it, and try again.", nil), [hostBundle name]]];
- return;
- }
-
- if (updateInProgress) { return; }
- updateInProgress = YES;
-
- // A value in the user defaults overrides one in the Info.plist (so preferences panels can be created wherein users choose between beta / release feeds).
- NSString *appcastString = [[SUUserDefaults standardUserDefaults] objectForKey:SUFeedURLKey]; // if URL is quoted (because of pre-processing in plist files), remove quotes from the string
- if (!appcastString)
- appcastString = [hostBundle objectForInfoDictionaryKey:SUFeedURLKey];
- if (!appcastString)
- [NSException raise:@"SUNoFeedURL" format:@"You must specify the URL of the appcast as the SUFeedURLKey in either the Info.plist or the user defaults!"];
-
- NSCharacterSet* quoteSet = [NSCharacterSet characterSetWithCharactersInString: @"\"\'"];
- SUAppcast *appcast = [[SUAppcast alloc] init];
- [appcast setDelegate:self];
- [appcast fetchAppcastFromURL:[NSURL URLWithString:[appcastString stringByTrimmingCharactersInSet:quoteSet]] parameters:[self feedParameters]];
-}
-
-- (BOOL)newVersionAvailable
-{
- // We also have to make sure that the newest version can run on the user's system.
- id <SUVersionComparison> comparator = [SUStandardVersionComparator defaultComparator];
- BOOL canRunOnCurrentSystem = ([comparator compareVersion:[updateItem minimumSystemVersion] toVersion:[SUUpdater systemVersionString]] != NSOrderedDescending);
- return (canRunOnCurrentSystem && ([comparator compareVersion:[hostBundle version] toVersion:[updateItem fileVersion]]) == NSOrderedAscending);
-}
-
-- (void)appcastDidFinishLoading:(SUAppcast *)ac
-{
- @try
- {
- if (!ac)
- [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."];
-
- updateItem = [[ac newestItem] retain];
- [ac autorelease];
-
- // Record the time of the check for host app use and for interval checks on startup.
- [[SUUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:SULastCheckTimeKey];
-
- if (![updateItem fileVersion])
- {
- [NSException raise:@"SUAppcastException" format:@"Can't extract a version string from the appcast feed. The filenames should look like YourApp_1.5.tgz, where 1.5 is the version number."];
- }
-
- if (!userInitiated && [[[SUUserDefaults standardUserDefaults] objectForKey:SUSkippedVersionKey] isEqualToString:[updateItem fileVersion]]) { [self abandonUpdate]; return; }
-
- if ([self newVersionAvailable])
- {
- if (checkTimer) // There's a new version! Let's disable the automated checking timer.
- {
- [checkTimer invalidate];
- checkTimer = nil;
- }
-
- if ([self isAutomaticallyUpdating])
- [self beginDownload];
- else
- [self showUpdateAlert];
- }
- else
- {
- if (userInitiated)
- {
- NSAlert *alert = [NSAlert alertWithMessageText:SULocalizedString(@"You're up to date!", nil) defaultButton:SULocalizedString(@"OK", nil) alternateButton:nil otherButton:nil informativeTextWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), [hostBundle name], [hostBundle displayVersion]];
- [alert setIcon:[hostBundle icon]];
- [alert runModal];
- }
- [self abandonUpdate];
- }
- }
- @catch (NSException *e)
- {
- [self abandonUpdate];
- if (userInitiated)
- {
- NSLog([e reason]);
- [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)];
- }
- }
-}
-
-- (NSString *)userAgentForAppcast:(SUAppcast *)ac
-{
- return [NSString stringWithFormat: @"%@/%@ Sparkle/1.5b1", [hostBundle name], [hostBundle displayVersion]];
-}
-
-- (void)appcastDidFailToLoad:(SUAppcast *)ac
-{
- [ac autorelease];
- updateInProgress = NO;
- if (userInitiated)
- [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)];
-}
-
-#pragma mark The update alert phase
-
-- (void)showUpdateAlert
-{
- updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem hostBundle:hostBundle];
- [updateAlert setDelegate:self];
-
- // Only show the update alert if the app is active; otherwise, we'll wait until it is.
- if ([NSApp isActive])
- [updateAlert showWindow:self];
- else
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"NSApplicationDidBecomeActiveNotification" object:NSApp];
-}
-
-- (void)applicationDidBecomeActive:(NSNotification *)aNotification
-{
- // We don't want to display the update alert until the application becomes active.
- [updateAlert showWindow:self];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:@"NSApplicationDidBecomeActiveNotification" object:NSApp];
-}
-
-- (void)updateAlert:(SUUpdateAlert *)alert finishedWithChoice:(SUUpdateAlertChoice)choice
-{
- [alert release];
- updateAlert = nil;
- switch (choice)
- {
- case SUInstallUpdateChoice:
- // Clear out the skipped version so the dialog will come back if the download fails.
- [[SUUserDefaults standardUserDefaults] setObject:nil forKey:SUSkippedVersionKey];
- [self beginDownload];
- break;
-
- case SURemindMeLaterChoice:
- // Clear out the skipped version so the dialog will actually come back if it was already skipped.
- [[SUUserDefaults standardUserDefaults] setObject:nil forKey:SUSkippedVersionKey];
- [self abandonUpdate];
- break;
-
- case SUSkipThisVersionChoice:
- [[SUUserDefaults standardUserDefaults] setObject:[updateItem fileVersion] forKey:SUSkippedVersionKey];
- [self abandonUpdate];
- break;
- }
-}
-
-#pragma mark The downloading phase
-
-- (void)beginDownload
-{
- statusController = [[SUStatusController alloc] initWithHostBundle:hostBundle];
- [statusController beginActionWithTitle:SULocalizedString(@"Downloading update...", nil) maxProgressValue:0 statusText:nil];
- [statusController setButtonTitle:SULocalizedString(@"Cancel", nil) target:self action:@selector(cancelDownload:) isDefault:NO];
- if ([self isAutomaticallyUpdating] == NO)
- [statusController showWindow:self];
-
- downloader = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self];
-}
-
-- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response
-{
- [statusController setMaxProgressValue:[response expectedContentLength]];
-}
-
-- (void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)name
-{
- // If name ends in .txt, the server probably has a stupid MIME configuration. We'll give
- // the developer the benefit of the doubt and chop that off.
- if ([[name pathExtension] isEqualToString:@"txt"])
- name = [name stringByDeletingPathExtension];
-
- // We create a temporary directory in /tmp and stick the file there.
- // Not using a GUID here because hdiutil for some reason chokes on GUIDs. Too long? I really have no idea.
- NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"update"];
- int cnt=1;
- while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999)
- tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"update%d", cnt++]];
- BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil];
- if (!success)
- {
- [NSException raise:@"SUFailTmpWrite" format:@"Couldn't create temporary directory at %@", tempDir];
- [download cancel];
- [download release];
- }
-
- [downloadPath autorelease];
- downloadPath = [[tempDir stringByAppendingPathComponent:name] retain];
- [download setDestination:downloadPath allowOverwrite:YES];
-}
-
-- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length
-{
- [statusController setProgressValue:[statusController progressValue] + length];
- [statusController setStatusText:[NSString stringWithFormat:SULocalizedString(@"%.0lfk of %.0lfk", nil), [statusController progressValue] / 1024.0, [statusController maxProgressValue] / 1024.0]];
-}
-
-- (IBAction)cancelDownload:sender
-{
- if (downloader)
- {
- [downloader cancel];
- [downloader release];
- }
- [self abandonUpdate];
-}
-
-- (void)downloadDidFinish:(NSURLDownload *)download
-{
- [download release];
- downloader = nil;
- [self extractUpdate];
-}
-
-- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
-{
- [self abandonUpdate];
-
- NSLog(@"Download error: %@", [error localizedDescription]);
- [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while trying to download the file. Please try again later.", nil)];
-}
-
-#pragma mark Extraction phase
-
-- (void)unarchiver:(SUUnarchiver *)ua extractedLength:(long)length
-{
- if ([statusController maxProgressValue] == 0)
- [statusController setMaxProgressValue:[[[[NSFileManager defaultManager] fileAttributesAtPath:downloadPath traverseLink:NO] objectForKey:NSFileSize] longValue]];