Subprocesses
There are several ways to execute subprocesses with the GNOME platform, but many of them are either cumbersome, error prone or limited. This article is about launching subprocesses in GJS with GSubprocess
.
GLib
GLib actually includes a number of utilities for working with subprocesses, but many applications written in C don't even use them. As a rule, you should always look in the higher-level Gio before you look in GLib for a utility.
Skip ahead to GSubprocess if you aren't interested in why GLib's spawn functions are less preferrable.
Synchronous Execution
This first example is very common to see in GNOME Shell extensions, which happens to be the worst place to use it.
Remember that the main thread of GNOME Shell is where user input and events are happening. This process will run synchronously, doing I/O on the main thread, blocking all other code from executing until it's finished:
'use strict';
const ByteArray = imports.byteArray;
const GLib = imports.gi.GLib;
try {
let [, stdout, stderr, status] = GLib.spawn_command_line_sync('ls /');
if (status !== 0) {
if (stderr instanceof Uint8Array)
stderr = ByteArray.toString(stderr);
throw new Error(stderr);
}
if (stdout instanceof Uint8Array)
stdout = ByteArray.toString(stdout);
// Now were done blocking the main loop, phewf!
log(stdout);
} catch (e) {
logError(e);
}
let loop = GLib.MainLoop.new(null, false);
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Asynchronous Execution
It is also possible to spawn simple processes asynchronously with GLib. Notice however, the function demonstrated below won't give you an indication of whether the process completed successfully (only if it started), when it completed or any output from it.
'use strict';
const GLib = imports.gi.GLib;
try {
GLib.spawn_command_line_async('ls /');
// The process must have started because it didn't throw an error, but did
// it actually succeed? By the way, where's my output?
} catch (e) {
logError(e);
}
2
3
4
5
6
7
8
9
10
11
12
13
Asynchronous Communication
It's also possible to communicate with a process spawned by GLib and check it's exit status. To do so, we'll need to open all three pipes (stdin
, stdout
and stderr
), close the ones we don't need, collect the output and add a child watch to be notified when it completes.
Below is how you might do that with GLib.spawn_async_with_pipes()
:
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
// A simple asynchronous read loop
function readOutput(stream, lineBuffer) {
stream.read_line_async(0, null, (stream, res) => {
try {
let line = stream.read_line_finish_utf8(res)[0];
if (line !== null) {
lineBuffer.push(line);
readOutput(stream, lineBuffer);
}
} catch (e) {
logError(e);
}
});
}
try {
let [, pid, stdin, stdout, stderr] = GLib.spawn_async_with_pipes(
// Working directory, passing %null to use the parent's
null,
// An array of arguments
['ls', '/'],
// Process ENV, passing %null to use the parent's
null,
// Flags; we need to use PATH so `ls` can be found and also need to know
// when the process has finished to check the output and status.
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
// Child setup function
null
);
// Any unsused streams still have to be closed explicitly, otherwise the
// file descriptors may be left open
GLib.close(stdin);
// Okay, now let's get output stream for `stdout`
let stdoutStream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: stdout,
close_fd: true
}),
close_base_stream: true
});
// We'll read the output asynchronously to avoid blocking the main thread
let stdoutLines = [];
readOutput(stdoutStream, stdoutLines);
// We want the real error from `stderr`, so we'll have to do the same here
let stderrStream = new Gio.DataInputStream({
base_stream: new Gio.UnixInputStream({
fd: stderr,
close_fd: true
}),
close_base_stream: true
});
let stderrLines = [];
readOutput(stderrStream, stderrLines);
// Watch for the process to finish, being sure to set a lower priority than
// we set for the read loop, so we get all the output
GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => {
if (status === 0) {
log(stdoutLines.join('\n'));
} else {
logError(new Error(stderrLines.join('\n')));
}
// Ensure we close the remaining streams and process
stdoutStream.close(null);
stderrStream.close(null);
GLib.spawn_close_pid(pid);
loop.quit();
});
} catch (e) {
logError(e);
loop.quit();
}
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
Depending on your use case you could simplify the above somewhat, but this is quite a bit more work that using Gio.Subprocess
. You can see the example below of communicating with a subprocess for a comparison.
GSubprocess
In contrast to the spawn functions available in GLib, Gio.Subprocess
is both simpler to use and safer for language bindings. It is just as powerful, does all the cleanup you'd have to do yourself and is far more convenient for most use cases.
Basic Usage
The simplest usage of Gio.Subprocess
amounts to creating a new initialized object. Once this function returns without error, the process will have started.
'use strict';
const Gio = imports.gi.Gio;
try {
// The process starts running immediately after this function is called. Any
// error thrown here will be a result of the process failing to start, not
// the success or failure of the process itself.
let proc = Gio.Subprocess.new(
// The program and command options are passed as a list of arguments
['ls', '-l', '/'],
// The flags control what I/O pipes are opened and how they are directed
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
);
// Once the process has started, you can end it with `force_exit()`
proc.force_exit();
} catch (e) {
logError(e);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Waiting for Processes
If you simply need to wait until a process completes before performing another operation, the best choice is Gio.Subprocess.wait_async()
. This will allow you to maintain a sequence of operations without blocking the main loop:
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
try {
let proc = Gio.Subprocess.new(['sleep', '10'], Gio.SubprocessFlags.NONE);
// NOTE: triggering the cancellable passed to these functions will only
// cancel the function NOT the process.
let cancellable = new Gio.Cancellable();
proc.wait_async(cancellable, (proc, result) => {
try {
// Strictly speaking, the only error that can be thrown by this
// function is Gio.IOErrorEnum.CANCELLED.
proc.wait_finish(result);
// The process has completed and you can check the exit status or
// ignore it if you just need notification the process completed.
if (proc.get_successful()) {
log('the process succeeded');
} else {
log('the process failed');
}
} catch (e) {
logError(e);
} finally {
loop.quit();
}
});
} catch (e) {
logError(e);
}
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Gio.Subprocess.wait_check_async()
is a convenience function for calling Gio.Subprocess.wait_async()
and then Gio.Subprocess.get_successful()
in the callback:
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
try {
let proc = Gio.Subprocess.new(['sleep', '10'], Gio.SubprocessFlags.NONE);
proc.wait_check_async(null, (proc, result) => {
try {
if (proc.wait_check_finish(result)) {
log('the process succeeded');
} else {
log('the process failed');
}
} catch (e) {
logError(e);
} finally {
loop.quit();
}
});
} catch (e) {
logError(e);
}
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Communicating with Processes
For single run processes with text output, the most convenient function is Gio.Subprocess.communicate_utf8()
open in new window. If the output of the process is not text or you just want the output in GLib.Bytes
, you can use Gio.Subprocess.communicate()
open in new window instead.
These two functions take (optional) input to pass to stdin
and collect all the output from stdout
and stderr
. Once the process completes the output is returned.
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
try {
let proc = Gio.Subprocess.new(
['ls', '/'],
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
);
proc.communicate_utf8_async(null, null, (proc, res) => {
try {
let [, stdout, stderr] = proc.communicate_utf8_finish(res);
if (proc.get_successful()) {
log(stdout);
} else {
throw new Error(stderr);
}
} catch (e) {
logError(e);
} finally {
loop.quit();
}
});
} catch (e) {
logError(e);
}
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
For processes that continue to run in the background, you can queue a callback for when the process completes while reading output and writing input as the process runs.
Below is a contrived example using a simple shell script to read lines from stdin
and write them back to stdout
:
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
// This is the process that we'll be running
let script = `
echo "BEGIN";
while read line; do
echo "$line";
sleep 1;
done;
`;
// This function simply writes the current time to `stdin`
function writeInput(stdin) {
let date = new Date().toLocaleString();
stdin.write_bytes_async(
new GLib.Bytes(`${date}\n`),
GLib.PRIORITY_DEFAULT,
null,
(stdin, res) => {
try {
stdin.write_bytes_finish(res);
log(`WROTE: ${date}`)
} catch (e) {
logError(e);
}
}
);
}
// This function reads a line from `stdout`, then queues another read/write
function readOutput(stdout, stdin) {
stdout.read_line_async(GLib.PRIORITY_LOW, null, (stdout, res) => {
try {
let line = stdout.read_line_finish_utf8(res)[0];
if (line !== null) {
log(`READ: ${line}`);
writeInput(stdin);
readOutput(stdout, stdin);
}
} catch (e) {
logError(e);
}
});
}
try {
let proc = Gio.Subprocess.new(
['bash', '-c', script],
(Gio.SubprocessFlags.STDIN_PIPE |
Gio.SubprocessFlags.STDOUT_PIPE)
);
// Watch for the process to exit, like normal
proc.wait_async(null, (proc, res) => {
try {
proc.wait_finish(res);
} catch (e) {
logError(e);
} finally {
loop.quit();
}
});
// Get the `stdin`and `stdout` pipes, wrapping `stdout` to make it easier to
// read lines of text
let stdinStream = proc.get_stdin_pipe();
let stdoutStream = new Gio.DataInputStream({
base_stream: proc.get_stdout_pipe(),
close_base_stream: true
});
// Start the loop
readOutput(stdoutStream, stdinStream);
} catch (e) {
logError(e);
}
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
Extra Tips
There are a few extra tricks you can use when working with Gio.Subprocess
.
Cancellable Processes
Gio.Subprocess
implements the Gio.Initable
open in new window interface, which allows for failable initialization. You may find passing cancellable useful if you want to prevent the process from starting if already cancelled or connecting to it to call Gio.Subprocess.force_exit()
open in new window if triggered later:
function execCancellable(argv, flags = 0, cancellable = null) {
// Create the process object with `new` and pass the arguments and flags as
// constructor properties. The process will start when `init()` returns,
// unless an error is thrown.
let proc = new Gio.Subprocess({
argv: argv,
flags: flags
});
// If the cancellable has already been triggered, the call to `init()` will
// throw an error and the process will not be started.
proc.init(cancellable);
// Chaining to the cancellable allows you to easily kill the process. You
// could use the same cancellabe for other related tasks allowing you to
// cancel them all without tracking them separately.
//
// NOTE: this is NOT the standard GObject.connect() function, so you should
// consult the documentation if the usage seems odd here.
let cancelId = 0;
if (cancellable instanceof Gio.Cancellable) {
cancelId = cancellable.connect(() => proc.force_exit());
}
return proc;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Command Line Parsing
If you happen to have the command line as a single string, you can use the GLib.shell_parse_argv()
open in new window function to parse it as a list of strings to pass to Gio.Subprocess
. This function can handle most common shell quoting, but may fail on some more complex usage.
// This function may throw an error
try {
// Returns: ['ls', '-l', '/']
let argv1 = GLib.shell_parse_argv('ls -l /')[1];
// Returns: ['ls', '-l', '/dir with spaces']
let argv2 = GLib.shell_parse_argv('ls -l "/dir with spaces"')[1];
} catch (e) {
logError(e);
}
2
3
4
5
6
7
8
9
10
Error handling
The error codes returned by Gio.Subprocess.get_exit_status()
are not the typical errors return by Gio functions. They can translated with the function Gio.io_error_from_errno()
open in new window and augmented with output from stderr
or GLib.strerror()
open in new window if that's not available:
proc.communicate_utf8_async(null, null, (proc, res) => {
try {
let [, stdout, stderr] = proc.communicate_utf8_finish(res);
let status = proc.get_exit_status();
if (status !== 0) {
throw new Gio.IOErrorEnum({
code: Gio.io_error_from_errno(status),
message: stderr ? stderr.trim() : GLib.strerror(status)
});
}
log(`SUCCESS: ${stdout.trim()}`);
} catch (e) {
logError(e);
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GSubprocessLauncher
Gio.SubprocessLauncher
open in new window is a re-usable object you can use to spawn processes. You can set the flags at construction, then just call Gio.SubprocessLauncher.spawnv()
with your arguments any time you want to spawn a process.
It also allows you to designate files for input and output, change the working directory and set or modify environment variables, which is expecially useful for spawning shell scripts.
In every other way, the returned object is a regular Gio.Subprocess
object and you can still call methods like communicate_utf8()
open in new window, wait_check()
open in new window and force_exit()
open in new window on it.
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
let loop = GLib.MainLoop.new(null, false);
let launcher = new Gio.SubprocessLauncher({
flags: (Gio.SubprocessFlags.STDIN_PIPE |
Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE)
});
// Set a custom ENV variable, which could be used in shell scripts
launcher.setenv("MY_VAR", "1", false);
// Log any errors to a file
launcher.set_stderr_file_path("error.log");
// Spawn as many processes with this launcher as you want
let proc1 = launcher.spawnv(['ls', '/']);
let proc2 = launcher.spawnv(['/home/me/script.sh']);
loop.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Complete Examples
Below is a few more complete, Promise-wrapped functions you can use in your code. The advantages here over GLib.spawn_command_line_async()
are checking the process actually completes successfully, the ability to stop it at any time, and notification when it does or improved errors when it doesn't.
'use strict';
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
/**
* Execute a command asynchronously and check the exit status.
*
* If given, @cancellable can be used to stop the process before it finishes.
*
* @param {string[]} argv - a list of string arguments
* @param {Gio.Cancellable} [cancellable] - optional cancellable object
* @returns {Promise<boolean>} - The process success
*/
async function execCheck(argv, cancellable = null) {
let cancelId = 0;
let proc = new Gio.Subprocess({
argv: argv,
flags: Gio.SubprocessFlags.NONE
});
proc.init(cancellable);
if (cancellable instanceof Gio.Cancellable) {
cancelId = cancellable.connect(() => proc.force_exit());
}
return new Promise((resolve, reject) => {
proc.wait_check_async(null, (proc, res) => {
try {
if (!proc.wait_check_finish(res)) {
let status = proc.get_exit_status();
throw new Gio.IOErrorEnum({
code: Gio.io_error_from_errno(status),
message: GLib.strerror(status)
});
}
resolve();
} catch (e) {
reject(e);
} finally {
if (cancelId > 0) {
cancellable.disconnect(cancelId);
}
}
});
});
}
/**
* Execute a command asynchronously and return the output from `stdout` on
* success or throw an error with output from `stderr` on failure.
*
* If given, @input will be passed to `stdin` and @cancellable can be used to
* stop the process before it finishes.
*
* @param {string[]} argv - a list of string arguments
* @param {string} [input] - Input to write to `stdin` or %null to ignore
* @param {Gio.Cancellable} [cancellable] - optional cancellable object
* @returns {Promise<string>} - The process output
*/
async function execCommunicate(argv, input = null, cancellable = null) {
let cancelId = 0;
let flags = (Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE);
if (input !== null)
flags |= Gio.SubprocessFlags.STDIN_PIPE;
let proc = new Gio.Subprocess({
argv: argv,
flags: flags
});
proc.init(cancellable);
if (cancellable instanceof Gio.Cancellable) {
cancelId = cancellable.connect(() => proc.force_exit());
}
return new Promise((resolve, reject) => {
proc.communicate_utf8_async(input, null, (proc, res) => {
try {
let [, stdout, stderr] = proc.communicate_utf8_finish(res);
let status = proc.get_exit_status();
if (status !== 0) {
throw new Gio.IOErrorEnum({
code: Gio.io_error_from_errno(status),
message: stderr ? stderr.trim() : GLib.strerror(status)
});
}
resolve(stdout.trim());
} catch (e) {
reject(e);
} finally {
if (cancelId > 0) {
cancellable.disconnect(cancelId);
}
}
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106