process.h
Other processes: launching one, a lock shared between processes, a library loaded at run time, and objects deleted at shutdown.
#include <sonore/process.h>- 4types
- 2other declarations
- Guides: Platforms and formats
On this page
Design notes
ChildProcess run a program with arguments -- never through a
shell -- read its output, wait, kill
InterProcessLock a named lock every process on the machine (well, in
this user's session) sees
DynamicLibrary LoadLibrary / dlopen as an object
DeletedAtShutdown objects a program deletes together at the end
currentExecutablePath(), currentModulePath()
A plugin that spawns processes
is something users find in a firewall log or a task manager and do not forgive, and the reason the feature map called this out of scope for so long. It is here because there are honest reasons for it -- an out-of- process scanner, a helper that does work a host would crash on, the standalone running a converter -- and because doing it WRONG is worse than not doing it: a child that inherits every handle the DAW has open, a command line pasted into a shell, a process left running after the plugin was removed from the track. So:
NO SHELL. Arguments go to CreateProcessW / posix_spawn as a list. A file name with a space, a quote or a semicolon in it is one argument, not a command. On Windows the list is quoted by the rules the C runtime's own argv parser undoes (CommandLineToArgvW agrees; the tests check the round trip), because Windows has no argv at the system level. A .bat or .cmd is refused on Windows: the system runs it through cmd.exe, which does not split a command line by those rules.
NOTHING INHERITED but stdin/stdout/stderr: on Windows the inheritable handles are restricted to exactly those three with PROC_THREAD_ATTRIBUTE_HANDLE_LIST (without it, every inheritable handle in the host -- some other plugin's pipe, a file it has open -- leaks into the child and keeps it alive); on POSIX every descriptor is close-on-exec and macOS is additionally told POSIX_SPAWN_CLOEXEC_DEFAULT. stdin is the null device, never the host's. The child's signal mask and dispositions are reset: a host that blocks SIGPIPE must not hand that to a converter.
THE CHILD DIES WITH THE OBJECT. The destructor kills a child that is still running. A plugin removed from a track must not leave a process behind.
OUTPUT NEVER STALLS THE CHILD. stdout and stderr are drained by a thread each from the moment the child starts, so a child that writes more than a pipe holds (64 KB on Linux, 4 KB on some systems) never blocks waiting for a reader that is busy in waitForProcessToFinish -- the classic deadlock of reading a child's output after waiting for it.
Threads
Everything here blocks or does I/O. Never on the audio thread.
Types 4
A program started with arguments, its output captured.
ChildProcess p;
if (p.start({"/usr/bin/flac", "-d", path})) {
p.waitForProcessToFinish(60000);
if (p.getExitCode() != 0) report(p.getStandardError());
}
The first argument is the program: a path, or a bare name looked up the way the platform looks one up (PATH; on Windows also the system folders, with ".exe" implied). See the header for what the child does and does not inherit, and why the destructor kills it.
A lock with a name, held by one process (one holder) at a time.
InterProcessLock lock("com.vendor.plugin.cache");
InterProcessLock::ScopedLockType hold(lock);
if (hold.isLocked()) rewriteTheSharedCache();
The scope is the user's session: a named mutex in Windows' Local\ namespace, and an flock() on a file in the user's temporary folder elsewhere -- so two plugin instances in two hosts, or a plugin and its standalone, exclude each other, and two users on one machine do not.
enter() nests within one object (each needs its exit()). A process that dies holding the lock releases it: the system does that for both kinds. On Windows the lock belongs to the THREAD that entered it, and only that thread can exit it.
Two objects with ONE name in one process are two holders, and the systems part ways there: on Windows a thread that holds one gets the other at once (the named mutex counts per thread), and elsewhere each excludes the other even on one thread, so entering the second from the thread that holds the first waits out its time-out. Within a process, share one object rather than nest two.
A library opened at run time: how every optional backend in this SDK is reached (JACK, WebKitGTK, libcurl, the ASIO drivers), as an object. The name is a UTF-8 path, or a bare name the system's loader searches for. A full path on Windows has its own folder searched for ITS dependencies (LOAD_WITH_ALTERED_SEARCH_PATH), which is what a plugin loading a library it ships beside itself needs.
Closing a library whose functions are still in use is a crash; keep the object alive as long as anything might call into it.
A base class for objects that live until the program ends and are deleted together then -- caches, registries, a shared font -- by one call to deleteAll() at shutdown, newest first.
Who calls deleteAll(): the standalone's main() after its loop, or a plugin as its module unloads (its entry point's exit/terminate function). Not from a static destructor: by then the objects' own dependencies may already be gone. An object deleted earlier by its owner simply leaves the list.
Objects created while deleteAll() runs (one destructor making another object) are deleted too; the list is emptied until it stays empty.
Functions 2
- fn
inline std::string currentExecutablePath()The running program's own executable -- for a plugin, the HOST's.process.h:232 - fn
inline std::string currentModulePath()The binary this code is in: the plugin's .dll / .so / bundle executable when called from a plugin, the program otherwise. Where a plugin finds a helper it ships beside itself.process.h:263