| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/devtools/devtools_background_services_context_impl.h" |
| |
| #include <algorithm> |
| |
| #include "base/guid.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "content/public/common/content_client.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| std::string CreateEntryKeyPrefix(devtools::proto::BackgroundService service) { |
| DCHECK_NE(service, devtools::proto::BackgroundService::UNKNOWN); |
| return "devtools_background_services_" + base::NumberToString(service) + "_"; |
| } |
| |
| std::string CreateEntryKey(devtools::proto::BackgroundService service) { |
| return CreateEntryKeyPrefix(service) + base::GenerateGUID(); |
| } |
| |
| void DidLogServiceEvent(blink::ServiceWorkerStatusCode status) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.LogEvent", status); |
| } |
| |
| void DidClearServiceEvents(blink::ServiceWorkerStatusCode status) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.ClearEvents", status); |
| } |
| |
| constexpr devtools::proto::BackgroundService ServiceToProtoEnum( |
| DevToolsBackgroundService service) { |
| switch (service) { |
| case DevToolsBackgroundService::kBackgroundFetch: |
| return devtools::proto::BACKGROUND_FETCH; |
| case DevToolsBackgroundService::kBackgroundSync: |
| return devtools::proto::BACKGROUND_SYNC; |
| case DevToolsBackgroundService::kPushMessaging: |
| return devtools::proto::PUSH_MESSAGING; |
| case DevToolsBackgroundService::kNotifications: |
| return devtools::proto::NOTIFICATIONS; |
| case DevToolsBackgroundService::kPaymentHandler: |
| return devtools::proto::PAYMENT_HANDLER; |
| case DevToolsBackgroundService::kPeriodicBackgroundSync: |
| return devtools::proto::PERIODIC_BACKGROUND_SYNC; |
| } |
| } |
| |
| } // namespace |
| |
| DevToolsBackgroundServicesContextImpl::DevToolsBackgroundServicesContextImpl( |
| BrowserContext* browser_context, |
| scoped_refptr<ServiceWorkerContextWrapper> service_worker_context) |
| : browser_context_(browser_context), |
| service_worker_context_(std::move(service_worker_context)) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| auto expiration_times = |
| GetContentClient()->browser()->GetDevToolsBackgroundServiceExpirations( |
| browser_context_); |
| |
| for (const auto& expiration_time : expiration_times) { |
| DCHECK(devtools::proto::BackgroundService_IsValid(expiration_time.first)); |
| expiration_times_[expiration_time.first] = expiration_time.second; |
| |
| auto service = |
| static_cast<devtools::proto::BackgroundService>(expiration_time.first); |
| // If the recording permission for |service| has expired, set it to null. |
| if (IsRecordingExpired(service)) |
| expiration_times_[expiration_time.first] = base::Time(); |
| } |
| } |
| |
| DevToolsBackgroundServicesContextImpl:: |
| ~DevToolsBackgroundServicesContextImpl() = default; |
| |
| void DevToolsBackgroundServicesContextImpl::AddObserver( |
| EventObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::RemoveObserver( |
| const EventObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::StartRecording( |
| devtools::proto::BackgroundService service) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // TODO(rayankans): Make the time delay finch configurable. |
| base::Time expiration_time = base::Time::Now() + base::TimeDelta::FromDays(3); |
| expiration_times_[service] = expiration_time; |
| |
| GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration( |
| browser_context_, service, expiration_time); |
| |
| for (EventObserver& observer : observers_) |
| observer.OnRecordingStateChanged(/* should_record= */ true, service); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::StopRecording( |
| devtools::proto::BackgroundService service) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| expiration_times_[service] = base::Time(); |
| GetContentClient()->browser()->UpdateDevToolsBackgroundServiceExpiration( |
| browser_context_, service, base::Time()); |
| |
| for (EventObserver& observer : observers_) |
| observer.OnRecordingStateChanged(/* should_record= */ false, service); |
| } |
| |
| bool DevToolsBackgroundServicesContextImpl::IsRecording( |
| DevToolsBackgroundService service) { |
| return IsRecording(ServiceToProtoEnum(service)); |
| } |
| |
| bool DevToolsBackgroundServicesContextImpl::IsRecording( |
| devtools::proto::BackgroundService service) { |
| // Returns whether |service| has been enabled. When the expiration time has |
| // been met it will be lazily updated to be null. |
| return !expiration_times_[service].is_null(); |
| } |
| |
| bool DevToolsBackgroundServicesContextImpl::IsRecordingExpired( |
| devtools::proto::BackgroundService service) { |
| // Copy the expiration time to avoid data races. |
| const base::Time expiration_time = expiration_times_[service]; |
| return !expiration_time.is_null() && expiration_time < base::Time::Now(); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::GetLoggedBackgroundServiceEvents( |
| devtools::proto::BackgroundService service, |
| GetLoggedBackgroundServiceEventsCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| RunOrPostTaskOnThread( |
| FROM_HERE, ServiceWorkerContext::GetCoreThreadId(), |
| base::BindOnce(&DevToolsBackgroundServicesContextImpl:: |
| GetLoggedBackgroundServiceEventsOnCoreThread, |
| weak_ptr_factory_core_.GetWeakPtr(), service, |
| std::move(callback))); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl:: |
| GetLoggedBackgroundServiceEventsOnCoreThread( |
| devtools::proto::BackgroundService service, |
| GetLoggedBackgroundServiceEventsCallback callback) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| |
| service_worker_context_->GetUserDataForAllRegistrationsByKeyPrefix( |
| CreateEntryKeyPrefix(service), |
| base::BindOnce(&DevToolsBackgroundServicesContextImpl::DidGetUserData, |
| weak_ptr_factory_core_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::DidGetUserData( |
| GetLoggedBackgroundServiceEventsCallback callback, |
| const std::vector<std::pair<int64_t, std::string>>& user_data, |
| blink::ServiceWorkerStatusCode status) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| |
| UMA_HISTOGRAM_ENUMERATION("DevTools.BackgroundService.GetEvents", status); |
| |
| std::vector<devtools::proto::BackgroundServiceEvent> events; |
| |
| if (status != blink::ServiceWorkerStatusCode::kOk) { |
| std::move(callback).Run(events); |
| return; |
| } |
| |
| events.reserve(user_data.size()); |
| for (const auto& data : user_data) { |
| devtools::proto::BackgroundServiceEvent event; |
| if (!event.ParseFromString(data.second)) { |
| // TODO(rayankans): Log errors to UMA. |
| std::move(callback).Run({}); |
| return; |
| } |
| DCHECK_EQ(data.first, event.service_worker_registration_id()); |
| events.push_back(std::move(event)); |
| } |
| |
| std::sort(events.begin(), events.end(), |
| [](const auto& state1, const auto& state2) { |
| return state1.timestamp() < state2.timestamp(); |
| }); |
| |
| GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), std::move(events))); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::ClearLoggedBackgroundServiceEvents( |
| devtools::proto::BackgroundService service) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| RunOrPostTaskOnThread( |
| FROM_HERE, ServiceWorkerContext::GetCoreThreadId(), |
| base::BindOnce(&DevToolsBackgroundServicesContextImpl:: |
| ClearLoggedBackgroundServiceEventsOnCoreThread, |
| weak_ptr_factory_core_.GetWeakPtr(), service)); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl:: |
| ClearLoggedBackgroundServiceEventsOnCoreThread( |
| devtools::proto::BackgroundService service) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| |
| service_worker_context_->ClearUserDataForAllRegistrationsByKeyPrefix( |
| CreateEntryKeyPrefix(service), base::BindOnce(&DidClearServiceEvents)); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::LogBackgroundServiceEvent( |
| uint64_t service_worker_registration_id, |
| const url::Origin& origin, |
| DevToolsBackgroundService service, |
| const std::string& event_name, |
| const std::string& instance_id, |
| const std::map<std::string, std::string>& event_metadata) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| RunOrPostTaskOnThread( |
| FROM_HERE, ServiceWorkerContext::GetCoreThreadId(), |
| base::BindOnce(&DevToolsBackgroundServicesContextImpl:: |
| LogBackgroundServiceEventOnCoreThread, |
| weak_ptr_factory_core_.GetWeakPtr(), |
| service_worker_registration_id, origin, service, |
| event_name, instance_id, event_metadata)); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl:: |
| LogBackgroundServiceEventOnCoreThread( |
| uint64_t service_worker_registration_id, |
| const url::Origin& origin, |
| DevToolsBackgroundService service, |
| const std::string& event_name, |
| const std::string& instance_id, |
| const std::map<std::string, std::string>& event_metadata) { |
| DCHECK_CURRENTLY_ON(ServiceWorkerContext::GetCoreThreadId()); |
| |
| if (!IsRecording(service)) |
| return; |
| |
| if (IsRecordingExpired(ServiceToProtoEnum(service))) { |
| // We should stop recording because of the expiration time. We should |
| // also inform the observers that we stopped recording. |
| RunOrPostTaskOnThread( |
| FROM_HERE, BrowserThread::UI, |
| base::BindOnce( |
| &DevToolsBackgroundServicesContextImpl::OnRecordingTimeExpired, |
| weak_ptr_factory_ui_.GetWeakPtr(), ServiceToProtoEnum(service))); |
| return; |
| } |
| |
| devtools::proto::BackgroundServiceEvent event; |
| event.set_timestamp( |
| base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| event.set_origin(origin.GetURL().spec()); |
| event.set_service_worker_registration_id(service_worker_registration_id); |
| event.set_background_service(ServiceToProtoEnum(service)); |
| event.set_event_name(event_name); |
| event.set_instance_id(instance_id); |
| event.mutable_event_metadata()->insert(event_metadata.begin(), |
| event_metadata.end()); |
| |
| service_worker_context_->StoreRegistrationUserData( |
| service_worker_registration_id, origin, |
| {{CreateEntryKey(event.background_service()), event.SerializeAsString()}}, |
| base::BindOnce(&DidLogServiceEvent)); |
| |
| RunOrPostTaskOnThread( |
| FROM_HERE, BrowserThread::UI, |
| base::BindOnce( |
| &DevToolsBackgroundServicesContextImpl::NotifyEventObservers, |
| weak_ptr_factory_ui_.GetWeakPtr(), std::move(event))); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::NotifyEventObservers( |
| const devtools::proto::BackgroundServiceEvent& event) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| for (EventObserver& observer : observers_) |
| observer.OnEventReceived(event); |
| } |
| |
| void DevToolsBackgroundServicesContextImpl::OnRecordingTimeExpired( |
| devtools::proto::BackgroundService service) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // This could have been stopped by the user in the meanwhile, or we |
| // received duplicate time expiry events. |
| if (IsRecordingExpired(service)) |
| StopRecording(service); |
| } |
| |
| } // namespace content |