blob: 3414b2ebac6d88007781e074ff15ef0a1ad0bb10 [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.h"
#include <string>
#include <utility>
#include "base/location.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/task_runner.h"
#include "base/threading/thread.h"
#include "cobalt/browser/user_agent_platform_info.h"
#include "cobalt/script/environment_settings.h"
#include "cobalt/web/error_event.h"
#include "cobalt/web/message_port.h"
#include "cobalt/worker/dedicated_worker_global_scope.h"
#include "cobalt/worker/worker_global_scope.h"
#include "cobalt/worker/worker_options.h"
#include "cobalt/worker/worker_settings.h"
namespace cobalt {
namespace worker {
namespace {
bool PermitAnyURL(const GURL&, bool) { return true; }
} // namespace
Worker::Worker(const Options& options) : options_(options) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 1. Let is shared be true if worker is a SharedWorker object, and false
// otherwise.
is_shared_ = options.is_shared;
// 2. Let owner be the relevant owner to add given outside settings.
// 3. Let parent worker global scope be null.
// 4. If owner is a WorkerGlobalScope object (i.e., we are creating a nested
// dedicated worker), then set parent worker global scope to owner.
// 5. Let unsafeWorkerCreationTime be the unsafe shared current time.
// 6. Let agent be the result of obtaining a dedicated/shared worker agent
// given outside settings and is shared. Run the rest of these steps in
// that agent.
web_agent_.reset(new web::Agent(
options.web_options,
base::Bind(&Worker::Initialize, base::Unretained(this)), this));
}
void Worker::WillDestroyCurrentMessageLoop() {
#if defined(ENABLE_DEBUGGER)
debug_module_.reset();
#endif // ENABLE_DEBUGGER
// Destroy members that were constructed in the worker thread.
loader_.reset();
worker_global_scope_ = nullptr;
message_port_ = nullptr;
content_.reset();
error_.reset();
}
Worker::~Worker() { Abort(); }
void Worker::Initialize(web::Context* context) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 7. Let realm execution context be the result of creating a new
// JavaScript realm given agent and the following customizations:
web_context_ = context;
// . For the global object, if is shared is true, create a new
// SharedWorkerGlobalScope object. Otherwise, create a new
// DedicatedWorkerGlobalScope object.
// TODO: Actual type here should depend on derived class (e.g. dedicated,
// shared, service)
web_context_->setup_environment_settings(
new WorkerSettings(options_.outside_port));
web_context_->environment_settings()->set_base_url(options_.url);
// 8. Let worker global scope be the global object of realm execution
// context's Realm component.
scoped_refptr<DedicatedWorkerGlobalScope> dedicated_worker_global_scope =
new DedicatedWorkerGlobalScope(web_context_->environment_settings(),
false);
worker_global_scope_ = dedicated_worker_global_scope;
// 9. Set up a worker environment settings object with realm execution
// context, outside settings, and unsafeWorkerCreationTime, and let
// inside settings be the result.
web_context_->global_environment()->CreateGlobalObject(
dedicated_worker_global_scope, web_context_->environment_settings());
DCHECK(!web_context_->GetWindowOrWorkerGlobalScope()->IsWindow());
DCHECK(web_context_->GetWindowOrWorkerGlobalScope()->IsDedicatedWorker());
DCHECK(!web_context_->GetWindowOrWorkerGlobalScope()->IsServiceWorker());
DCHECK(web_context_->GetWindowOrWorkerGlobalScope()->GetWrappableType() ==
base::GetTypeId<DedicatedWorkerGlobalScope>());
DCHECK_EQ(dedicated_worker_global_scope,
base::polymorphic_downcast<DedicatedWorkerGlobalScope*>(
web_context_->GetWindowOrWorkerGlobalScope()));
#if defined(ENABLE_DEBUGGER)
debug_module_.reset(new debug::backend::DebugModule(
nullptr /* debugger_hooks */, web_context_->global_environment(),
nullptr /* render_overlay */, nullptr /* resource_provider */,
nullptr /* window */, nullptr /* debugger_state */));
#endif // ENABLE_DEBUGGER
// 10. Set worker global scope's name to the value of options's name member.
dedicated_worker_global_scope->set_name(options_.web_options.name);
// 11. Append owner to worker global scope's owner set.
// 12. If is shared is true, then:
// 1. Set worker global scope's constructor origin to outside settings's
// origin.
// 2. Set worker global scope's constructor url to url.
// 3. Set worker global scope's type to the value of options's type
// member.
// 4. Set worker global scope's credentials to the value of options's
// credentials member.
// 13. Let destination be "sharedworker" if is shared is true, and
// "worker" otherwise.
// 14. Obtain script
Obtain();
}
// Fetch and Run classic script
void Worker::Obtain() {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 14. Obtain script by switching on the value of options's type member:
// - "classic"
// Fetch a classic worker script given url, outside settings,
// destination, and inside settings.
// - "module"
// Fetch a module worker script graph given url, outside settings,
// destination, the value of the credentials member of options, and
// inside settings.
// In both cases, to perform the fetch given request, perform the
// following steps if the is top-level flag is set:
// 1. Set request's reserved client to inside settings.
// 2. Fetch request, and asynchronously wait to run the remaining steps as
// part of fetch's process response for the response response.
const GURL& url = web_context_->environment_settings()->base_url();
loader::Origin origin = loader::Origin(url.GetOrigin());
// Todo: implement csp check (b/225037465)
csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
loader_ = web_context_->script_loader_factory()->CreateScriptLoader(
url, origin, csp_callback,
base::Bind(&Worker::OnContentProduced, base::Unretained(this)),
base::Bind(&Worker::OnLoadingComplete, base::Unretained(this)));
}
void Worker::OnContentProduced(const loader::Origin& last_url_origin,
std::unique_ptr<std::string> content) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
DCHECK(content);
// 14.3. "Set worker global scope's url to response's url."
worker_global_scope_->set_url(
web_context_->environment_settings()->base_url());
// 14.4 - 14.10 initialize worker global scope
worker_global_scope_->Initialize();
// 14.11. Asynchronously complete the perform the fetch steps with response.
content_ = std::move(content);
}
void Worker::OnLoadingComplete(const base::Optional<std::string>& error) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
error_ = error;
// If the algorithm asynchronously completes with null or with a script
// whose error to rethrow is non-null, then:
if (error_ || !content_) {
// 1. Queue a global task on the DOM manipulation task source given
// worker's relevant global object to fire an event named error at
// worker.
options_.outside_settings->context()
->message_loop()
->task_runner()
->PostTask(FROM_HERE,
base::BindOnce(
[](web::WindowOrWorkerGlobalScope* global_scope) {
global_scope->DispatchEvent(new web::ErrorEvent());
},
base::Unretained(options_.outside_settings->context()
->GetWindowOrWorkerGlobalScope())));
if (error_) {
LOG(WARNING) << "Script loading failed : " << *error;
} else {
LOG(WARNING) << "Script loading failed : no content received.";
}
// 2. Run the environment discarding steps for inside settings.
// 3. Return.
return;
}
OnReadyToExecute();
}
void Worker::OnReadyToExecute() {
DCHECK(content_);
Execute(*content_,
base::SourceLocation(
web_context_->environment_settings()->base_url().spec(), 1, 1));
content_.reset();
}
void Worker::Execute(const std::string& content,
const base::SourceLocation& script_location) {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 15. Associate worker with worker global scope.
// Done at step 8.
// 16. Let inside port be a new MessagePort object in inside settings's Realm.
// 17. Associate inside port with worker global scope.
message_port_ = new web::MessagePort(worker_global_scope_);
// 18. Entangle outside port and inside port.
// TODO(b/226640425): Implement this when Message Ports can be entangled.
// 19. Create a new WorkerLocation object and associate it with worker global
// scope.
// 20. Closing orphan workers: Start monitoring the worker such that no sooner
// than it stops being a protected worker, and no later than it stops
// being a permissible worker, worker global scope's closing flag is set
// to true.
// 21. Suspending workers: Start monitoring the worker, such that whenever
// worker global scope's closing flag is false and the worker is a
// suspendable worker, the user agent suspends execution of script in that
// worker until such time as either the closing flag switches to true or
// the worker stops being a suspendable worker.
// 22. Set inside settings's execution ready flag.
execution_ready_.Signal();
// 23. If script is a classic script, then run the classic script script.
// Otherwise, it is a module script; run the module script script.
bool mute_errors = false;
bool succeeded = false;
std::string retval = web_context_->script_runner()->Execute(
content, script_location, mute_errors, &succeeded);
if (!succeeded) {
LOG(WARNING) << "Script execution failed : " << retval;
options_.outside_settings->context()
->GetWindowOrWorkerGlobalScope()
->DispatchEvent(new web::Event(base::Tokens::error()));
}
// 24. Enable outside port's port message queue.
// TODO(b/226640425): Implement this when Message Ports can be entangled.
// 25. If is shared is false, enable the port message queue of the worker's
// implicit port.
// 26. If is shared is true, then queue a global task on DOM manipulation task
// source given worker global scope to fire an event named connect at
// worker global scope, using MessageEvent, with the data attribute
// initialized to the empty string, the ports attribute initialized to a
// new frozen array containing inside port, and the source attribute
// initialized to inside port.
// 27. Enable the client message queue of the ServiceWorkerContainer object
// whose associated service worker client is worker global scope's
// relevant settings object.
// TODO(b/226640425): Implement this when Message Ports can be entangled.
// 28. Event loop: Run the responsible event loop specified by inside settings
// until it is destroyed.
}
void Worker::Abort() {
// Algorithm for 'run a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#run-a-worker
// 29. Clear the worker global scope's map of active timers.
if (worker_global_scope_) {
DCHECK(message_loop());
message_loop()->task_runner()->PostBlockingTask(
FROM_HERE, base::Bind(
[](WorkerGlobalScope* worker_global_scope) {
worker_global_scope->DestroyTimers();
},
base::Unretained(worker_global_scope_.get())));
}
// 30. Disentangle all the ports in the list of the worker's ports.
// 31. Empty worker global scope's owner set.
if (web_agent_) {
DCHECK(message_loop());
web_agent_->WaitUntilDone();
web_agent_.reset();
web_context_ = nullptr;
}
}
// Algorithm for 'Terminate a worker'
// https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#terminate-a-worker
void Worker::Terminate() {
// 1. Set the worker's WorkerGlobalScope object's closing flag to true.
if (worker_global_scope_) {
worker_global_scope_->set_closing_flag(true);
}
// 2. If there are any tasks queued in the WorkerGlobalScope object's relevant
// agent's event loop's task queues, discard them without processing them.
// 3. Abort the script currently running in the worker.
Abort();
// 4. If the worker's WorkerGlobalScope object is actually a
// DedicatedWorkerGlobalScope object (i.e. the worker is a dedicated
// worker), then empty the port message queue of the port that the worker's
// implicit port is entangled with.
// TODO(b/226640425): Implement this when Message Ports can be entangled.
}
// void postMessage(any message, object transfer);
// -> void PostMessage(const script::ValueHandleHolder& message,
// script::Sequence<script::ValueHandle*> transfer) {}
void Worker::PostMessage(const std::string& message) {
DCHECK(message_loop());
if (base::MessageLoop::current() != message_loop()) {
// Block until the worker thread is ready to execute code to handle the
// event.
execution_ready_.Wait();
message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&Worker::PostMessage, base::Unretained(this), message));
return;
} else {
DCHECK(execution_ready_.IsSignaled());
DCHECK(message_port());
if (message_port()) message_port()->PostMessage(message);
}
}
} // namespace worker
} // namespace cobalt