| // Copyright 2017 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/service_worker/service_worker_installed_scripts_sender.h" |
| |
| #include "base/memory/ref_counted.h" |
| #include "base/trace_event/trace_event.h" |
| #include "content/browser/service_worker/service_worker_context_core.h" |
| #include "content/browser/service_worker/service_worker_disk_cache.h" |
| #include "content/browser/service_worker/service_worker_script_cache_map.h" |
| #include "content/browser/service_worker/service_worker_storage.h" |
| #include "net/http/http_response_headers.h" |
| #include "services/network/public/cpp/net_adapters.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| class MetaDataSender { |
| public: |
| enum class Status { kSuccess, kFailed }; |
| |
| MetaDataSender(scoped_refptr<net::IOBufferWithSize> meta_data, |
| mojo::ScopedDataPipeProducerHandle handle) |
| : meta_data_(std::move(meta_data)), |
| bytes_sent_(0), |
| handle_(std::move(handle)), |
| watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC), |
| weak_factory_(this) {} |
| |
| void Start(base::OnceCallback<void(Status)> callback) { |
| callback_ = std::move(callback); |
| watcher_.Watch( |
| handle_.get(), MOJO_HANDLE_SIGNAL_WRITABLE, |
| base::Bind(&MetaDataSender::OnWritable, weak_factory_.GetWeakPtr())); |
| } |
| |
| void OnWritable(MojoResult) { |
| // It isn't necessary to handle MojoResult here since WriteDataRaw() |
| // returns an equivalent error. |
| uint32_t size = meta_data_->size() - bytes_sent_; |
| MojoResult rv = handle_->WriteData(meta_data_->data() + bytes_sent_, &size, |
| MOJO_WRITE_DATA_FLAG_NONE); |
| switch (rv) { |
| case MOJO_RESULT_INVALID_ARGUMENT: |
| case MOJO_RESULT_OUT_OF_RANGE: |
| case MOJO_RESULT_BUSY: |
| NOTREACHED(); |
| return; |
| case MOJO_RESULT_FAILED_PRECONDITION: |
| OnCompleted(Status::kFailed); |
| return; |
| case MOJO_RESULT_SHOULD_WAIT: |
| return; |
| case MOJO_RESULT_OK: |
| break; |
| default: |
| // mojo::WriteDataRaw() should not return any other values. |
| OnCompleted(Status::kFailed); |
| return; |
| } |
| bytes_sent_ += size; |
| if (meta_data_->size() == bytes_sent_) |
| OnCompleted(Status::kSuccess); |
| } |
| |
| void OnCompleted(Status status) { |
| watcher_.Cancel(); |
| handle_.reset(); |
| std::move(callback_).Run(status); |
| } |
| |
| private: |
| base::OnceCallback<void(Status)> callback_; |
| |
| scoped_refptr<net::IOBufferWithSize> meta_data_; |
| int64_t bytes_sent_; |
| mojo::ScopedDataPipeProducerHandle handle_; |
| mojo::SimpleWatcher watcher_; |
| |
| base::WeakPtrFactory<MetaDataSender> weak_factory_; |
| }; |
| |
| } // namespace |
| |
| // Sender sends a single script to the renderer and calls |
| // ServiceWorkerIsntalledScriptsSender::OnFinishSendingScript() when done. |
| class ServiceWorkerInstalledScriptsSender::Sender { |
| public: |
| Sender(std::unique_ptr<ServiceWorkerResponseReader> reader, |
| ServiceWorkerInstalledScriptsSender* owner) |
| : reader_(std::move(reader)), |
| owner_(owner), |
| body_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL), |
| weak_factory_(this) {} |
| |
| void Start() { |
| auto info_buf = base::MakeRefCounted<HttpResponseInfoIOBuffer>(); |
| reader_->ReadInfo(info_buf.get(), base::Bind(&Sender::OnReadInfoComplete, |
| AsWeakPtr(), info_buf)); |
| } |
| |
| private: |
| void OnReadInfoComplete(scoped_refptr<HttpResponseInfoIOBuffer> http_info, |
| int result) { |
| DCHECK(owner_); |
| DCHECK(http_info); |
| if (!http_info->http_info) { |
| DCHECK_LT(result, 0); |
| ServiceWorkerMetrics::CountReadResponseResult( |
| ServiceWorkerMetrics::READ_HEADERS_ERROR); |
| CompleteSendIfNeeded(FinishedReason::kNoHttpInfoError); |
| return; |
| } |
| |
| DCHECK_GE(result, 0); |
| mojo::ScopedDataPipeConsumerHandle meta_data_consumer; |
| mojo::ScopedDataPipeConsumerHandle body_consumer; |
| DCHECK_GE(http_info->response_data_size, 0); |
| uint64_t body_size = http_info->response_data_size; |
| uint64_t meta_data_size = 0; |
| if (mojo::CreateDataPipe(nullptr, &body_handle_, &body_consumer) != |
| MOJO_RESULT_OK) { |
| CompleteSendIfNeeded(FinishedReason::kCreateDataPipeError); |
| return; |
| } |
| // Start sending meta data (V8 code cache data). |
| if (http_info->http_info->metadata) { |
| mojo::ScopedDataPipeProducerHandle meta_data_producer; |
| if (mojo::CreateDataPipe(nullptr, &meta_data_producer, |
| &meta_data_consumer) != MOJO_RESULT_OK) { |
| CompleteSendIfNeeded(FinishedReason::kCreateDataPipeError); |
| return; |
| } |
| meta_data_sender_ = std::make_unique<MetaDataSender>( |
| http_info->http_info->metadata, std::move(meta_data_producer)); |
| meta_data_sender_->Start( |
| base::BindOnce(&Sender::OnMetaDataSent, AsWeakPtr())); |
| DCHECK_GE(http_info->http_info->metadata->size(), 0); |
| meta_data_size = http_info->http_info->metadata->size(); |
| } |
| |
| // Start sending body. |
| body_watcher_.Watch(body_handle_.get(), MOJO_HANDLE_SIGNAL_WRITABLE, |
| base::Bind(&Sender::OnWritableBody, AsWeakPtr())); |
| body_watcher_.ArmOrNotify(); |
| |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| http_info->http_info->headers; |
| DCHECK(headers); |
| |
| std::string charset; |
| headers->GetCharset(&charset); |
| |
| // Create a map of response headers. |
| std::unordered_map<std::string, std::string> header_strings; |
| size_t iter = 0; |
| std::string key; |
| std::string value; |
| // This logic is copied from blink::ResourceResponse::AddHTTPHeaderField. |
| while (headers->EnumerateHeaderLines(&iter, &key, &value)) { |
| if (header_strings.find(key) == header_strings.end()) { |
| header_strings[key] = value; |
| } else { |
| header_strings[key] += ", " + value; |
| } |
| } |
| |
| owner_->SendScriptInfoToRenderer( |
| charset, std::move(header_strings), std::move(body_consumer), body_size, |
| std::move(meta_data_consumer), meta_data_size); |
| owner_->OnHttpInfoRead(http_info); |
| } |
| |
| void OnWritableBody(MojoResult) { |
| // It isn't necessary to handle MojoResult here since BeginWrite() returns |
| // an equivalent error. |
| DCHECK(!body_pending_write_); |
| DCHECK(body_handle_.is_valid()); |
| uint32_t num_bytes = 0; |
| MojoResult rv = network::NetToMojoPendingBuffer::BeginWrite( |
| &body_handle_, &body_pending_write_, &num_bytes); |
| switch (rv) { |
| case MOJO_RESULT_INVALID_ARGUMENT: |
| case MOJO_RESULT_BUSY: |
| NOTREACHED(); |
| return; |
| case MOJO_RESULT_FAILED_PRECONDITION: |
| CompleteSendIfNeeded(FinishedReason::kConnectionError); |
| return; |
| case MOJO_RESULT_SHOULD_WAIT: |
| body_watcher_.ArmOrNotify(); |
| return; |
| case MOJO_RESULT_OK: |
| // |body_handle_| must have been taken by |body_pending_write_|. |
| DCHECK(body_pending_write_); |
| DCHECK(!body_handle_.is_valid()); |
| break; |
| } |
| |
| scoped_refptr<network::NetToMojoIOBuffer> buffer = |
| base::MakeRefCounted<network::NetToMojoIOBuffer>( |
| body_pending_write_.get()); |
| reader_->ReadData(buffer.get(), num_bytes, |
| base::Bind(&Sender::OnResponseDataRead, AsWeakPtr())); |
| } |
| |
| void OnResponseDataRead(int read_bytes) { |
| if (read_bytes < 0) { |
| ServiceWorkerMetrics::CountReadResponseResult( |
| ServiceWorkerMetrics::READ_DATA_ERROR); |
| body_watcher_.Cancel(); |
| body_handle_.reset(); |
| CompleteSendIfNeeded(FinishedReason::kResponseReaderError); |
| return; |
| } |
| body_handle_ = body_pending_write_->Complete(read_bytes); |
| DCHECK(body_handle_.is_valid()); |
| body_pending_write_ = nullptr; |
| ServiceWorkerMetrics::CountReadResponseResult( |
| ServiceWorkerMetrics::READ_OK); |
| if (read_bytes == 0) { |
| // All data has been read. |
| body_watcher_.Cancel(); |
| body_handle_.reset(); |
| CompleteSendIfNeeded(FinishedReason::kSuccess); |
| return; |
| } |
| body_watcher_.ArmOrNotify(); |
| } |
| |
| void OnMetaDataSent(MetaDataSender::Status status) { |
| meta_data_sender_.reset(); |
| if (status != MetaDataSender::Status::kSuccess) { |
| body_watcher_.Cancel(); |
| body_handle_.reset(); |
| CompleteSendIfNeeded(FinishedReason::kMetaDataSenderError); |
| return; |
| } |
| |
| CompleteSendIfNeeded(FinishedReason::kSuccess); |
| } |
| |
| // CompleteSendIfNeeded notifies the end of data transfer to |owner_|, and |
| // |this| will be removed by |owner_| as a result. Errors are notified |
| // immediately, but when the transfer has been succeeded, it's notified when |
| // sending both of body and meta data is finished. |
| void CompleteSendIfNeeded(FinishedReason reason) { |
| if (reason != FinishedReason::kSuccess) { |
| owner_->OnFinishSendingScript(reason); |
| return; |
| } |
| |
| if (WasMetadataWritten() && WasBodyWritten()) |
| owner_->OnFinishSendingScript(reason); |
| } |
| |
| bool WasMetadataWritten() const { return !meta_data_sender_; } |
| |
| bool WasBodyWritten() const { |
| return !body_handle_.is_valid() && !body_pending_write_; |
| } |
| |
| base::WeakPtr<Sender> AsWeakPtr() { return weak_factory_.GetWeakPtr(); } |
| |
| std::unique_ptr<ServiceWorkerResponseReader> reader_; |
| ServiceWorkerInstalledScriptsSender* owner_; |
| |
| // For meta data. |
| std::unique_ptr<MetaDataSender> meta_data_sender_; |
| |
| // For body. |
| // Either |body_handle_| or |body_pending_write_| is valid during body is |
| // streamed. |
| mojo::ScopedDataPipeProducerHandle body_handle_; |
| scoped_refptr<network::NetToMojoPendingBuffer> body_pending_write_; |
| mojo::SimpleWatcher body_watcher_; |
| |
| base::WeakPtrFactory<Sender> weak_factory_; |
| }; |
| |
| ServiceWorkerInstalledScriptsSender::ServiceWorkerInstalledScriptsSender( |
| ServiceWorkerVersion* owner) |
| : owner_(owner), |
| main_script_url_(owner_->script_url()), |
| main_script_id_( |
| owner_->script_cache_map()->LookupResourceId(main_script_url_)), |
| sent_main_script_(false), |
| binding_(this), |
| state_(State::kNotStarted), |
| last_finished_reason_(FinishedReason::kNotFinished) { |
| DCHECK(ServiceWorkerVersion::IsInstalled(owner_->status())); |
| DCHECK_NE(kInvalidServiceWorkerResourceId, main_script_id_); |
| } |
| |
| ServiceWorkerInstalledScriptsSender::~ServiceWorkerInstalledScriptsSender() {} |
| |
| blink::mojom::ServiceWorkerInstalledScriptsInfoPtr |
| ServiceWorkerInstalledScriptsSender::CreateInfoAndBind() { |
| DCHECK_EQ(State::kNotStarted, state_); |
| |
| std::vector<ServiceWorkerDatabase::ResourceRecord> resources; |
| owner_->script_cache_map()->GetResources(&resources); |
| std::vector<GURL> installed_urls; |
| for (const auto& resource : resources) { |
| installed_urls.emplace_back(resource.url); |
| if (resource.url == main_script_url_) |
| continue; |
| pending_scripts_.emplace(resource.resource_id, resource.url); |
| } |
| DCHECK(!installed_urls.empty()) |
| << "At least the main script should be installed."; |
| |
| auto info = blink::mojom::ServiceWorkerInstalledScriptsInfo::New(); |
| info->manager_request = mojo::MakeRequest(&manager_); |
| info->installed_urls = std::move(installed_urls); |
| binding_.Bind(mojo::MakeRequest(&info->manager_host_ptr)); |
| return info; |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::Start() { |
| DCHECK_EQ(State::kNotStarted, state_); |
| DCHECK_NE(kInvalidServiceWorkerResourceId, main_script_id_); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("ServiceWorker", |
| "ServiceWorkerInstalledScriptsSender", this, |
| "main_script_url", main_script_url_.spec()); |
| StartSendingScript(main_script_id_, main_script_url_); |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::StartSendingScript( |
| int64_t resource_id, |
| const GURL& script_url) { |
| DCHECK(!running_sender_); |
| DCHECK(current_sending_url_.is_empty()); |
| state_ = State::kSendingScripts; |
| current_sending_url_ = script_url; |
| |
| auto reader = owner_->context()->storage()->CreateResponseReader(resource_id); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("ServiceWorker", "SendingScript", this, |
| "script_url", current_sending_url_.spec()); |
| running_sender_ = std::make_unique<Sender>(std::move(reader), this); |
| running_sender_->Start(); |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::SendScriptInfoToRenderer( |
| std::string encoding, |
| std::unordered_map<std::string, std::string> headers, |
| mojo::ScopedDataPipeConsumerHandle body_handle, |
| uint64_t body_size, |
| mojo::ScopedDataPipeConsumerHandle meta_data_handle, |
| uint64_t meta_data_size) { |
| DCHECK(running_sender_); |
| DCHECK_EQ(State::kSendingScripts, state_); |
| TRACE_EVENT_NESTABLE_ASYNC_INSTANT2( |
| "ServiceWorker", "SendScriptInfoToRenderer", this, "body_size", body_size, |
| "meta_data_size", meta_data_size); |
| auto script_info = blink::mojom::ServiceWorkerScriptInfo::New(); |
| script_info->script_url = current_sending_url_; |
| script_info->headers = std::move(headers); |
| script_info->encoding = std::move(encoding); |
| script_info->body = std::move(body_handle); |
| script_info->body_size = body_size; |
| script_info->meta_data = std::move(meta_data_handle); |
| script_info->meta_data_size = meta_data_size; |
| manager_->TransferInstalledScript(std::move(script_info)); |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::OnHttpInfoRead( |
| scoped_refptr<HttpResponseInfoIOBuffer> http_info) { |
| DCHECK(running_sender_); |
| DCHECK_EQ(State::kSendingScripts, state_); |
| if (IsSendingMainScript()) |
| owner_->SetMainScriptHttpResponseInfo(*http_info->http_info); |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::OnFinishSendingScript( |
| FinishedReason reason) { |
| DCHECK(running_sender_); |
| DCHECK_EQ(State::kSendingScripts, state_); |
| TRACE_EVENT_NESTABLE_ASYNC_END0("ServiceWorker", "SendingScript", this); |
| running_sender_.reset(); |
| current_sending_url_ = GURL(); |
| |
| if (IsSendingMainScript()) |
| sent_main_script_ = true; |
| |
| if (reason != FinishedReason::kSuccess) { |
| Abort(reason); |
| return; |
| } |
| |
| if (pending_scripts_.empty()) { |
| UpdateFinishedReasonAndBecomeIdle(FinishedReason::kSuccess); |
| TRACE_EVENT_NESTABLE_ASYNC_END0( |
| "ServiceWorker", "ServiceWorkerInstalledScriptsSender", this); |
| return; |
| } |
| |
| // Start sending the next script. |
| int64_t next_id = pending_scripts_.front().first; |
| GURL next_url = pending_scripts_.front().second; |
| pending_scripts_.pop(); |
| StartSendingScript(next_id, next_url); |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::Abort(FinishedReason reason) { |
| DCHECK_EQ(State::kSendingScripts, state_); |
| DCHECK_NE(FinishedReason::kSuccess, reason); |
| TRACE_EVENT_NESTABLE_ASYNC_END1("ServiceWorker", |
| "ServiceWorkerInstalledScriptsSender", this, |
| "FinishedReason", static_cast<int>(reason)); |
| |
| // Remove all pending scripts. |
| // Note that base::queue doesn't have clear(), and also base::STLClearObject |
| // is not applicable for base::queue since it doesn't have reserve(). |
| base::queue<std::pair<int64_t, GURL>> empty; |
| pending_scripts_.swap(empty); |
| |
| UpdateFinishedReasonAndBecomeIdle(reason); |
| |
| switch (reason) { |
| case FinishedReason::kNotFinished: |
| case FinishedReason::kSuccess: |
| NOTREACHED(); |
| return; |
| case FinishedReason::kNoHttpInfoError: |
| case FinishedReason::kResponseReaderError: |
| owner_->SetStartWorkerStatusCode(SERVICE_WORKER_ERROR_DISK_CACHE); |
| // Abort the worker by deleting from the registration since the data was |
| // corrupted. |
| if (owner_->context()) { |
| ServiceWorkerRegistration* registration = |
| owner_->context()->GetLiveRegistration(owner_->registration_id()); |
| // This ends up with destructing |this|. |
| registration->DeleteVersion(owner_); |
| } |
| return; |
| case FinishedReason::kCreateDataPipeError: |
| case FinishedReason::kConnectionError: |
| case FinishedReason::kMetaDataSenderError: |
| // Notify the renderer that a connection failure happened. Usually the |
| // failure means the renderer gets killed, and the error handler of |
| // EmbeddedWorkerInstance is invoked soon. |
| manager_.reset(); |
| binding_.Close(); |
| return; |
| } |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::UpdateFinishedReasonAndBecomeIdle( |
| FinishedReason reason) { |
| DCHECK_EQ(State::kSendingScripts, state_); |
| DCHECK_NE(FinishedReason::kNotFinished, reason); |
| DCHECK(current_sending_url_.is_empty()); |
| state_ = State::kIdle; |
| last_finished_reason_ = reason; |
| } |
| |
| void ServiceWorkerInstalledScriptsSender::RequestInstalledScript( |
| const GURL& script_url) { |
| TRACE_EVENT1("ServiceWorker", |
| "ServiceWorkerInstalledScriptsSender::RequestInstalledScript", |
| "script_url", script_url.spec()); |
| int64_t resource_id = |
| owner_->script_cache_map()->LookupResourceId(script_url); |
| |
| if (resource_id == kInvalidServiceWorkerResourceId) { |
| mojo::ReportBadMessage("Requested script was not installed."); |
| return; |
| } |
| |
| if (state_ == State::kSendingScripts) { |
| // The sender is now sending other scripts. Push the requested script into |
| // the waiting queue. |
| pending_scripts_.emplace(resource_id, script_url); |
| return; |
| } |
| |
| DCHECK_EQ(State::kIdle, state_); |
| TRACE_EVENT_NESTABLE_ASYNC_BEGIN1("ServiceWorker", |
| "ServiceWorkerInstalledScriptsSender", this, |
| "main_script_url", main_script_url_.spec()); |
| StartSendingScript(resource_id, script_url); |
| } |
| |
| bool ServiceWorkerInstalledScriptsSender::IsSendingMainScript() const { |
| // |current_sending_url_| could match |main_script_url_| even though |
| // |sent_main_script_| is false if calling importScripts for the main |
| // script. |
| return !sent_main_script_ && current_sending_url_ == main_script_url_; |
| } |
| |
| } // namespace content |