Conversation
session_windows.go was an honest refusal (GDK-862): Create returned ErrUnsupportedPlatform and the pane said "not on Windows yet" and meant it. It is an implementation now — CreatePseudoConsole plus a child spawned with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, cgo-free on x/sys/windows, no new module. Spawn is a direct CreateProcess rather than os/exec: attaching a child to a pseudoconsole needs an attribute list on STARTUPINFOEX, and syscall.SysProcAttr has no field that can carry one. wait() is WaitForSingleObject on the raw handle — the same observable answer, rebuilt. The unix half has no owner of the process tree, which is why it signals a process group and then walks the controlling terminal to find the jobs a shell moved out of it. Windows has an owner, so the ladder maps onto the Job Object instead: hangup closes the pseudoconsole (conhost tears the session down), kill terminates the job, and KILL_ON_JOB_CLOSE backs both up when the handle goes. The child starts CREATE_SUSPENDED and joins the job before its first instruction, so "every descendant is in the job" is a theorem rather than a race AssignProcessToJobObject has to win. That also makes sessionMembers a job query instead of the nil it used to return — enumeration becomes a lookup, with no revoked-device or recycled-pid hazard to guard against. Two unix contracts have no Windows twin and say so where they are read. winsize answers from the last size resize accepted, because ConPTY has no GetPseudoConsoleSize — the unix half trusts only the kernel's read-back, and this one cannot. PID is the shell's pid and nothing else: there is no process group to also be. Handles leak by default here, so the file opens with a ledger naming the single closer of each of the seven and the order they go in, and the control-plane calls sit behind a lock that zeroes each field as it closes — a syscall on a recycled handle value is not the benign EBADF a stale fd is on unix. Runtime proof is the Windows CI job; nothing in this file executes on a developer's macOS. The new step runs the four axes that matter — a command's output makes the round trip, a resize reaches the child, kill takes the grandchild with it, and closing twice is safe — and the last of those is the Job Object contract itself. What could be measured here was measured: both GOOS builds, vet, the full native suite, sourcelint, the staticcheck matrix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Holding this open a little longer, not for review but because its Windows job is the only place the code runs: three tests read zero bytes off the pseudoconsole and the job is empty, so the read path is dead. A round is adding the diagnostics that tell apart 'the child never started', 'it started outside the job' and 'the output pipe is the wrong way round' — the current log cannot. Once that lands the work goes to main directly. AI-authored comment. |
|
Landed on main as 0541173 + 311ae34. The second commit is what the Windows job here found: lpValue was the address of the box holding the pseudoconsole handle rather than the handle, and separately JOBOBJECT_BASIC_PROCESS_ID_LIST's ULONG_PTR counts were mirrored as uint32, so the pid list read the high half of a 64-bit field. The second one now has a byte-decoder test that runs everywhere. Closing the PR — work in this repo goes to main directly from here. The runtime verdict will land on main's own run. AI-authored comment. |
session_windows.gowas an honest refusal (GDK-862). It is an implementation now —CreatePseudoConsoleplus a child spawned withPROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, cgo-free onx/sys/windows, no new module.Spawn is a direct
CreateProcessrather thanos/exec: attaching a child to a pseudoconsole needs an attribute list onSTARTUPINFOEX, andsyscall.SysProcAttrhas no field that can carry one.wait()isWaitForSingleObjecton the raw handle.The tree has an owner here, so the ladder changes shape. The unix half signals a process group and then walks the controlling terminal to find the jobs a shell moved out of it, because nothing owns the tree. Windows has the Job Object:
hangupcloses the pseudoconsole,killterminates the job, andKILL_ON_JOB_CLOSEbacks both up. The child startsCREATE_SUSPENDEDand joins the job before its first instruction, so "every descendant is in the job" is a theorem rather than a raceAssignProcessToJobObjecthas to win.sessionMembersbecomes a job query instead of thenilit returned.Two unix contracts have no twin, and say so where they are read.
winsizeanswers from the last sizeresizeaccepted — ConPTY has noGetPseudoConsoleSize, and the unix half trusts only the kernel's read-back.PIDis the shell's pid and nothing else: there is no process group to also be.Handles leak by default, so the file opens with a ledger naming the single closer of each of the seven and the order they go in, and the control-plane calls sit behind a lock that zeroes each field as it closes — a syscall on a recycled handle value is not the benign
EBADFa stale fd is.Why this is a PR: nothing in the file executes on macOS. The new step in the Windows job is the runtime proof, and it runs the four axes that matter — a command's output makes the round trip, a resize reaches the child,
killtakes the grandchild with it, and closing twice is safe. The third is the Job Object contract itself.What could be measured locally was: both GOOS builds,
go veton each, the full native suite,gofmt,sourcelint.sh, thestaticcheck.shGOOS matrix. The round that wrote this reported an explicit list of what it could not execute, and the CI step exists to answer it.🤖 Generated with Claude Code