Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 295 lines (247 sloc) 13.082 kb
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
1 //
2 // SUBasicUpdateDriver.m
3 // Sparkle
4 //
5 // Created by Andy Matuschak on 4/23/08.
6 // Copyright 2008 Andy Matuschak. All rights reserved.
7 //
8
9 #import "SUBasicUpdateDriver.h"
10 #import "Sparkle.h"
11
12 @implementation SUBasicUpdateDriver
13
14 - (void)checkForUpdatesAtURL:(NSURL *)appcastURL hostBundle:(NSBundle *)hb
15 {
16 hostBundle = [hb retain];
8b3d15b @andymatuschak Moved the check for whether the host is running from a disk image to SUB...
andymatuschak authored
17
18 if ([hostBundle isRunningFromDiskImage])
19 {
1552d4e @andymatuschak Changed the disk image failure string to "%1$@ can't be updated when it'...
andymatuschak authored
20 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURunningFromDiskImageError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:SULocalizedString(@"%1$@ can't be updated when it's running from a disk image. Move %1$@ to your Applications folder, relaunch it from there, and try again.", nil), [hostBundle name]] forKey:NSLocalizedDescriptionKey]]];
8b3d15b @andymatuschak Moved the check for whether the host is running from a disk image to SUB...
andymatuschak authored
21 return;
22 }
23
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
24 SUAppcast *appcast = [[SUAppcast alloc] init];
25 CFRetain(appcast); // We'll manage the appcast's memory ourselves so we don't have to make it an IV to support GC.
26 [appcast release];
27
28 [appcast setDelegate:self];
0787530 @andymatuschak Bumped user agent string version number to 1.5b2.
andymatuschak authored
29 [appcast setUserAgentString:[NSString stringWithFormat: @"%@/%@ Sparkle/1.5b2", [hostBundle name], [hostBundle displayVersion]]];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
30 [appcast fetchAppcastFromURL:appcastURL];
251706d @andymatuschak Fixed a serious bug which could cause a server to DDoS'd in the case of ...
andymatuschak authored
31 [[SUUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:SULastCheckTimeKey];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
32 }
33
aaa60d2 @andymatuschak Fixed 244761
andymatuschak authored
34 - (id <SUVersionComparison>)_versionComparator
35 {
36 id <SUVersionComparison> comparator = nil;
37
38 // Give the delegate a chance to provide a custom version comparator
39 if ([delegate respondsToSelector:@selector(versionComparatorForHostBundle:)])
40 comparator = [delegate versionComparatorForHostBundle:hostBundle];
41
42 // If we don't get a comparator from the delegate, use the default comparator
43 if (!comparator)
44 comparator = [SUStandardVersionComparator defaultComparator];
45
46 return comparator;
47 }
48
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
49 - (BOOL)isItemNewer:(SUAppcastItem *)ui
50 {
aaa60d2 @andymatuschak Fixed 244761
andymatuschak authored
51 return [[self _versionComparator] compareVersion:[hostBundle version] toVersion:[ui versionString]] == NSOrderedAscending;
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
52 }
53
54 - (BOOL)hostSupportsItem:(SUAppcastItem *)ui
55 {
56 if ([ui minimumSystemVersion] == nil || [[ui minimumSystemVersion] isEqualToString:@""]) { return YES; }
aaa60d2 @andymatuschak Fixed 244761
andymatuschak authored
57 return [[self _versionComparator] compareVersion:[ui minimumSystemVersion] toVersion:[NSWorkspace systemVersionString]] != NSOrderedDescending;
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
58 }
59
60 - (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui
61 {
1264a6b @andymatuschak Sparkle now finds the newest update in an appcast for which the host mee...
andymatuschak authored
62 NSString *skippedVersion = [[SUUserDefaults standardUserDefaults] objectForKey:SUSkippedVersionKey];
63 if (skippedVersion == nil) { return NO; }
aaa60d2 @andymatuschak Fixed 244761
andymatuschak authored
64 return [[self _versionComparator] compareVersion:[ui versionString] toVersion:skippedVersion] != NSOrderedDescending;
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
65 }
66
67 - (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui
68 {
69 return [self hostSupportsItem:ui] && [self isItemNewer:ui] && ![self itemContainsSkippedVersion:ui];
70 }
71
72 - (void)appcastDidFinishLoading:(SUAppcast *)ac
73 {
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
74 if ([delegate respondsToSelector:@selector(appcastDidFinishLoading:forHostBundle:)])
75 [delegate appcastDidFinishLoading:ac forHostBundle:hostBundle];
251706d @andymatuschak Fixed a serious bug which could cause a server to DDoS'd in the case of ...
andymatuschak authored
76
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
77 // Now we have to find the best valid update in the appcast.
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
78 if ([delegate respondsToSelector:@selector(bestValidUpdateInAppcast:forHostBundle:)]) // Does the delegate want to handle it?
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
79 {
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
80 updateItem = [delegate bestValidUpdateInAppcast:ac forHostBundle:hostBundle];
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
81 }
82 else // If not, we'll take care of it ourselves.
83 {
84 // Find the first update we can actually use.
251706d @andymatuschak Fixed a serious bug which could cause a server to DDoS'd in the case of ...
andymatuschak authored
85 NSEnumerator *updateEnumerator = [[ac items] objectEnumerator];
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
86 do {
87 updateItem = [updateEnumerator nextObject];
88 } while (updateItem && ![self hostSupportsItem:updateItem]);
89
90 [updateItem retain];
91 }
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
92 CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
93 if (updateItem == nil) { [self didNotFindUpdate]; return; }
94
95 if ([self itemContainsValidUpdate:updateItem])
96 [self didFindValidUpdate];
97 else
98 [self didNotFindUpdate];
99 }
100
101 - (void)appcast:(SUAppcast *)ac failedToLoadWithError:(NSError *)error
102 {
103 CFRelease(ac); // Remember that we're explicitly managing the memory of the appcast.
104 [self abortUpdateWithError:error];
105 }
106
107 - (void)didFindValidUpdate
108 {
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
109 if ([delegate respondsToSelector:@selector(didFindValidUpdate:toHostBundle:)])
110 [delegate didFindValidUpdate:updateItem toHostBundle:hostBundle];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
111 [self downloadUpdate];
112 }
113
114 - (void)didNotFindUpdate
115 {
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
116 if ([delegate respondsToSelector:@selector(didNotFindUpdateToHostBundle:)])
117 [delegate didNotFindUpdateToHostBundle:hostBundle];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
118 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUNoUpdateError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:SULocalizedString(@"You already have the newest version of %@.", nil), [hostBundle name]] forKey:NSLocalizedDescriptionKey]]];
119 }
120
121 - (void)downloadUpdate
122 {
123 download = [[NSURLDownload alloc] initWithRequest:[NSURLRequest requestWithURL:[updateItem fileURL]] delegate:self];
124 }
125
126 - (void)download:(NSURLDownload *)d decideDestinationWithSuggestedFilename:(NSString *)name
127 {
128 // If name ends in .txt, the server probably has a stupid MIME configuration. We'll give the developer the benefit of the doubt and chop that off.
129 if ([[name pathExtension] isEqualToString:@"txt"])
130 name = [name stringByDeletingPathExtension];
131
132 // We create a temporary directory in /tmp and stick the file there.
133 // Not using a GUID here because hdiutil for some reason chokes on GUIDs. Too long? I really have no idea.
134 NSString *prefix = [NSString stringWithFormat:@"%@ %@ Update", [hostBundle name], [hostBundle version]];
135 NSString *tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:prefix];
136 int cnt=1;
137 while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999999)
138 tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d", prefix, cnt++]];
139 BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:tempDir attributes:nil];
140 if (!success)
141 {
142 // Okay, something's really broken with /tmp
143 [download cancel];
144 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUTemporaryDirectoryError userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Can't make a temporary directory for the update download at %@.",tempDir] forKey:NSLocalizedDescriptionKey]]];
145 }
146
147 downloadPath = [[tempDir stringByAppendingPathComponent:name] retain];
148 [download setDestination:downloadPath allowOverwrite:YES];
149 }
150
151 - (void)downloadDidFinish:(NSURLDownload *)d
152 {
153 [self extractUpdate];
154 }
155
156 - (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
157 {
49f717c @andymatuschak Fixes #230173 by removing partially downloaded files on download error.
andymatuschak authored
158 // Get rid of what we've downloaded so far, if anything.
159 if (downloadPath != nil)
160 [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[downloadPath stringByDeletingLastPathComponent] destination:@"" files:[NSArray arrayWithObject:[downloadPath lastPathComponent]] tag:NULL];
8dfd93e @andymatuschak Made the error displayed on download failure human-friendly.
andymatuschak authored
161 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while downloading the update. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
162 }
163
164 - (void)extractUpdate
165 {
166 // DSA verification, if activated by the developer
167 if ([[hostBundle objectForInfoDictionaryKey:SUExpectsDSASignatureKey] boolValue])
168 {
21b4aee @andymatuschak Fixes bug 228454.
andymatuschak authored
169 if (![[NSFileManager defaultManager] validatePath:downloadPath withEncodedDSASignature:[updateItem DSASignature] withPublicDSAKey:[hostBundle publicDSAKey]])
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
170 {
171 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUSignatureError userInfo:[NSDictionary dictionaryWithObject:@"The update is improperly signed." forKey:NSLocalizedDescriptionKey]]];
172 return;
173 }
174 }
175
3daa88d @andymatuschak --fixes lp:242525 --fixes lp:242561 --fixes lp:242564
andymatuschak authored
176 SUUnarchiver *unarchiver = [SUUnarchiver unarchiverForURL:[[[NSURL alloc] initFileURLWithPath:downloadPath] autorelease]];
441a538 @andymatuschak Oops! Fixed a warning that caused Sparkle not to compile. Made the unarc...
andymatuschak authored
177 if (!unarchiver)
178 {
179 NSLog(@"Sparkle Error: No valid unarchiver for %@!", downloadPath);
180 [self unarchiverDidFail:nil];
764bed3 @andymatuschak Okay, *really* fixing the bug this time. Thanks for watching my back, Sc...
andymatuschak authored
181 return;
441a538 @andymatuschak Oops! Fixed a warning that caused Sparkle not to compile. Made the unarc...
andymatuschak authored
182 }
3daa88d @andymatuschak --fixes lp:242525 --fixes lp:242561 --fixes lp:242564
andymatuschak authored
183 CFRetain(unarchiver); // Manage this memory manually so we don't have to make it an IV.
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
184 [unarchiver setDelegate:self];
93ea93d @andymatuschak Fixes 236695
andymatuschak authored
185 [unarchiver start];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
186 }
187
188 - (void)unarchiverDidFinish:(SUUnarchiver *)ua
189 {
441a538 @andymatuschak Oops! Fixed a warning that caused Sparkle not to compile. Made the unarc...
andymatuschak authored
190 if (ua) { CFRelease(ua); }
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
191 [self installUpdate];
192 }
193
194 - (void)unarchiverDidFail:(SUUnarchiver *)ua
195 {
441a538 @andymatuschak Oops! Fixed a warning that caused Sparkle not to compile. Made the unarc...
andymatuschak authored
196 if (ua) { CFRelease(ua); }
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
197 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:[NSDictionary dictionaryWithObject:SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil) forKey:NSLocalizedDescriptionKey]]];
198 }
199
6d8f5be @andymatuschak Merging in David Smith's branch to fix bug #230123. We now copy the rela...
andymatuschak authored
200 - (BOOL)shouldInstallSynchronously { return NO; }
201
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
202 - (void)installUpdate
203 {
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
204 if ([delegate respondsToSelector:@selector(updateWillInstall:toHostBundle:)])
205 [delegate updateWillInstall:updateItem toHostBundle:hostBundle];
6d8f5be @andymatuschak Merging in David Smith's branch to fix bug #230123. We now copy the rela...
andymatuschak authored
206 // Copy the relauncher into a temporary directory so we can get to it after the new version's installed.
207 NSString *relaunchPathToCopy = [[NSBundle bundleForClass:[self class]] pathForResource:@"relaunch" ofType:@""];
208 NSString *targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[relaunchPathToCopy lastPathComponent]];
209 // Only the paranoid survive: if there's already a stray copy of relaunch there, we would have problems
210 [[NSFileManager defaultManager] removeFileAtPath:targetPath handler:nil];
211 if ([[NSFileManager defaultManager] copyPath:relaunchPathToCopy toPath:targetPath handler:nil])
212 relaunchPath = [targetPath retain];
213
214 [SUInstaller installFromUpdateFolder:[downloadPath stringByDeletingLastPathComponent] overHostBundle:hostBundle delegate:self synchronously:[self shouldInstallSynchronously]];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
215 }
216
217 - (void)installerFinishedForHostBundle:(NSBundle *)hb
218 {
219 if (hb != hostBundle) { return; }
220 [self relaunchHostApp];
221 }
222
223 - (void)relaunchHostApp
224 {
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
225 // Give the host app an opportunity to postpone the relaunch.
226 static BOOL postponedOnce = NO;
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
227 if (!postponedOnce && [delegate respondsToSelector:@selector(shouldPostponeRelaunchForUpdate:toHostBundle:untilInvoking:)])
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
228 {
229 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[[self class] instanceMethodSignatureForSelector:@selector(relaunchHostApp)] retain]];
230 [invocation setSelector:@selector(relaunchHostApp)];
231 [invocation setTarget:self];
232 postponedOnce = YES;
7d8ada5 @andymatuschak Improved delegate names and made SUProbingUpdateDriver use SUUpdater's d...
andymatuschak authored
233 if ([delegate shouldPostponeRelaunchForUpdate:updateItem toHostBundle:hostBundle untilInvoking:invocation])
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
234 return;
235 }
46f9b4a @andymatuschak Fixes 242566
andymatuschak authored
236
237 [self cleanUp]; // Clean up the download and extracted files.
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
238
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
239 [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self];
5c32832 @andymatuschak Fixes bug 228446
andymatuschak authored
240 if ([delegate respondsToSelector:@selector(updaterWillRelaunchApplication)])
241 [delegate updaterWillRelaunchApplication];
242
1c7fb59 @andymatuschak Killed one of the two remaining @try/@catch blocks in my code. I'll get ...
andymatuschak authored
243 if(!relaunchPath || ![[NSFileManager defaultManager] fileExistsAtPath:relaunchPath])
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
244 {
245 // Note that we explicitly use the host app's name here, since updating plugin for Mail relaunches Mail, not just the plugin.
1c7fb59 @andymatuschak Killed one of the two remaining @try/@catch blocks in my code. I'll get ...
andymatuschak authored
246 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:SULocalizedString(@"An error occurred while relaunching %1$@, but the new version will be available next time you run %1$@.", nil), [[NSBundle mainBundle] name]], NSLocalizedDescriptionKey, [NSString stringWithFormat:@"Couldn't find the relauncher (expected to find it at %@)", relaunchPath], NSLocalizedFailureReasonErrorKey, nil]]];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
247 // We intentionally don't abandon the update here so that the host won't initiate another.
1c7fb59 @andymatuschak Killed one of the two remaining @try/@catch blocks in my code. I'll get ...
andymatuschak authored
248 return;
249 }
250
251 [NSTask launchedTaskWithLaunchPath:relaunchPath arguments:[NSArray arrayWithObjects:[hostBundle bundlePath], [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]], nil]];
252
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
253 [NSApp terminate:self];
254 }
255
46f9b4a @andymatuschak Fixes 242566
andymatuschak authored
256 - (void)cleanUp
257 {
258 [[NSFileManager defaultManager] removeFileAtPath:[downloadPath stringByDeletingLastPathComponent] handler:nil];
259 }
260
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
261 - (void)installerForHostBundle:(NSBundle *)hb failedWithError:(NSError *)error
262 {
263 if (hb != hostBundle) { return; }
6d8f5be @andymatuschak Merging in David Smith's branch to fix bug #230123. We now copy the rela...
andymatuschak authored
264 [[NSFileManager defaultManager] removeFileAtPath:relaunchPath handler:NULL]; // Clean up the copied relauncher.
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
265 [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:SULocalizedString(@"An error occurred while installing the update. Please try again later.", nil), NSLocalizedDescriptionKey, [error localizedDescription], NSLocalizedFailureReasonErrorKey, nil]]];
266 }
267
268 - (void)abortUpdate
269 {
270 [[NSNotificationCenter defaultCenter] removeObserver:self];
271 [super abortUpdate];
272 }
273
274 - (void)abortUpdateWithError:(NSError *)error
275 {
276 if ([error code] != SUNoUpdateError) // Let's not bother logging this.
277 NSLog(@"Sparkle Error: %@", [error localizedDescription]);
278 if ([error localizedFailureReason])
279 NSLog(@"Sparkle Error (continued): %@", [error localizedFailureReason]);
0a4bc65 @andymatuschak No longer leaking the NSURLDownload of the update archive.
andymatuschak authored
280 if (download)
281 [download cancel];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
282 [self abortUpdate];
283 }
284
285 - (void)dealloc
286 {
287 [hostBundle release];
0a4bc65 @andymatuschak No longer leaking the NSURLDownload of the update archive.
andymatuschak authored
288 [download release];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
289 [downloadPath release];
6d8f5be @andymatuschak Merging in David Smith's branch to fix bug #230123. We now copy the rela...
andymatuschak authored
290 [relaunchPath release];
bc3be9a Touched practically every line of code in a super-monster-awesome refact...
andym authored
291 [super dealloc];
292 }
293
294 @end
Something went wrong with that request. Please try again.