fix: crash in utilityProcess when generating code from strings (#38014)
This commit is contained in:
parent
f12e12b341
commit
0240f6664e
14 changed files with 72 additions and 48 deletions
|
@ -40,6 +40,7 @@
|
||||||
#include "shell/common/logging.h"
|
#include "shell/common/logging.h"
|
||||||
#include "shell/common/options_switches.h"
|
#include "shell/common/options_switches.h"
|
||||||
#include "shell/common/platform_util.h"
|
#include "shell/common/platform_util.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
#include "shell/common/thread_restrictions.h"
|
#include "shell/common/thread_restrictions.h"
|
||||||
#include "shell/renderer/electron_renderer_client.h"
|
#include "shell/renderer/electron_renderer_client.h"
|
||||||
#include "shell/renderer/electron_sandboxed_renderer_client.h"
|
#include "shell/renderer/electron_sandboxed_renderer_client.h"
|
||||||
|
@ -83,11 +84,6 @@ constexpr base::StringPiece kElectronDisableSandbox("ELECTRON_DISABLE_SANDBOX");
|
||||||
constexpr base::StringPiece kElectronEnableStackDumping(
|
constexpr base::StringPiece kElectronEnableStackDumping(
|
||||||
"ELECTRON_ENABLE_STACK_DUMPING");
|
"ELECTRON_ENABLE_STACK_DUMPING");
|
||||||
|
|
||||||
bool IsBrowserProcess(base::CommandLine* cmd) {
|
|
||||||
std::string process_type = cmd->GetSwitchValueASCII(::switches::kProcessType);
|
|
||||||
return process_type.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if this subprocess type needs the ResourceBundle initialized
|
// Returns true if this subprocess type needs the ResourceBundle initialized
|
||||||
// and resources loaded.
|
// and resources loaded.
|
||||||
bool SubprocessNeedsResourceBundle(const std::string& process_type) {
|
bool SubprocessNeedsResourceBundle(const std::string& process_type) {
|
||||||
|
@ -250,14 +246,12 @@ absl::optional<int> ElectronMainDelegate::BasicStartupComplete() {
|
||||||
|
|
||||||
// On Windows the terminal returns immediately, so we add a new line to
|
// On Windows the terminal returns immediately, so we add a new line to
|
||||||
// prevent output in the same line as the prompt.
|
// prevent output in the same line as the prompt.
|
||||||
if (IsBrowserProcess(command_line))
|
if (IsBrowserProcess())
|
||||||
std::wcout << std::endl;
|
std::wcout << std::endl;
|
||||||
#endif // !BUILDFLAG(IS_WIN)
|
#endif // !BUILDFLAG(IS_WIN)
|
||||||
|
|
||||||
auto env = base::Environment::Create();
|
auto env = base::Environment::Create();
|
||||||
|
|
||||||
gin_helper::Locker::SetIsBrowserProcess(IsBrowserProcess(command_line));
|
|
||||||
|
|
||||||
// Enable convenient stack printing. This is enabled by default in
|
// Enable convenient stack printing. This is enabled by default in
|
||||||
// non-official builds.
|
// non-official builds.
|
||||||
if (env->HasVar(kElectronEnableStackDumping))
|
if (env->HasVar(kElectronEnableStackDumping))
|
||||||
|
@ -290,7 +284,7 @@ absl::optional<int> ElectronMainDelegate::BasicStartupComplete() {
|
||||||
// bugs, but no use in Electron.
|
// bugs, but no use in Electron.
|
||||||
base::win::DisableHandleVerifier();
|
base::win::DisableHandleVerifier();
|
||||||
|
|
||||||
if (IsBrowserProcess(command_line))
|
if (IsBrowserProcess())
|
||||||
base::win::PinUser32();
|
base::win::PinUser32();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -386,7 +380,7 @@ void ElectronMainDelegate::PreSandboxStartup() {
|
||||||
crash_keys::SetPlatformCrashKey();
|
crash_keys::SetPlatformCrashKey();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (IsBrowserProcess(command_line)) {
|
if (IsBrowserProcess()) {
|
||||||
// Only append arguments for browser process.
|
// Only append arguments for browser process.
|
||||||
|
|
||||||
// Allow file:// URIs to read other file:// URIs by default.
|
// Allow file:// URIs to read other file:// URIs by default.
|
||||||
|
|
|
@ -22,11 +22,11 @@
|
||||||
#include "shell/common/application_info.h"
|
#include "shell/common/application_info.h"
|
||||||
#include "shell/common/gin_converters/file_path_converter.h"
|
#include "shell/common/gin_converters/file_path_converter.h"
|
||||||
#include "shell/common/gin_helper/dictionary.h"
|
#include "shell/common/gin_helper/dictionary.h"
|
||||||
#include "shell/common/gin_helper/locker.h"
|
|
||||||
#include "shell/common/gin_helper/microtasks_scope.h"
|
#include "shell/common/gin_helper/microtasks_scope.h"
|
||||||
#include "shell/common/gin_helper/promise.h"
|
#include "shell/common/gin_helper/promise.h"
|
||||||
#include "shell/common/heap_snapshot.h"
|
#include "shell/common/heap_snapshot.h"
|
||||||
#include "shell/common/node_includes.h"
|
#include "shell/common/node_includes.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
#include "shell/common/thread_restrictions.h"
|
#include "shell/common/thread_restrictions.h"
|
||||||
#include "third_party/blink/renderer/platform/heap/process_heap.h" // nogncheck
|
#include "third_party/blink/renderer/platform/heap/process_heap.h" // nogncheck
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ void ElectronBindings::BindProcess(v8::Isolate* isolate,
|
||||||
process->SetMethod("getCreationTime", &GetCreationTime);
|
process->SetMethod("getCreationTime", &GetCreationTime);
|
||||||
process->SetMethod("getHeapStatistics", &GetHeapStatistics);
|
process->SetMethod("getHeapStatistics", &GetHeapStatistics);
|
||||||
process->SetMethod("getBlinkMemoryInfo", &GetBlinkMemoryInfo);
|
process->SetMethod("getBlinkMemoryInfo", &GetBlinkMemoryInfo);
|
||||||
if (gin_helper::Locker::IsBrowserProcess()) {
|
if (electron::IsBrowserProcess()) {
|
||||||
process->SetMethod("getProcessMemoryInfo", &GetProcessMemoryInfo);
|
process->SetMethod("getProcessMemoryInfo", &GetProcessMemoryInfo);
|
||||||
}
|
}
|
||||||
process->SetMethod("getSystemMemoryInfo", &GetSystemMemoryInfo);
|
process->SetMethod("getSystemMemoryInfo", &GetSystemMemoryInfo);
|
||||||
|
@ -209,7 +209,7 @@ v8::Local<v8::Value> ElectronBindings::GetSystemMemoryInfo(
|
||||||
// static
|
// static
|
||||||
v8::Local<v8::Promise> ElectronBindings::GetProcessMemoryInfo(
|
v8::Local<v8::Promise> ElectronBindings::GetProcessMemoryInfo(
|
||||||
v8::Isolate* isolate) {
|
v8::Isolate* isolate) {
|
||||||
CHECK(gin_helper::Locker::IsBrowserProcess());
|
CHECK(electron::IsBrowserProcess());
|
||||||
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
|
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
|
||||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "base/cxx17_backports.h"
|
#include "base/cxx17_backports.h"
|
||||||
#include "content/public/browser/browser_thread.h"
|
#include "content/public/browser/browser_thread.h"
|
||||||
#include "gin/dictionary.h"
|
#include "gin/dictionary.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
namespace gin_helper {
|
namespace gin_helper {
|
||||||
|
|
||||||
|
@ -70,7 +71,7 @@ void CallTranslater(v8::Local<v8::External> external,
|
||||||
struct DeleteOnUIThread {
|
struct DeleteOnUIThread {
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static void Destruct(const T* x) {
|
static void Destruct(const T* x) {
|
||||||
if (gin_helper::Locker::IsBrowserProcess() &&
|
if (electron::IsBrowserProcess() &&
|
||||||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
||||||
content::BrowserThread::DeleteSoon(content::BrowserThread::UI, FROM_HERE,
|
content::BrowserThread::DeleteSoon(content::BrowserThread::UI, FROM_HERE,
|
||||||
x);
|
x);
|
||||||
|
|
|
@ -4,19 +4,15 @@
|
||||||
|
|
||||||
#include "shell/common/gin_helper/locker.h"
|
#include "shell/common/gin_helper/locker.h"
|
||||||
|
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
namespace gin_helper {
|
namespace gin_helper {
|
||||||
|
|
||||||
Locker::Locker(v8::Isolate* isolate) {
|
Locker::Locker(v8::Isolate* isolate) {
|
||||||
if (IsBrowserProcess())
|
if (electron::IsBrowserProcess())
|
||||||
locker_ = std::make_unique<v8::Locker>(isolate);
|
locker_ = std::make_unique<v8::Locker>(isolate);
|
||||||
}
|
}
|
||||||
|
|
||||||
Locker::~Locker() = default;
|
Locker::~Locker() = default;
|
||||||
|
|
||||||
void Locker::SetIsBrowserProcess(bool is_browser_process) {
|
|
||||||
g_is_browser_process = is_browser_process;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Locker::g_is_browser_process = false;
|
|
||||||
|
|
||||||
} // namespace gin_helper
|
} // namespace gin_helper
|
||||||
|
|
|
@ -21,12 +21,6 @@ class Locker {
|
||||||
Locker(const Locker&) = delete;
|
Locker(const Locker&) = delete;
|
||||||
Locker& operator=(const Locker&) = delete;
|
Locker& operator=(const Locker&) = delete;
|
||||||
|
|
||||||
// Returns whether current process is browser process, currently we detect it
|
|
||||||
// by checking whether current has used V8 Lock, but it might be a bad idea.
|
|
||||||
static inline bool IsBrowserProcess() { return g_is_browser_process; }
|
|
||||||
|
|
||||||
static void SetIsBrowserProcess(bool is_browser_process);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void* operator new(size_t size);
|
void* operator new(size_t size);
|
||||||
void operator delete(void*, size_t);
|
void operator delete(void*, size_t);
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
#include "shell/common/gin_helper/microtasks_scope.h"
|
#include "shell/common/gin_helper/microtasks_scope.h"
|
||||||
|
|
||||||
#include "shell/common/gin_helper/locker.h"
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
namespace gin_helper {
|
namespace gin_helper {
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ MicrotasksScope::MicrotasksScope(v8::Isolate* isolate,
|
||||||
v8::MicrotaskQueue* microtask_queue,
|
v8::MicrotaskQueue* microtask_queue,
|
||||||
bool ignore_browser_checkpoint,
|
bool ignore_browser_checkpoint,
|
||||||
v8::MicrotasksScope::Type scope_type) {
|
v8::MicrotasksScope::Type scope_type) {
|
||||||
if (Locker::IsBrowserProcess()) {
|
if (electron::IsBrowserProcess()) {
|
||||||
if (!ignore_browser_checkpoint)
|
if (!ignore_browser_checkpoint)
|
||||||
v8::MicrotasksScope::PerformCheckpoint(isolate);
|
v8::MicrotasksScope::PerformCheckpoint(isolate);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -68,7 +68,7 @@ v8::Local<v8::Promise::Resolver> PromiseBase::GetInner() const {
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void Promise<void>::ResolvePromise(Promise<void> promise) {
|
void Promise<void>::ResolvePromise(Promise<void> promise) {
|
||||||
if (gin_helper::Locker::IsBrowserProcess() &&
|
if (electron::IsBrowserProcess() &&
|
||||||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
||||||
content::GetUIThreadTaskRunner({})->PostTask(
|
content::GetUIThreadTaskRunner({})->PostTask(
|
||||||
FROM_HERE,
|
FROM_HERE,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#include "shell/common/gin_converters/std_converter.h"
|
#include "shell/common/gin_converters/std_converter.h"
|
||||||
#include "shell/common/gin_helper/locker.h"
|
#include "shell/common/gin_helper/locker.h"
|
||||||
#include "shell/common/gin_helper/microtasks_scope.h"
|
#include "shell/common/gin_helper/microtasks_scope.h"
|
||||||
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
namespace gin_helper {
|
namespace gin_helper {
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ class PromiseBase {
|
||||||
// Note: The parameter type is PromiseBase&& so it can take the instances of
|
// Note: The parameter type is PromiseBase&& so it can take the instances of
|
||||||
// Promise<T> type.
|
// Promise<T> type.
|
||||||
static void RejectPromise(PromiseBase&& promise, base::StringPiece errmsg) {
|
static void RejectPromise(PromiseBase&& promise, base::StringPiece errmsg) {
|
||||||
if (gin_helper::Locker::IsBrowserProcess() &&
|
if (electron::IsBrowserProcess() &&
|
||||||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
||||||
content::GetUIThreadTaskRunner({})->PostTask(
|
content::GetUIThreadTaskRunner({})->PostTask(
|
||||||
FROM_HERE,
|
FROM_HERE,
|
||||||
|
@ -89,7 +90,7 @@ class Promise : public PromiseBase {
|
||||||
|
|
||||||
// Helper for resolving the promise with |result|.
|
// Helper for resolving the promise with |result|.
|
||||||
static void ResolvePromise(Promise<RT> promise, RT result) {
|
static void ResolvePromise(Promise<RT> promise, RT result) {
|
||||||
if (gin_helper::Locker::IsBrowserProcess() &&
|
if (electron::IsBrowserProcess() &&
|
||||||
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) {
|
||||||
content::GetUIThreadTaskRunner({})->PostTask(
|
content::GetUIThreadTaskRunner({})->PostTask(
|
||||||
FROM_HERE, base::BindOnce([](Promise<RT> promise,
|
FROM_HERE, base::BindOnce([](Promise<RT> promise,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
#include "base/functional/bind.h"
|
#include "base/functional/bind.h"
|
||||||
#include "base/supports_user_data.h"
|
#include "base/supports_user_data.h"
|
||||||
#include "shell/browser/electron_browser_main_parts.h"
|
#include "shell/browser/electron_browser_main_parts.h"
|
||||||
#include "shell/common/gin_helper/locker.h"
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
namespace gin_helper {
|
namespace gin_helper {
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class IDUserData : public base::SupportsUserData::Data {
|
||||||
|
|
||||||
TrackableObjectBase::TrackableObjectBase() {
|
TrackableObjectBase::TrackableObjectBase() {
|
||||||
// TODO(zcbenz): Make TrackedObject work in renderer process.
|
// TODO(zcbenz): Make TrackedObject work in renderer process.
|
||||||
DCHECK(gin_helper::Locker::IsBrowserProcess())
|
DCHECK(electron::IsBrowserProcess())
|
||||||
<< "This class only works for browser process";
|
<< "This class only works for browser process";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -171,7 +171,7 @@ bool AllowWasmCodeGenerationCallback(v8::Local<v8::Context> context,
|
||||||
// If we're running with contextIsolation enabled in the renderer process,
|
// If we're running with contextIsolation enabled in the renderer process,
|
||||||
// fall back to Blink's logic.
|
// fall back to Blink's logic.
|
||||||
if (node::Environment::GetCurrent(context) == nullptr) {
|
if (node::Environment::GetCurrent(context) == nullptr) {
|
||||||
if (gin_helper::Locker::IsBrowserProcess())
|
if (!electron::IsRendererProcess())
|
||||||
return false;
|
return false;
|
||||||
return blink::V8Initializer::WasmCodeGenerationCheckCallbackInMainThread(
|
return blink::V8Initializer::WasmCodeGenerationCheckCallbackInMainThread(
|
||||||
context, source);
|
context, source);
|
||||||
|
@ -188,7 +188,7 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
|
||||||
// No node environment means we're in the renderer process, either in a
|
// No node environment means we're in the renderer process, either in a
|
||||||
// sandboxed renderer or in an unsandboxed renderer with context isolation
|
// sandboxed renderer or in an unsandboxed renderer with context isolation
|
||||||
// enabled.
|
// enabled.
|
||||||
if (gin_helper::Locker::IsBrowserProcess()) {
|
if (!electron::IsRendererProcess()) {
|
||||||
NOTREACHED();
|
NOTREACHED();
|
||||||
return {false, {}};
|
return {false, {}};
|
||||||
}
|
}
|
||||||
|
@ -197,21 +197,20 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings(
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here then we have a node environment, so either a) we're in the
|
// If we get here then we have a node environment, so either a) we're in the
|
||||||
// main process, or b) we're in the renderer process in a context that has
|
// non-rendrer process, or b) we're in the renderer process in a context that
|
||||||
// both node and blink, i.e. contextIsolation disabled.
|
// has both node and blink, i.e. contextIsolation disabled.
|
||||||
|
|
||||||
// If we're in the main process, delegate to node.
|
|
||||||
if (gin_helper::Locker::IsBrowserProcess()) {
|
|
||||||
return node::ModifyCodeGenerationFromStrings(context, source, is_code_like);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're in the renderer with contextIsolation disabled, ask blink first
|
// If we're in the renderer with contextIsolation disabled, ask blink first
|
||||||
// (for CSP), and iff that allows codegen, delegate to node.
|
// (for CSP), and iff that allows codegen, delegate to node.
|
||||||
v8::ModifyCodeGenerationFromStringsResult result =
|
if (electron::IsRendererProcess()) {
|
||||||
blink::V8Initializer::CodeGenerationCheckCallbackInMainThread(
|
v8::ModifyCodeGenerationFromStringsResult result =
|
||||||
context, source, is_code_like);
|
blink::V8Initializer::CodeGenerationCheckCallbackInMainThread(
|
||||||
if (!result.codegen_allowed)
|
context, source, is_code_like);
|
||||||
return result;
|
if (!result.codegen_allowed)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in the main process or utility process, delegate to node.
|
||||||
return node::ModifyCodeGenerationFromStrings(context, source, is_code_like);
|
return node::ModifyCodeGenerationFromStrings(context, source, is_code_like);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
#include "shell/common/process_util.h"
|
#include "shell/common/process_util.h"
|
||||||
|
|
||||||
|
#include "base/command_line.h"
|
||||||
|
#include "content/public/common/content_switches.h"
|
||||||
#include "gin/dictionary.h"
|
#include "gin/dictionary.h"
|
||||||
#include "shell/common/gin_converters/callback_converter.h"
|
#include "shell/common/gin_converters/callback_converter.h"
|
||||||
#include "shell/common/node_includes.h"
|
#include "shell/common/node_includes.h"
|
||||||
|
@ -24,4 +26,18 @@ void EmitWarning(node::Environment* env,
|
||||||
emit_warning.Run(warning_msg, warning_type, "");
|
emit_warning.Run(warning_msg, warning_type, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsBrowserProcess() {
|
||||||
|
auto* command_line = base::CommandLine::ForCurrentProcess();
|
||||||
|
std::string process_type =
|
||||||
|
command_line->GetSwitchValueASCII(switches::kProcessType);
|
||||||
|
return process_type.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRendererProcess() {
|
||||||
|
auto* command_line = base::CommandLine::ForCurrentProcess();
|
||||||
|
std::string process_type =
|
||||||
|
command_line->GetSwitchValueASCII(switches::kProcessType);
|
||||||
|
return process_type == switches::kRendererProcess;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace electron
|
} // namespace electron
|
||||||
|
|
|
@ -17,6 +17,9 @@ void EmitWarning(node::Environment* env,
|
||||||
const std::string& warning_msg,
|
const std::string& warning_msg,
|
||||||
const std::string& warning_type);
|
const std::string& warning_type);
|
||||||
|
|
||||||
|
bool IsBrowserProcess();
|
||||||
|
bool IsRendererProcess();
|
||||||
|
|
||||||
} // namespace electron
|
} // namespace electron
|
||||||
|
|
||||||
#endif // ELECTRON_SHELL_COMMON_PROCESS_UTIL_H_
|
#endif // ELECTRON_SHELL_COMMON_PROCESS_UTIL_H_
|
||||||
|
|
|
@ -360,5 +360,19 @@ describe('utilityProcess module', () => {
|
||||||
await once(child, 'exit');
|
await once(child, 'exit');
|
||||||
expect(log).to.equal('hello\n');
|
expect(log).to.equal('hello\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not crash when running eval', async () => {
|
||||||
|
const child = utilityProcess.fork('./eval.js', [], {
|
||||||
|
cwd: fixturesPath,
|
||||||
|
stdio: 'ignore'
|
||||||
|
});
|
||||||
|
await once(child, 'spawn');
|
||||||
|
const [data] = await once(child, 'message');
|
||||||
|
expect(data).to.equal(42);
|
||||||
|
// Cleanup.
|
||||||
|
const exit = once(child, 'exit');
|
||||||
|
expect(child.kill()).to.be.true();
|
||||||
|
await exit;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
6
spec/fixtures/api/utility-process/eval.js
vendored
Normal file
6
spec/fixtures/api/utility-process/eval.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const vm = require('node:vm');
|
||||||
|
|
||||||
|
const contextObject = { result: 0 };
|
||||||
|
vm.createContext(contextObject);
|
||||||
|
vm.runInContext('eval(\'result = 42\')', contextObject);
|
||||||
|
process.parentPort.postMessage(contextObject.result);
|
Loading…
Add table
Add a link
Reference in a new issue