root/trunk/Update Checker Sources/UpdateCheckerAppDelegate.m

Revision 917, 10.6 kB (checked in by astrange, 3 months ago)

Delete progress indicators from the prefpane, they're ugly and pointless.
Get rid of alert panels in the update checker and use IPC to show them in the prefpane instead.
Fix the update checker never cleaning up its locks.
Synchronize user defaults more often since it seems necessary.
Delete code for not running too often in the update checker, it's a duplicate of the same more-maintained code in Perian.

This requires merging the nib into release branch again...

Line 
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
Note: See TracBrowser for help on using the browser.