source: trunk/Sparkle/SUBasicUpdateDriver.m @ 1046

Revision 992, 14.0 KB checked in by speck, 13 months ago (diff)

Prefer CFBundleShortVersionString from LS in Sparkle info. Fix bugs in Sparkle that made it send semi-garbled UA string.

Line 
1//
2//  SUBasicUpdateDriver.m
3//  Sparkle
4//
5//  Created by Andy Matuschak on 4/23/08.
6//  Copyright 2008 Andy Matuschak. All rights reserved.
7//
8
9#import "SUBasicUpdateDriver.h"
10
11#import "SUHost.h"
12#import "SUDSAVerifier.h"
13#import "SUInstaller.h"
14#import "SUStandardVersionComparator.h"
15#import "SUUnarchiver.h"
16
17@implementation SUBasicUpdateDriver
18
19- (void)checkForUpdatesAtURL:(NSURL *)URL host:(SUHost *)aHost
20{   
21    [super checkForUpdatesAtURL:URL host:aHost];
22    if ([aHost isRunningOnReadOnlyVolume])
23    {
24        [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURunningFromDiskImageError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:SULocalizedString(@"%1$@ can't be updated when it's running from a read-only volume like a disk image or an optical drive. Move %1$@ to your Applications folder, relaunch it from there, and try again.", nil), [aHost name]] forKey:NSLocalizedDescriptionKey]]];
25        return;
26    }   
27   
28    SUAppcast *appcast = [[SUAppcast alloc] init];
29    CFRetain(appcast); // We'll manage the appcast's memory ourselves so we don't have to make it an IV to support GC.
30    [appcast release];
31   
32    [appcast setDelegate:self];
33    NSString *vers = [SPARKLE_BUNDLE objectForInfoDictionaryKey:@"CFBundleVersion"];
34    NSString *userAgent = [NSString stringWithFormat: @"%@/%@ Sparkle/%@", [aHost name], [aHost displayVersion], ([vers length] ? vers : @"0")];
35    NSData * cleanedAgent = [userAgent dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
36    userAgent = [NSString stringWithCString:[cleanedAgent bytes] encoding:NSASCIIStringEncoding];
37    [appcast setUserAgentString:userAgent];
38    [appcast fetchAppcastFromURL:URL];
39}
40
41- (id <SUVersionComparison>)_versionComparator
42{
43    id <SUVersionComparison> comparator = nil;
44   
45    // Give the delegate a chance to provide a custom version comparator
46    if ([[updater delegate] respondsToSelector:@selector(versionComparatorForUpdater:)])
47        comparator = [[updater delegate] versionComparatorForUpdater:updater];
48   
49    // If we don't get a comparator from the delegate, use the default comparator
50    if (!comparator)
51        comparator = [SUStandardVersionComparator defaultComparator];
52   
53    return comparator; 
54}
55
56- (BOOL)isItemNewer:(SUAppcastItem *)ui
57{
58    return [[self _versionComparator] compareVersion:[host version] toVersion:[ui versionString]] == NSOrderedAscending;
59}
60
61- (BOOL)hostSupportsItem:(SUAppcastItem *)ui
62{
63    if ([ui minimumSystemVersion] == nil || [[ui minimumSystemVersion] isEqualToString:@""]) { return YES; }
64    return [[SUStandardVersionComparator defaultComparator] compareVersion:[ui minimumSystemVersion] toVersion:[SUHost systemVersionString]] != NSOrderedDescending;
65}
66
67- (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui
68{
69    NSString *skippedVersion = [host objectForUserDefaultsKey:SUSkippedVersionKey];
70    if (skippedVersion == nil) { return NO; }
71    return [[self _versionComparator] compareVersion:[ui versionString] toVersion:skippedVersion] != NSOrderedDescending;
72}
73
74- (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui
75{
76    return [self hostSupportsItem:ui] && [self isItemNewer:ui] && ![self itemContainsSkippedVersion:ui];
77}
78
79- (void)appcastDidFinishLoading:(SUAppcast *)ac
80{
81    if ([[updater delegate] respondsToSelector:@selector(updater:didFinishLoadingAppcast:)])
82        [[updater delegate] updater:updater didFinishLoadingAppcast:ac];
83   
84    SUAppcastItem *item = nil;
85   
86    // Now we have to find the best valid update in the appcast.
87    if ([[updater delegate] respondsToSelector:@selector(bestValidUpdateInAppcast:forUpdater:)]) // Does the delegate want to handle it?
88    {
89        item = [[updater delegate] bestValidUpdateInAppcast:ac forUpdater:updater];
90    }
91    else // If not, we'll take care of it ourselves.
92    {
93        // Find the first update we can actually use.
94        NSEnumerator *updateEnumerator = [[ac items] objectEnumerator];
95        do {
96            item = [updateEnumerator nextObject];
97        } while (item && ![self hostSupportsItem:item]);
98    }
99   
100    updateItem = [item retain];
101    CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
102    if (updateItem == nil) { [self didNotFindUpdate]; return; }
103   
104    if ([self itemContainsValidUpdate:updateItem])
105        [self didFindValidUpdate];
106    else
107        [self didNotFindUpdate];
108}
109
110- (void)appcast:(SUAppcast *)ac failedToLoadWithError:(NSError *)error
111{
112    CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
113    [self abortUpdateWithError:error];
114}
115
116- (void)didFindValidUpdate
117{
118    if ([[updater delegate] respondsToSelector:@selector(updater:didFindValidUpdate:)])
119        [[updater delegate] updater:updater didFindValidUpdate:updateItem];
120    [self downloadUpdate];
121}
122
123- (void)didNotFindUpdate
124{
125    if ([[updater delegate] respondsToSelector:@selector(updaterDidNotFindUpdate:)])
126        [[updater delegate] updaterDidNotFindUpdate:updater];
127    [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUNoUpdateError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:SULocalizedString(@"You already have the newest version of %@.", nil), [host name]] forKey:NSLocalizedDescriptionKey]]];
128}
129
130- (void)downloadUpdate
131{
132    download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self];   
133}
134
135- (void)download:(NSURLDownload *)d decideDestinationWithSuggestedFilename:(NSString *)name
136{
137    // 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.
138    if ([[name pathExtension] isEqualToString:@"txt"])
139        name = [name stringByDeletingPathExtension];
140   
141    // We create a temporary directory in /tmp and stick the file there.
142    // Not using a GUID here because hdiutil for some reason chokes on GUIDs. Too long? I really have no idea.
143    NSString *prefix = [NSString stringWithFormat:@"%@ %@ Update", [host name], [host version]];
144    NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:prefix];
145    int cnt=1;
146    while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999)
147        tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d", prefix, cnt++]];
148    BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil];
149    if (!success)
150    {
151        // Okay, something's really broken with /tmp
152        [download cancel];
153        [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]]];
154    }
155   
156    downloadPath = [[tempDir stringByAppendingPathComponent:name] retain];
157    [download setDestination:downloadPath allowOverwrite:YES];
158}
159
160- (void)downloadDidFinish:(NSURLDownload *)d
161{
162    // New in Sparkle 1.5: we're now checking signatures on all non-secure downloads, where "secure" is defined as both the appcast and the download being transmitted over SSL.
163    NSURL *downloadURL = [[d request] URL];
164    if (!([[downloadURL scheme] isEqualToString:@"https"] && [[appcastURL scheme] isEqualToString:@"https"]) || !([downloadURL isFileURL] && [appcastURL isFileURL]) || [host publicDSAKey])
165    {
166        if (![SUDSAVerifier validatePath:downloadPath withEncodedDSASignature:[updateItem DSASignature] withPublicDSAKey:[host publicDSAKey]])
167        {
168            [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUSignatureError userInfo:[NSDictionary dictionaryWithObject:@"The update is improperly signed." forKey:NSLocalizedDescriptionKey]]];
169            return;
170        }
171    }
172   
173    [self extractUpdate];
174}
175
176- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
177{
178    // Get rid of what we've downloaded so far, if anything.
179    if (downloadPath != nil)
180        [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[downloadPath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[downloadPath lastPathComponent]] tag:NULL];
181    [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while downloading the update. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]];
182}
183
184- (BOOL)download:(NSURLDownload *)download shouldDecodeSourceDataOfMIMEType:(NSString *)encodingType
185{
186    // We don't want the download system to extract our gzips.
187    // Note that we use a substring matching here instead of direct comparison because the docs say "application/gzip" but the system *uses* "application/x-gzip". This is a documentation bug.
188    return ([encodingType rangeOfString:@"gzip"].location == NSNotFound);
189}
190
191- (void)extractUpdate
192{   
193    SUUnarchiver *unarchiver = [SUUnarchiver unarchiverForPath:downloadPath];
194    if (!unarchiver)
195    {
196        NSLog(@"Sparkle Error: No valid unarchiver for %@!", downloadPath);
197        [self unarchiverDidFail:nil];
198        return;
199    }
200    CFRetain(unarchiver); // Manage this memory manually so we don't have to make it an IV.
201    [unarchiver setDelegate:self];
202    [unarchiver start];
203}
204
205- (void)unarchiverDidFinish:(SUUnarchiver *)ua
206{
207    if (ua) { CFRelease(ua); }
208    [self installUpdate];
209}
210
211- (void)unarchiverDidFail:(SUUnarchiver *)ua
212{
213    if (ua) { CFRelease(ua); }
214    [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]]];
215}
216
217- (BOOL)shouldInstallSynchronously { return NO; }
218
219- (void)installUpdate
220{
221    if ([[updater delegate] respondsToSelector:@selector(updater:willInstallUpdate:)])
222        [[updater delegate] updater:updater willInstallUpdate:updateItem];
223    // Copy the relauncher into a temporary directory so we can get to it after the new version's installed.
224    NSString *relaunchPathToCopy = [[NSBundle bundleForClass:[self class]]  pathForResource:@"relaunch" ofType:@""];
225    NSString *targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[relaunchPathToCopy lastPathComponent]];
226    // Only the paranoid survive: if there's already a stray copy of relaunch there, we would have problems
227    [[NSFileManager defaultManager] removeFileAtPath:targetPath handler:nil];
228    if ([[NSFileManager defaultManager] copyPath:relaunchPathToCopy toPath:targetPath handler:nil])
229        relaunchPath = [targetPath retain];
230   
231    [SUInstaller installFromUpdateFolder:[downloadPath stringByDeletingLastPathComponent] overHost:host delegate:self synchronously:[self shouldInstallSynchronously] versionComparator:[self _versionComparator] updater:updater];
232}
233
234- (void)installerFinishedForHost:(SUHost *)aHost
235{
236    if (aHost != host) { return; }
237    [self relaunchHostApp];
238}
239
240- (void)relaunchHostApp
241{
242    // Give the host app an opportunity to postpone the relaunch.
243    static BOOL postponedOnce = NO;
244    if (!postponedOnce && [[updater delegate] respondsToSelector:@selector(updater:shouldPostponeRelaunchForUpdate:untilInvoking:)])
245    {
246        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[[self class] instanceMethodSignatureForSelector:@selector(relaunchHostApp)] retain]];
247        [invocation setSelector:@selector(relaunchHostApp)];
248        [invocation setTarget:self];
249        postponedOnce = YES;
250        if ([[updater delegate] updater:updater shouldPostponeRelaunchForUpdate:updateItem untilInvoking:invocation])
251            return;
252    }
253
254    [self cleanUp]; // Clean up the download and extracted files.
255   
256    [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self];
257    if ([[updater delegate] respondsToSelector:@selector(updaterWillRelaunchApplication:)])
258        [[updater delegate] updaterWillRelaunchApplication:updater];
259   
260    if(!relaunchPath || ![[NSFileManager defaultManager] fileExistsAtPath:relaunchPath])
261    {
262        // Note that we explicitly use the host app's name here, since updating plugin for Mail relaunches Mail, not just the plugin.
263        [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), [host name]], NSLocalizedDescriptionKey, [NSString stringWithFormat:@"Couldn't find the relauncher (expected to find it at %@)", relaunchPath], NSLocalizedFailureReasonErrorKey, nil]]];
264        // We intentionally don't abandon the update here so that the host won't initiate another.
265        return;
266    }       
267   
268    NSString *pathToRelaunch = [host bundlePath];
269    if ([[updater delegate] respondsToSelector:@selector(pathToRelaunchForUpdater:)])
270        pathToRelaunch = [[updater delegate] pathToRelaunchForUpdater:updater];
271    [NSTask launchedTaskWithLaunchPath:relaunchPath arguments:[NSArray arrayWithObjects:pathToRelaunch, [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]], nil]];
272
273    [NSApp terminate:self];
274}
275
276- (void)cleanUp
277{
278    [[NSFileManager defaultManager] removeFileAtPath:[downloadPath stringByDeletingLastPathComponent] handler:nil];
279}
280
281- (void)installerForHost:(SUHost *)aHost failedWithError:(NSError *)error
282{
283    if (aHost != host) { return; }
284    [[NSFileManager defaultManager] removeFileAtPath:relaunchPath handler:NULL]; // Clean up the copied relauncher.
285    [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]]];
286}
287
288- (void)abortUpdate
289{
290    [[NSNotificationCenter defaultCenter] removeObserver:self];
291    [super abortUpdate];
292}
293
294- (void)abortUpdateWithError:(NSError *)error
295{
296    if ([error code] != SUNoUpdateError) // Let's not bother logging this.
297        NSLog(@"Sparkle Error: %@", [error localizedDescription]);
298    if ([error localizedFailureReason])
299        NSLog(@"Sparkle Error (continued): %@", [error localizedFailureReason]);
300    if (download)
301        [download cancel];
302    [self abortUpdate];
303}
304
305- (void)dealloc
306{
307    [updateItem release];
308    [download release];
309    [downloadPath release];
310    [relaunchPath release];
311    [super dealloc];
312}
313
314@end
Note: See TracBrowser for help on using the repository browser.