/
QSUpdateController.m
481 lines (409 loc) · 17.8 KB
/
QSUpdateController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//
// QSUpdateController.m
// Quicksilver
//
// Created by Alcor on 7/22/04.
// Copyright 2004 Blacktree. All rights reserved.
//
// Ankur, Dec 12:
// update task is now cancelled on "connection error".
// networkIsReachable returning YES. commented out.
#import "Quicksilver.h"
#import "QSUpdateController.h"
@implementation QSUpdateController
+ (id)sharedInstance {
static id _sharedInstance;
if (!_sharedInstance) _sharedInstance = [[[self class] allocWithZone:[self zone]] init];
return _sharedInstance;
}
- (id)init {
self = [super init];
return self;
}
- (void)forceStartupCheck {
NSLog(@"Updated: Forcing Plugin Check");
doStartupCheck = YES;
}
- (void)setUpdateTimer {
// ***warning * fix me
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
#ifdef DEBUG
if (![defaults boolForKey:@"QSPreventAutomaticUpdate"]) {
#else
if ([defaults boolForKey:kCheckForUpdates]) {
#endif
NSDate *lastCheck = [defaults objectForKey:kLastUpdateCheck];
// leaving this `nil` can cause Quicksilver to hang if it starts very soon after login
if (!lastCheck) {
lastCheck = [NSDate distantPast];
}
NSInteger frequency = [defaults integerForKey:kCheckForUpdateFrequency];
#ifdef DEBUG
NSInteger versionType = [defaults integerForKey:@"QSUpdateReleaseLevel"];
if (versionType>0 && frequency>1)
frequency = 1;
#endif
BOOL shouldRepeat = (frequency>0);
NSTimeInterval checkInterval = frequency*24*60*60;
//NSLog(@"Last Version Check at : %@", [lastCheck description]);
NSDate *nextCheck = [[NSDate alloc] initWithTimeInterval:checkInterval sinceDate:lastCheck];
//nextCheck = [NSDate distantPast];
//nextCheck = [NSDate dateWithTimeIntervalSinceNow: 20.0];
if (updateTimer) {
[updateTimer invalidate];
[updateTimer release];
}
updateTimer = [[NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(threadedCheckForUpdate:) userInfo:nil repeats:shouldRepeat] retain];
[updateTimer setFireDate:( doStartupCheck ? [NSDate dateWithTimeIntervalSinceNow:33.333f] : nextCheck )];
#ifdef DEBUG
if (VERBOSE) NSLog(@"Next Version Check at : %@", [[updateTimer fireDate] description]);
#endif
[nextCheck release];
}
}
- (NSURL *)buildUpdateCheckURL {
NSString *checkURL = [[[NSProcessInfo processInfo] environment] objectForKey:@"QSCheckUpdateURL"];
if (!checkURL)
checkURL = kCheckUpdateURL;
NSString *thisVersionString = (NSString *)CFBundleGetValueForInfoDictionaryKey(CFBundleGetMainBundle(), kCFBundleVersionKey);
NSString *versionType = nil;
switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"QSUpdateReleaseLevel"]) {
case 2:
versionType = @"dev";
break;
case 1:
versionType = @"pre";
break;
default:
versionType = @"rel";
break;
}
checkURL = [checkURL stringByAppendingFormat:@"?type=%@¤t=%@", versionType, thisVersionString];
#ifdef DEBUG
if (VERBOSE) NSLog(@"Update Check URL: %@", checkURL);
#endif
return [NSURL URLWithString:checkURL];
}
typedef enum {
kQSUpdateCheckSkip = -2,
kQSUpdateCheckError = -1,
kQSUpdateCheckNoUpdate = 0,
kQSUpdateCheckUpdateAvailable = 1,
} QSUpdateCheckResult;
- (QSUpdateCheckResult)checkForUpdates:(BOOL)force {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([defaults boolForKey:@"QSPreventAutomaticUpdate"] || (![defaults boolForKey:kCheckForUpdates] && !force)) {
NSLog(@"Preventing update check.");
return kQSUpdateCheckSkip;
}
NSString *thisVersionString = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
NSString *checkVersionString = nil;
NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:[self buildUpdateCheckURL] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
[theRequest setValue:kQSUserAgent forHTTPHeaderField:@"User-Agent"];
NSData *data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:nil];
checkVersionString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
[defaults setObject:[NSDate date] forKey:kLastUpdateCheck];
if (![checkVersionString length] || [checkVersionString length] > 10) {
NSLog(@"Unable to check for new version.");
[[QSTaskController sharedInstance] removeTask:@"Check for Update"];
return kQSUpdateCheckError;
}
BOOL newVersionAvailable = [checkVersionString hexIntValue] > [thisVersionString hexIntValue];
/* We have to get the current available version, because it will get displayed to the user,
* so force happens only if there's a valid response from the server
*/
newVersion = [checkVersionString retain];
#ifdef DEBUG
if (VERBOSE)
NSLog(@"Installed Version: %@, Available Version: %@, Valid: %@, Force update: %@", thisVersionString, checkVersionString, (newVersionAvailable ? @"YES" : @"NO"), (force ? @"YES" : @"NO"));
#endif
return newVersionAvailable ? kQSUpdateCheckUpdateAvailable : kQSUpdateCheckNoUpdate;
}
- (BOOL)checkForUpdatesInBackground:(BOOL)quiet force:(BOOL)force {
[[QSTaskController sharedInstance] updateTask:@"Check for Update" status:@"Check for Update" progress:-1];
BOOL updated = NO;
NSInteger check = [self checkForUpdates:force];
[[QSTaskController sharedInstance] removeTask:@"Check for Update"];
switch (check) {
case kQSUpdateCheckError:
if (!quiet)
NSRunInformationalAlertPanel(@"Internet Connection Error", @"Unable to check for updates, the server could not be reached. Please check your internet connection", @"OK", nil, nil);
return NO;
break;
case kQSUpdateCheckUpdateAvailable:
if (!force && [[NSUserDefaults standardUserDefaults] boolForKey:@"QSDownloadUpdatesInBackground"]) {
/** Diable automatically checking for updates in the background for DEBUG builds
You can still check for updates by clicking the "Check Now" button **/
#ifndef DEBUG
[self performSelectorOnMainThread:@selector(installAppUpdate) withObject:nil waitUntilDone:NO];
#endif
} else {
NSInteger selection = NSRunInformationalAlertPanel([NSString stringWithFormat:@"New Version of Quicksilver Available", nil], @"A new version of Quicksilver is available, would you like to update now?\n\n(Update from %@ → %@)", @"Install Update", @"Later", nil, [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey],newVersion); //, @"More Info");
if (selection == 1) {
[self performSelectorOnMainThread:@selector(installAppUpdate) withObject:nil waitUntilDone:NO];
} else if (selection == -1) { //Go to web site
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kWebSiteURL]];
}
}
return YES;
break;
case kQSUpdateCheckNoUpdate:
{
QSPluginUpdateStatus updateStatus;
updateStatus = [[QSPlugInManager sharedInstance] checkForPlugInUpdates];
if (updateStatus == QSPluginUpdateStatusNoUpdates) {
updated = NO;
NSLog(@"Quicksilver is up to date.");
if (!quiet)
NSRunInformationalAlertPanel(@"You're up-to-date!", [NSString stringWithFormat:@"You already have the latest version of Quicksilver (%@) and all installed plugins", [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]] , @"OK", nil, nil);
}
return updated;
break;
}
default:
case kQSUpdateCheckSkip:
break;
}
return NO;
}
- (BOOL)threadedCheckForUpdates:(BOOL)force {
@autoreleasepool {
BOOL res = [self checkForUpdatesInBackground:NO force:force];
return res;
}
}
- (BOOL)threadedCheckForUpdatesInBackground:(BOOL)force {
@autoreleasepool {
BOOL res = [self checkForUpdatesInBackground:YES force:force];
return res;
}
}
- (IBAction)checkForUpdate:(id)sender {
BOOL quiet = !sender || sender == self || [sender isKindOfClass:[NSTimer class]];
BOOL forceUpdate = [sender isEqual:@"Force"];
[self checkForUpdatesInBackground:quiet force:forceUpdate];
}
- (void)handleURL:(NSURL *)url {
[self threadedCheckForUpdatesInBackground:NO];
}
- (IBAction)threadedCheckForUpdate:(id)sender {
// Test to see if the update request is an automatic request (e.g. on launch)
BOOL quiet = !sender || sender == self || [sender isKindOfClass:[NSTimer class]];
if (quiet) {
[self threadedCheckForUpdatesInBackground:NO];
}
else {
[self threadedCheckForUpdates:NO];
}
}
- (IBAction)threadedRequestedCheckForUpdate:(id)sender {
[self threadedCheckForUpdates:YES];
}
- (void)installAppUpdate {
if (updateTask) return;
NSString *fileURL = [[[NSProcessInfo processInfo] environment] objectForKey:@"QSDownloadUpdateURL"];
if (!fileURL)
fileURL = kDownloadUpdateURL;
fileURL = [fileURL stringByAppendingFormat:@"?id=%@&type=dmg&new=yes", [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]];
NSInteger versionType = [[NSUserDefaults standardUserDefaults] integerForKey:@"QSUpdateReleaseLevel"];
if (versionType == 2)
fileURL = [fileURL stringByAppendingString:@"&dev=1"];
else if (versionType == 1)
fileURL = [fileURL stringByAppendingString:@"&pre=1"];
#ifdef DEBUG
if (VERBOSE) NSLog(@"Downloading update from %@", fileURL);
#endif
NSURL *url = [NSURL URLWithString:fileURL];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
// NSLog(@"app %@", theRequest);
// create the connection with the request
// and start loading the data
appDownload = [[QSURLDownload alloc] initWithRequest:theRequest delegate:(id)self];
if (appDownload) {
updateTask = [[QSTask taskWithIdentifier:@"QSAppUpdateInstalling"] retain];
[updateTask setName:@"Downloading Update"];
[updateTask setProgress:-1];
[updateTask setCancelAction:@selector(cancelUpdate:)];
[updateTask setCancelTarget:self];
[QSTaskController showViewer];
[updateTask startTask:nil];
[appDownload start];
}
}
//- (NSDictionary *)downloadInfoForDownload:(NSURLDownload *)download {
// //NSLog(@"url %@ %@", [appDownload objectForKey:@"download"] , download);
// if ([appDownload isEqual:download]) return appDownload;
//
// NSEnumerator *e = [[self downloadsQueue] objectEnumerator];
//
// NSMutableDictionary *info;
// while(info = [e nextObject]) {
// if ([[info objectForKey:@"download"] isEqual:download]) break;
// }
// return info;
//}
- (void)download:(QSURLDownload *)download didFailWithError:(NSError *)error {
if (download != appDownload)
return;
NSLog(@"Download Failed: %@", error);
// [[QSTaskController sharedInstance] removeTask:@"QSAppUpdateInstalling"];
[updateTask stopTask:nil];
[updateTask release];
updateTask = nil;
NSRunInformationalAlertPanel(@"Download Failed", @"An error occured while updating: %@", @"OK", nil, nil, [error localizedDescription] );
[appDownload cancel];
[appDownload release], appDownload = nil;
}
- (void)downloadDidFinish:(QSURLDownload *)download {
if (download != appDownload)
return;
[updateTask setStatus:@"Download Complete"];
[updateTask setProgress:1.0];
BOOL plugInUpdates = [[QSPlugInManager sharedInstance] updatePlugInsForNewVersion:newVersion];
if (plugInUpdates) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(finishAppInstall)
name:@"QSPlugInUpdatesFinished"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(finishAppInstall)
name:@"QSPlugInUpdatesFailed"
object:nil];
} else {
NSLog(@"Plugins don't need update");
[self finishAppInstall];
}
}
- (void)downloadDidUpdate:(QSURLDownload *)download {
NSString * status = [NSString stringWithFormat:@"%.0fk of %.0fk", (double) [download currentContentLength] /1024, (double)[download expectedContentLength] /1024];
[updateTask setStatus:status];
[updateTask setProgress:[(QSURLDownload *)download progress]];
}
- (void)cancelUpdate:(QSTask *)task {
shouldCancel = YES;
[appDownload cancel];
[appDownload release], appDownload = nil;
[updateTask stopTask:nil];
[updateTask release];
updateTask = nil;
}
- (void)finishAppInstall {
NSString *path = [appDownload destination];
NSInteger selection;
BOOL update = [[NSUserDefaults standardUserDefaults] boolForKey:@"QSUpdateWithoutAsking"];
if (!update) {
selection = NSRunInformationalAlertPanel(@"Download Successful", @"A new version of Quicksilver has been downloaded. Quicksilver must relaunch to install it.", @"Install and Relaunch", @"Cancel Update", nil);
update = (selection == NSAlertDefaultReturn);
}
BOOL installSuccessful = NO;
if (update) {
installSuccessful = [self installAppFromDiskImage:path];
if (!installSuccessful) {
selection = NSRunInformationalAlertPanel(@"Installation Failed", @"It was not possible to decompress downloaded file.", @"Cancel Update", @"Download manually", nil);
if (selection == NSAlertAlternateReturn)
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kWebSiteURL]];
}
}
if (installSuccessful) {
BOOL relaunch = [[NSUserDefaults standardUserDefaults] boolForKey:@"QSUpdateWithoutAsking"];
if (!relaunch) {
selection = NSRunInformationalAlertPanel(@"Installation Successful", @"A new version of Quicksilver has been installed. Quicksilver must relaunch to install it.", @"Relaunch", @"Relaunch Later", nil);
relaunch = (selection == NSAlertDefaultReturn);
}
if (relaunch) {
[NSApp relaunchFromPath:nil];
}
}
[updateTask stopTask:nil];
[updateTask release], updateTask = nil;
[appDownload release], appDownload = nil;
}
- (BOOL)installAppFromDiskImage:(NSString *)path {
NSFileManager *manager = [NSFileManager defaultManager];
// Create a temp directory to mount the .dmg
NSError *err = nil;
NSString *tempDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString uniqueString]];
[manager createDirectoryAtPath:tempDirectory withIntermediateDirectories:YES
attributes:nil error:&err];
if(err) {
NSLog(@"Error: %@", err);
return NO;
}
[updateTask setName:@"Installing Update"];
[updateTask setStatus:@"Verifying Data"];
[updateTask setProgress:-1.0];
// mount the .dmg
NSTask *task = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/hdiutil"
arguments:[NSArray arrayWithObjects:@"attach", path, @"-nobrowse", @"-mountpoint", tempDirectory, nil]];
[task waitUntilExit];
if ([task terminationStatus] != 0)
return NO;
NSArray *extracted = [[manager contentsOfDirectoryAtPath:tempDirectory error:nil] pathsMatchingExtensions:[NSArray arrayWithObject:@"app"]];
if ([extracted count] != 1)
return NO;
NSString *mountedAppPath = [tempDirectory stringByAppendingPathComponent:[extracted lastObject]];
if (!mountedAppPath) {
return NO;
}
// Copy Quicksilver.app from the .dmg to a writeable folder (QS App Support folder)
// Attempt to delete any old update folders
if ([manager fileExistsAtPath:pUpdatePath]) {
[manager removeItemAtPath:pUpdatePath error:&err];
if (err) {
// report the error, but attempt to carry on
NSLog(@"Error: %@",err);
err = nil;
}
}
[manager createDirectoryAtPath:pUpdatePath withIntermediateDirectories:YES attributes:nil error:&err];
if (err) {
NSLog(@"Error: %@",err);
return NO;
}
NSString *storedAppPath = [pUpdatePath stringByAppendingPathComponent:[mountedAppPath lastPathComponent]];
NSError *copyErr = nil;
[manager copyItemAtPath:mountedAppPath toPath:storedAppPath error:©Err];
if (copyErr) {
NSLog(@"Error: %@",copyErr);
return NO;
}
// Copy the Application over the current app
[updateTask setStatus:@"Copying Application"];
BOOL copySuccess = [NSApp moveToPath:[[NSBundle mainBundle] bundlePath] fromPath:storedAppPath];
[updateTask setStatus:@"Cleaning Up"];
// Unmount .dmg and tidyup
task = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/hdiutil"
arguments:[NSArray arrayWithObjects:@"detach", tempDirectory, nil]];
[task waitUntilExit];
[manager removeItemAtPath:tempDirectory error:&err];
if(err) {
// Couldn't delete the temp directory. Not the end of the world: report and continue
NSLog(@"Error: %@",err);
err = nil;
}
[manager removeItemAtPath:pUpdatePath error:&err];
if(err) {
// Couldn't delete the update directory. Not the end of the world: report and continue
NSLog(@"Error: %@",err);
err = nil;
}
return copySuccess;
}
- (NSArray *)extractFilesFromQSPkg:(NSString *)path toPath:(NSString *)tempDirectory {
if (!path) return nil;
NSFileManager *manager = [NSFileManager defaultManager];
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath:@"/usr/bin/ditto"];
[task setArguments:[NSArray arrayWithObjects:@"-x", @"-rsrc", path, tempDirectory, nil]];
[task launch];
[task waitUntilExit];
NSInteger status = [task terminationStatus];
if (status == 0) {
[manager removeItemAtPath:path error:nil];
[[NSWorkspace sharedWorkspace] noteFileSystemChanged:[path stringByDeletingLastPathComponent]];
return [manager contentsOfDirectoryAtPath:tempDirectory error:nil];
} else {
return nil;
}
}
@end