Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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