Skip to content
Merged
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 go/internal/e2e/auto_tier_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ func TestAutoTierE2E(t *testing.T) {
})

t.Run("should restore and override fast auto tier on cold resume", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx, client := newAutoClient(t)
fastSession, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
Model: "auto",
Expand Down Expand Up @@ -287,6 +290,9 @@ func TestAutoTierE2E(t *testing.T) {
})

t.Run("should commit fast auto tier after successful turn", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
_, client := newAutoClient(t)
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
Model: "auto",
Expand Down Expand Up @@ -358,6 +364,9 @@ func TestAutoTierE2E(t *testing.T) {
})

t.Run("should preserve effective tier when fast activation fails", func(t *testing.T) {
if testharness.RunInIsolatedProcess(t) {
return
}
ctx, client := newAutoClient(t)
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
Model: "auto",
Expand Down
1 change: 1 addition & 0 deletions go/internal/e2e/mcp_oauth_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ func startOAuthMCPServer(t *testing.T, cimdSupported ...bool) string {
if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
return
}
testharness.PrepareForProcessWait()
_ = cmd.Process.Kill()
_, _ = cmd.Process.Wait()
})
Expand Down
3 changes: 3 additions & 0 deletions go/internal/e2e/testharness/inprocess_cleanup_disabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ package testharness
func waitForInProcessCleanup() error {
return nil
}

// PrepareForProcessWait is a no-op when the in-process runtime is unavailable.
func PrepareForProcessWait() {}
6 changes: 6 additions & 0 deletions go/internal/e2e/testharness/inprocess_cleanup_enabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ func waitForInProcessCleanup() error {
}
return nil
}

// PrepareForProcessWait repairs signal handlers before the harness stops or
// waits for a child process while an in-process runtime is active.
func PrepareForProcessWait() {
ffihost.PrepareForChildProcessWait()
}
2 changes: 2 additions & 0 deletions go/internal/e2e/testharness/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error {
p.proxyURL = ""
}()

PrepareForProcessWait()

