blob: edc69fb30cbeb108681327da993a93cdc886d625 [file] [log] [blame]
// Copyright 2022 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/worker/worker_global_scope.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "base/message_loop/message_loop_current.h"
#include "base/threading/thread.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/loader/origin.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/web/context.h"
#include "cobalt/web/dom_exception.h"
#include "cobalt/web/user_agent_platform_info.h"
#include "cobalt/web/window_or_worker_global_scope.h"
#include "cobalt/web/window_timers.h"
#include "cobalt/worker/service_worker_object.h"
#include "cobalt/worker/worker_location.h"
#include "cobalt/worker/worker_navigator.h"
#include "starboard/atomic.h"
#include "url/gurl.h"
namespace cobalt {
namespace worker {
namespace {
bool PermitAnyURL(const GURL& url, bool) { return true; }
class ScriptLoader : public base::MessageLoop::DestructionObserver {
public:
explicit ScriptLoader(web::Context* context) : context_(context) {
thread_.reset(new base::Thread("ImportScriptsLoader"));
thread_->Start();
// Register as a destruction observer to shut down the Loaders once all
// pending tasks have been executed and the message loop is about to be
// destroyed. This allows us to safely stop the thread, drain the task
// queue, then destroy the internal components before the message loop is
// reset. No posted tasks will be executed once the thread is stopped.
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE, base::Bind(&base::MessageLoop::AddDestructionObserver,
base::Unretained(thread_->message_loop()),
base::Unretained(this)));
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](loader::FetcherFactory* fetcher_factory,
std::unique_ptr<loader::ScriptLoaderFactory>*
script_loader_factory_) {
TRACE_EVENT0("cobalt::worker",
"ScriptLoader::ScriptLoaderFactory Task");
script_loader_factory_->reset(new loader::ScriptLoaderFactory(
"ImportScriptsLoader", fetcher_factory));
},
context_->fetcher_factory(), &script_loader_factory_));
}
~ScriptLoader() {
if (thread_) {
// Stop the thread. This will cause the destruction observer to be
// notified.
thread_->Stop();
}
}
void WillDestroyCurrentMessageLoop() {
// Destroy members that were constructed in the worker thread.
errors_.clear();
errors_.shrink_to_fit();
contents_.clear();
contents_.shrink_to_fit();
loaders_.clear();
loaders_.shrink_to_fit();
script_loader_factory_.reset();
}
void Load(const loader::Origin& origin,
const std::vector<GURL>& resolved_urls) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::Load()");
number_of_loads_ = resolved_urls.size();
contents_.resize(resolved_urls.size());
loaders_.resize(resolved_urls.size());
errors_.resize(resolved_urls.size());
if (resolved_urls.empty()) return;
for (int i = 0; i < resolved_urls.size(); ++i) {
const GURL& url = resolved_urls[i];
thread_->message_loop()->task_runner()->PostTask(
FROM_HERE, base::BindOnce(&ScriptLoader::LoaderTask,
base::Unretained(this), &loaders_[i],
origin, url, &contents_[i], &errors_[i]));
}
load_finished_.Wait();
}
void LoaderTask(std::unique_ptr<loader::Loader>* loader,
const loader::Origin& origin, const GURL& url,
std::unique_ptr<std::string>* content,
std::unique_ptr<std::string>* error) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoaderTask()");
// Todo: implement csp check (b/225037465)
csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
// If there is a request callback, call it to possibly retrieve previously
// requested content.
*loader = script_loader_factory_->CreateScriptLoader(
url, origin, csp_callback,
base::Bind(
[](std::unique_ptr<std::string>* output_content,
const loader::Origin& last_url_origin,
std::unique_ptr<std::string> content) {
*output_content = std::move(content);
},
content),
base::Bind(&ScriptLoader::LoadingCompleteCallback,
base::Unretained(this), loader, error));
}
void LoadingCompleteCallback(std::unique_ptr<loader::Loader>* loader,
std::unique_ptr<std::string>* output_error,
const base::Optional<std::string>& error) {
TRACE_EVENT0("cobalt::worker", "ScriptLoader::LoadingCompleteCallback()");
if (error) {
output_error->reset(new std::string(std::move(error.value())));
}
if (!SbAtomicNoBarrier_Increment(&number_of_loads_, -1)) {
// Clear the loader factory after this callback
// completes.
base::MessageLoop::current()->task_runner()->PostTask(
FROM_HERE, base::BindOnce(
[](base::WaitableEvent* load_finished_) {
load_finished_->Signal();
},
&load_finished_));
}
}
const std::unique_ptr<std::string>& GetContents(int index) {
return contents_[index];
}
const std::unique_ptr<std::string>& GetError(int index) {
return errors_[index];
}
private:
web::Context* context_;
base::WaitableEvent load_finished_ = {
base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED};
std::unique_ptr<base::Thread> thread_;
std::unique_ptr<loader::ScriptLoaderFactory> script_loader_factory_;
volatile SbAtomic32 number_of_loads_;
std::vector<std::unique_ptr<std::string>> contents_;
std::vector<std::unique_ptr<std::string>> errors_;
std::vector<std::unique_ptr<loader::Loader>> loaders_;
base::Optional<std::string> error_;
};
} // namespace
WorkerGlobalScope::WorkerGlobalScope(script::EnvironmentSettings* settings)
: web::WindowOrWorkerGlobalScope(
settings, /*stat_tracker=*/NULL,
// TODO (b/233788170): once application state is
// available, update this to use the actual state.
base::ApplicationState::kApplicationStateStarted),
location_(new WorkerLocation(settings->creation_url())),
navigator_(new WorkerNavigator(settings)) {
set_navigator_base(navigator_);
}
void WorkerGlobalScope::ImportScripts(const std::vector<std::string>& urls,
script::ExceptionState* exception_state) {
ImportScriptsInternal(urls, exception_state, URLLookupCallback(),
ResponseCallback());
}
bool WorkerGlobalScope::LoadImportsAndReturnIfUpdated(
const ScriptResourceMap& previous_resource_map,
ScriptResourceMap* new_resource_map) {
bool has_updated_resources = false;
// Steps from Algorithm for Update:
// https://w3c.github.io/ServiceWorker/#update-algorithm
// 8.21.1. For each importUrl -> storedResponse of newestWorker’s script
// resource map:
std::vector<GURL> request_urls;
for (const auto& resource_map_entry : previous_resource_map) {
// 8.21.1.1. If importUrl is url, then continue.
if (new_resource_map->find(resource_map_entry.first) !=
new_resource_map->end()) {
continue;
}
request_urls.push_back(resource_map_entry.first);
}
// 8.21.1.2. Let importRequest be a new request whose url is importUrl,
// client is job’s client, destination is "script", parser
// metadata is "not parser-inserted", synchronous flag is set,
// and whose use-URL-credentials flag is set.
// 8.21.1.3. Set importRequest’s cache mode to "no-cache" if any of the
// following are true:
// - registration’s update via cache mode is "none".
// - job’s force bypass cache flag is set.
// - registration is stale.
// 8.21.1.4. Let fetchedResponse be the result of fetching importRequest.
web::EnvironmentSettings* settings = environment_settings();
const GURL& base_url = settings->base_url();
loader::Origin origin = loader::Origin(base_url.GetOrigin());
ScriptLoader script_loader(settings->context());
script_loader.Load(origin, request_urls);
for (int index = 0; index < request_urls.size(); ++index) {
const auto& error = script_loader.GetError(index);
if (error) continue;
const GURL& url = request_urls[index];
const std::unique_ptr<std::string>& script =
script_loader.GetContents(index);
// 8.21.1.5. Set updatedResourceMap[importRequest’s url] to
// fetchedResponse.
(*new_resource_map)[url].reset(new std::string(*script.get()));
// 8.21.1.6. Set fetchedResponse to fetchedResponse’s unsafe response.
// 8.21.1.7. If fetchedResponse’s cache state is not
// "local", set registration’s last update check time to the
// current time.
// 8.21.1.8. If fetchedResponse is a bad import script response, continue.
// 8.21.1.9. If fetchedResponse’s body is not byte-for-byte identical with
// storedResponse’s unsafe response's body, set
// hasUpdatedResources to true.
DCHECK(previous_resource_map.find(url) != previous_resource_map.end());
if (*script != *(previous_resource_map.find(url)->second)) {
has_updated_resources = true;
}
}
return has_updated_resources;
}
void WorkerGlobalScope::ImportScriptsInternal(
const std::vector<std::string>& urls,
script::ExceptionState* exception_state,
URLLookupCallback url_lookup_callback, ResponseCallback response_callback) {
// Algorithm for import scripts into worker global scope:
// https://html.spec.whatwg.org/multipage/workers.html#import-scripts-into-worker-global-scope
// 1. If worker global scope's type is "module", throw a TypeError exception.
// Cobalt does not support "module" type scripts.
// 2. Let settings object be the current settings object.
web::EnvironmentSettings* settings = environment_settings();
DCHECK(settings->context()
->message_loop()
->task_runner()
->BelongsToCurrentThread());
// 3. If urls is empty, return.
if (urls.empty()) return;
// 4. Parse each value in urls relative to settings object. If any fail, throw
// a "SyntaxError" DOMException.
std::vector<GURL> request_urls;
std::vector<std::string*> looked_up_content;
request_urls.reserve(urls.size());
std::vector<GURL> resolved_urls(urls.size());
std::vector<int> request_url_indexes(urls.size());
const GURL& base_url = settings->base_url();
for (int index = 0; index < urls.size(); ++index) {
const std::string& url = urls[index];
resolved_urls[index] = base_url.Resolve(url);
if (resolved_urls[index].is_empty()) {
web::DOMException::Raise(web::DOMException::kSyntaxErr, exception_state);
return;
}
std::string* content =
url_lookup_callback
? url_lookup_callback.Run(resolved_urls[index], exception_state)
: nullptr;
// Return if the url lookup callback has set the exception state.
if (exception_state->is_exception_set()) return;
if (content) {
// Store the result of the url lookup callback.
request_url_indexes[index] = -1;
looked_up_content.push_back(content);
} else {
// Add the url to the list to pass to ScriptLoader for loading.
request_url_indexes[index] = request_urls.size();
request_urls.push_back(resolved_urls[index]);
}
}
loader::Origin origin = loader::Origin(base_url.GetOrigin());
// 5. For each url in the resulting URL records, run these substeps:
// 5.1. Fetch a classic worker-imported script given url and settings
// object, passing along any custom perform the fetch steps provided.
// If this succeeds, let script be the result. Otherwise, rethrow the
// exception.
ScriptLoader script_loader(settings->context());
script_loader.Load(origin, request_urls);
// 5. For each url in the resulting URL records, run these substeps:
int content_lookup_index = 0;
for (int index = 0; index < resolved_urls.size(); ++index) {
int request_index = request_url_indexes[index];
std::string* script = nullptr;
if (request_index == -1) {
// The content at this index was received from the url lookup callback.
script = looked_up_content[content_lookup_index++];
} else {
const auto& error = script_loader.GetError(request_index);
if (error) {
LOG(WARNING) << "Script Loading Failed : " << *error;
web::DOMException::Raise(web::DOMException::kNetworkErr, *error,
exception_state);
break;
}
script = script_loader.GetContents(request_index).get();
}
// 5.2. Run the classic script script, with the rethrow errors argument
// set to true.
if (script) {
if (response_callback) {
script = response_callback.Run(resolved_urls[index], script);
}
bool succeeded = false;
std::string retval;
if (script) {
bool mute_errors = false;
const base::SourceLocation script_location(resolved_urls[index].spec(),
1, 1);
retval = settings->context()->script_runner()->Execute(
*script, script_location, mute_errors, &succeeded);
}
if (!succeeded) {
// TODO(): Handle script execution errors.
LOG(WARNING) << "Script Execution Failed : " << retval;
web::DOMException::Raise(web::DOMException::kSyntaxErr,
exception_state);
return;
}
}
}
// If an exception was thrown or if the script was prematurely aborted, then
// abort all these steps, letting the exception or aborting continue to be
// processed by the calling script.
}
} // namespace worker
} // namespace cobalt