diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 021f7b6e..6b98f598 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -25,6 +25,7 @@ builds: goos: - linux - darwin + - windows goarch: - amd64 @@ -32,6 +33,10 @@ builds: archives: - id: default + # Windows users expect .zip; other platforms keep the default tarball. + format_overrides: + - goos: windows + formats: [zip] name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ diff --git a/internal/sidecar/terminal.go b/internal/sidecar/terminal_unix.go similarity index 84% rename from internal/sidecar/terminal.go rename to internal/sidecar/terminal_unix.go index 7d0f45cd..2eec6feb 100644 --- a/internal/sidecar/terminal.go +++ b/internal/sidecar/terminal_unix.go @@ -1,3 +1,5 @@ +//go:build !windows + package sidecar import ( @@ -11,6 +13,8 @@ import ( // watchWindowSize listens for SIGWINCH and updates the remote PTY size. // It returns when done is closed. +// +// Windows has no SIGWINCH; see terminal_windows.go for the polling equivalent. func watchWindowSize(fd int, sess *ssh.Session, done <-chan struct{}) { ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGWINCH) diff --git a/internal/sidecar/terminal_windows.go b/internal/sidecar/terminal_windows.go new file mode 100644 index 00000000..4cdf0e0d --- /dev/null +++ b/internal/sidecar/terminal_windows.go @@ -0,0 +1,47 @@ +//go:build windows + +package sidecar + +import ( + "time" + + "golang.org/x/crypto/ssh" + "golang.org/x/term" +) + +// windowSizePollInterval is how often the local terminal is measured on +// Windows. SIGWINCH does not exist there, so resize is detected by polling. +const windowSizePollInterval = 250 * time.Millisecond + +// watchWindowSize polls the local terminal size and updates the remote PTY +// when it changes. It returns when done is closed. +// +// This is the Windows counterpart to the SIGWINCH-driven implementation in +// terminal_unix.go. +func watchWindowSize(fd int, sess *ssh.Session, done <-chan struct{}) { + ticker := time.NewTicker(windowSizePollInterval) + defer ticker.Stop() + + lastW, lastH, err := term.GetSize(fd) + if err != nil { + // Not a measurable terminal; nothing useful to report. + lastW, lastH = 0, 0 + } + + for { + select { + case <-done: + return + case <-ticker.C: + w, h, err := term.GetSize(fd) + if err != nil { + continue + } + if w == lastW && h == lastH { + continue + } + lastW, lastH = w, h + _ = sess.WindowChange(h, w) + } + } +} diff --git a/internal/telemetry/delegate.go b/internal/telemetry/delegate.go index 8d24699f..b5736240 100644 --- a/internal/telemetry/delegate.go +++ b/internal/telemetry/delegate.go @@ -8,7 +8,6 @@ import ( "os/exec" "path/filepath" "sync" - "syscall" "github.com/segmentio/analytics-go/v3" @@ -63,7 +62,7 @@ func (d *delegateDestination) send(in io.Reader) error { cmd := exec.Command(bin, "receive-telemetry") cmd.Stdout = io.Discard cmd.Stderr = io.Discard - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // avoid signals sent to the parent (e.g. Ctrl-C) reaching this subprocess too + detachProcess(cmd) // avoid signals sent to the parent (e.g. Ctrl-C) reaching this subprocess too cmd.Env = append(os.Environ(), receiver.EnvWriteKey+"="+d.writeKey, receiver.EnvTelemetryEndpoint+"="+d.endpoint, diff --git a/internal/telemetry/delegate_test.go b/internal/telemetry/delegate_test.go index 1c9ef6d2..9f51180f 100644 --- a/internal/telemetry/delegate_test.go +++ b/internal/telemetry/delegate_test.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "sync" "testing" "time" @@ -37,7 +38,13 @@ func runTestMain(m *testing.M) int { } defer os.RemoveAll(dir) + // Windows will not execute a file without an executable extension, and + // `go build -o` does not add one when the output path is explicit. Without + // this the stub builds fine but never starts, and the tests see zero events. receiverBinPath = filepath.Join(dir, "receiverbin") + if runtime.GOOS == "windows" { + receiverBinPath += ".exe" + } build := exec.Command("go", "build", "-o", receiverBinPath, "./testdata/receiverbin") build.Stderr = os.Stderr if err := build.Run(); err != nil { diff --git a/internal/telemetry/detach_unix.go b/internal/telemetry/detach_unix.go new file mode 100644 index 00000000..1c7a060a --- /dev/null +++ b/internal/telemetry/detach_unix.go @@ -0,0 +1,14 @@ +//go:build !windows + +package telemetry + +import ( + "os/exec" + "syscall" +) + +// detachProcess puts the child in its own process group so that signals sent +// to the parent's group (e.g. Ctrl-C) do not reach it. +func detachProcess(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} diff --git a/internal/telemetry/detach_windows.go b/internal/telemetry/detach_windows.go new file mode 100644 index 00000000..cc2986cf --- /dev/null +++ b/internal/telemetry/detach_windows.go @@ -0,0 +1,15 @@ +//go:build windows + +package telemetry + +import ( + "os/exec" + "syscall" +) + +// detachProcess puts the child in a new process group so that console control +// events sent to the parent's group (e.g. Ctrl-C) do not reach it. This is the +// Windows analogue of setpgid. +func detachProcess(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP} +}