detached 8784b11

fix(guest): escalate cancellation SIGINT/SIGTERM/SIGKILL, stream through frame writers, forward init signals; cancel channel in Drive

commit
8784b11a7a400942fc6c0c2dda70628c27f9cf57
author
brice · · 2026-10-12 16:00 UTC
parent
af04fdc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +682 -145
A buf.gen.yaml +5 -0
@@ -0,0 +1,5 @@
1+version: v2
2+plugins:
3+ - local: [go, tool, protoc-gen-go]
4+ out: ../..
5+ opt: module=bnema.dev/ermite-runner
@@ -0,0 +1,5 @@
1+version: v2
2+plugins:
3+ - local: [go, tool, protoc-gen-go]
4+ out: ../..
5+ opt: module=bnema.dev/ermite-runner
A buf.yaml +12 -0
@@ -0,0 +1,12 @@
1+version: v2
2+modules:
3+ - path: internal/execstream
4+lint:
5+ use:
6+ - STANDARD
7+ except:
8+ # The proto is internal to this module and lives next to its Go package.
9+ - PACKAGE_DIRECTORY_MATCH
10+breaking:
11+ use:
12+ - FILE
@@ -0,0 +1,12 @@
1+version: v2
2+modules:
3+ - path: internal/execstream
4+lint:
5+ use:
6+ - STANDARD
7+ except:
8+ # The proto is internal to this module and lives next to its Go package.
9+ - PACKAGE_DIRECTORY_MATCH
10+breaking:
11+ use:
12+ - FILE
M go.mod +76 -6
@@ -12,25 +12,95 @@ require (
1212 )
1313
1414 require (
15+ buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 // indirect
16+ buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 // indirect
17+ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect
18+ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1 // indirect
19+ buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2 // indirect
20+ buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 // indirect
21+ buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a // indirect
22+ buf.build/go/bufplugin v0.10.0 // indirect
23+ buf.build/go/bufprivateusage v0.1.0 // indirect
24+ buf.build/go/interrupt v1.1.0 // indirect
25+ buf.build/go/protovalidate v1.4.0 // indirect
26+ buf.build/go/protoyaml v0.7.0 // indirect
27+ buf.build/go/spdx v0.2.0 // indirect
28+ buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
29+ cel.dev/cel-go v0.32.0 // indirect
30+ cel.dev/expr v0.25.3 // indirect
31+ connectrpc.com/connect v1.20.0 // indirect
32+ connectrpc.com/otelconnect v0.9.0 // indirect
1533 github.com/Microsoft/go-winio v0.6.2 // indirect
34+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
35+ github.com/bufbuild/buf v1.73.0 // indirect
36+ github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201 // indirect
37+ github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
1638 github.com/cespare/xxhash/v2 v2.3.0 // indirect
39+ github.com/cli/browser v1.3.0 // indirect
1740 github.com/containerd/errdefs/pkg v0.3.0 // indirect
41+ github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
1842 github.com/distribution/reference v0.6.0 // indirect
43+ github.com/docker/cli v29.8.0+incompatible // indirect
44+ github.com/docker/docker-credential-helpers v0.9.9 // indirect
1945 github.com/docker/go-connections v0.8.2 // indirect
2046 github.com/docker/go-units v0.5.0 // indirect
21- github.com/felixge/httpsnoop v1.0.4 // indirect
47+ github.com/felixge/httpsnoop v1.1.0 // indirect
2248 github.com/go-logr/logr v1.4.4 // indirect
2349 github.com/go-logr/stdr v1.2.2 // indirect
50+ github.com/gofrs/flock v0.13.1 // indirect
51+ github.com/google/go-containerregistry v0.22.1 // indirect
52+ github.com/google/uuid v1.6.0 // indirect
53+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
54+ github.com/jdx/go-netrc v1.0.0 // indirect
55+ github.com/klauspost/compress v1.20.0 // indirect
56+ github.com/klauspost/pgzip v1.2.6 // indirect
2457 github.com/mattn/go-colorable v0.1.15 // indirect
25- github.com/mattn/go-isatty v0.0.22 // indirect
58+ github.com/mattn/go-isatty v0.0.24 // indirect
2659 github.com/moby/docker-image-spec v1.3.1 // indirect
2760 github.com/opencontainers/go-digest v1.0.0 // indirect
2861 github.com/opencontainers/image-spec v1.1.1 // indirect
62+ github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect
63+ github.com/quic-go/qpack v0.6.0 // indirect
64+ github.com/quic-go/quic-go v0.62.0 // indirect
65+ github.com/rivo/uniseg v0.4.7 // indirect
66+ github.com/rs/cors v1.11.1 // indirect
2967 github.com/rs/zerolog v1.35.1 // indirect
68+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
69+ github.com/segmentio/asm v1.2.1 // indirect
70+ github.com/segmentio/encoding v0.5.4 // indirect
71+ github.com/sirupsen/logrus v1.10.2 // indirect
72+ github.com/spf13/cobra v1.10.2 // indirect
73+ github.com/spf13/pflag v1.0.10 // indirect
74+ github.com/tetratelabs/wazero v1.12.0 // indirect
75+ github.com/tidwall/btree v1.8.1 // indirect
76+ go.lsp.dev/jsonrpc2 v0.10.0 // indirect
77+ go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
78+ go.lsp.dev/protocol v0.12.0 // indirect
79+ go.lsp.dev/uri v0.3.0 // indirect
3080 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
31- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
32- go.opentelemetry.io/otel v1.45.0 // indirect
33- go.opentelemetry.io/otel/metric v1.45.0 // indirect
34- go.opentelemetry.io/otel/trace v1.45.0 // indirect
81+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
82+ go.opentelemetry.io/otel v1.46.0 // indirect
83+ go.opentelemetry.io/otel/metric v1.46.0 // indirect
84+ go.opentelemetry.io/otel/trace v1.46.0 // indirect
85+ go.uber.org/multierr v1.11.0 // indirect
86+ go.uber.org/zap v1.28.0 // indirect
87+ go.yaml.in/yaml/v3 v3.0.5 // indirect
88+ golang.org/x/crypto v0.56.0 // indirect
89+ golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect
90+ golang.org/x/mod v0.40.0 // indirect
91+ golang.org/x/net v0.58.0 // indirect
92+ golang.org/x/sync v0.23.0 // indirect
93+ golang.org/x/term v0.45.0 // indirect
94+ golang.org/x/text v0.41.0 // indirect
95+ google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4 // indirect
96+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4 // indirect
3597 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
98+ gopkg.in/yaml.v3 v3.0.1 // indirect
99+ mvdan.cc/xurls/v2 v2.6.0 // indirect
100+ pluginrpc.com/pluginrpc v0.5.0 // indirect
101+)
102+
103+tool (
104+ github.com/bufbuild/buf/cmd/buf
105+ google.golang.org/protobuf/cmd/protoc-gen-go
36106 )
@@ -12,25 +12,95 @@ require (
12 )12 )
13 13
14 require (14 require (
15+ buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 // indirect
16+ buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 // indirect
17+ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect
18+ buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1 // indirect
19+ buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2 // indirect
20+ buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 // indirect
21+ buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a // indirect
22+ buf.build/go/bufplugin v0.10.0 // indirect
23+ buf.build/go/bufprivateusage v0.1.0 // indirect
24+ buf.build/go/interrupt v1.1.0 // indirect
25+ buf.build/go/protovalidate v1.4.0 // indirect
26+ buf.build/go/protoyaml v0.7.0 // indirect
27+ buf.build/go/spdx v0.2.0 // indirect
28+ buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect
29+ cel.dev/cel-go v0.32.0 // indirect
30+ cel.dev/expr v0.25.3 // indirect
31+ connectrpc.com/connect v1.20.0 // indirect
32+ connectrpc.com/otelconnect v0.9.0 // indirect
15 github.com/Microsoft/go-winio v0.6.2 // indirect33 github.com/Microsoft/go-winio v0.6.2 // indirect
34+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
35+ github.com/bufbuild/buf v1.73.0 // indirect
36+ github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201 // indirect
37+ github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect
16 github.com/cespare/xxhash/v2 v2.3.0 // indirect38 github.com/cespare/xxhash/v2 v2.3.0 // indirect
39+ github.com/cli/browser v1.3.0 // indirect
17 github.com/containerd/errdefs/pkg v0.3.0 // indirect40 github.com/containerd/errdefs/pkg v0.3.0 // indirect
41+ github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
18 github.com/distribution/reference v0.6.0 // indirect42 github.com/distribution/reference v0.6.0 // indirect
43+ github.com/docker/cli v29.8.0+incompatible // indirect
44+ github.com/docker/docker-credential-helpers v0.9.9 // indirect
19 github.com/docker/go-connections v0.8.2 // indirect45 github.com/docker/go-connections v0.8.2 // indirect
20 github.com/docker/go-units v0.5.0 // indirect46 github.com/docker/go-units v0.5.0 // indirect
21- github.com/felixge/httpsnoop v1.0.4 // indirect47+ github.com/felixge/httpsnoop v1.1.0 // indirect
22 github.com/go-logr/logr v1.4.4 // indirect48 github.com/go-logr/logr v1.4.4 // indirect
23 github.com/go-logr/stdr v1.2.2 // indirect49 github.com/go-logr/stdr v1.2.2 // indirect
50+ github.com/gofrs/flock v0.13.1 // indirect
51+ github.com/google/go-containerregistry v0.22.1 // indirect
52+ github.com/google/uuid v1.6.0 // indirect
53+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
54+ github.com/jdx/go-netrc v1.0.0 // indirect
55+ github.com/klauspost/compress v1.20.0 // indirect
56+ github.com/klauspost/pgzip v1.2.6 // indirect
24 github.com/mattn/go-colorable v0.1.15 // indirect57 github.com/mattn/go-colorable v0.1.15 // indirect
25- github.com/mattn/go-isatty v0.0.22 // indirect58+ github.com/mattn/go-isatty v0.0.24 // indirect
26 github.com/moby/docker-image-spec v1.3.1 // indirect59 github.com/moby/docker-image-spec v1.3.1 // indirect
27 github.com/opencontainers/go-digest v1.0.0 // indirect60 github.com/opencontainers/go-digest v1.0.0 // indirect
28 github.com/opencontainers/image-spec v1.1.1 // indirect61 github.com/opencontainers/image-spec v1.1.1 // indirect
62+ github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect
63+ github.com/quic-go/qpack v0.6.0 // indirect
64+ github.com/quic-go/quic-go v0.62.0 // indirect
65+ github.com/rivo/uniseg v0.4.7 // indirect
66+ github.com/rs/cors v1.11.1 // indirect
29 github.com/rs/zerolog v1.35.1 // indirect67 github.com/rs/zerolog v1.35.1 // indirect
68+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
69+ github.com/segmentio/asm v1.2.1 // indirect
70+ github.com/segmentio/encoding v0.5.4 // indirect
71+ github.com/sirupsen/logrus v1.10.2 // indirect
72+ github.com/spf13/cobra v1.10.2 // indirect
73+ github.com/spf13/pflag v1.0.10 // indirect
74+ github.com/tetratelabs/wazero v1.12.0 // indirect
75+ github.com/tidwall/btree v1.8.1 // indirect
76+ go.lsp.dev/jsonrpc2 v0.10.0 // indirect
77+ go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect
78+ go.lsp.dev/protocol v0.12.0 // indirect
79+ go.lsp.dev/uri v0.3.0 // indirect
30 go.opentelemetry.io/auto/sdk v1.2.1 // indirect80 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
31- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect81+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect
32- go.opentelemetry.io/otel v1.45.0 // indirect82+ go.opentelemetry.io/otel v1.46.0 // indirect
33- go.opentelemetry.io/otel/metric v1.45.0 // indirect83+ go.opentelemetry.io/otel/metric v1.46.0 // indirect
34- go.opentelemetry.io/otel/trace v1.45.0 // indirect84+ go.opentelemetry.io/otel/trace v1.46.0 // indirect
85+ go.uber.org/multierr v1.11.0 // indirect
86+ go.uber.org/zap v1.28.0 // indirect
87+ go.yaml.in/yaml/v3 v3.0.5 // indirect
88+ golang.org/x/crypto v0.56.0 // indirect
89+ golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect
90+ golang.org/x/mod v0.40.0 // indirect
91+ golang.org/x/net v0.58.0 // indirect
92+ golang.org/x/sync v0.23.0 // indirect
93+ golang.org/x/term v0.45.0 // indirect
94+ golang.org/x/text v0.41.0 // indirect
95+ google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4 // indirect
96+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4 // indirect
35 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect97 gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
98+ gopkg.in/yaml.v3 v3.0.1 // indirect
99+ mvdan.cc/xurls/v2 v2.6.0 // indirect
100+ pluginrpc.com/pluginrpc v0.5.0 // indirect
101+)
102+
103+tool (
104+ github.com/bufbuild/buf/cmd/buf
105+ google.golang.org/protobuf/cmd/protoc-gen-go
36 )106 )
M go.sum +178 -20
@@ -1,36 +1,112 @@
1+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 h1:z5+rVknD0AvorY/QV9cb3ohUXw4E7HvxU4+2JUH6EHU=
2+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc=
3+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo=
4+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2/go.mod h1:Gjxz0o7qPHokNsmlX8D+/aKd29NRLRr9CCUkPeMAzJ0=
5+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY=
6+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw=
7+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1 h1:BpToyjv+oz0qldatN/ZFTL+vVtPVavFCY+g4rRu03bs=
8+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1/go.mod h1:hMqlU8iWXqCU7UOBs8PTDG/z7BFnhIknJZ4XMWcBx3E=
9+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2 h1:cik2XvKM1UV8HgLWD2vnBvV4GgT5NfpLJBb9UgLKzzg=
10+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2/go.mod h1:91H8i3iR9GoomNkM9X13RTFL8cVNAHnS44BdgAnbqz4=
11+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 h1:9xw8g8Q6zHLC7CO/jtpl2UK0kQticxIG+mI6N86f2/M=
12+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2/go.mod h1:+0bw+k8FY5MU3tojFh33HeA5xMfGWjAUQpr+maW5Z3o=
13+buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a h1:m3hkeJOeLSq/1yTSMLyV5ipY6u3XrfS3gV8ZIqkUJJA=
14+buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a/go.mod h1:w19H+Lh2cAV/uL1FhcdDlyoxLTm8SkQAHW3CfmXZbFU=
15+buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU=
16+buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I=
17+buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
18+buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=
19+buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
20+buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
21+buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM=
22+buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs=
23+buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38=
24+buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0=
25+buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
26+buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
27+buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2bPPU2Yzp7iAU0Wk9KJ2OoAhNiU=
28+buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU=
29+cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk=
30+cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU=
31+cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0=
32+cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
33+connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
34+connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
35+connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
36+connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
37+connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
38+connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
139 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
240 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
41+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
42+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
43+github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
44+github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
345 github.com/bnema/zerowrap v1.4.1 h1:v70w8LDU2+FU+EX/etY9DjVwOAWN0fUKShwZyZHnyLU=
446 github.com/bnema/zerowrap v1.4.1/go.mod h1:8M8Qz3uRpbszYkXqi0Dnr1TZ4lDrQY8/orGm30zwKKg=
47+github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
48+github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
49+github.com/bufbuild/buf v1.73.0 h1:fDNKlxaf+PYi20dmVM7Yt3kOfXEhwrgKDLMNZFjztDs=
50+github.com/bufbuild/buf v1.73.0/go.mod h1:rfWpi8FmfTPW4CbLFIXZYAux2FWBiCMY6BUOlGqnR84=
51+github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201 h1:jBaIQ1eixwqtjBKJk8gw+dKtmbkp3w8sLJtJ7kav1EE=
52+github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201/go.mod h1:bX3ObJfML+aki7PJevkWLSlOfEZ8EnNsfCz0hiYkhiI=
53+github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
54+github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
555 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
656 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57+github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
58+github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
759 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
860 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
961 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
1062 github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
11-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
12-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
63+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
64+github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
65+github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
1366 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
1467 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
68+github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk=
69+github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
70+github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
71+github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
1572 github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
1673 github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
1774 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
1875 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
19-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
20-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
76+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
77+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
2178 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
2279 github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
2380 github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
2481 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
2582 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
83+github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
84+github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
85+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2686 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2787 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
88+github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
89+github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
2890 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
2991 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
92+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
93+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
94+github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
95+github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
96+github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
97+github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
98+github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
99+github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
100+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
101+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
102+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
103+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
104+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
105+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
30106 github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
31107 github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
32-github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
33-github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
108+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
109+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
34110 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
35111 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
36112 github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
@@ -41,35 +117,117 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
41117 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
42118 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
43119 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
120+github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E=
121+github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
44122 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
45123 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
124+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
125+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4=
126+github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
127+github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
128+github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
129+github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
130+github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8=
131+github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
132+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
133+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
134+github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
135+github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
136+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
137+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
138+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
139+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
46140 github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
47141 github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
48-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
49-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
142+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
143+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
144+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
145+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
146+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
147+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
148+github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
149+github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
150+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
151+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
152+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
153+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
154+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
155+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
156+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
157+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
158+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
159+github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
160+github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
161+go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
162+go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
163+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
164+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
165+go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
166+go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
167+go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
168+go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
50169 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
51170 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
52-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
53-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
54-go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
55-go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
56-go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
57-go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
58-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
59-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
60-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
61-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
62-go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
63-go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
171+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
172+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
173+go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
174+go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
175+go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
176+go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
177+go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
178+go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
179+go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
180+go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
181+go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
182+go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
183+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
184+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
185+go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
186+go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
187+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
188+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
189+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
190+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
191+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
192+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
193+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
194+golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
195+golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
196+golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
197+golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
198+golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
199+golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
200+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
201+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
202+golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
203+golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
64204 golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
65205 golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
206+golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
207+golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
208+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
209+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
210+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
211+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
212+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
213+google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4 h1:NCe/UiklGd/9xjT+ROBVhJ1kf6TRQaFedsR+z7u1gvo=
214+google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4/go.mod h1:fJ2lYaWjqNknJyQBOCd0fA3HnEElJqGplH71a2txi+g=
215+google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4 h1:5t+ZydAFj5kGVLrgCvLmpmCf9ylGRd64hpEronfRaws=
216+google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
66217 google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
67218 google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
219+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
220+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
221+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
68222 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
69223 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
70224 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
71225 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72226 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
73227 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
228+mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
229+mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
74230 pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
75231 pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
232+pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
233+pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
@@ -1,36 +1,112 @@
1+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 h1:z5+rVknD0AvorY/QV9cb3ohUXw4E7HvxU4+2JUH6EHU=
2+buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc=
3+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo=
4+buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2/go.mod h1:Gjxz0o7qPHokNsmlX8D+/aKd29NRLRr9CCUkPeMAzJ0=
5+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 h1:NbnlmV26O7oZ1iM5tsCI+GEx+3ZSdrhvnKQ/eWSrLiY=
6+buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw=
7+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1 h1:BpToyjv+oz0qldatN/ZFTL+vVtPVavFCY+g4rRu03bs=
8+buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260831211851-b4432e12a6e7.1/go.mod h1:hMqlU8iWXqCU7UOBs8PTDG/z7BFnhIknJZ4XMWcBx3E=
9+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2 h1:cik2XvKM1UV8HgLWD2vnBvV4GgT5NfpLJBb9UgLKzzg=
10+buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.12-20260831211851-b4432e12a6e7.2/go.mod h1:91H8i3iR9GoomNkM9X13RTFL8cVNAHnS44BdgAnbqz4=
11+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2 h1:9xw8g8Q6zHLC7CO/jtpl2UK0kQticxIG+mI6N86f2/M=
12+buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.12-20241007202033-cf42259fcbfc.2/go.mod h1:+0bw+k8FY5MU3tojFh33HeA5xMfGWjAUQpr+maW5Z3o=
13+buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a h1:m3hkeJOeLSq/1yTSMLyV5ipY6u3XrfS3gV8ZIqkUJJA=
14+buf.build/go/app v0.2.1-0.20260824172350-b0e76892c61a/go.mod h1:w19H+Lh2cAV/uL1FhcdDlyoxLTm8SkQAHW3CfmXZbFU=
15+buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU=
16+buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I=
17+buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4=
18+buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w=
19+buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
20+buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
21+buf.build/go/protovalidate v1.4.0 h1:UjLrYbt5VX7+TMOs2+pG5FhZhIG1mSfK4EIopbb4LcM=
22+buf.build/go/protovalidate v1.4.0/go.mod h1:8vJfzNT6NIG2qm3uFsJDXMlRmG+bQJzbcIn1Aa0vPGs=
23+buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38=
24+buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0=
25+buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
26+buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8=
27+buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2bPPU2Yzp7iAU0Wk9KJ2OoAhNiU=
28+buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU=
29+cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk=
30+cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU=
31+cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0=
32+cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
33+connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
34+connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
35+connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
36+connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
37+connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
38+connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
1 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=39 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
2 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=40 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
41+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
42+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
43+github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
44+github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
3 github.com/bnema/zerowrap v1.4.1 h1:v70w8LDU2+FU+EX/etY9DjVwOAWN0fUKShwZyZHnyLU=45 github.com/bnema/zerowrap v1.4.1 h1:v70w8LDU2+FU+EX/etY9DjVwOAWN0fUKShwZyZHnyLU=
4 github.com/bnema/zerowrap v1.4.1/go.mod h1:8M8Qz3uRpbszYkXqi0Dnr1TZ4lDrQY8/orGm30zwKKg=46 github.com/bnema/zerowrap v1.4.1/go.mod h1:8M8Qz3uRpbszYkXqi0Dnr1TZ4lDrQY8/orGm30zwKKg=
47+github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
48+github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
49+github.com/bufbuild/buf v1.73.0 h1:fDNKlxaf+PYi20dmVM7Yt3kOfXEhwrgKDLMNZFjztDs=
50+github.com/bufbuild/buf v1.73.0/go.mod h1:rfWpi8FmfTPW4CbLFIXZYAux2FWBiCMY6BUOlGqnR84=
51+github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201 h1:jBaIQ1eixwqtjBKJk8gw+dKtmbkp3w8sLJtJ7kav1EE=
52+github.com/bufbuild/protocompile v0.14.2-0.20260910151042-7436f7c76201/go.mod h1:bX3ObJfML+aki7PJevkWLSlOfEZ8EnNsfCz0hiYkhiI=
53+github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ=
54+github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
5 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=55 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
6 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=56 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
57+github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
58+github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
7 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=59 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
8 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=60 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
9 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=61 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
10 github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=62 github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
11-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=63+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
12-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=64+github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
65+github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
13 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=66 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
14 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=67 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
68+github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk=
69+github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
70+github.com/docker/docker-credential-helpers v0.9.9 h1:BkydjIgZ46JnDbqyM2p2fc63KMw6y+KHL3Em/2AGJ7w=
71+github.com/docker/docker-credential-helpers v0.9.9/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
15 github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=72 github.com/docker/go-connections v0.8.2 h1:9d9DEokx5W7qlr0shkFXIxEA7+TTD+llwzcykSlxMNc=
16 github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=73 github.com/docker/go-connections v0.8.2/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
17 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=74 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
18 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=75 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
19-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=76+github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
20-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=77+github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
21 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=78 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
22 github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=79 github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
23 github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=80 github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
24 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=81 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
25 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=82 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
83+github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
84+github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
85+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=86 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
27 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=87 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
88+github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84=
89+github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0=
28 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=90 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
29 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=91 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
92+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
93+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
94+github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
95+github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
96+github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88=
97+github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM=
98+github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
99+github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
100+github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
101+github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
102+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
103+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
104+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
105+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
30 github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=106 github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
31 github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=107 github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
32-github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=108+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
33-github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=109+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
34 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=110 github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
35 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=111 github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
36 github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=112 github.com/moby/moby/api v1.56.1 h1:PpWkvVPB7Fr/No8w+TfyJ/I6rWZ2YPZhT3JorIg+06c=
@@ -41,35 +117,117 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
41 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=117 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
42 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=118 github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
43 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=119 github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
120+github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E=
121+github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
44 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=122 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
45 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=123 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
124+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4=
125+github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4=
126+github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
127+github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
128+github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
129+github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
130+github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8=
131+github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
132+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
133+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
134+github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
135+github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
136+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
137+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
138+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
139+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
46 github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=140 github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
47 github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=141 github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
48-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=142+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
49-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=143+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
144+github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
145+github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
146+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
147+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
148+github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
149+github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
150+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
151+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
152+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
153+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
154+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
155+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
156+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
157+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
158+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
159+github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
160+github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
161+go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI=
162+go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
163+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE=
164+go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw=
165+go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg=
166+go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ=
167+go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo=
168+go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I=
50 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=169 go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
51 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=170 go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
52-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=171+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
53-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=172+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
54-go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=173+go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
55-go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=174+go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
56-go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=175+go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
57-go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=176+go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
58-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=177+go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
59-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=178+go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
60-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=179+go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
61-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=180+go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
62-go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=181+go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
63-go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=182+go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
183+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
184+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
185+go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
186+go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
187+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
188+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
189+go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
190+go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
191+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
192+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
193+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
194+golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
195+golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
196+golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
197+golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
198+golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
199+golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
200+golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
201+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
202+golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
203+golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
64 golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=204 golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
65 golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=205 golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
206+golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
207+golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
208+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
209+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
210+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
211+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
212+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
213+google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4 h1:NCe/UiklGd/9xjT+ROBVhJ1kf6TRQaFedsR+z7u1gvo=
214+google.golang.org/genproto/googleapis/api v0.0.0-20260904194346-d0f1323225a4/go.mod h1:fJ2lYaWjqNknJyQBOCd0fA3HnEElJqGplH71a2txi+g=
215+google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4 h1:5t+ZydAFj5kGVLrgCvLmpmCf9ylGRd64hpEronfRaws=
216+google.golang.org/genproto/googleapis/rpc v0.0.0-20260904194346-d0f1323225a4/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
66 google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=217 google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
67 google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=218 google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
219+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
220+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
221+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
68 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=222 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
69 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=223 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
70 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=224 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
71 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=225 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=226 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
73 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=227 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
228+mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
229+mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
74 pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=230 pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
75 pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=231 pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
232+pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo=
233+pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o=
M internal/execstream/exec.proto +8 -2
@@ -54,5 +54,11 @@ message Exit {
5454 string error = 2; // set when the executor failed to run the script at all
5555 }
5656
57-// Cancel asks the executor to stop the running script: SIGINT now.
58-message Cancel {}
57+// Cancel asks the executor to stop the running script. The executor escalates
58+// like the official runner: SIGINT to the script's process group at once,
59+// SIGTERM 7.5 s later, SIGKILL 2.5 s after that.
60+message Cancel {
61+ // reason is for the job log and diagnostics ("cancelled by the user",
62+ // "job timeout", ...); it never changes the behaviour.
63+ string reason = 1;
64+}
@@ -54,5 +54,11 @@ message Exit {
54 string error = 2; // set when the executor failed to run the script at all54 string error = 2; // set when the executor failed to run the script at all
55 }55 }
56 56
57-// Cancel asks the executor to stop the running script: SIGINT now.57+// Cancel asks the executor to stop the running script. The executor escalates
58-message Cancel {}58+// like the official runner: SIGINT to the script's process group at once,
59+// SIGTERM 7.5 s later, SIGKILL 2.5 s after that.
60+message Cancel {
61+ // reason is for the job log and diagnostics ("cancelled by the user",
62+ // "job timeout", ...); it never changes the behaviour.
63+ string reason = 1;
64+}
M internal/execstream/execpb/exec.pb.go +19 -6 generated file: collapsed
M internal/execstream/frame.go +24 -13
@@ -9,11 +9,10 @@
99 // transport-agnostic: vsock in a micro-VM, a Unix socket in a container.
1010 package execstream
1111
12-//go:generate protoc --proto_path=. --go_out=execpb --go_opt=paths=source_relative exec.proto
12+//go:generate go tool buf generate ../.. --template ../../buf.gen.yaml
1313
1414 import (
1515 "bufio"
16- "context"
1716 "encoding/binary"
1817 "errors"
1918 "fmt"
@@ -92,10 +91,15 @@ func (c *Conn) Recv() (*execpb.Frame, error) {
9291
9392 // Drive is the host side of a session: it waits for the executor's Hello, sends
9493 // run, passes every Output frame to onOutput, and returns the Exit frame.
95-// When ctx ends first, a Cancel frame is sent once and Drive keeps waiting for
96-// the executor's Exit; the caller bounds that wait by stopping the sandbox,
97-// which closes the stream and makes Drive return an error.
98-func Drive(ctx context.Context, rw io.ReadWriter, run *execpb.Run, onOutput func(*execpb.Output)) (*execpb.Hello, *execpb.Exit, error) {
94+//
95+// cancel, when non-nil, asks the executor to stop the script: the first value
96+// received is sent as one Cancel frame (the executor escalates SIGINT, SIGTERM,
97+// SIGKILL by itself); closing the channel without a value sends nothing. It is
98+// deliberately not a context: cancelling the session's context would also abort
99+// the reads that deliver the executor's Exit. Drive keeps waiting for Exit after
100+// a Cancel; the caller bounds that wait by stopping the sandbox, which closes
101+// the stream and makes Drive return an error.
102+func Drive(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, onOutput func(*execpb.Output)) (*execpb.Hello, *execpb.Exit, error) {
99103 c := NewConn(rw)
100104 first, err := c.Recv()
101105 if err != nil {
@@ -111,13 +115,20 @@ func Drive(ctx context.Context, rw io.ReadWriter, run *execpb.Run, onOutput func
111115
112116 done := make(chan struct{})
113117 defer close(done)
114- go func() {
115- select {
116- case <-ctx.Done():
117- _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Cancel{Cancel: &execpb.Cancel{}}})
118- case <-done:
119- }
120- }()
118+ if cancel != nil {
119+ go func() {
120+ select {
121+ case cn, ok := <-cancel:
122+ if ok {
123+ if cn == nil {
124+ cn = &execpb.Cancel{}
125+ }
126+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Cancel{Cancel: cn}})
127+ }
128+ case <-done:
129+ }
130+ }()
131+ }
121132
122133 for {
123134 f, err := c.Recv()
@@ -9,11 +9,10 @@
9 // transport-agnostic: vsock in a micro-VM, a Unix socket in a container.9 // transport-agnostic: vsock in a micro-VM, a Unix socket in a container.
10 package execstream10 package execstream
11 11
12-//go:generate protoc --proto_path=. --go_out=execpb --go_opt=paths=source_relative exec.proto12+//go:generate go tool buf generate ../.. --template ../../buf.gen.yaml
13 13
14 import (14 import (
15 "bufio"15 "bufio"
16- "context"
17 "encoding/binary"16 "encoding/binary"
18 "errors"17 "errors"
19 "fmt"18 "fmt"
@@ -92,10 +91,15 @@ func (c *Conn) Recv() (*execpb.Frame, error) {
92 91
93 // Drive is the host side of a session: it waits for the executor's Hello, sends92 // Drive is the host side of a session: it waits for the executor's Hello, sends
94 // run, passes every Output frame to onOutput, and returns the Exit frame.93 // run, passes every Output frame to onOutput, and returns the Exit frame.
95-// When ctx ends first, a Cancel frame is sent once and Drive keeps waiting for94+//
96-// the executor's Exit; the caller bounds that wait by stopping the sandbox,95+// cancel, when non-nil, asks the executor to stop the script: the first value
97-// which closes the stream and makes Drive return an error.96+// received is sent as one Cancel frame (the executor escalates SIGINT, SIGTERM,
98-func Drive(ctx context.Context, rw io.ReadWriter, run *execpb.Run, onOutput func(*execpb.Output)) (*execpb.Hello, *execpb.Exit, error) {97+// SIGKILL by itself); closing the channel without a value sends nothing. It is
98+// deliberately not a context: cancelling the session's context would also abort
99+// the reads that deliver the executor's Exit. Drive keeps waiting for Exit after
100+// a Cancel; the caller bounds that wait by stopping the sandbox, which closes
101+// the stream and makes Drive return an error.
102+func Drive(rw io.ReadWriter, run *execpb.Run, cancel <-chan *execpb.Cancel, onOutput func(*execpb.Output)) (*execpb.Hello, *execpb.Exit, error) {
99 c := NewConn(rw)103 c := NewConn(rw)
100 first, err := c.Recv()104 first, err := c.Recv()
101 if err != nil {105 if err != nil {
@@ -111,13 +115,20 @@ func Drive(ctx context.Context, rw io.ReadWriter, run *execpb.Run, onOutput func
111 115
112 done := make(chan struct{})116 done := make(chan struct{})
113 defer close(done)117 defer close(done)
114- go func() {118+ if cancel != nil {
115- select {119+ go func() {
116- case <-ctx.Done():120+ select {
117- _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Cancel{Cancel: &execpb.Cancel{}}})121+ case cn, ok := <-cancel:
118- case <-done:122+ if ok {
119- }123+ if cn == nil {
120- }()124+ cn = &execpb.Cancel{}
125+ }
126+ _ = c.Send(&execpb.Frame{Body: &execpb.Frame_Cancel{Cancel: cn}})
127+ }
128+ case <-done:
129+ }
130+ }()
131+ }
121 132
122 for {133 for {
123 f, err := c.Recv()134 f, err := c.Recv()
M internal/execstream/frame_test.go +6 -7
@@ -4,7 +4,6 @@ package execstream
44
55 import (
66 "bytes"
7- "context"
87 "encoding/binary"
98 "errors"
109 "io"
@@ -93,7 +92,7 @@ func TestDrive(t *testing.T) {
9392 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})
9493 }()
9594 var got []byte
96- hello, exit, err := Drive(context.Background(), host, &execpb.Run{Script: "echo hi"}, func(o *execpb.Output) { got = append(got, o.Data...) })
95+ hello, exit, err := Drive(host, &execpb.Run{Script: "echo hi"}, nil, func(o *execpb.Output) { got = append(got, o.Data...) })
9796 if err != nil {
9897 t.Fatal(err)
9998 }
@@ -112,12 +111,12 @@ func TestDriveCancelSendsCancel(t *testing.T) {
112111 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
113112 _, _ = g.Recv() // run
114113 f, err := g.Recv()
115- gotCancel <- err == nil && f.GetCancel() != nil
114+ gotCancel <- err == nil && f.GetCancel() != nil && f.GetCancel().Reason == "cancelled by the user"
116115 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})
117116 }()
118- ctx, cancel := context.WithCancel(context.Background())
119- time.AfterFunc(50*time.Millisecond, cancel)
120- _, exit, err := Drive(ctx, host, &execpb.Run{}, nil)
117+ cancel := make(chan *execpb.Cancel, 1)
118+ time.AfterFunc(50*time.Millisecond, func() { cancel <- &execpb.Cancel{Reason: "cancelled by the user"} })
119+ _, exit, err := Drive(host, &execpb.Run{}, cancel, nil)
121120 if err != nil || exit.Code != 130 {
122121 t.Fatalf("exit=%v err=%v", exit, err)
123122 }
@@ -133,7 +132,7 @@ func TestDriveHelloMissing(t *testing.T) {
133132 defer func() { _ = guest.Close() }()
134133 _ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
135134 }()
136- if _, _, err := Drive(context.Background(), host, &execpb.Run{}, nil); err == nil {
135+ if _, _, err := Drive(host, &execpb.Run{}, nil, nil); err == nil {
137136 t.Fatal("want error when the first frame is not hello")
138137 }
139138 }
@@ -4,7 +4,6 @@ package execstream
4 4
5 import (5 import (
6 "bytes"6 "bytes"
7- "context"
8 "encoding/binary"7 "encoding/binary"
9 "errors"8 "errors"
10 "io"9 "io"
@@ -93,7 +92,7 @@ func TestDrive(t *testing.T) {
93 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})92 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})
94 }()93 }()
95 var got []byte94 var got []byte
96- hello, exit, err := Drive(context.Background(), host, &execpb.Run{Script: "echo hi"}, func(o *execpb.Output) { got = append(got, o.Data...) })95+ hello, exit, err := Drive(host, &execpb.Run{Script: "echo hi"}, nil, func(o *execpb.Output) { got = append(got, o.Data...) })
97 if err != nil {96 if err != nil {
98 t.Fatal(err)97 t.Fatal(err)
99 }98 }
@@ -112,12 +111,12 @@ func TestDriveCancelSendsCancel(t *testing.T) {
112 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})111 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
113 _, _ = g.Recv() // run112 _, _ = g.Recv() // run
114 f, err := g.Recv()113 f, err := g.Recv()
115- gotCancel <- err == nil && f.GetCancel() != nil114+ gotCancel <- err == nil && f.GetCancel() != nil && f.GetCancel().Reason == "cancelled by the user"
116 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})115 _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})
117 }()116 }()
118- ctx, cancel := context.WithCancel(context.Background())117+ cancel := make(chan *execpb.Cancel, 1)
119- time.AfterFunc(50*time.Millisecond, cancel)118+ time.AfterFunc(50*time.Millisecond, func() { cancel <- &execpb.Cancel{Reason: "cancelled by the user"} })
120- _, exit, err := Drive(ctx, host, &execpb.Run{}, nil)119+ _, exit, err := Drive(host, &execpb.Run{}, cancel, nil)
121 if err != nil || exit.Code != 130 {120 if err != nil || exit.Code != 130 {
122 t.Fatalf("exit=%v err=%v", exit, err)121 t.Fatalf("exit=%v err=%v", exit, err)
123 }122 }
@@ -133,7 +132,7 @@ func TestDriveHelloMissing(t *testing.T) {
133 defer func() { _ = guest.Close() }()132 defer func() { _ = guest.Close() }()
134 _ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})133 _ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
135 }()134 }()
136- if _, _, err := Drive(context.Background(), host, &execpb.Run{}, nil); err == nil {135+ if _, _, err := Drive(host, &execpb.Run{}, nil, nil); err == nil {
137 t.Fatal("want error when the first frame is not hello")136 t.Fatal("want error when the first frame is not hello")
138 }137 }
139 }138 }
M internal/guest/exec.go +144 -43
@@ -20,6 +20,17 @@ import (
2020 // DefaultScript is what the stub executor runs when the host sends no script.
2121 const DefaultScript = "uname -a; id; cat /etc/os-release; docker info"
2222
23+// Cancellation timings, like the official runner's ProcessInvoker: SIGINT, then
24+// SIGTERM after 7.5 s, then SIGKILL 2.5 s later.
25+const (
26+ DefaultTermAfter = 7500 * time.Millisecond
27+ DefaultKillAfter = 2500 * time.Millisecond
28+ // DefaultWaitDelay bounds how long the executor waits for the output pipes
29+ // to close after the script's shell exited (background children may hold
30+ // them open).
31+ DefaultWaitDelay = 500 * time.Millisecond
32+)
33+
2334 // ExecConfig configures the executor.
2435 type ExecConfig struct {
2536 Target Target
@@ -31,22 +42,48 @@ type ExecConfig struct {
3142 ConnectTimeout time.Duration
3243 // RunTimeout bounds the wait for the host's Run frame (default 60 s).
3344 RunTimeout time.Duration
45+ // TermAfter and KillAfter are the escalation delays after a Cancel (or after
46+ // the executor's own context ends): SIGINT at once, SIGTERM after TermAfter,
47+ // SIGKILL KillAfter later. WaitDelay is the pipe-drain grace after the shell
48+ // exits. Zero means the defaults above.
49+ TermAfter, KillAfter, WaitDelay time.Duration
50+
51+ // signal sends a signal to a process group; tests replace it to observe the
52+ // escalation. Default: syscall.Kill(-pgid, sig).
53+ signal func(pgid int, sig syscall.Signal)
54+}
55+
56+func (c *ExecConfig) defaults() {
57+ if c.Shell == "" {
58+ c.Shell = "/bin/sh"
59+ }
60+ if c.ConnectTimeout == 0 {
61+ c.ConnectTimeout = 30 * time.Second
62+ }
63+ if c.RunTimeout == 0 {
64+ c.RunTimeout = 60 * time.Second
65+ }
66+ if c.TermAfter == 0 {
67+ c.TermAfter = DefaultTermAfter
68+ }
69+ if c.KillAfter == 0 {
70+ c.KillAfter = DefaultKillAfter
71+ }
72+ if c.WaitDelay == 0 {
73+ c.WaitDelay = DefaultWaitDelay
74+ }
75+ if c.signal == nil {
76+ c.signal = func(pgid int, sig syscall.Signal) { _ = syscall.Kill(-pgid, sig) }
77+ }
3478 }
3579
3680 // Exec is `ermite-runner exec`, the CI-1 stub executor: it connects back to the
3781 // host, receives one Run, runs the script with the shell, streams stdout and
3882 // stderr as Output frames, and ends with an Exit frame. It returns the exit code
39-// it reported. A Cancel frame sends SIGINT to the script's process group.
83+// it reported. A Cancel frame, or the end of ctx, stops the script's process
84+// group with escalating signals (see ExecConfig).
4085 func Exec(ctx context.Context, cfg ExecConfig) (int, error) {
41- if cfg.Shell == "" {
42- cfg.Shell = "/bin/sh"
43- }
44- if cfg.ConnectTimeout == 0 {
45- cfg.ConnectTimeout = 30 * time.Second
46- }
47- if cfg.RunTimeout == 0 {
48- cfg.RunTimeout = 60 * time.Second
49- }
86+ cfg.defaults()
5087 raw, err := cfg.Target.Dial(ctx, cfg.ConnectTimeout)
5188 if err != nil {
5289 return 1, err
@@ -55,6 +92,85 @@ func Exec(ctx context.Context, cfg ExecConfig) (int, error) {
5592 return serve(ctx, raw, cfg)
5693 }
5794
95+// frameWriter turns writes into Output frames of at most ChunkSize bytes. It is
96+// the command's Stdout/Stderr, so no goroutine of ours sits between the pipe
97+// and the stream.
98+type frameWriter struct {
99+ conn *execstream.Conn
100+ kind execpb.StreamKind
101+}
102+
103+func (w frameWriter) Write(p []byte) (int, error) {
104+ n := 0
105+ for len(p) > 0 {
106+ chunk := p
107+ if len(chunk) > execstream.ChunkSize {
108+ chunk = chunk[:execstream.ChunkSize]
109+ }
110+ err := w.conn.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{
111+ Stream: w.kind, Data: append([]byte(nil), chunk...), UnixNano: time.Now().UnixNano(),
112+ }}})
113+ if err != nil {
114+ return n, err
115+ }
116+ n += len(chunk)
117+ p = p[len(chunk):]
118+ }
119+ return n, nil
120+}
121+
122+// escalator signals a process group and escalates after a cancel. Once finish
123+// has been called (the shell was reaped) it never signals again: the group id
124+// could have been reused by then.
125+type escalator struct {
126+ cfg ExecConfig
127+ pgid int
128+
129+ mu sync.Mutex
130+ finished bool
131+ started bool
132+ timers []*time.Timer
133+}
134+
135+func (e *escalator) send(sig syscall.Signal) {
136+ e.mu.Lock()
137+ defer e.mu.Unlock()
138+ if !e.finished {
139+ e.cfg.signal(e.pgid, sig)
140+ }
141+}
142+
143+// cancel starts the escalation; only the first call does anything.
144+func (e *escalator) cancel() {
145+ e.mu.Lock()
146+ if e.finished || e.started {
147+ e.mu.Unlock()
148+ return
149+ }
150+ e.started = true
151+ e.cfg.signal(e.pgid, syscall.SIGINT)
152+ e.timers = append(e.timers,
153+ time.AfterFunc(e.cfg.TermAfter, func() { e.send(syscall.SIGTERM) }),
154+ time.AfterFunc(e.cfg.TermAfter+e.cfg.KillAfter, func() { e.send(syscall.SIGKILL) }),
155+ )
156+ e.mu.Unlock()
157+}
158+
159+// finish is called right after the shell was reaped: it kills what is left of
160+// the group (background children), stops the timers and forbids further signals.
161+func (e *escalator) finish() {
162+ e.mu.Lock()
163+ defer e.mu.Unlock()
164+ if e.finished {
165+ return
166+ }
167+ e.cfg.signal(e.pgid, syscall.SIGKILL)
168+ e.finished = true
169+ for _, t := range e.timers {
170+ t.Stop()
171+ }
172+}
173+
58174 func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, error) {
59175 conn := execstream.NewConn(rw)
60176 host, _ := os.Hostname()
@@ -96,58 +212,43 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err
96212 cmd.Env = append(cmd.Env, k+"="+v)
97213 }
98214 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
99- stdout, err := cmd.StdoutPipe()
100- if err != nil {
101- return sendExit(conn, 1, err)
102- }
103- stderr, err := cmd.StderrPipe()
104- if err != nil {
105- return sendExit(conn, 1, err)
106- }
215+ // Frame writers as stdout/stderr: exec copies the pipes for us, and
216+ // WaitDelay bounds the wait when a background child keeps a pipe open.
217+ cmd.Stdout = frameWriter{conn, execpb.StreamKind_STREAM_KIND_STDOUT}
218+ cmd.Stderr = frameWriter{conn, execpb.StreamKind_STREAM_KIND_STDERR}
219+ cmd.WaitDelay = cfg.WaitDelay
107220 if err := cmd.Start(); err != nil {
108221 return sendExit(conn, 127, fmt.Errorf("start %s: %w", cfg.Shell, err))
109222 }
223+ esc := &escalator{cfg: cfg, pgid: cmd.Process.Pid}
110224
111- // Cancel frames arrive while the script runs.
225+ // Frames from the host while the script runs: a Cancel starts the escalation;
226+ // a lost host kills the script rather than let it run unobserved.
112227 go func() {
113228 for {
114229 f, err := conn.Recv()
115230 if err != nil {
116- // Host went away: stop the script rather than run unobserved.
117- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
231+ esc.send(syscall.SIGKILL)
118232 return
119233 }
120234 if f.GetCancel() != nil {
121- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)
235+ esc.cancel()
122236 }
123237 }
124238 }()
239+ // The executor's own context (SIGTERM forwarded by init) cancels the same way.
240+ stopCtx := context.AfterFunc(ctx, esc.cancel)
241+ defer stopCtx()
125242
126- var wg sync.WaitGroup
127- pump := func(r io.Reader, kind execpb.StreamKind) {
128- defer wg.Done()
129- buf := make([]byte, execstream.ChunkSize)
130- for {
131- n, err := r.Read(buf)
132- if n > 0 {
133- _ = conn.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{
134- Stream: kind, Data: append([]byte(nil), buf[:n]...), UnixNano: time.Now().UnixNano(),
135- }}})
136- }
137- if err != nil {
138- return
139- }
140- }
141- }
142- wg.Add(2)
143- go pump(stdout, execpb.StreamKind_STREAM_KIND_STDOUT)
144- go pump(stderr, execpb.StreamKind_STREAM_KIND_STDERR)
145- wg.Wait() // pipes must be drained before Wait closes them
146243 waitErr := cmd.Wait()
244+ esc.finish()
245+
147246 code := 0
148247 var ee *exec.ExitError
149248 switch {
150- case waitErr == nil:
249+ case waitErr == nil, errors.Is(waitErr, exec.ErrWaitDelay):
250+ // ErrWaitDelay: the shell exited cleanly but a background child kept the
251+ // pipes open; the group was killed above.
151252 case errors.As(waitErr, &ee):
152253 code = ee.ExitCode()
153254 if code < 0 { // killed by a signal: 128+signal like a shell
@@ -20,6 +20,17 @@ import (
20 // DefaultScript is what the stub executor runs when the host sends no script.20 // DefaultScript is what the stub executor runs when the host sends no script.
21 const DefaultScript = "uname -a; id; cat /etc/os-release; docker info"21 const DefaultScript = "uname -a; id; cat /etc/os-release; docker info"
22 22
23+// Cancellation timings, like the official runner's ProcessInvoker: SIGINT, then
24+// SIGTERM after 7.5 s, then SIGKILL 2.5 s later.
25+const (
26+ DefaultTermAfter = 7500 * time.Millisecond
27+ DefaultKillAfter = 2500 * time.Millisecond
28+ // DefaultWaitDelay bounds how long the executor waits for the output pipes
29+ // to close after the script's shell exited (background children may hold
30+ // them open).
31+ DefaultWaitDelay = 500 * time.Millisecond
32+)
33+
23 // ExecConfig configures the executor.34 // ExecConfig configures the executor.
24 type ExecConfig struct {35 type ExecConfig struct {
25 Target Target36 Target Target
@@ -31,22 +42,48 @@ type ExecConfig struct {
31 ConnectTimeout time.Duration42 ConnectTimeout time.Duration
32 // RunTimeout bounds the wait for the host's Run frame (default 60 s).43 // RunTimeout bounds the wait for the host's Run frame (default 60 s).
33 RunTimeout time.Duration44 RunTimeout time.Duration
45+ // TermAfter and KillAfter are the escalation delays after a Cancel (or after
46+ // the executor's own context ends): SIGINT at once, SIGTERM after TermAfter,
47+ // SIGKILL KillAfter later. WaitDelay is the pipe-drain grace after the shell
48+ // exits. Zero means the defaults above.
49+ TermAfter, KillAfter, WaitDelay time.Duration
50+
51+ // signal sends a signal to a process group; tests replace it to observe the
52+ // escalation. Default: syscall.Kill(-pgid, sig).
53+ signal func(pgid int, sig syscall.Signal)
54+}
55+
56+func (c *ExecConfig) defaults() {
57+ if c.Shell == "" {
58+ c.Shell = "/bin/sh"
59+ }
60+ if c.ConnectTimeout == 0 {
61+ c.ConnectTimeout = 30 * time.Second
62+ }
63+ if c.RunTimeout == 0 {
64+ c.RunTimeout = 60 * time.Second
65+ }
66+ if c.TermAfter == 0 {
67+ c.TermAfter = DefaultTermAfter
68+ }
69+ if c.KillAfter == 0 {
70+ c.KillAfter = DefaultKillAfter
71+ }
72+ if c.WaitDelay == 0 {
73+ c.WaitDelay = DefaultWaitDelay
74+ }
75+ if c.signal == nil {
76+ c.signal = func(pgid int, sig syscall.Signal) { _ = syscall.Kill(-pgid, sig) }
77+ }
34 }78 }
35 79
36 // Exec is `ermite-runner exec`, the CI-1 stub executor: it connects back to the80 // Exec is `ermite-runner exec`, the CI-1 stub executor: it connects back to the
37 // host, receives one Run, runs the script with the shell, streams stdout and81 // host, receives one Run, runs the script with the shell, streams stdout and
38 // stderr as Output frames, and ends with an Exit frame. It returns the exit code82 // stderr as Output frames, and ends with an Exit frame. It returns the exit code
39-// it reported. A Cancel frame sends SIGINT to the script's process group.83+// it reported. A Cancel frame, or the end of ctx, stops the script's process
84+// group with escalating signals (see ExecConfig).
40 func Exec(ctx context.Context, cfg ExecConfig) (int, error) {85 func Exec(ctx context.Context, cfg ExecConfig) (int, error) {
41- if cfg.Shell == "" {86+ cfg.defaults()
42- cfg.Shell = "/bin/sh"
43- }
44- if cfg.ConnectTimeout == 0 {
45- cfg.ConnectTimeout = 30 * time.Second
46- }
47- if cfg.RunTimeout == 0 {
48- cfg.RunTimeout = 60 * time.Second
49- }
50 raw, err := cfg.Target.Dial(ctx, cfg.ConnectTimeout)87 raw, err := cfg.Target.Dial(ctx, cfg.ConnectTimeout)
51 if err != nil {88 if err != nil {
52 return 1, err89 return 1, err
@@ -55,6 +92,85 @@ func Exec(ctx context.Context, cfg ExecConfig) (int, error) {
55 return serve(ctx, raw, cfg)92 return serve(ctx, raw, cfg)
56 }93 }
57 94
95+// frameWriter turns writes into Output frames of at most ChunkSize bytes. It is
96+// the command's Stdout/Stderr, so no goroutine of ours sits between the pipe
97+// and the stream.
98+type frameWriter struct {
99+ conn *execstream.Conn
100+ kind execpb.StreamKind
101+}
102+
103+func (w frameWriter) Write(p []byte) (int, error) {
104+ n := 0
105+ for len(p) > 0 {
106+ chunk := p
107+ if len(chunk) > execstream.ChunkSize {
108+ chunk = chunk[:execstream.ChunkSize]
109+ }
110+ err := w.conn.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{
111+ Stream: w.kind, Data: append([]byte(nil), chunk...), UnixNano: time.Now().UnixNano(),
112+ }}})
113+ if err != nil {
114+ return n, err
115+ }
116+ n += len(chunk)
117+ p = p[len(chunk):]
118+ }
119+ return n, nil
120+}
121+
122+// escalator signals a process group and escalates after a cancel. Once finish
123+// has been called (the shell was reaped) it never signals again: the group id
124+// could have been reused by then.
125+type escalator struct {
126+ cfg ExecConfig
127+ pgid int
128+
129+ mu sync.Mutex
130+ finished bool
131+ started bool
132+ timers []*time.Timer
133+}
134+
135+func (e *escalator) send(sig syscall.Signal) {
136+ e.mu.Lock()
137+ defer e.mu.Unlock()
138+ if !e.finished {
139+ e.cfg.signal(e.pgid, sig)
140+ }
141+}
142+
143+// cancel starts the escalation; only the first call does anything.
144+func (e *escalator) cancel() {
145+ e.mu.Lock()
146+ if e.finished || e.started {
147+ e.mu.Unlock()
148+ return
149+ }
150+ e.started = true
151+ e.cfg.signal(e.pgid, syscall.SIGINT)
152+ e.timers = append(e.timers,
153+ time.AfterFunc(e.cfg.TermAfter, func() { e.send(syscall.SIGTERM) }),
154+ time.AfterFunc(e.cfg.TermAfter+e.cfg.KillAfter, func() { e.send(syscall.SIGKILL) }),
155+ )
156+ e.mu.Unlock()
157+}
158+
159+// finish is called right after the shell was reaped: it kills what is left of
160+// the group (background children), stops the timers and forbids further signals.
161+func (e *escalator) finish() {
162+ e.mu.Lock()
163+ defer e.mu.Unlock()
164+ if e.finished {
165+ return
166+ }
167+ e.cfg.signal(e.pgid, syscall.SIGKILL)
168+ e.finished = true
169+ for _, t := range e.timers {
170+ t.Stop()
171+ }
172+}
173+
58 func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, error) {174 func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, error) {
59 conn := execstream.NewConn(rw)175 conn := execstream.NewConn(rw)
60 host, _ := os.Hostname()176 host, _ := os.Hostname()
@@ -96,58 +212,43 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err
96 cmd.Env = append(cmd.Env, k+"="+v)212 cmd.Env = append(cmd.Env, k+"="+v)
97 }213 }
98 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}214 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
99- stdout, err := cmd.StdoutPipe()215+ // Frame writers as stdout/stderr: exec copies the pipes for us, and
100- if err != nil {216+ // WaitDelay bounds the wait when a background child keeps a pipe open.
101- return sendExit(conn, 1, err)217+ cmd.Stdout = frameWriter{conn, execpb.StreamKind_STREAM_KIND_STDOUT}
102- }218+ cmd.Stderr = frameWriter{conn, execpb.StreamKind_STREAM_KIND_STDERR}
103- stderr, err := cmd.StderrPipe()219+ cmd.WaitDelay = cfg.WaitDelay
104- if err != nil {
105- return sendExit(conn, 1, err)
106- }
107 if err := cmd.Start(); err != nil {220 if err := cmd.Start(); err != nil {
108 return sendExit(conn, 127, fmt.Errorf("start %s: %w", cfg.Shell, err))221 return sendExit(conn, 127, fmt.Errorf("start %s: %w", cfg.Shell, err))
109 }222 }
223+ esc := &escalator{cfg: cfg, pgid: cmd.Process.Pid}
110 224
111- // Cancel frames arrive while the script runs.225+ // Frames from the host while the script runs: a Cancel starts the escalation;
226+ // a lost host kills the script rather than let it run unobserved.
112 go func() {227 go func() {
113 for {228 for {
114 f, err := conn.Recv()229 f, err := conn.Recv()
115 if err != nil {230 if err != nil {
116- // Host went away: stop the script rather than run unobserved.231+ esc.send(syscall.SIGKILL)
117- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
118 return232 return
119 }233 }
120 if f.GetCancel() != nil {234 if f.GetCancel() != nil {
121- _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT)235+ esc.cancel()
122 }236 }
123 }237 }
124 }()238 }()
239+ // The executor's own context (SIGTERM forwarded by init) cancels the same way.
240+ stopCtx := context.AfterFunc(ctx, esc.cancel)
241+ defer stopCtx()
125 242
126- var wg sync.WaitGroup
127- pump := func(r io.Reader, kind execpb.StreamKind) {
128- defer wg.Done()
129- buf := make([]byte, execstream.ChunkSize)
130- for {
131- n, err := r.Read(buf)
132- if n > 0 {
133- _ = conn.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{
134- Stream: kind, Data: append([]byte(nil), buf[:n]...), UnixNano: time.Now().UnixNano(),
135- }}})
136- }
137- if err != nil {
138- return
139- }
140- }
141- }
142- wg.Add(2)
143- go pump(stdout, execpb.StreamKind_STREAM_KIND_STDOUT)
144- go pump(stderr, execpb.StreamKind_STREAM_KIND_STDERR)
145- wg.Wait() // pipes must be drained before Wait closes them
146 waitErr := cmd.Wait()243 waitErr := cmd.Wait()
244+ esc.finish()
245+
147 code := 0246 code := 0
148 var ee *exec.ExitError247 var ee *exec.ExitError
149 switch {248 switch {
150- case waitErr == nil:249+ case waitErr == nil, errors.Is(waitErr, exec.ErrWaitDelay):
250+ // ErrWaitDelay: the shell exited cleanly but a background child kept the
251+ // pipes open; the group was killed above.
151 case errors.As(waitErr, &ee):252 case errors.As(waitErr, &ee):
152 code = ee.ExitCode()253 code = ee.ExitCode()
153 if code < 0 { // killed by a signal: 128+signal like a shell254 if code < 0 { // killed by a signal: 128+signal like a shell
M internal/guest/guest_test.go +172 -22
@@ -4,10 +4,15 @@ package guest
44
55 import (
66 "context"
7+ "fmt"
78 "net"
9+ "os"
810 "path/filepath"
911 "reflect"
12+ "strconv"
1013 "strings"
14+ "sync"
15+ "sync/atomic"
1116 "syscall"
1217 "testing"
1318 "time"
@@ -45,7 +50,7 @@ func TestParseTarget(t *testing.T) {
4550 }
4651
4752 // host plays the host process on a unix socket and runs one session.
48-func host(t *testing.T, script string, ctxFn func(context.Context) context.Context) (out, errOut string, exit *execpb.Exit, hello *execpb.Hello, code int, execErr error) {
53+func host(t *testing.T, script string, cancelAfter time.Duration, tweak func(*ExecConfig)) (out, errOut string, exit *execpb.Exit, hello *execpb.Hello, code int, execErr error) {
4954 t.Helper()
5055 sock := filepath.Join(t.TempDir(), "s")
5156 ln, err := net.Listen("unix", sock)
@@ -59,7 +64,11 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
5964 }
6065 done := make(chan res, 1)
6166 go func() {
62- c, err := Exec(context.Background(), ExecConfig{Target: Target{"unix", sock}, Sandbox: "container", Version: "test", Shell: "/bin/sh", RunTimeout: 5 * time.Second})
67+ cfg := ExecConfig{Target: Target{"unix", sock}, Sandbox: "container", Version: "test", Shell: "/bin/sh", RunTimeout: 5 * time.Second}
68+ if tweak != nil {
69+ tweak(&cfg)
70+ }
71+ c, err := Exec(context.Background(), cfg)
6372 done <- res{c, err}
6473 }()
6574 conn, err := ln.Accept()
@@ -67,12 +76,13 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
6776 t.Fatal(err)
6877 }
6978 defer func() { _ = conn.Close() }()
70- ctx := context.Background()
71- if ctxFn != nil {
72- ctx = ctxFn(ctx)
79+ var cancel chan *execpb.Cancel
80+ if cancelAfter > 0 {
81+ cancel = make(chan *execpb.Cancel, 1)
82+ time.AfterFunc(cancelAfter, func() { cancel <- &execpb.Cancel{Reason: "test"} })
7383 }
7484 var so, se strings.Builder
75- hello, exit, err = execstream.Drive(ctx, conn, &execpb.Run{Script: script, Env: map[string]string{"STUB_VAR": "from-host"}}, func(o *execpb.Output) {
85+ hello, exit, err = execstream.Drive(conn, &execpb.Run{Script: script, Env: map[string]string{"STUB_VAR": "from-host"}}, cancel, func(o *execpb.Output) {
7686 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
7787 se.Write(o.Data)
7888 } else {
@@ -87,7 +97,7 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
8797 }
8898
8999 func TestExecStreamsOutputAndExitCode(t *testing.T) {
90- out, errOut, exit, hello, code, err := host(t, `echo out-line; echo err-line >&2; echo "$STUB_VAR"; exit 3`, nil)
100+ out, errOut, exit, hello, code, err := host(t, `echo out-line; echo err-line >&2; echo "$STUB_VAR"; exit 3`, 0, nil)
91101 if err != nil {
92102 t.Fatal(err)
93103 }
@@ -105,7 +115,7 @@ func TestExecStreamsOutputAndExitCode(t *testing.T) {
105115 func TestExecDefaultScriptRunsDiagnostics(t *testing.T) {
106116 // The default script is the CI-1 stub: uname, id, os-release, docker info.
107117 // docker may be absent here: only the layout of the output is checked.
108- out, errOut, exit, _, _, _ := host(t, "", nil)
118+ out, errOut, exit, _, _, _ := host(t, "", 0, nil)
109119 if !strings.Contains(out, "Linux") || !strings.Contains(out, "uid=") {
110120 t.Fatalf("uname/id missing: out=%q err=%q", out, errOut)
111121 }
@@ -118,7 +128,7 @@ func TestExecDefaultScriptRunsDiagnostics(t *testing.T) {
118128 }
119129
120130 func TestExecLargeOutputIsChunkedAndComplete(t *testing.T) {
121- out, _, exit, _, _, _ := host(t, `head -c 300000 /dev/zero | tr '\0' 'x'`, nil)
131+ out, _, exit, _, _, _ := host(t, `head -c 300000 /dev/zero | tr '\0' 'x'`, 0, nil)
122132 if len(out) != 300000 || exit.Code != 0 {
123133 t.Fatalf("len=%d exit=%v", len(out), exit)
124134 }
@@ -126,11 +136,7 @@ func TestExecLargeOutputIsChunkedAndComplete(t *testing.T) {
126136
127137 func TestExecCancelSendsSIGINT(t *testing.T) {
128138 start := time.Now()
129- out, _, exit, _, _, _ := host(t, `trap 'echo got-int; exit 130' INT; echo ready; while :; do sleep 0.05; done`, func(ctx context.Context) context.Context {
130- c, cancel := context.WithCancel(ctx)
131- time.AfterFunc(500*time.Millisecond, cancel)
132- return c
133- })
139+ out, _, exit, _, _, _ := host(t, `trap 'echo got-int; exit 130' INT; echo ready; while :; do sleep 0.05; done`, 500*time.Millisecond, nil)
134140 if !strings.Contains(out, "got-int") || exit.Code != 130 {
135141 t.Fatalf("out=%q exit=%v", out, exit)
136142 }
@@ -139,8 +145,123 @@ func TestExecCancelSendsSIGINT(t *testing.T) {
139145 }
140146 }
141147
148+// sigRecorder replaces the signal function: it records (signal, time since
149+// start) and still delivers the signal.
150+type sigRecorder struct {
151+ mu sync.Mutex
152+ start time.Time
153+ got []string
154+ times []time.Duration
155+}
156+
157+func (r *sigRecorder) fn(pgid int, sig syscall.Signal) {
158+ r.mu.Lock()
159+ r.got = append(r.got, sig.String())
160+ r.times = append(r.times, time.Since(r.start))
161+ r.mu.Unlock()
162+ _ = syscall.Kill(-pgid, sig)
163+}
164+
165+// C1: a script that ignores SIGINT and SIGTERM is stopped by the escalation:
166+// SIGINT at once, SIGTERM after TermAfter, SIGKILL KillAfter later.
167+func TestExecCancelEscalatesToKill(t *testing.T) {
168+ rec := &sigRecorder{start: time.Now()}
169+ termAfter, killAfter := 400*time.Millisecond, 300*time.Millisecond
170+ _, _, exit, _, _, _ := host(t, `trap '' INT TERM; echo ready; while :; do sleep 0.05; done`, 300*time.Millisecond, func(c *ExecConfig) {
171+ c.TermAfter, c.KillAfter, c.signal = termAfter, killAfter, rec.fn
172+ })
173+ if exit.Code != 128+int32(syscall.SIGKILL) {
174+ t.Fatalf("a script ignoring INT and TERM must end by SIGKILL, exit=%v", exit)
175+ }
176+ rec.mu.Lock()
177+ defer rec.mu.Unlock()
178+ // SIGINT, SIGTERM, SIGKILL (from the escalation), then the final group kill
179+ // issued after the shell was reaped.
180+ if len(rec.got) < 3 || rec.got[0] != "interrupt" || rec.got[1] != "terminated" || rec.got[2] != "killed" {
181+ t.Fatalf("signals: %v", rec.got)
182+ }
183+ cancelAt := rec.times[0]
184+ if d := rec.times[1] - cancelAt; d < termAfter-50*time.Millisecond || d > termAfter+400*time.Millisecond {
185+ t.Errorf("SIGTERM %v after SIGINT, want about %v", d, termAfter)
186+ }
187+ if d := rec.times[2] - rec.times[1]; d < killAfter-50*time.Millisecond || d > killAfter+400*time.Millisecond {
188+ t.Errorf("SIGKILL %v after SIGTERM, want about %v", d, killAfter)
189+ }
190+}
191+
192+// C1 + C9: a script that exits on SIGINT stops the escalation: the timers are
193+// stopped when Wait returns, so no SIGTERM/SIGKILL follows, and nothing is
194+// signalled after the shell was reaped except the final group sweep.
195+func TestExecEscalationStopsWhenScriptExits(t *testing.T) {
196+ rec := &sigRecorder{start: time.Now()}
197+ _, _, exit, _, _, _ := host(t, `trap 'exit 130' INT; while :; do sleep 0.05; done`, 200*time.Millisecond, func(c *ExecConfig) {
198+ c.TermAfter, c.KillAfter, c.signal = 300*time.Millisecond, 300*time.Millisecond, rec.fn
199+ })
200+ if exit.Code != 130 {
201+ t.Fatalf("exit=%v", exit)
202+ }
203+ time.Sleep(1200 * time.Millisecond) // well past TermAfter+KillAfter
204+ rec.mu.Lock()
205+ defer rec.mu.Unlock()
206+ if len(rec.got) != 2 || rec.got[0] != "interrupt" || rec.got[1] != "killed" {
207+ t.Fatalf("want SIGINT then the final sweep only, got %v", rec.got)
208+ }
209+}
210+
211+// C9: after the shell was reaped the escalator never signals again, even if its
212+// timers had fired concurrently.
213+func TestEscalatorNeverSignalsAfterFinish(t *testing.T) {
214+ var n atomic.Int32
215+ e := &escalator{cfg: ExecConfig{TermAfter: time.Millisecond, KillAfter: time.Millisecond, signal: func(int, syscall.Signal) { n.Add(1) }}, pgid: 1 << 30}
216+ e.cancel()
217+ e.finish()
218+ after := n.Load()
219+ time.Sleep(50 * time.Millisecond)
220+ e.send(syscall.SIGTERM)
221+ e.cancel()
222+ if n.Load() != after {
223+ t.Fatalf("signalled %d times after finish", n.Load()-after)
224+ }
225+}
226+
227+// C2: a background child that keeps stdout open must not hang the executor: it
228+// reports the script's exit promptly and the child is killed.
229+func TestExecBackgroundChildDoesNotHoldTheSession(t *testing.T) {
230+ marker := filepath.Join(t.TempDir(), "child.pid")
231+ start := time.Now()
232+ out, _, exit, _, _, _ := host(t, `sleep 30 & echo $! > `+marker+`; echo parent-done`, 0, func(c *ExecConfig) { c.WaitDelay = 300 * time.Millisecond })
233+ if time.Since(start) > 5*time.Second {
234+ t.Fatalf("the session was held by a background child for %v", time.Since(start))
235+ }
236+ if exit == nil || exit.Code != 0 || !strings.Contains(out, "parent-done") {
237+ t.Fatalf("exit=%v out=%q", exit, out)
238+ }
239+ b, err := os.ReadFile(marker)
240+ if err != nil {
241+ t.Fatal(err)
242+ }
243+ pid, _ := strconv.Atoi(strings.TrimSpace(string(b)))
244+ deadline := time.Now().Add(3 * time.Second)
245+ for time.Now().Before(deadline) {
246+ if st, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err != nil || strings.Contains(string(st), ") Z") {
247+ return
248+ }
249+ time.Sleep(50 * time.Millisecond)
250+ }
251+ t.Fatalf("background child %d survived the job", pid)
252+}
253+
254+// C2: output written by the script before the shell exits is complete even when
255+// the stream is large and a background child holds the pipe.
256+func TestExecOutputCompleteWithBackgroundChild(t *testing.T) {
257+ out, _, exit, _, _, _ := host(t, `sleep 30 & head -c 200000 /dev/zero | tr '\0' 'y'`, 0, func(c *ExecConfig) { c.WaitDelay = 300 * time.Millisecond })
258+ if exit.Code != 0 || len(out) != 200000 {
259+ t.Fatalf("len=%d exit=%v", len(out), exit)
260+ }
261+}
262+
142263 func TestExecKilledBySignalReports128PlusSignal(t *testing.T) {
143- _, _, exit, _, _, _ := host(t, `kill -TERM $$`, nil)
264+ _, _, exit, _, _, _ := host(t, `kill -TERM $$`, 0, nil)
144265 if exit.Code != 128+int32(syscall.SIGTERM) {
145266 t.Fatalf("exit=%v", exit)
146267 }
@@ -156,7 +277,7 @@ func TestExecMissingShell(t *testing.T) {
156277 go func() {
157278 c, _ := ln.Accept()
158279 defer func() { _ = c.Close() }()
159- _, exit, _ := execstream.Drive(context.Background(), c, &execpb.Run{Script: "true"}, nil)
280+ _, exit, _ := execstream.Drive(c, &execpb.Run{Script: "true"}, nil, nil)
160281 if exit == nil || exit.Code != 127 || exit.Error == "" {
161282 t.Errorf("want 127 with an error message, got %v", exit)
162283 }
@@ -233,14 +354,13 @@ func TestInitInContainerSkipsVMSetup(t *testing.T) {
233354 return
234355 }
235356 defer func() { _ = c.Close() }()
236- _, _, _ = execstream.Drive(context.Background(), c, &execpb.Run{Script: "exit 0"}, nil)
357+ _, _, _ = execstream.Drive(c, &execpb.Run{Script: "exit 0"}, nil, nil)
237358 }()
238359 code := Init(context.Background(), InitConfig{
239- Cmdline: "ermite.sandbox=vm ermite.connect=unix:" + sock,
240- Getpid: func() int { return 4242 },
241- ExecArgs: []string{"/bin/sh", "-c", "exit 5"},
242- DockerTimeout: time.Millisecond,
243- SkipDocker: true,
360+ Cmdline: "ermite.sandbox=vm ermite.connect=unix:" + sock,
361+ Getpid: func() int { return 4242 },
362+ ExecArgs: []string{"/bin/sh", "-c", "exit 5"},
363+ SkipDocker: true,
244364 })
245365 // ExecArgs is a stand-in for the executor: its exit code is Init's.
246366 if code != 5 {
@@ -259,3 +379,33 @@ func TestWriteResolvConfNeedsGateway(t *testing.T) {
259379 t.Fatal("empty gateway accepted")
260380 }
261381 }
382+
383+// C11: init forwards SIGTERM to the executor and finishes with the executor's
384+// own exit code, instead of dying with its child still running.
385+func TestInitForwardsSIGTERMToTheExecutor(t *testing.T) {
386+ sigs := make(chan os.Signal, 1)
387+ done := make(chan int, 1)
388+ marker := filepath.Join(t.TempDir(), "got-term")
389+ go func() {
390+ done <- Init(context.Background(), InitConfig{
391+ Cmdline: "ermite.sandbox=container ermite.connect=unix:/nonexistent",
392+ Getpid: func() int { return 4242 },
393+ SkipDocker: true,
394+ Signals: sigs,
395+ ExecArgs: []string{"/bin/sh", "-c", `trap 'echo term > ` + marker + `; exit 9' TERM; while :; do sleep 0.05; done`},
396+ })
397+ }()
398+ time.Sleep(500 * time.Millisecond)
399+ sigs <- syscall.SIGTERM
400+ select {
401+ case code := <-done:
402+ if code != 9 {
403+ t.Fatalf("init must return the executor's exit code, got %d", code)
404+ }
405+ case <-time.After(10 * time.Second):
406+ t.Fatal("init did not finish after SIGTERM")
407+ }
408+ if _, err := os.Stat(marker); err != nil {
409+ t.Fatal("the executor never received SIGTERM")
410+ }
411+}
@@ -4,10 +4,15 @@ package guest
4 4
5 import (5 import (
6 "context"6 "context"
7+ "fmt"
7 "net"8 "net"
9+ "os"
8 "path/filepath"10 "path/filepath"
9 "reflect"11 "reflect"
12+ "strconv"
10 "strings"13 "strings"
14+ "sync"
15+ "sync/atomic"
11 "syscall"16 "syscall"
12 "testing"17 "testing"
13 "time"18 "time"
@@ -45,7 +50,7 @@ func TestParseTarget(t *testing.T) {
45 }50 }
46 51
47 // host plays the host process on a unix socket and runs one session.52 // host plays the host process on a unix socket and runs one session.
48-func host(t *testing.T, script string, ctxFn func(context.Context) context.Context) (out, errOut string, exit *execpb.Exit, hello *execpb.Hello, code int, execErr error) {53+func host(t *testing.T, script string, cancelAfter time.Duration, tweak func(*ExecConfig)) (out, errOut string, exit *execpb.Exit, hello *execpb.Hello, code int, execErr error) {
49 t.Helper()54 t.Helper()
50 sock := filepath.Join(t.TempDir(), "s")55 sock := filepath.Join(t.TempDir(), "s")
51 ln, err := net.Listen("unix", sock)56 ln, err := net.Listen("unix", sock)
@@ -59,7 +64,11 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
59 }64 }
60 done := make(chan res, 1)65 done := make(chan res, 1)
61 go func() {66 go func() {
62- c, err := Exec(context.Background(), ExecConfig{Target: Target{"unix", sock}, Sandbox: "container", Version: "test", Shell: "/bin/sh", RunTimeout: 5 * time.Second})67+ cfg := ExecConfig{Target: Target{"unix", sock}, Sandbox: "container", Version: "test", Shell: "/bin/sh", RunTimeout: 5 * time.Second}
68+ if tweak != nil {
69+ tweak(&cfg)
70+ }
71+ c, err := Exec(context.Background(), cfg)
63 done <- res{c, err}72 done <- res{c, err}
64 }()73 }()
65 conn, err := ln.Accept()74 conn, err := ln.Accept()
@@ -67,12 +76,13 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
67 t.Fatal(err)76 t.Fatal(err)
68 }77 }
69 defer func() { _ = conn.Close() }()78 defer func() { _ = conn.Close() }()
70- ctx := context.Background()79+ var cancel chan *execpb.Cancel
71- if ctxFn != nil {80+ if cancelAfter > 0 {
72- ctx = ctxFn(ctx)81+ cancel = make(chan *execpb.Cancel, 1)
82+ time.AfterFunc(cancelAfter, func() { cancel <- &execpb.Cancel{Reason: "test"} })
73 }83 }
74 var so, se strings.Builder84 var so, se strings.Builder
75- hello, exit, err = execstream.Drive(ctx, conn, &execpb.Run{Script: script, Env: map[string]string{"STUB_VAR": "from-host"}}, func(o *execpb.Output) {85+ hello, exit, err = execstream.Drive(conn, &execpb.Run{Script: script, Env: map[string]string{"STUB_VAR": "from-host"}}, cancel, func(o *execpb.Output) {
76 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {86 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
77 se.Write(o.Data)87 se.Write(o.Data)
78 } else {88 } else {
@@ -87,7 +97,7 @@ func host(t *testing.T, script string, ctxFn func(context.Context) context.Conte
87 }97 }
88 98
89 func TestExecStreamsOutputAndExitCode(t *testing.T) {99 func TestExecStreamsOutputAndExitCode(t *testing.T) {
90- out, errOut, exit, hello, code, err := host(t, `echo out-line; echo err-line >&2; echo "$STUB_VAR"; exit 3`, nil)100+ out, errOut, exit, hello, code, err := host(t, `echo out-line; echo err-line >&2; echo "$STUB_VAR"; exit 3`, 0, nil)
91 if err != nil {101 if err != nil {
92 t.Fatal(err)102 t.Fatal(err)
93 }103 }
@@ -105,7 +115,7 @@ func TestExecStreamsOutputAndExitCode(t *testing.T) {
105 func TestExecDefaultScriptRunsDiagnostics(t *testing.T) {115 func TestExecDefaultScriptRunsDiagnostics(t *testing.T) {
106 // The default script is the CI-1 stub: uname, id, os-release, docker info.116 // The default script is the CI-1 stub: uname, id, os-release, docker info.
107 // docker may be absent here: only the layout of the output is checked.117 // docker may be absent here: only the layout of the output is checked.
108- out, errOut, exit, _, _, _ := host(t, "", nil)118+ out, errOut, exit, _, _, _ := host(t, "", 0, nil)
109 if !strings.Contains(out, "Linux") || !strings.Contains(out, "uid=") {119 if !strings.Contains(out, "Linux") || !strings.Contains(out, "uid=") {
110 t.Fatalf("uname/id missing: out=%q err=%q", out, errOut)120 t.Fatalf("uname/id missing: out=%q err=%q", out, errOut)
111 }121 }
@@ -118,7 +128,7 @@ func TestExecDefaultScriptRunsDiagnostics(t *testing.T) {
118 }128 }
119 129
120 func TestExecLargeOutputIsChunkedAndComplete(t *testing.T) {130 func TestExecLargeOutputIsChunkedAndComplete(t *testing.T) {
121- out, _, exit, _, _, _ := host(t, `head -c 300000 /dev/zero | tr '\0' 'x'`, nil)131+ out, _, exit, _, _, _ := host(t, `head -c 300000 /dev/zero | tr '\0' 'x'`, 0, nil)
122 if len(out) != 300000 || exit.Code != 0 {132 if len(out) != 300000 || exit.Code != 0 {
123 t.Fatalf("len=%d exit=%v", len(out), exit)133 t.Fatalf("len=%d exit=%v", len(out), exit)
124 }134 }
@@ -126,11 +136,7 @@ func TestExecLargeOutputIsChunkedAndComplete(t *testing.T) {
126 136
127 func TestExecCancelSendsSIGINT(t *testing.T) {137 func TestExecCancelSendsSIGINT(t *testing.T) {
128 start := time.Now()138 start := time.Now()
129- out, _, exit, _, _, _ := host(t, `trap 'echo got-int; exit 130' INT; echo ready; while :; do sleep 0.05; done`, func(ctx context.Context) context.Context {139+ out, _, exit, _, _, _ := host(t, `trap 'echo got-int; exit 130' INT; echo ready; while :; do sleep 0.05; done`, 500*time.Millisecond, nil)
130- c, cancel := context.WithCancel(ctx)
131- time.AfterFunc(500*time.Millisecond, cancel)
132- return c
133- })
134 if !strings.Contains(out, "got-int") || exit.Code != 130 {140 if !strings.Contains(out, "got-int") || exit.Code != 130 {
135 t.Fatalf("out=%q exit=%v", out, exit)141 t.Fatalf("out=%q exit=%v", out, exit)
136 }142 }
@@ -139,8 +145,123 @@ func TestExecCancelSendsSIGINT(t *testing.T) {
139 }145 }
140 }146 }
141 147
148+// sigRecorder replaces the signal function: it records (signal, time since
149+// start) and still delivers the signal.
150+type sigRecorder struct {
151+ mu sync.Mutex
152+ start time.Time
153+ got []string
154+ times []time.Duration
155+}
156+
157+func (r *sigRecorder) fn(pgid int, sig syscall.Signal) {
158+ r.mu.Lock()
159+ r.got = append(r.got, sig.String())
160+ r.times = append(r.times, time.Since(r.start))
161+ r.mu.Unlock()
162+ _ = syscall.Kill(-pgid, sig)
163+}
164+
165+// C1: a script that ignores SIGINT and SIGTERM is stopped by the escalation:
166+// SIGINT at once, SIGTERM after TermAfter, SIGKILL KillAfter later.
167+func TestExecCancelEscalatesToKill(t *testing.T) {
168+ rec := &sigRecorder{start: time.Now()}
169+ termAfter, killAfter := 400*time.Millisecond, 300*time.Millisecond
170+ _, _, exit, _, _, _ := host(t, `trap '' INT TERM; echo ready; while :; do sleep 0.05; done`, 300*time.Millisecond, func(c *ExecConfig) {
171+ c.TermAfter, c.KillAfter, c.signal = termAfter, killAfter, rec.fn
172+ })
173+ if exit.Code != 128+int32(syscall.SIGKILL) {
174+ t.Fatalf("a script ignoring INT and TERM must end by SIGKILL, exit=%v", exit)
175+ }
176+ rec.mu.Lock()
177+ defer rec.mu.Unlock()
178+ // SIGINT, SIGTERM, SIGKILL (from the escalation), then the final group kill
179+ // issued after the shell was reaped.
180+ if len(rec.got) < 3 || rec.got[0] != "interrupt" || rec.got[1] != "terminated" || rec.got[2] != "killed" {
181+ t.Fatalf("signals: %v", rec.got)
182+ }
183+ cancelAt := rec.times[0]
184+ if d := rec.times[1] - cancelAt; d < termAfter-50*time.Millisecond || d > termAfter+400*time.Millisecond {
185+ t.Errorf("SIGTERM %v after SIGINT, want about %v", d, termAfter)
186+ }
187+ if d := rec.times[2] - rec.times[1]; d < killAfter-50*time.Millisecond || d > killAfter+400*time.Millisecond {
188+ t.Errorf("SIGKILL %v after SIGTERM, want about %v", d, killAfter)
189+ }
190+}
191+
192+// C1 + C9: a script that exits on SIGINT stops the escalation: the timers are
193+// stopped when Wait returns, so no SIGTERM/SIGKILL follows, and nothing is
194+// signalled after the shell was reaped except the final group sweep.
195+func TestExecEscalationStopsWhenScriptExits(t *testing.T) {
196+ rec := &sigRecorder{start: time.Now()}
197+ _, _, exit, _, _, _ := host(t, `trap 'exit 130' INT; while :; do sleep 0.05; done`, 200*time.Millisecond, func(c *ExecConfig) {
198+ c.TermAfter, c.KillAfter, c.signal = 300*time.Millisecond, 300*time.Millisecond, rec.fn
199+ })
200+ if exit.Code != 130 {
201+ t.Fatalf("exit=%v", exit)
202+ }
203+ time.Sleep(1200 * time.Millisecond) // well past TermAfter+KillAfter
204+ rec.mu.Lock()
205+ defer rec.mu.Unlock()
206+ if len(rec.got) != 2 || rec.got[0] != "interrupt" || rec.got[1] != "killed" {
207+ t.Fatalf("want SIGINT then the final sweep only, got %v", rec.got)
208+ }
209+}
210+
211+// C9: after the shell was reaped the escalator never signals again, even if its
212+// timers had fired concurrently.
213+func TestEscalatorNeverSignalsAfterFinish(t *testing.T) {
214+ var n atomic.Int32
215+ e := &escalator{cfg: ExecConfig{TermAfter: time.Millisecond, KillAfter: time.Millisecond, signal: func(int, syscall.Signal) { n.Add(1) }}, pgid: 1 << 30}
216+ e.cancel()
217+ e.finish()
218+ after := n.Load()
219+ time.Sleep(50 * time.Millisecond)
220+ e.send(syscall.SIGTERM)
221+ e.cancel()
222+ if n.Load() != after {
223+ t.Fatalf("signalled %d times after finish", n.Load()-after)
224+ }
225+}
226+
227+// C2: a background child that keeps stdout open must not hang the executor: it
228+// reports the script's exit promptly and the child is killed.
229+func TestExecBackgroundChildDoesNotHoldTheSession(t *testing.T) {
230+ marker := filepath.Join(t.TempDir(), "child.pid")
231+ start := time.Now()
232+ out, _, exit, _, _, _ := host(t, `sleep 30 & echo $! > `+marker+`; echo parent-done`, 0, func(c *ExecConfig) { c.WaitDelay = 300 * time.Millisecond })
233+ if time.Since(start) > 5*time.Second {
234+ t.Fatalf("the session was held by a background child for %v", time.Since(start))
235+ }
236+ if exit == nil || exit.Code != 0 || !strings.Contains(out, "parent-done") {
237+ t.Fatalf("exit=%v out=%q", exit, out)
238+ }
239+ b, err := os.ReadFile(marker)
240+ if err != nil {
241+ t.Fatal(err)
242+ }
243+ pid, _ := strconv.Atoi(strings.TrimSpace(string(b)))
244+ deadline := time.Now().Add(3 * time.Second)
245+ for time.Now().Before(deadline) {
246+ if st, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); err != nil || strings.Contains(string(st), ") Z") {
247+ return
248+ }
249+ time.Sleep(50 * time.Millisecond)
250+ }
251+ t.Fatalf("background child %d survived the job", pid)
252+}
253+
254+// C2: output written by the script before the shell exits is complete even when
255+// the stream is large and a background child holds the pipe.
256+func TestExecOutputCompleteWithBackgroundChild(t *testing.T) {
257+ out, _, exit, _, _, _ := host(t, `sleep 30 & head -c 200000 /dev/zero | tr '\0' 'y'`, 0, func(c *ExecConfig) { c.WaitDelay = 300 * time.Millisecond })
258+ if exit.Code != 0 || len(out) != 200000 {
259+ t.Fatalf("len=%d exit=%v", len(out), exit)
260+ }
261+}
262+
142 func TestExecKilledBySignalReports128PlusSignal(t *testing.T) {263 func TestExecKilledBySignalReports128PlusSignal(t *testing.T) {
143- _, _, exit, _, _, _ := host(t, `kill -TERM $$`, nil)264+ _, _, exit, _, _, _ := host(t, `kill -TERM $$`, 0, nil)
144 if exit.Code != 128+int32(syscall.SIGTERM) {265 if exit.Code != 128+int32(syscall.SIGTERM) {
145 t.Fatalf("exit=%v", exit)266 t.Fatalf("exit=%v", exit)
146 }267 }
@@ -156,7 +277,7 @@ func TestExecMissingShell(t *testing.T) {
156 go func() {277 go func() {
157 c, _ := ln.Accept()278 c, _ := ln.Accept()
158 defer func() { _ = c.Close() }()279 defer func() { _ = c.Close() }()
159- _, exit, _ := execstream.Drive(context.Background(), c, &execpb.Run{Script: "true"}, nil)280+ _, exit, _ := execstream.Drive(c, &execpb.Run{Script: "true"}, nil, nil)
160 if exit == nil || exit.Code != 127 || exit.Error == "" {281 if exit == nil || exit.Code != 127 || exit.Error == "" {
161 t.Errorf("want 127 with an error message, got %v", exit)282 t.Errorf("want 127 with an error message, got %v", exit)
162 }283 }
@@ -233,14 +354,13 @@ func TestInitInContainerSkipsVMSetup(t *testing.T) {
233 return354 return
234 }355 }
235 defer func() { _ = c.Close() }()356 defer func() { _ = c.Close() }()
236- _, _, _ = execstream.Drive(context.Background(), c, &execpb.Run{Script: "exit 0"}, nil)357+ _, _, _ = execstream.Drive(c, &execpb.Run{Script: "exit 0"}, nil, nil)
237 }()358 }()
238 code := Init(context.Background(), InitConfig{359 code := Init(context.Background(), InitConfig{
239- Cmdline: "ermite.sandbox=vm ermite.connect=unix:" + sock,360+ Cmdline: "ermite.sandbox=vm ermite.connect=unix:" + sock,
240- Getpid: func() int { return 4242 },361+ Getpid: func() int { return 4242 },
241- ExecArgs: []string{"/bin/sh", "-c", "exit 5"},362+ ExecArgs: []string{"/bin/sh", "-c", "exit 5"},
242- DockerTimeout: time.Millisecond,363+ SkipDocker: true,
243- SkipDocker: true,
244 })364 })
245 // ExecArgs is a stand-in for the executor: its exit code is Init's.365 // ExecArgs is a stand-in for the executor: its exit code is Init's.
246 if code != 5 {366 if code != 5 {
@@ -259,3 +379,33 @@ func TestWriteResolvConfNeedsGateway(t *testing.T) {
259 t.Fatal("empty gateway accepted")379 t.Fatal("empty gateway accepted")
260 }380 }
261 }381 }
382+
383+// C11: init forwards SIGTERM to the executor and finishes with the executor's
384+// own exit code, instead of dying with its child still running.
385+func TestInitForwardsSIGTERMToTheExecutor(t *testing.T) {
386+ sigs := make(chan os.Signal, 1)
387+ done := make(chan int, 1)
388+ marker := filepath.Join(t.TempDir(), "got-term")
389+ go func() {
390+ done <- Init(context.Background(), InitConfig{
391+ Cmdline: "ermite.sandbox=container ermite.connect=unix:/nonexistent",
392+ Getpid: func() int { return 4242 },
393+ SkipDocker: true,
394+ Signals: sigs,
395+ ExecArgs: []string{"/bin/sh", "-c", `trap 'echo term > ` + marker + `; exit 9' TERM; while :; do sleep 0.05; done`},
396+ })
397+ }()
398+ time.Sleep(500 * time.Millisecond)
399+ sigs <- syscall.SIGTERM
400+ select {
401+ case code := <-done:
402+ if code != 9 {
403+ t.Fatalf("init must return the executor's exit code, got %d", code)
404+ }
405+ case <-time.After(10 * time.Second):
406+ t.Fatal("init did not finish after SIGTERM")
407+ }
408+ if _, err := os.Stat(marker); err != nil {
409+ t.Fatal("the executor never received SIGTERM")
410+ }
411+}
M internal/guest/init.go +29 -21
@@ -6,6 +6,7 @@ import (
66 "context"
77 "fmt"
88 "os"
9+ "os/signal"
910 "strings"
1011 "syscall"
1112 "time"
@@ -24,8 +25,9 @@ type InitConfig struct {
2425 Cmdline string
2526 // Getpid reports the process id; Init only acts as a VM's PID 1 when it is 1.
2627 Getpid func() int
27- // DockerTimeout bounds the wait for the Docker socket (default 60 s).
28- DockerTimeout time.Duration
28+ // Signals delivers the termination signals to forward to the executor;
29+ // nil means "subscribe to SIGTERM and SIGINT". Tests inject their own.
30+ Signals <-chan os.Signal
2931 // SkipDocker does not start dockerd (tests, images without Docker).
3032 SkipDocker bool
3133 // ExecArgs is the executor command line; default
@@ -51,9 +53,6 @@ func Init(ctx context.Context, cfg InitConfig) int {
5153 if cfg.Getpid == nil {
5254 cfg.Getpid = os.Getpid
5355 }
54- if cfg.DockerTimeout == 0 {
55- cfg.DockerTimeout = 60 * time.Second
56- }
5756 if cfg.ExecArgs == nil {
5857 cfg.ExecArgs = []string{sandbox.GuestExecDir + "/" + sandbox.GuestExecName, "exec"}
5958 }
@@ -134,11 +133,32 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
134133 log.Error().Err(err).Msg("init: start executor")
135134 return 1
136135 }
137- // Docker is awaited in the background: the executor starts right away and
138- // the stub script's `docker info` waits for the socket itself.
139- if dockerPid != 0 {
140- go waitSocket(ctx, "/var/run/docker.sock", cfg.DockerTimeout)
136+ // SIGTERM/SIGINT (docker stop, or the host stopping the VM) go to the
137+ // executor, which cancels its script with the usual escalation and reports
138+ // its Exit; init then finishes normally instead of dying with children.
139+ sigs := cfg.Signals
140+ if sigs == nil {
141+ ch := make(chan os.Signal, 4)
142+ signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
143+ defer signal.Stop(ch)
144+ sigs = ch
141145 }
146+ stopFwd := make(chan struct{})
147+ defer close(stopFwd)
148+ go func() {
149+ for {
150+ select {
151+ case sig := <-sigs:
152+ if s, ok := sig.(syscall.Signal); ok {
153+ log.Info().Str("signal", s.String()).Msg("init: forwarding the signal to the executor")
154+ _ = syscall.Kill(-execPid, s) // the executor leads its own process group
155+ _ = syscall.Kill(execPid, s)
156+ }
157+ case <-stopFwd:
158+ return
159+ }
160+ }
161+ }()
142162 code := reapUntil(execPid)
143163 if dockerPid != 0 {
144164 _ = syscall.Kill(dockerPid, syscall.SIGTERM)
@@ -154,18 +174,6 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
154174 return code
155175 }
156176
157-func waitSocket(ctx context.Context, path string, timeout time.Duration) {
158- log := zerowrap.FromCtx(ctx)
159- deadline := time.Now().Add(timeout)
160- for time.Now().Before(deadline) {
161- if _, err := os.Stat(path); err == nil {
162- return
163- }
164- time.Sleep(200 * time.Millisecond)
165- }
166- log.Warn().Str("socket", path).Msg("init: docker socket did not appear")
167-}
168-
169177 // spawn starts argv[0] as a child (own process group) without os/exec, so that
170178 // reapUntil is the only waiter and orphans can be reaped safely.
171179 func spawn(argv, env []string) (int, error) {
@@ -6,6 +6,7 @@ import (
6 "context"6 "context"
7 "fmt"7 "fmt"
8 "os"8 "os"
9+ "os/signal"
9 "strings"10 "strings"
10 "syscall"11 "syscall"
11 "time"12 "time"
@@ -24,8 +25,9 @@ type InitConfig struct {
24 Cmdline string25 Cmdline string
25 // Getpid reports the process id; Init only acts as a VM's PID 1 when it is 1.26 // Getpid reports the process id; Init only acts as a VM's PID 1 when it is 1.
26 Getpid func() int27 Getpid func() int
27- // DockerTimeout bounds the wait for the Docker socket (default 60 s).28+ // Signals delivers the termination signals to forward to the executor;
28- DockerTimeout time.Duration29+ // nil means "subscribe to SIGTERM and SIGINT". Tests inject their own.
30+ Signals <-chan os.Signal
29 // SkipDocker does not start dockerd (tests, images without Docker).31 // SkipDocker does not start dockerd (tests, images without Docker).
30 SkipDocker bool32 SkipDocker bool
31 // ExecArgs is the executor command line; default33 // ExecArgs is the executor command line; default
@@ -51,9 +53,6 @@ func Init(ctx context.Context, cfg InitConfig) int {
51 if cfg.Getpid == nil {53 if cfg.Getpid == nil {
52 cfg.Getpid = os.Getpid54 cfg.Getpid = os.Getpid
53 }55 }
54- if cfg.DockerTimeout == 0 {
55- cfg.DockerTimeout = 60 * time.Second
56- }
57 if cfg.ExecArgs == nil {56 if cfg.ExecArgs == nil {
58 cfg.ExecArgs = []string{sandbox.GuestExecDir + "/" + sandbox.GuestExecName, "exec"}57 cfg.ExecArgs = []string{sandbox.GuestExecDir + "/" + sandbox.GuestExecName, "exec"}
59 }58 }
@@ -134,11 +133,32 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
134 log.Error().Err(err).Msg("init: start executor")133 log.Error().Err(err).Msg("init: start executor")
135 return 1134 return 1
136 }135 }
137- // Docker is awaited in the background: the executor starts right away and136+ // SIGTERM/SIGINT (docker stop, or the host stopping the VM) go to the
138- // the stub script's `docker info` waits for the socket itself.137+ // executor, which cancels its script with the usual escalation and reports
139- if dockerPid != 0 {138+ // its Exit; init then finishes normally instead of dying with children.
140- go waitSocket(ctx, "/var/run/docker.sock", cfg.DockerTimeout)139+ sigs := cfg.Signals
140+ if sigs == nil {
141+ ch := make(chan os.Signal, 4)
142+ signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
143+ defer signal.Stop(ch)
144+ sigs = ch
141 }145 }
146+ stopFwd := make(chan struct{})
147+ defer close(stopFwd)
148+ go func() {
149+ for {
150+ select {
151+ case sig := <-sigs:
152+ if s, ok := sig.(syscall.Signal); ok {
153+ log.Info().Str("signal", s.String()).Msg("init: forwarding the signal to the executor")
154+ _ = syscall.Kill(-execPid, s) // the executor leads its own process group
155+ _ = syscall.Kill(execPid, s)
156+ }
157+ case <-stopFwd:
158+ return
159+ }
160+ }
161+ }()
142 code := reapUntil(execPid)162 code := reapUntil(execPid)
143 if dockerPid != 0 {163 if dockerPid != 0 {
144 _ = syscall.Kill(dockerPid, syscall.SIGTERM)164 _ = syscall.Kill(dockerPid, syscall.SIGTERM)
@@ -154,18 +174,6 @@ func runPayload(ctx context.Context, cfg InitConfig, mode string, args map[strin
154 return code174 return code
155 }175 }
156 176
157-func waitSocket(ctx context.Context, path string, timeout time.Duration) {
158- log := zerowrap.FromCtx(ctx)
159- deadline := time.Now().Add(timeout)
160- for time.Now().Before(deadline) {
161- if _, err := os.Stat(path); err == nil {
162- return
163- }
164- time.Sleep(200 * time.Millisecond)
165- }
166- log.Warn().Str("socket", path).Msg("init: docker socket did not appear")
167-}
168-
169 // spawn starts argv[0] as a child (own process group) without os/exec, so that177 // spawn starts argv[0] as a child (own process group) without os/exec, so that
170 // reapUntil is the only waiter and orphans can be reaped safely.178 // reapUntil is the only waiter and orphans can be reaped safely.
171 func spawn(argv, env []string) (int, error) {179 func spawn(argv, env []string) (int, error) {
M internal/sandbox/container/container_test.go +1 -1
@@ -242,7 +242,7 @@ func (e *cenv) leftovers() []string {
242242 func stub(t *testing.T, conn sandbox.Conn) (string, int) {
243243 t.Helper()
244244 var out strings.Builder
245- _, exit, err := execstream.Drive(context.Background(), conn, &execpb.Run{}, func(o *execpb.Output) { out.Write(o.Data) })
245+ _, exit, err := execstream.Drive(conn, &execpb.Run{}, nil, func(o *execpb.Output) { out.Write(o.Data) })
246246 if err != nil {
247247 t.Fatal(err)
248248 }
@@ -242,7 +242,7 @@ func (e *cenv) leftovers() []string {
242 func stub(t *testing.T, conn sandbox.Conn) (string, int) {242 func stub(t *testing.T, conn sandbox.Conn) (string, int) {
243 t.Helper()243 t.Helper()
244 var out strings.Builder244 var out strings.Builder
245- _, exit, err := execstream.Drive(context.Background(), conn, &execpb.Run{}, func(o *execpb.Output) { out.Write(o.Data) })245+ _, exit, err := execstream.Drive(conn, &execpb.Run{}, nil, func(o *execpb.Output) { out.Write(o.Data) })
246 if err != nil {246 if err != nil {
247 t.Fatal(err)247 t.Fatal(err)
248 }248 }
M internal/sandbox/container/docker_api_test.go +1 -1
@@ -73,7 +73,7 @@ func TestDockerAPIWiring(t *testing.T) {
7373 t.Fatalf("job %d: %v", i, err)
7474 }
7575 var out strings.Builder
76- hello, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: "echo wired; id -u; exit 4"}, func(o *execpb.Output) { out.Write(o.Data) })
76+ hello, exit, err := execstream.Drive(conn, &execpb.Run{Script: "echo wired; id -u; exit 4"}, nil, func(o *execpb.Output) { out.Write(o.Data) })
7777 if err != nil {
7878 t.Fatal(err)
7979 }
@@ -73,7 +73,7 @@ func TestDockerAPIWiring(t *testing.T) {
73 t.Fatalf("job %d: %v", i, err)73 t.Fatalf("job %d: %v", i, err)
74 }74 }
75 var out strings.Builder75 var out strings.Builder
76- hello, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: "echo wired; id -u; exit 4"}, func(o *execpb.Output) { out.Write(o.Data) })76+ hello, exit, err := execstream.Drive(conn, &execpb.Run{Script: "echo wired; id -u; exit 4"}, nil, func(o *execpb.Output) { out.Write(o.Data) })
77 if err != nil {77 if err != nil {
78 t.Fatal(err)78 t.Fatal(err)
79 }79 }
M internal/sandbox/integration/integration_test.go +1 -1
@@ -125,7 +125,7 @@ type result struct {
125125
126126 func runJob(ctx context.Context, conn sandbox.Conn, script string) (result, error) {
127127 var so, se strings.Builder
128- hello, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: script}, func(o *execpb.Output) {
128+ hello, exit, err := execstream.Drive(conn, &execpb.Run{Script: script}, nil, func(o *execpb.Output) {
129129 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
130130 se.Write(o.Data)
131131 } else {
@@ -125,7 +125,7 @@ type result struct {
125 125
126 func runJob(ctx context.Context, conn sandbox.Conn, script string) (result, error) {126 func runJob(ctx context.Context, conn sandbox.Conn, script string) (result, error) {
127 var so, se strings.Builder127 var so, se strings.Builder
128- hello, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: script}, func(o *execpb.Output) {128+ hello, exit, err := execstream.Drive(conn, &execpb.Run{Script: script}, nil, func(o *execpb.Output) {
129 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {129 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
130 se.Write(o.Data)130 se.Write(o.Data)
131 } else {131 } else {
M internal/sandbox/vm/vm_test.go +1 -1
@@ -196,7 +196,7 @@ func (e *env) leftovers(t *testing.T) []string {
196196 func runStub(t *testing.T, conn sandbox.Conn) (string, int) {
197197 t.Helper()
198198 var out strings.Builder
199- _, exit, err := execstream.Drive(context.Background(), conn, &execpb.Run{}, func(o *execpb.Output) { out.Write(o.Data) })
199+ _, exit, err := execstream.Drive(conn, &execpb.Run{}, nil, func(o *execpb.Output) { out.Write(o.Data) })
200200 if err != nil {
201201 t.Fatal(err)
202202 }
@@ -196,7 +196,7 @@ func (e *env) leftovers(t *testing.T) []string {
196 func runStub(t *testing.T, conn sandbox.Conn) (string, int) {196 func runStub(t *testing.T, conn sandbox.Conn) (string, int) {
197 t.Helper()197 t.Helper()
198 var out strings.Builder198 var out strings.Builder
199- _, exit, err := execstream.Drive(context.Background(), conn, &execpb.Run{}, func(o *execpb.Output) { out.Write(o.Data) })199+ _, exit, err := execstream.Drive(conn, &execpb.Run{}, nil, func(o *execpb.Output) { out.Write(o.Data) })
200 if err != nil {200 if err != nil {
201 t.Fatal(err)201 t.Fatal(err)
202 }202 }
M sandbox_check.go +5 -1
@@ -55,7 +55,11 @@ func sandboxCheck(ctx context.Context, args []string) int {
5555 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
5656 return 1
5757 }
58- _, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: strings.Join(args, " ")}, func(o *execpb.Output) {
58+ // Ctrl-C / SIGTERM cancel the job: the executor escalates by itself.
59+ cancel := make(chan *execpb.Cancel, 1)
60+ stop := context.AfterFunc(ctx, func() { cancel <- &execpb.Cancel{Reason: "interrupted"} })
61+ defer stop()
62+ _, exit, err := execstream.Drive(conn, &execpb.Run{Script: strings.Join(args, " ")}, cancel, func(o *execpb.Output) {
5963 f := os.Stdout
6064 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
6165 f = os.Stderr
@@ -55,7 +55,11 @@ func sandboxCheck(ctx context.Context, args []string) int {
55 fmt.Fprintln(os.Stderr, "ermite-runner:", err)55 fmt.Fprintln(os.Stderr, "ermite-runner:", err)
56 return 156 return 1
57 }57 }
58- _, exit, err := execstream.Drive(ctx, conn, &execpb.Run{Script: strings.Join(args, " ")}, func(o *execpb.Output) {58+ // Ctrl-C / SIGTERM cancel the job: the executor escalates by itself.
59+ cancel := make(chan *execpb.Cancel, 1)
60+ stop := context.AfterFunc(ctx, func() { cancel <- &execpb.Cancel{Reason: "interrupted"} })
61+ defer stop()
62+ _, exit, err := execstream.Drive(conn, &execpb.Run{Script: strings.Join(args, " ")}, cancel, func(o *execpb.Output) {
59 f := os.Stdout63 f := os.Stdout
60 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {64 if o.Stream == execpb.StreamKind_STREAM_KIND_STDERR {
61 f = os.Stderr65 f = os.Stderr

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close