| 1 |
// |
|---|
| 2 |
// UpdateCheckerAppDelegate.m |
|---|
| 3 |
// Perian |
|---|
| 4 |
// |
|---|
| 5 |
// Created by Augie Fackler on 1/6/07. |
|---|
| 6 |
// Copyright 2007 __MyCompanyName__. All rights reserved. |
|---|
| 7 |
// |
|---|
| 8 |
// This is really just a heavily-customized version of SUUpdate designed |
|---|
| 9 |
// so that the updates are done using an app and not a framework. We do |
|---|
| 10 |
// this so that things like little snitch don't give us problems, and so |
|---|
| 11 |
// we can be more sure that the update won't get run twice accidentaly. |
|---|
| 12 |
|
|---|
| 13 |
#import "UpdateCheckerAppDelegate.h" |
|---|
| 14 |
#include <stdlib.h> |
|---|
| 15 |
|
|---|
| 16 |
//define the following to use the beta appcast URL, but DON'T commit that change |
|---|
| 17 |
//#define betaAppcastUrl @"whatever" |
|---|
| 18 |
|
|---|
| 19 |
@interface UpdateCheckerAppDelegate (private) |
|---|
| 20 |
- (void)showUpdateErrorAlertWithInfo:(NSString *)info; |
|---|
| 21 |
@end |
|---|
| 22 |
|
|---|
| 23 |
@implementation UpdateCheckerAppDelegate |
|---|
| 24 |
|
|---|
| 25 |
- (void)dealloc |
|---|
| 26 |
{ |
|---|
| 27 |
[downloader release]; |
|---|
| 28 |
[downloadPath release]; |
|---|
| 29 |
[lastRunDate release]; |
|---|
| 30 |
[super dealloc]; |
|---|
| 31 |
} |
|---|
| 32 |
|
|---|
| 33 |
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification |
|---|
| 34 |
{ |
|---|
| 35 |
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; |
|---|
| 36 |
lastRunDate = [[defaults objectForKey:NEXT_RUN_KEY] retain]; |
|---|
| 37 |
|
|---|
| 38 |
[defaults setObject:[NSDate dateWithTimeIntervalSinceNow:TIME_INTERVAL_TIL_NEXT_RUN] forKey:NEXT_RUN_KEY]; |
|---|
| 39 |
manualRun = [defaults boolForKey:MANUAL_RUN_KEY]; |
|---|
| 40 |
[defaults removeObjectForKey:MANUAL_RUN_KEY]; |
|---|
| 41 |
[defaults synchronize]; |
|---|
| 42 |
[self doUpdateCheck]; |
|---|
| 43 |
} |
|---|
| 44 |
|
|---|
| 45 |
- (void)doUpdateCheck |
|---|
| 46 |
{ |
|---|
| 47 |
NSString *updateUrlString = SUInfoValueForKey(UPDATE_URL_KEY); |
|---|
| 48 |
|
|---|
| 49 |
if (!updateUrlString) { [NSException raise:@"NoFeedURL" format:@"No feed URL is specified in the Info.plist!"]; } |
|---|
| 50 |
|
|---|
| 51 |
#ifdef betaAppcastUrl |
|---|
| 52 |
updateUrlString = [[updateUrlString substringToIndex:[updateUrlString length]-4] stringByAppendingFormat:@"-%@.xml", betaAppcastUrl]; |
|---|
| 53 |
#endif |
|---|
| 54 |
if(manualRun) |
|---|
| 55 |
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:UPDATE_STATUS_NOTIFICATION object:@"Starting"]; |
|---|
| 56 |
|
|---|
| 57 |
SUAppcast *appcast = [[SUAppcast alloc] init]; |
|---|
| 58 |
[appcast setDelegate:self]; |
|---|
| 59 |
[appcast fetchAppcastFromURL:[NSURL URLWithString:updateUrlString]]; |
|---|
| 60 |
} |
|---|
| 61 |
|
|---|
| 62 |
- (void)appcastDidFinishLoading:(SUAppcast *)appcast |
|---|
| 63 |
{ |
|---|
| 64 |
latest = [[appcast newestItem] retain]; |
|---|
| 65 |
|
|---|
| 66 |
if (![latest fileVersion]) |
|---|
| 67 |
{ |
|---|
| 68 |
[self updateFailed]; |
|---|
| 69 |
[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."]; |
|---|
| 70 |
} |
|---|
| 71 |
|
|---|
| 72 |
// OS version (Apple recommends using SystemVersion.plist instead of Gestalt() here, don't ask me why). |
|---|
| 73 |
// This code *should* use NSSearchPathForDirectoriesInDomains(NSCoreServiceDirectory, NSSystemDomainMask, YES) |
|---|
| 74 |
// but that returns /Library/CoreServices for some reason |
|---|
| 75 |
NSString *versionPlistPath = @"/System/Library/CoreServices/SystemVersion.plist"; |
|---|
| 76 |
NSString *currentSystemVersion = [[[NSDictionary dictionaryWithContentsOfFile:versionPlistPath] objectForKey:@"ProductVersion"] retain]; |
|---|
| 77 |
|
|---|
| 78 |
BOOL updateAvailable = SUStandardVersionComparison([latest minimumSystemVersion], currentSystemVersion); |
|---|
| 79 |
NSString *panePath = [[[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]; |
|---|
| 80 |
updateAvailable = (updateAvailable && (SUStandardVersionComparison([latest fileVersion], [[NSBundle bundleWithPath:panePath] objectForInfoDictionaryKey:@"CFBundleVersion"]) == NSOrderedAscending)); |
|---|
| 81 |
|
|---|
| 82 |
if (![panePath isEqualToString:@"Perian.prefPane"]) { |
|---|
| 83 |
NSLog(@"The update checker needs to be run from inside the preference pane, quitting..."); |
|---|
| 84 |
updateAvailable = 0; |
|---|
| 85 |
} |
|---|
| 86 |
|
|---|
| 87 |
NSString *skippedVersion = [[NSUserDefaults standardUserDefaults] objectForKey:SKIPPED_VERSION_KEY]; |
|---|
| 88 |
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; |
|---|
| 89 |
if (updateAvailable && (!skippedVersion || |
|---|
| 90 |
(skippedVersion && ![skippedVersion isEqualToString:[latest versionString]]))) { |
|---|
| 91 |
if(manualRun) |
|---|
| 92 |
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:UPDATE_STATUS_NOTIFICATION object:@"YesUpdates"]; |
|---|
| 93 |
[self showUpdatePanelForItem:latest]; |
|---|
| 94 |
} else { |
|---|
| 95 |
if(manualRun) |
|---|
| 96 |
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:UPDATE_STATUS_NOTIFICATION object:@"NoUpdates"]; |
|---|
| 97 |
[[NSApplication sharedApplication] terminate:self]; |
|---|
| 98 |
} |
|---|
| 99 |
|
|---|
| 100 |
[appcast release]; |
|---|
| 101 |
} |
|---|
| 102 |
|
|---|
| 103 |
- (void)appcastDidFailToLoad:(SUAppcast *)appcast |
|---|
| 104 |
{ |
|---|
| 105 |
[self updateFailed]; |
|---|
| 106 |
if(manualRun) |
|---|
| 107 |
[[NSDistributedNotificationCenter defaultCenter] postNotificationName:UPDATE_STATUS_NOTIFICATION object:@"Error"]; |
|---|
| 108 |
[appcast release]; |
|---|
| 109 |
[[NSApplication sharedApplication] terminate:self]; |
|---|
| 110 |
} |
|---|
| 111 |
|
|---|
| 112 |
- (void)showUpdatePanelForItem:(SUAppcastItem *)updateItem |
|---|
| 113 |
{ |
|---|
| 114 |
updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:updateItem]; |
|---|
| 115 |
[updateAlert setDelegate:self]; |
|---|
| 116 |
[updateAlert showWindow:self]; |
|---|
| 117 |
} |
|---|
| 118 |
|
|---|
| 119 |
- (void)updateAlert:(SUUpdateAlert *)alert finishedWithChoice:(SUUpdateAlertChoice)choice |
|---|
| 120 |
{ |
|---|
| 121 |
if (choice == SUInstallUpdateChoice) { |
|---|
| 122 |
[self beginDownload]; |
|---|
| 123 |
} else { |
|---|
| 124 |
if (choice == SUSkipThisVersionChoice) |
|---|
| 125 |
[[NSUserDefaults standardUserDefaults] setObject:[latest versionString] forKey:SKIPPED_VERSION_KEY]; |
|---|
| 126 |
[[NSApplication sharedApplication] terminate:self]; |
|---|
| 127 |
} |
|---|
| 128 |
} |
|---|
| 129 |
|
|---|
| 130 |
- (void)showUpdateErrorAlertWithInfo:(NSString *)info |
|---|
| 131 |
{ |
|---|
| 132 |
NSRunAlertPanel(SULocalizedString(@"Update Error!", nil), info, SULocalizedString(@"Cancel", nil), nil, nil); |
|---|
| 133 |
} |
|---|
| 134 |
|
|---|
| 135 |
- (void)beginDownload |
|---|
| 136 |
{ |
|---|
| 137 |
statusController = [[SUStatusController alloc] init]; |
|---|
| 138 |
[statusController beginActionWithTitle:SULocalizedString(@"Downloading update...", nil) maxProgressValue:0 statusText:nil]; |
|---|
| 139 |
[statusController setButtonTitle:SULocalizedString(@"Cancel", nil) target:self action:@selector(cancelDownload:) isDefault:NO]; |
|---|
| 140 |
[statusController showWindow:self]; |
|---|
| 141 |
|
|---|
| 142 |
downloader = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[latest fileURL]] delegate:self]; |
|---|
| 143 |
} |
|---|
| 144 |
|
|---|
| 145 |
- (void)cancelDownload:(id)sender |
|---|
| 146 |
{ |
|---|
| 147 |
[downloader cancel]; |
|---|
| 148 |
[statusController close]; |
|---|
| 149 |
[[NSApplication sharedApplication] terminate:self]; |
|---|
| 150 |
} |
|---|
| 151 |
|
|---|
| 152 |
#pragma mark NSURLDownload delegate methods |
|---|
| 153 |
- (void)download:(NSURLDownload *)download didReceiveResponse:(NSURLResponse *)response |
|---|
| 154 |
{ |
|---|
| 155 |
[statusController setMaxProgressValue:[response expectedContentLength]]; |
|---|
| 156 |
} |
|---|
| 157 |
|
|---|
| 158 |
- (void)download:(NSURLDownload *)download decideDestinationWithSuggestedFilename:(NSString *)name |
|---|
| 159 |
{ |
|---|
| 160 |
// If name ends in .txt, the server probably has a stupid MIME configuration. We'll give |
|---|
| 161 |
// the developer the benefit of the doubt and chop that off. |
|---|
| 162 |
if ([[name pathExtension] isEqualToString:@"txt"]) |
|---|
| 163 |
name = [name stringByDeletingPathExtension]; |
|---|
| 164 |
|
|---|
| 165 |
// We create a temporary directory in /tmp and stick the file there. |
|---|
| 166 |
NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; |
|---|
| 167 |
BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil]; |
|---|
| 168 |
if (!success) |
|---|
| 169 |
{ |
|---|
| 170 |
[NSException raise:@"SUFailTmpWrite" format:@"Couldn't create temporary directory in /tmp"]; |
|---|
| 171 |
[download cancel]; |
|---|
| 172 |
[download release]; |
|---|
| 173 |
} |
|---|
| 174 |
|
|---|
| 175 |
downloadPath = [[tempDir stringByAppendingPathComponent:name] retain]; |
|---|
| 176 |
[download setDestination:downloadPath allowOverwrite:YES]; |
|---|
| 177 |
} |
|---|
| 178 |
|
|---|
| 179 |
- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length |
|---|
| 180 |
{ |
|---|
| 181 |
[statusController setProgressValue:[statusController progressValue] + length]; |
|---|
| 182 |
[statusController setStatusText:[NSString stringWithFormat:SULocalizedString(@"%.0lfk of %.0lfk", nil), [statusController progressValue] / 1024.0, [statusController maxProgressValue] / 1024.0]]; |
|---|
| 183 |
} |
|---|
| 184 |
|
|---|
| 185 |
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error |
|---|
| 186 |
{ |
|---|
| 187 |
[self updateFailed]; |
|---|
| 188 |
NSLog(@"Download error: %@", [error localizedDescription]); |
|---|
| 189 |
[self showUpdateErrorAlertWithInfo:SULocalizedString(@"An error occurred while trying to download the newest version of Perian. Please try again later.", nil)]; |
|---|
| 190 |
[[NSApplication sharedApplication] terminate:self]; |
|---|
| 191 |
} |
|---|
| 192 |
|
|---|
| 193 |
//Stolen from sprakle |
|---|
| 194 |
- (BOOL)extractDMG:(NSString *)archivePath |
|---|
| 195 |
{ |
|---|
| 196 |
// First, we internet-enable the volume. |
|---|
| 197 |
NSTask *hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"internet-enable", @"-quiet", archivePath, nil]]; |
|---|
| 198 |
[hdiTask waitUntilExit]; |
|---|
| 199 |
if ([hdiTask terminationStatus] != 0) { return NO; } |
|---|
| 200 |
|
|---|
| 201 |
// Now, open the volume; it'll extract into its own directory. |
|---|
| 202 |
hdiTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/env" arguments:[NSArray arrayWithObjects:@"hdiutil", @"attach", @"-idme", @"-noidmereveal", @"-noidmetrash", @"-noverify", @"-nobrowse", @"-noautoopen", @"-quiet", archivePath, nil]]; |
|---|
| 203 |
[hdiTask waitUntilExit]; |
|---|
| 204 |
if ([hdiTask terminationStatus] != 0) { return NO; } |
|---|
| 205 |
|
|---|
| 206 |
return YES; |
|---|
| 207 |
} |
|---|
| 208 |
|
|---|
| 209 |
extern char **environ; |
|---|
| 210 |
|
|---|
| 211 |
- (void)downloadDidFinish:(NSURLDownload *)download |
|---|
| 212 |
{ |
|---|
| 213 |
[download release]; |
|---|
| 214 |
downloader = nil; |
|---|
| 215 |
|
|---|
| 216 |
//Indeterminate progress bar |
|---|
| 217 |
[statusController setMaxProgressValue:0]; |
|---|
| 218 |
[statusController setStatusText:SULocalizedString(@"Extracting...", nil)]; |
|---|
| 219 |
|
|---|
| 220 |
if(![self extractDMG:downloadPath]) |
|---|
| 221 |
{ |
|---|
| 222 |
[self updateFailed]; |
|---|
| 223 |
[self showUpdateErrorAlertWithInfo:NSLocalizedString(@"Could not Extract Downloaded File",@"")]; |
|---|
| 224 |
} |
|---|
| 225 |
|
|---|
| 226 |
NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:[downloadPath stringByDeletingLastPathComponent]]; |
|---|
| 227 |
NSString *file = nil; |
|---|
| 228 |
NSString *prefpanelocation = nil; |
|---|
| 229 |
while((file = [dirEnum nextObject]) != nil) |
|---|
| 230 |
{ |
|---|
| 231 |
if([[[dirEnum fileAttributes] objectForKey:NSFileTypeSymbolicLink] boolValue]) |
|---|
| 232 |
[dirEnum skipDescendents]; |
|---|
| 233 |
if([[file pathExtension] isEqualToString:@"prefPane"]) |
|---|
| 234 |
{ |
|---|
| 235 |
NSString *containingLocation = [downloadPath stringByDeletingLastPathComponent]; |
|---|
| 236 |
NSString *oldLocation = [containingLocation stringByAppendingPathComponent:file]; |
|---|
| 237 |
prefpanelocation = [[containingLocation stringByDeletingLastPathComponent] stringByAppendingPathComponent:[file lastPathComponent]]; |
|---|
| 238 |
[[NSFileManager defaultManager] movePath:oldLocation toPath:prefpanelocation handler:nil]; |
|---|
| 239 |
} |
|---|
| 240 |
} |
|---|
| 241 |
|
|---|
| 242 |
char *buf = NULL; |
|---|
| 243 |
asprintf(&buf, "open \"$PREFPANE_LOCATION\"; rm -rf \"$TEMP_FOLDER\""); |
|---|
| 244 |
if(!buf) |
|---|
| 245 |
{ |
|---|
| 246 |
[self updateFailed]; |
|---|
| 247 |
[self showUpdateErrorAlertWithInfo:NSLocalizedString(@"Could not Create Extraction Script",@"")]; |
|---|
| 248 |
} |
|---|
| 249 |
|
|---|
| 250 |
char *args[] = {"/bin/sh", "-c", buf, NULL}; |
|---|
| 251 |
setenv("PREFPANE_LOCATION", [prefpanelocation fileSystemRepresentation], 1); |
|---|
| 252 |
setenv("TEMP_FOLDER", [[downloadPath stringByDeletingLastPathComponent] fileSystemRepresentation], 1); |
|---|
| 253 |
int forkVal = fork(); |
|---|
| 254 |
if(forkVal == -1) |
|---|
| 255 |
{ |
|---|
| 256 |
[self updateFailed]; |
|---|
| 257 |
[self showUpdateErrorAlertWithInfo:NSLocalizedString(@"Could not Run Update",@"")]; |
|---|
| 258 |
} |
|---|
| 259 |
if(forkVal == 0) |
|---|
| 260 |
execve("/bin/sh", args, environ); |
|---|
| 261 |
[NSApp terminate:self]; |
|---|
| 262 |
//And, we are out of here!!! |
|---|
| 263 |
} |
|---|
| 264 |
|
|---|
| 265 |
- (BOOL)showsReleaseNotes |
|---|
| 266 |
{ |
|---|
| 267 |
return YES; |
|---|
| 268 |
} |
|---|
| 269 |
|
|---|
| 270 |
- (void)updateFailed |
|---|
| 271 |
{ |
|---|
| 272 |
if(lastRunDate == nil) |
|---|
| 273 |
[[NSUserDefaults standardUserDefaults] removeObjectForKey:NEXT_RUN_KEY]; |
|---|
| 274 |
else |
|---|
| 275 |
[[NSUserDefaults standardUserDefaults] setObject:lastRunDate forKey:NEXT_RUN_KEY]; |
|---|
| 276 |
} |
|---|
| 277 |
|
|---|
| 278 |
|
|---|
| 279 |
@end |
|---|