Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 735 lines (633 sloc) 27.484 kB
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
1 //
2 // SUUpdater.m
3 // Sparkle
4 //
5 // Created by Andy Matuschak on 1/4/06.
6 // Copyright 2006 Andy Matuschak. All rights reserved.
7 //
8
9 #import "SUUpdater.h"
10 #import "SUAppcast.h"
11 #import "SUAppcastItem.h"
12 #import "SUUnarchiver.h"
13 #import "SUUtilities.h"
14
15 #import "SUUpdateAlert.h"
16 #import "SUAutomaticUpdateAlert.h"
17 #import "SUStatusController.h"
18
19 #import "NSFileManager+Authentication.h"
20 #import "NSFileManager+Verification.h"
21 #import "NSApplication+AppCopies.h"
22
23 #import <stdio.h>
24 #import <sys/stat.h>
25 #import <unistd.h>
26 #import <signal.h>
27 #import <dirent.h>
28
29 @interface SUUpdater (Private)
30 - (void)checkForUpdatesAndNotify:(BOOL)verbosity;
31 - (void)showUpdateErrorAlertWithInfo:(NSString *)info;
32 - (NSTimeInterval)storedCheckInterval;
33 - (void)abandonUpdate;
34 - (IBAction)installAndRestart:sender;
35 - (NSString *)systemVersionString;
36 @end
37
38 @implementation SUUpdater
39
40 - init
41 {
5e05339 #76 Support for Plug-ins
catlan authored
42 self = [super init];
43
44 updateBundle = [[NSBundle mainBundle] retain];
45 utilities = [[SUUtilities alloc] initWithUpdater:self];
46
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
47 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:@"NSApplicationDidFinishLaunchingNotification" object:NSApp];
48
49 // OS version (Apple recommends using SystemVersion.plist instead of Gestalt() here, don't ask me why).
50 // This code *should* use NSSearchPathForDirectoriesInDomains(NSCoreServiceDirectory, NSSystemDomainMask, YES)
51 // but that returns /Library/CoreServices for some reason
52 NSString *versionPlistPath = @"/System/Library/CoreServices/SystemVersion.plist";
53 //gets a version string of the form X.Y.Z
54 currentSystemVersion = [[[NSDictionary dictionaryWithContentsOfFile:versionPlistPath] objectForKey:@"ProductVersion"] retain];
5e05339 #76 Support for Plug-ins
catlan authored
55
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
56 return self;
57 }
58
5e05339 #76 Support for Plug-ins
catlan authored
59
60 - (id)initWithBundle:(NSBundle *)bundle
61 {
62 self = [super init];
63 if (self != nil) {
64 updateBundle = [bundle retain];
65 utilities = [[SUUtilities alloc] initWithUpdater:self];
66
67 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:@"NSApplicationDidFinishLaunchingNotification" object:NSApp];
68
69 // OS version (Apple recommends using SystemVersion.plist instead of Gestalt() here, don't ask me why).
70 // This code *should* use NSSearchPathForDirectoriesInDomains(NSCoreServiceDirectory, NSSystemDomainMask, YES)
71 // but that returns /Library/CoreServices for some reason
72 NSString *versionPlistPath = @"/System/Library/CoreServices/SystemVersion.plist";
73 //gets a version string of the form X.Y.Z
74 currentSystemVersion = [[[NSDictionary dictionaryWithContentsOfFile:versionPlistPath] objectForKey:@"ProductVersion"] retain];
75 }
76 return self;
77 }
78
79 - (NSBundle *)updateBundle
80 {
81 return [[updateBundle retain] autorelease];
82 }
83
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
84 - (void)scheduleCheckWithInterval:(NSTimeInterval)interval
85 {
86 if (checkTimer)
87 {
88 [checkTimer invalidate];
89 checkTimer = nil;
90 }
91
92 checkInterval = interval;
93 if (interval > 0)
94 checkTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(checkForUpdatesInBackground) userInfo:nil repeats:YES];
95 }
96
97 - (void)scheduleCheckWithIntervalObject:(NSNumber *)interval
98 {
99 [self scheduleCheckWithInterval:[interval doubleValue]];
100 }
101
102 - (void)applicationDidFinishLaunching:(NSNotification *)note
103 {
1c1bb3a #77 SUIgnoreChecks to ignore update, needed for Sparkle in large depl…
catlan authored
104 if ([[utilities standardBundleDefaults] boolForKey:SUIgnoreChecksKey])
105 return;
106
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
107 // If there's a scheduled interval, we see if it's been longer than that interval since the last
108 // check. If so, we perform a startup check; if not, we don't.
109 if ([self storedCheckInterval])
110 {
111 NSTimeInterval interval = [self storedCheckInterval];
5e05339 #76 Support for Plug-ins
catlan authored
112 NSDate *lastCheck = [[utilities standardBundleDefaults] objectForKey:SULastCheckTimeKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
113 if (!lastCheck) { lastCheck = [NSDate date]; }
114 NSTimeInterval intervalSinceCheck = [[NSDate date] timeIntervalSinceDate:lastCheck];
115 if (intervalSinceCheck < interval)
116 {
117 // Hasn't been long enough; schedule a check for the future.
118 [self performSelector:@selector(checkForUpdatesInBackground) withObject:nil afterDelay:(interval-intervalSinceCheck)];
119 [self performSelector:@selector(scheduleCheckWithIntervalObject:) withObject:[NSNumber numberWithLong:interval] afterDelay:(interval-intervalSinceCheck)];
120 }
121 else
122 {
123 [self scheduleCheckWithInterval:interval];
124 [self checkForUpdatesInBackground];
125 }
126 }
127 else
128 {
129 // There's no scheduled check, so let's see if we're supposed to check on startup.
5e05339 #76 Support for Plug-ins
catlan authored
130 NSNumber *shouldCheckAtStartup = [[utilities standardBundleDefaults] objectForKey:SUCheckAtStartupKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
131 if (!shouldCheckAtStartup) // hasn't been set yet; ask the user
132 {
133 // Let's see if there's a key in Info.plist for a default, though. We'll let that override the dialog if it's there.
5e05339 #76 Support for Plug-ins
catlan authored
134 NSNumber *infoStartupValue = [utilities infoValueForKey:SUCheckAtStartupKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
135 if (infoStartupValue)
136 {
137 shouldCheckAtStartup = infoStartupValue;
138 }
139 else
140 {
5e05339 #76 Support for Plug-ins
catlan authored
141 NSImage *bundleIcon = [utilities hostAppIcon];
142 NSImage *appIcon = [NSImage imageNamed: @"NSApplicationIcon"];
143 if ([appIcon setName:@"NSApplicationIconWorkAround"])
144 [bundleIcon setName:@"NSApplicationIcon"];
145
146 shouldCheckAtStartup = [NSNumber numberWithBool:NSRunAlertPanel(SULocalizedString(@"Check for updates on startup?", nil), [NSString stringWithFormat:SULocalizedString(@"Would you like %@ to check for updates on startup? If not, you can initiate the check manually from the %@ menu.", nil), [utilities hostAppDisplayName], [utilities hostAppDisplayName]], SULocalizedString(@"Yes", nil), SULocalizedString(@"No", nil), nil) == NSAlertDefaultReturn];
147
148 if ([bundleIcon setName:@"NSApplicationIconWorkAround2"])
149 [appIcon setName:@"NSApplicationIcon"];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
150 }
5e05339 #76 Support for Plug-ins
catlan authored
151 [[utilities standardBundleDefaults] setObject:shouldCheckAtStartup forKey:SUCheckAtStartupKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
152 }
153
154 if ([shouldCheckAtStartup boolValue])
155 [self checkForUpdatesInBackground];
156 }
157 }
158
159 - (void)dealloc
160 {
161 [updateItem release];
162 [updateAlert release];
163
164 [downloadPath release];
165 [statusController release];
166 [downloader release];
167
168 if (checkTimer)
169 [checkTimer invalidate];
170
171 if (currentSystemVersion)
172 [currentSystemVersion release];
173
5e05339 #76 Support for Plug-ins
catlan authored
174 if (updateBundle)
175 [updateBundle release];
176
177 if (utilities)
178 [utilities release];
179
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
180 [[NSNotificationCenter defaultCenter] removeObserver:self];
181 [super dealloc];
182 }
183
184 - (void)checkForUpdatesInBackground
185 {
186 [self checkForUpdatesAndNotify:NO];
187 }
188
189 - (IBAction)checkForUpdates:sender
190 {
191 [self checkForUpdatesAndNotify:YES]; // if we're coming from IB, then we want to be more verbose.
192 }
193
1c1bb3a #77 SUIgnoreChecks to ignore update, needed for Sparkle in large depl…
catlan authored
194 - (BOOL)validateMenuItem:(NSMenuItem *)item {
195 if ([item action] == @selector(checkForUpdates:)) {
196 if (![[utilities standardBundleDefaults] boolForKey:SUIgnoreChecksKey])
197 {
198 if (updateInProgress)
199 return NO;
200 else
201 return YES;
202 } else {
203 return NO;
204 }
205 }
206 return NO;
207 }
208
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
209 // If the verbosity flag is YES, Sparkle will say when it can't reach the server and when there's no new update.
210 // This is generally useful for a menu item--when the check is explicitly invoked.
211 - (void)checkForUpdatesAndNotify:(BOOL)verbosity
212 {
1c1bb3a #77 SUIgnoreChecks to ignore update, needed for Sparkle in large depl…
catlan authored
213 if ([[utilities standardBundleDefaults] boolForKey:SUIgnoreChecksKey])
214 return;
215
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
216 if (updateInProgress)
217 {
218 if (verbosity)
219 {
220 NSBeep();
5880c5f Fixed bug #67 courtesy Chris Lawson.
andym authored
221 if (statusController && [[statusController window] isVisible])
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
222 [statusController showWindow:self];
5880c5f Fixed bug #67 courtesy Chris Lawson.
andym authored
223 else if (updateAlert && [[updateAlert window] isVisible])
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
224 [updateAlert showWindow:self];
225 else
226 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An update is already in progress!", nil)];
227 }
228 return;
229 }
230 verbose = verbosity;
231 updateInProgress = YES;
232
233 // 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).
5e05339 #76 Support for Plug-ins
catlan authored
234 NSString *appcastString = [[utilities standardBundleDefaults] objectForKey:SUFeedURLKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
235 if (!appcastString)
5e05339 #76 Support for Plug-ins
catlan authored
236 appcastString = [utilities infoValueForKey:SUFeedURLKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
237 if (!appcastString) { [NSException raise:@"SUNoFeedURL" format:@"No feed URL is specified in the Info.plist or the user defaults!"]; }
238
239 SUAppcast *appcast = [[SUAppcast alloc] init];
240 [appcast setDelegate:self];
241 [appcast fetchAppcastFromURL:[NSURL URLWithString:appcastString]];
242 }
243
244 - (BOOL)automaticallyUpdates
245 {
246 // If the SUAllowsAutomaticUpdatesKey exists and is set to NO, return NO.
5e05339 #76 Support for Plug-ins
catlan authored
247 if ([[utilities infoValueForKey:SUAllowsAutomaticUpdatesKey] boolValue] == NO && [utilities infoValueForKey:SUAllowsAutomaticUpdatesKey]) { return NO; }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
248
249 // If we're not using DSA signatures, we aren't going to trust any updates automatically.
5e05339 #76 Support for Plug-ins
catlan authored
250 if (![[utilities infoValueForKey:SUExpectsDSASignatureKey] boolValue]) { return NO; }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
251
252 // If there's no setting, we default to NO.
5e05339 #76 Support for Plug-ins
catlan authored
253 if (![[utilities standardBundleDefaults] objectForKey:SUAutomaticallyUpdateKey]) { return NO; }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
254
5e05339 #76 Support for Plug-ins
catlan authored
255 return [[[utilities standardBundleDefaults] objectForKey:SUAutomaticallyUpdateKey] boolValue];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
256 }
257
258 - (BOOL)isAutomaticallyUpdating
259 {
260 return [self automaticallyUpdates] && !verbose;
261 }
262
263 - (void)showUpdateErrorAlertWithInfo:(NSString *)info
264 {
265 if ([self isAutomaticallyUpdating]) { return; }
5e05339 #76 Support for Plug-ins
catlan authored
266
267 NSImage *bundleIcon = [utilities hostAppIcon];
268 NSImage *appIcon = [NSImage imageNamed: @"NSApplicationIcon"];
269 if ([appIcon setName:@"NSApplicationIconWorkAround"])
270 [bundleIcon setName:@"NSApplicationIcon"];
271
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
272 NSRunAlertPanel(SULocalizedString(@"Update Error!", nil), info, SULocalizedString(@"Cancel", nil), nil, nil);
5e05339 #76 Support for Plug-ins
catlan authored
273
274 if ([bundleIcon setName:@"NSApplicationIconWorkAround2"])
275 [appIcon setName:@"NSApplicationIcon"];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
276 }
277
278 - (NSTimeInterval)storedCheckInterval
279 {
7946696 Added a minimum to the check interval so that developers don't accide…
andym authored
280 // Define some minimum intervals to avoid DOS-like checking attacks.
281 #ifdef DEBUG
282 #define MIN_INTERVAL 60
283 #else
284 #define MIN_INTERVAL 60*60
285 #endif
286
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
287 // Returns the scheduled check interval stored in the user defaults / info.plist. User defaults override Info.plist.
7946696 Added a minimum to the check interval so that developers don't accide…
andym authored
288 long interval = 0; // 0 signifies not to do timed checking.
5e05339 #76 Support for Plug-ins
catlan authored
289 if ([[utilities standardBundleDefaults] objectForKey:SUScheduledCheckIntervalKey])
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
290 {
5e05339 #76 Support for Plug-ins
catlan authored
291 interval = [[[utilities standardBundleDefaults] objectForKey:SUScheduledCheckIntervalKey] longValue];
7946696 Added a minimum to the check interval so that developers don't accide…
andym authored
292 }
5e05339 #76 Support for Plug-ins
catlan authored
293 else if ([utilities infoValueForKey:SUScheduledCheckIntervalKey])
7946696 Added a minimum to the check interval so that developers don't accide…
andym authored
294 {
5e05339 #76 Support for Plug-ins
catlan authored
295 interval = [[utilities infoValueForKey:SUScheduledCheckIntervalKey] longValue];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
296 }
7946696 Added a minimum to the check interval so that developers don't accide…
andym authored
297 if (interval >= MIN_INTERVAL)
298 return interval;
299 else
300 return 0;
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
301 }
302
303 - (void)beginDownload
304 {
305 if (![self isAutomaticallyUpdating])
306 {
938336b Fixed bug introduced in last revision wherein the status window would…
andym authored
307 statusController = [[SUStatusController alloc] initWithUtilities:utilities];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
308 [statusController beginActionWithTitle:SULocalizedString(@"Downloading update...", nil) maxProgressValue:0 statusText:nil];
309 [statusController setButtonTitle:SULocalizedString(@"Cancel", nil) target:self action:@selector(cancelDownload:) isDefault:NO];
310 [statusController showWindow:self];
311 }
312
313 downloader = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self];
314 }
315
316 - (void)remindMeLater
317 {
318 // Clear out the skipped version so the dialog will actually come back if it was already skipped.
5e05339 #76 Support for Plug-ins
catlan authored
319 [[utilities standardBundleDefaults] setObject:nil forKey:SUSkippedVersionKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
320
321 if (checkInterval)
322 [self scheduleCheckWithInterval:checkInterval];
323 else
324 {
325 // If the host hasn't provided a check interval, we'll use 30 minutes.
326 [self scheduleCheckWithInterval:30 * 60];
327 }
328 }
329
330 - (void)updateAlert:(SUUpdateAlert *)alert finishedWithChoice:(SUUpdateAlertChoice)choice
331 {
332 [alert release];
5880c5f Fixed bug #67 courtesy Chris Lawson.
andym authored
333 updateAlert = nil;
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
334 switch (choice)
335 {
336 case SUInstallUpdateChoice:
337 // Clear out the skipped version so the dialog will come back if the download fails.
5e05339 #76 Support for Plug-ins
catlan authored
338 [[utilities standardBundleDefaults] setObject:nil forKey:SUSkippedVersionKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
339 [self beginDownload];
340 break;
341
342 case SURemindMeLaterChoice:
343 updateInProgress = NO;
344 [self remindMeLater];
345 break;
346
347 case SUSkipThisVersionChoice:
348 updateInProgress = NO;
5e05339 #76 Support for Plug-ins
catlan authored
349 [[utilities standardBundleDefaults] setObject:[updateItem fileVersion] forKey:SUSkippedVersionKey];
a5e239d Improved the check at startup string to use the application's name wh…
andym authored
350 if (checkInterval)
351 [self scheduleCheckWithInterval:checkInterval];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
352 break;
353 }
354 }
355
356 - (void)showUpdatePanel
357 {
5e05339 #76 Support for Plug-ins
catlan authored
358 updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem andUtilities:utilities];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
359 [updateAlert setDelegate:self];
360
361 // Only show the update alert if the app is active; otherwise, we'll wait until it is.
362 if ([NSApp isActive])
363 [updateAlert showWindow:self];
364 else
365 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:@"NSApplicationDidBecomeActiveNotification" object:NSApp];
366 }
367
368 - (void)appcastDidFailToLoad:(SUAppcast *)ac
369 {
370 [ac autorelease];
371 updateInProgress = NO;
372 if (verbose)
373 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)];
374 }
375
376 // Override this to change the new version comparison logic!
377 - (BOOL)newVersionAvailable
378 {
dedd632 Fixed a dumb bug in newVersionAvailable that would make Sparkle think…
andym authored
379 BOOL canRunOnCurrentSystem = (SUStandardVersionComparison([updateItem minimumSystemVersion], [self systemVersionString]) != NSOrderedAscending);
5e05339 #76 Support for Plug-ins
catlan authored
380 return (canRunOnCurrentSystem && (SUStandardVersionComparison([updateItem fileVersion], [utilities hostAppVersion]) == NSOrderedAscending));
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
381 // Want straight-up string comparison like Sparkle 1.0b3 and earlier? Uncomment the line below and comment the one above.
382 // return ![SUHostAppVersion() isEqualToString:[updateItem fileVersion]];
383 }
384
385 - (NSString *)systemVersionString
386 {
387 return currentSystemVersion;
388 }
389
390 - (void)appcastDidFinishLoading:(SUAppcast *)ac
391 {
392 @try
393 {
394 if (!ac) { [NSException raise:@"SUAppcastException" format:@"Couldn't get a valid appcast from the server."]; }
395
396 updateItem = [[ac newestItem] retain];
397 [ac autorelease];
398
399 // Record the time of the check for host app use and for interval checks on startup.
5e05339 #76 Support for Plug-ins
catlan authored
400 [[utilities standardBundleDefaults] setObject:[NSDate date] forKey:SULastCheckTimeKey];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
401
402 if (![updateItem fileVersion])
403 {
404 [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."];
405 }
406
5e05339 #76 Support for Plug-ins
catlan authored
407 if (!verbose && [[[utilities standardBundleDefaults] objectForKey:SUSkippedVersionKey] isEqualToString:[updateItem fileVersion]]) { updateInProgress = NO; return; }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
408
409 if ([self newVersionAvailable])
410 {
411 if (checkTimer) // There's a new version! Let's disable the automated checking timer unless the user cancels.
412 {
413 [checkTimer invalidate];
414 checkTimer = nil;
415 }
416
417 if ([self isAutomaticallyUpdating])
418 {
419 [self beginDownload];
420 }
421 else
422 {
423 [self showUpdatePanel];
424 }
425 }
426 else
427 {
428 if (verbose) // We only notify on no new version when we're being verbose.
429 {
5e05339 #76 Support for Plug-ins
catlan authored
430 NSImage *bundleIcon = [utilities hostAppIcon];
431 NSImage *appIcon = [NSImage imageNamed: @"NSApplicationIcon"];
432 if ([appIcon setName:@"NSApplicationIconWorkAround"])
433 [bundleIcon setName:@"NSApplicationIcon"];
434
435 NSRunAlertPanel(SULocalizedString(@"You're up to date!", nil), [NSString stringWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), [utilities hostAppDisplayName], [utilities hostAppVersionString]], SULocalizedString(@"OK", nil), nil, nil);
436
437 if ([bundleIcon setName:@"NSApplicationIconWorkAround2"])
438 [appIcon setName:@"NSApplicationIcon"];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
439 }
440 updateInProgress = NO;
441 }
442 }
443 @catch (NSException *e)
444 {
445 NSLog([e reason]);
446 updateInProgress = NO;
447 if (verbose)
448 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil)];
449 }
450 }
451
452 - (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response
453 {
454 [statusController setMaxProgressValue:[response expectedContentLength]];
455 }
456
457 - (void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)name
458 {
459 // If name ends in .txt, the server probably has a stupid MIME configuration. We'll give
460 // the developer the benefit of the doubt and chop that off.
461 if ([[name pathExtension] isEqualToString:@"txt"])
462 name = [name stringByDeletingPathExtension];
463
464 // We create a temporary directory in /tmp and stick the file there.
465 NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
466 BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil];
467 if (!success)
468 {
469 [NSException raise:@"SUFailTmpWrite" format:@"Couldn't create temporary directory in /var/tmp"];
470 [download cancel];
471 [download release];
472 }
473
474 [downloadPath autorelease];
475 downloadPath = [[tempDir stringByAppendingPathComponent:name] retain];
476 [download setDestination:downloadPath allowOverwrite:YES];
477 }
478
479 - (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length
480 {
481 [statusController setProgressValue:[statusController progressValue] + length];
482 [statusController setStatusText:[NSString stringWithFormat:SULocalizedString(@"%.0lfk of %.0lfk", nil), [statusController progressValue] / 1024.0, [statusController maxProgressValue] / 1024.0]];
483 }
484
485 - (void)unarchiver:(SUUnarchiver *)ua extractedLength:(long)length
486 {
487 if ([self isAutomaticallyUpdating]) { return; }
488 if ([statusController maxProgressValue] == 0)
489 [statusController setMaxProgressValue:[[[[NSFileManager defaultManager] fileAttributesAtPath:downloadPath traverseLink:NO] objectForKey:NSFileSize] longValue]];
490 [statusController setProgressValue:[statusController progressValue] + length];
491 }
492
493 - (void)unarchiverDidFinish:(SUUnarchiver *)ua
494 {
495 [ua autorelease];
496
497 if ([self isAutomaticallyUpdating])
498 {
499 [self installAndRestart:self];
500 }
501 else
502 {
503 [statusController beginActionWithTitle:SULocalizedString(@"Ready to install!", nil) maxProgressValue:1 statusText:nil];
504 [statusController setProgressValue:1]; // fill the bar
505 [statusController setButtonTitle:SULocalizedString(@"Install and Relaunch", nil) target:self action:@selector(installAndRestart:) isDefault:YES];
506 [NSApp requestUserAttention:NSInformationalRequest];
507 }
508 }
509
510 - (void)unarchiverDidFail:(SUUnarchiver *)ua
511 {
512 [ua autorelease];
513 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil)];
514 [self abandonUpdate];
515 }
516
517 - (void)extractUpdate
518 {
519 // Now we have to extract the downloaded archive.
520 if (![self isAutomaticallyUpdating])
521 [statusController beginActionWithTitle:SULocalizedString(@"Extracting update...", nil) maxProgressValue:0 statusText:nil];
522
523 @try
524 {
525 // If the developer's provided a sparkle:md5Hash attribute on the enclosure, let's verify that.
526 if ([updateItem MD5Sum] && ![[NSFileManager defaultManager] validatePath:downloadPath withMD5Hash:[updateItem MD5Sum]])
527 {
528 [NSException raise:@"SUUnarchiveException" format:@"MD5 verification of the update archive failed."];
529 }
530
531 // DSA verification, if activated by the developer
5e05339 #76 Support for Plug-ins
catlan authored
532 if ([[utilities infoValueForKey:SUExpectsDSASignatureKey] boolValue])
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
533 {
534 NSString *dsaSignature = [updateItem DSASignature];
5e05339 #76 Support for Plug-ins
catlan authored
535 NSString *pkeyString = [utilities infoValueForKey:SUPublicDSAKeyKey]; // Fetch the app's public DSA key.
536 if (![[NSFileManager defaultManager] validatePath:downloadPath withEncodedDSASignature:dsaSignature withPublicDSAKey:pkeyString])
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
537 {
538 [NSException raise:@"SUUnarchiveException" format:@"DSA verification of the update archive failed."];
539 }
540 }
541
542 SUUnarchiver *unarchiver = [[SUUnarchiver alloc] init];
543 [unarchiver setDelegate:self];
544 [unarchiver unarchivePath:downloadPath]; // asynchronous extraction!
545 }
546 @catch(NSException *e) {
547 NSLog([e reason]);
548 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil)];
549 [self abandonUpdate];
550 }
551 }
552
553 - (void)downloadDidFinish:(NSURLDownload *)download
554 {
555 [download release];
556 downloader = nil;
557 [self extractUpdate];
558 }
559
560 - (void)abandonUpdate
561 {
562 [updateItem autorelease];
563 updateItem = nil;
564 [statusController close];
565 [statusController autorelease];
566 statusController = nil;
567 updateInProgress = NO;
568 }
569
570 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
571 {
572 [self abandonUpdate];
573
574 NSLog(@"Download error: %@", [error localizedDescription]);
575 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while trying to download the file. Please try again later.", nil)];
576 }
577
578 - (IBAction)installAndRestart:sender
579 {
580 NSString *currentAppPath = [[NSBundle mainBundle] bundlePath];
d7cf93d #76 compare pathExtension with [utilities hostAppExtension] instead o…
catlan authored
581 NSString *currentBundlePath = [updateBundle bundlePath];
5e05339 #76 Support for Plug-ins
catlan authored
582 NSString *newAppDownloadPath = [[downloadPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:[[utilities unlocalizedInfoValueForKey:@"CFBundleName"] stringByAppendingPathExtension:@"app"]];
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
583 BOOL isPackage = NO;
584 int processIdentifier = [[NSProcessInfo processInfo] processIdentifier];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
585 @try
586 {
587 if (![self isAutomaticallyUpdating])
588 {
589 [statusController beginActionWithTitle:SULocalizedString(@"Installing update...", nil) maxProgressValue:0 statusText:nil];
590 [statusController setButtonEnabled:NO];
591
592 // We have to wait for the UI to update.
593 NSEvent *event;
594 while((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES]))
595 [NSApp sendEvent:event];
596 }
597
598 // We assume that the archive will contain a file named {CFBundleName}.app
599 // (where, obviously, CFBundleName comes from Info.plist)
5e05339 #76 Support for Plug-ins
catlan authored
600 if (![utilities unlocalizedInfoValueForKey:@"CFBundleName"]) { [NSException raise:@"SUInstallException" format:@"This application has no CFBundleName! This key must be set to the application's name."]; }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
601
602 // Search subdirectories for the application
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
603 NSString *file, *appName = [utilities unlocalizedInfoValueForKey:@"CFBundleName"];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
604 NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:[downloadPath stringByDeletingLastPathComponent]];
605 while ((file = [dirEnum nextObject]))
606 {
607 // Some DMGs have symlinks into /Applications! That's no good!
608 if ([file isEqualToString:@"/Applications"])
609 [dirEnum skipDescendents];
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
610 if ([[file pathExtension] isEqualToString:[utilities hostAppExtension]] &&
611 [[file stringByDeletingPathExtension] isEqualToString:appName]) // We found one!
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
612 {
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
613 isPackage = NO;
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
614 newAppDownloadPath = [[downloadPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:file];
615 break;
616 }
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
617 else if (([[file pathExtension] isEqualToString:@"pkg"] || [[file pathExtension] isEqualToString:@"mpkg"]) &&
618 [[file stringByDeletingPathExtension] isEqualToString:appName])
619 {
620 isPackage = YES;
621 newAppDownloadPath = [[downloadPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:file];
622 break;
623 }
624 if ([[file pathExtension] isEqualToString:[utilities hostAppExtension]] ||
625 [[file pathExtension] isEqualToString:@"pkg"] ||
626 [[file pathExtension] isEqualToString:@"mpkg"]) // No point in looking in bundles.
627 {
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
628 [dirEnum skipDescendents];
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
629 }
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
630 }
631
632 if (!newAppDownloadPath || ![[NSFileManager defaultManager] fileExistsAtPath:newAppDownloadPath])
633 {
d7cf93d #76 compare pathExtension with [utilities hostAppExtension] instead o…
catlan authored
634 [NSException raise:@"SUInstallException" format:@"The update archive didn't contain an application with the proper name: %@. Remember, the updated app's file name must be identical to {CFBundleString}.{Extension}", [[utilities unlocalizedInfoValueForKey:@"CFBundleName"] stringByAppendingPathExtension:[utilities hostAppExtension]]];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
635 }
636 }
637 @catch(NSException *e)
638 {
639 NSLog([e reason]);
640 [self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred during installation. Please try again later.", nil)];
641 [self abandonUpdate];
642 return;
643 }
644
645 if ([self isAutomaticallyUpdating]) // Don't do authentication if we're automatically updating; that'd be surprising.
646 {
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
647 if (!isPackage)
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
648 {
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
649 int tag = 0;
650 BOOL result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[currentBundlePath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[currentBundlePath lastPathComponent]] tag:&tag];
651 result &= [[NSFileManager defaultManager] movePath:newAppDownloadPath toPath:currentBundlePath handler:nil];
652 if (!result)
653 {
654 [self abandonUpdate];
655 return;
656 }
657 }
658 else
659 {
660 NSString *installerPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.installer"];
661 installerPath = [installerPath stringByAppendingString:@"/Contents/MacOS/Installer"];
662 NSTask *installer = [NSTask launchedTaskWithLaunchPath:installerPath arguments:[NSArray arrayWithObjects:newAppDownloadPath, nil]];
663 processIdentifier = [installer processIdentifier];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
664 }
665 }
666 else // But if we're updating by the action of the user, do an authenticated move.
667 {
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
668 if (!isPackage)
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
669 {
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
670 // Outside of the @try block because we want to be a little more informative on this error.
671 if (![[NSFileManager defaultManager] movePathWithAuthentication:newAppDownloadPath toPath:currentBundlePath])
672 {
673 [self showUpdateErrorAlertWithInfo:[NSString stringWithFormat:SULocalizedString(@"%@ does not have permission to write to the application's directory! Are you running off a disk image? If not, ask your system administrator for help.", nil), [utilities hostAppDisplayName]]];
674 [self abandonUpdate];
675 return;
676 }
677 }
678 else
679 {
680 NSString *installerPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.installer"];
681 installerPath = [installerPath stringByAppendingString:@"/Contents/MacOS/Installer"];
682 NSTask *installer = [NSTask launchedTaskWithLaunchPath:installerPath arguments:[NSArray arrayWithObjects:newAppDownloadPath, nil]];
683 processIdentifier = [installer processIdentifier];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
684 }
685 }
686
687 // Prompt for permission to restart if we're automatically updating.
688 if ([self isAutomaticallyUpdating])
689 {
5e05339 #76 Support for Plug-ins
catlan authored
690 SUAutomaticUpdateAlert *alert = [[SUAutomaticUpdateAlert alloc] initWithAppcastItem:updateItem andUtilities:utilities];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
691 if ([NSApp runModalForWindow:[alert window]] == NSAlertAlternateReturn)
692 {
693 [alert release];
694 return;
695 }
696 }
697
698 [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self];
699
b2eab97 Made the relaunching process much snazzier, courtesy of Cedric Luthi.…
andym authored
700 NSString *relaunchPath = [[[[NSBundle bundleForClass:[self class]] executablePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"relaunch"];
701 if (!relaunchPath) // slight hack to resolve issues with running with in configurations
702 {
703 NSString *frameworkPath = [[[NSBundle mainBundle] sharedFrameworksPath] stringByAppendingPathComponent:@"Sparkle.framework"];
704 NSBundle *framework = [NSBundle bundleWithPath:frameworkPath];
705 relaunchPath = [[[framework executablePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"relaunch"];
706 }
707
46ace28 #79 Support packages (*.pkg and *.mpkg) to update
catlan authored
708 [NSTask launchedTaskWithLaunchPath:relaunchPath arguments:[NSArray arrayWithObjects:currentAppPath, [NSString stringWithFormat:@"%d", processIdentifier], nil]];
b2eab97 Made the relaunching process much snazzier, courtesy of Cedric Luthi.…
andym authored
709 [NSApp terminate:self];
9fa3da5 Holy restructuring, batman! Watch out for falling folders.
andym authored
710 }
711
712 - (IBAction)cancelDownload:sender
713 {
714 if (downloader)
715 {
716 [downloader cancel];
717 [downloader release];
718 }
719 [self abandonUpdate];
720
721 if (checkInterval)
722 {
723 [self scheduleCheckWithInterval:checkInterval];
724 }
725 }
726
727 - (void)applicationDidBecomeActive:(NSNotification *)aNotification
728 {
729 // We don't want to display the update alert until the application becomes active.
730 [updateAlert showWindow:self];
731 [[NSNotificationCenter defaultCenter] removeObserver:self name:@"NSApplicationDidBecomeActiveNotification" object:NSApp];
732 }
733
734 @end
Something went wrong with that request. Please try again.