| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #import "chrome/app/keystone_glue.h" | |
| 6 | |
| 7 #include "base/logging.h" | |
| 8 #include "base/mac_util.h" | |
| 9 #import "base/worker_pool_mac.h" | |
| 10 #include "chrome/common/chrome_constants.h" | |
| 11 | |
| 12 namespace { | |
| 13 | |
| 14 // Provide declarations of the Keystone registration bits needed here. From | |
| 15 // KSRegistration.h. | |
| 16 typedef enum { kKSPathExistenceChecker } KSExistenceCheckerType; | |
| 17 | |
| 18 NSString *KSRegistrationCheckForUpdateNotification = | |
| 19 @"KSRegistrationCheckForUpdateNotification"; | |
| 20 NSString *KSRegistrationStatusKey = @"Status"; | |
| 21 NSString *KSRegistrationVersionKey = @"Version"; | |
| 22 NSString *KSRegistrationUpdateCheckErrorKey = @"Error"; | |
| 23 | |
| 24 NSString *KSRegistrationStartUpdateNotification = | |
| 25 @"KSRegistrationStartUpdateNotification"; | |
| 26 NSString *KSUpdateCheckSuccessfulKey = @"CheckSuccessful"; | |
| 27 NSString *KSUpdateCheckSuccessfullyInstalledKey = @"SuccessfullyInstalled"; | |
| 28 | |
| 29 NSString *KSRegistrationRemoveExistingTag = @""; | |
| 30 #define KSRegistrationPreserveExistingTag nil | |
| 31 | |
| 32 } // namespace | |
| 33 | |
| 34 @interface KSRegistration : NSObject | |
| 35 | |
| 36 + (id)registrationWithProductID:(NSString*)productID; | |
| 37 | |
| 38 - (BOOL)registerWithVersion:(NSString*)version | |
| 39 existenceCheckerType:(KSExistenceCheckerType)xctype | |
| 40 existenceCheckerString:(NSString*)xc | |
| 41 serverURLString:(NSString*)serverURLString | |
| 42 preserveTTToken:(BOOL)preserveToken | |
| 43 tag:(NSString*)tag; | |
| 44 | |
| 45 - (void)setActive; | |
| 46 - (void)checkForUpdate; | |
| 47 - (void)startUpdate; | |
| 48 | |
| 49 @end // @interface KSRegistration | |
| 50 | |
| 51 @interface KeystoneGlue(Private) | |
| 52 | |
| 53 // Called periodically to announce activity by pinging the Keystone server. | |
| 54 - (void)markActive:(NSTimer*)timer; | |
| 55 | |
| 56 // Called when an update check or update installation is complete. Posts the | |
| 57 // kAutoupdateStatusNotification notification to the default notification | |
| 58 // center. | |
| 59 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version; | |
| 60 | |
| 61 // These three methods are used to determine the version of the application | |
| 62 // currently installed on disk, compare that to the currently-running version, | |
| 63 // decide whether any updates have been installed, and call | |
| 64 // -updateStatus:version:. | |
| 65 // | |
| 66 // In order to check the version on disk, the installed application's | |
| 67 // Info.plist dictionary must be read; in order to see changes as updates are | |
| 68 // applied, the dictionary must be read each time, bypassing any caches such | |
| 69 // as the one that NSBundle might be maintaining. Reading files can be a | |
| 70 // blocking operation, and blocking operations are to be avoided on the main | |
| 71 // thread. I'm not quite sure what jank means, but I bet that a blocked main | |
| 72 // thread would cause some of it. | |
| 73 // | |
| 74 // -determineUpdateStatusAsync is called on the main thread to initiate the | |
| 75 // operation. It performs initial set-up work that must be done on the main | |
| 76 // thread and arranges for -determineUpdateStatusAtPath: to be called on a | |
| 77 // work queue thread managed by NSOperationQueue. | |
| 78 // -determineUpdateStatusAtPath: then reads the Info.plist, gets the version | |
| 79 // from the CFBundleShortVersionString key, and performs | |
| 80 // -determineUpdateStatusForVersion: on the main thread. | |
| 81 // -determineUpdateStatusForVersion: does the actual comparison of the version | |
| 82 // on disk with the running version and calls -updateStatus:version: with the | |
| 83 // results of its analysis. | |
| 84 - (void)determineUpdateStatusAsync; | |
| 85 - (void)determineUpdateStatusAtPath:(NSString*)appPath; | |
| 86 - (void)determineUpdateStatusForVersion:(NSString*)version; | |
| 87 | |
| 88 @end // @interface KeystoneGlue(Private) | |
| 89 | |
| 90 const NSString* const kAutoupdateStatusNotification = | |
| 91 @"AutoupdateStatusNotification"; | |
| 92 const NSString* const kAutoupdateStatusStatus = @"status"; | |
| 93 const NSString* const kAutoupdateStatusVersion = @"version"; | |
| 94 | |
| 95 @implementation KeystoneGlue | |
| 96 | |
| 97 // TODO(jrg): use base::SingletonObjC<KeystoneGlue> | |
| 98 static KeystoneGlue* sDefaultKeystoneGlue = nil; // leaked | |
| 99 | |
| 100 + (id)defaultKeystoneGlue { | |
| 101 if (!sDefaultKeystoneGlue) { | |
| 102 sDefaultKeystoneGlue = [[KeystoneGlue alloc] init]; | |
| 103 [sDefaultKeystoneGlue loadParameters]; | |
| 104 if (![sDefaultKeystoneGlue loadKeystoneRegistration]) { | |
| 105 [sDefaultKeystoneGlue release]; | |
| 106 sDefaultKeystoneGlue = nil; | |
| 107 } | |
| 108 } | |
| 109 return sDefaultKeystoneGlue; | |
| 110 } | |
| 111 | |
| 112 - (id)init { | |
| 113 if ((self = [super init])) { | |
| 114 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; | |
| 115 | |
| 116 [center addObserver:self | |
| 117 selector:@selector(checkForUpdateComplete:) | |
| 118 name:KSRegistrationCheckForUpdateNotification | |
| 119 object:nil]; | |
| 120 | |
| 121 [center addObserver:self | |
| 122 selector:@selector(installUpdateComplete:) | |
| 123 name:KSRegistrationStartUpdateNotification | |
| 124 object:nil]; | |
| 125 } | |
| 126 | |
| 127 return self; | |
| 128 } | |
| 129 | |
| 130 - (void)dealloc { | |
| 131 [url_ release]; | |
| 132 [productID_ release]; | |
| 133 [version_ release]; | |
| 134 [channel_ release]; | |
| 135 [registration_ release]; | |
| 136 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
| 137 [super dealloc]; | |
| 138 } | |
| 139 | |
| 140 - (NSDictionary*)infoDictionary { | |
| 141 // Use mac_util::MainAppBundle() to get the app framework's dictionary. | |
| 142 return [mac_util::MainAppBundle() infoDictionary]; | |
| 143 } | |
| 144 | |
| 145 - (void)loadParameters { | |
| 146 NSDictionary* infoDictionary = [self infoDictionary]; | |
| 147 NSString* url = [infoDictionary objectForKey:@"KSUpdateURL"]; | |
| 148 NSString* product = [infoDictionary objectForKey:@"KSProductID"]; | |
| 149 if (product == nil) { | |
| 150 // Use [NSBundle mainBundle] to fall back to the app's own bundle | |
| 151 // identifier, not the app framework's. | |
| 152 product = [[NSBundle mainBundle] bundleIdentifier]; | |
| 153 } | |
| 154 NSString* version = [infoDictionary objectForKey:@"KSVersion"]; | |
| 155 if (!product || !url || !version) { | |
| 156 // If parameters required for Keystone are missing, don't use it. | |
| 157 return; | |
| 158 } | |
| 159 | |
| 160 NSString* channel = [infoDictionary objectForKey:@"KSChannelID"]; | |
| 161 // The stable channel has no tag. If updating to stable, remove the | |
| 162 // dev and beta tags since we've been "promoted". | |
| 163 if (channel == nil) | |
| 164 channel = KSRegistrationRemoveExistingTag; | |
| 165 | |
| 166 url_ = [url retain]; | |
| 167 productID_ = [product retain]; | |
| 168 version_ = [version retain]; | |
| 169 channel_ = [channel retain]; | |
| 170 } | |
| 171 | |
| 172 - (BOOL)loadKeystoneRegistration { | |
| 173 if (!productID_ || !url_ || !version_) | |
| 174 return NO; | |
| 175 | |
| 176 // Load the KeystoneRegistration framework bundle if present. It lives | |
| 177 // inside the framework, so use mac_util::MainAppBundle(); | |
| 178 NSString* ksrPath = | |
| 179 [[mac_util::MainAppBundle() privateFrameworksPath] | |
| 180 stringByAppendingPathComponent:@"KeystoneRegistration.framework"]; | |
| 181 NSBundle* ksrBundle = [NSBundle bundleWithPath:ksrPath]; | |
| 182 [ksrBundle load]; | |
| 183 | |
| 184 // Harness the KSRegistration class. | |
| 185 Class ksrClass = [ksrBundle classNamed:@"KSRegistration"]; | |
| 186 KSRegistration* ksr = [ksrClass registrationWithProductID:productID_]; | |
| 187 if (!ksr) | |
| 188 return NO; | |
| 189 | |
| 190 registration_ = [ksr retain]; | |
| 191 return YES; | |
| 192 } | |
| 193 | |
| 194 - (void)registerWithKeystone { | |
| 195 // The existence checks should use the path to the app bundle, not the | |
| 196 // app framework bundle, so use [NSBundle mainBundle] instead of | |
| 197 // mac_util::MainBundle(). | |
| 198 [registration_ registerWithVersion:version_ | |
| 199 existenceCheckerType:kKSPathExistenceChecker | |
| 200 existenceCheckerString:[[NSBundle mainBundle] bundlePath] | |
| 201 serverURLString:url_ | |
| 202 preserveTTToken:YES | |
| 203 tag:channel_]; | |
| 204 | |
| 205 // Mark an active RIGHT NOW; don't wait an hour for the first one. | |
| 206 [registration_ setActive]; | |
| 207 | |
| 208 // Set up hourly activity pings. | |
| 209 timer_ = [NSTimer scheduledTimerWithTimeInterval:60 * 60 // One hour | |
| 210 target:self | |
| 211 selector:@selector(markActive:) | |
| 212 userInfo:registration_ | |
| 213 repeats:YES]; | |
| 214 } | |
| 215 | |
| 216 - (void)stopTimer { | |
| 217 [timer_ invalidate]; | |
| 218 } | |
| 219 | |
| 220 - (void)markActive:(NSTimer*)timer { | |
| 221 KSRegistration* ksr = [timer userInfo]; | |
| 222 [ksr setActive]; | |
| 223 } | |
| 224 | |
| 225 - (void)checkForUpdate { | |
| 226 DCHECK(![self asyncOperationPending]); | |
| 227 | |
| 228 if (!registration_) { | |
| 229 [self updateStatus:kAutoupdateCheckFailed version:nil]; | |
| 230 return; | |
| 231 } | |
| 232 | |
| 233 [self updateStatus:kAutoupdateChecking version:nil]; | |
| 234 | |
| 235 [registration_ checkForUpdate]; | |
| 236 | |
| 237 // Upon completion, KSRegistrationCheckForUpdateNotification will be posted, | |
| 238 // and -checkForUpdateComplete: will be called. | |
| 239 } | |
| 240 | |
| 241 - (void)checkForUpdateComplete:(NSNotification*)notification { | |
| 242 NSDictionary* userInfo = [notification userInfo]; | |
| 243 | |
| 244 if ([[userInfo objectForKey:KSRegistrationUpdateCheckErrorKey] boolValue]) { | |
| 245 [self updateStatus:kAutoupdateCheckFailed version:nil]; | |
| 246 } else if ([[userInfo objectForKey:KSRegistrationStatusKey] boolValue]) { | |
| 247 // If an update is known to be available, go straight to | |
| 248 // -updateStatus:version:. It doesn't matter what's currently on disk. | |
| 249 NSString* version = [userInfo objectForKey:KSRegistrationVersionKey]; | |
| 250 [self updateStatus:kAutoupdateAvailable version:version]; | |
| 251 } else { | |
| 252 // If no updates are available, check what's on disk, because an update | |
| 253 // may have already been installed. This check happens on another thread, | |
| 254 // and -updateStatus:version: will be called on the main thread when done. | |
| 255 [self determineUpdateStatusAsync]; | |
| 256 } | |
| 257 } | |
| 258 | |
| 259 - (void)installUpdate { | |
| 260 DCHECK(![self asyncOperationPending]); | |
| 261 | |
| 262 if (!registration_) { | |
| 263 [self updateStatus:kAutoupdateInstallFailed version:nil]; | |
| 264 return; | |
| 265 } | |
| 266 | |
| 267 [self updateStatus:kAutoupdateInstalling version:nil]; | |
| 268 | |
| 269 [registration_ startUpdate]; | |
| 270 | |
| 271 // Upon completion, KSRegistrationStartUpdateNotification will be posted, | |
| 272 // and -installUpdateComplete: will be called. | |
| 273 } | |
| 274 | |
| 275 - (void)installUpdateComplete:(NSNotification*)notification { | |
| 276 NSDictionary* userInfo = [notification userInfo]; | |
| 277 | |
| 278 if (![[userInfo objectForKey:KSUpdateCheckSuccessfulKey] boolValue] || | |
| 279 ![[userInfo objectForKey:KSUpdateCheckSuccessfullyInstalledKey] | |
| 280 intValue]) { | |
| 281 [self updateStatus:kAutoupdateInstallFailed version:nil]; | |
| 282 } else { | |
| 283 updateSuccessfullyInstalled_ = YES; | |
| 284 | |
| 285 // Nothing in the notification dictionary reports the version that was | |
| 286 // installed. Figure it out based on what's on disk. | |
| 287 [self determineUpdateStatusAsync]; | |
| 288 } | |
| 289 } | |
| 290 | |
| 291 // Runs on the main thread. | |
| 292 - (void)determineUpdateStatusAsync { | |
| 293 // NSBundle is not documented as being thread-safe. Do NSBundle operations | |
| 294 // on the main thread before jumping over to a NSOperationQueue-managed | |
| 295 // thread to do blocking file input. | |
| 296 DCHECK([NSThread isMainThread]); | |
| 297 | |
| 298 SEL selector = @selector(determineUpdateStatusAtPath:); | |
| 299 NSString* appPath = [[NSBundle mainBundle] bundlePath]; | |
| 300 NSInvocationOperation* operation = | |
| 301 [[[NSInvocationOperation alloc] initWithTarget:self | |
| 302 selector:selector | |
| 303 object:appPath] autorelease]; | |
| 304 | |
| 305 NSOperationQueue* operationQueue = [WorkerPoolObjC sharedOperationQueue]; | |
| 306 [operationQueue addOperation:operation]; | |
| 307 } | |
| 308 | |
| 309 // Runs on a thread managed by NSOperationQueue. | |
| 310 - (void)determineUpdateStatusAtPath:(NSString*)appPath { | |
| 311 DCHECK(![NSThread isMainThread]); | |
| 312 | |
| 313 NSString* appInfoPlistPath = | |
| 314 [[appPath stringByAppendingPathComponent:@"Contents"] | |
| 315 stringByAppendingPathComponent:@"Info.plist"]; | |
| 316 NSDictionary* infoPlist = | |
| 317 [NSDictionary dictionaryWithContentsOfFile:appInfoPlistPath]; | |
| 318 NSString* version = [infoPlist objectForKey:@"CFBundleShortVersionString"]; | |
| 319 | |
| 320 [self performSelectorOnMainThread:@selector(determineUpdateStatusForVersion:) | |
| 321 withObject:version | |
| 322 waitUntilDone:NO]; | |
| 323 } | |
| 324 | |
| 325 // Runs on the main thread. | |
| 326 - (void)determineUpdateStatusForVersion:(NSString*)version { | |
| 327 DCHECK([NSThread isMainThread]); | |
| 328 | |
| 329 AutoupdateStatus status; | |
| 330 if (updateSuccessfullyInstalled_) { | |
| 331 // If an update was successfully installed and this object saw it happen, | |
| 332 // then don't even bother comparing versions. | |
| 333 status = kAutoupdateInstalled; | |
| 334 } else { | |
| 335 NSString* currentVersion = | |
| 336 [NSString stringWithUTF8String:chrome::kChromeVersion]; | |
| 337 if (!version) { | |
| 338 // If the version on disk could not be determined, assume that | |
| 339 // whatever's running is current. | |
| 340 version = currentVersion; | |
| 341 status = kAutoupdateCurrent; | |
| 342 } else if ([version isEqualToString:currentVersion]) { | |
| 343 status = kAutoupdateCurrent; | |
| 344 } else { | |
| 345 // If the version on disk doesn't match what's currently running, an | |
| 346 // update must have been applied in the background, without this app's | |
| 347 // direct participation. Leave updateSuccessfullyInstalled_ alone | |
| 348 // because there's no direct knowledge of what actually happened. | |
| 349 status = kAutoupdateInstalled; | |
| 350 } | |
| 351 } | |
| 352 | |
| 353 [self updateStatus:status version:version]; | |
| 354 } | |
| 355 | |
| 356 - (void)updateStatus:(AutoupdateStatus)status version:(NSString*)version { | |
| 357 NSNumber* statusNumber = [NSNumber numberWithInt:status]; | |
| 358 NSMutableDictionary* dictionary = | |
| 359 [NSMutableDictionary dictionaryWithObject:statusNumber | |
| 360 forKey:kAutoupdateStatusStatus]; | |
| 361 if (version) { | |
| 362 [dictionary setObject:version forKey:kAutoupdateStatusVersion]; | |
| 363 } | |
| 364 | |
| 365 NSNotification* notification = | |
| 366 [NSNotification notificationWithName:kAutoupdateStatusNotification | |
| 367 object:self | |
| 368 userInfo:dictionary]; | |
| 369 recentNotification_.reset([notification retain]); | |
| 370 | |
| 371 [[NSNotificationCenter defaultCenter] postNotification:notification]; | |
| 372 } | |
| 373 | |
| 374 - (NSNotification*)recentNotification { | |
| 375 return [[recentNotification_ retain] autorelease]; | |
| 376 } | |
| 377 | |
| 378 - (AutoupdateStatus)recentStatus { | |
| 379 NSDictionary* dictionary = [recentNotification_ userInfo]; | |
| 380 return static_cast<AutoupdateStatus>( | |
| 381 [[dictionary objectForKey:kAutoupdateStatusStatus] intValue]); | |
| 382 } | |
| 383 | |
| 384 - (BOOL)asyncOperationPending { | |
| 385 AutoupdateStatus status = [self recentStatus]; | |
| 386 return status == kAutoupdateChecking || status == kAutoupdateInstalling; | |
| 387 } | |
| 388 | |
| 389 @end // @implementation KeystoneGlue | |
| OLD | NEW |