Commit 88937c31 authored by Asami Doi's avatar Asami Doi Committed by Commit Bot

Worker: Implement the 'name' attribute to DedicatedWorkerGlobalScope

This CL allows a user to use 'name' property on DedicatedWorker
and access from worker thread by 'self.name'.

i.e
// index.html
const worker = new Worker("dedicated_worker.js", { name: "worker_name" });

// dedicated_worker.js
console.log(self.name); // worker_name

Intent to Ship: https://groups.google.com/a/chromium.org/d/msg/blink-dev/aZ804HggENE/WwWgDrz4DgAJ

Spec: https://html.spec.whatwg.org/multipage/workers.html#workeroptions
Spec: https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-dedicatedworkerglobalscope-interface

Bug: 721219
Change-Id: I36127df5ee249ecf6c13ae30aaac42621b3720b5
Reviewed-on: https://chromium-review.googlesource.com/1176887Reviewed-by: default avatarPhilip Jägenstedt <foolip@chromium.org>
Reviewed-by: default avatarHiroki Nakagawa <nhiroki@chromium.org>
Reviewed-by: default avatarMatt Falkenhagen <falken@chromium.org>
Commit-Queue: Asami Doi <asamidoi@google.com>
Cr-Commit-Position: refs/heads/master@{#585348}
parent 202353cc
...@@ -3579,7 +3579,6 @@ crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/ ...@@ -3579,7 +3579,6 @@ crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/
crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/size_50.html [ Failure ] crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/size_50.html [ Failure ]
crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/too_many_cues.html [ Failure ] crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/too_many_cues.html [ Failure ]
crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/too_many_cues_wrapped.html [ Failure ] crbug.com/626703 external/wpt/webvtt/rendering/cues-with-video/processing-model/too_many_cues_wrapped.html [ Failure ]
crbug.com/815116 external/wpt/workers/name-property.html [ Timeout Failure Pass ]
crbug.com/626703 external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ] crbug.com/626703 external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ]
crbug.com/626703 virtual/outofblink-cors/external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ] crbug.com/626703 virtual/outofblink-cors/external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ]
crbug.com/626703 virtual/outofblink-cors-ns/external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ] crbug.com/626703 virtual/outofblink-cors-ns/external/wpt/xhr/event-readystatechange-loaded.htm [ Failure Timeout ]
......
This is a testharness.js-based test. This is a testharness.js-based test.
Found 161 tests; 149 PASS, 12 FAIL, 0 TIMEOUT, 0 NOTRUN. Found 161 tests; 151 PASS, 10 FAIL, 0 TIMEOUT, 0 NOTRUN.
PASS EventTarget interface: existence and properties of interface object PASS EventTarget interface: existence and properties of interface object
PASS EventTarget interface object length PASS EventTarget interface object length
PASS EventTarget interface object name PASS EventTarget interface object name
...@@ -47,7 +47,7 @@ PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of ...@@ -47,7 +47,7 @@ PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of
PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true
PASS DedicatedWorkerGlobalScope interface: existence and properties of interface prototype object's "constructor" property PASS DedicatedWorkerGlobalScope interface: existence and properties of interface prototype object's "constructor" property
PASS DedicatedWorkerGlobalScope interface: existence and properties of interface prototype object's @@unscopables property PASS DedicatedWorkerGlobalScope interface: existence and properties of interface prototype object's @@unscopables property
FAIL DedicatedWorkerGlobalScope interface: attribute name assert_own_property: The global object must have a property "name" expected property "name" missing PASS DedicatedWorkerGlobalScope interface: attribute name
PASS DedicatedWorkerGlobalScope interface: operation postMessage(any, [object Object]) PASS DedicatedWorkerGlobalScope interface: operation postMessage(any, [object Object])
PASS DedicatedWorkerGlobalScope interface: operation close() PASS DedicatedWorkerGlobalScope interface: operation close()
PASS DedicatedWorkerGlobalScope interface: attribute onmessage PASS DedicatedWorkerGlobalScope interface: attribute onmessage
...@@ -60,7 +60,7 @@ PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of ...@@ -60,7 +60,7 @@ PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of
PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true PASS DedicatedWorkerGlobalScope interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true
PASS DedicatedWorkerGlobalScope must be primary interface of self PASS DedicatedWorkerGlobalScope must be primary interface of self
PASS Stringification of self PASS Stringification of self
FAIL DedicatedWorkerGlobalScope interface: self must inherit property "name" with the proper type assert_own_property: expected property "name" missing PASS DedicatedWorkerGlobalScope interface: self must inherit property "name" with the proper type
PASS DedicatedWorkerGlobalScope interface: self must inherit property "postMessage(any, [object Object])" with the proper type PASS DedicatedWorkerGlobalScope interface: self must inherit property "postMessage(any, [object Object])" with the proper type
PASS DedicatedWorkerGlobalScope interface: calling postMessage(any, [object Object]) on self with too few arguments must throw TypeError PASS DedicatedWorkerGlobalScope interface: calling postMessage(any, [object Object]) on self with too few arguments must throw TypeError
PASS DedicatedWorkerGlobalScope interface: self must inherit property "close()" with the proper type PASS DedicatedWorkerGlobalScope interface: self must inherit property "close()" with the proper type
......
This is a testharness.js-based test. This is a testharness.js-based test.
FAIL name property value for DedicatedWorkerGlobalScope assert_true: property exists on the global expected true got false PASS name property value for DedicatedWorkerGlobalScope
PASS name property is replaceable for DedicatedWorkerGlobalScope
PASS Declaring name as an accidental global must not cause a harness error for DedicatedWorkerGlobalScope
FAIL name property value for SharedWorkerGlobalScope assert_equals: expected "my name" but got "[object Object]" FAIL name property value for SharedWorkerGlobalScope assert_equals: expected "my name" but got "[object Object]"
FAIL name property is replaceable for SharedWorkerGlobalScope Cannot assign to read only property 'name' of object '#<SharedWorkerGlobalScope>'
FAIL name-as-accidental-global Uncaught TypeError: Cannot assign to read only property 'name' of object '#<SharedWorkerGlobalScope>'
Harness: the test ran to completion. Harness: the test ran to completion.
...@@ -2565,6 +2565,7 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -2565,6 +2565,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] [GLOBAL OBJECT] [Worker] [GLOBAL OBJECT]
[Worker] attribute console [Worker] attribute console
[Worker] attribute internals [Worker] attribute internals
[Worker] getter name
[Worker] getter onmessage [Worker] getter onmessage
[Worker] getter onmessageerror [Worker] getter onmessageerror
[Worker] method close [Worker] method close
...@@ -2574,6 +2575,7 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -2574,6 +2575,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method webkitRequestFileSystemSync [Worker] method webkitRequestFileSystemSync
[Worker] method webkitResolveLocalFileSystemSyncURL [Worker] method webkitResolveLocalFileSystemSyncURL
[Worker] method webkitResolveLocalFileSystemURL [Worker] method webkitResolveLocalFileSystemURL
[Worker] setter name
[Worker] setter onmessage [Worker] setter onmessage
[Worker] setter onmessageerror [Worker] setter onmessageerror
PASS successfullyParsed is true PASS successfullyParsed is true
......
...@@ -3552,6 +3552,7 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -3552,6 +3552,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] [GLOBAL OBJECT] [Worker] [GLOBAL OBJECT]
[Worker] attribute console [Worker] attribute console
[Worker] attribute internals [Worker] attribute internals
[Worker] getter name
[Worker] getter onmessage [Worker] getter onmessage
[Worker] getter onmessageerror [Worker] getter onmessageerror
[Worker] method close [Worker] method close
...@@ -3561,6 +3562,7 @@ Starting worker: resources/global-interface-listing-worker.js ...@@ -3561,6 +3562,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method webkitRequestFileSystemSync [Worker] method webkitRequestFileSystemSync
[Worker] method webkitResolveLocalFileSystemSyncURL [Worker] method webkitResolveLocalFileSystemSyncURL
[Worker] method webkitResolveLocalFileSystemURL [Worker] method webkitResolveLocalFileSystemURL
[Worker] setter name
[Worker] setter onmessage [Worker] setter onmessage
[Worker] setter onmessageerror [Worker] setter onmessageerror
PASS successfullyParsed is true PASS successfullyParsed is true
......
...@@ -74,14 +74,6 @@ DedicatedWorker* DedicatedWorker::Create(ExecutionContext* context, ...@@ -74,14 +74,6 @@ DedicatedWorker* DedicatedWorker::Create(ExecutionContext* context,
return nullptr; return nullptr;
} }
if (options.name() != "") {
// TODO(asamidoi): Implement 'name' option (https://crbug.com/721219)
context->AddConsoleMessage(ConsoleMessage::Create(
kJSMessageSource, kInfoMessageLevel,
"'name' param in WorkerOptions is not implemented yet. See "
"https://crbug.com/721219 for details."));
}
// TODO(nhiroki): Remove this flag check once module loading for // TODO(nhiroki): Remove this flag check once module loading for
// DedicatedWorker is enabled by default (https://crbug.com/680046). // DedicatedWorker is enabled by default (https://crbug.com/680046).
if (options.type() == "module" && if (options.type() == "module" &&
...@@ -256,6 +248,10 @@ bool DedicatedWorker::HasPendingActivity() const { ...@@ -256,6 +248,10 @@ bool DedicatedWorker::HasPendingActivity() const {
return context_proxy_->HasPendingActivity() || classic_script_loader_; return context_proxy_->HasPendingActivity() || classic_script_loader_;
} }
const String DedicatedWorker::Name() const {
return options_.name();
}
WorkerClients* DedicatedWorker::CreateWorkerClients() { WorkerClients* DedicatedWorker::CreateWorkerClients() {
WorkerClients* worker_clients = WorkerClients::Create(); WorkerClients* worker_clients = WorkerClients::Create();
CoreInitializer::GetInstance().ProvideLocalFileSystemToWorker( CoreInitializer::GetInstance().ProvideLocalFileSystemToWorker(
......
...@@ -73,6 +73,9 @@ class CORE_EXPORT DedicatedWorker final ...@@ -73,6 +73,9 @@ class CORE_EXPORT DedicatedWorker final
// (via AbstractWorker -> EventTargetWithInlineData -> EventTarget). // (via AbstractWorker -> EventTargetWithInlineData -> EventTarget).
bool HasPendingActivity() const final; bool HasPendingActivity() const final;
// Returns the name specified by WorkerOptions.
const String Name() const;
DEFINE_ATTRIBUTE_EVENT_LISTENER(message); DEFINE_ATTRIBUTE_EVENT_LISTENER(message);
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
......
...@@ -52,10 +52,12 @@ ...@@ -52,10 +52,12 @@
namespace blink { namespace blink {
DedicatedWorkerGlobalScope::DedicatedWorkerGlobalScope( DedicatedWorkerGlobalScope::DedicatedWorkerGlobalScope(
const String& name,
std::unique_ptr<GlobalScopeCreationParams> creation_params, std::unique_ptr<GlobalScopeCreationParams> creation_params,
DedicatedWorkerThread* thread, DedicatedWorkerThread* thread,
base::TimeTicks time_origin) base::TimeTicks time_origin)
: WorkerGlobalScope(std::move(creation_params), thread, time_origin) {} : WorkerGlobalScope(std::move(creation_params), thread, time_origin),
name_(name) {}
DedicatedWorkerGlobalScope::~DedicatedWorkerGlobalScope() = default; DedicatedWorkerGlobalScope::~DedicatedWorkerGlobalScope() = default;
...@@ -84,6 +86,10 @@ void DedicatedWorkerGlobalScope::ImportModuleScript( ...@@ -84,6 +86,10 @@ void DedicatedWorkerGlobalScope::ImportModuleScript(
new WorkerModuleTreeClient(modulator)); new WorkerModuleTreeClient(modulator));
} }
const String DedicatedWorkerGlobalScope::name() const {
return name_;
}
void DedicatedWorkerGlobalScope::postMessage(ScriptState* script_state, void DedicatedWorkerGlobalScope::postMessage(ScriptState* script_state,
const ScriptValue& message, const ScriptValue& message,
Vector<ScriptValue>& transfer, Vector<ScriptValue>& transfer,
......
...@@ -49,7 +49,8 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope { ...@@ -49,7 +49,8 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope {
DEFINE_WRAPPERTYPEINFO(); DEFINE_WRAPPERTYPEINFO();
public: public:
DedicatedWorkerGlobalScope(std::unique_ptr<GlobalScopeCreationParams>, DedicatedWorkerGlobalScope(const String& name,
std::unique_ptr<GlobalScopeCreationParams>,
DedicatedWorkerThread*, DedicatedWorkerThread*,
base::TimeTicks time_origin); base::TimeTicks time_origin);
~DedicatedWorkerGlobalScope() override; ~DedicatedWorkerGlobalScope() override;
...@@ -65,6 +66,8 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope { ...@@ -65,6 +66,8 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope {
FetchClientSettingsObjectSnapshot* outside_settings_object, FetchClientSettingsObjectSnapshot* outside_settings_object,
network::mojom::FetchCredentialsMode) override; network::mojom::FetchCredentialsMode) override;
const String name() const;
void postMessage(ScriptState*, void postMessage(ScriptState*,
const ScriptValue& message, const ScriptValue& message,
Vector<ScriptValue>& transfer, Vector<ScriptValue>& transfer,
...@@ -80,6 +83,9 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope { ...@@ -80,6 +83,9 @@ class CORE_EXPORT DedicatedWorkerGlobalScope final : public WorkerGlobalScope {
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
DedicatedWorkerObjectProxy& WorkerObjectProxy() const; DedicatedWorkerObjectProxy& WorkerObjectProxy() const;
private:
const String name_;
}; };
DEFINE_TYPE_CASTS(DedicatedWorkerGlobalScope, DEFINE_TYPE_CASTS(DedicatedWorkerGlobalScope,
......
...@@ -34,6 +34,8 @@ ...@@ -34,6 +34,8 @@
Global=(Worker,DedicatedWorker), Global=(Worker,DedicatedWorker),
Exposed=DedicatedWorker Exposed=DedicatedWorker
] interface DedicatedWorkerGlobalScope : WorkerGlobalScope { ] interface DedicatedWorkerGlobalScope : WorkerGlobalScope {
[Replaceable] readonly attribute DOMString name;
[CallWith=ScriptState, RaisesException] void postMessage(any message, optional sequence<object> transfer = []); [CallWith=ScriptState, RaisesException] void postMessage(any message, optional sequence<object> transfer = []);
[RuntimeEnabled=PostMessageOptions, CallWith=ScriptState, RaisesException] void postMessage(any message, PostMessageOptions options); [RuntimeEnabled=PostMessageOptions, CallWith=ScriptState, RaisesException] void postMessage(any message, PostMessageOptions options);
......
...@@ -193,8 +193,8 @@ DedicatedWorkerMessagingProxy::CreateBackingThreadStartupData( ...@@ -193,8 +193,8 @@ DedicatedWorkerMessagingProxy::CreateBackingThreadStartupData(
std::unique_ptr<WorkerThread> std::unique_ptr<WorkerThread>
DedicatedWorkerMessagingProxy::CreateWorkerThread() { DedicatedWorkerMessagingProxy::CreateWorkerThread() {
return DedicatedWorkerThread::Create(GetExecutionContext(), return DedicatedWorkerThread::Create(
WorkerObjectProxy()); worker_object_->Name(), GetExecutionContext(), WorkerObjectProxy());
} }
} // namespace blink } // namespace blink
...@@ -30,7 +30,8 @@ namespace blink { ...@@ -30,7 +30,8 @@ namespace blink {
class DedicatedWorkerThreadForTest final : public DedicatedWorkerThread { class DedicatedWorkerThreadForTest final : public DedicatedWorkerThread {
public: public:
DedicatedWorkerThreadForTest(DedicatedWorkerObjectProxy& worker_object_proxy) DedicatedWorkerThreadForTest(DedicatedWorkerObjectProxy& worker_object_proxy)
: DedicatedWorkerThread(nullptr /* parent_execution_context*/, : DedicatedWorkerThread("fake worker name",
nullptr /* parent_execution_context*/,
worker_object_proxy) { worker_object_proxy) {
worker_backing_thread_ = WorkerBackingThread::Create( worker_backing_thread_ = WorkerBackingThread::Create(
WebThreadCreationParams(WebThreadType::kTestThread)); WebThreadCreationParams(WebThreadType::kTestThread));
...@@ -39,7 +40,7 @@ class DedicatedWorkerThreadForTest final : public DedicatedWorkerThread { ...@@ -39,7 +40,7 @@ class DedicatedWorkerThreadForTest final : public DedicatedWorkerThread {
WorkerOrWorkletGlobalScope* CreateWorkerGlobalScope( WorkerOrWorkletGlobalScope* CreateWorkerGlobalScope(
std::unique_ptr<GlobalScopeCreationParams> creation_params) override { std::unique_ptr<GlobalScopeCreationParams> creation_params) override {
auto* global_scope = new DedicatedWorkerGlobalScope( auto* global_scope = new DedicatedWorkerGlobalScope(
std::move(creation_params), this, time_origin_); "fake worker name", std::move(creation_params), this, time_origin_);
// Initializing a global scope with a dummy creation params may emit warning // Initializing a global scope with a dummy creation params may emit warning
// messages (e.g., invalid CSP directives). Clear them here for tests that // messages (e.g., invalid CSP directives). Clear them here for tests that
// check console messages (i.e., UseCounter tests). // check console messages (i.e., UseCounter tests).
......
...@@ -46,16 +46,19 @@ ...@@ -46,16 +46,19 @@
namespace blink { namespace blink {
std::unique_ptr<DedicatedWorkerThread> DedicatedWorkerThread::Create( std::unique_ptr<DedicatedWorkerThread> DedicatedWorkerThread::Create(
const String& name,
ExecutionContext* parent_execution_context, ExecutionContext* parent_execution_context,
DedicatedWorkerObjectProxy& worker_object_proxy) { DedicatedWorkerObjectProxy& worker_object_proxy) {
return base::WrapUnique( return base::WrapUnique(new DedicatedWorkerThread(
new DedicatedWorkerThread(parent_execution_context, worker_object_proxy)); name, parent_execution_context, worker_object_proxy));
} }
DedicatedWorkerThread::DedicatedWorkerThread( DedicatedWorkerThread::DedicatedWorkerThread(
const String& name,
ExecutionContext* parent_execution_context, ExecutionContext* parent_execution_context,
DedicatedWorkerObjectProxy& worker_object_proxy) DedicatedWorkerObjectProxy& worker_object_proxy)
: WorkerThread(worker_object_proxy), : WorkerThread(worker_object_proxy),
name_(name.IsolatedCopy()),
worker_object_proxy_(worker_object_proxy) { worker_object_proxy_(worker_object_proxy) {
FrameOrWorkerScheduler* scheduler = FrameOrWorkerScheduler* scheduler =
parent_execution_context ? parent_execution_context->GetScheduler() parent_execution_context ? parent_execution_context->GetScheduler()
...@@ -73,7 +76,7 @@ void DedicatedWorkerThread::ClearWorkerBackingThread() { ...@@ -73,7 +76,7 @@ void DedicatedWorkerThread::ClearWorkerBackingThread() {
WorkerOrWorkletGlobalScope* DedicatedWorkerThread::CreateWorkerGlobalScope( WorkerOrWorkletGlobalScope* DedicatedWorkerThread::CreateWorkerGlobalScope(
std::unique_ptr<GlobalScopeCreationParams> creation_params) { std::unique_ptr<GlobalScopeCreationParams> creation_params) {
return new DedicatedWorkerGlobalScope(std::move(creation_params), this, return new DedicatedWorkerGlobalScope(name_, std::move(creation_params), this,
time_origin_); time_origin_);
} }
......
...@@ -41,6 +41,7 @@ struct GlobalScopeCreationParams; ...@@ -41,6 +41,7 @@ struct GlobalScopeCreationParams;
class CORE_EXPORT DedicatedWorkerThread : public WorkerThread { class CORE_EXPORT DedicatedWorkerThread : public WorkerThread {
public: public:
static std::unique_ptr<DedicatedWorkerThread> Create( static std::unique_ptr<DedicatedWorkerThread> Create(
const String& name,
ExecutionContext* parent_execution_context, ExecutionContext* parent_execution_context,
DedicatedWorkerObjectProxy&); DedicatedWorkerObjectProxy&);
~DedicatedWorkerThread() override; ~DedicatedWorkerThread() override;
...@@ -56,7 +57,8 @@ class CORE_EXPORT DedicatedWorkerThread : public WorkerThread { ...@@ -56,7 +57,8 @@ class CORE_EXPORT DedicatedWorkerThread : public WorkerThread {
private: private:
friend class DedicatedWorkerThreadForTest; friend class DedicatedWorkerThreadForTest;
DedicatedWorkerThread(ExecutionContext* parent_execution_context, DedicatedWorkerThread(const String& name,
ExecutionContext* parent_execution_context,
DedicatedWorkerObjectProxy&); DedicatedWorkerObjectProxy&);
WorkerOrWorkletGlobalScope* CreateWorkerGlobalScope( WorkerOrWorkletGlobalScope* CreateWorkerGlobalScope(
std::unique_ptr<GlobalScopeCreationParams>) override; std::unique_ptr<GlobalScopeCreationParams>) override;
...@@ -66,6 +68,7 @@ class CORE_EXPORT DedicatedWorkerThread : public WorkerThread { ...@@ -66,6 +68,7 @@ class CORE_EXPORT DedicatedWorkerThread : public WorkerThread {
} }
std::unique_ptr<WorkerBackingThread> worker_backing_thread_; std::unique_ptr<WorkerBackingThread> worker_backing_thread_;
const String name_;
DedicatedWorkerObjectProxy& worker_object_proxy_; DedicatedWorkerObjectProxy& worker_object_proxy_;
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment