fix(control): keep command-line sessions alive on macOS #516
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!516
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/longshot-ctl-darwin"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks; no game run) · Needs: Linux, any GPU
Why
On a Mac,
longshot_ctlcannot attach to, stop or finish a game session:scripts/longshot_ctl.pythinks everydurable session (#47) is dead.
process_start()reads/proc/<pid>/stat, which macOS does not have. The game, its ownerand its launcher all look gone.
tests/test_longshot_ctl.pyfails on a Mac with 2 failures and 26 errors. This is partof the macOS port (#420).
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, in the game-free test on
main(the Darwin branch is new; Linux and Windowskeep their paths).
Settings and switches: defaults.
Steps: on a Mac, in a checkout of
main:python -I -B tests/test_longshot_ctl.py.Expected:
OK: sessions are found alive while their processes run.Actual: at
9c9d69ed(before the rebase):Ran 38 tests,FAILED (failures=2, errors=26);process_start()returns no identity on Darwin. At
6c9edffetest_longshot_ctlis amongmain's failures on this Mac.What changed
process_start()gets a Darwin branch: the process's identity is its kernel start time (microseconds) and the boot'ssession UUID, read through
sysctl, with a zombie or a gone pid counted as dead, as on Linux. One test checks it onmacOS, and
test_copy_savenow expects the resolved run folder thatlaunchalready uses.process_start(). Sessions store the identity as JSON, so it is plain ints and astring.
process_start()inscripts/longshot_ctl.pygets a Darwin branch that readssysctlkern.proc.pidthrough
ctypes, standard library only.kp_proc.p_starttime, astruct timevalthat the kernel sets at fork andnever changes. It has microsecond resolution and is kept as
{"clock": "darwin-start-us", "ticks": …}.kern.bootsessionuuid, as Linux addsboot_id.p_stat == SZOMB) or a pid that is gone (the call answers with no bytes) counts as dead, as Linux'sZ/Xdoes. The answer's own
p_pidmust match the pid asked for.struct kinfo_proclayout was checked with the SDK (offsetof). On arm64 and x86_64 alike it is 648 bytes, withthe start time at 0,
p_statat 36 andp_pidat 40.test_darwin_process_identity(Darwin only) checks four things:sysctl -n kern.bootsessionuuid.offsets.
os.kill(pid, 0)) reads as dead.test_copy_savecompares launch's data folder withos.path.realpath(run).launchresolves--run(
Path.resolve()), and on macOS/tmpis a link to/private/tmp. There is no platform case: on Linux theresolved path is the same.
ps -o lstart=:lstarthas one-second resolution, so a pid reused within the samesecond would pass as the same process, which is weaker than Linux's start ticks. It is also locale-formatted text
from a subprocess, about 7 ms per call on this Mac, and the session code polls
same_process()every 20 ms. sysctlis one system call, about 14 µs, with microsecond resolution and the zombie state in the same answer. libproc's
PROC_PIDTBSDINFOwould give the same start time. sysctl needs no second library, andpsreads the samekinfo_proc[inference: from howpsworks on BSD; its source was not read here].p_starttimediffers. The comparison isthe whole identity, as on Linux and Windows. The existing
pid was reusedcheck inSessionsstill passes. It isstrictly finer than Linux's 10 ms ticks, and the boot UUID covers a reboot, as
boot_iddoes.ctypes.CDLL(None)findssysctlandsysctlbynamein libSystem through the interpreter's own image.6c9edffe: a clean rebase, no conflict.0b605a55is nowef712604and52695384is now04d947b3.The measurements name the commits they ran at.
Measurements
Verdict: fixed on macOS, and nothing else moves: at this head
tests/test_longshot_ctl.pypasses (45 tests OK)where it fails on
main, and the full suite changes by that one check only. Before the rebase it failed onmainandpassed here in two interleaved rounds. Pass or fail: no timing median.
Commits: main
6c9edffe5d(2026-11-14); this PRmac/longshot-ctl-darwin@04d947b356. Before the rebase: main9c9d69ed09(2026-11-14); this PR @
526953848f.Hardware: Apple M4 Max (16 CPUs), 64 GB, macOS 26.7.1
Scene: none (game-free checks; no GPU, no game)
Method: after the rebase,
tests/test_longshot_ctl.pyon this head throughtools/slot.pyin the suite's cleanenvironment, and the full suite on
main@6c9edffeand on this head, compared by test name. Before the rebase:tests/run_all.py test_longshot_ctl, run main, this PR, main, this PR. The results are pass or fail and the same inboth rounds, so there is no timing median. Also the full game-free suite, compared by test name with main's run on
this Mac.
How to reproduce:
python -I -B tests/test_longshot_ctl.pyin each checkout. Add--source-root <checkout> Sessions.test_darwin_process_identityto point one test at another checkout'slongshot_ctl.py.6c9edffe04d947b3tests/test_longshot_ctl.pyon macOSmain's full suite on this Mac)tests/run_all.py --jobs 6)test_longshot_ctlchanged statusBefore the rebase (history at the commits the columns name):
9c9d69ed52695384tests/test_longshot_ctl.py, rounds 1 and 2test_darwin_process_identitypointed at main'slongshot_ctl.py'NoneType' object is not subscriptable(no identity on Darwin)tests/run_all.py --jobs 6)The new check catches real faults. In a scratch copy of this PR's
longshot_ctl.pywith the zombie check(
p_stat == 5) removed, it fails at the zombie ({'clock': 'darwin-start-us', …} is not None). This PR's copy passes.Screenshots: none; nothing on screen.
Flags and settings
None.
Testing
Tested:
Re-tested after the rebase (head
04d947b3on6c9edffe):python -I -B tests/test_longshot_ctl.py, throughtools/slot.pyin the suite's clean environment: 45 tests OK (39 at9c9d69ed).On an Apple M4 Max with macOS 26.7.1, game-free only.
test_longshot_ctlfails on main and passes here, in two interleaved rounds.The struct offsets on arm64 and on x86_64 (Rosetta) with the SDK's
offsetof.Probe of the live process, a child, the child as a zombie, after reaping, pid 1 and a pid that does not exist, with a
separate probe script that is not part of this PR.
The full suite after the rebase,
tests/run_all.py --jobs 6, compared by test name with a fullmainrun at6c9edffeon the same Mac:Only one status changed:
tests/test_longshot_ctl.py, FAIL onmain, passes here. The other 31 failures also failon
main.Before the rebase (
main@9c9d69ed: 533 passed, 20 failed, 17 skipped; this PR @52695384: 534 passed, 19failed, 17 skipped), only one status changed:
tests/test_longshot_ctl.py, FAIL on main, passed here. The other 19failures were main's on this Mac, by name. Trees were clean before and after, and the run took 179 s.
After the rebase onto main
aa95d35f, at3b9d3cfa(04d947b3before the rebases ontoe090cc48andaa95d35f):Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
apply) gave the same results as before;
python -I -B tests/run_all.pyon wave S1's merge11a5b4e7, which carriesthis head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
(
reports/2026-11-15-rebase-sweep-3.md).Not tested:
process_start()are unchanged.test_copy_save's expectation resolves to the samepath where
/tmpis not a link [inference: not run there].test_copy_saveon Linux: its launch block (skipped on Windows) now expectsos.path.realpath(run); nobody has runit there.
Must be tested:
python -I -B tests/test_longshot_ctl.pyin a checkout of this PR (head3b9d3cfa) andof main
aa95d35f, expectingOKon both (this PR's Darwin test skipped), sotest_copy_save's new expectation, theresolved run folder, holds there as main's did (by hand: test request T-PFDX).
Risks
What could break: macOS only, in the tool. If sysctl fails or answers with the wrong size, the process reads as
dead. That is the same outcome as before this PR, so it fails safe. On Linux
test_copy_savecompares with the resolvedrun folder, which is the same path unless the test's temporary folder sits behind a link.
Turning it off: revert the two commits; that restores main.
Compatibility: nothing changes for a player or on Windows and Linux. On macOS,
longshot_ctl's session files storethe new identity (
{"clock": "darwin-start-us", …}); before this PR every macOS session read as dead. Saves,longshot_options.ini, the caches and the vk ABI are unchanged.Issues
See #47 (durable sessions).
See #420 (macOS tracking).
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head3b9d3cfa, without a conflict; this PR's owncommits, old → new:
ef712604→56f4ca87,04d947b3→3b9d3cfa; the commit names elsewhere in this descriptionare from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS, after therebase: 532 passed, 17 skipped, and 31 failures that also fail on
main(531 passed, 32 failed, 17 skipped at6c9edffe); by test name under Testing. After the rebase onto mainaa95d35f(head3b9d3cfa): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.tests/test_longshot_ctl.py:one Darwin test added;
test_copy_saveexpects the resolved run folder, the same path as before where/tmpis nota link.
526953848f04d947b35604d947b356bcde1195e1bcde1195e13b9d3cfa2aWIP: fix(control): keep command-line sessions alive on macOSto fix(control): keep command-line sessions alive on macOSFolded into #845 (feat(macos): run the host on macOS: build, runtime, control, crash records and the Vulkan loader)
Pull request closed