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