Skip to content

Subprocesses

There are several ways to execute subprocesses with the GNOME platform, but many of them are either cumbersome, error prone and most developers unknowingly end up rewriting high-level APIs already available in Gio.

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.

Promise Wrappers

This document uses asynchronous methods wrapped with Gio._promisify().

Copy & Paste
js
import Gio from 'gi://Gio';


/* Gio.Subprocess */
Gio._promisify(Gio.Subprocess.prototype, 'communicate_async');
Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async');
Gio._promisify(Gio.Subprocess.prototype, 'wait_async');
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');

/* Ancillary Methods */
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async',
    'read_line_finish_utf8');
Gio._promisify(Gio.OutputStream.prototype, 'write_bytes_async');

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.

js
import Gio from '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.
    const 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);
}

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:

js
import Gio from 'gi://Gio';


try {
    const 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.
    const cancellable = new Gio.Cancellable();

    // Strictly speaking, the only error that can be thrown by
    // this function is Gio.IOErrorEnum.CANCELLED.
    await proc.wait_async(cancellable);

    // 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())
        console.log('the process succeeded');
    else
        console.log('the process failed');
} catch (e) {
    logError(e);
}

Gio.Subprocess.wait_check_async() is a convenience function for calling Gio.Subprocess.wait_async() and then Gio.Subprocess.get_successful() in the callback:

js
import Gio from 'gi://Gio';


try {
    const proc = Gio.Subprocess.new(['sleep', '10'],
        Gio.SubprocessFlags.NONE);

    const success = await proc.wait_check_async(null);
    console.log(`The process ${success ? 'succeeded' : 'failed'}`);
} catch (e) {
    logError(e);
}

Communicating with Processes

For single run processes with text output, the most convenient function is Gio.Subprocess.communicate_utf8(). If the output of the process is not text or you just want the output in GLib.Bytes, you can use Gio.Subprocess.communicate() 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.

js
import Gio from 'gi://Gio';


try {
    const proc = Gio.Subprocess.new(['ls', '/'],
        Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);

    const [stdout, stderr] = await proc.communicate_utf8_async(null, null);

    if (proc.get_successful())
        console.log(stdout);
    else
        throw new Error(stderr);
} catch (e) {
    logError(e);
}

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:

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


// This is the process that we'll be running
const script = `
echo "BEGIN";

while read line; do
  echo "$line";
  sleep 1;
done;
`;


/**
 * This function simply writes the current time to `stdin`
 *
 * @param {Gio.InputStream} stdin - the `stdin` stream
 */
async function writeInput(stdin) {
    try {
        const date = new Date().toLocaleString();
        await stdin.write_bytes_async(new GLib.Bytes(`${date}\n`),
            GLib.PRIORITY_DEFAULT, null);

        console.log(`WROTE: ${date}`);
    } catch (e) {
        logError(e);
    }
}

/**
 * Reads a line from `stdout`, then queues another read/write
 *
 * @param {Gio.OutputStream} stdout - the `stdout` stream
 * @param {Gio.DataInputStream} stdin - the `stdin` stream
 */
function readOutput(stdout, stdin) {
    stdout.read_line_async(GLib.PRIORITY_LOW, null, (stream, result) => {
        try {
            const [line] = stream.read_line_finish_utf8(result);

            if (line !== null) {
                console.log(`READ: ${line}`);
                writeInput(stdin);
                readOutput(stdout, stdin);
            }
        } catch (e) {
            logError(e);
        }
    });
}

try {
    const proc = Gio.Subprocess.new(['bash', '-c', script],
        Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE);

    // Get the `stdin`and `stdout` pipes, wrapping `stdout` to make it easier to
    // read lines of text
    const stdinStream = proc.get_stdin_pipe();
    const stdoutStream = new Gio.DataInputStream({
        base_stream: proc.get_stdout_pipe(),
        close_base_stream: true,
    });

    // Start the loop
    readOutput(stdoutStream, stdinStream);
} catch (e) {
    logError(e);
}

Extra Tips

There are a few extra tricks you can use when working with Gio.Subprocess.

Cancellable Processes

Gio.Subprocess implements the Gio.Initable interface, which allows for failable initialization. You may find passing a cancellable useful to prevent the process from starting if already cancelled, or connecting to it to call Gio.Subprocess.force_exit() if triggered later:

js
import Gio from 'gi://Gio';


try {
    // 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.
    const proc = new Gio.Subprocess({
        argv: ['sleep', '10'],
        flags: Gio.SubprocessFlags.NONE,
    });

    // If the cancellable has already been triggered, the call to `init()` will
    // throw an error and the process will not be started.
    const cancellable = new Gio.Cancellable();

    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());
} catch (e) {
    logError(e);
}

Command Line Parsing

If you happen to have the command line as a single string, you can use the GLib.shell_parse_argv() 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.

js
import GLib from 'gi://GLib';


// This function may throw an error
try {
    // Returns: ['ls', '-l', '/']
    const [, argv1] = GLib.shell_parse_argv('ls -l /');

    // Returns: ['ls', '-l', '/dir with spaces']
    const [, argv2] = GLib.shell_parse_argv('ls -l "/dir with spaces"');
} catch (e) {
    logError(e);
}

GSubprocessLauncher

Gio.SubprocessLauncher 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 especially 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(), wait_check() and force_exit() on it.

js
import Gio from 'gi://Gio';


const 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
const proc1 = launcher.spawnv(['ls', '/']);
const proc2 = launcher.spawnv(['/home/me/script.sh']);

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.

js
import GLib from 'gi://GLib';
import Gio from '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;
    const proc = new Gio.Subprocess({
        argv,
        flags: Gio.SubprocessFlags.NONE,
    });
    proc.init(cancellable);

    if (cancellable instanceof Gio.Cancellable)
        cancelId = cancellable.connect(() => proc.force_exit());

    try {
        const success = await proc.wait_check_async(null);

        if (!success) {
            const status = proc.get_exit_status();

            throw new Gio.IOErrorEnum({
                code: Gio.IOErrorEnum.FAILED,
                message: `Command '${argv}' failed with exit code ${status}`,
            });
        }
    } 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;

    const proc = new Gio.Subprocess({argv, flags});
    proc.init(cancellable);

    if (cancellable instanceof Gio.Cancellable)
        cancelId = cancellable.connect(() => proc.force_exit());

    try {
        const [stdout, stderr] = await proc.communicate_utf8_async(input, null);

        const status = proc.get_exit_status();

        if (status !== 0) {
            throw new Gio.IOErrorEnum({
                code: Gio.IOErrorEnum.FAILED,
                message: stderr ? stderr.trim() : `Command '${argv}' failed with exit code ${status}`,
            });
        }

        return stdout.trim();
    } finally {
        if (cancelId > 0)
            cancellable.disconnect(cancelId);
    }
}

MIT Licensed | GJS, A GNOME Project