Skip to content

Commit

Permalink
chore(posix): implement stdio pipe interface
Browse files Browse the repository at this point in the history
  • Loading branch information
deepak1556 committed Aug 17, 2022
1 parent a6a886b commit 504a928
Show file tree
Hide file tree
Showing 6 changed files with 578 additions and 5 deletions.
31 changes: 30 additions & 1 deletion docs/api/utility-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ Process: [Main](../glossary.md#main-process)<br />
* `options` Object (optional)
* `env` Object - Environment key-value pairs. Default is `process.env`.
* `execArgv` string[] - List of string arguments passed to the executable. Default is `process.execArgv`.
* `stdio` (string[] | string) - Child's stdout and stderr configuration. Default is `pipe`.
String value can be one of `pipe`, `ignore`, `inherit`, for more details on these values you can refer to
[stdio][] documentation from Node.js. Currently this option does not allow configuring
stdin and is always set to `ignore`. For example, the supported values will be processed as following:
* `pipe`: equivalent to ['ignore', 'pipe', 'pipe'] (the default)
* `ignore`: equivalent to 'ignore', 'ignore', 'ignore']
* `inherit`: equivalent to ['ignore', 'inherit', 'inherit']
* `serviceName` string - Name of the process that will appear in `name` property of
[`child-process-gone` event of `app`](app.md#event-child-process-gone).
Default is `node.mojom.NodeService`.
Expand Down Expand Up @@ -69,9 +76,30 @@ if kill succeeds, and false otherwise.

#### `child.pid`

A `Integer` representing the process identifier (PID) of the child process.
A `Integer | undefined` representing the process identifier (PID) of the child process.
If the child process fails to spawn due to errors, then the value is `undefined`.

#### `child.stdout`

A `NodeJS.ReadableStream | null | undefined` that represents the child process's stdout.
If the child was spawned with options.stdio[1] set to anything other than 'pipe', then this will be `null`.
The property will be `undefined` if the child process could not be successfully spawned.

```js
// Main process
const { port1, port2 } = new MessageChannelMain()
const child = new UtilityProcess(path.join(__dirname, 'test.js'))
child.stdout.on('data', (data) => {
console.log(`Received chunk ${data}`)
})
```

#### `child.stderr`

A `NodeJS.ReadableStream | null | undefined` that represents the child process's stderr.
If the child was spawned with options.stdio[2] set to anything other than 'pipe', then this will be `null`.
The property will be `undefined` if the child process could not be successfully spawned.

### Instance Events

#### Event: 'spawn'
Expand All @@ -90,3 +118,4 @@ For other abnormal exit cases listen to the [`child-process-gone` event of `app`

[`child_process.fork`]: https://nodejs.org/dist/latest-v16.x/docs/api/child_process.html#child_processforkmodulepath-args-options
[Services API]: https://chromium.googlesource.com/chromium/src/+/master/docs/mojo_and_services.md
[stdio]: https://nodejs.org/dist/latest/docs/api/child_process.html#optionsstdio
85 changes: 84 additions & 1 deletion lib/browser/api/utility-process.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import * as path from 'path';
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
const { createProcessWrapper } = process._linkedBinding('electron_browser_utility_process');
Expand Down Expand Up @@ -39,8 +40,39 @@ function sanitizeKillSignal (signal: string | number): number {
}
}

class IOReadable extends Readable {
_shouldPush: boolean = false;
_data: (Buffer | null)[] = [];
_resume: (() => void) | null = null;

_storeInternalData (chunk: Buffer | null, resume: (() => void) | null) {
this._resume = resume;
this._data.push(chunk);
this._pushInternalData();
}

_pushInternalData () {
while (this._shouldPush && this._data.length > 0) {
const chunk = this._data.shift();
this._shouldPush = this.push(chunk);
}
if (this._shouldPush && this._resume) {
const resume = this._resume;
this._resume = null;
resume();
}
}

_read () {
this._shouldPush = true;
this._pushInternalData();
}
}

export default class UtilityProcess extends EventEmitter {
_handle: any
_handle: any;
_stdout: IOReadable | null | undefined = new IOReadable();
_stderr: IOReadable | null | undefined = new IOReadable();
constructor (modulePath: string, args: string[] = [], options: Electron.UtilityProcessConstructorOptions) {
super();
let relativeEntryPath = null;
Expand Down Expand Up @@ -78,11 +110,46 @@ export default class UtilityProcess extends EventEmitter {
}
}

if (typeof options.stdio === 'string') {
const stdio : Array<'pipe' | 'ignore' | 'inherit'> = [];
switch (options.stdio) {
case 'inherit':
case 'ignore':
this._stdout = null;
this._stderr = null;
// falls through
case 'pipe':
stdio.push('ignore', options.stdio, options.stdio);
break;
default:
throw new Error('stdio must be of the following values: inherit, pipe, ignore');
}
options.stdio = stdio;
} else if (Array.isArray(options.stdio)) {
if (options.stdio.length >= 3) {
if (options.stdio[0] !== 'ignore') {
throw new Error('stdin value other than ignore is not supported.');
}
if (options.stdio[1] === 'ignore' || options.stdio[1] === 'inherit') {
this._stdout = null;
}
if (options.stdio[2] === 'ignore' || options.stdio[2] === 'inherit') {
this._stderr = null;
}
} else {
throw new Error('configuration missing for stdin, stdout or stderr.');
}
}

this._handle = createProcessWrapper({ modulePath, args, ...options });
this._handle.emit = (channel: string, ...args: any[]) => {
if (channel === 'exit') {
this.emit('exit', ...args);
this._handle = null;
} else if (channel === 'stdout') {
this._stdout!._storeInternalData(Buffer.from(args[1]), args[2]);
} else if (channel === 'stderr') {
this._stderr!._storeInternalData(Buffer.from(args[1]), args[2]);
} else {
this.emit(channel, ...args);
}
Expand All @@ -96,6 +163,22 @@ export default class UtilityProcess extends EventEmitter {
return this._handle.pid;
}

get stdout () {
if (this._handle === null) {
this._stdout = null;
return undefined;
}
return this._stdout;
}

get stderr () {
if (this._handle === null) {
this._stderr = null;
return undefined;
}
return this._stderr;
}

postMessage (...args: any[]) {
if (this._handle === null) {
return;
Expand Down
1 change: 1 addition & 0 deletions patches/chromium/.patches
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,4 @@ chore_allow_chromium_to_handle_synthetic_mouse_events_for_touch.patch
add_maximized_parameter_to_linuxui_getwindowframeprovider.patch
add_electron_deps_to_license_credits_file.patch
feat_add_set_can_resize_mutator.patch
chore_posix_support_remapping_fds_when_launching_service_process.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Wed, 17 Aug 2022 22:04:47 +0900
Subject: chore(posix): support remapping fds when launching service process

Allows configuring base::LaunchOptions::fds_to_remap when launching the child process.
An example use of this option, UtilityProcess API allows reading the output From
stdout and stderr of child process by creating a pipe, whose write end is remapped
to STDOUT_FILENO and STDERR_FILENO allowing the parent process to read from the pipe.

diff --git a/content/browser/service_process_host_impl.cc b/content/browser/service_process_host_impl.cc
index f232eb49fa86e202d1a8ac3f0ca1e7c3ca28baf1..4b6fb0d26e54983790613a96113c7a8181ece4fd 100644
--- a/content/browser/service_process_host_impl.cc
+++ b/content/browser/service_process_host_impl.cc
@@ -184,6 +184,9 @@ void LaunchServiceProcess(mojo::GenericPendingReceiver receiver,
host->SetExtraCommandLineSwitches(std::move(options.extra_switches));
if (options.child_flags)
host->set_child_flags(*options.child_flags);
+#if BUILDFLAG(IS_POSIX)
+ host->SetAdditionalFds(std::move(options.fds_to_remap));
+#endif
host->Start();
host->GetChildProcess()->BindServiceInterface(std::move(receiver));
}
diff --git a/content/browser/utility_process_host.cc b/content/browser/utility_process_host.cc
index b96072fd917fce5cd8abb16a4ef705aea4dfe132..37a207eab8508ddc27227053a963a43f897a6dd0 100644
--- a/content/browser/utility_process_host.cc
+++ b/content/browser/utility_process_host.cc
@@ -153,6 +153,12 @@ void UtilityProcessHost::SetExtraCommandLineSwitches(
extra_switches_ = std::move(switches);
}

+#if BUILDFLAG(IS_POSIX)
+void UtilityProcessHost::SetAdditionalFds(base::FileHandleMappingVector mapping) {
+ fds_to_remap_ = std::move(mapping);
+}
+#endif
+
mojom::ChildProcess* UtilityProcessHost::GetChildProcess() {
return static_cast<ChildProcessHostImpl*>(process_->GetHost())
->child_process();
@@ -357,6 +363,15 @@ bool UtilityProcessHost::StartProcess() {
}
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)

+#if BUILDFLAG(IS_POSIX)
+ if (!fds_to_remap_.empty()) {
+ for (const auto& remapped_fd : fds_to_remap_) {
+ file_data->additional_remapped_fds.emplace(
+ remapped_fd.second, remapped_fd.first);
+ }
+ }
+#endif
+
std::unique_ptr<UtilitySandboxedProcessLauncherDelegate> delegate =
std::make_unique<UtilitySandboxedProcessLauncherDelegate>(
sandbox_type_, env_, *cmd_line);
diff --git a/content/browser/utility_process_host.h b/content/browser/utility_process_host.h
index efacea697ca7ba48e2dfbd61b28b543018b697d8..7f27414311b38028ff0a84fad875cd6828d36359 100644
--- a/content/browser/utility_process_host.h
+++ b/content/browser/utility_process_host.h
@@ -118,6 +118,10 @@ class CONTENT_EXPORT UtilityProcessHost
// Provides extra switches to append to the process's command line.
void SetExtraCommandLineSwitches(std::vector<std::string> switches);

+#if BUILDFLAG(IS_POSIX)
+ void SetAdditionalFds(base::FileHandleMappingVector mapping);
+#endif
+
// Returns a control interface for the running child process.
mojom::ChildProcess* GetChildProcess();

@@ -159,6 +163,12 @@ class CONTENT_EXPORT UtilityProcessHost
// Extra command line switches to append.
std::vector<std::string> extra_switches_;

+#if BUILDFLAG(IS_POSIX)
+ // Specifies file descriptors to propagate into the child process
+ // based on the mapping.
+ base::FileHandleMappingVector fds_to_remap_;
+#endif
+
// Indicates whether the process has been successfully launched yet, or if
// launch failed.
enum class LaunchState {
diff --git a/content/public/browser/service_process_host.cc b/content/public/browser/service_process_host.cc
index 75535e4d07051861a9f5e7ca6e18c017caf2ec8a..dba3e628d66a7970a6144b486e3c00904617e025 100644
--- a/content/public/browser/service_process_host.cc
+++ b/content/public/browser/service_process_host.cc
@@ -46,6 +46,14 @@ ServiceProcessHost::Options::WithExtraCommandLineSwitches(
return *this;
}

+#if BUILDFLAG(IS_POSIX)
+ServiceProcessHost::Options& ServiceProcessHost::Options::WithAdditionalFds(
+ base::FileHandleMappingVector mapping) {
+ fds_to_remap = std::move(mapping);
+ return *this;
+}
+#endif
+
ServiceProcessHost::Options& ServiceProcessHost::Options::WithProcessCallback(
base::OnceCallback<void(const base::Process&)> callback) {
process_callback = std::move(callback);
diff --git a/content/public/browser/service_process_host.h b/content/public/browser/service_process_host.h
index 3fd103b0d8486ed8fa5a6dd720b4de9aa189a178..72af3938499e1b01013a437d25b6298988bc9d44 100644
--- a/content/public/browser/service_process_host.h
+++ b/content/public/browser/service_process_host.h
@@ -13,6 +13,7 @@
#include "base/callback.h"
#include "base/command_line.h"
#include "base/observer_list_types.h"
+#include "base/process/launch.h"
#include "base/process/process_handle.h"
#include "base/strings/string_piece.h"
#include "build/chromecast_buildflags.h"
@@ -89,6 +90,12 @@ class CONTENT_EXPORT ServiceProcessHost {
// Specifies extra command line switches to append before launch.
Options& WithExtraCommandLineSwitches(std::vector<std::string> switches);

+#if BUILDFLAG(IS_POSIX)
+ // Specifies file descriptors to propagate into the child process
+ // based on the mapping.
+ Options& WithAdditionalFds(base::FileHandleMappingVector mapping);
+#endif
+
// Specifies a callback to be invoked with service process once it's
// launched. Will be on UI thread.
Options& WithProcessCallback(
@@ -102,6 +109,9 @@ class CONTENT_EXPORT ServiceProcessHost {
std::u16string display_name;
absl::optional<int> child_flags;
std::vector<std::string> extra_switches;
+#if BUILDFLAG(IS_POSIX)
+ base::FileHandleMappingVector fds_to_remap;
+#endif
base::OnceCallback<void(const base::Process&)> process_callback;
};

0 comments on commit 504a928

Please sign in to comment.