diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f51213..3cccb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ Begin agent-assisted development era. - README: improved Contributing section for both agents and humans - LICENSE: updated copyright to include Equinix Metal and 2025 A. Tobey +### Fixed + +- `exec` exits 128 + signal when the child is killed by a signal, not 255 (Unix) +- `exec` no longer panics on Ctrl-C during the span export + ## [0.5.0] - 2025-04-21 Fork otel-cli to @tobert's personal GitHub. diff --git a/data_for_test.go b/data_for_test.go index abfe8d1..5027311 100644 --- a/data_for_test.go +++ b/data_for_test.go @@ -583,14 +583,27 @@ var suites = []FixtureSuite{ IsLongTest: true, }, Expect: Results{ - Config: otelcli.DefaultConfig(), + Config: otelcli.DefaultConfig(), + ExitCode: 42, }, - CheckFuncs: []CheckFunc{ - func(t *testing.T, f Fixture, r Results) { - if r.ExitCode != 42 { - t.Errorf("expected exit code 42 from child process, got %d", r.ExitCode) - } + }, + { + Name: "exec ctrl-c during export does not panic", + Config: FixtureConfig{ + CliArgs: []string{"exec", + "--endpoint", "{{endpoint}}", + "--timeout", "1s", + "--", "/bin/true", }, + StopServerBeforeExec: true, + KillAfter: time.Millisecond * 200, // child is done, export is retrying + KillSignal: syscall.SIGINT, + TestTimeoutMs: 3000, + IsLongTest: true, + }, + Expect: Results{ + Config: otelcli.DefaultConfig(), + CommandFailed: true, // ended by the signal, not a panic (exit 2) }, }, }, @@ -1155,6 +1168,7 @@ var suites = []FixtureSuite{ CliOutputRe: regexp.MustCompile(`^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `), CliOutput: "invalid protocol setting \"xxx\"\n", Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), + ExitCode: 1, // --fail Diagnostics: otelcli.Diagnostics{ IsRecording: false, NumArgs: 7, @@ -1162,7 +1176,6 @@ var suites = []FixtureSuite{ ParsedTimeoutMs: 1000, Endpoint: "*", EndpointSource: "*", - ExecExitCode: 1, }, SpanCount: 0, }, @@ -1403,7 +1416,7 @@ var suites = []FixtureSuite{ Expect: Results{ SpanCount: 1, Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), - ExitCode: 2, + ExitCode: 130, // 128 + SIGINT, as shells report it SpanData: map[string]string{ "status_code": "2", "status_description": "exec command failed: signal: interrupt", @@ -1423,7 +1436,7 @@ var suites = []FixtureSuite{ Expect: Results{ SpanCount: 1, Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), - ExitCode: 2, + ExitCode: 137, // 128 + SIGKILL SpanData: map[string]string{ "status_code": "2", "status_description": "exec command failed: signal: killed", @@ -1444,7 +1457,7 @@ var suites = []FixtureSuite{ Expect: Results{ SpanCount: 1, Config: otelcli.DefaultConfig().WithEndpoint("{{endpoint}}"), - ExitCode: 0, + ExitCode: 137, // --command-timeout kills sleep at 100ms }, }, }, diff --git a/main_test.go b/main_test.go index 563c13e..84e3f3e 100644 --- a/main_test.go +++ b/main_test.go @@ -207,6 +207,12 @@ func checkProcess(t *testing.T, fixture Fixture, results Results) bool { t.Errorf("[%s] command failed is %t but expected %t", fixture.Name, results.CommandFailed, fixture.Expect.CommandFailed) return false } + // a killed process has no exit code of its own; TimedOut alone is not enough + // to skip, because the server timer sets it even when otel-cli exits normally + if !results.CommandFailed && results.ExitCode != fixture.Expect.ExitCode { + t.Errorf("[%s] exit code is %d but expected %d", fixture.Name, results.ExitCode, fixture.Expect.ExitCode) + return false + } return true } diff --git a/otelcli/exec.go b/otelcli/exec.go index c8f7945..47edbef 100644 --- a/otelcli/exec.go +++ b/otelcli/exec.go @@ -8,6 +8,7 @@ import ( "os/signal" "os/user" "strings" + "syscall" "time" "github.com/spf13/cobra" @@ -156,11 +157,18 @@ func doExec(cmd *cobra.Command, args []string) { // capture the child's exit code before OTLP export so SoftFail can use it (#360) if child.ProcessState != nil { Diag.ExecExitCode = child.ProcessState.ExitCode() + // a child killed by a signal reports -1, which os.Exit turns into 255; + // exit 128 + signal instead, as shells do + if status, ok := child.ProcessState.Sys().(syscall.WaitStatus); ok && status.Signaled() { + Diag.ExecExitCode = 128 + int(status.Signal()) + } } else { Diag.ExecExitCode = 127 } cancelCtxDeadline() + // stop delivery before close, or a ctrl-c during export panics on a closed channel + signal.Stop(signals) close(signals) <-signalsDone