Subprocesses and Pseudo Terminals

Creating Subprocesses

Builder provides a powerful abstraction for creating subprocesses. Ide.Subprocess allows you to setup and modify how a processes should be launched without the burden of how to launch the subprocess. This means that Builder can use different strategies based on the host system, subprocess requirements, and plugins that may need to modify the program arguments.

When Builder is not running in a sandbox, it can generally execute subprocesses the normal way using fork and exec. However, if Builder is sandboxed, it may need to run the subprocess on the host rather than inside the sandbox. To ensure your subprocess is run on the host, use Ide.Subprocess.set_run_on_host().

Note

You can only run programs on the host that are already installed. Use which program-name to determine if the process is available on the host.

If you are integrating an external tool, such as Valgrind, you might need to inject arguments into the argument array. For the Valgrind case, Ide.Subprocess.prepend_argv() of "valgrind" would be appropriate.

Some runtime plugins may need to modify the argument array even further. For example, the Flatpak plugin will require that all commands start with flatpak build ... so that the commands are never run on the host system, but instead inside the runtime. Plugins that require this should inject their additional arguments from the Ide.SubprocessLauncher.spawn() virtual-method so that plugins do not get confused about the placement of arguments.

from gi.repository import GLib
from gi.repository import Ide

# You may want access to stdin/stdout/stderr. If so, ensure you specify
# the appropriate Gio.SubprocessFlags for your subprocess.
launcher = Ide.SubprocessLauncher.new(Gio.SubprocessFlags.STDOUT_PIPE |
                                      Gio.SubprocessFlags.STDERR_PIPE |
                                      Gio.SubprocessFlags.STDIN_PIPE)

# If you need to specify where to launch the process. The default
# is the home directory.
launcher.set_cwd(os.path.join(GLib.get_home_dir(), 'Projects'))

# Push some arguments onto argv
launcher.push_argv('which')
launcher.push_argv('ls')

# Set some environment variables
launcher.setenv('LANG', 'C', True)

# Spawn the process. If you pass in a Gio.Cancellable, you can kill the
# subprocess by calling Gio.Cancellable.cancel().
subprocess = launcher.spawn(None)

# We need to wait for the child to complete. If you want to read the
# output of the subprocess, see Ide.Subprocess.communicate_utf8().
# wait_check() will ensure the return value is zero. If you do not
# care about the return value, just use wait().
try:
    subprocess.wait_check(None)
except Exception as ex:
    print(repr(ex))

 # May Ide.Subprocess API have async variants. Consider using them to
 # avoid needlessly blocking threads.

Supervising Subprocesses

There are times where you might want to respawn a process in case it exits prematurely. Builder provides the Ide.SubprocessSupervisor abstraction to simplify this for you.

The Ide.SubprocessSupervisor has a simple API. Just attach your Ide.SubprocessLauncher using Ide.SubprocessSupervisor.set_launcher() and call Ide.SubprocessSupervisor.start().

If the subprocess begins flapping (exiting immediately after spawning) some delay will be added to slow things down.

To stop the subprocess, use Ide.SubprocessSupervisor.stop().

If you need access to the subprocess, you can access it either via the Ide.SubprocessSupervisor.get_subprocess() method or by connecting to the Ide.SubprocessSupervisor::spawned() signal.

def on_subprocess_spawned(supervisor, subprocess):
    print("Spawned process " + subprocess.get_identifier())

launcher = create_launcher()

supervisor = Ide.SubprocessSupervisor()
supervisor.set_launcher(launcher)
supervisor.connect('spawned', on_subprocess_spawned)
supervisor.start()

Pseudo Terminals

Pseudo terminals are tricky business. In general, if you need access to a PTY, use the VTE library like Builder’s terminal plugin. For an example of how to setup the PTY, we use a flow like this.

// This code does little to no error checking.
// Your code should be more careful.

// First create our PTY master
VtePty *pty = vte_terminal_pty_new_sync (terminal,
                                         VTE_PTY_DEFAULT | VTE_PTY_NO_LASTLOG | VTE_PTY_NO_UTMP | VTE_PTY_NO_WTMP,
                                         NULL, &error);

// Now go through the PTY slave setup
int master_fd = vte_pty_get_fd (pty);

assert (grantpt (master_fd) != 0);
assert (unlockpt (master_fd) != 0);

// Get the path to the PTY slave
char name[PATH_MAX];
assert (ptsname_r (master_fd, name, sizeof name - 1) != 0);
name [sizeof name - 1] = '\0';

// Open the PTY slave
int slave_fd = open (name, O_RDWR | O_CLOEXEC);

// Now, when spawning a process, you can set stdin/stdout/stderr to the FD
// of the slave. We use dup() because the callee takes ownership.
ide_subprocess_launcher_take_stdin_fd (launcher, dup (slave_fd));
ide_subprocess_launcher_take_stdout_fd (launcher, dup (slave_fd));
ide_subprocess_launcher_take_stderr_fd (launcher, dup (slave_fd));
close (slave_fd);

When launching the subprocess with Builder, it will detect that stdin, stdout, or stderr are pseudo terminals and perform the proper ioctl() setup for you. This allows for the PTY to cross the sandbox boundary to the host, ensuring that you may have a host-based shell with a PTY from within the sandbox.