Index: chrome/app/keystone_glue.mm |
=================================================================== |
--- chrome/app/keystone_glue.mm (revision 30049) |
+++ chrome/app/keystone_glue.mm (working copy) |
@@ -2,17 +2,13 @@ |
// Use of this source code is governed by a BSD-style license that can be |
// found in the LICENSE file. |
-#include "base/mac_util.h" |
#import "chrome/app/keystone_glue.h" |
-@interface KeystoneGlue(Private) |
+#include "base/logging.h" |
+#include "base/mac_util.h" |
+#import "base/worker_pool_mac.h" |
+#include "chrome/common/chrome_constants.h" |
-// Called periodically to announce activity by pinging the Keystone server. |
-- (void)markActive:(NSTimer*)timer; |
- |
-@end |
- |
- |
// Provide declarations of the Keystone registration bits needed here. From |
// KSRegistration.h. |
typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType; |
@@ -31,7 +27,9 @@ |
#define KSRegistrationPreserveExistingTag nil |
@interface KSRegistration : NSObject |
+ |
+ (id)registrationWithProductID:(NSString*)productID; |
+ |
// Older API |
- (BOOL)registerWithVersion:(NSString*)version |
existenceCheckerType:(KSExistenceCheckerType)xctype |
@@ -43,20 +41,65 @@ |
existenceCheckerString:(NSString*)xc |
serverURLString:(NSString*)serverURLString |
preserveTTToken:(BOOL)preserveToken |
- tag:(NSString *)tag; |
+ tag:(NSString*)tag; |
+ |
- (void)setActive; |
- (void)checkForUpdate; |
- (void)startUpdate; |
-@end |
+@end // @interface KSRegistration |
+@interface KeystoneGlue(Private) |
+ |
+// Called periodically to announce activity by pinging the Keystone server. |
+- (void)markActive:(NSTimer*)timer; |
+ |
+// Called when an update check or update installation is complete. Posts the |
+// kAutoupdateStatusNotification notification to the default notification |
+// center. |
+- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; |
+ |
+// These three methods are used to determine the version of the application |
+// currently installed on disk, compare that to the currently-running version, |
+// decide whether any updates have been installed, and call |
+// -updateStatus:version:. |
+// |
+// In order to check the version on disk, the installed application's |
+// Info.plist dictionary must be read; in order to see changes as updates are |
+// applied, the dictionary must be read each time, bypassing any caches such |
+// as the one that NSBundle might be maintaining. Reading files can be a |
+// blocking operation, and blocking operations are to be avoided on the main |
+// thread. I'm not quite sure what jank means, but I bet that a blocked main |
+// thread would cause some of it. |
+// |
+// -determineUpdateStatusAsync is called on the main thread to initiate the |
+// operation. It performs initial set-up work that must be done on the main |
+// thread and arranges for -determineUpdateStatusAtPath: to be called on a |
+// work queue thread managed by NSOperationQueue. |
+// -determineUpdateStatusAtPath: then reads the Info.plist, gets the version |
+// from the CFBundleShortVersionString key, and performs |
+// -determineUpdateStatusForVersion: on the main thread. |
+// -determineUpdateStatusForVersion: does the actual comparison of the version |
+// on disk with the running version and calls -updateStatus:version: with the |
+// results of its analysis. |
+- (void)determineUpdateStatusAsync; |
+- (void)determineUpdateStatusAtPath:(NSString*)appPath; |
+- (void)determineUpdateStatusForVersion:(NSString*)version; |
+ |
+@end // @interface KeystoneGlue(Private) |
+ |
+const NSString* const kAutoupdateStatusNotification = |
+ @"AutoupdateStatusNotification"; |
+const NSString* const kAutoupdateStatusStatus = @"status"; |
+const NSString* const kAutoupdateStatusVersion = @"version"; |
+ |
@implementation KeystoneGlue |
+// TODO(jrg): use base::SingletonObjC<KeystoneGlue> |
+static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked |
+ |
+ (id)defaultKeystoneGlue { |
- // TODO(jrg): use base::SingletonObjC<KeystoneGlue> |
- static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked |
- |
- if (sDefaultKeystoneGlue == nil) { |
+ if (!sDefaultKeystoneGlue) { |
sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; |
[sDefaultKeystoneGlue loadParameters]; |
if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { |
@@ -67,6 +110,29 @@ |
return sDefaultKeystoneGlue; |
} |
++ (void)releaseDefaultKeystoneGlue { |
+ [sDefaultKeystoneGlue release]; |
+ sDefaultKeystoneGlue = nil; |
+} |
+ |
+- (id)init { |
+ if ((self = [super init])) { |
+ NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
+ |
+ [center addObserver:self |
+ selector:@selector(checkForUpdateComplete:) |
+ name:KSRegistrationCheckForUpdateNotification |
+ object:nil]; |
+ |
+ [center addObserver:self |
+ selector:@selector(installUpdateComplete:) |
+ name:KSRegistrationStartUpdateNotification |
+ object:nil]; |
+ } |
+ |
+ return self; |
+} |
+ |
- (void)dealloc { |
[url_ release]; |
[productID_ release]; |
@@ -174,64 +240,155 @@ |
[ksr setActive]; |
} |
-- (void)checkComplete:(NSNotification *)notification { |
- NSDictionary *userInfo = [notification userInfo]; |
- BOOL updatesAvailable = [[userInfo objectForKey:KSRegistrationStatusKey] |
- boolValue]; |
- NSString *latestVersion = [userInfo objectForKey:KSRegistrationVersionKey]; |
+- (void)checkForUpdate { |
+ if (!registration_) { |
+ [self updateStatus:kAutoupdateCheckFailed version:nil]; |
+ return; |
+ } |
- [checkTarget_ upToDateCheckCompleted:updatesAvailable |
- latestVersion:latestVersion]; |
- [checkTarget_ release]; |
- checkTarget_ = nil; |
+ [registration_ checkForUpdate]; |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- [center removeObserver:self |
- name:KSRegistrationCheckForUpdateNotification |
- object:nil]; |
+ // Upon completion, KSRegistrationCheckForUpdateNotification will be posted, |
+ // and -checkForUpdateComplete: will be called. |
} |
-- (BOOL)checkForUpdate:(NSObject<KeystoneGlueCallbacks>*)target { |
- if (registration_ == nil) |
- return NO; |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- [center addObserver:self |
- selector:@selector(checkComplete:) |
- name:KSRegistrationCheckForUpdateNotification |
- object:nil]; |
- checkTarget_ = [target retain]; |
- [registration_ checkForUpdate]; |
- return YES; |
+- (void)checkForUpdateComplete:(NSNotification*)notification { |
+ NSDictionary* userInfo = [notification userInfo]; |
+ BOOL updatesAvailable = |
+ [[userInfo objectForKey:KSRegistrationStatusKey] boolValue]; |
+ |
+ if (updatesAvailable) { |
+ // If an update is known to be available, go straight to |
+ // -updateStatus:version:. It doesn't matter what's currently on disk. |
+ NSString* version = [userInfo objectForKey:KSRegistrationVersionKey]; |
+ [self updateStatus:kAutoupdateAvailable version:version]; |
+ } else { |
+ // If no updates are available, check what's on disk, because an update |
+ // may have already been installed. This check happens on another thread, |
+ // and -updateStatus:version: will be called on the main thread when done. |
+ [self determineUpdateStatusAsync]; |
+ } |
} |
-- (void)startUpdateComplete:(NSNotification *)notification { |
- NSDictionary *userInfo = [notification userInfo]; |
- BOOL checkSuccessful = [[userInfo objectForKey:KSUpdateCheckSuccessfulKey] |
- boolValue]; |
- int installs = [[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] |
- intValue]; |
+- (void)installUpdate { |
+ if (!registration_) { |
+ [self updateStatus:kAutoupdateInstallFailed version:nil]; |
+ return; |
+ } |
- [startTarget_ updateCompleted:checkSuccessful installs:installs]; |
- [startTarget_ release]; |
- startTarget_ = nil; |
+ [registration_ startUpdate]; |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- [center removeObserver:self |
- name:KSRegistrationStartUpdateNotification |
- object:nil]; |
+ // Upon completion, KSRegistrationStartUpdateNotification will be posted, |
+ // and -installUpdateComplete: will be called. |
} |
-- (BOOL)startUpdate:(NSObject<KeystoneGlueCallbacks>*)target { |
- if (registration_ == nil) |
- return NO; |
- NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
- [center addObserver:self |
- selector:@selector(startUpdateComplete:) |
- name:KSRegistrationStartUpdateNotification |
- object:nil]; |
- startTarget_ = [target retain]; |
- [registration_ startUpdate]; |
- return YES; |
+- (void)installUpdateComplete:(NSNotification*)notification { |
+ NSDictionary* userInfo = [notification userInfo]; |
+ BOOL checkSuccessful = |
+ [[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue]; |
+ int installs = |
+ [[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] intValue]; |
+ |
+ if (!checkSuccessful || !installs) { |
+ [self updateStatus:kAutoupdateInstallFailed version:nil]; |
+ } else { |
+ updateSuccessfullyInstalled_ = YES; |
+ |
+ // Nothing in the notification dictionary reports the version that was |
+ // installed. Figure it out based on what's on disk. |
+ [self determineUpdateStatusAsync]; |
+ } |
} |
-@end |
+// Runs on the main thread. |
+- (void)determineUpdateStatusAsync { |
+ // NSBundle is not documented as being thread-safe. Do NSBundle operations |
+ // on the main thread before jumping over to a NSOperationQueue-managed |
+ // thread to do blocking file input. |
+ DCHECK([NSThread isMainThread]); |
+ |
+ SEL selector = @selector(determineUpdateStatusAtPath:); |
+ NSString* appPath = [[NSBundle mainBundle] bundlePath]; |
+ NSInvocationOperation* operation = |
+ [[[NSInvocationOperation alloc] initWithTarget:self |
+ selector:selector |
+ object:appPath] autorelease]; |
+ |
+ NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue]; |
+ [operationQueue addOperation:operation]; |
+} |
+ |
+// Runs on a thread managed by NSOperationQueue. |
+- (void)determineUpdateStatusAtPath:(NSString*)appPath { |
+ DCHECK(![NSThread isMainThread]); |
+ |
+ NSString* appInfoPlistPath = |
+ [[appPath stringByAppendingPathComponent:@"Contents"] |
+ stringByAppendingPathComponent:@"Info.plist"]; |
+ NSDictionary* infoPlist = |
+ [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; |
+ NSString* version = [infoPlist objectForKey:@"CFBundleShortVersionString"]; |
+ |
+ [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) |
+ withObject:version |
+ waitUntilDone:NO]; |
+} |
+ |
+// Runs on the main thread. |
+- (void)determineUpdateStatusForVersion:(NSString*)version { |
+ DCHECK([NSThread isMainThread]); |
+ |
+ AutoupdateStatus status; |
+ if (updateSuccessfullyInstalled_) { |
+ // If an update was successfully installed and this object saw it happen, |
+ // then don't even bother comparing versions. |
+ status = kAutoupdateInstalled; |
+ } else { |
+ NSString* currentVersion = |
+ [NSString stringWithUTF8String:chrome::kChromeVersion]; |
+ if (!version) { |
+ // If the version on disk could not be determined, assume that |
+ // whatever's running is current. |
+ version = currentVersion; |
+ status = kAutoupdateCurrent; |
+ } else if ([version isEqualToString:currentVersion]) { |
+ status = kAutoupdateCurrent; |
+ } else { |
+ // If the version on disk doesn't match what's currently running, an |
+ // update must have been applied in the background, without this app's |
+ // direct participation. Leave updateSuccessfullyInstalled_ alone |
+ // because there's no direct knowledge of what actually happened. |
+ status = kAutoupdateInstalled; |
+ } |
+ } |
+ |
+ [self updateStatus:status version:version]; |
+} |
+ |
+- (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { |
+ NSNumber* statusNumber = [NSNumber numberWithInt:status]; |
+ NSMutableDictionary* dictionary = |
+ [NSMutableDictionary dictionaryWithObject:statusNumber |
+ forKey:kAutoupdateStatusStatus]; |
+ if (version) { |
+ [dictionary setObject:version forKey:kAutoupdateStatusVersion]; |
+ } |
+ |
+ NSNotification* notification = |
+ [NSNotification notificationWithName:kAutoupdateStatusNotification |
+ object:self |
+ userInfo:dictionary]; |
+ recentNotification_.reset([notification retain]); |
+ |
+ [[NSNotificationCenter defaultCenter] postNotification:notification]; |
+} |
+ |
+- (NSNotification*)recentNotification { |
+ return [[recentNotification_ retain] autorelease]; |
+} |
+ |
+- (void)clearRecentNotification { |
+ recentNotification_.reset(nil); |
+} |
+ |
+@end // @implementation KeystoneGlue |