Before you start#
Read D-Bus messages and services and Wayland objects and wire messages. This lesson uses GLib 2.82.2 and wayland-protocols 1.36 as separately pinned source baselines. The practice parses a private file without launching an application or contacting a bus.
Learning goals#
Resolve a desktop entry, explain how its executable arguments differ from shell commands, and trace GLib's conditional D-Bus and spawn paths. Distinguish an activation request, a running primary instance and permission to focus a window.
Source walkthrough#
- Desktop entry
Resolve the selected file and validate its fields
- Activation choice
Use D-Bus activation or construct the executable arguments
- Application
A primary instance receives the request or a process starts
- Result
Report errors and handle focus policy independently
A desktop entry is a structured text file used to describe an application or another desktop object. The [Desktop Entry] group contains keys such as Type, Name, Exec, Icon and optionally DBusActivatable. Values follow desktop-entry escaping and quoting rules, not arbitrary shell syntax. The desktop-entry specification describes accepted types, locale-specific keys, field codes, actions and validation. A desktop environment can display an entry in a menu, associate it with MIME types or use it for another defined purpose; merely existing on disk does not mean the application is installed, trusted or already running. See the Desktop Entry Specification and GLib's desktop entry implementation.
XDG data locations provide a preference order: $XDG_DATA_HOME is considered before entries beneath $XDG_DATA_DIRS. For an application entry, its desktop-file ID is computed relative to the particular data directory: remove the applications/ prefix, remove .desktop, then replace / with -. Thus applications/vendor/editor.desktop has ID vendor-editor. A higher-priority file with the same ID can shadow the lower-priority one. A menu entry is therefore not necessarily the first file with the same display name found by a filesystem search. Hidden=true marks an entry deleted in the relevant lookup context; NoDisplay=true keeps an entry out of menus while allowing other uses. See the XDG Base Directory Specification and the Desktop Entry Specification.
The Exec field describes an executable and arguments with desktop-entry field codes. %f and %u represent a file and a URI respectively, while %F and %U represent lists under their defined rules. %i, %c and %k have their own substitutions and constraints. Expansion and quoting must preserve argument boundaries; a file name with a space should not silently become two unrelated arguments. A launcher must follow the specification and its implementation rather than concatenate untrusted filenames into a command line to be interpreted by a shell. Shell metacharacters do not become pipelines or redirections simply because they appear in an entry. An entry can explicitly invoke a shell as its executable, but that is a different and potentially dangerous choice.
- %f
- One file path per launch, even when several files are selected.
- %u
- One URI per launch, for an entry that takes a single URI.
- %F
- Zero or more file paths, each passed as its own argument.
- %U
- Zero or more URIs, each passed as its own argument.
Expansion and spawn
GLib's GDesktopAppInfo represents a parsed desktop entry. Its launch implementation can expand fields, construct an argument vector, and spawn a process. The vector is an array of strings passed to a process-creation interface; it is not a single shell command. A working directory, environment and standard streams have their own lifetime and error handling. A launch context can supply platform data and receive information about startup. The caller must check errors and retain objects or buffers for as long as the relevant asynchronous API requires; a returned success from one stage is not proof that the application initialized or displayed a window.
From GLib 2.82.2 gio/gdesktopappinfo.c, expand_application_parameters, LGPL-2.1-or-later as stated in the file; the field expansion loop and return are omitted:
res = g_shell_parse_argv (expanded_exec->str, argc, argv, error);
g_string_free (expanded_exec, TRUE);
The first statement parses the expanded text into an argument vector or reports an error. Despite its name, this parser does not execute a shell. The second frees the temporary string after parsing; the returned vector has its own ownership and must be released by its caller. Earlier in the same function a missing Exec produces a G_IO_ERROR_FAILED and returns false. If a field expansion or quoting is invalid, a launch must fail or use the implementation's defined error behavior rather than treating the raw text as trusted shell input. GLib also has implementation-specific behavior for URI lists and field substitutions, so do not generalize every detail to every launcher.
TryExec is a check on whether a named executable is available, commonly used to decide whether an entry should be shown. It is not a guarantee that the eventual process will start: the file can disappear, permissions can change, a required library can be missing, or the executable can fail. Terminal=true requests terminal handling according to the desktop implementation; it does not mean that arbitrary terminal input is safe. Categories support menu organization. MimeType describes supported types, and the user's MIME association configuration chooses a handler; an association is not a grant of authority to read every file. Desktop actions provide named alternate commands with their own fields, rather than changing the semantics of the main entry's executable.
Autostart uses a related desktop-entry format and separate search locations and precedence rules. A file in an autostart directory can cause a session component to request execution during startup, subject to implementation and policy. A user-writable entry is not authenticated software merely because it has a familiar icon or application name. This chapter's exercise writes only into a temporary directory and never puts a file into a real applications or autostart directory. The Desktop Application Autostart Specification defines the format and placement rules; desktop-specific condition keys are not universal.
D-Bus activation and an existing instance
A desktop entry can declare DBusActivatable=true. For a conforming entry, the desktop-file ID and D-Bus application naming rules connect the launcher to a well-known bus name. A well-known name is an address that can have an owner; an individual connection also has a unique name assigned by the bus. A primary application instance may own the well-known name and receive org.freedesktop.Application requests. Another invocation can become a remote instance that forwards activation, open or command-line requests, depending on application flags and platform support. The bus can activate an appropriate service if configured; not every desktop entry, application or session bus uses this route.
GLib's GApplication manages registration and distinguishes a primary instance from a remote one. Activate requests normal activation without file arguments; Open carries resources; command-line handling has different semantics and application flags. Platform data can convey information relevant to startup and presentation. A bus connection, well-known name, process and window are separate lifetimes: the primary process can exist without a visible window, and a bus request can be accepted before an application creates one. If a bus connection closes, its unique name and owned names are lost; clients must treat pending requests and replies as failed or cancelled according to their APIs. See GLib gio/gapplication.c.
From expand_application_parameters in GLib 2.82.2 gio/gdesktopappinfo.c, LGPL-2.1-or-later, with the following expansion path omitted:
if (exec_line == NULL)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Desktop file didn’t specify Exec field"));
return FALSE;
}
The condition detects an absent executable field. The error creation records a specific failure for the caller, and the final return stops this expansion path before an argument vector can be constructed. This is relevant when GLib chooses spawning; an entry handled by D-Bus activation has different requirements and should not be assumed to execute this branch.
The separately inspected g_desktop_app_info_launch_uris_internal tests whether a session bus and application ID are available. It uses the asynchronous D-Bus path when both are present and otherwise invokes its spawn path. This is not a claim that any failed asynchronous bus activation automatically retries Exec. The shown branch chooses spawn when its preconditions for the bus path are absent; once a bus request is made, a later error must be handled according to the asynchronous operation. The surrounding code flushes and unreferences the bus connection, with a reference held for the asynchronous flush. A caller must distinguish local dispatch success, bus reply, service startup and actual application readiness.
The branch structure is visible in gio/gdesktopappinfo.c, GLib 2.82.2, SPDX LGPL-2.1-or-later (surrounding code omitted):
session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
if (session_bus && info->app_id)
/* This is non-blocking API. Similar to launching via fork()/exec()
* we don't wait around to see if the program crashed during startup.
* This is what startup-notification's job is...
*/
g_desktop_app_info_launch_uris_with_dbus (info, session_bus, uris, launch_context,
NULL, NULL, NULL);
else
success = g_desktop_app_info_launch_uris_with_spawn (info, session_bus, info->exec, uris, launch_context,
spawn_flags, user_setup, user_setup_data,
pid_callback, pid_callback_data,
stdin_fd, stdout_fd, stderr_fd, error);
The bus path is taken only when a session bus connection and info->app_id both exist; otherwise the spawn helper receives info->exec. The bus helper is called with NULL for its cancellable, callback and user data, so this branch does not return a later reply, a launch error or evidence that an application started.
- Session bus
g_bus_get_sync returns a connection or NULL; NULL leads to the spawn branch.
- Application ID
The bus branch also requires info->app_id, otherwise spawn is chosen.
- D-Bus branch
launch_uris_with_dbus makes an asynchronous request with NULL callbacks.
- Spawn branch
Uses info->exec; a missing Exec is reported as a failure by the expansion path.
- Flush and release
An asynchronous flush holds its own reference before the connection is unreferenced.
Worked entry and activation choice
Consider this fictional entry in a private file: Type=Application, Name=Notes, Exec=notes --open %f, and DBusActivatable=false. A launcher receives one path /tmp/lesson/my notes.txt. Under the appropriate desktop-entry parsing and field expansion rules, the intended argument vector contains executable notes, argument --open, and one path argument containing the space. The path is data, not two arguments and not shell syntax. Whether notes exists is a separate question; in this lesson no such process is run. If notes is missing, a spawn attempt would report an error rather than create a window.
Now consider a separate fictional entry with a valid application ID and DBusActivatable=true. If the launcher's GLib path obtains a session bus and identifies an application ID, the cited branch initiates D-Bus activation or forwarding. If the necessary bus precondition is absent, the cited implementation takes its spawn branch, provided there is a usable Exec. If the bus exists but rejects an asynchronous activation later, the excerpt does not establish an automatic spawn fallback. A synthetic result table can therefore say: no bus plus valid Exec selects spawn; bus plus ID selects D-Bus; absent Exec on the spawn path yields an error. It cannot say that a window appeared or that every launcher uses GLib.
Focus and startup context
Launching an application is not the same as granting it permission to take focus. In an X11 environment, startup notification conventions can associate startup feedback with a launch and help a window manager apply policy. On Wayland, the optional xdg-activation protocol allows a client to request an activation token and later provide it in an activation request. A token can carry context such as an input serial and seat under the protocol; the compositor decides whether the request is valid and whether to activate the target. A token is neither a general-purpose secret granting arbitrary access nor proof that the compositor will raise a surface. The exact behavior depends on protocol version and compositor policy; see xdg-activation 1.36.
Environment variables can carry launch context, but they are process data with inheritance and lifetime rules, not a durable global authority. The launching process should avoid leaking descriptors and sensitive environment data to children. In a D-Bus method that passes file descriptors, the descriptor is a reference transferred through the bus's supported Unix-fd mechanism, not a path string or a copy of file bytes. Ownership after transfer is specified by the particular interface and binding. A desktop entry's URI argument alone does not imply that a sandboxed application has access to its backing file; a portal or other authorization mechanism may be needed.
A race can occur when an application exits just as a second launch request arrives. The bus may report a name-owner change or activation failure, while a launcher has already returned from an asynchronous call. A robust caller handles the completion and associated error, avoids indefinitely retaining launch context or URI data, and does not assume a unique name identifies a future replacement process. Service activation policy, timeout and sandbox permissions are configuration-specific. Retrying blindly can create duplicate work if the original request reached the application but its reply was lost; the application protocol determines whether an operation is safe to repeat.
Multiple files and application instances
A launcher must also account for how many resources a field code can represent. A singular file code is not interchangeable with a list code: a launcher may need to start or activate an application more than once when an entry accepts one file at a time. Whether an existing primary instance handles those requests in one process depends on application registration and flags. The number of desktop launch requests therefore does not determine the number of running processes or open windows. An application can receive several open requests while it is still initializing, and its own state machine must retain or reject each request without losing references to the URI data.
Consider two private paths, one containing a space and one containing a literal percent sign. Field-code expansion happens according to the entry grammar, not by scanning the substituted filename again as if it were part of the original Exec template. Treating substituted data as fresh template or shell syntax can change arguments or create an injection bug. A defensive implementation uses the documented parser and vector API, preserves the supplied argument as data, and reports parse or spawn errors. It does not infer trust from a display name, icon or matching MIME association.
A process can also accept an activation request but fail while opening the selected file. The launcher's bus reply and the application's later file error belong to different operations. A launcher that merely displayed startup feedback should not claim the document was successfully loaded. Applications that want stronger end-to-end feedback need an interface that explicitly reports the relevant completion and error, with clear timeout and retry semantics. Those semantics are not supplied automatically by a desktop entry.
Practice#
Use Python's standard library to parse a private desktop-like file. This examines values only: it does not implement desktop-entry field expansion, launch a program, or connect to D-Bus.
python3 - <<'PY'
import configparser, pathlib, tempfile
with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d) / 'example.desktop'
p.write_text('[Desktop Entry]\nType=Application\nName=Notes\nExec=notes --open %f\nDBusActivatable=false\n')
c = configparser.ConfigParser(interpolation=None)
c.read(p)
print('type:', c['Desktop Entry']['Type'])
print('exec field:', c['Desktop Entry']['Exec'])
print('bus activation:', c['Desktop Entry']['DBusActivatable'])
PY
Expected values are Application, the literal notes --open %f, and false. Python's generic INI parser is not a complete desktop-entry validator and does not prove how GLib expands field codes. The supplied source-only alternative is the fictional argument vector above: list its three arguments and explain why the third contains a space. For the failure case, remove the Exec field and compare the result with GLib's cited missing-field branch; do not launch anything. Deliver the parsed fields, argument table and conditional bus/spawn table.
Knowledge check#
- Why is
Execnot equivalent to passing its value to a shell? - What does
TryExecestablish, and what does it not establish? - When does the cited GLib branch choose its D-Bus path?
- Does an asynchronous bus error prove automatic fallback to spawn?
- Does a Wayland activation token guarantee focus?
Answers#
- Desktop-entry syntax defines argument and field-code parsing; GLib builds an argument vector. A shell is executed only if explicitly chosen as the executable.
- It provides an availability check used by a launcher; the process can still fail later because of permissions, missing dependencies or races.
- The shown implementation tests that a session bus connection and application ID are available. Other launchers and versions can differ.
- No. The excerpt shows an alternative when the bus precondition is absent, not a retry after every later asynchronous failure.
- No. It conveys activation context to a compositor, which applies its own protocol and focus policy.
Checkpoint#
Submit the three-argument example and the bus/spawn decision table. Pass when the filename remains one argument, missing Exec has an error path, and a bus request is not labeled a displayed window. If not, reread the two source excerpts and repeat the private parse. This is practice, not certification.
Sources#
Consult GLib desktop application implementation, GLib application implementation, xdg-activation protocol, and the Desktop Entry Specification. The example did not execute a desktop entry or contact a session bus.