feat: UtilityProcess API (#34980)
* chore: initial scaffolding * chore: implement interface and docs * chore: address code style review * fix: cleanup of utility process on shutdown * chore: simplify NodeBindings::CreateEnvironment * chore: rename disableLibraryValidation => allowLoadingUnsignedLibraries * chore: implement process.parentPort * chore(posix): implement stdio pipe interface * chore(win): implement stdio interface * chore: reenable SetNodeOptions for utility process * chore: add specs * chore: fix lint * fix: update kill API * fix: update process.parentPort API * fix: exit event * docs: update exit event * fix: tests on linux * chore: expand on some comments * fix: shutdown of pipe reader Avoid logging since it is always the case that reader end of pipe will terminate after the child process. * fix: remove exit code check for crash spec * fix: rm PR_SET_NO_NEW_PRIVS for unsandbox utility process * chore: fix incorrect rebase * fix: address review feedback * chore: rename utility_process -> utility * chore: update docs * chore: cleanup c++ implemantation * fix: leak in NodeServiceHost impl * chore: minor cleanup * chore: cleanup JS implementation * chore: flip default stdio to inherit * fix: some api improvements * Support cwd option * Remove path restriction for modulePath * Rewire impl for env support * fix: add tests for cwd and env option * chore: alt impl for reading stdio handles * chore: support message queuing * chore: fix lint * chore: new UtilityProcess => utilityProcess.fork * fix: support for uncaught exception exits * chore: remove process.execArgv as default * fix: windows build * fix: style changes * fix: docs and style changes * chore: update patches * spec: disable flaky test on win32 arm CI Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
This commit is contained in:
		
					parent
					
						
							
								44c40efecf
							
						
					
				
			
			
				commit
				
					
						da0fd286b4
					
				
			
		
					 59 changed files with 2700 additions and 54 deletions
				
			
		
							
								
								
									
										420
									
								
								shell/browser/api/electron_api_utility_process.cc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								shell/browser/api/electron_api_utility_process.cc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,420 @@ | |||
| // Copyright (c) 2022 Microsoft, Inc.
 | ||||
| // Use of this source code is governed by the MIT license that can be
 | ||||
| // found in the LICENSE file.
 | ||||
| 
 | ||||
| #include "shell/browser/api/electron_api_utility_process.h" | ||||
| 
 | ||||
| #include <map> | ||||
| #include <utility> | ||||
| 
 | ||||
| #include "base/bind.h" | ||||
| #include "base/files/file_util.h" | ||||
| #include "base/no_destructor.h" | ||||
| #include "base/process/kill.h" | ||||
| #include "base/process/launch.h" | ||||
| #include "base/process/process.h" | ||||
| #include "content/public/browser/service_process_host.h" | ||||
| #include "content/public/common/child_process_host.h" | ||||
| #include "content/public/common/result_codes.h" | ||||
| #include "gin/handle.h" | ||||
| #include "gin/object_template_builder.h" | ||||
| #include "gin/wrappable.h" | ||||
| #include "mojo/public/cpp/bindings/pending_receiver.h" | ||||
| #include "shell/browser/api/message_port.h" | ||||
| #include "shell/browser/javascript_environment.h" | ||||
| #include "shell/common/gin_converters/callback_converter.h" | ||||
| #include "shell/common/gin_converters/file_path_converter.h" | ||||
| #include "shell/common/gin_helper/dictionary.h" | ||||
| #include "shell/common/gin_helper/object_template_builder.h" | ||||
| #include "shell/common/node_includes.h" | ||||
| #include "shell/common/v8_value_serializer.h" | ||||
| #include "third_party/blink/public/common/messaging/message_port_descriptor.h" | ||||
| #include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h" | ||||
| 
 | ||||
| #if BUILDFLAG(IS_POSIX) | ||||
| #include "base/posix/eintr_wrapper.h" | ||||
| #endif | ||||
| 
 | ||||
| #if BUILDFLAG(IS_WIN) | ||||
| #include <fcntl.h> | ||||
| #include <io.h> | ||||
| #include "base/win/windows_types.h" | ||||
| #endif | ||||
| 
 | ||||
| namespace electron { | ||||
| 
 | ||||
| base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>& | ||||
| GetAllUtilityProcessWrappers() { | ||||
|   static base::NoDestructor< | ||||
|       base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>> | ||||
|       s_all_utility_process_wrappers; | ||||
|   return *s_all_utility_process_wrappers; | ||||
| } | ||||
| 
 | ||||
| namespace api { | ||||
| 
 | ||||
| gin::WrapperInfo UtilityProcessWrapper::kWrapperInfo = { | ||||
|     gin::kEmbedderNativeGin}; | ||||
| 
 | ||||
| UtilityProcessWrapper::UtilityProcessWrapper( | ||||
|     node::mojom::NodeServiceParamsPtr params, | ||||
|     std::u16string display_name, | ||||
|     std::map<IOHandle, IOType> stdio, | ||||
|     base::EnvironmentMap env_map, | ||||
|     base::FilePath current_working_directory, | ||||
|     bool use_plugin_helper) { | ||||
| #if BUILDFLAG(IS_WIN) | ||||
|   base::win::ScopedHandle stdout_write(nullptr); | ||||
|   base::win::ScopedHandle stderr_write(nullptr); | ||||
| #elif BUILDFLAG(IS_POSIX) | ||||
|   base::FileHandleMappingVector fds_to_remap; | ||||
| #endif | ||||
|   for (const auto& [io_handle, io_type] : stdio) { | ||||
|     if (io_type == IOType::IO_PIPE) { | ||||
| #if BUILDFLAG(IS_WIN) | ||||
|       HANDLE read = nullptr; | ||||
|       HANDLE write = nullptr; | ||||
|       // Ideally we would create with SECURITY_ATTRIBUTES.bInheritHandles
 | ||||
|       // set to TRUE so that the write handle can be duplicated into the
 | ||||
|       // child process for use,
 | ||||
|       // See
 | ||||
|       // https://learn.microsoft.com/en-us/windows/win32/procthread/inheritance#inheriting-handles
 | ||||
|       // for inheritance behavior of child process. But we don't do it here
 | ||||
|       // since base::Launch already takes of setting the
 | ||||
|       // inherit attribute when configuring
 | ||||
|       // `base::LaunchOptions::handles_to_inherit` Refs
 | ||||
|       // https://source.chromium.org/chromium/chromium/src/+/main:base/process/launch_win.cc;l=303-332
 | ||||
|       if (!::CreatePipe(&read, &write, nullptr, 0)) { | ||||
|         PLOG(ERROR) << "pipe creation failed"; | ||||
|         return; | ||||
|       } | ||||
|       if (io_handle == IOHandle::STDOUT) { | ||||
|         stdout_write.Set(write); | ||||
|         stdout_read_handle_ = read; | ||||
|         stdout_read_fd_ = | ||||
|             _open_osfhandle(reinterpret_cast<intptr_t>(read), _O_RDONLY); | ||||
|       } else if (io_handle == IOHandle::STDERR) { | ||||
|         stderr_write.Set(write); | ||||
|         stderr_read_handle_ = read; | ||||
|         stderr_read_fd_ = | ||||
|             _open_osfhandle(reinterpret_cast<intptr_t>(read), _O_RDONLY); | ||||
|       } | ||||
| #elif BUILDFLAG(IS_POSIX) | ||||
|       int pipe_fd[2]; | ||||
|       if (HANDLE_EINTR(pipe(pipe_fd)) < 0) { | ||||
|         PLOG(ERROR) << "pipe creation failed"; | ||||
|         return; | ||||
|       } | ||||
|       if (io_handle == IOHandle::STDOUT) { | ||||
|         fds_to_remap.push_back(std::make_pair(pipe_fd[1], STDOUT_FILENO)); | ||||
|         stdout_read_fd_ = pipe_fd[0]; | ||||
|       } else if (io_handle == IOHandle::STDERR) { | ||||
|         fds_to_remap.push_back(std::make_pair(pipe_fd[1], STDERR_FILENO)); | ||||
|         stderr_read_fd_ = pipe_fd[0]; | ||||
|       } | ||||
| #endif | ||||
|     } else if (io_type == IOType::IO_IGNORE) { | ||||
| #if BUILDFLAG(IS_WIN) | ||||
|       HANDLE handle = | ||||
|           CreateFileW(L"NUL", FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES, | ||||
|                       FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, | ||||
|                       OPEN_EXISTING, 0, nullptr); | ||||
|       if (handle == INVALID_HANDLE_VALUE) { | ||||
|         PLOG(ERROR) << "Failed to create null handle"; | ||||
|         return; | ||||
|       } | ||||
|       if (io_handle == IOHandle::STDOUT) { | ||||
|         stdout_write.Set(handle); | ||||
|       } else if (io_handle == IOHandle::STDERR) { | ||||
|         stderr_write.Set(handle); | ||||
|       } | ||||
| #elif BUILDFLAG(IS_POSIX) | ||||
|       int devnull = open("/dev/null", O_WRONLY); | ||||
|       if (devnull < 0) { | ||||
|         PLOG(ERROR) << "failed to open /dev/null"; | ||||
|         return; | ||||
|       } | ||||
|       if (io_handle == IOHandle::STDOUT) { | ||||
|         fds_to_remap.push_back(std::make_pair(devnull, STDOUT_FILENO)); | ||||
|       } else if (io_handle == IOHandle::STDERR) { | ||||
|         fds_to_remap.push_back(std::make_pair(devnull, STDERR_FILENO)); | ||||
|       } | ||||
| #endif | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   mojo::PendingReceiver<node::mojom::NodeService> receiver = | ||||
|       node_service_remote_.BindNewPipeAndPassReceiver(); | ||||
| 
 | ||||
|   content::ServiceProcessHost::Launch( | ||||
|       std::move(receiver), | ||||
|       content::ServiceProcessHost::Options() | ||||
|           .WithDisplayName(display_name.empty() | ||||
|                                ? std::u16string(u"Node Utility Process") | ||||
|                                : display_name) | ||||
|           .WithExtraCommandLineSwitches(params->exec_args) | ||||
|           .WithCurrentDirectory(current_working_directory) | ||||
|           // Inherit parent process environment when there is no custom
 | ||||
|           // environment provided by the user.
 | ||||
|           .WithEnvironment(env_map, | ||||
|                            env_map.empty() ? false : true /*clear_environment*/) | ||||
| #if BUILDFLAG(IS_WIN) | ||||
|           .WithStdoutHandle(std::move(stdout_write)) | ||||
|           .WithStderrHandle(std::move(stderr_write)) | ||||
| #elif BUILDFLAG(IS_POSIX) | ||||
|           .WithAdditionalFds(std::move(fds_to_remap)) | ||||
| #endif | ||||
| #if BUILDFLAG(IS_MAC) | ||||
|           .WithChildFlags(use_plugin_helper | ||||
|                               ? content::ChildProcessHost::CHILD_PLUGIN | ||||
|                               : content::ChildProcessHost::CHILD_NORMAL) | ||||
| #endif | ||||
|           .WithProcessCallback( | ||||
|               base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunched, | ||||
|                              weak_factory_.GetWeakPtr())) | ||||
|           .Pass()); | ||||
|   node_service_remote_.set_disconnect_with_reason_handler( | ||||
|       base::BindOnce(&UtilityProcessWrapper::OnServiceProcessDisconnected, | ||||
|                      weak_factory_.GetWeakPtr())); | ||||
| 
 | ||||
|   // We use a separate message pipe to support postMessage API
 | ||||
|   // instead of the existing receiver interface so that we can
 | ||||
|   // support queuing of messages without having to block other
 | ||||
|   // interfaces.
 | ||||
|   blink::MessagePortDescriptorPair pipe; | ||||
|   host_port_ = pipe.TakePort0(); | ||||
|   params->port = pipe.TakePort1(); | ||||
|   connector_ = std::make_unique<mojo::Connector>( | ||||
|       host_port_.TakeHandleToEntangleWithEmbedder(), | ||||
|       mojo::Connector::SINGLE_THREADED_SEND, | ||||
|       base::ThreadTaskRunnerHandle::Get()); | ||||
|   connector_->set_incoming_receiver(this); | ||||
|   connector_->set_connection_error_handler(base::BindOnce( | ||||
|       &UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr())); | ||||
| 
 | ||||
|   node_service_remote_->Initialize(std::move(params)); | ||||
| } | ||||
| 
 | ||||
| UtilityProcessWrapper::~UtilityProcessWrapper() = default; | ||||
| 
 | ||||
| void UtilityProcessWrapper::OnServiceProcessLaunched( | ||||
|     const base::Process& process) { | ||||
|   DCHECK(node_service_remote_.is_connected()); | ||||
|   pid_ = process.Pid(); | ||||
|   GetAllUtilityProcessWrappers().AddWithID(this, pid_); | ||||
|   if (stdout_read_fd_ != -1) { | ||||
|     EmitWithoutCustomEvent("stdout", stdout_read_fd_); | ||||
|   } | ||||
|   if (stderr_read_fd_ != -1) { | ||||
|     EmitWithoutCustomEvent("stderr", stderr_read_fd_); | ||||
|   } | ||||
|   // Emit 'spawn' event
 | ||||
|   EmitWithoutCustomEvent("spawn"); | ||||
| } | ||||
| 
 | ||||
| void UtilityProcessWrapper::OnServiceProcessDisconnected( | ||||
|     uint32_t error_code, | ||||
|     const std::string& description) { | ||||
|   if (pid_ != base::kNullProcessId) | ||||
|     GetAllUtilityProcessWrappers().Remove(pid_); | ||||
|   CloseConnectorPort(); | ||||
|   // Emit 'exit' event
 | ||||
|   EmitWithoutCustomEvent("exit", error_code); | ||||
|   Unpin(); | ||||
| } | ||||
| 
 | ||||
| void UtilityProcessWrapper::CloseConnectorPort() { | ||||
|   if (!connector_closed_ && connector_->is_valid()) { | ||||
|     host_port_.GiveDisentangledHandle(connector_->PassMessagePipe()); | ||||
|     connector_ = nullptr; | ||||
|     host_port_.Reset(); | ||||
|     connector_closed_ = true; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| void UtilityProcessWrapper::Shutdown(int exit_code) { | ||||
|   if (pid_ != base::kNullProcessId) | ||||
|     GetAllUtilityProcessWrappers().Remove(pid_); | ||||
|   node_service_remote_.reset(); | ||||
|   CloseConnectorPort(); | ||||
|   // Emit 'exit' event
 | ||||
|   EmitWithoutCustomEvent("exit", exit_code); | ||||
|   Unpin(); | ||||
| } | ||||
| 
 | ||||
| void UtilityProcessWrapper::PostMessage(gin::Arguments* args) { | ||||
|   if (!node_service_remote_.is_connected()) | ||||
|     return; | ||||
| 
 | ||||
|   blink::TransferableMessage transferable_message; | ||||
|   v8::Local<v8::Value> message_value; | ||||
|   if (args->GetNext(&message_value)) { | ||||
|     if (!electron::SerializeV8Value(args->isolate(), message_value, | ||||
|                                     &transferable_message)) { | ||||
|       // SerializeV8Value sets an exception.
 | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   v8::Local<v8::Value> transferables; | ||||
|   std::vector<gin::Handle<MessagePort>> wrapped_ports; | ||||
|   if (args->GetNext(&transferables)) { | ||||
|     if (!gin::ConvertFromV8(args->isolate(), transferables, &wrapped_ports)) { | ||||
|       gin_helper::ErrorThrower(args->isolate()) | ||||
|           .ThrowTypeError("Invalid value for transfer"); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   bool threw_exception = false; | ||||
|   transferable_message.ports = MessagePort::DisentanglePorts( | ||||
|       args->isolate(), wrapped_ports, &threw_exception); | ||||
|   if (threw_exception) | ||||
|     return; | ||||
| 
 | ||||
|   mojo::Message mojo_message = blink::mojom::TransferableMessage::WrapAsMessage( | ||||
|       std::move(transferable_message)); | ||||
|   connector_->Accept(&mojo_message); | ||||
| } | ||||
| 
 | ||||
| bool UtilityProcessWrapper::Kill() const { | ||||
|   if (pid_ == base::kNullProcessId) | ||||
|     return 0; | ||||
|   base::Process process = base::Process::Open(pid_); | ||||
|   bool result = process.Terminate(content::RESULT_CODE_NORMAL_EXIT, false); | ||||
|   // Refs https://bugs.chromium.org/p/chromium/issues/detail?id=818244
 | ||||
|   // Currently utility process is not sandboxed which
 | ||||
|   // means Zygote is not used on linux, refs
 | ||||
|   // content::UtilitySandboxedProcessLauncherDelegate::GetZygote.
 | ||||
|   // If sandbox feature is enabled for the utility process, then the
 | ||||
|   // process reap should be signaled through the zygote via
 | ||||
|   // content::ZygoteCommunication::EnsureProcessTerminated.
 | ||||
|   base::EnsureProcessTerminated(std::move(process)); | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| v8::Local<v8::Value> UtilityProcessWrapper::GetOSProcessId( | ||||
|     v8::Isolate* isolate) const { | ||||
|   if (pid_ == base::kNullProcessId) | ||||
|     return v8::Undefined(isolate); | ||||
|   return gin::ConvertToV8(isolate, pid_); | ||||
| } | ||||
| 
 | ||||
| bool UtilityProcessWrapper::Accept(mojo::Message* mojo_message) { | ||||
|   blink::TransferableMessage message; | ||||
|   if (!blink::mojom::TransferableMessage::DeserializeFromMessage( | ||||
|           std::move(*mojo_message), &message)) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); | ||||
|   v8::HandleScope handle_scope(isolate); | ||||
|   v8::Local<v8::Value> message_value = | ||||
|       electron::DeserializeV8Value(isolate, message); | ||||
|   EmitWithoutCustomEvent("message", message_value); | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| // static
 | ||||
| raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId( | ||||
|     base::ProcessId pid) { | ||||
|   auto* utility_process_wrapper = GetAllUtilityProcessWrappers().Lookup(pid); | ||||
|   return !!utility_process_wrapper ? utility_process_wrapper : nullptr; | ||||
| } | ||||
| 
 | ||||
| // static
 | ||||
| gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create( | ||||
|     gin::Arguments* args) { | ||||
|   gin_helper::Dictionary dict; | ||||
|   if (!args->GetNext(&dict)) { | ||||
|     args->ThrowTypeError("Options must be an object."); | ||||
|     return gin::Handle<UtilityProcessWrapper>(); | ||||
|   } | ||||
| 
 | ||||
|   std::u16string display_name; | ||||
|   bool use_plugin_helper = false; | ||||
|   std::map<IOHandle, IOType> stdio; | ||||
|   base::FilePath current_working_directory; | ||||
|   base::EnvironmentMap env_map; | ||||
|   node::mojom::NodeServiceParamsPtr params = | ||||
|       node::mojom::NodeServiceParams::New(); | ||||
|   dict.Get("modulePath", ¶ms->script); | ||||
|   if (dict.Has("args") && !dict.Get("args", ¶ms->args)) { | ||||
|     args->ThrowTypeError("Invalid value for args"); | ||||
|     return gin::Handle<UtilityProcessWrapper>(); | ||||
|   } | ||||
| 
 | ||||
|   gin_helper::Dictionary opts; | ||||
|   if (dict.Get("options", &opts)) { | ||||
|     if (opts.Has("env") && !opts.Get("env", &env_map)) { | ||||
|       args->ThrowTypeError("Invalid value for env"); | ||||
|       return gin::Handle<UtilityProcessWrapper>(); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.Has("execArgv") && !opts.Get("execArgv", ¶ms->exec_args)) { | ||||
|       args->ThrowTypeError("Invalid value for execArgv"); | ||||
|       return gin::Handle<UtilityProcessWrapper>(); | ||||
|     } | ||||
| 
 | ||||
|     opts.Get("serviceName", &display_name); | ||||
|     opts.Get("cwd", ¤t_working_directory); | ||||
| 
 | ||||
|     std::vector<std::string> stdio_arr{"ignore", "inherit", "inherit"}; | ||||
|     opts.Get("stdio", &stdio_arr); | ||||
|     for (size_t i = 0; i < 3; i++) { | ||||
|       IOType type; | ||||
|       if (stdio_arr[i] == "ignore") | ||||
|         type = IOType::IO_IGNORE; | ||||
|       else if (stdio_arr[i] == "inherit") | ||||
|         type = IOType::IO_INHERIT; | ||||
|       else if (stdio_arr[i] == "pipe") | ||||
|         type = IOType::IO_PIPE; | ||||
| 
 | ||||
|       stdio.emplace(static_cast<IOHandle>(i), type); | ||||
|     } | ||||
| 
 | ||||
| #if BUILDFLAG(IS_MAC) | ||||
|     opts.Get("allowLoadingUnsignedLibraries", &use_plugin_helper); | ||||
| #endif | ||||
|   } | ||||
|   auto handle = gin::CreateHandle( | ||||
|       args->isolate(), | ||||
|       new UtilityProcessWrapper(std::move(params), display_name, | ||||
|                                 std::move(stdio), env_map, | ||||
|                                 current_working_directory, use_plugin_helper)); | ||||
|   handle->Pin(args->isolate()); | ||||
|   return handle; | ||||
| } | ||||
| 
 | ||||
| // static
 | ||||
| gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder( | ||||
|     v8::Isolate* isolate) { | ||||
|   return gin_helper::EventEmitterMixin< | ||||
|              UtilityProcessWrapper>::GetObjectTemplateBuilder(isolate) | ||||
|       .SetMethod("postMessage", &UtilityProcessWrapper::PostMessage) | ||||
|       .SetMethod("kill", &UtilityProcessWrapper::Kill) | ||||
|       .SetProperty("pid", &UtilityProcessWrapper::GetOSProcessId); | ||||
| } | ||||
| 
 | ||||
| const char* UtilityProcessWrapper::GetTypeName() { | ||||
|   return "UtilityProcessWrapper"; | ||||
| } | ||||
| 
 | ||||
| }  // namespace api
 | ||||
| 
 | ||||
| }  // namespace electron
 | ||||
| 
 | ||||
| namespace { | ||||
| 
 | ||||
| void Initialize(v8::Local<v8::Object> exports, | ||||
|                 v8::Local<v8::Value> unused, | ||||
|                 v8::Local<v8::Context> context, | ||||
|                 void* priv) { | ||||
|   v8::Isolate* isolate = context->GetIsolate(); | ||||
|   gin_helper::Dictionary dict(isolate, exports); | ||||
|   dict.SetMethod("_fork", &electron::api::UtilityProcessWrapper::Create); | ||||
| } | ||||
| 
 | ||||
| }  // namespace
 | ||||
| 
 | ||||
| NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_utility_process, Initialize) | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Robo
				Robo