1 // Copyright (c) 2012 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 #include "chrome/browser/extensions/updater/extension_updater.h" 6 7 #include <algorithm> 8 #include <set> 9 #include <vector> 10 11 #include "base/bind.h" 12 #include "base/logging.h" 13 #include "base/metrics/histogram.h" 14 #include "base/prefs/pref_service.h" 15 #include "base/rand_util.h" 16 #include "base/stl_util.h" 17 #include "base/strings/string_number_conversions.h" 18 #include "base/strings/string_split.h" 19 #include "chrome/browser/chrome_notification_types.h" 20 #include "chrome/browser/extensions/api/module/module.h" 21 #include "chrome/browser/extensions/crx_installer.h" 22 #include "chrome/browser/extensions/extension_service.h" 23 #include "chrome/browser/extensions/pending_extension_manager.h" 24 #include "chrome/browser/extensions/updater/extension_downloader.h" 25 #include "chrome/browser/profiles/profile.h" 26 #include "chrome/common/pref_names.h" 27 #include "content/public/browser/browser_thread.h" 28 #include "content/public/browser/notification_details.h" 29 #include "content/public/browser/notification_service.h" 30 #include "content/public/browser/notification_source.h" 31 #include "crypto/sha2.h" 32 #include "extensions/browser/extension_prefs.h" 33 #include "extensions/browser/extension_registry.h" 34 #include "extensions/browser/pref_names.h" 35 #include "extensions/common/constants.h" 36 #include "extensions/common/extension.h" 37 #include "extensions/common/extension_set.h" 38 #include "extensions/common/manifest.h" 39 40 using base::RandDouble; 41 using base::RandInt; 42 using base::Time; 43 using base::TimeDelta; 44 using content::BrowserThread; 45 46 typedef extensions::ExtensionDownloaderDelegate::Error Error; 47 typedef extensions::ExtensionDownloaderDelegate::PingResult PingResult; 48 49 namespace { 50 51 // Wait at least 5 minutes after browser startup before we do any checks. If you 52 // change this value, make sure to update comments where it is used. 53 const int kStartupWaitSeconds = 60 * 5; 54 55 // For sanity checking on update frequency - enforced in release mode only. 56 #if defined(NDEBUG) 57 const int kMinUpdateFrequencySeconds = 30; 58 #endif 59 const int kMaxUpdateFrequencySeconds = 60 * 60 * 24 * 7; // 7 days 60 61 // Require at least 5 seconds between consecutive non-succesful extension update 62 // checks. 63 const int kMinUpdateThrottleTime = 5; 64 65 // When we've computed a days value, we want to make sure we don't send a 66 // negative value (due to the system clock being set backwards, etc.), since -1 67 // is a special sentinel value that means "never pinged", and other negative 68 // values don't make sense. 69 int SanitizeDays(int days) { 70 if (days < 0) 71 return 0; 72 return days; 73 } 74 75 // Calculates the value to use for the ping days parameter. 76 int CalculatePingDays(const Time& last_ping_day) { 77 int days = extensions::ManifestFetchData::kNeverPinged; 78 if (!last_ping_day.is_null()) { 79 days = SanitizeDays((Time::Now() - last_ping_day).InDays()); 80 } 81 return days; 82 } 83 84 int CalculateActivePingDays(const Time& last_active_ping_day, 85 bool hasActiveBit) { 86 if (!hasActiveBit) 87 return 0; 88 if (last_active_ping_day.is_null()) 89 return extensions::ManifestFetchData::kNeverPinged; 90 return SanitizeDays((Time::Now() - last_active_ping_day).InDays()); 91 } 92 93 } // namespace 94 95 namespace extensions { 96 97 ExtensionUpdater::CheckParams::CheckParams() 98 : install_immediately(false) {} 99 100 ExtensionUpdater::CheckParams::~CheckParams() {} 101 102 ExtensionUpdater::FetchedCRXFile::FetchedCRXFile( 103 const std::string& i, 104 const base::FilePath& p, 105 bool file_ownership_passed, 106 const std::set<int>& request_ids) 107 : extension_id(i), 108 path(p), 109 file_ownership_passed(file_ownership_passed), 110 request_ids(request_ids) {} 111 112 ExtensionUpdater::FetchedCRXFile::FetchedCRXFile() 113 : path(), file_ownership_passed(true) {} 114 115 ExtensionUpdater::FetchedCRXFile::~FetchedCRXFile() {} 116 117 ExtensionUpdater::InProgressCheck::InProgressCheck() 118 : install_immediately(false) {} 119 120 ExtensionUpdater::InProgressCheck::~InProgressCheck() {} 121 122 struct ExtensionUpdater::ThrottleInfo { 123 ThrottleInfo() 124 : in_progress(true), 125 throttle_delay(kMinUpdateThrottleTime), 126 check_start(Time::Now()) {} 127 128 bool in_progress; 129 int throttle_delay; 130 Time check_start; 131 }; 132 133 ExtensionUpdater::ExtensionUpdater(ExtensionServiceInterface* service, 134 ExtensionPrefs* extension_prefs, 135 PrefService* prefs, 136 Profile* profile, 137 int frequency_seconds, 138 ExtensionCache* cache) 139 : alive_(false), 140 weak_ptr_factory_(this), 141 service_(service), frequency_seconds_(frequency_seconds), 142 will_check_soon_(false), extension_prefs_(extension_prefs), 143 prefs_(prefs), profile_(profile), 144 next_request_id_(0), 145 crx_install_is_running_(false), 146 extension_cache_(cache) { 147 DCHECK_GE(frequency_seconds_, 5); 148 DCHECK_LE(frequency_seconds_, kMaxUpdateFrequencySeconds); 149 #if defined(NDEBUG) 150 // In Release mode we enforce that update checks don't happen too often. 151 frequency_seconds_ = std::max(frequency_seconds_, kMinUpdateFrequencySeconds); 152 #endif 153 frequency_seconds_ = std::min(frequency_seconds_, kMaxUpdateFrequencySeconds); 154 155 registrar_.Add(this, 156 chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED, 157 content::NotificationService::AllBrowserContextsAndSources()); 158 } 159 160 ExtensionUpdater::~ExtensionUpdater() { 161 Stop(); 162 } 163 164 // The overall goal here is to balance keeping clients up to date while 165 // avoiding a thundering herd against update servers. 166 TimeDelta ExtensionUpdater::DetermineFirstCheckDelay() { 167 DCHECK(alive_); 168 // If someone's testing with a quick frequency, just allow it. 169 if (frequency_seconds_ < kStartupWaitSeconds) 170 return TimeDelta::FromSeconds(frequency_seconds_); 171 172 // If we've never scheduled a check before, start at frequency_seconds_. 173 if (!prefs_->HasPrefPath(pref_names::kNextUpdateCheck)) 174 return TimeDelta::FromSeconds(frequency_seconds_); 175 176 // If it's been a long time since our last actual check, we want to do one 177 // relatively soon. 178 Time now = Time::Now(); 179 Time last = Time::FromInternalValue(prefs_->GetInt64( 180 pref_names::kLastUpdateCheck)); 181 int days = (now - last).InDays(); 182 if (days >= 30) { 183 // Wait 5-10 minutes. 184 return TimeDelta::FromSeconds(RandInt(kStartupWaitSeconds, 185 kStartupWaitSeconds * 2)); 186 } else if (days >= 14) { 187 // Wait 10-20 minutes. 188 return TimeDelta::FromSeconds(RandInt(kStartupWaitSeconds * 2, 189 kStartupWaitSeconds * 4)); 190 } else if (days >= 3) { 191 // Wait 20-40 minutes. 192 return TimeDelta::FromSeconds(RandInt(kStartupWaitSeconds * 4, 193 kStartupWaitSeconds * 8)); 194 } 195 196 // Read the persisted next check time, and use that if it isn't too soon 197 // or too late. Otherwise pick something random. 198 Time saved_next = Time::FromInternalValue(prefs_->GetInt64( 199 pref_names::kNextUpdateCheck)); 200 Time earliest = now + TimeDelta::FromSeconds(kStartupWaitSeconds); 201 Time latest = now + TimeDelta::FromSeconds(frequency_seconds_); 202 if (saved_next >= earliest && saved_next <= latest) { 203 return saved_next - now; 204 } else { 205 return TimeDelta::FromSeconds(RandInt(kStartupWaitSeconds, 206 frequency_seconds_)); 207 } 208 } 209 210 void ExtensionUpdater::Start() { 211 DCHECK(!alive_); 212 // If these are NULL, then that means we've been called after Stop() 213 // has been called. 214 DCHECK(service_); 215 DCHECK(extension_prefs_); 216 DCHECK(prefs_); 217 DCHECK(profile_); 218 DCHECK(!weak_ptr_factory_.HasWeakPtrs()); 219 alive_ = true; 220 // Make sure our prefs are registered, then schedule the first check. 221 ScheduleNextCheck(DetermineFirstCheckDelay()); 222 } 223 224 void ExtensionUpdater::Stop() { 225 weak_ptr_factory_.InvalidateWeakPtrs(); 226 alive_ = false; 227 service_ = NULL; 228 extension_prefs_ = NULL; 229 prefs_ = NULL; 230 profile_ = NULL; 231 timer_.Stop(); 232 will_check_soon_ = false; 233 downloader_.reset(); 234 } 235 236 void ExtensionUpdater::ScheduleNextCheck(const TimeDelta& target_delay) { 237 DCHECK(alive_); 238 DCHECK(!timer_.IsRunning()); 239 DCHECK(target_delay >= TimeDelta::FromSeconds(1)); 240 241 // Add +/- 10% random jitter. 242 double delay_ms = target_delay.InMillisecondsF(); 243 double jitter_factor = (RandDouble() * .2) - 0.1; 244 delay_ms += delay_ms * jitter_factor; 245 TimeDelta actual_delay = TimeDelta::FromMilliseconds( 246 static_cast<int64>(delay_ms)); 247 248 // Save the time of next check. 249 Time next = Time::Now() + actual_delay; 250 prefs_->SetInt64(pref_names::kNextUpdateCheck, next.ToInternalValue()); 251 252 timer_.Start(FROM_HERE, actual_delay, this, &ExtensionUpdater::TimerFired); 253 } 254 255 void ExtensionUpdater::TimerFired() { 256 DCHECK(alive_); 257 CheckNow(default_params_); 258 259 // If the user has overridden the update frequency, don't bother reporting 260 // this. 261 if (frequency_seconds_ == extensions::kDefaultUpdateFrequencySeconds) { 262 Time last = Time::FromInternalValue(prefs_->GetInt64( 263 pref_names::kLastUpdateCheck)); 264 if (last.ToInternalValue() != 0) { 265 // Use counts rather than time so we can use minutes rather than millis. 266 UMA_HISTOGRAM_CUSTOM_COUNTS("Extensions.UpdateCheckGap", 267 (Time::Now() - last).InMinutes(), 268 TimeDelta::FromSeconds(kStartupWaitSeconds).InMinutes(), 269 TimeDelta::FromDays(40).InMinutes(), 270 50); // 50 buckets seems to be the default. 271 } 272 } 273 274 // Save the last check time, and schedule the next check. 275 int64 now = Time::Now().ToInternalValue(); 276 prefs_->SetInt64(pref_names::kLastUpdateCheck, now); 277 ScheduleNextCheck(TimeDelta::FromSeconds(frequency_seconds_)); 278 } 279 280 void ExtensionUpdater::CheckSoon() { 281 DCHECK(alive_); 282 if (will_check_soon_) 283 return; 284 if (BrowserThread::PostTask( 285 BrowserThread::UI, FROM_HERE, 286 base::Bind(&ExtensionUpdater::DoCheckSoon, 287 weak_ptr_factory_.GetWeakPtr()))) { 288 will_check_soon_ = true; 289 } else { 290 NOTREACHED(); 291 } 292 } 293 294 bool ExtensionUpdater::WillCheckSoon() const { 295 return will_check_soon_; 296 } 297 298 void ExtensionUpdater::DoCheckSoon() { 299 DCHECK(will_check_soon_); 300 CheckNow(default_params_); 301 will_check_soon_ = false; 302 } 303 304 void ExtensionUpdater::AddToDownloader( 305 const ExtensionSet* extensions, 306 const std::list<std::string>& pending_ids, 307 int request_id) { 308 InProgressCheck& request = requests_in_progress_[request_id]; 309 for (ExtensionSet::const_iterator extension_iter = extensions->begin(); 310 extension_iter != extensions->end(); ++extension_iter) { 311 const Extension& extension = *extension_iter->get(); 312 if (!Manifest::IsAutoUpdateableLocation(extension.location())) { 313 VLOG(2) << "Extension " << extension.id() << " is not auto updateable"; 314 continue; 315 } 316 // An extension might be overwritten by policy, and have its update url 317 // changed. Make sure existing extensions aren't fetched again, if a 318 // pending fetch for an extension with the same id already exists. 319 std::list<std::string>::const_iterator pending_id_iter = std::find( 320 pending_ids.begin(), pending_ids.end(), extension.id()); 321 if (pending_id_iter == pending_ids.end()) { 322 if (downloader_->AddExtension(extension, request_id)) 323 request.in_progress_ids_.push_back(extension.id()); 324 } 325 } 326 } 327 328 void ExtensionUpdater::CheckNow(const CheckParams& params) { 329 int request_id = next_request_id_++; 330 331 VLOG(2) << "Starting update check " << request_id; 332 if (params.ids.empty()) 333 NotifyStarted(); 334 335 DCHECK(alive_); 336 337 InProgressCheck& request = requests_in_progress_[request_id]; 338 request.callback = params.callback; 339 request.install_immediately = params.install_immediately; 340 341 if (!downloader_.get()) { 342 downloader_.reset( 343 new ExtensionDownloader(this, profile_->GetRequestContext())); 344 } 345 346 // Add fetch records for extensions that should be fetched by an update URL. 347 // These extensions are not yet installed. They come from group policy 348 // and external install sources. 349 const PendingExtensionManager* pending_extension_manager = 350 service_->pending_extension_manager(); 351 352 std::list<std::string> pending_ids; 353 354 if (params.ids.empty()) { 355 // If no extension ids are specified, check for updates for all extensions. 356 pending_extension_manager->GetPendingIdsForUpdateCheck(&pending_ids); 357 358 std::list<std::string>::const_iterator iter; 359 for (iter = pending_ids.begin(); iter != pending_ids.end(); ++iter) { 360 const PendingExtensionInfo* info = pending_extension_manager->GetById( 361 *iter); 362 if (!Manifest::IsAutoUpdateableLocation(info->install_source())) { 363 VLOG(2) << "Extension " << *iter << " is not auto updateable"; 364 continue; 365 } 366 if (downloader_->AddPendingExtension(*iter, info->update_url(), 367 request_id)) 368 request.in_progress_ids_.push_back(*iter); 369 } 370 371 ExtensionRegistry* registry = ExtensionRegistry::Get(profile_); 372 AddToDownloader(®istry->enabled_extensions(), pending_ids, request_id); 373 AddToDownloader(®istry->disabled_extensions(), pending_ids, request_id); 374 } else { 375 for (std::list<std::string>::const_iterator it = params.ids.begin(); 376 it != params.ids.end(); ++it) { 377 const Extension* extension = service_->GetExtensionById(*it, true); 378 DCHECK(extension); 379 if (downloader_->AddExtension(*extension, request_id)) 380 request.in_progress_ids_.push_back(extension->id()); 381 } 382 } 383 384 // StartAllPending() might call OnExtensionDownloadFailed/Finished before 385 // it returns, which would cause NotifyIfFinished to incorrectly try to 386 // send out a notification. So check before we call StartAllPending if any 387 // extensions are going to be updated, and use that to figure out if 388 // NotifyIfFinished should be called. 389 bool noChecks = request.in_progress_ids_.empty(); 390 391 // StartAllPending() will call OnExtensionDownloadFailed or 392 // OnExtensionDownloadFinished for each extension that was checked. 393 downloader_->StartAllPending(extension_cache_); 394 395 if (noChecks) 396 NotifyIfFinished(request_id); 397 } 398 399 bool ExtensionUpdater::CheckExtensionSoon(const std::string& extension_id, 400 const FinishedCallback& callback) { 401 bool have_throttle_info = ContainsKey(throttle_info_, extension_id); 402 ThrottleInfo& info = throttle_info_[extension_id]; 403 if (have_throttle_info) { 404 // We already had a ThrottleInfo object for this extension, check if the 405 // update check request should be allowed. 406 407 // If another check is in progress, don't start a new check. 408 if (info.in_progress) 409 return false; 410 411 Time now = Time::Now(); 412 Time last = info.check_start; 413 // If somehow time moved back, we don't want to infinitely keep throttling. 414 if (now < last) { 415 last = now; 416 info.check_start = now; 417 } 418 Time earliest = last + TimeDelta::FromSeconds(info.throttle_delay); 419 // If check is too soon, throttle. 420 if (now < earliest) 421 return false; 422 423 // TODO(mek): Somehow increase time between allowing checks when checks 424 // are repeatedly throttled and don't result in updates being installed. 425 426 // It's okay to start a check, update values. 427 info.check_start = now; 428 info.in_progress = true; 429 } 430 431 CheckParams params; 432 params.ids.push_back(extension_id); 433 params.callback = base::Bind(&ExtensionUpdater::ExtensionCheckFinished, 434 weak_ptr_factory_.GetWeakPtr(), 435 extension_id, callback); 436 CheckNow(params); 437 return true; 438 } 439 440 void ExtensionUpdater::ExtensionCheckFinished( 441 const std::string& extension_id, 442 const FinishedCallback& callback) { 443 std::map<std::string, ThrottleInfo>::iterator it = 444 throttle_info_.find(extension_id); 445 if (it != throttle_info_.end()) { 446 it->second.in_progress = false; 447 } 448 callback.Run(); 449 } 450 451 void ExtensionUpdater::OnExtensionDownloadFailed( 452 const std::string& id, 453 Error error, 454 const PingResult& ping, 455 const std::set<int>& request_ids) { 456 DCHECK(alive_); 457 UpdatePingData(id, ping); 458 bool install_immediately = false; 459 for (std::set<int>::const_iterator it = request_ids.begin(); 460 it != request_ids.end(); ++it) { 461 InProgressCheck& request = requests_in_progress_[*it]; 462 install_immediately |= request.install_immediately; 463 request.in_progress_ids_.remove(id); 464 NotifyIfFinished(*it); 465 } 466 467 // This method is called if no updates were found. However a previous update 468 // check might have queued an update for this extension already. If a 469 // current update check has |install_immediately| set the previously 470 // queued update should be installed now. 471 if (install_immediately && service_->GetPendingExtensionUpdate(id)) 472 service_->FinishDelayedInstallation(id); 473 } 474 475 void ExtensionUpdater::OnExtensionDownloadFinished( 476 const std::string& id, 477 const base::FilePath& path, 478 bool file_ownership_passed, 479 const GURL& download_url, 480 const std::string& version, 481 const PingResult& ping, 482 const std::set<int>& request_ids) { 483 DCHECK(alive_); 484 UpdatePingData(id, ping); 485 486 VLOG(2) << download_url << " written to " << path.value(); 487 488 FetchedCRXFile fetched(id, path, file_ownership_passed, request_ids); 489 fetched_crx_files_.push(fetched); 490 491 // MaybeInstallCRXFile() removes extensions from |in_progress_ids_| after 492 // starting the crx installer. 493 MaybeInstallCRXFile(); 494 } 495 496 bool ExtensionUpdater::GetPingDataForExtension( 497 const std::string& id, 498 ManifestFetchData::PingData* ping_data) { 499 DCHECK(alive_); 500 ping_data->rollcall_days = CalculatePingDays( 501 extension_prefs_->LastPingDay(id)); 502 ping_data->is_enabled = service_->IsExtensionEnabled(id); 503 ping_data->active_days = 504 CalculateActivePingDays(extension_prefs_->LastActivePingDay(id), 505 extension_prefs_->GetActiveBit(id)); 506 return true; 507 } 508 509 std::string ExtensionUpdater::GetUpdateUrlData(const std::string& id) { 510 DCHECK(alive_); 511 return extension::GetUpdateURLData(extension_prefs_, id); 512 } 513 514 bool ExtensionUpdater::IsExtensionPending(const std::string& id) { 515 DCHECK(alive_); 516 return service_->pending_extension_manager()->IsIdPending(id); 517 } 518 519 bool ExtensionUpdater::GetExtensionExistingVersion(const std::string& id, 520 std::string* version) { 521 DCHECK(alive_); 522 const Extension* extension = service_->GetExtensionById(id, true); 523 if (!extension) 524 return false; 525 const Extension* update = service_->GetPendingExtensionUpdate(id); 526 if (update) 527 *version = update->VersionString(); 528 else 529 *version = extension->VersionString(); 530 return true; 531 } 532 533 void ExtensionUpdater::UpdatePingData(const std::string& id, 534 const PingResult& ping_result) { 535 DCHECK(alive_); 536 if (ping_result.did_ping) 537 extension_prefs_->SetLastPingDay(id, ping_result.day_start); 538 if (extension_prefs_->GetActiveBit(id)) { 539 extension_prefs_->SetActiveBit(id, false); 540 extension_prefs_->SetLastActivePingDay(id, ping_result.day_start); 541 } 542 } 543 544 void ExtensionUpdater::MaybeInstallCRXFile() { 545 if (crx_install_is_running_ || fetched_crx_files_.empty()) 546 return; 547 548 std::set<int> request_ids; 549 550 while (!fetched_crx_files_.empty() && !crx_install_is_running_) { 551 const FetchedCRXFile& crx_file = fetched_crx_files_.top(); 552 553 VLOG(2) << "updating " << crx_file.extension_id 554 << " with " << crx_file.path.value(); 555 556 // The ExtensionService is now responsible for cleaning up the temp file 557 // at |crx_file.path|. 558 CrxInstaller* installer = NULL; 559 if (service_->UpdateExtension(crx_file.extension_id, 560 crx_file.path, 561 crx_file.file_ownership_passed, 562 &installer)) { 563 crx_install_is_running_ = true; 564 current_crx_file_ = crx_file; 565 566 for (std::set<int>::const_iterator it = crx_file.request_ids.begin(); 567 it != crx_file.request_ids.end(); ++it) { 568 InProgressCheck& request = requests_in_progress_[*it]; 569 if (request.install_immediately) { 570 installer->set_install_immediately(true); 571 break; 572 } 573 } 574 575 // Source parameter ensures that we only see the completion event for the 576 // the installer we started. 577 registrar_.Add(this, 578 chrome::NOTIFICATION_CRX_INSTALLER_DONE, 579 content::Source<CrxInstaller>(installer)); 580 } else { 581 for (std::set<int>::const_iterator it = crx_file.request_ids.begin(); 582 it != crx_file.request_ids.end(); ++it) { 583 InProgressCheck& request = requests_in_progress_[*it]; 584 request.in_progress_ids_.remove(crx_file.extension_id); 585 } 586 request_ids.insert(crx_file.request_ids.begin(), 587 crx_file.request_ids.end()); 588 } 589 fetched_crx_files_.pop(); 590 } 591 592 for (std::set<int>::const_iterator it = request_ids.begin(); 593 it != request_ids.end(); ++it) { 594 NotifyIfFinished(*it); 595 } 596 } 597 598 void ExtensionUpdater::Observe(int type, 599 const content::NotificationSource& source, 600 const content::NotificationDetails& details) { 601 switch (type) { 602 case chrome::NOTIFICATION_CRX_INSTALLER_DONE: { 603 // No need to listen for CRX_INSTALLER_DONE anymore. 604 registrar_.Remove(this, 605 chrome::NOTIFICATION_CRX_INSTALLER_DONE, 606 source); 607 crx_install_is_running_ = false; 608 609 const FetchedCRXFile& crx_file = current_crx_file_; 610 for (std::set<int>::const_iterator it = crx_file.request_ids.begin(); 611 it != crx_file.request_ids.end(); ++it) { 612 InProgressCheck& request = requests_in_progress_[*it]; 613 request.in_progress_ids_.remove(crx_file.extension_id); 614 NotifyIfFinished(*it); 615 } 616 617 // If any files are available to update, start one. 618 MaybeInstallCRXFile(); 619 break; 620 } 621 case chrome::NOTIFICATION_EXTENSION_INSTALLED_DEPRECATED: { 622 const Extension* extension = 623 content::Details<const InstalledExtensionInfo>(details)->extension; 624 if (extension) 625 throttle_info_.erase(extension->id()); 626 break; 627 } 628 default: 629 NOTREACHED(); 630 } 631 } 632 633 void ExtensionUpdater::NotifyStarted() { 634 content::NotificationService::current()->Notify( 635 chrome::NOTIFICATION_EXTENSION_UPDATING_STARTED, 636 content::Source<Profile>(profile_), 637 content::NotificationService::NoDetails()); 638 } 639 640 void ExtensionUpdater::NotifyIfFinished(int request_id) { 641 DCHECK(ContainsKey(requests_in_progress_, request_id)); 642 const InProgressCheck& request = requests_in_progress_[request_id]; 643 if (request.in_progress_ids_.empty()) { 644 VLOG(2) << "Finished update check " << request_id; 645 if (!request.callback.is_null()) 646 request.callback.Run(); 647 requests_in_progress_.erase(request_id); 648 } 649 } 650 651 } // namespace extensions 652