API — ICoreEssentials/Process
The public contract of 5 header(s) under ICoreEssentials/Process — 4 class/struct definition(s), 50 declaration(s). Each section shows the header's banner and its public (and protected-virtual) surface exactly as the file writes it.
| Header | Defines | Declarations | Bases |
|---|---|---|---|
ICoreProcess.h | ICoreProcess | 24 | — |
ICoreProcessDispatcher.h | ICoreProcessDispatcher | 4 | — |
ICoreProcessEnvironment.h | ICoreProcessEnvironment | 6 | — |
ICoreProcessOsSupport.h | — | 0 | — |
ICorePty.h | ICorePty | 16 | — |
ICoreProcess.h#
ICoreEssentials/Process/ICoreProcess.h
ICoreProcess -- run a child program: start it, write to it, read what it printed, and learn how it ended.
One object owns one child. Working directory, environment and channel mode are set before start(); after it, output can be taken synchronously with readAllStandardOutput() / readAllStandardError() (or readAll(), if the channels were merged), or waited for with waitForFinished(). A caller that must not block registers onOutputReady() / onFinished() / onFailedToStart() instead and is called back as the child runs; clearCallbacks() stops delivery permanently, which is what an owner abandoning a child wants. terminate() asks the child to stop and kill() does not ask; terminateThenKill() is the usual pair with a grace period between them.
Two implementations of this class exist and no caller can tell them apart:
ICoreProcess#
ICoreProcess.h:178 · class · pImpl · 24 declaration(s)
ICoreProcess -- a child process: start it, read what it printed, learn how it ended.
class ICoreProcess {
public:
// How a finished child ended. See the design note on why this is declared
// here rather than aliased to QProcess::ExitStatus.
enum class ExitStatus { Normal, Crash };
ICoreProcess();
// Deletes the child object unless deleteLater() already released it. Does
// NOT kill a running child -- neither did the QProcess this replaces, and
// the two owners that care take it down explicitly first.
~ICoreProcess();
// Owns an OS resource; copying one is meaningless and no call site moves
// one, so both are deleted rather than defined.
ICoreProcess(const ICoreProcess&) = delete;
ICoreProcess& operator=(const ICoreProcess&) = delete;
// --- setup, all before start() ------------------------------------------
void setWorkingDirectory(const ICoreString& dir);
// Takes the snapshot of the parent's environment that ICoreProcessEnvironment
// deliberately no longer takes for itself -- it went Qt-free and now records
// only the overrides, in insert order. Replaying them here is what keeps
// <QProcessEnvironment> inside this wrapper, which still owns a QProcess and
// so has to speak Qt anyway; see that wrapper's header note. (The include
// now sits in the .cpp, which is where the replay does.)
void setEnvironment(const ICoreProcessEnvironment& env);
// stderr folded into stdout. Named for the one mode the project uses --
// see the design note.
void setMergedChannels();
// --- running ------------------------------------------------------------
void start(const ICoreString& program, const ICoreStringList& arguments);
// Fire-and-forget: starts a child that outlives this process and is never
// waited on. Static because there is nothing left to hold afterwards.
static bool startDetached(const ICoreString& program, const ICoreStringList& arguments);
// Blocks. Returns false on timeout, which both synchronous call sites treat
// as "the tool hung" and follow with kill().
bool waitForFinished(int msecs);
void terminate(); // polite: SIGTERM
void kill(); // not polite: SIGKILL
long long write(const ICoreByteArray& data);
// --- results ------------------------------------------------------------
bool isRunning() const;
int exitCode() const;
ExitStatus exitStatus() const;
// ⚠ THESE THREE RETURNED QByteArray UNTIL A9.11. A Qt type in a RETURN
// position is a link blocker (ui.md's triage rule), so they could not be
// defined outside the Qt zone at all. The fix was the §0.28 question
// rather than a narrower type: all three call sites in the tree --
// ICoreGitClient.cpp:69, ICoreCRuntime.cpp:64 and :243 -- immediately
// wrap the result in ICoreString::fromUtf8(), so NOT ONE OF THEM WANTED
// A QByteArray. Counting them was the whole of the decision.
ICoreByteArray readAll();
ICoreByteArray readAllStandardOutput();
ICoreByteArray readAllStandardError();
ICoreString errorString() const;
// --- lifetime -----------------------------------------------------------
// Hands the child to the event loop and releases it. See the design note --
// this is what makes destroying the wrapper from inside a signal handler
// safe, and it is why the call sites that settle asynchronously call it.
void deleteLater();
// --- the connect seam: CLOSED (A9.11, 2026-08-22) -----------------------
//
// `QProcess* qt() const noexcept` stood here, handed out for connect(),
// disconnect() and QPointer only. The design note above calls every call of
// it "an honest marker of remaining Qt coupling" and says grepping for it
// measures what phase 2 owes -- phase 2 has landed, the callbacks below
// replaced all three uses, and that count reached zero.
//
// Removed rather than left declared-and-unused. A member nobody calls still
// names QProcess in a portable header, and the native seat could not have
// defined it at all: A9.3's declared-and-undefined rule buys a seat time,
// it does not make the name free.
// --- asynchronous results (PHASE 2) -------------------------------------
//
// The callback registration the design note above chartered to phase 2 and
// deliberately left out of phase 1. It is here now because it is the last
// thing standing between three service classes and a Qt-free spelling:
// without it they cannot learn that a child printed or exited except
// through qt(), and so had to keep a QObject base purely to be a connect
// context.
//
// DELIVERY IS UNCHANGED, which was phase 1's stated worry. Each handler is
// connected with the CHILD OBJECT ITSELF as the context, and a QProcess
// emits on the thread it lives in -- so the connection is direct and the
// handler runs synchronously inside the emission, exactly as it did when
// the call sites passed their own same-thread object. What changes is only
// who owns the subscription: this wrapper, which is also who owns the
// child, instead of a separate object that had to remember to disconnect.
//
// Registering twice replaces the previous handler rather than adding a
// second one -- these are callbacks, not a signal, and every call site
// wants exactly one.
//
// =====================================================================
// ⚠⚠ A HANDLER MAY RUN BEFORE THE CALL THAT CAUSED IT HAS RETURNED.
// DELIVERY IS NOT GUARANTEED TO BE DEFERRED, AND onFailedToStart IS
// ROUTINELY DELIVERED FROM INSIDE start().
// =====================================================================
//
// ⚖ OWNER RULING, 2026-08-28 -- Linux backend plan L9.19 / §L106.1. This
// is the CONTRACT, not an implementation detail a later seat may change:
// start() reports a failure to launch by posting it, the dispatcher runs
// posted work inline, and so your onFailedToStart handler executes while
// start() is still on the stack. The two alternatives were considered and
// declined with reasons on §L106.1 -- a drain API a hookless program must
// call (a program that never drains then never gets callbacks, which is
// the very failure this was opened over), and deferring to a reader
// thread which, for a child that never started, was never created.
//
// WHAT THIS MEANS FOR YOU, in one line: do not write anything after
// start() that assumes your handler has not run yet.
//
// ⚠⚠ AND IF YOUR HANDLER DESTROYS THIS WRAPPER, USE clearCallbacks() +
// deleteLater() -- NEVER a direct delete or a unique_ptr::reset() of
// a live child. Destroying the object whose notification you are
// inside leaves start() running on a dangling `this`. deleteLater()
// (L9.18) releases the CHILD later, so the reset() that follows frees
// only the wrapper and the seat below never touches it again.
// ICoreGitClient::run() is the worked example and its comments say so.
//
// 📌 THE SEATS HOLD ONE INVARIANT THAT MAKES THIS SURVIVABLE, AND IT IS
// LOAD-BEARING RATHER THAN INCIDENTAL: every start() in every backend
// touches NOTHING -- no member, no impl, no capture -- after the point
// at which it may deliver. Each failure path is `fail(...); return;`
// and the `return` is the whole of it. Verified on all three seats
// when this ruling landed (POSIX, Native/Windows, Qt). A seat that
// adds a line after its delivery point breaks this contract silently,
// in a call site's destructor, on someone else's machine -- so the
// same paragraph is repeated at each seat's fail() rather than left
// here where a seat author would not read it.
//
// ⚠ THE COST IS RE-ENTRANCY AND THIS TREE HAS PAID IT THREE TIMES
// ALREADY -- L8.7 (ICoreLineEdit::setText called from inside its own
// change notification: a SIGSEGV inside harfbuzz), L8.8 and L8.9 are
// all "a callback that runs while the object that raised it is
// mid-call". Permitting it and saying so is the whole of the defence:
// a contract that permits re-entrancy and does not mention it is the
// same defect as one that forbids it and does not enforce it.
// The child wrote to stdout (stderr too, under setMergedChannels).
void onOutputReady(std::function<void()> fn);
// The child ended. Never fires when it failed to START -- that is
// onFailedToStart, and a call site that must settle exactly once has to
// handle both, as the terminal session's `settled` flag does.
void onFinished(std::function<void(int exitCode, ExitStatus status)> fn);
// The child could not be launched at all -- and see the delivery contract
// above: THIS ONE IS ROUTINELY CALLED FROM INSIDE start(), before start()
// has returned (⚖ L9.19, §L106.1). onFinished never follows it.
void onFailedToStart(std::function<void()> fn);
// Stop delivering, permanently. This is what the call sites' old
// `qt()->disconnect(this)` meant: an owner that is abandoning a child, or
// settling a command, and must not be called back afterwards. Safe to call
// from inside a handler.
void clearCallbacks();
// Ask the child to stop, and kill it if it has not gone within `graceMs`.
//
// Folded in here because doing it correctly needs a weak handle to the
// child -- a raw one could be matched by a LATER child landing on the same
// address, and the delayed kill would then take down the wrong command.
// Inside the wrapper there is only ever one child, so the guarded pointer
// below cannot confuse two; a caller holding this by value gets that for
// free. The kill is dropped if the child ends, or this wrapper dies, first.
void terminateThenKill(int graceMs);
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreProcessDispatcher.h#
ICoreEssentials/Process/ICoreProcessDispatcher.h
ICoreProcessDispatcher#
ICoreProcessDispatcher.h:44 · class · 4 declaration(s)
ICoreProcessDispatcher -- the Process tier's own way onto the thread that owns its callbacks.
class ICoreProcessDispatcher {
public:
ICoreProcessDispatcher() = delete;
// The hop. Takes the callable the Process tier wants run on the owning
// thread; the installed hook decides when and where.
using Hook = std::function<void(std::function<void()>)>;
// Install the hop. Called once, early, by whoever owns the loop. Passing a
// null Hook restores the inline default -- which is what a test tearing
// down its own loop should do, since a hook that outlives its loop is
// worse than none.
static void installHook(Hook hook);
// Run `fn` on the owning thread. Safe to call from any thread.
static void post(std::function<void()> fn);
// Whether a hook is installed. For diagnostics and for tests that want to
// assert their own wiring rather than trust it.
[[nodiscard]] static bool hasHook() noexcept;
};
};
ICoreProcessEnvironment.h#
ICoreEssentials/Process/ICoreProcessEnvironment.h
ICoreProcessEnvironment -- the environment to start a child process with.
QT-FREE (phase 3). This header names no Qt type. <QProcessEnvironment> does not leave the layer, though -- it MOVES to ICoreProcess.h, which still owns a QProcess and so still has to speak Qt to hand it an environment. Read that as the honest accounting it is: this wrapper is done, the umbrella's include count is unchanged, and the line finally goes when ICoreProcess itself does.
WHAT THIS CLASS IS, and it is less than its name suggests. Its entire surface is a static named systemEnvironment() and an insert(). Nothing reads a value back, nothing removes one, nothing enumerates. Both call sites are the same three lines:
auto env = ICoreProcessEnvironment::systemEnvironment();
ICoreProcessEnvironment#
ICoreProcessEnvironment.h:69 · class · pImpl · 6 declaration(s)
class ICoreProcessEnvironment {
public:
~ICoreProcessEnvironment();
// Copyable, as it always was. The copy is written out because a
// unique_ptr<Impl> member deletes the implicit one; declaring it also
// suppresses the implicit move, which is what this type already did once
// its destructor became user-declared.
ICoreProcessEnvironment(const ICoreProcessEnvironment& other);
ICoreProcessEnvironment& operator=(const ICoreProcessEnvironment& other);
// "Inherit the parent's environment." See the header note on why the copy
// is not taken until the environment is actually handed to a child.
[[nodiscard]] static ICoreProcessEnvironment systemEnvironment();
void insert(const ICoreString& name, const ICoreString& value);
// The overrides, in insert order, later inserts of a name last. This is
// what ICoreProcess::setEnvironment replays onto the real environment; see
// the note above for why it is public rather than a friend's reach.
[[nodiscard]] std::vector<std::pair<ICoreString, ICoreString>> overrides() const;
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};
ICoreProcessOsSupport.h#
ICoreEssentials/Process/ICoreProcessOsSupport.h
ICoreProcessOsSupport -- the OS-side vocabulary two classes in this module both need to start a child, and NEITHER of them should own.
⚠ NOT A PUBLIC HEADER. It is not in the ICoreEssentials.h umbrella and no caller outside Process/ may include it: everything here is a detail of how this module launches children, and all of it names either the platform or a platform's string encoding. It exists because ICorePty.cpp and ICoreProcess.cpp otherwise carry two copies of the SAME three functions, and the two that matter are not the kind of code that survives being copied:
- appendQuotedArgument() implements the CommandLineToArgvW backslash rule,
where a run of backslashes is doubled only when it precedes a quote. Two copies of that drift, and the drift shows up as an argument containing a Windows path with a trailing separator arriving at the
Declares no class of its own — see the file.
ICorePty.h#
ICoreEssentials/Process/ICorePty.h
ICorePty#
ICorePty.h:72 · class · pImpl · 16 declaration(s)
ICorePty -- a child process running under a PSEUDO-TERMINAL: it believes it owns a real tty, so it turns its interactive behaviour on.
class ICorePty {
public:
ICorePty();
// Stops the reader thread, hangs up the pty and reaps the child. Safe
// whether or not start() was ever called, and whether or not the child has
// already exited.
~ICorePty();
// Owns an OS resource and a thread; copying one is meaningless.
ICorePty(const ICorePty&) = delete;
ICorePty& operator=(const ICorePty&) = delete;
// --- setup, all before start() ------------------------------------------
void setWorkingDirectory(const ICoreString& dir);
// The parent's environment plus these overrides, exactly as ICoreProcess
// spells it. Note what a pty makes possible here: the child can be a LOGIN
// shell, which builds its own PATH from the user's profile -- which is how
// a GUI app launched from Finder stops having the bare launchd PATH.
void setEnvironment(const ICoreProcessEnvironment& env);
// The size the child is told the terminal is, in CELLS. Valid before AND
// after start(): before, it seeds the pty; after, it issues TIOCSWINSZ,
// which makes the kernel raise SIGWINCH on the foreground process group --
// the signal a TUI redraws itself on.
//
// A child that starts at 0x0 draws into a phantom viewport, so this
// defaults to 80x24 rather than to the kernel's zeroes. Call it from the
// view's resize hook with the cell geometry, not the pixel geometry.
void setViewport(int columns, int rows);
// --- running ------------------------------------------------------------
// Forks a child under a new pty and execs `program`. False on failure, with
// errorString() set; the object stays usable and start() may be retried.
//
// ON POSIX `program` must be an ABSOLUTE PATH -- PATH is not searched.
// That is a consequence of honouring setEnvironment(): the portable
// PATH-searching exec spellings take the environment from the parent
// instead, which would silently drop every override. Callers name a shell
// they resolved themselves ($SHELL, /bin/sh), so this costs them nothing.
//
// ON WINDOWS there is no such rule: CreateProcessW searches PATH itself and
// takes the child's environment as a separate argument, so the conflict
// does not arise and a bare name works. Callers that want one behaviour on
// both platforms should pass an absolute path.
bool start(const ICoreString& program, const ICoreStringList& arguments);
bool isRunning() const;
// Bytes to the child's terminal input. This is where a keystroke goes --
// as bytes, not as an event: the line discipline is what turns 0x03 into
// SIGINT for the foreground process group, which is what makes Ctrl+C
// interrupt the running command rather than the shell (see row T4.2).
void write(const ICoreByteArray& data);
// SIGHUP to the child. ADVISORY, and measured to be so: an interactive
// shell survives it. What actually hangs a terminal up is closing the
// master descriptor, which the destructor does -- so "close the terminal"
// means destroy this object, not call this. Kept for callers that want to
// nudge a child that has its own SIGHUP handling.
//
// ⚠ A NO-OP ON WINDOWS. There is no SIGHUP; the hangup there is the
// destructor closing the pseudo-console. The nearest alternative, a
// Ctrl+Break event, is a different signal with different semantics, so
// this deliberately does nothing rather than pretending.
void hangUp();
// SIGKILL the child, or TerminateProcess on Windows. The blunt instrument,
// for a child that ignored everything else. The destructor escalates to
// this on its own.
void kill();
// --- results ------------------------------------------------------------
ICoreString errorString() const;
// --- asynchronous results, ALL DELIVERED ON THE GUI THREAD --------------
//
// Registering twice replaces the previous handler rather than adding a
// second one -- these are callbacks, not signals, and every call site
// wants exactly one. Matches ICoreProcess.
// The child wrote. Chunks are COALESCED: a child writing fast produces
// fewer, larger calls rather than one per read, so a build log cannot
// flood the event queue (row T6.2 soaks exactly this). A chunk is a slice
// of a byte stream and carries no alignment guarantee whatsoever -- it can
// split a UTF-8 sequence or an escape sequence down the middle, and the
// parser above must be resumable. It is never empty.
void onBytesRead(std::function<void(const ICoreByteArray& bytes)> fn);
// The child's end of the pty closed -- it exited, or the pty was hung up.
// Raised exactly once, after the last onBytesRead.
//
// `exitCode` is the child's status, or -1 when it could not be collected.
// `crashed` is true when a signal killed it rather than a return from
// main -- which is the normal way a terminal's child dies when the window
// is closed, so it is not by itself an error to report to the user.
void onClosed(std::function<void(int exitCode, bool crashed)> fn);
// Stop delivering, permanently. Safe to call from inside a handler, and
// called for you by the destructor.
void clearCallbacks();
private:
class Impl; // the two-line residue; state lives here
std::unique_ptr<Impl> impl;
};