diff options
Diffstat (limited to 'chrome/app')
-rw-r--r-- | chrome/app/keystone_glue.h | 90 | ||||
-rw-r--r-- | chrome/app/keystone_glue.mm | 283 | ||||
-rw-r--r-- | chrome/app/keystone_glue_unittest.mm | 76 |
3 files changed, 326 insertions, 123 deletions
diff --git a/chrome/app/keystone_glue.h b/chrome/app/keystone_glue.h index 9a8eb01..f8d2a53 100644 --- a/chrome/app/keystone_glue.h +++ b/chrome/app/keystone_glue.h @@ -6,27 +6,30 @@ #define CHROME_APP_KEYSTONE_GLUE_H_ #import <Foundation/Foundation.h> - -// Objects which request callbacks from KeystoneGlue (e.g. information -// on update availability) should implement this protocol. All callbacks -// require the caller to be spinning in the runloop to happen. -@protocol KeystoneGlueCallbacks - -// Callback when a checkForUpdate completes. -// |latestVersion| may be nil if not returned from the server. -// |latestVersion| is not a localizable string. -- (void)upToDateCheckCompleted:(BOOL)upToDate - latestVersion:(NSString*)latestVersion; - -// Callback when a startUpdate completes. -// |successful| tells if the *check* was successful. This does not -// necessarily mean updates installed successfully. -// |installs| tells the number of updates that installed successfully -// (typically 0 or 1). -- (void)updateCompleted:(BOOL)successful installs:(int)installs; - -@end // protocol KeystoneGlueCallbacks - +#import <base/scoped_nsobject.h> + +// Possible outcomes of -checkForUpdate and -installUpdate. A version may +// accompany some of these, but beware: a version is never required. For +// statuses that can be accompanied by a version, the comment indicates what +// version is referenced. +enum AutoupdateStatus { + kAutoupdateCurrent = 0, // version of the running application + kAutoupdateAvailable, // version of the update that is available + kAutoupdateInstalled, // version of the update that was installed + kAutoupdateCheckFailed, // no version + kAutoupdateInstallFailed // no version +}; + +// kAutoupdateStatusNotification is the name of the notification posted when +// -checkForUpdate and -installUpdate complete. This notification will be +// sent with with its sender object set to the KeystoneGlue instance sending +// the notification. Its userInfo dictionary will contain an AutoupdateStatus +// value as an intValue at key kAutoupdateStatusStatus. If a version is +// available (see AutoupdateStatus), it will be present at key +// kAutoupdateStatusVersion. +extern const NSString* const kAutoupdateStatusNotification; +extern const NSString* const kAutoupdateStatusStatus; +extern const NSString* const kAutoupdateStatusVersion; // KeystoneGlue is an adapter around the KSRegistration class, allowing it to // be used without linking directly against its containing KeystoneRegistration @@ -55,10 +58,11 @@ KSRegistration* registration_; // strong NSTimer* timer_; // strong - // Data for callbacks, all strong. Deallocated (if needed) in a - // NSNotificationCenter callback. - NSObject<KeystoneGlueCallbacks>* startTarget_; - NSObject<KeystoneGlueCallbacks>* checkTarget_; + // The most recent kAutoupdateStatusNotification notification posted. + scoped_nsobject<NSNotification> recentNotification_; + + // YES if an update was ever successfully installed by -installUpdate. + BOOL updateSuccessfullyInstalled_; } // Return the default Keystone Glue object. @@ -68,21 +72,27 @@ // with Keystone, and set up periodic activity pings. - (void)registerWithKeystone; -// Check if updates are available. -// upToDateCheckCompleted:: called on target when done. -// Return NO if we could not start the check. -- (BOOL)checkForUpdate:(NSObject<KeystoneGlueCallbacks>*)target; +// -checkForUpdate launches a check for updates, and -installUpdate begins +// installing an available update. For each, status will be communicated via +// a kAutoupdateStatusNotification notification, and will also be available +// through -recentUpdateStatus. +- (void)checkForUpdate; +- (void)installUpdate; + +// Accessor for recentNotification_. Returns an autoreleased NSNotification. +- (NSNotification*)recentNotification; -// Start an update. -// updateCompleted:: called on target when done. -// This cannot be cancelled. -// Return NO if we could not start the check. -- (BOOL)startUpdate:(NSObject<KeystoneGlueCallbacks>*)target; +// Clears the saved recentNotification_. +- (void)clearRecentNotification; -@end // KeystoneGlue +@end // @interface KeystoneGlue +@interface KeystoneGlue(ExposedForTesting) -@interface KeystoneGlue (ExposedForTesting) +// Release the shared instance. Use this in tests to reset the shared +// instance in case strange things are done to it for testing purposes. Never +// call this from non-test code. ++ (void)releaseDefaultKeystoneGlue; // Load any params we need for configuring Keystone. - (void)loadParameters; @@ -94,11 +104,11 @@ - (void)stopTimer; // Called when a checkForUpdate: notification completes. -- (void)checkComplete:(NSNotification *)notification; +- (void)checkForUpdateComplete:(NSNotification*)notification; -// Called when a startUpdate: notification completes. -- (void)startUpdateComplete:(NSNotification *)notification; +// Called when an installUpdate: notification completes. +- (void)installUpdateComplete:(NSNotification*)notification; -@end // KeystoneGlue (ExposedForTesting) +@end // @interface KeystoneGlue(ExposedForTesting) #endif // CHROME_APP_KEYSTONE_GLUE_H_ diff --git a/chrome/app/keystone_glue.mm b/chrome/app/keystone_glue.mm index 736972c..689cfdb 100644 --- a/chrome/app/keystone_glue.mm +++ b/chrome/app/keystone_glue.mm @@ -2,16 +2,12 @@ // 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) - -// Called periodically to announce activity by pinging the Keystone server. -- (void)markActive:(NSTimer*)timer; - -@end - +#include "base/logging.h" +#include "base/mac_util.h" +#import "base/worker_pool_mac.h" +#include "chrome/common/chrome_constants.h" // Provide declarations of the Keystone registration bits needed here. From // KSRegistration.h. @@ -31,7 +27,9 @@ NSString *KSRegistrationRemoveExistingTag = @""; #define KSRegistrationPreserveExistingTag nil @interface KSRegistration : NSObject + + (id)registrationWithProductID:(NSString*)productID; + // Older API - (BOOL)registerWithVersion:(NSString*)version existenceCheckerType:(KSExistenceCheckerType)xctype @@ -43,20 +41,65 @@ NSString *KSRegistrationRemoveExistingTag = @""; 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 -+ (id)defaultKeystoneGlue { - // TODO(jrg): use base::SingletonObjC<KeystoneGlue> - static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked +// TODO(jrg): use base::SingletonObjC<KeystoneGlue> +static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked - if (sDefaultKeystoneGlue == nil) { ++ (id)defaultKeystoneGlue { + if (!sDefaultKeystoneGlue) { sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; [sDefaultKeystoneGlue loadParameters]; if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { @@ -67,6 +110,29 @@ NSString *KSRegistrationRemoveExistingTag = @""; 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 @@ NSString *KSRegistrationRemoveExistingTag = @""; [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]; + } +} + +// 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 +@end // @implementation KeystoneGlue diff --git a/chrome/app/keystone_glue_unittest.mm b/chrome/app/keystone_glue_unittest.mm index 614bdbc..90a2f40 100644 --- a/chrome/app/keystone_glue_unittest.mm +++ b/chrome/app/keystone_glue_unittest.mm @@ -14,18 +14,38 @@ @implementation FakeGlueRegistration -- (void)checkForUpdate { } -- (void)startUpdate { } + +// Send the notifications that a real KeystoneGlue object would send. + +- (void)checkForUpdate { + NSNumber* yesNumber = [NSNumber numberWithBool:YES]; + NSString* statusKey = @"Status"; + NSDictionary* dictionary = [NSDictionary dictionaryWithObject:yesNumber + forKey:statusKey]; + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationCheckForUpdateNotification" + object:nil + userInfo:dictionary]; +} + +- (void)startUpdate { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:@"KSRegistrationStartUpdateNotification" + object:nil]; +} + @end -@interface FakeKeystoneGlue : KeystoneGlue<KeystoneGlueCallbacks> { +@interface FakeKeystoneGlue : KeystoneGlue { @public BOOL upToDate_; NSString *latestVersion_; BOOL successful_; int installs_; } + +- (void)fakeAboutWindowCallback:(NSNotification*)notification; @end @@ -38,10 +58,23 @@ latestVersion_ = @"foo bar"; successful_ = YES; installs_ = 1010101010; + + // Set up an observer that takes the notification that the About window + // listens for. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(fakeAboutWindowCallback:) + name:kAutoupdateStatusNotification + object:nil]; } return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + // For mocking - (NSDictionary*)infoDictionary { NSDictionary* dict = [NSDictionary dictionaryWithObjectsAndKeys: @@ -69,21 +102,24 @@ return timer_ ? YES : NO; } -- (void)upToDateCheckCompleted:(BOOL)upToDate - latestVersion:(NSString*)latestVersion { - upToDate_ = upToDate; - latestVersion_ = latestVersion; -} - -- (void)updateCompleted:(BOOL)successful installs:(int)installs { - successful_ = successful; - installs_ = installs; -} - - (void)addFakeRegistration { registration_ = [[FakeGlueRegistration alloc] init]; } +- (void)fakeAboutWindowCallback:(NSNotification*)notification { + NSDictionary* dictionary = [notification userInfo]; + AutoupdateStatus status = static_cast<AutoupdateStatus>( + [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); + + if (status == kAutoupdateAvailable) { + upToDate_ = NO; + latestVersion_ = [dictionary objectForKey:kAutoupdateStatusVersion]; + } else if (status == kAutoupdateInstallFailed) { + successful_ = NO; + installs_ = 0; + } +} + // Confirm we look like callbacks with nil NSNotifications - (BOOL)confirmCallbacks { return (!upToDate_ && @@ -114,12 +150,16 @@ TEST_F(KeystoneGlueTest, BasicGlobalCreate) { Method loadMethod_ = class_getInstanceMethod([KeystoneGlue class], lks); method_setImplementation(loadMethod_, newLoadImp_); + // Dump any existing KeystoneGlue shared instance so that a new one can be + // created with the mocked methods. + [KeystoneGlue releaseDefaultKeystoneGlue]; KeystoneGlue *glue = [KeystoneGlue defaultKeystoneGlue]; ASSERT_TRUE(glue); // Fix back up the class to the way we found it. method_setImplementation(infoMethod_, oldInfoImp_); method_setImplementation(loadMethod_, oldLoadImp_); + [KeystoneGlue releaseDefaultKeystoneGlue]; } TEST_F(KeystoneGlueTest, BasicUse) { @@ -136,14 +176,10 @@ TEST_F(KeystoneGlueTest, BasicUse) { ASSERT_TRUE([glue hasATimer]); [glue stopTimer]; - ASSERT_TRUE(![glue checkForUpdate:glue] && ![glue startUpdate:glue]); - // Brief exercise of callbacks [glue addFakeRegistration]; - ASSERT_TRUE([glue checkForUpdate:glue]); - [glue checkComplete:nil]; - ASSERT_TRUE([glue startUpdate:glue]); - [glue startUpdateComplete:nil]; + [glue checkForUpdate]; + [glue installUpdate]; ASSERT_TRUE([glue confirmCallbacks]); } |