wait_for_exit counted any live PID with the same number as a leftover, so a
reused PID produced a misleading "still present after SIGKILL" message. Compare
the original start time and use the process state only to include the original
process while it is an unreaped zombie.
Children whose parent dies are reparented to the guest launchd, and its job
manager only reaps PIDs it has a job record for, so every leftover mldr/CEF
process from a killed client tree stayed a zombie until shutdown. Reap zombies
that launchd owns but does not track on a 5 second timer; tracked jobs keep
their exit status for job_reap(). proc_listpids() is not implemented in Darling
yet, so the scan enumerates the guest PID namespace through /proc.
recover-stale-client now signals the stale tree deepest-first so a live parent
can still reap its own children, and prepare.py repairs interrupted checkouts
and missing Git LFS objects instead of publishing pointer text as a library.
Resolve conflicts with #122: keep both feature docs and CI steps, and drop
this branch's copy of the close-helper block, which main now starts after
the installed-client check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- fail closed when X11 omits _NET_CLIENT_LIST or WM_CLASS, and recognize the
RiotClientServices window class;
- require stale client processes to be at least 60 seconds old;
- recheck process identity before SIGTERM and SIGKILL and recheck the window
before signalling;
- document the age and window-detection guards.
- restore hidden windows even when the launcher output pipe is closed;
- exit the hide watcher after ten idle minutes and start it only after the
installed-client check;
- reject missing, malformed or invalid close-during-game preferences instead
of silently selecting hide;
- document the preference contract and idle behavior.
A launcher can exit while its long-lived close/hide helper remains. The next
run then starts another watcher, so several copies watch the same prefix.
Add an opt-in RCLOL_CLEAN_STALE_HELPERS=1 preflight that removes only helper
processes of this prefix reparented to init/systemd. Helpers of a live
launcher, other prefixes, the game, Riot/League/CEF, darlingserver and the
runtime are preserved. Unreadable or relative process data leaves a process
alone.
Validated across repeated launcher starts: the cleanup removed four, then one,
then one orphaned watcher; a subsequent dry run reported no orphaned helpers.
Zombie reaping after partial recovery is tracked separately in #124.
- strip inline YAML comments from preference values, including quoted values;
- keep a successfully unmapped window tracked when the success log raises
BrokenPipeError, while still discarding a window whose unmap failed;
- cover both cases with tests.
Darlingserver normally stays alive after the Riot Client or game is closed.
If the client window disappears but Riot/League/CEF processes remain,
restarting via run.sh reuses a half-dead runtime and can fail to open a
window.
Add an opt-in RCLOL_RECOVER_STALE_CLIENT=1 preflight. When a darlingserver is
alive, no game is running, no managed client window is visible and stale
client processes belong to this prefix, stop only that client/CEF tree.
Preserve darlingserver, launchd/mldr, sockets and .init.pid, then continue
with the normal product launch.
The helper fails closed when X11 is unavailable, never touches a running game
or a visible client window, and does not patch client binaries, CEF or assets.
It saves the Darling bootstrap, but not Riot Client/CEF startup; a full runtime
restart remains the fallback if no window appears.
- restrict preference reads to the lol-user-experience section;
- distinguish a failed close-preference read from a missing key;
- reject xprop's 'not found' text as a WM_CLASS value;
- track a window before unmap so an interrupting signal still restores it;
- report the tricks-mode opt-out in the hide helper's startup message;
- cover the new cases with preference, window and interruption tests.
Add RCLOL_CLIENT_IN_GAME_TRICKS=1 as the practical launcher switch. It reads
lol-user-experience from LCULocalPreferences.yaml:
- potatoModeEnabled false or unreadable: no action;
- closeLeagueClientDuringGame true: close the client UI;
- closeLeagueClientDuringGame false: hide the client windows.
The existing close and hide switches remain low-level options when the
automatic switch is not set. The hide helper accepts the master switch, and
the resolver plus preference cases are covered by tests and documentation.
The local .desktop entry was validated with the master switch; the installed
preferences select hide on this machine.
- associate target windows with the selected prefix through the guest PID,
X11 _NET_WM_PID, NSpid and the loader socket; fail closed when ownership is
unknown;
- let --once run without either opt-in gate, matching the documented
diagnostic;
- route SIGHUP through the restore handler;
- cache non-empty WM_CLASS results per window and prune stale entries;
- cover unreadable /proc, host PID mapping and prefix ownership in tests.
Add an opt-in alternative to closing LeagueClientUx: hide the managed
LeagueClientUx and Riot Client X11 windows while the game process runs and
show them again when it exits. The client stays open; the game window is
never touched.
Detection is attributed to the launcher prefix through __mldr_sockpath and
fails closed when proc, ownership or window state is unreadable. The helper
requires X11/Xwayland, restores the window on end, signal or error, and is
mutually exclusive with RCLOL_CLOSE_CLIENT_IN_GAME=1.
Document the flag, compliance scope and limits. A local 30-second test on an
i5-3470/RX 550 reduced summed client/CEF CPU from 120.7% to 93.4%; this is a
CPU-bound-host measurement, not a universal FPS claim.
No client binaries, CEF, assets or anti-cheat are modified.
Match the legacy entry against this checkout's desktop-run.sh instead of any
desktop-run.sh, and delete the legacy icon files only after that entry or the
old artwork proves ownership. Quote and escape Exec arguments as the Desktop
Entry specification requires, so a checkout path with reserved characters no
longer produces an invalid entry, and escape the Path value.
Use StartupWMClass=RiotClientServices in both entries: Darling derives WM_CLASS
from -[NSProcessInfo processName], the basename of argv[0], and the launcher
starts .../RiotClientServices. "Riot Client" is only the bundle display name,
so the checkout entry never matched the window. Both entries now share
Name/GenericName/Comment/Icon/Keywords/Categories and a test keeps them in
sync; Terminal stays different because the checkout entry logs to
build/desktop-launch.log while the packaged entry runs the session in a
terminal.
Also drop the stray top-level icon copies, handle dangling legacy symlinks,
name the icon for assistive technology, document the alias and menu entry, and
cover the helper with disposable-HOME tests.
Match the legacy entry against this checkout's desktop-run.sh instead of any
desktop-run.sh, and delete the legacy icon files only after that entry or the
old artwork proves ownership. Quote and escape Exec arguments as the Desktop
Entry specification requires, so a checkout path with reserved characters no
longer produces an invalid entry, and escape the Path value.
Use StartupWMClass=RiotClientServices in both entries: Darling derives WM_CLASS
from -[NSProcessInfo processName], the basename of argv[0], and the launcher
starts .../RiotClientServices. "Riot Client" is only the bundle display name,
so the checkout entry never matched the window. Both entries now share
Name/GenericName/Comment/Icon/Keywords/Categories and a test keeps them in
sync; Terminal stays different because the checkout entry logs to
build/desktop-launch.log while the packaged entry runs the session in a
terminal.
Also drop the stray top-level icon copies, handle dangling legacy symlinks,
name the icon for assistive technology, document the alias and menu entry, and
cover the helper with disposable-HOME tests.
Open with no row selected passed nil to -_setFilename:, and NSOpenPanel boxed it in an
array literal, so the action threw before the modal session was stopped and the panel
stayed up. Both buttons now go through one helper that stops the session only when
this panel is the modal window and always orders the panel out; Open accepts the
selection (filtered by canChooseFiles/canChooseDirectories and multiple-selection),
or the shown directory for directory choosers. Escape cancels.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Open with no row selected passed nil to -_setFilename:, and NSOpenPanel boxed it in an
array literal, so the action threw before the modal session was stopped and the panel
stayed up. Both buttons now go through one helper that stops the session only when
this panel is the modal window and always orders the panel out; Open accepts the
selection (filtered by canChooseFiles/canChooseDirectories and multiple-selection),
or the shown directory for directory choosers. Escape cancels.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Restack and makeKey no longer map a window AppKit considers hidden, and
setIsVisible: NO always unmaps, so the client's splash stays hidden.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Restack and makeKey no longer map a window AppKit considers hidden, and
setIsVisible: NO always unmaps, so the client's splash stays hidden.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
#79 made update.sh stop Darling again after the overlays and run
configure-darling-platform.py --replace-known-defaults --write. The
rootless update test still expected the old call list and failed. Stub the
identity tool, expect the stop + identity calls last, and check a failed
install never reaches them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#79 made update.sh stop Darling again after the overlays and run
configure-darling-platform.py --replace-known-defaults --write. The
rootless update test still expected the old call list and failed. Stub the
identity tool, expect the stop + identity calls last, and check a failed
install never reaches them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Older setups shipped serial C02ZL0GVMD6N and UUID
6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B to every install. After installing,
update.sh now runs configure-darling-platform.py --replace-known-defaults,
which provisions the prefix only when its saved identity still has one of
those values. Other custom values (imported serials/UUIDs, model, board,
manufacturer, ROM, MLB) are kept; prefixes with no saved identity or only
custom values are not touched.
provision() also drops a standalone system-id or provisioning_UDID derived
from the shared UUID instead of keeping it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>