Skip to content
Snippets Groups Projects
Commit 6929b007 authored by Simon McVittie's avatar Simon McVittie
Browse files

launcher: Add ability to run multiple commands in one container


See the new pressure-vessel-launcher(1) man page for details.

We need this for Proton games, where running a game takes several
steps. At the moment each step is its own container, which means
they can't share locks, IPC sockets and other state.

Signed-off-by: default avatarSimon McVittie <smcv@collabora.com>
parent 9307dcd3
No related branches found
No related tags found
No related merge requests found
......@@ -109,6 +109,8 @@ SCRIPTS = [
]
EXECUTABLES = [
'pressure-vessel-adverb',
'pressure-vessel-launch',
'pressure-vessel-launcher',
'pressure-vessel-try-setlocale',
'pressure-vessel-wrap',
]
......
---
title: pressure-vessel-launch
section: 1
...
# NAME
pressure-vessel-launch - client to launch processes in a container
# SYNOPSIS
**pressure-vessel-launch**
[**--clear-env**]
[**--directory** *DIR*]
[**--env** _VAR_**=**_VALUE_]
[**--forward-fd** *FD*]
[**--verbose**]
{**--bus-name** *NAME*|**--dbus-address** *ADDRESS*|**--socket** *SOCKET*}
[**--**]
*COMMAND* [*ARGUMENTS...*]
# DESCRIPTION
**pressure-vessel-launch** connects to an `AF_UNIX` socket established
by **pressure-vessel-launcher**(1), and executes an arbitrary command
as a subprocess of **pressure-vessel-launcher**.
# OPTIONS
**--socket** *PATH*, **--socket** *@ABSTRACT*
: Connect to the given `AF_UNIX` socket, which can either be an
absolute path, or `@` followed by an arbitrary string.
An absolute path indicates a filesystem-based socket, which is
associated with the filesystem and can be shared between filesystem
namespaces by bind-mounting.
`@` indicates an an abstract socket, which is associated with a
network namespace, is shared between all containers that are in
the same network namespace, and cannot be shared across network
namespace boundaries.
**--dbus-address** *ADDRESS*
: The same as **--socket**, but the socket is specified in the form
of a D-Bus address.
**--bus-name** *NAME*
: Connect to the well-known D-Bus session bus and send commands to
the given *NAME*, which is assumed to be owned by
**pressure-vessel-launcher**.
**--clear-env**
: The *COMMAND* runs in an empty environment, apart from any environment
variables set by **--env**. By default, it inherits environment
variables from **pressure-vessel-launcher**, with **--env**
overriding individual variables.
**--directory** *DIR*
: Arrange for the *COMMAND* to run in *DIR*.
By default, it inherits the current working directory from
**pressure-vessel-launcher**.
**--env** _VAR=VALUE_
: Set environment variable _VAR_ to _VALUE_.
This is mostly equivalent to using
**env** _VAR=VALUE_ *COMMAND* *ARGUMENTS...*
as the command.
**--forward-fd** *FD*
: Arrange for the *COMMAND* to receive file descriptor number *FD*
from outside the container. File descriptors 0, 1 and 2
(standard input, standard output and standard error) are always
forwarded.
**--verbose**
: Be more verbose.
# OUTPUT
The standard output from *COMMAND* is printed on standard output.
The standard error from *COMMAND* is printed on standard error.
Diagnostic messages from **pressure-vessel-launch** may also be printed
on standard error.
# EXIT STATUS
The exit status is similar to **env**(1):
0
: The *COMMAND* exited successfully with status 0.
125
: Invalid arguments were given, or **pressure-vessel-launch** failed
to start.
126
: Reserved to indicate inability to launch the *COMMAND*.
This is not currently distinguished from exit status 125.
127
: Reserved to indicate that the *COMMAND* was not found.
This is not currently distinguished from exit status 125.
128
: The *COMMAND* was launched, but its exit status could not be
determined. This happens if the wait-status was neither
normal exit nor termination by a signal. It also happens if
**pressure-vessel-launch** was disconnected from the D-Bus
session bus or the **--socket** before the exit status could be
determined.
Any value less than 128
: The *COMMAND* exited unsuccessfully with the status indicated.
128 + *n*
: The *COMMAND* was killed by signal *n*.
# EXAMPLES
See **pressure-vessel-launcher**(1).
<!-- vim:set sw=4 sts=4 et: -->
---
title: pressure-vessel-launcher
section: 1
...
# NAME
pressure-vessel-launcher - server to launch processes in a container
# SYNOPSIS
**pressure-vessel-launcher**
[**--replace**]
[**--verbose**]
{**--bus-name** *NAME*|**--socket** *SOCKET*|**--socket-directory** *PATH*}
# DESCRIPTION
**pressure-vessel-launcher** listens on an `AF_UNIX` socket or the
D-Bus session bus, and executes arbitrary commands as subprocesses.
# OPTIONS
**--socket** *PATH*, **--socket** *@ABSTRACT*
: Listen on the given `AF_UNIX` socket, which can either be an
absolute path, or `@` followed by an arbitrary string. In both cases,
the length is limited to 107 bytes and the value must not contain
ASCII control characters.
An absolute path indicates a filesystem-based socket, which is
associated with the filesystem and can be shared between filesystem
namespaces by bind-mounting.
The path must not exist before **pressure-vessel-launcher** is run.
`@` indicates an an abstract socket, which is associated with a
network namespace, is shared between all containers that are in
the same network namespace, and cannot be shared across network
namespace boundaries.
**--socket-directory** *PATH*
: Create a filesystem-based socket with an arbitrary name in the
given directory and listen on that. Clients can use the information
printed on standard output to connect to the socket. Due to
limitations of `AF_UNIX` socket addresses, the absolute path to the
directory must be no more than 64 bytes. It must not contain ASCII
control characters.
**--bus-name** *NAME*
: Connect to the well-known D-Bus session bus, request the given name
and listen for commands there.
**--replace**
: When used with **--bus-name**, allow other
**pressure-vessel-launcher** processes to take over the bus name,
and exit with status 0 if that happens. This option is ignored
if **--bus-name** is not used.
**--verbose**
: Be more verbose.
# OUTPUT
**pressure-vessel-launcher** prints zero or more lines of
structured text on standard output, and then closes standard output:
**socket=**PATH
: The launcher is listening on *PATH*, and can be contacted by
**pressure-vessel-launch --socket** _PATH_
or by connecting a peer-to-peer D-Bus client to a D-Bus address
corresponding to _PATH_.
**dbus_address=**ADDRESS
: The launcher is listening on *ADDRESS*, and can be contacted by
**pressure-vessel-launch --dbus-address** _ADDRESS_,
or by connecting another peer-to-peer D-Bus client
(such as **dbus-send --peer=ADDRESS**) to _ADDRESS_.
**bus_name=**NAME
: The launcher is listening on the well-known D-Bus session bus,
and can be contacted by
**pressure-vessel-launch --bus-name** *NAME*,
or by connecting an ordinary D-Bus client
(such as **dbus-send --session**) to the session bus and sending
method calls to _NAME_.
Clients must wait until after standard output has been closed, or wait
for the bus name to appear, before connecting by bus name.
Clients must wait until after standard output has been closed before
connecting by socket name.
Unstructured diagnostic messages are printed on standard error,
which remains open throughout.
# EXIT STATUS
0
: Terminated gracefully by a signal, by being disconnected from the
D-Bus session bus after having obtained *NAME* (with **--bus-name**),
or by *NAME* being replaced by another process
(with **--bus-name** and **--replace**).
64 (**EX_USAGE** from **sysexits.h**)
: Invalid arguments were given.
Other nonzero values
: Startup failed.
# INTENDED USE IN STEAM
## Once per game launch
When a game is launched, run one **pressure-vessel-launcher** per game
(whatever that means in practice - probably one per app-ID) as an
asynchronous child process, wrapped in **pressure-vessel-wrap**
(either directly or by using **run-in-steamrt** or **run-in-scout**),
with the write end of a **pipe**(2) as its standard output.
Read from the read end of the pipe until EOF is reached,
then leave it running.
Steam should choose a **--socket**, **--socket-directory** or
**--bus-name** according to the granularity required.
If the **realpath**(3) of **~/.steam** is sufficiently short, then Steam
could use a subdirectory of that path as a **--socket-directory**,
but if the user's home directory is very long, that might not work
(the maximum length for a **--socket-directory** is only 64 bytes).
Otherwise, if Steam itself is in a container (for example if
**/.flatpak-info** exists), then it must choose a way to communicate
that can be shared with other containers and the host system. Using the
D-Bus session bus and **--bus-name** is probably the most reliable option
in this case.
If Steam is not in a container, then it can create a random subdirectory
of **/tmp**, for example with **mkdtemp**(3), and either use it as a
**--socket-directory** or create a **--socket** with a meaningful name
in that directory.
In the case of a **--socket-directory**, the socket can be determined by
parsing the data read from standard output.
When the game is to be terminated, Steam should send **SIGINT** or
**SIGTERM** to the child process (which by now will be the
**pressure-vessel-adverb** process that replaces **pressure-vessel-wrap**),
and to the process group that it leads (if any):
assert (child_pid > 1);
kill (child_pid, SIGTERM);
kill (-child_pid, SIGTERM);
It must not use **SIGKILL**, because that cannot be forwarded across
the IPC connection.
## Once per command
For each command that is to be run as part of the game, Steam must
run **pressure-vessel-launch**(1) with a **--socket**, **--dbus-address**
or **--bus-name** option that indicates how to communicate with the
launcher. Alternatively, it could do this itself by using D-Bus.
If Steam needs to set environment variables for the commands that are
run as part of the game, it can do so in one of two ways:
* If the environment variable is equally valid for all commands, it can
be part of the environment of **pressure-vessel-launcher**.
* If the environment variable is specific to one command, Steam can pass
**--env VAR=VALUE** to **pressure-vessel-launch**.
If the command is to be terminated without affecting other commands,
Steam should send **SIGINT** to **pressure-vessel-launch**.
It must not use **SIGKILL**, because that cannot be forwarded across the
IPC connection. It should not use **SIGTERM** unless the entire game is
being terminated, because that's only sent to a single process and not
to an entire process group.
# EXAMPLES
Listen on the session bus, and run two commands, and exit:
name="com.steampowered.PressureVessel.App${SteamAppId}"
pressure-vessel-wrap \
... \
-- \
pressure-vessel-launcher --bus-name "$name" &
launcher_pid="$!"
gdbus wait --session --bus-name "$name"
pressure-vessel-launch \
--bus-name "$name" \
-- \
ls -al
pressure-vessel-launch \
--bus-name "$name" \
-- \
id
kill -TERM "$launcher_pid"
wait "$launcher_pid"
Listen on a socket in a temporary directory:
tmpdir="$(mktemp -d)"
mkfifo "${tmpdir}/fifo"
pressure-vessel-wrap \
--filesystem="${tmpdir}" \
... \
-- \
pressure-vessel-launcher \
--socket="${tmpdir}/launcher" > "${tmpdir}/fifo" &
launcher_pid="$!"
# Wait for EOF so we know the socket is available
cat "${tmpdir}/fifo" > /dev/null
pressure-vessel-launch \
--socket="${tmpdir}/launcher" \
-- \
ls -al
pressure-vessel-launch \
--socket="${tmpdir}/launcher" \
-- \
id
kill -TERM "$launcher_pid"
wait "$launcher_pid"
rm -fr "$tmpdir"
<!-- vim:set sw=4 sts=4 et: -->
......@@ -33,7 +33,7 @@ else
pandoc_markdown_nosmart = 'markdown'
endif
foreach tool : ['locale-gen', 'try-setlocale']
foreach tool : ['launch', 'launcher', 'locale-gen', 'try-setlocale']
custom_target(
tool + '.1',
build_by_default : true,
......
......@@ -129,6 +129,19 @@ c_compiler = meson.get_compiler('c')
supported_warning_cflags = c_compiler.get_supported_arguments(warning_cflags)
add_global_arguments(supported_warning_cflags, language : 'c')
# gdbus-codegen in scout generates code that has many warnings
silence_gdbus_codegen_warnings = c_compiler.get_supported_arguments([
'-Wno-error',
'-Wno-error=discarded-qualifiers',
'-Wno-error=redundant-decls',
'-Wno-error=shadow',
'-Wno-error=write-strings',
'-Wno-discarded-qualifiers',
'-Wno-redundant-decls',
'-Wno-shadow',
'-Wno-write-strings',
])
foreach flag : no_warning_cflags
supported_no_warning_cflags = c_compiler.get_supported_arguments([
'-Wno-error=' + flag,
......@@ -142,6 +155,7 @@ add_project_arguments(supported_warning_cflags, language : 'c')
libglnx = subproject('libglnx')
threads = dependency('threads')
steam_runtime_tools = dependency(
'steam-runtime-tools-0',
version : '>=0.20200331.1',
......
<!DOCTYPE node PUBLIC
"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<!--
Copyright (C) 2018 Red Hat, Inc.
Copyright (C) 2020 Collabora Ltd.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
Based on org.freedesktop.portal.Flatpak.
Author: Alexander Larsson <alexl@redhat.com>
-->
<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
<!--
com.steampowered.PressureVessel.Launcher1:
@short_description: Interface for process-launching
Methods on this interface can be invoked by processes outside
the game's container (and in particular Steam), to start and
control programs inside the container.
This documentation describes version 0 of this interface.
-->
<interface name='com.steampowered.PressureVessel.Launcher1'>
<property name="Version" type="u" access="read"/>
<!--
SupportedLaunchFlags:
A bitmask containing all the flags that can be passed to
the Launch() method.
-->
<property name="SupportedLaunchFlags" type="u" access="read"/>
<!--
Launch:
@cwd_path: the working directory for the new process, resolved
in the container's filesystem namespace. The empty string means
use whatever is the launcher's current working directory.
@argv: the argv for the new process, starting with the executable to launch
@fds: an array of file descriptors to pass to the new process
@envs: an array of variable/value pairs for the environment of the new process
@flags: flags
@options: Vardict with optional further information
@pid: the PID of the new process inside pressure-vessel's container
Start a new program in the container.
The following flags values are supported:
<variablelist>
<varlistentry>
<term>1</term>
<listitem><para>
Clear the environment. If set, the environment will be
exactly @envs, similar to `env - X=1 Y=2 COMMAND`.
If unset, @envs are used to override environment variables
and any environment variable that is not overridden is
left unchanged, similar to `env X=1 Y=2 COMMAND`.
</para></listitem>
</varlistentry>
</variablelist>
Unknown (unsupported) flags are an error and will cause Launch()
to fail.
No options are currently known. Unknown (unsupported) options
are ignored.
This method returns as soon as the process ID of the new process
is known, and before the process exits. If you need to know when
the process exits, subscribe to the ProcessExited signal before
calling this method, and use the returned process ID to correlate
signals with method calls.
-->
<method name="Launch">
<annotation name="org.gtk.GDBus.C.UnixFD" value="true"/>
<arg type='ay' name='cwd_path' direction='in'/>
<arg type='aay' name='argv' direction='in'/>
<arg type='a{uh}' name='fds' direction='in'/>
<arg type='a{ss}' name='envs' direction='in'/>
<arg type='u' name='flags' direction='in'/>
<arg type="a{sv}" name="options" direction="in"/>
<arg type='u' name='pid' direction='out'/>
</method>
<!--
SendSignal:
@pid: the PID inside the container to signal
@signal: the signal to send (see `signal(7)`)
@to_process_group: whether to send the signal to the process group
This methods let you send a Unix signal to a process
that was started with com.steampowered.PressureVessel.Launcher1.Launch().
The @pid argument here should be the PID that is returned
by the Launch() call: it is not necessarily valid in the caller's
PID namespace.
-->
<method name="SendSignal">
<arg type='u' name='pid' direction='in'/>
<arg type='u' name='signal' direction='in'/>
<arg type='b' name='to_process_group' direction='in'/>
</method>
<!--
ProcessExited:
@pid: the PID of the process that has ended
@wait_status: the wait status (see waitpid(2))
Emitted when a process started by
com.steampowered.PressureVessel.Launcher1.Launch() exits.
Use g_spawn_check_exit_status(), or the macros such as
`WIFEXITED` documented in `waitpid(2)`, to interpret
the @wait_status.
This signal is not emitted for processes that were not launched
directly by Launch(), for example if a process launched by
Launch() runs foreground or background child processes.
-->
<signal name="ProcessExited">
<arg type='u' name='pid'/>
<arg type='u' name='wait_status'/>
</signal>
</interface>
</node>
This diff is collapsed.
This diff is collapsed.
/*
* pressure-vessel-launcher — accept IPC requests to create child processes
*
* Copyright © 2020 Collabora Ltd.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <sys/socket.h>
#include <sys/un.h>
#include <glib.h>
#include <gio/gio.h>
#include "glib-backports.h"
#include "libglnx/libglnx.h"
#include "launcher1.h"
#define DBUS_NAME_DBUS "org.freedesktop.DBus"
#define DBUS_INTERFACE_DBUS DBUS_NAME_DBUS
#define DBUS_PATH_DBUS "/org/freedesktop/DBus"
#define LAUNCHER_IFACE "com.steampowered.PressureVessel.Launcher1"
#define LAUNCHER_PATH "/com/steampowered/PressureVessel/Launcher1"
typedef enum
{
PV_LAUNCH_FLAGS_CLEAR_ENV = (1 << 0),
PV_LAUNCH_FLAGS_NONE = 0,
PV_LAUNCH_FLAGS_MASK = (
PV_LAUNCH_FLAGS_CLEAR_ENV |
PV_LAUNCH_FLAGS_NONE
),
} PvLaunchFlags;
#if !GLIB_CHECK_VERSION (2, 47, 92)
G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvLauncher1Skeleton, g_object_unref)
#endif
/* Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx */
#define PV_UUID_STRLEN 36
#define PV_MAX_SOCKET_DIRECTORY_LEN 64
/* If ${socket_directory} is no longer than PV_MAX_SOCKET_DIRECTORY_LEN,
* then struct sockaddr_un.sun_path is long enough to contain
* "${socket_directory}/${uuid}\0" */
G_STATIC_ASSERT (sizeof (struct sockaddr_un) >=
(G_STRUCT_OFFSET (struct sockaddr_un, sun_path) +
PV_MAX_SOCKET_DIRECTORY_LEN +
PV_UUID_STRLEN +
2));
/* Chosen to be similar to env(1) */
enum
{
LAUNCH_EX_USAGE = 125,
LAUNCH_EX_FAILED = 125,
LAUNCH_EX_CANNOT_INVOKE = 126,
LAUNCH_EX_NOT_FOUND = 127,
LAUNCH_EX_CANNOT_REPORT = 128
};
......@@ -27,6 +27,13 @@ headers = [
'runtime.h',
]
launcher1 = gnome.gdbus_codegen(
'launcher1',
sources : 'com.steampowered.PressureVessel.Launcher1.xml',
interface_prefix : 'com.steampowered.PressureVessel.',
namespace : 'Pv',
)
enums = gnome.mkenums_simple(
'enumtypes',
sources : headers,
......@@ -59,6 +66,17 @@ pressure_vessel_utils = static_library(
install: false,
)
launcher_codegen = static_library(
'pressure-vessel-launcher-codegen',
sources : launcher1,
dependencies : [
gio_unix,
],
c_args : silence_gdbus_codegen_warnings,
include_directories : project_include_dirs,
install: false,
)
executable(
'pressure-vessel-adverb',
sources : [
......@@ -78,6 +96,47 @@ executable(
install_rpath : '${ORIGIN}/../' + get_option('libdir'),
)
executable(
'pressure-vessel-launch',
sources: [
'launch.c',
],
dependencies : [
threads,
gio_unix,
libglnx.get_variable('libglnx_dep'),
],
link_with : [
pressure_vessel_utils,
],
include_directories : project_include_dirs,
install : true,
install_dir : get_option('bindir'),
build_rpath : '${ORIGIN}/../' + get_option('libdir'),
install_rpath : '${ORIGIN}/../' + get_option('libdir'),
)
executable(
'pressure-vessel-launcher',
sources: [
'launcher.c',
],
dependencies : [
threads,
gio_unix,
libglnx.get_variable('libglnx_dep'),
],
link_with : [
launcher_codegen,
pressure_vessel_utils,
],
include_directories : project_include_dirs,
install : true,
install_dir : get_option('bindir'),
build_rpath : '${ORIGIN}/../' + get_option('libdir'),
install_rpath : '${ORIGIN}/../' + get_option('libdir'),
)
executable(
'pressure-vessel-wrap',
sources : [
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment