detached b44fbc4

feat(execstream): add the framed protobuf stream between host and executor

commit
b44fbc45e98d9a8809b7043b5f8bc04e11b0b655
author
brice · · 2026-10-12 15:35 UTC
parent
c37a8f5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +915 -0
A internal/execstream/exec.proto +58 -0
@@ -0,0 +1,58 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Internal wire format between the host process of ermite-runner and the
4+// executor running inside a sandbox (vsock in a micro-VM, Unix socket in a
5+// container). Frames are length-prefixed on the stream (see frame.go); this file
6+// defines the payload. It is internal to ermite-runner: no compatibility is
7+// promised between runner versions, which is safe because the host attaches its
8+// own executable to every sandbox.
9+
10+syntax = "proto3";
11+
12+package ermite.runner.exec.v1;
13+
14+option go_package = "bnema.dev/ermite-runner/internal/execstream/execpb";
15+
16+// Frame is the only message on the stream.
17+message Frame {
18+ oneof body {
19+ Hello hello = 1; // executor -> host, first frame
20+ Run run = 2; // host -> executor, after Hello
21+ Output output = 3; // executor -> host
22+ Exit exit = 4; // executor -> host, last frame
23+ Cancel cancel = 5; // host -> executor
24+ }
25+}
26+
27+message Hello {
28+ string version = 1; // runner version of the executor
29+ string sandbox = 2; // "vm" or "container"
30+ string hostname = 3;
31+}
32+
33+// Run asks the executor to run a script. CI-1 stub executor: the script is run
34+// with `sh -c`; an empty script means the built-in diagnostics script.
35+message Run {
36+ string script = 1;
37+ map<string, string> env = 2;
38+}
39+
40+enum StreamKind {
41+ STREAM_KIND_UNSPECIFIED = 0;
42+ STREAM_KIND_STDOUT = 1;
43+ STREAM_KIND_STDERR = 2;
44+}
45+
46+message Output {
47+ StreamKind stream = 1;
48+ bytes data = 2;
49+ int64 unix_nano = 3;
50+}
51+
52+message Exit {
53+ int32 code = 1;
54+ string error = 2; // set when the executor failed to run the script at all
55+}
56+
57+// Cancel asks the executor to stop the running script: SIGINT now.
58+message Cancel {}
@@ -0,0 +1,58 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Internal wire format between the host process of ermite-runner and the
4+// executor running inside a sandbox (vsock in a micro-VM, Unix socket in a
5+// container). Frames are length-prefixed on the stream (see frame.go); this file
6+// defines the payload. It is internal to ermite-runner: no compatibility is
7+// promised between runner versions, which is safe because the host attaches its
8+// own executable to every sandbox.
9+
10+syntax = "proto3";
11+
12+package ermite.runner.exec.v1;
13+
14+option go_package = "bnema.dev/ermite-runner/internal/execstream/execpb";
15+
16+// Frame is the only message on the stream.
17+message Frame {
18+ oneof body {
19+ Hello hello = 1; // executor -> host, first frame
20+ Run run = 2; // host -> executor, after Hello
21+ Output output = 3; // executor -> host
22+ Exit exit = 4; // executor -> host, last frame
23+ Cancel cancel = 5; // host -> executor
24+ }
25+}
26+
27+message Hello {
28+ string version = 1; // runner version of the executor
29+ string sandbox = 2; // "vm" or "container"
30+ string hostname = 3;
31+}
32+
33+// Run asks the executor to run a script. CI-1 stub executor: the script is run
34+// with `sh -c`; an empty script means the built-in diagnostics script.
35+message Run {
36+ string script = 1;
37+ map<string, string> env = 2;
38+}
39+
40+enum StreamKind {
41+ STREAM_KIND_UNSPECIFIED = 0;
42+ STREAM_KIND_STDOUT = 1;
43+ STREAM_KIND_STDERR = 2;
44+}
45+
46+message Output {
47+ StreamKind stream = 1;
48+ bytes data = 2;
49+ int64 unix_nano = 3;
50+}
51+
52+message Exit {
53+ int32 code = 1;
54+ string error = 2; // set when the executor failed to run the script at all
55+}
56+
57+// Cancel asks the executor to stop the running script: SIGINT now.
58+message Cancel {}
A internal/execstream/execpb/exec.pb.go +582 -0
@@ -0,0 +1,582 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Internal wire format between the host process of ermite-runner and the
4+// executor running inside a sandbox (vsock in a micro-VM, Unix socket in a
5+// container). Frames are length-prefixed on the stream (see frame.go); this file
6+// defines the payload. It is internal to ermite-runner: no compatibility is
7+// promised between runner versions, which is safe because the host attaches its
8+// own executable to every sandbox.
9+
10+// Code generated by protoc-gen-go. DO NOT EDIT.
11+// versions:
12+// protoc-gen-go v1.36.11
13+// protoc v7.36.1
14+// source: exec.proto
15+
16+package execpb
17+
18+import (
19+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
20+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
21+ reflect "reflect"
22+ sync "sync"
23+ unsafe "unsafe"
24+)
25+
26+const (
27+ // Verify that this generated code is sufficiently up-to-date.
28+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
29+ // Verify that runtime/protoimpl is sufficiently up-to-date.
30+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
31+)
32+
33+type StreamKind int32
34+
35+const (
36+ StreamKind_STREAM_KIND_UNSPECIFIED StreamKind = 0
37+ StreamKind_STREAM_KIND_STDOUT StreamKind = 1
38+ StreamKind_STREAM_KIND_STDERR StreamKind = 2
39+)
40+
41+// Enum value maps for StreamKind.
42+var (
43+ StreamKind_name = map[int32]string{
44+ 0: "STREAM_KIND_UNSPECIFIED",
45+ 1: "STREAM_KIND_STDOUT",
46+ 2: "STREAM_KIND_STDERR",
47+ }
48+ StreamKind_value = map[string]int32{
49+ "STREAM_KIND_UNSPECIFIED": 0,
50+ "STREAM_KIND_STDOUT": 1,
51+ "STREAM_KIND_STDERR": 2,
52+ }
53+)
54+
55+func (x StreamKind) Enum() *StreamKind {
56+ p := new(StreamKind)
57+ *p = x
58+ return p
59+}
60+
61+func (x StreamKind) String() string {
62+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
63+}
64+
65+func (StreamKind) Descriptor() protoreflect.EnumDescriptor {
66+ return file_exec_proto_enumTypes[0].Descriptor()
67+}
68+
69+func (StreamKind) Type() protoreflect.EnumType {
70+ return &file_exec_proto_enumTypes[0]
71+}
72+
73+func (x StreamKind) Number() protoreflect.EnumNumber {
74+ return protoreflect.EnumNumber(x)
75+}
76+
77+// Deprecated: Use StreamKind.Descriptor instead.
78+func (StreamKind) EnumDescriptor() ([]byte, []int) {
79+ return file_exec_proto_rawDescGZIP(), []int{0}
80+}
81+
82+// Frame is the only message on the stream.
83+type Frame struct {
84+ state protoimpl.MessageState `protogen:"open.v1"`
85+ // Types that are valid to be assigned to Body:
86+ //
87+ // *Frame_Hello
88+ // *Frame_Run
89+ // *Frame_Output
90+ // *Frame_Exit
91+ // *Frame_Cancel
92+ Body isFrame_Body `protobuf_oneof:"body"`
93+ unknownFields protoimpl.UnknownFields
94+ sizeCache protoimpl.SizeCache
95+}
96+
97+func (x *Frame) Reset() {
98+ *x = Frame{}
99+ mi := &file_exec_proto_msgTypes[0]
100+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
101+ ms.StoreMessageInfo(mi)
102+}
103+
104+func (x *Frame) String() string {
105+ return protoimpl.X.MessageStringOf(x)
106+}
107+
108+func (*Frame) ProtoMessage() {}
109+
110+func (x *Frame) ProtoReflect() protoreflect.Message {
111+ mi := &file_exec_proto_msgTypes[0]
112+ if x != nil {
113+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
114+ if ms.LoadMessageInfo() == nil {
115+ ms.StoreMessageInfo(mi)
116+ }
117+ return ms
118+ }
119+ return mi.MessageOf(x)
120+}
121+
122+// Deprecated: Use Frame.ProtoReflect.Descriptor instead.
123+func (*Frame) Descriptor() ([]byte, []int) {
124+ return file_exec_proto_rawDescGZIP(), []int{0}
125+}
126+
127+func (x *Frame) GetBody() isFrame_Body {
128+ if x != nil {
129+ return x.Body
130+ }
131+ return nil
132+}
133+
134+func (x *Frame) GetHello() *Hello {
135+ if x != nil {
136+ if x, ok := x.Body.(*Frame_Hello); ok {
137+ return x.Hello
138+ }
139+ }
140+ return nil
141+}
142+
143+func (x *Frame) GetRun() *Run {
144+ if x != nil {
145+ if x, ok := x.Body.(*Frame_Run); ok {
146+ return x.Run
147+ }
148+ }
149+ return nil
150+}
151+
152+func (x *Frame) GetOutput() *Output {
153+ if x != nil {
154+ if x, ok := x.Body.(*Frame_Output); ok {
155+ return x.Output
156+ }
157+ }
158+ return nil
159+}
160+
161+func (x *Frame) GetExit() *Exit {
162+ if x != nil {
163+ if x, ok := x.Body.(*Frame_Exit); ok {
164+ return x.Exit
165+ }
166+ }
167+ return nil
168+}
169+
170+func (x *Frame) GetCancel() *Cancel {
171+ if x != nil {
172+ if x, ok := x.Body.(*Frame_Cancel); ok {
173+ return x.Cancel
174+ }
175+ }
176+ return nil
177+}
178+
179+type isFrame_Body interface {
180+ isFrame_Body()
181+}
182+
183+type Frame_Hello struct {
184+ Hello *Hello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` // executor -> host, first frame
185+}
186+
187+type Frame_Run struct {
188+ Run *Run `protobuf:"bytes,2,opt,name=run,proto3,oneof"` // host -> executor, after Hello
189+}
190+
191+type Frame_Output struct {
192+ Output *Output `protobuf:"bytes,3,opt,name=output,proto3,oneof"` // executor -> host
193+}
194+
195+type Frame_Exit struct {
196+ Exit *Exit `protobuf:"bytes,4,opt,name=exit,proto3,oneof"` // executor -> host, last frame
197+}
198+
199+type Frame_Cancel struct {
200+ Cancel *Cancel `protobuf:"bytes,5,opt,name=cancel,proto3,oneof"` // host -> executor
201+}
202+
203+func (*Frame_Hello) isFrame_Body() {}
204+
205+func (*Frame_Run) isFrame_Body() {}
206+
207+func (*Frame_Output) isFrame_Body() {}
208+
209+func (*Frame_Exit) isFrame_Body() {}
210+
211+func (*Frame_Cancel) isFrame_Body() {}
212+
213+type Hello struct {
214+ state protoimpl.MessageState `protogen:"open.v1"`
215+ Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // runner version of the executor
216+ Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // "vm" or "container"
217+ Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"`
218+ unknownFields protoimpl.UnknownFields
219+ sizeCache protoimpl.SizeCache
220+}
221+
222+func (x *Hello) Reset() {
223+ *x = Hello{}
224+ mi := &file_exec_proto_msgTypes[1]
225+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
226+ ms.StoreMessageInfo(mi)
227+}
228+
229+func (x *Hello) String() string {
230+ return protoimpl.X.MessageStringOf(x)
231+}
232+
233+func (*Hello) ProtoMessage() {}
234+
235+func (x *Hello) ProtoReflect() protoreflect.Message {
236+ mi := &file_exec_proto_msgTypes[1]
237+ if x != nil {
238+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
239+ if ms.LoadMessageInfo() == nil {
240+ ms.StoreMessageInfo(mi)
241+ }
242+ return ms
243+ }
244+ return mi.MessageOf(x)
245+}
246+
247+// Deprecated: Use Hello.ProtoReflect.Descriptor instead.
248+func (*Hello) Descriptor() ([]byte, []int) {
249+ return file_exec_proto_rawDescGZIP(), []int{1}
250+}
251+
252+func (x *Hello) GetVersion() string {
253+ if x != nil {
254+ return x.Version
255+ }
256+ return ""
257+}
258+
259+func (x *Hello) GetSandbox() string {
260+ if x != nil {
261+ return x.Sandbox
262+ }
263+ return ""
264+}
265+
266+func (x *Hello) GetHostname() string {
267+ if x != nil {
268+ return x.Hostname
269+ }
270+ return ""
271+}
272+
273+// Run asks the executor to run a script. CI-1 stub executor: the script is run
274+// with `sh -c`; an empty script means the built-in diagnostics script.
275+type Run struct {
276+ state protoimpl.MessageState `protogen:"open.v1"`
277+ Script string `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"`
278+ Env map[string]string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
279+ unknownFields protoimpl.UnknownFields
280+ sizeCache protoimpl.SizeCache
281+}
282+
283+func (x *Run) Reset() {
284+ *x = Run{}
285+ mi := &file_exec_proto_msgTypes[2]
286+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
287+ ms.StoreMessageInfo(mi)
288+}
289+
290+func (x *Run) String() string {
291+ return protoimpl.X.MessageStringOf(x)
292+}
293+
294+func (*Run) ProtoMessage() {}
295+
296+func (x *Run) ProtoReflect() protoreflect.Message {
297+ mi := &file_exec_proto_msgTypes[2]
298+ if x != nil {
299+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
300+ if ms.LoadMessageInfo() == nil {
301+ ms.StoreMessageInfo(mi)
302+ }
303+ return ms
304+ }
305+ return mi.MessageOf(x)
306+}
307+
308+// Deprecated: Use Run.ProtoReflect.Descriptor instead.
309+func (*Run) Descriptor() ([]byte, []int) {
310+ return file_exec_proto_rawDescGZIP(), []int{2}
311+}
312+
313+func (x *Run) GetScript() string {
314+ if x != nil {
315+ return x.Script
316+ }
317+ return ""
318+}
319+
320+func (x *Run) GetEnv() map[string]string {
321+ if x != nil {
322+ return x.Env
323+ }
324+ return nil
325+}
326+
327+type Output struct {
328+ state protoimpl.MessageState `protogen:"open.v1"`
329+ Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
330+ Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
331+ UnixNano int64 `protobuf:"varint,3,opt,name=unix_nano,json=unixNano,proto3" json:"unix_nano,omitempty"`
332+ unknownFields protoimpl.UnknownFields
333+ sizeCache protoimpl.SizeCache
334+}
335+
336+func (x *Output) Reset() {
337+ *x = Output{}
338+ mi := &file_exec_proto_msgTypes[3]
339+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
340+ ms.StoreMessageInfo(mi)
341+}
342+
343+func (x *Output) String() string {
344+ return protoimpl.X.MessageStringOf(x)
345+}
346+
347+func (*Output) ProtoMessage() {}
348+
349+func (x *Output) ProtoReflect() protoreflect.Message {
350+ mi := &file_exec_proto_msgTypes[3]
351+ if x != nil {
352+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
353+ if ms.LoadMessageInfo() == nil {
354+ ms.StoreMessageInfo(mi)
355+ }
356+ return ms
357+ }
358+ return mi.MessageOf(x)
359+}
360+
361+// Deprecated: Use Output.ProtoReflect.Descriptor instead.
362+func (*Output) Descriptor() ([]byte, []int) {
363+ return file_exec_proto_rawDescGZIP(), []int{3}
364+}
365+
366+func (x *Output) GetStream() StreamKind {
367+ if x != nil {
368+ return x.Stream
369+ }
370+ return StreamKind_STREAM_KIND_UNSPECIFIED
371+}
372+
373+func (x *Output) GetData() []byte {
374+ if x != nil {
375+ return x.Data
376+ }
377+ return nil
378+}
379+
380+func (x *Output) GetUnixNano() int64 {
381+ if x != nil {
382+ return x.UnixNano
383+ }
384+ return 0
385+}
386+
387+type Exit struct {
388+ state protoimpl.MessageState `protogen:"open.v1"`
389+ Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
390+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // set when the executor failed to run the script at all
391+ unknownFields protoimpl.UnknownFields
392+ sizeCache protoimpl.SizeCache
393+}
394+
395+func (x *Exit) Reset() {
396+ *x = Exit{}
397+ mi := &file_exec_proto_msgTypes[4]
398+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
399+ ms.StoreMessageInfo(mi)
400+}
401+
402+func (x *Exit) String() string {
403+ return protoimpl.X.MessageStringOf(x)
404+}
405+
406+func (*Exit) ProtoMessage() {}
407+
408+func (x *Exit) ProtoReflect() protoreflect.Message {
409+ mi := &file_exec_proto_msgTypes[4]
410+ if x != nil {
411+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
412+ if ms.LoadMessageInfo() == nil {
413+ ms.StoreMessageInfo(mi)
414+ }
415+ return ms
416+ }
417+ return mi.MessageOf(x)
418+}
419+
420+// Deprecated: Use Exit.ProtoReflect.Descriptor instead.
421+func (*Exit) Descriptor() ([]byte, []int) {
422+ return file_exec_proto_rawDescGZIP(), []int{4}
423+}
424+
425+func (x *Exit) GetCode() int32 {
426+ if x != nil {
427+ return x.Code
428+ }
429+ return 0
430+}
431+
432+func (x *Exit) GetError() string {
433+ if x != nil {
434+ return x.Error
435+ }
436+ return ""
437+}
438+
439+// Cancel asks the executor to stop the running script: SIGINT now.
440+type Cancel struct {
441+ state protoimpl.MessageState `protogen:"open.v1"`
442+ unknownFields protoimpl.UnknownFields
443+ sizeCache protoimpl.SizeCache
444+}
445+
446+func (x *Cancel) Reset() {
447+ *x = Cancel{}
448+ mi := &file_exec_proto_msgTypes[5]
449+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
450+ ms.StoreMessageInfo(mi)
451+}
452+
453+func (x *Cancel) String() string {
454+ return protoimpl.X.MessageStringOf(x)
455+}
456+
457+func (*Cancel) ProtoMessage() {}
458+
459+func (x *Cancel) ProtoReflect() protoreflect.Message {
460+ mi := &file_exec_proto_msgTypes[5]
461+ if x != nil {
462+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
463+ if ms.LoadMessageInfo() == nil {
464+ ms.StoreMessageInfo(mi)
465+ }
466+ return ms
467+ }
468+ return mi.MessageOf(x)
469+}
470+
471+// Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
472+func (*Cancel) Descriptor() ([]byte, []int) {
473+ return file_exec_proto_rawDescGZIP(), []int{5}
474+}
475+
476+var File_exec_proto protoreflect.FileDescriptor
477+
478+const file_exec_proto_rawDesc = "" +
479+ "\n" +
480+ "\n" +
481+ "exec.proto\x12\x15ermite.runner.exec.v1\"\x9a\x02\n" +
482+ "\x05Frame\x124\n" +
483+ "\x05hello\x18\x01 \x01(\v2\x1c.ermite.runner.exec.v1.HelloH\x00R\x05hello\x12.\n" +
484+ "\x03run\x18\x02 \x01(\v2\x1a.ermite.runner.exec.v1.RunH\x00R\x03run\x127\n" +
485+ "\x06output\x18\x03 \x01(\v2\x1d.ermite.runner.exec.v1.OutputH\x00R\x06output\x121\n" +
486+ "\x04exit\x18\x04 \x01(\v2\x1b.ermite.runner.exec.v1.ExitH\x00R\x04exit\x127\n" +
487+ "\x06cancel\x18\x05 \x01(\v2\x1d.ermite.runner.exec.v1.CancelH\x00R\x06cancelB\x06\n" +
488+ "\x04body\"W\n" +
489+ "\x05Hello\x12\x18\n" +
490+ "\aversion\x18\x01 \x01(\tR\aversion\x12\x18\n" +
491+ "\asandbox\x18\x02 \x01(\tR\asandbox\x12\x1a\n" +
492+ "\bhostname\x18\x03 \x01(\tR\bhostname\"\x8c\x01\n" +
493+ "\x03Run\x12\x16\n" +
494+ "\x06script\x18\x01 \x01(\tR\x06script\x125\n" +
495+ "\x03env\x18\x02 \x03(\v2#.ermite.runner.exec.v1.Run.EnvEntryR\x03env\x1a6\n" +
496+ "\bEnvEntry\x12\x10\n" +
497+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
498+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" +
499+ "\x06Output\x129\n" +
500+ "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
501+ "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
502+ "\tunix_nano\x18\x03 \x01(\x03R\bunixNano\"0\n" +
503+ "\x04Exit\x12\x12\n" +
504+ "\x04code\x18\x01 \x01(\x05R\x04code\x12\x14\n" +
505+ "\x05error\x18\x02 \x01(\tR\x05error\"\b\n" +
506+ "\x06Cancel*Y\n" +
507+ "\n" +
508+ "StreamKind\x12\x1b\n" +
509+ "\x17STREAM_KIND_UNSPECIFIED\x10\x00\x12\x16\n" +
510+ "\x12STREAM_KIND_STDOUT\x10\x01\x12\x16\n" +
511+ "\x12STREAM_KIND_STDERR\x10\x02B4Z2bnema.dev/ermite-runner/internal/execstream/execpbb\x06proto3"
512+
513+var (
514+ file_exec_proto_rawDescOnce sync.Once
515+ file_exec_proto_rawDescData []byte
516+)
517+
518+func file_exec_proto_rawDescGZIP() []byte {
519+ file_exec_proto_rawDescOnce.Do(func() {
520+ file_exec_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)))
521+ })
522+ return file_exec_proto_rawDescData
523+}
524+
525+var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
526+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
527+var file_exec_proto_goTypes = []any{
528+ (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
529+ (*Frame)(nil), // 1: ermite.runner.exec.v1.Frame
530+ (*Hello)(nil), // 2: ermite.runner.exec.v1.Hello
531+ (*Run)(nil), // 3: ermite.runner.exec.v1.Run
532+ (*Output)(nil), // 4: ermite.runner.exec.v1.Output
533+ (*Exit)(nil), // 5: ermite.runner.exec.v1.Exit
534+ (*Cancel)(nil), // 6: ermite.runner.exec.v1.Cancel
535+ nil, // 7: ermite.runner.exec.v1.Run.EnvEntry
536+}
537+var file_exec_proto_depIdxs = []int32{
538+ 2, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
539+ 3, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
540+ 4, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
541+ 5, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
542+ 6, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
543+ 7, // 5: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
544+ 0, // 6: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
545+ 7, // [7:7] is the sub-list for method output_type
546+ 7, // [7:7] is the sub-list for method input_type
547+ 7, // [7:7] is the sub-list for extension type_name
548+ 7, // [7:7] is the sub-list for extension extendee
549+ 0, // [0:7] is the sub-list for field type_name
550+}
551+
552+func init() { file_exec_proto_init() }
553+func file_exec_proto_init() {
554+ if File_exec_proto != nil {
555+ return
556+ }
557+ file_exec_proto_msgTypes[0].OneofWrappers = []any{
558+ (*Frame_Hello)(nil),
559+ (*Frame_Run)(nil),
560+ (*Frame_Output)(nil),
561+ (*Frame_Exit)(nil),
562+ (*Frame_Cancel)(nil),
563+ }
564+ type x struct{}
565+ out := protoimpl.TypeBuilder{
566+ File: protoimpl.DescBuilder{
567+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
568+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
569+ NumEnums: 1,
570+ NumMessages: 7,
571+ NumExtensions: 0,
572+ NumServices: 0,
573+ },
574+ GoTypes: file_exec_proto_goTypes,
575+ DependencyIndexes: file_exec_proto_depIdxs,
576+ EnumInfos: file_exec_proto_enumTypes,
577+ MessageInfos: file_exec_proto_msgTypes,
578+ }.Build()
579+ File_exec_proto = out.File
580+ file_exec_proto_goTypes = nil
581+ file_exec_proto_depIdxs = nil
582+}
@@ -0,0 +1,582 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Internal wire format between the host process of ermite-runner and the
4+// executor running inside a sandbox (vsock in a micro-VM, Unix socket in a
5+// container). Frames are length-prefixed on the stream (see frame.go); this file
6+// defines the payload. It is internal to ermite-runner: no compatibility is
7+// promised between runner versions, which is safe because the host attaches its
8+// own executable to every sandbox.
9+
10+// Code generated by protoc-gen-go. DO NOT EDIT.
11+// versions:
12+// protoc-gen-go v1.36.11
13+// protoc v7.36.1
14+// source: exec.proto
15+
16+package execpb
17+
18+import (
19+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
20+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
21+ reflect "reflect"
22+ sync "sync"
23+ unsafe "unsafe"
24+)
25+
26+const (
27+ // Verify that this generated code is sufficiently up-to-date.
28+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
29+ // Verify that runtime/protoimpl is sufficiently up-to-date.
30+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
31+)
32+
33+type StreamKind int32
34+
35+const (
36+ StreamKind_STREAM_KIND_UNSPECIFIED StreamKind = 0
37+ StreamKind_STREAM_KIND_STDOUT StreamKind = 1
38+ StreamKind_STREAM_KIND_STDERR StreamKind = 2
39+)
40+
41+// Enum value maps for StreamKind.
42+var (
43+ StreamKind_name = map[int32]string{
44+ 0: "STREAM_KIND_UNSPECIFIED",
45+ 1: "STREAM_KIND_STDOUT",
46+ 2: "STREAM_KIND_STDERR",
47+ }
48+ StreamKind_value = map[string]int32{
49+ "STREAM_KIND_UNSPECIFIED": 0,
50+ "STREAM_KIND_STDOUT": 1,
51+ "STREAM_KIND_STDERR": 2,
52+ }
53+)
54+
55+func (x StreamKind) Enum() *StreamKind {
56+ p := new(StreamKind)
57+ *p = x
58+ return p
59+}
60+
61+func (x StreamKind) String() string {
62+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
63+}
64+
65+func (StreamKind) Descriptor() protoreflect.EnumDescriptor {
66+ return file_exec_proto_enumTypes[0].Descriptor()
67+}
68+
69+func (StreamKind) Type() protoreflect.EnumType {
70+ return &file_exec_proto_enumTypes[0]
71+}
72+
73+func (x StreamKind) Number() protoreflect.EnumNumber {
74+ return protoreflect.EnumNumber(x)
75+}
76+
77+// Deprecated: Use StreamKind.Descriptor instead.
78+func (StreamKind) EnumDescriptor() ([]byte, []int) {
79+ return file_exec_proto_rawDescGZIP(), []int{0}
80+}
81+
82+// Frame is the only message on the stream.
83+type Frame struct {
84+ state protoimpl.MessageState `protogen:"open.v1"`
85+ // Types that are valid to be assigned to Body:
86+ //
87+ // *Frame_Hello
88+ // *Frame_Run
89+ // *Frame_Output
90+ // *Frame_Exit
91+ // *Frame_Cancel
92+ Body isFrame_Body `protobuf_oneof:"body"`
93+ unknownFields protoimpl.UnknownFields
94+ sizeCache protoimpl.SizeCache
95+}
96+
97+func (x *Frame) Reset() {
98+ *x = Frame{}
99+ mi := &file_exec_proto_msgTypes[0]
100+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
101+ ms.StoreMessageInfo(mi)
102+}
103+
104+func (x *Frame) String() string {
105+ return protoimpl.X.MessageStringOf(x)
106+}
107+
108+func (*Frame) ProtoMessage() {}
109+
110+func (x *Frame) ProtoReflect() protoreflect.Message {
111+ mi := &file_exec_proto_msgTypes[0]
112+ if x != nil {
113+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
114+ if ms.LoadMessageInfo() == nil {
115+ ms.StoreMessageInfo(mi)
116+ }
117+ return ms
118+ }
119+ return mi.MessageOf(x)
120+}
121+
122+// Deprecated: Use Frame.ProtoReflect.Descriptor instead.
123+func (*Frame) Descriptor() ([]byte, []int) {
124+ return file_exec_proto_rawDescGZIP(), []int{0}
125+}
126+
127+func (x *Frame) GetBody() isFrame_Body {
128+ if x != nil {
129+ return x.Body
130+ }
131+ return nil
132+}
133+
134+func (x *Frame) GetHello() *Hello {
135+ if x != nil {
136+ if x, ok := x.Body.(*Frame_Hello); ok {
137+ return x.Hello
138+ }
139+ }
140+ return nil
141+}
142+
143+func (x *Frame) GetRun() *Run {
144+ if x != nil {
145+ if x, ok := x.Body.(*Frame_Run); ok {
146+ return x.Run
147+ }
148+ }
149+ return nil
150+}
151+
152+func (x *Frame) GetOutput() *Output {
153+ if x != nil {
154+ if x, ok := x.Body.(*Frame_Output); ok {
155+ return x.Output
156+ }
157+ }
158+ return nil
159+}
160+
161+func (x *Frame) GetExit() *Exit {
162+ if x != nil {
163+ if x, ok := x.Body.(*Frame_Exit); ok {
164+ return x.Exit
165+ }
166+ }
167+ return nil
168+}
169+
170+func (x *Frame) GetCancel() *Cancel {
171+ if x != nil {
172+ if x, ok := x.Body.(*Frame_Cancel); ok {
173+ return x.Cancel
174+ }
175+ }
176+ return nil
177+}
178+
179+type isFrame_Body interface {
180+ isFrame_Body()
181+}
182+
183+type Frame_Hello struct {
184+ Hello *Hello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"` // executor -> host, first frame
185+}
186+
187+type Frame_Run struct {
188+ Run *Run `protobuf:"bytes,2,opt,name=run,proto3,oneof"` // host -> executor, after Hello
189+}
190+
191+type Frame_Output struct {
192+ Output *Output `protobuf:"bytes,3,opt,name=output,proto3,oneof"` // executor -> host
193+}
194+
195+type Frame_Exit struct {
196+ Exit *Exit `protobuf:"bytes,4,opt,name=exit,proto3,oneof"` // executor -> host, last frame
197+}
198+
199+type Frame_Cancel struct {
200+ Cancel *Cancel `protobuf:"bytes,5,opt,name=cancel,proto3,oneof"` // host -> executor
201+}
202+
203+func (*Frame_Hello) isFrame_Body() {}
204+
205+func (*Frame_Run) isFrame_Body() {}
206+
207+func (*Frame_Output) isFrame_Body() {}
208+
209+func (*Frame_Exit) isFrame_Body() {}
210+
211+func (*Frame_Cancel) isFrame_Body() {}
212+
213+type Hello struct {
214+ state protoimpl.MessageState `protogen:"open.v1"`
215+ Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` // runner version of the executor
216+ Sandbox string `protobuf:"bytes,2,opt,name=sandbox,proto3" json:"sandbox,omitempty"` // "vm" or "container"
217+ Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"`
218+ unknownFields protoimpl.UnknownFields
219+ sizeCache protoimpl.SizeCache
220+}
221+
222+func (x *Hello) Reset() {
223+ *x = Hello{}
224+ mi := &file_exec_proto_msgTypes[1]
225+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
226+ ms.StoreMessageInfo(mi)
227+}
228+
229+func (x *Hello) String() string {
230+ return protoimpl.X.MessageStringOf(x)
231+}
232+
233+func (*Hello) ProtoMessage() {}
234+
235+func (x *Hello) ProtoReflect() protoreflect.Message {
236+ mi := &file_exec_proto_msgTypes[1]
237+ if x != nil {
238+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
239+ if ms.LoadMessageInfo() == nil {
240+ ms.StoreMessageInfo(mi)
241+ }
242+ return ms
243+ }
244+ return mi.MessageOf(x)
245+}
246+
247+// Deprecated: Use Hello.ProtoReflect.Descriptor instead.
248+func (*Hello) Descriptor() ([]byte, []int) {
249+ return file_exec_proto_rawDescGZIP(), []int{1}
250+}
251+
252+func (x *Hello) GetVersion() string {
253+ if x != nil {
254+ return x.Version
255+ }
256+ return ""
257+}
258+
259+func (x *Hello) GetSandbox() string {
260+ if x != nil {
261+ return x.Sandbox
262+ }
263+ return ""
264+}
265+
266+func (x *Hello) GetHostname() string {
267+ if x != nil {
268+ return x.Hostname
269+ }
270+ return ""
271+}
272+
273+// Run asks the executor to run a script. CI-1 stub executor: the script is run
274+// with `sh -c`; an empty script means the built-in diagnostics script.
275+type Run struct {
276+ state protoimpl.MessageState `protogen:"open.v1"`
277+ Script string `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"`
278+ Env map[string]string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
279+ unknownFields protoimpl.UnknownFields
280+ sizeCache protoimpl.SizeCache
281+}
282+
283+func (x *Run) Reset() {
284+ *x = Run{}
285+ mi := &file_exec_proto_msgTypes[2]
286+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
287+ ms.StoreMessageInfo(mi)
288+}
289+
290+func (x *Run) String() string {
291+ return protoimpl.X.MessageStringOf(x)
292+}
293+
294+func (*Run) ProtoMessage() {}
295+
296+func (x *Run) ProtoReflect() protoreflect.Message {
297+ mi := &file_exec_proto_msgTypes[2]
298+ if x != nil {
299+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
300+ if ms.LoadMessageInfo() == nil {
301+ ms.StoreMessageInfo(mi)
302+ }
303+ return ms
304+ }
305+ return mi.MessageOf(x)
306+}
307+
308+// Deprecated: Use Run.ProtoReflect.Descriptor instead.
309+func (*Run) Descriptor() ([]byte, []int) {
310+ return file_exec_proto_rawDescGZIP(), []int{2}
311+}
312+
313+func (x *Run) GetScript() string {
314+ if x != nil {
315+ return x.Script
316+ }
317+ return ""
318+}
319+
320+func (x *Run) GetEnv() map[string]string {
321+ if x != nil {
322+ return x.Env
323+ }
324+ return nil
325+}
326+
327+type Output struct {
328+ state protoimpl.MessageState `protogen:"open.v1"`
329+ Stream StreamKind `protobuf:"varint,1,opt,name=stream,proto3,enum=ermite.runner.exec.v1.StreamKind" json:"stream,omitempty"`
330+ Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
331+ UnixNano int64 `protobuf:"varint,3,opt,name=unix_nano,json=unixNano,proto3" json:"unix_nano,omitempty"`
332+ unknownFields protoimpl.UnknownFields
333+ sizeCache protoimpl.SizeCache
334+}
335+
336+func (x *Output) Reset() {
337+ *x = Output{}
338+ mi := &file_exec_proto_msgTypes[3]
339+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
340+ ms.StoreMessageInfo(mi)
341+}
342+
343+func (x *Output) String() string {
344+ return protoimpl.X.MessageStringOf(x)
345+}
346+
347+func (*Output) ProtoMessage() {}
348+
349+func (x *Output) ProtoReflect() protoreflect.Message {
350+ mi := &file_exec_proto_msgTypes[3]
351+ if x != nil {
352+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
353+ if ms.LoadMessageInfo() == nil {
354+ ms.StoreMessageInfo(mi)
355+ }
356+ return ms
357+ }
358+ return mi.MessageOf(x)
359+}
360+
361+// Deprecated: Use Output.ProtoReflect.Descriptor instead.
362+func (*Output) Descriptor() ([]byte, []int) {
363+ return file_exec_proto_rawDescGZIP(), []int{3}
364+}
365+
366+func (x *Output) GetStream() StreamKind {
367+ if x != nil {
368+ return x.Stream
369+ }
370+ return StreamKind_STREAM_KIND_UNSPECIFIED
371+}
372+
373+func (x *Output) GetData() []byte {
374+ if x != nil {
375+ return x.Data
376+ }
377+ return nil
378+}
379+
380+func (x *Output) GetUnixNano() int64 {
381+ if x != nil {
382+ return x.UnixNano
383+ }
384+ return 0
385+}
386+
387+type Exit struct {
388+ state protoimpl.MessageState `protogen:"open.v1"`
389+ Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
390+ Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // set when the executor failed to run the script at all
391+ unknownFields protoimpl.UnknownFields
392+ sizeCache protoimpl.SizeCache
393+}
394+
395+func (x *Exit) Reset() {
396+ *x = Exit{}
397+ mi := &file_exec_proto_msgTypes[4]
398+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
399+ ms.StoreMessageInfo(mi)
400+}
401+
402+func (x *Exit) String() string {
403+ return protoimpl.X.MessageStringOf(x)
404+}
405+
406+func (*Exit) ProtoMessage() {}
407+
408+func (x *Exit) ProtoReflect() protoreflect.Message {
409+ mi := &file_exec_proto_msgTypes[4]
410+ if x != nil {
411+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
412+ if ms.LoadMessageInfo() == nil {
413+ ms.StoreMessageInfo(mi)
414+ }
415+ return ms
416+ }
417+ return mi.MessageOf(x)
418+}
419+
420+// Deprecated: Use Exit.ProtoReflect.Descriptor instead.
421+func (*Exit) Descriptor() ([]byte, []int) {
422+ return file_exec_proto_rawDescGZIP(), []int{4}
423+}
424+
425+func (x *Exit) GetCode() int32 {
426+ if x != nil {
427+ return x.Code
428+ }
429+ return 0
430+}
431+
432+func (x *Exit) GetError() string {
433+ if x != nil {
434+ return x.Error
435+ }
436+ return ""
437+}
438+
439+// Cancel asks the executor to stop the running script: SIGINT now.
440+type Cancel struct {
441+ state protoimpl.MessageState `protogen:"open.v1"`
442+ unknownFields protoimpl.UnknownFields
443+ sizeCache protoimpl.SizeCache
444+}
445+
446+func (x *Cancel) Reset() {
447+ *x = Cancel{}
448+ mi := &file_exec_proto_msgTypes[5]
449+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
450+ ms.StoreMessageInfo(mi)
451+}
452+
453+func (x *Cancel) String() string {
454+ return protoimpl.X.MessageStringOf(x)
455+}
456+
457+func (*Cancel) ProtoMessage() {}
458+
459+func (x *Cancel) ProtoReflect() protoreflect.Message {
460+ mi := &file_exec_proto_msgTypes[5]
461+ if x != nil {
462+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
463+ if ms.LoadMessageInfo() == nil {
464+ ms.StoreMessageInfo(mi)
465+ }
466+ return ms
467+ }
468+ return mi.MessageOf(x)
469+}
470+
471+// Deprecated: Use Cancel.ProtoReflect.Descriptor instead.
472+func (*Cancel) Descriptor() ([]byte, []int) {
473+ return file_exec_proto_rawDescGZIP(), []int{5}
474+}
475+
476+var File_exec_proto protoreflect.FileDescriptor
477+
478+const file_exec_proto_rawDesc = "" +
479+ "\n" +
480+ "\n" +
481+ "exec.proto\x12\x15ermite.runner.exec.v1\"\x9a\x02\n" +
482+ "\x05Frame\x124\n" +
483+ "\x05hello\x18\x01 \x01(\v2\x1c.ermite.runner.exec.v1.HelloH\x00R\x05hello\x12.\n" +
484+ "\x03run\x18\x02 \x01(\v2\x1a.ermite.runner.exec.v1.RunH\x00R\x03run\x127\n" +
485+ "\x06output\x18\x03 \x01(\v2\x1d.ermite.runner.exec.v1.OutputH\x00R\x06output\x121\n" +
486+ "\x04exit\x18\x04 \x01(\v2\x1b.ermite.runner.exec.v1.ExitH\x00R\x04exit\x127\n" +
487+ "\x06cancel\x18\x05 \x01(\v2\x1d.ermite.runner.exec.v1.CancelH\x00R\x06cancelB\x06\n" +
488+ "\x04body\"W\n" +
489+ "\x05Hello\x12\x18\n" +
490+ "\aversion\x18\x01 \x01(\tR\aversion\x12\x18\n" +
491+ "\asandbox\x18\x02 \x01(\tR\asandbox\x12\x1a\n" +
492+ "\bhostname\x18\x03 \x01(\tR\bhostname\"\x8c\x01\n" +
493+ "\x03Run\x12\x16\n" +
494+ "\x06script\x18\x01 \x01(\tR\x06script\x125\n" +
495+ "\x03env\x18\x02 \x03(\v2#.ermite.runner.exec.v1.Run.EnvEntryR\x03env\x1a6\n" +
496+ "\bEnvEntry\x12\x10\n" +
497+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
498+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"t\n" +
499+ "\x06Output\x129\n" +
500+ "\x06stream\x18\x01 \x01(\x0e2!.ermite.runner.exec.v1.StreamKindR\x06stream\x12\x12\n" +
501+ "\x04data\x18\x02 \x01(\fR\x04data\x12\x1b\n" +
502+ "\tunix_nano\x18\x03 \x01(\x03R\bunixNano\"0\n" +
503+ "\x04Exit\x12\x12\n" +
504+ "\x04code\x18\x01 \x01(\x05R\x04code\x12\x14\n" +
505+ "\x05error\x18\x02 \x01(\tR\x05error\"\b\n" +
506+ "\x06Cancel*Y\n" +
507+ "\n" +
508+ "StreamKind\x12\x1b\n" +
509+ "\x17STREAM_KIND_UNSPECIFIED\x10\x00\x12\x16\n" +
510+ "\x12STREAM_KIND_STDOUT\x10\x01\x12\x16\n" +
511+ "\x12STREAM_KIND_STDERR\x10\x02B4Z2bnema.dev/ermite-runner/internal/execstream/execpbb\x06proto3"
512+
513+var (
514+ file_exec_proto_rawDescOnce sync.Once
515+ file_exec_proto_rawDescData []byte
516+)
517+
518+func file_exec_proto_rawDescGZIP() []byte {
519+ file_exec_proto_rawDescOnce.Do(func() {
520+ file_exec_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)))
521+ })
522+ return file_exec_proto_rawDescData
523+}
524+
525+var file_exec_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
526+var file_exec_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
527+var file_exec_proto_goTypes = []any{
528+ (StreamKind)(0), // 0: ermite.runner.exec.v1.StreamKind
529+ (*Frame)(nil), // 1: ermite.runner.exec.v1.Frame
530+ (*Hello)(nil), // 2: ermite.runner.exec.v1.Hello
531+ (*Run)(nil), // 3: ermite.runner.exec.v1.Run
532+ (*Output)(nil), // 4: ermite.runner.exec.v1.Output
533+ (*Exit)(nil), // 5: ermite.runner.exec.v1.Exit
534+ (*Cancel)(nil), // 6: ermite.runner.exec.v1.Cancel
535+ nil, // 7: ermite.runner.exec.v1.Run.EnvEntry
536+}
537+var file_exec_proto_depIdxs = []int32{
538+ 2, // 0: ermite.runner.exec.v1.Frame.hello:type_name -> ermite.runner.exec.v1.Hello
539+ 3, // 1: ermite.runner.exec.v1.Frame.run:type_name -> ermite.runner.exec.v1.Run
540+ 4, // 2: ermite.runner.exec.v1.Frame.output:type_name -> ermite.runner.exec.v1.Output
541+ 5, // 3: ermite.runner.exec.v1.Frame.exit:type_name -> ermite.runner.exec.v1.Exit
542+ 6, // 4: ermite.runner.exec.v1.Frame.cancel:type_name -> ermite.runner.exec.v1.Cancel
543+ 7, // 5: ermite.runner.exec.v1.Run.env:type_name -> ermite.runner.exec.v1.Run.EnvEntry
544+ 0, // 6: ermite.runner.exec.v1.Output.stream:type_name -> ermite.runner.exec.v1.StreamKind
545+ 7, // [7:7] is the sub-list for method output_type
546+ 7, // [7:7] is the sub-list for method input_type
547+ 7, // [7:7] is the sub-list for extension type_name
548+ 7, // [7:7] is the sub-list for extension extendee
549+ 0, // [0:7] is the sub-list for field type_name
550+}
551+
552+func init() { file_exec_proto_init() }
553+func file_exec_proto_init() {
554+ if File_exec_proto != nil {
555+ return
556+ }
557+ file_exec_proto_msgTypes[0].OneofWrappers = []any{
558+ (*Frame_Hello)(nil),
559+ (*Frame_Run)(nil),
560+ (*Frame_Output)(nil),
561+ (*Frame_Exit)(nil),
562+ (*Frame_Cancel)(nil),
563+ }
564+ type x struct{}
565+ out := protoimpl.TypeBuilder{
566+ File: protoimpl.DescBuilder{
567+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
568+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_exec_proto_rawDesc), len(file_exec_proto_rawDesc)),
569+ NumEnums: 1,
570+ NumMessages: 7,
571+ NumExtensions: 0,
572+ NumServices: 0,
573+ },
574+ GoTypes: file_exec_proto_goTypes,
575+ DependencyIndexes: file_exec_proto_depIdxs,
576+ EnumInfos: file_exec_proto_enumTypes,
577+ MessageInfos: file_exec_proto_msgTypes,
578+ }.Build()
579+ File_exec_proto = out.File
580+ file_exec_proto_goTypes = nil
581+ file_exec_proto_depIdxs = nil
582+}
A internal/execstream/frame.go +136 -0
@@ -0,0 +1,136 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Package execstream is the framed protobuf stream between the host process of
4+// ermite-runner and the executor running inside a sandbox.
5+//
6+// Wire format: each frame is a 4-byte big-endian length followed by that many
7+// bytes of a serialized execpb.Frame (see exec.proto). A frame is at most
8+// MaxFrame bytes; longer output is split by the sender. The stream is
9+// transport-agnostic: vsock in a micro-VM, a Unix socket in a container.
10+package execstream
11+
12+//go:generate protoc --proto_path=. --go_out=execpb --go_opt=paths=source_relative exec.proto
13+
14+import (
15+ "bufio"
16+ "context"
17+ "encoding/binary"
18+ "errors"
19+ "fmt"
20+ "io"
21+ "sync"
22+
23+ "google.golang.org/protobuf/proto"
24+
25+ "bnema.dev/ermite-runner/internal/execstream/execpb"
26+)
27+
28+// MaxFrame is the largest accepted serialized frame.
29+const MaxFrame = 1 << 20
30+
31+// ChunkSize is the largest output payload placed in one Output frame.
32+const ChunkSize = 32 * 1024
33+
34+// ErrFrameTooLarge is returned for a frame above MaxFrame, on either side.
35+var ErrFrameTooLarge = errors.New("execstream: frame too large")
36+
37+// Conn reads and writes frames on a byte stream. Send is safe for concurrent
38+// use; Recv must be called from one goroutine.
39+type Conn struct {
40+ rw io.ReadWriter
41+ br *bufio.Reader
42+ wmu sync.Mutex
43+}
44+
45+// NewConn wraps a byte stream.
46+func NewConn(rw io.ReadWriter) *Conn {
47+ return &Conn{rw: rw, br: bufio.NewReaderSize(rw, 64*1024)}
48+}
49+
50+// Send writes one frame.
51+func (c *Conn) Send(f *execpb.Frame) error {
52+ b, err := proto.Marshal(f)
53+ if err != nil {
54+ return fmt.Errorf("execstream: marshal: %w", err)
55+ }
56+ if len(b) > MaxFrame {
57+ return ErrFrameTooLarge
58+ }
59+ buf := make([]byte, 4+len(b))
60+ binary.BigEndian.PutUint32(buf, uint32(len(b)))
61+ copy(buf[4:], b)
62+ c.wmu.Lock()
63+ defer c.wmu.Unlock()
64+ _, err = c.rw.Write(buf)
65+ return err
66+}
67+
68+// Recv reads one frame. It returns io.EOF only on a clean end of stream between
69+// frames; a stream cut inside a frame yields io.ErrUnexpectedEOF.
70+func (c *Conn) Recv() (*execpb.Frame, error) {
71+ var hdr [4]byte
72+ if _, err := io.ReadFull(c.br, hdr[:]); err != nil {
73+ return nil, err
74+ }
75+ n := binary.BigEndian.Uint32(hdr[:])
76+ if n > MaxFrame {
77+ return nil, ErrFrameTooLarge
78+ }
79+ b := make([]byte, n)
80+ if _, err := io.ReadFull(c.br, b); err != nil {
81+ if errors.Is(err, io.EOF) {
82+ err = io.ErrUnexpectedEOF
83+ }
84+ return nil, err
85+ }
86+ f := &execpb.Frame{}
87+ if err := proto.Unmarshal(b, f); err != nil {
88+ return nil, fmt.Errorf("execstream: unmarshal: %w", err)
89+ }
90+ return f, nil
91+}
92+
93+// 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.
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) {
99+ c := NewConn(rw)
100+ first, err := c.Recv()
101+ if err != nil {
102+ return nil, nil, fmt.Errorf("execstream: waiting for hello: %w", err)
103+ }
104+ hello := first.GetHello()
105+ if hello == nil {
106+ return nil, nil, fmt.Errorf("execstream: first frame is not hello")
107+ }
108+ if err := c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: run}}); err != nil {
109+ return hello, nil, fmt.Errorf("execstream: sending run: %w", err)
110+ }
111+
112+ done := make(chan struct{})
113+ 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+ }()
121+
122+ for {
123+ f, err := c.Recv()
124+ if err != nil {
125+ return hello, nil, fmt.Errorf("execstream: reading: %w", err)
126+ }
127+ switch b := f.Body.(type) {
128+ case *execpb.Frame_Output:
129+ if onOutput != nil {
130+ onOutput(b.Output)
131+ }
132+ case *execpb.Frame_Exit:
133+ return hello, b.Exit, nil
134+ }
135+ }
136+}
@@ -0,0 +1,136 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Package execstream is the framed protobuf stream between the host process of
4+// ermite-runner and the executor running inside a sandbox.
5+//
6+// Wire format: each frame is a 4-byte big-endian length followed by that many
7+// bytes of a serialized execpb.Frame (see exec.proto). A frame is at most
8+// MaxFrame bytes; longer output is split by the sender. The stream is
9+// transport-agnostic: vsock in a micro-VM, a Unix socket in a container.
10+package execstream
11+
12+//go:generate protoc --proto_path=. --go_out=execpb --go_opt=paths=source_relative exec.proto
13+
14+import (
15+ "bufio"
16+ "context"
17+ "encoding/binary"
18+ "errors"
19+ "fmt"
20+ "io"
21+ "sync"
22+
23+ "google.golang.org/protobuf/proto"
24+
25+ "bnema.dev/ermite-runner/internal/execstream/execpb"
26+)
27+
28+// MaxFrame is the largest accepted serialized frame.
29+const MaxFrame = 1 << 20
30+
31+// ChunkSize is the largest output payload placed in one Output frame.
32+const ChunkSize = 32 * 1024
33+
34+// ErrFrameTooLarge is returned for a frame above MaxFrame, on either side.
35+var ErrFrameTooLarge = errors.New("execstream: frame too large")
36+
37+// Conn reads and writes frames on a byte stream. Send is safe for concurrent
38+// use; Recv must be called from one goroutine.
39+type Conn struct {
40+ rw io.ReadWriter
41+ br *bufio.Reader
42+ wmu sync.Mutex
43+}
44+
45+// NewConn wraps a byte stream.
46+func NewConn(rw io.ReadWriter) *Conn {
47+ return &Conn{rw: rw, br: bufio.NewReaderSize(rw, 64*1024)}
48+}
49+
50+// Send writes one frame.
51+func (c *Conn) Send(f *execpb.Frame) error {
52+ b, err := proto.Marshal(f)
53+ if err != nil {
54+ return fmt.Errorf("execstream: marshal: %w", err)
55+ }
56+ if len(b) > MaxFrame {
57+ return ErrFrameTooLarge
58+ }
59+ buf := make([]byte, 4+len(b))
60+ binary.BigEndian.PutUint32(buf, uint32(len(b)))
61+ copy(buf[4:], b)
62+ c.wmu.Lock()
63+ defer c.wmu.Unlock()
64+ _, err = c.rw.Write(buf)
65+ return err
66+}
67+
68+// Recv reads one frame. It returns io.EOF only on a clean end of stream between
69+// frames; a stream cut inside a frame yields io.ErrUnexpectedEOF.
70+func (c *Conn) Recv() (*execpb.Frame, error) {
71+ var hdr [4]byte
72+ if _, err := io.ReadFull(c.br, hdr[:]); err != nil {
73+ return nil, err
74+ }
75+ n := binary.BigEndian.Uint32(hdr[:])
76+ if n > MaxFrame {
77+ return nil, ErrFrameTooLarge
78+ }
79+ b := make([]byte, n)
80+ if _, err := io.ReadFull(c.br, b); err != nil {
81+ if errors.Is(err, io.EOF) {
82+ err = io.ErrUnexpectedEOF
83+ }
84+ return nil, err
85+ }
86+ f := &execpb.Frame{}
87+ if err := proto.Unmarshal(b, f); err != nil {
88+ return nil, fmt.Errorf("execstream: unmarshal: %w", err)
89+ }
90+ return f, nil
91+}
92+
93+// 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.
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) {
99+ c := NewConn(rw)
100+ first, err := c.Recv()
101+ if err != nil {
102+ return nil, nil, fmt.Errorf("execstream: waiting for hello: %w", err)
103+ }
104+ hello := first.GetHello()
105+ if hello == nil {
106+ return nil, nil, fmt.Errorf("execstream: first frame is not hello")
107+ }
108+ if err := c.Send(&execpb.Frame{Body: &execpb.Frame_Run{Run: run}}); err != nil {
109+ return hello, nil, fmt.Errorf("execstream: sending run: %w", err)
110+ }
111+
112+ done := make(chan struct{})
113+ 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+ }()
121+
122+ for {
123+ f, err := c.Recv()
124+ if err != nil {
125+ return hello, nil, fmt.Errorf("execstream: reading: %w", err)
126+ }
127+ switch b := f.Body.(type) {
128+ case *execpb.Frame_Output:
129+ if onOutput != nil {
130+ onOutput(b.Output)
131+ }
132+ case *execpb.Frame_Exit:
133+ return hello, b.Exit, nil
134+ }
135+ }
136+}
A internal/execstream/frame_test.go +139 -0
@@ -0,0 +1,139 @@
1+// SPDX-License-Identifier: MIT
2+
3+package execstream
4+
5+import (
6+ "bytes"
7+ "context"
8+ "encoding/binary"
9+ "errors"
10+ "io"
11+ "net"
12+ "testing"
13+ "testing/iotest"
14+ "time"
15+
16+ "bnema.dev/ermite-runner/internal/execstream/execpb"
17+)
18+
19+func TestRoundTrip(t *testing.T) {
20+ var buf bytes.Buffer
21+ c := NewConn(&buf)
22+ in := []*execpb.Frame{
23+ {Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "v", Sandbox: "vm"}}},
24+ {Body: &execpb.Frame_Output{Output: &execpb.Output{Stream: execpb.StreamKind_STREAM_KIND_STDERR, Data: []byte("x\x00y")}}},
25+ {Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 3}}},
26+ }
27+ for _, f := range in {
28+ if err := c.Send(f); err != nil {
29+ t.Fatal(err)
30+ }
31+ }
32+ // One byte at a time: framing must not depend on read sizes.
33+ r := NewConn(struct {
34+ io.Reader
35+ io.Writer
36+ }{iotest.OneByteReader(&buf), io.Discard})
37+ for i := range in {
38+ out, err := r.Recv()
39+ if err != nil {
40+ t.Fatalf("frame %d: %v", i, err)
41+ }
42+ if out.String() != in[i].String() {
43+ t.Fatalf("frame %d: got %v want %v", i, out, in[i])
44+ }
45+ }
46+ if _, err := r.Recv(); err != io.EOF {
47+ t.Fatalf("want io.EOF at end, got %v", err)
48+ }
49+}
50+
51+func TestRecvErrors(t *testing.T) {
52+ huge := make([]byte, 4)
53+ binary.BigEndian.PutUint32(huge, MaxFrame+1)
54+ if _, err := NewConn(bytes.NewBuffer(huge)).Recv(); !errors.Is(err, ErrFrameTooLarge) {
55+ t.Fatalf("oversize: %v", err)
56+ }
57+ trunc := []byte{0, 0, 0, 9, 1, 2}
58+ if _, err := NewConn(bytes.NewBuffer(trunc)).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
59+ t.Fatalf("truncated body: %v", err)
60+ }
61+ if _, err := NewConn(bytes.NewBuffer([]byte{0, 0})).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
62+ t.Fatalf("truncated header: %v", err)
63+ }
64+ garbage := []byte{0, 0, 0, 3, 0xff, 0xff, 0xff}
65+ if _, err := NewConn(bytes.NewBuffer(garbage)).Recv(); err == nil {
66+ t.Fatal("garbage payload accepted")
67+ }
68+}
69+
70+func TestSendTooLarge(t *testing.T) {
71+ c := NewConn(struct {
72+ io.Reader
73+ io.Writer
74+ }{bytes.NewReader(nil), io.Discard})
75+ err := c.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: make([]byte, MaxFrame)}}})
76+ if !errors.Is(err, ErrFrameTooLarge) {
77+ t.Fatalf("got %v", err)
78+ }
79+}
80+
81+func TestDrive(t *testing.T) {
82+ host, guest := net.Pipe()
83+ defer func() { _ = host.Close() }()
84+ go func() {
85+ defer func() { _ = guest.Close() }()
86+ g := NewConn(guest)
87+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Sandbox: "container"}}})
88+ f, err := g.Recv()
89+ if err != nil || f.GetRun() == nil || f.GetRun().Script != "echo hi" {
90+ return
91+ }
92+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: []byte("hi\n")}}})
93+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})
94+ }()
95+ 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...) })
97+ if err != nil {
98+ t.Fatal(err)
99+ }
100+ if hello.Sandbox != "container" || exit.Code != 7 || string(got) != "hi\n" {
101+ t.Fatalf("hello=%v exit=%v out=%q", hello, exit, got)
102+ }
103+}
104+
105+func TestDriveCancelSendsCancel(t *testing.T) {
106+ host, guest := net.Pipe()
107+ defer func() { _ = host.Close() }()
108+ gotCancel := make(chan bool, 1)
109+ go func() {
110+ defer func() { _ = guest.Close() }()
111+ g := NewConn(guest)
112+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
113+ _, _ = g.Recv() // run
114+ f, err := g.Recv()
115+ gotCancel <- err == nil && f.GetCancel() != nil
116+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})
117+ }()
118+ ctx, cancel := context.WithCancel(context.Background())
119+ time.AfterFunc(50*time.Millisecond, cancel)
120+ _, exit, err := Drive(ctx, host, &execpb.Run{}, nil)
121+ if err != nil || exit.Code != 130 {
122+ t.Fatalf("exit=%v err=%v", exit, err)
123+ }
124+ if !<-gotCancel {
125+ t.Fatal("guest did not receive a Cancel frame")
126+ }
127+}
128+
129+func TestDriveHelloMissing(t *testing.T) {
130+ host, guest := net.Pipe()
131+ defer func() { _ = host.Close() }()
132+ go func() {
133+ defer func() { _ = guest.Close() }()
134+ _ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
135+ }()
136+ if _, _, err := Drive(context.Background(), host, &execpb.Run{}, nil); err == nil {
137+ t.Fatal("want error when the first frame is not hello")
138+ }
139+}
@@ -0,0 +1,139 @@
1+// SPDX-License-Identifier: MIT
2+
3+package execstream
4+
5+import (
6+ "bytes"
7+ "context"
8+ "encoding/binary"
9+ "errors"
10+ "io"
11+ "net"
12+ "testing"
13+ "testing/iotest"
14+ "time"
15+
16+ "bnema.dev/ermite-runner/internal/execstream/execpb"
17+)
18+
19+func TestRoundTrip(t *testing.T) {
20+ var buf bytes.Buffer
21+ c := NewConn(&buf)
22+ in := []*execpb.Frame{
23+ {Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Version: "v", Sandbox: "vm"}}},
24+ {Body: &execpb.Frame_Output{Output: &execpb.Output{Stream: execpb.StreamKind_STREAM_KIND_STDERR, Data: []byte("x\x00y")}}},
25+ {Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 3}}},
26+ }
27+ for _, f := range in {
28+ if err := c.Send(f); err != nil {
29+ t.Fatal(err)
30+ }
31+ }
32+ // One byte at a time: framing must not depend on read sizes.
33+ r := NewConn(struct {
34+ io.Reader
35+ io.Writer
36+ }{iotest.OneByteReader(&buf), io.Discard})
37+ for i := range in {
38+ out, err := r.Recv()
39+ if err != nil {
40+ t.Fatalf("frame %d: %v", i, err)
41+ }
42+ if out.String() != in[i].String() {
43+ t.Fatalf("frame %d: got %v want %v", i, out, in[i])
44+ }
45+ }
46+ if _, err := r.Recv(); err != io.EOF {
47+ t.Fatalf("want io.EOF at end, got %v", err)
48+ }
49+}
50+
51+func TestRecvErrors(t *testing.T) {
52+ huge := make([]byte, 4)
53+ binary.BigEndian.PutUint32(huge, MaxFrame+1)
54+ if _, err := NewConn(bytes.NewBuffer(huge)).Recv(); !errors.Is(err, ErrFrameTooLarge) {
55+ t.Fatalf("oversize: %v", err)
56+ }
57+ trunc := []byte{0, 0, 0, 9, 1, 2}
58+ if _, err := NewConn(bytes.NewBuffer(trunc)).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
59+ t.Fatalf("truncated body: %v", err)
60+ }
61+ if _, err := NewConn(bytes.NewBuffer([]byte{0, 0})).Recv(); !errors.Is(err, io.ErrUnexpectedEOF) {
62+ t.Fatalf("truncated header: %v", err)
63+ }
64+ garbage := []byte{0, 0, 0, 3, 0xff, 0xff, 0xff}
65+ if _, err := NewConn(bytes.NewBuffer(garbage)).Recv(); err == nil {
66+ t.Fatal("garbage payload accepted")
67+ }
68+}
69+
70+func TestSendTooLarge(t *testing.T) {
71+ c := NewConn(struct {
72+ io.Reader
73+ io.Writer
74+ }{bytes.NewReader(nil), io.Discard})
75+ err := c.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: make([]byte, MaxFrame)}}})
76+ if !errors.Is(err, ErrFrameTooLarge) {
77+ t.Fatalf("got %v", err)
78+ }
79+}
80+
81+func TestDrive(t *testing.T) {
82+ host, guest := net.Pipe()
83+ defer func() { _ = host.Close() }()
84+ go func() {
85+ defer func() { _ = guest.Close() }()
86+ g := NewConn(guest)
87+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{Sandbox: "container"}}})
88+ f, err := g.Recv()
89+ if err != nil || f.GetRun() == nil || f.GetRun().Script != "echo hi" {
90+ return
91+ }
92+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Output{Output: &execpb.Output{Data: []byte("hi\n")}}})
93+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 7}}})
94+ }()
95+ 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...) })
97+ if err != nil {
98+ t.Fatal(err)
99+ }
100+ if hello.Sandbox != "container" || exit.Code != 7 || string(got) != "hi\n" {
101+ t.Fatalf("hello=%v exit=%v out=%q", hello, exit, got)
102+ }
103+}
104+
105+func TestDriveCancelSendsCancel(t *testing.T) {
106+ host, guest := net.Pipe()
107+ defer func() { _ = host.Close() }()
108+ gotCancel := make(chan bool, 1)
109+ go func() {
110+ defer func() { _ = guest.Close() }()
111+ g := NewConn(guest)
112+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Hello{Hello: &execpb.Hello{}}})
113+ _, _ = g.Recv() // run
114+ f, err := g.Recv()
115+ gotCancel <- err == nil && f.GetCancel() != nil
116+ _ = g.Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{Code: 130}}})
117+ }()
118+ ctx, cancel := context.WithCancel(context.Background())
119+ time.AfterFunc(50*time.Millisecond, cancel)
120+ _, exit, err := Drive(ctx, host, &execpb.Run{}, nil)
121+ if err != nil || exit.Code != 130 {
122+ t.Fatalf("exit=%v err=%v", exit, err)
123+ }
124+ if !<-gotCancel {
125+ t.Fatal("guest did not receive a Cancel frame")
126+ }
127+}
128+
129+func TestDriveHelloMissing(t *testing.T) {
130+ host, guest := net.Pipe()
131+ defer func() { _ = host.Close() }()
132+ go func() {
133+ defer func() { _ = guest.Close() }()
134+ _ = NewConn(guest).Send(&execpb.Frame{Body: &execpb.Frame_Exit{Exit: &execpb.Exit{}}})
135+ }()
136+ if _, _, err := Drive(context.Background(), host, &execpb.Run{}, nil); err == nil {
137+ t.Fatal("want error when the first frame is not hello")
138+ }
139+}

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