Repository navigation
Sandbox: Keep test runs away from the developer's tmux server - #123
Merged
Merged
Conversation
why: The quarantine lived inside libtmux.java-library's Test configuration, so a module that did not apply that convention could not have it, and nothing but a copy could give it to one. libtmux-kotlin applies no java-library and runs its tests with the caller's TMUX. A convention of its own can be applied by id, by every module that runs tests. what: - Move the Test quarantine into libtmux.tmux-quarantine unchanged - Apply it from libtmux.java-library, so every module that applied the quarantine before still gets it
why: Only Test tasks were quarantined, and nothing cleared LIBTMUX_SOCKET and LIBTMUX_SOCKET_PATH, which a configuration that names no server is meant to read. A run that starts a client from a JavaExec task, such as a module-tests consumer, had no way to the quarantine. A rule for every JavaExec task would take TMUX from `run` and `runDevelopment`, which a developer runs from inside tmux on purpose, so a task asks for it. Measured with a recording tmux first on PATH and the environment of a pane: a Test task starts its clients with no selector set and a TMUX_TMPDIR of its own, and a JavaExec task that opted in does the same. `runDevelopment` still follows TMUX. The first version did the work in a closure over this script, which added a third configuration-cache problem to `runDevelopment --configuration-cache` and to `:libtmux:test --configuration-cache`. As an Action of plain values both report the two that origin/HEAD does. what: - Give every Test task no TMUX, TMUX_PANE or LIBTMUX_SOCKET*, and a TMUX_TMPDIR of its own under the socket root, created 0700 - Export quarantineTmux, which takes the provider of a JavaExec task that runs a client in CI, and withoutServerSelectors, for a process that is not a task - Replace the environment instead of removing from it: removing from the map a providers.javaexec spec returns left the variables in the child - Add TmuxQuarantineTest, which runs a compiled program under a build that starts from a pane's environment: a task that opts in is scrubbed, one that does not is left alone, the entry survives the configuration cache and its reuse, and the directory is 0700. It reports presence and never a value. Each case failed against a mutant: no scrub, a closure over the script, every JavaExec quarantined, and a directory open to others - Read the export in the test build with `extra[...]` and not `val x by extra`, which Gradle 9.6 deprecates
why: Nothing looked at the end of a run. A scratch test that ran a bare `tmux -f /dev/null new-session -d -s rrleak` left a live server on <root>/<id>/tmux-<uid>/default and the build was green. The only guard was the next start refusing a leftover socket, which never stops the server and is reached only if the same daemon runs the same task again. A server started on `-S <quarantine>/custom.sock` was worse: a sweep of tmux-<uid> alone does not see it, and removing the directory orphans it. Measured with a scratch test in libtmux. Before: BUILD SUCCESSFUL with the server alive until killed by its pid. After: the check failed with ":libtmux:test left a tmux server running: tmux-<uid>/default" and the server was gone. With the server on custom.sock and a test that passes, the sweep of tmux-<uid> gave BUILD SUCCESSFUL and `ss -xlp` still listed the server. This one gives BUILD FAILED naming custom.sock and `ss -xlp` lists nothing. With a kill-server that does nothing it fails and keeps the quarantine. `:libtmux:test --configuration-cache` reports the two problems origin/HEAD does, none of them the new tasks. what: - Register `<task>QuarantineCheck`, which finalizes every Test task and each JavaExec task that opts in, so it runs when the task fails too - Walk the whole quarantine without following links, and take as a socket any entry that is not a file, a directory or a link - Ask each socket with a connect; one nothing answers on is not a leak - Stop each server that answers with `tmux -S <socket> kill-server`, using the lane's binary or -PlibtmuxTmux, and verify it: a connect must fail within about three seconds - Remove the directory only when nothing still answers; otherwise keep it, say so, and fail. Fail naming the sockets below the quarantine, and no value, even when the tests passed - Register from afterEvaluate, and from quarantineTmux, which now takes the task's provider: Gradle refuses to register a task while another is being created - Export stopLeakedServers, which returns the sockets it stopped and the ones still answering, for a process that is not a task - Add TestKit cases: the default socket, a custom socket, a nested one, a stale socket file, a link to a server outside, a kill-server that does nothing, and a failing task. Each guard failed a case when removed: the finalizer, running after a failure, the whole-tree and nested scan, not following links, asking before stopping, verifying the stop, keeping the directory, and failing after a pass
why: libtmux-kotlin applies no libtmux.java-library, so none of its test tasks, including the test-tmux-* lanes, ran quarantined. Its tests reach tmux through TmuxExtension's explicit -S today, but a test that built a bare client would have followed the caller's TMUX. Measured with a recording tmux first on PATH and the environment of a pane: a bare client in a libtmux-kotlin test was started with TMUX, TMUX_PANE, both LIBTMUX_SOCKET variables and the pane's TMUX_TMPDIR intact on origin/HEAD. With the plugin it holds none of them and a TMUX_TMPDIR of the task's own. what: - Apply libtmux.tmux-quarantine to libtmux-kotlin, whose Test tasks it reaches by type, the matrix lanes included
why: The builds under module-tests/ are builds of their own, so no convention of this one reaches them. Their run tasks started a consumer program or the staged workspace CLI with the Gradle daemon's environment, and `cli` ran `debug-info`, `load` and `freeze` through providers.javaexec, which is a spec and not a task. The commands name -S today; a future one that does not would follow the caller's TMUX. Measured with a recording tmux first on PATH and the environment of a pane, against the staged release: each `run` started its client with no TMUX, TMUX_PANE or LIBTMUX_SOCKET*, and the `cli` child had a TMUX_TMPDIR under its own temporary directory. With the opt-in removed from the java run, both its clients held every selector. `debug-info` starts no tmux at all, so it is covered by the same scrub and not by a finding. what: - Apply libtmux.tmux-quarantine to java, kotlin, scala and cli by path, so nothing is resolved from this build - Opt the run task of java, kotlin and scala in with quarantineTmux, read with `extra[...]` and not `val x by extra`, which Gradle 9.6 deprecates and `--warning-mode all` no longer reports here - Give cli's providers.javaexec children the same scrub and a TMUX_TMPDIR inside the run's own directory, and stop and report any server the workspace CLI left anywhere in it when the run ends; keep the run's directory while a server still answers
why: The quarantine is configured on the Test task, so it cannot see a run that never went through one: a test class started from an IDE, or a Test task registered by a module that forgot the plugin, still inherits TMUX and the default socket. The suite then fails quietly or drives the developer's own server. The refusal belongs in the process that would run the tests. Measured with the plugin's scrub disabled and the environment of a pane: `:libtmux:test` stopped before its first test with a message naming TMUX, TMUX_PANE, LIBTMUX_SOCKET and LIBTMUX_SOCKET_PATH. With the marker property removed it stopped naming that instead. With the plugin restored both modules ran. what: - Add the build-logic project test-sandbox: a JUnit Platform LauncherSessionListener, registered with ServiceLoader so it covers every engine, that throws before the first test unless the environment holds no TMUX, TMUX_PANE or LIBTMUX_SOCKET* and TMUX_TMPDIR is the directory the build prepared for the task; a finding names the variable and never prints its value - Have libtmux.tmux-quarantine pass that directory to the test JVM as libtmux.test.sandbox - Put it on the test runtime classpath of libtmux.java-library and libtmux-kotlin - Run the project's own tests under the quarantine, applied by path, and in the root check
why: The quarantine and the refusal are opt-in per module, so a module that registers a Test task and applies neither runs its tests with the caller's TMUX and default socket, and nothing says so. libtmux-kotlin was exactly that: the only module whose tests the convention did not reach, found by reading, not by a failing build. Shown to bite: dropping the plugin id from libtmux-kotlin, and then only its test-sandbox dependency, each failed the task with "these run tests outside the tmux quarantine: :libtmux-kotlin". what: - Add everyTestModuleIsQuarantined: every subproject with a Test task must apply libtmux.tmux-quarantine and have test-sandbox on its test runtime classpath - Run it in the root check
why: The quarantine says what a task's environment should hold, and the refusal checks the test JVM's own. Neither shows what the library hands a child process, which is where a bare client picks its server: the default endpoint adds no flag, so TMUX and TMUX_TMPDIR alone decide it. A canary on the real path catches a change that makes the library, or a later default-selection rule, reach past them. A recording tmux shows what a client received; only a real one shows where its server ends up. Shown to bite: with the plugin's scrub and the refusal both disabled and the environment of a pane, the live case failed naming TMUX, TMUX_PANE, LIBTMUX_SOCKET and LIBTMUX_SOCKET_PATH. With TMUX_TMPDIR no longer exported and the refusal removed, the real bare client started its server outside the quarantine and its case failed. The other cases feed the same reader a client given a pane's environment, a -S outside the quarantine and an ambient TMUX_TMPDIR, so it is shown to flag each, and the same client inside the quarantine to pass. what: - Add SandboxCanaryTest to libtmux: a bare Server.open runs a recording tmux, and no recorded client may hold a selector, name a socket outside the quarantine, or resolve its default socket elsewhere - Start a real bare client, when tmux runs, with no -S and no -L, assert its server listens on the quarantine's tmux-<uid>/default, and stop it with -S; a control started with another TMUX_TMPDIR shows the assertion can fail - Have the recording tmux write argv, the names of the selectors it saw and whether TMUX_TMPDIR is the quarantine, and print no value in any message: an environment holds secrets; a failure reports only whether -S or -L was present, not what they named
why: api/run.sh cleared TMUX and TMUX_PANE and nothing else. With decoys planted, the program it starts still saw LIBTMUX_SOCKET, LIBTMUX_SOCKET_PATH and the caller's TMUX_TMPDIR, so a client in it that named no socket could land on a server the runner did not own. A server the program left behind outlived the run, and one on a socket of its own choosing was not found even by a cleanup that looked. And a closed output ended the runner by SIGPIPE before its cleanup ran, which `sh run.sh prog | head` reproduces. Measured with the new test against the previous runner: all five cases that existed then failed. Each part of the new runner was removed in turn, and each removal failed a case. what: - Unset TMUX, TMUX_PANE and every LIBTMUX_SOCKET* before the program starts - Give it a TMUX_TMPDIR from a fresh 0700 mktemp directory under a short root, and refuse a root whose default socket path would pass 100 bytes; API_RUN_ROOT moves the root, so the test can - On exit, hangup, interrupt, termination and a closed output, find sockets anywhere under both directories without following links, stop each server that answers with -S and verify the stop for about three seconds, and remove the directories only when nothing answers. A server the program left fails the run naming its socket, even when the program succeeded; one that will not stop keeps both directories and says so. A socket nothing answers on is not a leak - Add ApiRunnerSandboxTest to the examples suite: the selectors, the private mode-0700 TMUX_TMPDIR with and without one in the caller's environment, the length refusal, a server on the default socket, a custom one and a nested one, a stale socket file, a link to a server outside, a kill-server that does nothing, and a terminated run; the probe reports names and booleans only - Say so in the examples README
why: CONTRIBUTING.md told a contributor which roots a test may use and never the default socket, and not how the build keeps a task off it. A refused run from an IDE, a server the check stops, or a module the gate names is otherwise a message with no page behind it. Nor did it say which runs are left alone: `runDevelopment` follows the caller's TMUX on purpose. what: - Say what each Test task and module-tests run starts without and with - Say which development runs are not quarantined, and to pass -S - Say what the finalizer does when a quarantined task leaves a server: it looks anywhere in the quarantine without following links, fails even after a pass, and keeps the directory while a server answers; and that the examples runner does the same - Say the test JVM refuses to start otherwise, and to run through ./gradlew - Name the gate for a module with tests
why: The finalizer looked for servers only under a task's TMUX_TMPDIR, but a test names its own socket under java.io.tmpdir, which was the root every task and checkout shares. A probe test that left a server on a @tempdir socket passed the build, and JUnit then deleted the socket, so the server lived on with no path to it. The scan covered the quarantine, not the tree the tests actually use. what: - Give java.io.tmpdir the same directory as TMUX_TMPDIR, for every task the quarantine covers, so the sockets a program names itself are inside the swept tree - Set junit.jupiter.tempdir.cleanup.mode.default to never on Test tasks, so a @tempdir socket is still on disk when the finalizer looks; the finalizer removes the quarantine itself afterwards - Pin both in the canary, and in TmuxQuarantineTest a leaked server on a socket under the JVM's temporary directory, found and stopped - Say in CONTRIBUTING which servers the finalizer cannot see
why: Two of the six contract points had no test that fails without their code. The cap on the socket root, which keeps every socket path inside the 100 bytes AF_UNIX allows, could be raised with the whole suite still green. So could the listener's body or its ServiceLoader entry, because the tests that ran under the listener were sandboxed and never saw it refuse. what: - Refuse a libtmuxSocketRoot of 41 characters in TmuxQuarantineTest before the task runs and before any directory is made - Run the listener in a child JVM handed a pane's environment, one with no quarantine and one the build prepared, and find it through ServiceLoader as the JUnit Platform does - Put the launcher API on the test-sandbox compile classpath
why: A contributor who runs the tests now gets a refusal outside ./gradlew and a failed build for a server a test leaves, which a changelog reader would otherwise meet with no explanation. what: - Add a Development entry under Unreleased for the guard
why: The quarantine is now the JVM's temporary directory, and a Test task keeps its @tempdir until the finalizer runs. A run cancelled in a Gradle daemon that survives leaves those files behind. The next run of the task in that daemon found them, took them for stale sockets and refused to start, though only a socket can have a server behind it. what: - Delete plain files in an existing quarantine and refuse only what is neither a directory nor a regular file - Pin both: temporary files left by an earlier build in the same daemon are cleared, and a socket is still refused by name; the cases skip when the builds land in different daemons
why: everyTestModuleIsQuarantined sat in the root build script, where no test could reach it, so nothing showed it could fail. A module that lost the plugin or the listener would have been caught only if the gate worked as written, and that was never exercised. what: - Move the task into libtmux.tmux-quarantine-gate, which the root build applies by path like the module-tests builds apply the quarantine - Add QuarantineGateTest: a covered module passes, and a module without the plugin or without the listener fails the build and is named
why: The contract has messages carry names and booleans, never values. The plugin printed the quarantine's path, which comes from TMUX_TMPDIR, and the root a caller gave libtmuxSocketRoot. run.sh printed both of its directories, which sit under API_RUN_ROOT, a value the caller sets. what: - Name a stale entry by its path below the quarantine, and a quarantine that is not a directory by its own name - Leave the root out of the message that refuses it - Have run.sh name the two directories it kept, not their paths
why: The probes in TmuxQuarantineTest run JavaExec tasks only. The line that finalizes every Test task, which is the case the issue is about, and the lookup of the tmux binary a matrix lane names could be deleted with the whole suite still green. what: - Run a build whose Test task leaves a server on a socket in the JVM's temporary directory, with libtmux.tmux naming a tmux that ignores kill-server, and require the failure to say the quarantine was kept
why: The real-client canary and its control were skipped when tmux could not be started, so a run on a host without it passed with the guard unobserved. Every other test in libtmux fails hard without tmux. what: - Fail the canary when the tmux it was given does not run
why: Only the listener and the tests, all in its package, use SandboxCheck, and CLAUDE.md keeps new API package-private until a caller outside the package needs it. what: - Make the class and its three members package-private
why: WRITING.md keeps a source comment to one or two lines and sends rationale to the commit message. The header ran twelve lines and the root comment eight. What they argued, kept here: tmux resolves its default socket under TMUX_TMPDIR when it execs, and an inherited TMUX takes precedence for a client started inside a pane, which the Gradle daemon may be. A test is supposed to pass -S, but nothing in the language enforces it, and a command that omits it addresses a real server and can kill it. Sibling libtmux ports start their own tmux servers under /tmp on the same machine. A server of theirs left by an exited run holds a pty and answers to a name this suite might have chosen, so their debris became this suite's intermittent failures. This port takes a root of its own and everything a task creates lands under it. Every task and checkout shares that root, so no finalizer sweeps it whole. JUnit removes a @tempdir when its test ends and the socket of a server the test left goes with it; -S cannot reach a server whose socket is gone, so the finalizer would find nothing. what: - Cut the header, the root, the sweep, the two class and the tempdir comments to the fact
why: The testing guide still said a task refuses to remove a stale file or socket, and named neither LIBTMUX_SOCKET* nor the end-of-task sweep. The contributing guide and the changelog claimed more than the guard does: operationBenchmark is a Test task and is quarantined, the build-logic tests are outside the gate, and the module-tests/cli children get the scrub and a TMUX_TMPDIR but not a temporary directory. what: - Rewrite the guide's isolation section for the quarantine as it is - Say which Test tasks, which benchmark and which module-tests child the guard covers, and that the build-logic tests are outside the gate - Drop the changelog's claim of a temporary directory for every run
why: The stale-socket case skipped when the build neither refused nor printed the quarantine's name, to tell another daemon from a refusal. A message that named the wrong path looked like another daemon, so breaking the message left the build green. what: - Treat any stale-entry message as proof the build ran in this daemon, then require it to name the entry below the quarantine
why: The module-tests builds resolve nothing from the root build, so the root gate cannot see them. Deleting the line that opts a run into the quarantine, or the scrub in the cli build, left every test green while the changelog and the contributing guide said each run was covered. what: - Read the four module-tests build scripts and require that each applies the quarantine file, that java, kotlin and scala opt their run in, and that cli scrubs its children, gives them a TMUX_TMPDIR and stops what they leave
why: Ten finalizer cases skipped when tmux was missing, so a job without it passed with the proof of contract point 5 skipped; the canary already fails there. Freshness was unpinned too: deleting the step that clears an existing quarantine left every case green, because the finalizer removes the directory afterwards. what: - Fail the cases that need tmux instead of skipping them; only the two-daemon cases may still skip - Have the probe report how many entries the quarantine held when the task started, and require none after debris was planted - Wrap the lines past 120 columns, and cut the class comment to the fact it carries
why: The canary checked that the junit property was set, not that JUnit honours it, so a rename of the property or a JUnit that ignored it left the check green while @tempdir sockets vanished before the finalizer. what: - Remember each test's @tempdir and require, after the last test, that every one still exists - Replace the property assertion, wrap the Javadoc line past 120 columns and cut the class comment to the fact it carries
why: Four assertions appended the runner's whole output to their failure message. A tmux error in that output can carry a path under TMUX_TMPDIR, which the contract keeps out of messages. what: - Say the launcher failed and leave its output to the assertions that look for fixed strings in it - Cut the class comment to the fact it carries
why: .editorconfig sets 120 columns for kts, and WRITING.md keeps a source comment to one or two lines. The test-sandbox build header argued for six; it held the fact that the listener is the in-process half and that a project in this build cannot apply the product modules' compiler settings to itself. The listener test's class comment explained why a child JVM is needed: this JVM's environment cannot be changed. what: - Wrap the run.sh message and the root check dependency - Cut the test-sandbox header and the listener test comment to the fact
why: run.sh reads API_RUN_ROOT and refuses a root whose default socket path would not fit, but its README documented only TMUX_BIN. The contributing guide named two fixtures whose servers the finalizer cannot see; a few tests also make directories directly under the shared root. what: - Describe API_RUN_ROOT and its default beside TMUX_BIN - Say the same of the tests that name a path under the root, and that nothing checks they end their servers
why: The negative control asserted that no socket file existed in the quarantine. tmux can leave the socket file of a server it stopped, so the control failed whenever the bare-client case ran first, and passed only because JUnit's default method order put it earlier. what: - Ask the quarantine's socket whether a server answers, which a stale file does not - Cut the Javadoc of the recording tmux to the fact it carries
why: CLAUDE.md puts every tmux server this project starts under /tmp/libtmux-java-test. The plugin tests rooted their quarantines in the JVM's temporary directory, outside it, and put the link control's server in a JUnit temp directory, where a killed run left servers that tools/reap-stale-servers.sh could not find. what: - Root the probe quarantines and the link control's socket under /tmp/libtmux-java-test/tq<pid> - Wrap the lines past 120 columns
why: The runner refuses a root whose default socket path passes 100 bytes. The tests rooted it under a @tempdir inside the quarantine, which left one byte of slack at uid 1000 and none from a five-digit uid, where every case failed with "API_RUN_ROOT is too long". what: - Give each test a short root of its own under the quarantine
why: The cleanup check ran in a finally block, so a leaked server replaced the exception that ended the run and hid why it ended. what: - Keep the failure of the run as the one thrown, and add the leaked servers to it as suppressed; throw them alone when the run passed
why: "Nothing checks that they did" was wider than the claim: the fixtures do prove their own teardown. What is missing is the finalizer's view of servers outside its tree. what: - Say the finalizer does not check that those tests end their servers
why: The cli run starts its server on an explicit socket in its own directory and stops it with kill-server, ignoring the result. The sweep that followed looked only at the TMUX_TMPDIR beside it, then deleted the directory, so a server that ignored kill-server lost its socket unseen, which the contract forbids. what: - Sweep the run's whole directory, which holds the explicit socket and the TMUX_TMPDIR, before deleting it - Wrap the line past 120 columns
why: The test reads lines of text, which a comment could satisfy, and the builds it covers cannot run without a staged repository. The class comment should say so rather than leave a reader to find out. what: - Add that to the class comment
tony
force-pushed
the
test/sandbox-default-server
branch
from
November 1, 2026 15:42
2a43703 to
b1a7266
Compare
tony
marked this pull request as ready for review
November 1, 2026 15:45
tony
added a commit
that referenced
this pull request
Nov 1, 2026
why: Cut 0.1.2 from master so the test-run sandbox guard (#123) and its cli await fix (#130), the per-module changelogs (#125) and the Kotlin and Scala Server capability directory (#127) ship. The install snippets and the examples' snapshot coordinates need to name the release that ships them. what: - Date the 0.1.2 section (2026-11-01) in the root changelog and in each module's, and keep an empty unreleased section above it; the modules with nothing new say so - Move the install snippets and the examples' snapshot coordinates to 0.1.2 - Set the development version to 0.1.2-SNAPSHOT; libtmuxApiBaseline stays until 0.1.2 is published
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #121
Part of #111
Since #117 a
Serverwith no socket lets tmux pick the server, which from a terminal is the one named byTMUX, and otherwise the default socket. A test, doctest or example that forgets its socket now reaches the server the developer is working in, and a run that crashes leaves servers behind. The guard has to be in place before anything relies on that default, so every run is cut off from that server and a server a run leaves is found and stopped.What it does
Every
Testtask and each opted-inJavaExecrun goes throughlibtmux.tmux-quarantine, applied to every module bylibtmux.java-libraryand by path to themodule-testsbuilds.quarantine()clears the selectors from the task's environment, andPrepareQuarantinemakes the task's quarantine as it starts: a fresh0700directory under/tmp/libtmux-java-test/whose name is a digest of the checkout, the task path and the daemon, exported asTMUX_TMPDIRand also given to the JVM asjava.io.tmpdir. The test JVM then runsSandboxSessionListener, which refuses the run whenSandboxCheck.findingsreports a selector, no quarantine, or aTMUX_TMPDIRthat is not the one the build made.everyTestModuleIsQuarantinedfailscheckwhen a module with tests lacks the plugin or the listener. When the task ends, even by failing,CheckQuarantinerunsLeakedServers.sweep: it stops each server that answers on a socket anywhere under the quarantine with-S, checks that it stopped, fails the build naming the sockets, and removes the directory only when nothing still answers.examples/api/run.shdoes the same for the program the API examples start. Development runs (run,runDevelopment, the benchmarks) keep the caller's environment on purpose, as before.Bare tests, doctests and examples are covered because the guard is on the task, not on the test:
DocumentedCommandsTest,ReadmeExamplesTestand the other documentation tests areTesttasks, the API examples run throughrun.sh, and themodule-testsruns opt in. The canary below builds aServerwith no endpoint, which is the new risk #117 created.Where the first branch fell short
The branch I started from claimed a whole-tree scan, and a probe showed it was not. The finalizer swept only the quarantine, but tests name their sockets under
java.io.tmpdir, which was the root every task shares. A test that left a server on a@TempDirsocket passed the build, and JUnit then deleted the socket, so the server lived on with no path to it. Probe on a realTesttask before the fix:BUILD SUCCESSFUL, one tmux server still running, stopped by hand by its pid. After the fix:BUILD FAILED,:libtmux-junit5:test left a tmux server running: junit-13350919600370463308/s, and the server is gone. Two settings make it hold:java.io.tmpdiris the quarantine, and aTesttask keeps each@TempDiruntil the finalizer has looked, because-Scannot reach a server whose socket is gone.The six points of the sandbox contract
Each break was applied alone to the committed code and the named test run, then restored. The first column is the point, the second the break, the third what failed.
ServerSelectors.selectmatches nothinga JavaExec task that opts in has no selector and a private TMUX_TMPDIR of its own0755instead of0700TMUX_TMPDIRexporteda socket root too long to leave room for a socket is refused before the task runstemporary files a cancelled run left are cleared before the next run,a socket a cancelled run left is refused, not silently clearedfindingsreturns nothingSandboxCheckTestcases and the two refusal cases ofSandboxSessionListenerTestrequireaSessionOpenedInAPaneIsRefused,aSessionOpenedWithNoQuarantineIsRefusedServiceLoaderentry removedtheLauncherLoadsTheListenerOnItsOwna module with tests and no quarantine plugin fails the build and is named,a module with tests and no refusal fails the build and is namedlibtmux-kotlinwithout the plugin, then without the listener, in this repositoryeveryTestModuleIsQuarantined, naming:libtmux-kotlinboth timesjavarun is not opted in; theclichildren are not scrubbedthe java, kotlin and scala builds apply the quarantine and opt their run in,the cli build scrubs its children, gives them a TMUX_TMPDIR and stops what they leave-Soutside the quarantineaBareClientReachesNoServerOutsideTheQuarantineaRealBareClientStartsItsServerInsideTheQuarantine,aRealClientThatEscapedTheQuarantineIsNotSeenInsideIt, and the finalizer names the two servers it stoppedjava.io.tmpdirleft alonetheQuarantineIsADirectoryAndTheJvmsTemporaryDirectory,a server on a socket in the temporary directory the JVM was given is found too@TempDirremoved with its testtempDirsOutliveTheirTestsSoTheFinalizerCanLookInThem, which fails the classa server on a socket in a subdirectory of the quarantine is found too,a quarantined task that leaves a server running fails the build and the server is stoppedTesttasks are not finalized, or the finalizer ignores thelibtmux.tmuxpropertya Test task is finalized by the check, with the tmux its libtmux tmux property namesthe quarantine is kept and the build fails when kill-server does not stop the servera quarantined task that leaves a server running fails the build and the server is stopped,the check still runs when the task itself failsa socket a cancelled run left is refused, not silently clearednoFindingAndNoRefusalPrintsAValue,everySocketSelectorIsReportedByNameThe new tests that cover points 2, 3 and 5 are in
TmuxQuarantineTest,SandboxSessionListenerTestandSandboxCanaryTest. Failure messages in them name variables and state what was expected, and none prints an environment value.Checked
./gradlew check --continue --max-workers=4throughheavy.sh --mem 10G, from the commit that movedjava.io.tmpdir(its code is identical to15130969), on tmux 3.8a, took 7 minutes. Every*QuarantineCheckfinalizer passed,everyTestModuleIsQuarantinedpassed, and one test failed:ServerTest.killingThroughAnotherTransportWaitsOnNoLocalProcess. It copies/bin/sleeptotmux-lookalike, and on this host (Ubuntu 26.04)/bin/sleepis a link to theuutilsmulticall binary, so the copy exits at once withunknown program 'tmux-lookalike'. That has nothing to do with the guard, and I did not run it onmaster. CI on its own runners is the judge.Afterwards I compared the tmux servers listening before and after the run. They were the same five, none from this run, and none under
/tmp/libtmux-java-testor/tmp/libtmux-java-dev. The later commits were run on what they change:build-logic check(TmuxQuarantineTest15 cases,QuarantineGateTest,SandboxSessionListenerTest,SandboxCheckTest), thenSandboxCanaryTest,everyTestModuleIsQuarantined,examples:test,docs:testandlibtmux-junit5:checkon the root build.Servers a
TmuxExtensionfixture or the ScalaOwnedTmuxstarts live in a directory of their own under/tmp/libtmux-java-test/, named for the owning JVM, and about ten tests name a path directly under that root. The finalizer cannot see either. They end their servers themselves, the finalizer does not check that they did, and none was left by the run above..github/CONTRIBUTING.mdsays so.Judgement
TMUX_TMPDIRandjava.io.tmpdir. A directory per test would need every test to cooperate.tools/reap-stale-servers.shis unchanged. It reaps fixture directories named for a pid; quarantine directories are hashed and removed by their own finalizer, so it cannot tell a live daemon's from a dead one's.PaneOperationsIntegrationTest.theUnnamedBreakIsNamedWhatTmuxWouldHaveNamedItwas recorded as a flake in the queue. It passed in the full run and 150 times in a row here, and none of the nine failedCIortmux matrixruns among the last 300 mentions it, so no issue is filed.