// Send stop request to the server
if p.proxyURL != "" {
stopURL := p.proxyURL + "/stop"
Expand Down
38 changes: 26 additions & 12 deletions go/internal/ffihost/ffihost.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ type Host struct {
callbackToken uintptr
}

func (h *Host) rearmForeignSignalHandlers() {
if h.cliEntrypoint != "" {
rearmForeignSignalHandlers(h.lib.handle)
}
}

// PrepareForChildProcessWait repairs signal handlers that the embedded runtime
// may have replaced before the Go process stops or waits for its own child.
func PrepareForChildProcessWait() {
rearmForeignSignalHandlers(0)
}

// Create resolves the native library and prepares the host. environment and
// args contain SDK-managed runtime options.
func Create(runtimeEntrypoint, cliEntrypoint string, environment map[string]string, args []string) (*Host, error) {
Expand Down Expand Up @@ -260,10 +272,8 @@ func (h *Host) Start() error {
return fmt.Errorf("copilot_runtime_host_start failed (library %q)", h.libraryPath)
}

if h.cliEntrypoint != "" {
// A legacy embedded host may install a SIGCHLD handler without SA_ONSTACK.
rearmForeignSignalHandlers(h.lib.handle)
}
// An embedded host may install a SIGCHLD handler without SA_ONSTACK.
h.rearmForeignSignalHandlers()

callbackHandle := sharedOutboundCallback()
callbackToken := uintptr(nextOutboundToken.Add(1))
Expand All @@ -273,13 +283,15 @@ func (h *Host) Start() error {
if h.connectionID == 0 {
outboundTargets.Delete(callbackToken)
h.callbackToken = 0
h.rearmForeignSignalHandlers()
h.lib.hostShutdown(h.serverID)
if h.cliEntrypoint != "" {
rearmForeignSignalHandlers(h.lib.handle)
}
h.rearmForeignSignalHandlers()
h.serverID = 0
return fmt.Errorf("copilot_runtime_connection_open failed")
}
// Connection initialization may install Tokio's SIGCHLD handler after
// host startup, so repair it again before any child process can exit.
h.rearmForeignSignalHandlers()
return nil
}

Expand Down Expand Up @@ -360,6 +372,9 @@ func (h *Host) writeFrame(frame []byte) (int, error) {
if len(frame) == 0 {
return 0, nil
}
// A prior runtime request may have installed or restored Tokio's SIGCHLD
// handler. Repair it before another request can stop a native child process.
h.rearmForeignSignalHandlers()
ok := h.lib.connectionWrite(connID, unsafe.Pointer(&frame[0]), uintptr(len(frame)))
runtime.KeepAlive(frame)
if !ok {
Expand Down Expand Up @@ -392,6 +407,7 @@ func (h *Host) tryFinalizeCleanupLocked() bool {
connID := h.connectionID

if connID != 0 {
h.rearmForeignSignalHandlers()
if !h.lib.connectionClose(connID) {
return false
}
Expand All @@ -403,17 +419,15 @@ func (h *Host) tryFinalizeCleanupLocked() bool {
if callbackToken != 0 {
outboundTargets.Delete(callbackToken)
}

serverID := h.serverID
if serverID != 0 {
h.rearmForeignSignalHandlers()
if !h.lib.hostShutdown(serverID) {
log.Printf("FfiRuntimeHost: host_shutdown did not recognize server %d", serverID)
}
h.serverID = 0
if h.cliEntrypoint != "" {
// A legacy host may restore its saved SIGCHLD action during shutdown.
rearmForeignSignalHandlers(h.lib.handle)
}
// A host may restore its saved SIGCHLD action during shutdown.
h.rearmForeignSignalHandlers()
}
return true
}
Expand Down
14 changes: 6 additions & 8 deletions go/internal/ffihost/sigonstack_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ const (
)

// rearmForeignSignalHandlers re-adds the SA_ONSTACK flag to any signal handler
// installed by the native runtime (libnode/libuv, loaded via dlopen) that
// omitted it. The Go runtime aborts with "non-Go code set up signal handler
// without SA_ONSTACK flag" when such a signal (notably SIGCHLD, signal 20 on
// Darwin) is delivered while a Go-managed child process is reaped. libuv
// installs a SIGCHLD handler without SA_ONSTACK, which poisons every subsequent
// os/exec child reaped by Go in the same process (enforced by the Go runtime on
// both macOS and Linux; the Linux variant lives in sigonstack_linux.go).
// installed by the native runtime that omitted it. Tokio's process-global
// SIGCHLD registration through signal-hook-registry chains Go's handler but
// replaces its flags without SA_ONSTACK. The Go runtime aborts with "non-Go code
// set up signal handler without SA_ONSTACK flag" when SIGCHLD (signal 20 on
// Darwin) is delivered while a Go-managed child process is reaped.
//
// We preserve each foreign handler and merely OR in SA_ONSTACK, so libuv's child
// We preserve each foreign handler and merely OR in SA_ONSTACK, so Tokio's child
// watching keeps working while the Go runtime stays happy. Handlers left at
// SIG_DFL/SIG_IGN and Go's own handlers (which already carry SA_ONSTACK) are
// untouched. Best-effort: any failure is silently ignored, since the worst case
Expand Down
13 changes: 6 additions & 7 deletions go/internal/ffihost/sigonstack_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ type linuxSigaction struct {
}

// rearmForeignSignalHandlers re-adds the SA_ONSTACK flag to any signal handler
// installed by the native runtime (libnode/libuv, loaded via dlopen) that
// omitted it. The Go runtime aborts with "non-Go code set up signal handler
// without SA_ONSTACK flag" when such a signal (notably SIGCHLD, signal 17 on
// Linux) is delivered while a Go-managed child process is reaped. libuv installs
// a SIGCHLD handler without SA_ONSTACK, which poisons every subsequent os/exec
// child reaped by Go in the same process.
// installed by the native runtime that omitted it. Tokio's process-global
// SIGCHLD registration through signal-hook-registry chains Go's handler but
// replaces its flags without SA_ONSTACK. The Go runtime aborts with "non-Go code
// set up signal handler without SA_ONSTACK flag" when SIGCHLD (signal 17 on
// Linux) is delivered while a Go-managed child process is reaped.
//
// We preserve each foreign handler and merely OR in SA_ONSTACK, so libuv's child
// We preserve each foreign handler and merely OR in SA_ONSTACK, so Tokio's child
// watching keeps working while the Go runtime stays happy. Handlers left at
// SIG_DFL/SIG_IGN and Go's own handlers (which already carry SA_ONSTACK) are
// untouched. Best-effort: any failure is silently ignored, since the worst case
Expand Down
80 changes: 80 additions & 0 deletions go/internal/ffihost/sigonstack_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/signal"
"syscall"
"testing"
"unsafe"
)

func TestRearmForeignSignalHandlersAddsOnStack(t *testing.T) {
Expand Down Expand Up @@ -36,3 +37,82 @@ func TestRearmForeignSignalHandlersAddsOnStack(t *testing.T) {
t.Fatal("SA_ONSTACK was not restored")
}
}

func TestHostRearmsSignalHandlersAroundNativeOperations(t *testing.T) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGUSR1)
defer signal.Stop(signals)

var original linuxSigaction
if !linuxGetSigaction(int(syscall.SIGUSR1), &original) {
t.Fatal("failed to read SIGUSR1 action")
}
defer linuxSetSigaction(int(syscall.SIGUSR1), &original)

host := &Host{
cliEntrypoint: "copilot",
lib: &ffiLibrary{
hostStart: func(unsafe.Pointer, uintptr, unsafe.Pointer, uintptr) uint32 {
return 1
},
connectionOpen: func(uint32, uintptr, uintptr, unsafe.Pointer, uintptr, unsafe.Pointer, uintptr, unsafe.Pointer, uintptr) uint32 {
withoutOnStack := original
withoutOnStack.flags &^= linuxSaOnStack
if !linuxSetSigaction(int(syscall.SIGUSR1), &withoutOnStack) {
t.Fatal("failed to clear SA_ONSTACK during connection initialization")
}
return 2
},
connectionWrite: func(uint32, unsafe.Pointer, uintptr) bool {
assertSignalHandlerOnStack(t, "connection write")
return true
},
connectionClose: func(uint32) bool {
assertSignalHandlerOnStack(t, "connection close")
return true
},
hostShutdown: func(uint32) bool {
assertSignalHandlerOnStack(t, "host shutdown")
return true
},
},
recv: newReceiveBuffer(),
}
if err := host.Start(); err != nil {
t.Fatal(err)
}
defer host.Dispose()

var rearmed linuxSigaction
if !linuxGetSigaction(int(syscall.SIGUSR1), &rearmed) {
t.Fatal("failed to read rearmed SIGUSR1 action")
}
if rearmed.flags&linuxSaOnStack == 0 {
t.Fatal("SA_ONSTACK was not restored after connection initialization")
}

withoutOnStack := original
withoutOnStack.flags &^= linuxSaOnStack
if !linuxSetSigaction(int(syscall.SIGUSR1), &withoutOnStack) {
t.Fatal("failed to clear SA_ONSTACK before connection write")
}
if _, err := host.writeFrame([]byte("request")); err != nil {
t.Fatal(err)
}

if !linuxSetSigaction(int(syscall.SIGUSR1), &withoutOnStack) {
t.Fatal("failed to clear SA_ONSTACK before disposal")
}
host.Dispose()
}

func assertSignalHandlerOnStack(t *testing.T, operation string) {
t.Helper()
var action linuxSigaction
if !linuxGetSigaction(int(syscall.SIGUSR1), &action) {
t.Fatalf("failed to read SIGUSR1 action before %s", operation)
}
if action.flags&linuxSaOnStack == 0 {
t.Fatalf("SA_ONSTACK was not restored before %s", operation)
}
}
2 changes: 1 addition & 1 deletion go/internal/ffihost/sigonstack_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ package ffihost

// rearmForeignSignalHandlers is a no-op on platforms other than darwin and
// linux. Only those Unix platforms deliver the SA_ONSTACK-less SIGCHLD handler
// (installed by libuv) that the Go runtime rejects; Windows is unaffected.
// installed by Tokio that the Go runtime rejects; Windows is unaffected.
func rearmForeignSignalHandlers(_ uintptr) {}
2 changes: 1 addition & 1 deletion nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"url": "https://github.com/github/copilot-sdk.git"
},
"version": "0.0.0-dev",
"copilotCliVersion": "1.0.85",
"copilotCliVersion": "1.0.86-0",
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
"main": "./dist/cjs/index.js",
"types": "./dist/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion nodejs/src/cliVersion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const COPILOT_CLI_VERSION = "1.0.85";
export const COPILOT_CLI_VERSION = "1.0.86-0";

export const COPILOT_CLI_USE_NPM_PACKAGE = false;
Loading