Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,15 @@ jobs:
working-directory: desktop
run: go test ./... -run 'Protocol' -count=1

# GDK-891: the ConPTY contract (spawn roundtrip, resize answered by
# the child, Job-Object tree kill, close idempotence) can only
# execute on Windows — dev machines are macOS. Runs from the repo
# root: internal/term belongs to the root module, unlike the desktop
# step above.
- name: Terminal (ConPTY) tests
if: steps.cifilter.outputs.run == 'true'
run: go test ./internal/term/ -count=1

# makeappx validates the manifest schema; only an install proves the
# identity, the full-trust entry point, the protocol declaration and the
# app-execution alias are accepted. The Store re-signs with its own certificate, so here
Expand Down
23 changes: 23 additions & 0 deletions cmd/gadak/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/midagedev/gadak/internal/skillinstall"
"github.com/midagedev/gadak/internal/store"
syncer "github.com/midagedev/gadak/internal/sync"
"github.com/midagedev/gadak/internal/term"
issuetap "github.com/midagedev/issuetap"
)

Expand Down Expand Up @@ -181,6 +182,13 @@ type doctorReport struct {
// opens, on the unaugmented environment), never a defect.
HostEnv doctorHostEnv `json:"host_env"`

// TermWin32 is the ConPTY control plane's last failure — which Win32
// call, which error code (GDK-891) — so "my pane won't open on
// Windows" has a next question to ask without a debugger. A call name
// and a number only, and empty (none recorded) on every healthy
// serve and every non-Windows build.
TermWin32 string `json:"term_win32,omitempty"`

// localDBSchema is local.db's on-disk PRAGMA user_version when the file
// exists — the local half of the `schemas` line (GDK-1967), collected
// before any open so the refusal paths report it too. Unexported: the
Expand Down Expand Up @@ -524,6 +532,7 @@ func collectDoctor() doctorReport {
rep.BinaryPath, rep.BinarySignature = collectBuildIdentity()
// Independent of the mirror — the early returns below must not lose it.
rep.HostEnv = collectHostEnv()
rep.TermWin32 = collectTermWin32()

// Agent wiring is independent of the mirror, and the mirror branch below
// returns early — collect it first so a user with no mirror still gets the
Expand Down Expand Up @@ -1323,6 +1332,7 @@ func formatDoctorText(r doctorReport) string {
line("go_version", r.GoVersion)
line("os", r.OS+"/"+r.Arch)
line("hostenv", formatDoctorHostEnv(r.HostEnv))
line("term-conpty", r.TermWin32)
line("profile", r.Profile)
line("workspace_kind", r.WorkspaceKind)
line("origin_type", r.Workspace.OriginType)
Expand Down Expand Up @@ -1673,6 +1683,19 @@ func collectHostEnv() doctorHostEnv {
}
}

// collectTermWin32 is the terminal subsystem's last control-plane failure
// as one paste-safe token pair: the Win32 call's name and its error code
// (GDK-891) — the same counts-and-short-tokens rule the hostenv line
// keeps. Non-Windows builds and healthy serves report none recorded; the
// recorder is ConPTY code.
func collectTermWin32() string {
call, code, ok := term.LastWin32Failure()
if !ok {
return "none recorded"
}
return fmt.Sprintf("%s win32 %d", call, code)
}

// countPathAdded measures what the augmentation contributed: PATH entries
// present after it that base did not have. Counts only — the entries
// themselves stay out of this paste-safe document.
Expand Down
11 changes: 9 additions & 2 deletions internal/term/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,15 @@
// .CloseByToken is how `gadak pairing revoke` reaches a live shell —
// see internal/server's watchdog for who calls it.
//
// - Windows returns ErrUnsupportedPlatform from Create, naming GDK-861
// (the ConPTY shape). An honest stub beats a silent one.
// - Windows runs on ConPTY (GDK-891): CreatePseudoConsole plus a child
// spawned with PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, inside a Job
// Object. The unix ladder maps onto the job rather than onto signals —
// hangup closes the pseudoconsole, kill terminates the job, and the
// job owns every descendant, so the tree needs no controlling-terminal
// walk. ErrUnsupportedPlatform survives as the contract for a platform
// that has no PTY at all; no supported GOOS answers with it today.
// Runtime proof is the Windows CI job — the shape cannot be exercised
// on a developer's macOS.
//
// Snapshot() is the debug surface: per-session id, pid, pids (every
// process on the session's controlling terminal), size, attachment
Expand Down
5 changes: 4 additions & 1 deletion internal/term/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ type Info struct {
// (GDK-1192): "matched/N" (read-back agreed on attempt N),
// "unverified:<err>" (read-back failed and the set was trusted),
// "exhausted" (bounded attempts, error returned), "set-error:<err>",
// or "unsupported" on Windows. Empty before the first resize. Pure
// or, on Windows, "set" — ConPTY has no GetPseudoConsoleSize to read
// the size back with, so there is no read-back branch to name and the
// word says only that ResizePseudoConsole returned (GDK-891).
// Empty before the first resize. Pure
// diagnostic — it changes no behaviour, it names it.
LastResizeExit string `json:"last_resize_exit,omitempty"`
}
Expand Down
20 changes: 19 additions & 1 deletion internal/term/members_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@

package term

func sessionMembers(int) []int { return nil }
// sessionMembers answers membersOf's "every process on this session's
// terminal" (the idle verdict and the roster's PIDs column) with a query
// to the session's Job Object — see jobPids.
//
// The unix halves walk the controlling-terminal device because nothing
// there owns the tree; the walk is once-and-remembered, with start-time
// stamps against pid recycling, because the terminal is revoked under it.
// Here the job owns the tree, so the member list is a query instead of a
// walk: no revocation race to survive, no stamps to check. A pid the
// registry does not know (never started, killed, or closed) answers nil,
// which the callers read as "nothing running" — the same fail direction
// as the unix walk coming back empty.
func sessionMembers(shellPID int) []int {
p := lookupProc(shellPID)
if p == nil {
return nil
}
return p.jobPids()
}
6 changes: 4 additions & 2 deletions internal/term/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,10 @@ type Session struct {
// ID is the session id a socket URL carries.
func (s *Session) ID() string { return s.id }

// PID is the shell's process id — also its process-group id, because the
// child is started with Setsid.
// PID is the shell's process id. On unix it is also its process-group id,
// because the child is started with Setsid; Windows has no process group
// at all — there the tree's owner is the session's Job Object, and this is
// the shell's pid and nothing more (GDK-891).
func (s *Session) PID() int { return s.pid }

// TokenID is the pairing token this session was opened with, empty for a
Expand Down
Loading
Loading