Emit an explicit "Started" message and support sending signals (#56)
* Emit an explicit "Started" message and support sending signals Co-authored-by: Fedor Korotkov <fkorotkov@openai.com> * Run exec commands in dedicated process groups * Disable wsl_v5 linter * Clean up exec processes on stream failures * Fix gosec linter error * Wrap exec signals in a message --------- Co-authored-by: Fedor Korotkov <fkorotkov@openai.com>
This commit is contained in:
parent
df114a194a
commit
d4ebcbde41
|
|
@ -32,6 +32,7 @@ linters:
|
||||||
|
|
||||||
# Style linters that are total nuts.
|
# Style linters that are total nuts.
|
||||||
- wsl
|
- wsl
|
||||||
|
- wsl_v5
|
||||||
- funlen
|
- funlen
|
||||||
|
|
||||||
# Enough parallelism for now.
|
# Enough parallelism for now.
|
||||||
|
|
@ -61,6 +62,15 @@ linters:
|
||||||
# Not all errors need to be checked
|
# Not all errors need to be checked
|
||||||
- errcheck
|
- errcheck
|
||||||
|
|
||||||
|
# It's OK to not initialize some struct fields
|
||||||
|
- exhaustruct
|
||||||
|
|
||||||
|
# This is not a library, so it's OK to use dynamic errors
|
||||||
|
- err113
|
||||||
|
|
||||||
|
# Inline error handling keeps assignment and checking together
|
||||||
|
- noinlineerr
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
# Don't hide multiple issues that belong to one class since GitHub annotations can handle them all nicely.
|
# Don't hide multiple issues that belong to one class since GitHub annotations can handle them all nicely.
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.36.6
|
// protoc-gen-go v1.36.6
|
||||||
// protoc (unknown)
|
// protoc v7.35.1
|
||||||
// source: rpc/agent.proto
|
// source: rpc/agent.proto
|
||||||
|
|
||||||
package rpc
|
package rpc
|
||||||
|
|
@ -22,6 +22,55 @@ const (
|
||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ExecRequest_SendSignal_Signal int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExecRequest_SendSignal_SIGNAL_UNSPECIFIED ExecRequest_SendSignal_Signal = 0
|
||||||
|
ExecRequest_SendSignal_SIGNAL_SIGTERM ExecRequest_SendSignal_Signal = 1
|
||||||
|
ExecRequest_SendSignal_SIGNAL_SIGKILL ExecRequest_SendSignal_Signal = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ExecRequest_SendSignal_Signal.
|
||||||
|
var (
|
||||||
|
ExecRequest_SendSignal_Signal_name = map[int32]string{
|
||||||
|
0: "SIGNAL_UNSPECIFIED",
|
||||||
|
1: "SIGNAL_SIGTERM",
|
||||||
|
2: "SIGNAL_SIGKILL",
|
||||||
|
}
|
||||||
|
ExecRequest_SendSignal_Signal_value = map[string]int32{
|
||||||
|
"SIGNAL_UNSPECIFIED": 0,
|
||||||
|
"SIGNAL_SIGTERM": 1,
|
||||||
|
"SIGNAL_SIGKILL": 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ExecRequest_SendSignal_Signal) Enum() *ExecRequest_SendSignal_Signal {
|
||||||
|
p := new(ExecRequest_SendSignal_Signal)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ExecRequest_SendSignal_Signal) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExecRequest_SendSignal_Signal) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_rpc_agent_proto_enumTypes[0].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ExecRequest_SendSignal_Signal) Type() protoreflect.EnumType {
|
||||||
|
return &file_rpc_agent_proto_enumTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ExecRequest_SendSignal_Signal) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExecRequest_SendSignal_Signal.Descriptor instead.
|
||||||
|
func (ExecRequest_SendSignal_Signal) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_rpc_agent_proto_rawDescGZIP(), []int{0, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
type ExecRequest struct {
|
type ExecRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Types that are valid to be assigned to Type:
|
// Types that are valid to be assigned to Type:
|
||||||
|
|
@ -29,6 +78,7 @@ type ExecRequest struct {
|
||||||
// *ExecRequest_Command_
|
// *ExecRequest_Command_
|
||||||
// *ExecRequest_StandardInput
|
// *ExecRequest_StandardInput
|
||||||
// *ExecRequest_TerminalResize
|
// *ExecRequest_TerminalResize
|
||||||
|
// *ExecRequest_SendSignal_
|
||||||
Type isExecRequest_Type `protobuf_oneof:"type"`
|
Type isExecRequest_Type `protobuf_oneof:"type"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|
@ -98,6 +148,15 @@ func (x *ExecRequest) GetTerminalResize() *TerminalSize {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ExecRequest) GetSendSignal() *ExecRequest_SendSignal {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Type.(*ExecRequest_SendSignal_); ok {
|
||||||
|
return x.SendSignal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type isExecRequest_Type interface {
|
type isExecRequest_Type interface {
|
||||||
isExecRequest_Type()
|
isExecRequest_Type()
|
||||||
}
|
}
|
||||||
|
|
@ -114,12 +173,18 @@ type ExecRequest_TerminalResize struct {
|
||||||
TerminalResize *TerminalSize `protobuf:"bytes,3,opt,name=terminal_resize,json=terminalResize,proto3,oneof"`
|
TerminalResize *TerminalSize `protobuf:"bytes,3,opt,name=terminal_resize,json=terminalResize,proto3,oneof"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecRequest_SendSignal_ struct {
|
||||||
|
SendSignal *ExecRequest_SendSignal `protobuf:"bytes,4,opt,name=send_signal,json=sendSignal,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
func (*ExecRequest_Command_) isExecRequest_Type() {}
|
func (*ExecRequest_Command_) isExecRequest_Type() {}
|
||||||
|
|
||||||
func (*ExecRequest_StandardInput) isExecRequest_Type() {}
|
func (*ExecRequest_StandardInput) isExecRequest_Type() {}
|
||||||
|
|
||||||
func (*ExecRequest_TerminalResize) isExecRequest_Type() {}
|
func (*ExecRequest_TerminalResize) isExecRequest_Type() {}
|
||||||
|
|
||||||
|
func (*ExecRequest_SendSignal_) isExecRequest_Type() {}
|
||||||
|
|
||||||
type ExecResponse struct {
|
type ExecResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Types that are valid to be assigned to Type:
|
// Types that are valid to be assigned to Type:
|
||||||
|
|
@ -127,6 +192,7 @@ type ExecResponse struct {
|
||||||
// *ExecResponse_Exit_
|
// *ExecResponse_Exit_
|
||||||
// *ExecResponse_StandardOutput
|
// *ExecResponse_StandardOutput
|
||||||
// *ExecResponse_StandardError
|
// *ExecResponse_StandardError
|
||||||
|
// *ExecResponse_Started_
|
||||||
Type isExecResponse_Type `protobuf_oneof:"type"`
|
Type isExecResponse_Type `protobuf_oneof:"type"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
|
|
@ -196,6 +262,15 @@ func (x *ExecResponse) GetStandardError() *IOChunk {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *ExecResponse) GetStarted() *ExecResponse_Started {
|
||||||
|
if x != nil {
|
||||||
|
if x, ok := x.Type.(*ExecResponse_Started_); ok {
|
||||||
|
return x.Started
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type isExecResponse_Type interface {
|
type isExecResponse_Type interface {
|
||||||
isExecResponse_Type()
|
isExecResponse_Type()
|
||||||
}
|
}
|
||||||
|
|
@ -212,12 +287,18 @@ type ExecResponse_StandardError struct {
|
||||||
StandardError *IOChunk `protobuf:"bytes,3,opt,name=standard_error,json=standardError,proto3,oneof"`
|
StandardError *IOChunk `protobuf:"bytes,3,opt,name=standard_error,json=standardError,proto3,oneof"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecResponse_Started_ struct {
|
||||||
|
Started *ExecResponse_Started `protobuf:"bytes,4,opt,name=started,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
func (*ExecResponse_Exit_) isExecResponse_Type() {}
|
func (*ExecResponse_Exit_) isExecResponse_Type() {}
|
||||||
|
|
||||||
func (*ExecResponse_StandardOutput) isExecResponse_Type() {}
|
func (*ExecResponse_StandardOutput) isExecResponse_Type() {}
|
||||||
|
|
||||||
func (*ExecResponse_StandardError) isExecResponse_Type() {}
|
func (*ExecResponse_StandardError) isExecResponse_Type() {}
|
||||||
|
|
||||||
|
func (*ExecResponse_Started_) isExecResponse_Type() {}
|
||||||
|
|
||||||
type TerminalSize struct {
|
type TerminalSize struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Rows uint32 `protobuf:"varint,1,opt,name=rows,proto3" json:"rows,omitempty"`
|
Rows uint32 `protobuf:"varint,1,opt,name=rows,proto3" json:"rows,omitempty"`
|
||||||
|
|
@ -394,6 +475,50 @@ func (x *ResolveIPResponse) GetIp() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecRequest_SendSignal struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Signal ExecRequest_SendSignal_Signal `protobuf:"varint,1,opt,name=signal,proto3,enum=ExecRequest_SendSignal_Signal" json:"signal,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecRequest_SendSignal) Reset() {
|
||||||
|
*x = ExecRequest_SendSignal{}
|
||||||
|
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecRequest_SendSignal) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExecRequest_SendSignal) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExecRequest_SendSignal) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_rpc_agent_proto_msgTypes[6]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExecRequest_SendSignal.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExecRequest_SendSignal) Descriptor() ([]byte, []int) {
|
||||||
|
return file_rpc_agent_proto_rawDescGZIP(), []int{0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecRequest_SendSignal) GetSignal() ExecRequest_SendSignal_Signal {
|
||||||
|
if x != nil {
|
||||||
|
return x.Signal
|
||||||
|
}
|
||||||
|
return ExecRequest_SendSignal_SIGNAL_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
type ExecRequest_Command struct {
|
type ExecRequest_Command struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||||
|
|
@ -410,7 +535,7 @@ type ExecRequest_Command struct {
|
||||||
|
|
||||||
func (x *ExecRequest_Command) Reset() {
|
func (x *ExecRequest_Command) Reset() {
|
||||||
*x = ExecRequest_Command{}
|
*x = ExecRequest_Command{}
|
||||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
mi := &file_rpc_agent_proto_msgTypes[7]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -422,7 +547,7 @@ func (x *ExecRequest_Command) String() string {
|
||||||
func (*ExecRequest_Command) ProtoMessage() {}
|
func (*ExecRequest_Command) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ExecRequest_Command) ProtoReflect() protoreflect.Message {
|
func (x *ExecRequest_Command) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_rpc_agent_proto_msgTypes[6]
|
mi := &file_rpc_agent_proto_msgTypes[7]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -435,7 +560,7 @@ func (x *ExecRequest_Command) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use ExecRequest_Command.ProtoReflect.Descriptor instead.
|
// Deprecated: Use ExecRequest_Command.ProtoReflect.Descriptor instead.
|
||||||
func (*ExecRequest_Command) Descriptor() ([]byte, []int) {
|
func (*ExecRequest_Command) Descriptor() ([]byte, []int) {
|
||||||
return file_rpc_agent_proto_rawDescGZIP(), []int{0, 0}
|
return file_rpc_agent_proto_rawDescGZIP(), []int{0, 1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *ExecRequest_Command) GetName() string {
|
func (x *ExecRequest_Command) GetName() string {
|
||||||
|
|
@ -503,7 +628,7 @@ type ExecResponse_Exit struct {
|
||||||
|
|
||||||
func (x *ExecResponse_Exit) Reset() {
|
func (x *ExecResponse_Exit) Reset() {
|
||||||
*x = ExecResponse_Exit{}
|
*x = ExecResponse_Exit{}
|
||||||
mi := &file_rpc_agent_proto_msgTypes[8]
|
mi := &file_rpc_agent_proto_msgTypes[9]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -515,7 +640,7 @@ func (x *ExecResponse_Exit) String() string {
|
||||||
func (*ExecResponse_Exit) ProtoMessage() {}
|
func (*ExecResponse_Exit) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *ExecResponse_Exit) ProtoReflect() protoreflect.Message {
|
func (x *ExecResponse_Exit) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_rpc_agent_proto_msgTypes[8]
|
mi := &file_rpc_agent_proto_msgTypes[9]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -538,15 +663,60 @@ func (x *ExecResponse_Exit) GetCode() int32 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecResponse_Started struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecResponse_Started) Reset() {
|
||||||
|
*x = ExecResponse_Started{}
|
||||||
|
mi := &file_rpc_agent_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExecResponse_Started) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExecResponse_Started) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExecResponse_Started) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_rpc_agent_proto_msgTypes[10]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExecResponse_Started.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExecResponse_Started) Descriptor() ([]byte, []int) {
|
||||||
|
return file_rpc_agent_proto_rawDescGZIP(), []int{1, 1}
|
||||||
|
}
|
||||||
|
|
||||||
var File_rpc_agent_proto protoreflect.FileDescriptor
|
var File_rpc_agent_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_rpc_agent_proto_rawDesc = "" +
|
const file_rpc_agent_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x0frpc/agent.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xeb\x03\n" +
|
"\x0frpc/agent.proto\x1a\x1bgoogle/protobuf/empty.proto\"\xb8\x05\n" +
|
||||||
"\vExecRequest\x120\n" +
|
"\vExecRequest\x120\n" +
|
||||||
"\acommand\x18\x01 \x01(\v2\x14.ExecRequest.CommandH\x00R\acommand\x121\n" +
|
"\acommand\x18\x01 \x01(\v2\x14.ExecRequest.CommandH\x00R\acommand\x121\n" +
|
||||||
"\x0estandard_input\x18\x02 \x01(\v2\b.IOChunkH\x00R\rstandardInput\x128\n" +
|
"\x0estandard_input\x18\x02 \x01(\v2\b.IOChunkH\x00R\rstandardInput\x128\n" +
|
||||||
"\x0fterminal_resize\x18\x03 \x01(\v2\r.TerminalSizeH\x00R\x0eterminalResize\x1a\xb4\x02\n" +
|
"\x0fterminal_resize\x18\x03 \x01(\v2\r.TerminalSizeH\x00R\x0eterminalResize\x12:\n" +
|
||||||
|
"\vsend_signal\x18\x04 \x01(\v2\x17.ExecRequest.SendSignalH\x00R\n" +
|
||||||
|
"sendSignal\x1a\x8e\x01\n" +
|
||||||
|
"\n" +
|
||||||
|
"SendSignal\x126\n" +
|
||||||
|
"\x06signal\x18\x01 \x01(\x0e2\x1e.ExecRequest.SendSignal.SignalR\x06signal\"H\n" +
|
||||||
|
"\x06Signal\x12\x16\n" +
|
||||||
|
"\x12SIGNAL_UNSPECIFIED\x10\x00\x12\x12\n" +
|
||||||
|
"\x0eSIGNAL_SIGTERM\x10\x01\x12\x12\n" +
|
||||||
|
"\x0eSIGNAL_SIGKILL\x10\x02\x1a\xb4\x02\n" +
|
||||||
"\aCommand\x12\x12\n" +
|
"\aCommand\x12\x12\n" +
|
||||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
"\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" +
|
||||||
"\x04args\x18\x02 \x03(\tR\x04args\x12 \n" +
|
"\x04args\x18\x02 \x03(\tR\x04args\x12 \n" +
|
||||||
|
|
@ -559,13 +729,15 @@ const file_rpc_agent_proto_rawDesc = "" +
|
||||||
"\bEnvEntry\x12\x10\n" +
|
"\bEnvEntry\x12\x10\n" +
|
||||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" +
|
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" +
|
||||||
"\x04type\"\xc4\x01\n" +
|
"\x04type\"\x82\x02\n" +
|
||||||
"\fExecResponse\x12(\n" +
|
"\fExecResponse\x12(\n" +
|
||||||
"\x04exit\x18\x01 \x01(\v2\x12.ExecResponse.ExitH\x00R\x04exit\x123\n" +
|
"\x04exit\x18\x01 \x01(\v2\x12.ExecResponse.ExitH\x00R\x04exit\x123\n" +
|
||||||
"\x0fstandard_output\x18\x02 \x01(\v2\b.IOChunkH\x00R\x0estandardOutput\x121\n" +
|
"\x0fstandard_output\x18\x02 \x01(\v2\b.IOChunkH\x00R\x0estandardOutput\x121\n" +
|
||||||
"\x0estandard_error\x18\x03 \x01(\v2\b.IOChunkH\x00R\rstandardError\x1a\x1a\n" +
|
"\x0estandard_error\x18\x03 \x01(\v2\b.IOChunkH\x00R\rstandardError\x121\n" +
|
||||||
|
"\astarted\x18\x04 \x01(\v2\x15.ExecResponse.StartedH\x00R\astarted\x1a\x1a\n" +
|
||||||
"\x04Exit\x12\x12\n" +
|
"\x04Exit\x12\x12\n" +
|
||||||
"\x04code\x18\x01 \x01(\x05R\x04codeB\x06\n" +
|
"\x04code\x18\x01 \x01(\x05R\x04code\x1a\t\n" +
|
||||||
|
"\aStartedB\x06\n" +
|
||||||
"\x04type\"6\n" +
|
"\x04type\"6\n" +
|
||||||
"\fTerminalSize\x12\x12\n" +
|
"\fTerminalSize\x12\x12\n" +
|
||||||
"\x04rows\x18\x01 \x01(\rR\x04rows\x12\x12\n" +
|
"\x04rows\x18\x01 \x01(\rR\x04rows\x12\x12\n" +
|
||||||
|
|
@ -591,36 +763,43 @@ func file_rpc_agent_proto_rawDescGZIP() []byte {
|
||||||
return file_rpc_agent_proto_rawDescData
|
return file_rpc_agent_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_rpc_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
var file_rpc_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||||
|
var file_rpc_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
|
||||||
var file_rpc_agent_proto_goTypes = []any{
|
var file_rpc_agent_proto_goTypes = []any{
|
||||||
(*ExecRequest)(nil), // 0: ExecRequest
|
(ExecRequest_SendSignal_Signal)(0), // 0: ExecRequest.SendSignal.Signal
|
||||||
(*ExecResponse)(nil), // 1: ExecResponse
|
(*ExecRequest)(nil), // 1: ExecRequest
|
||||||
(*TerminalSize)(nil), // 2: TerminalSize
|
(*ExecResponse)(nil), // 2: ExecResponse
|
||||||
(*IOChunk)(nil), // 3: IOChunk
|
(*TerminalSize)(nil), // 3: TerminalSize
|
||||||
(*ResolveIPRequest)(nil), // 4: ResolveIPRequest
|
(*IOChunk)(nil), // 4: IOChunk
|
||||||
(*ResolveIPResponse)(nil), // 5: ResolveIPResponse
|
(*ResolveIPRequest)(nil), // 5: ResolveIPRequest
|
||||||
(*ExecRequest_Command)(nil), // 6: ExecRequest.Command
|
(*ResolveIPResponse)(nil), // 6: ResolveIPResponse
|
||||||
nil, // 7: ExecRequest.Command.EnvEntry
|
(*ExecRequest_SendSignal)(nil), // 7: ExecRequest.SendSignal
|
||||||
(*ExecResponse_Exit)(nil), // 8: ExecResponse.Exit
|
(*ExecRequest_Command)(nil), // 8: ExecRequest.Command
|
||||||
|
nil, // 9: ExecRequest.Command.EnvEntry
|
||||||
|
(*ExecResponse_Exit)(nil), // 10: ExecResponse.Exit
|
||||||
|
(*ExecResponse_Started)(nil), // 11: ExecResponse.Started
|
||||||
}
|
}
|
||||||
var file_rpc_agent_proto_depIdxs = []int32{
|
var file_rpc_agent_proto_depIdxs = []int32{
|
||||||
6, // 0: ExecRequest.command:type_name -> ExecRequest.Command
|
8, // 0: ExecRequest.command:type_name -> ExecRequest.Command
|
||||||
3, // 1: ExecRequest.standard_input:type_name -> IOChunk
|
4, // 1: ExecRequest.standard_input:type_name -> IOChunk
|
||||||
2, // 2: ExecRequest.terminal_resize:type_name -> TerminalSize
|
3, // 2: ExecRequest.terminal_resize:type_name -> TerminalSize
|
||||||
8, // 3: ExecResponse.exit:type_name -> ExecResponse.Exit
|
7, // 3: ExecRequest.send_signal:type_name -> ExecRequest.SendSignal
|
||||||
3, // 4: ExecResponse.standard_output:type_name -> IOChunk
|
10, // 4: ExecResponse.exit:type_name -> ExecResponse.Exit
|
||||||
3, // 5: ExecResponse.standard_error:type_name -> IOChunk
|
4, // 5: ExecResponse.standard_output:type_name -> IOChunk
|
||||||
2, // 6: ExecRequest.Command.terminal_size:type_name -> TerminalSize
|
4, // 6: ExecResponse.standard_error:type_name -> IOChunk
|
||||||
7, // 7: ExecRequest.Command.env:type_name -> ExecRequest.Command.EnvEntry
|
11, // 7: ExecResponse.started:type_name -> ExecResponse.Started
|
||||||
0, // 8: Agent.Exec:input_type -> ExecRequest
|
0, // 8: ExecRequest.SendSignal.signal:type_name -> ExecRequest.SendSignal.Signal
|
||||||
4, // 9: Agent.ResolveIP:input_type -> ResolveIPRequest
|
3, // 9: ExecRequest.Command.terminal_size:type_name -> TerminalSize
|
||||||
1, // 10: Agent.Exec:output_type -> ExecResponse
|
9, // 10: ExecRequest.Command.env:type_name -> ExecRequest.Command.EnvEntry
|
||||||
5, // 11: Agent.ResolveIP:output_type -> ResolveIPResponse
|
1, // 11: Agent.Exec:input_type -> ExecRequest
|
||||||
10, // [10:12] is the sub-list for method output_type
|
5, // 12: Agent.ResolveIP:input_type -> ResolveIPRequest
|
||||||
8, // [8:10] is the sub-list for method input_type
|
2, // 13: Agent.Exec:output_type -> ExecResponse
|
||||||
8, // [8:8] is the sub-list for extension type_name
|
6, // 14: Agent.ResolveIP:output_type -> ResolveIPResponse
|
||||||
8, // [8:8] is the sub-list for extension extendee
|
13, // [13:15] is the sub-list for method output_type
|
||||||
0, // [0:8] is the sub-list for field type_name
|
11, // [11:13] is the sub-list for method input_type
|
||||||
|
11, // [11:11] is the sub-list for extension type_name
|
||||||
|
11, // [11:11] is the sub-list for extension extendee
|
||||||
|
0, // [0:11] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_rpc_agent_proto_init() }
|
func init() { file_rpc_agent_proto_init() }
|
||||||
|
|
@ -632,24 +811,27 @@ func file_rpc_agent_proto_init() {
|
||||||
(*ExecRequest_Command_)(nil),
|
(*ExecRequest_Command_)(nil),
|
||||||
(*ExecRequest_StandardInput)(nil),
|
(*ExecRequest_StandardInput)(nil),
|
||||||
(*ExecRequest_TerminalResize)(nil),
|
(*ExecRequest_TerminalResize)(nil),
|
||||||
|
(*ExecRequest_SendSignal_)(nil),
|
||||||
}
|
}
|
||||||
file_rpc_agent_proto_msgTypes[1].OneofWrappers = []any{
|
file_rpc_agent_proto_msgTypes[1].OneofWrappers = []any{
|
||||||
(*ExecResponse_Exit_)(nil),
|
(*ExecResponse_Exit_)(nil),
|
||||||
(*ExecResponse_StandardOutput)(nil),
|
(*ExecResponse_StandardOutput)(nil),
|
||||||
(*ExecResponse_StandardError)(nil),
|
(*ExecResponse_StandardError)(nil),
|
||||||
|
(*ExecResponse_Started_)(nil),
|
||||||
}
|
}
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_agent_proto_rawDesc), len(file_rpc_agent_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_rpc_agent_proto_rawDesc), len(file_rpc_agent_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 1,
|
||||||
NumMessages: 9,
|
NumMessages: 11,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
GoTypes: file_rpc_agent_proto_goTypes,
|
GoTypes: file_rpc_agent_proto_goTypes,
|
||||||
DependencyIndexes: file_rpc_agent_proto_depIdxs,
|
DependencyIndexes: file_rpc_agent_proto_depIdxs,
|
||||||
|
EnumInfos: file_rpc_agent_proto_enumTypes,
|
||||||
MessageInfos: file_rpc_agent_proto_msgTypes,
|
MessageInfos: file_rpc_agent_proto_msgTypes,
|
||||||
}.Build()
|
}.Build()
|
||||||
File_rpc_agent_proto = out.File
|
File_rpc_agent_proto = out.File
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,30 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/creack/pty"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
standardStreamsBufferSize = 4096
|
standardStreamsBufferSize = 4096
|
||||||
|
|
||||||
eofChar = 0x04
|
eofChar = 0x04
|
||||||
|
|
||||||
|
// execRuntimeFailureExitCode matches Docker's exit code for runtime failures before a process starts.
|
||||||
|
execRuntimeFailureExitCode = 125
|
||||||
|
// signalExitCodeOffset is the base for shell-style exit codes of processes terminated by signals.
|
||||||
|
signalExitCodeOffset = 128
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||||
|
|
@ -58,12 +65,21 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return err
|
zap.S().Warnf("failed to start %s: %v", formatCommandAndArgs(firstExecRequestCommand.Command.GetName(),
|
||||||
|
firstExecRequestCommand.Command.GetArgs()), err)
|
||||||
|
|
||||||
|
return sendStartFailure(stream)
|
||||||
}
|
}
|
||||||
if cmd.Process != nil {
|
|
||||||
|
// Release ownership before sending responses so failures do not leak the process handle
|
||||||
if err := cmd.Process.Release(); err != nil {
|
if err := cmd.Process.Release(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly notify the client that the process was started
|
||||||
|
err = sendStartSuccess(stream)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stream.Send(&ExecResponse{
|
if err := stream.Send(&ExecResponse{
|
||||||
|
|
@ -79,6 +95,11 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kill the whole process group when the exec stream is canceled
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
return signalProcessGroup(cmd.Process, syscall.SIGKILL)
|
||||||
|
}
|
||||||
|
|
||||||
var stdin io.WriteCloser
|
var stdin io.WriteCloser
|
||||||
var stdout, stderr io.ReadCloser
|
var stdout, stderr io.ReadCloser
|
||||||
var ptmx *os.File
|
var ptmx *os.File
|
||||||
|
|
@ -95,6 +116,9 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
stdout = ptmx
|
stdout = ptmx
|
||||||
stderr = ptmx
|
stderr = ptmx
|
||||||
} else {
|
} else {
|
||||||
|
// Start the command in its own process group so signals reach all descendants
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
|
||||||
if firstExecRequestCommand.Command.Interactive {
|
if firstExecRequestCommand.Command.Interactive {
|
||||||
stdin, err = cmd.StdinPipe()
|
stdin, err = cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -114,22 +138,47 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
|
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
zap.S().Warnf("failed to start %s: %v", formatCommandAndArgs(firstExecRequestCommand.Command.GetName(),
|
||||||
|
firstExecRequestCommand.Command.GetArgs()), err)
|
||||||
|
|
||||||
|
return sendStartFailure(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the PTY is closed if sending the Started response fails
|
||||||
if ptmx != nil {
|
if ptmx != nil {
|
||||||
defer ptmx.Close()
|
defer ptmx.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicitly notify the client that the process was started
|
||||||
|
err = sendStartSuccess(stream)
|
||||||
|
if err != nil {
|
||||||
|
// Output readers have not started yet, so cancel and reap directly
|
||||||
|
_ = cmd.Cancel()
|
||||||
|
_ = cmd.Wait()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Handle standard input and terminal resize events from the client
|
// Handle standard input and terminal resize events from the client
|
||||||
fromClientErrCh := make(chan error, 1)
|
fromClientErrCh := make(chan error, 1)
|
||||||
|
reportClientError := func(err error) {
|
||||||
|
fromClientErrCh <- err
|
||||||
|
_ = cmd.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
request, err := stream.Recv()
|
request, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Allow the client to close its sending side while continuing to receive responses
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
fromClientErrCh <- err
|
reportClientError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -156,7 +205,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
} else {
|
} else {
|
||||||
// Close the standard input
|
// Close the standard input
|
||||||
if err := stdin.Close(); err != nil {
|
if err := stdin.Close(); err != nil {
|
||||||
fromClientErrCh <- err
|
reportClientError(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +215,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := stdin.Write(dataToWrite); err != nil {
|
if _, err := stdin.Write(dataToWrite); err != nil {
|
||||||
fromClientErrCh <- err
|
reportClientError(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -181,7 +230,26 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
Rows: uint16(typedAction.TerminalResize.GetRows()),
|
Rows: uint16(typedAction.TerminalResize.GetRows()),
|
||||||
Cols: uint16(typedAction.TerminalResize.GetCols()),
|
Cols: uint16(typedAction.TerminalResize.GetCols()),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fromClientErrCh <- err
|
reportClientError(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case *ExecRequest_SendSignal_:
|
||||||
|
var signal syscall.Signal
|
||||||
|
|
||||||
|
switch typedAction.SendSignal.GetSignal() {
|
||||||
|
case ExecRequest_SendSignal_SIGNAL_SIGTERM:
|
||||||
|
signal = syscall.SIGTERM
|
||||||
|
case ExecRequest_SendSignal_SIGNAL_SIGKILL:
|
||||||
|
signal = syscall.SIGKILL
|
||||||
|
default:
|
||||||
|
reportClientError(fmt.Errorf("unsupported exec signal %q", typedAction.SendSignal.GetSignal().String()))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := signalProcessGroup(cmd.Process, signal); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||||
|
reportClientError(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +257,16 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Serialize responses from the stdout and stderr goroutines
|
||||||
|
var sendMutex sync.Mutex
|
||||||
|
|
||||||
|
sendResponse := func(response *ExecResponse) error {
|
||||||
|
sendMutex.Lock()
|
||||||
|
defer sendMutex.Unlock()
|
||||||
|
|
||||||
|
return stream.Send(response)
|
||||||
|
}
|
||||||
|
|
||||||
group, _ := errgroup.WithContext(stream.Context())
|
group, _ := errgroup.WithContext(stream.Context())
|
||||||
|
|
||||||
// Handle standard output from the command
|
// Handle standard output from the command
|
||||||
|
|
@ -210,7 +288,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stream.Send(&ExecResponse{
|
if err := sendResponse(&ExecResponse{
|
||||||
Type: &ExecResponse_StandardOutput{
|
Type: &ExecResponse_StandardOutput{
|
||||||
StandardOutput: &IOChunk{
|
StandardOutput: &IOChunk{
|
||||||
Data: slices.Clone(buf[:n]),
|
Data: slices.Clone(buf[:n]),
|
||||||
|
|
@ -240,7 +318,7 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stream.Send(&ExecResponse{
|
if err := sendResponse(&ExecResponse{
|
||||||
Type: &ExecResponse_StandardError{
|
Type: &ExecResponse_StandardError{
|
||||||
StandardError: &IOChunk{
|
StandardError: &IOChunk{
|
||||||
Data: slices.Clone(buf[:n]),
|
Data: slices.Clone(buf[:n]),
|
||||||
|
|
@ -258,15 +336,29 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the command to finish
|
// Wait for the command to finish
|
||||||
|
err = cmd.Wait()
|
||||||
|
|
||||||
|
// Prefer a client error over the command exit result
|
||||||
|
select {
|
||||||
|
case err := <-fromClientErrCh:
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
exitCode := 0
|
exitCode := 0
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
if err != nil {
|
||||||
var exitError *exec.ExitError
|
var exitError *exec.ExitError
|
||||||
if errors.As(err, &exitError) {
|
if !errors.As(err, &exitError) {
|
||||||
exitCode = exitError.ExitCode()
|
|
||||||
} else {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExitCode returns -1 for signals; report the containerd-compatible 128 + signal instead
|
||||||
|
exitCode = exitError.ExitCode()
|
||||||
|
|
||||||
|
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok && waitStatus.Signaled() {
|
||||||
|
exitCode = signalExitCodeOffset + int(waitStatus.Signal())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream.Send(&ExecResponse{
|
return stream.Send(&ExecResponse{
|
||||||
|
|
@ -278,6 +370,37 @@ func (rpc *RPC) Exec(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func signalProcessGroup(process *os.Process, signal syscall.Signal) error {
|
||||||
|
if err := syscall.Kill(-process.Pid, signal); err != nil {
|
||||||
|
// Translate a missing process group into the process-finished error expected by os/exec
|
||||||
|
if errors.Is(err, syscall.ESRCH) {
|
||||||
|
return os.ErrProcessDone
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendStartSuccess(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||||
|
return stream.Send(&ExecResponse{
|
||||||
|
Type: &ExecResponse_Started_{
|
||||||
|
Started: &ExecResponse_Started{},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendStartFailure(stream grpc.BidiStreamingServer[ExecRequest, ExecResponse]) error {
|
||||||
|
return stream.Send(&ExecResponse{
|
||||||
|
Type: &ExecResponse_Exit_{
|
||||||
|
Exit: &ExecResponse_Exit{
|
||||||
|
Code: execRuntimeFailureExitCode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func applyExecOverrides(cmd *exec.Cmd, command *ExecRequest_Command) {
|
func applyExecOverrides(cmd *exec.Cmd, command *ExecRequest_Command) {
|
||||||
if command.Workdir != "" {
|
if command.Workdir != "" {
|
||||||
cmd.Dir = command.Workdir
|
cmd.Dir = command.Workdir
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
// In-process stream scaffolding intentionally favors direct test construction.
|
||||||
|
//
|
||||||
|
//nolint:containedctx,testpackage,wsl_v5
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
execTestShell = "/bin/sh"
|
||||||
|
execTestTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type execTestStream struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
requests chan *ExecRequest
|
||||||
|
responses chan *ExecResponse
|
||||||
|
sendHook func(*ExecResponse) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ grpc.BidiStreamingServer[ExecRequest, ExecResponse] = (*execTestStream)(nil)
|
||||||
|
|
||||||
|
func newExecTestStream(ctx context.Context) *execTestStream {
|
||||||
|
return &execTestStream{
|
||||||
|
ctx: ctx,
|
||||||
|
requests: make(chan *ExecRequest, 8),
|
||||||
|
responses: make(chan *ExecResponse, 8),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *execTestStream) Send(response *ExecResponse) error {
|
||||||
|
if stream.sendHook != nil {
|
||||||
|
if err := stream.sendHook(response); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case stream.responses <- response:
|
||||||
|
return nil
|
||||||
|
case <-stream.ctx.Done():
|
||||||
|
return stream.ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *execTestStream) Recv() (*ExecRequest, error) {
|
||||||
|
select {
|
||||||
|
case request, ok := <-stream.requests:
|
||||||
|
if !ok {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
return request, nil
|
||||||
|
case <-stream.ctx.Done():
|
||||||
|
return nil, stream.ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stream *execTestStream) Context() context.Context { return stream.ctx }
|
||||||
|
|
||||||
|
func TestExecSendsStartedBeforeOutputAndExit(t *testing.T) {
|
||||||
|
stream, result := startExecTest(t, &ExecRequest_Command{
|
||||||
|
Name: execTestShell,
|
||||||
|
Args: []string{"-c", "printf hello"},
|
||||||
|
})
|
||||||
|
|
||||||
|
first := receiveExecResponse(t, stream)
|
||||||
|
require.NotNil(t, first.GetStarted())
|
||||||
|
|
||||||
|
var output []byte
|
||||||
|
for {
|
||||||
|
response := receiveExecResponse(t, stream)
|
||||||
|
switch response := response.GetType().(type) {
|
||||||
|
case *ExecResponse_StandardOutput:
|
||||||
|
output = append(output, response.StandardOutput.GetData()...)
|
||||||
|
case *ExecResponse_Exit_:
|
||||||
|
require.EqualValues(t, 0, response.Exit.GetCode())
|
||||||
|
require.Equal(t, []byte("hello"), output)
|
||||||
|
require.NoError(t, receiveExecResult(t, result))
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
t.Fatalf("unexpected exec response %T", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecReportsStartFailureBeforeStarted(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command *ExecRequest_Command
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing executable",
|
||||||
|
command: &ExecRequest_Command{
|
||||||
|
Name: "/definitely/missing/tart-guest-agent-test-command",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing workdir",
|
||||||
|
command: &ExecRequest_Command{
|
||||||
|
Name: execTestShell,
|
||||||
|
Workdir: "/definitely/missing/tart-guest-agent-test-workdir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
stream, result := startExecTest(t, test.command)
|
||||||
|
response := receiveExecResponse(t, stream)
|
||||||
|
require.Nil(t, response.GetStarted())
|
||||||
|
require.EqualValues(t, execRuntimeFailureExitCode, response.GetExit().GetCode())
|
||||||
|
require.NoError(t, receiveExecResult(t, result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecSignalsProcess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
signal ExecRequest_SendSignal_Signal
|
||||||
|
code int32
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SIGTERM",
|
||||||
|
signal: ExecRequest_SendSignal_SIGNAL_SIGTERM,
|
||||||
|
code: int32(signalExitCodeOffset + syscall.SIGTERM),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SIGKILL",
|
||||||
|
signal: ExecRequest_SendSignal_SIGNAL_SIGKILL,
|
||||||
|
code: int32(signalExitCodeOffset + syscall.SIGKILL),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsupported",
|
||||||
|
signal: ExecRequest_SendSignal_SIGNAL_UNSPECIFIED,
|
||||||
|
err: `unsupported exec signal "SIGNAL_UNSPECIFIED"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
stream, result := startExecTest(t, &ExecRequest_Command{
|
||||||
|
Name: "/bin/sleep",
|
||||||
|
Args: []string{"30"},
|
||||||
|
})
|
||||||
|
require.NotNil(t, receiveExecResponse(t, stream).GetStarted())
|
||||||
|
|
||||||
|
stream.requests <- &ExecRequest{
|
||||||
|
Type: &ExecRequest_SendSignal_{
|
||||||
|
SendSignal: &ExecRequest_SendSignal{Signal: test.signal},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.err != "" {
|
||||||
|
require.EqualError(t, receiveExecResult(t, result), test.err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := receiveExecResponse(t, stream)
|
||||||
|
require.NotNil(t, response.GetExit())
|
||||||
|
require.Equal(t, test.code, response.GetExit().GetCode())
|
||||||
|
require.NoError(t, receiveExecResult(t, result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecSignalsProcessGroup(t *testing.T) {
|
||||||
|
stream, result := startExecTest(t, &ExecRequest_Command{
|
||||||
|
Name: execTestShell,
|
||||||
|
Args: []string{"-c", "sleep 30 & printf ready; wait"},
|
||||||
|
})
|
||||||
|
require.NotNil(t, receiveExecResponse(t, stream).GetStarted())
|
||||||
|
require.Equal(t, []byte("ready"), receiveExecResponse(t, stream).GetStandardOutput().GetData())
|
||||||
|
|
||||||
|
stream.requests <- &ExecRequest{
|
||||||
|
Type: &ExecRequest_SendSignal_{
|
||||||
|
SendSignal: &ExecRequest_SendSignal{Signal: ExecRequest_SendSignal_SIGNAL_SIGTERM},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response := receiveExecResponse(t, stream)
|
||||||
|
require.EqualValues(t, signalExitCodeOffset+syscall.SIGTERM, response.GetExit().GetCode())
|
||||||
|
require.NoError(t, receiveExecResult(t, result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecReapsProcessWhenStartedCannotBeSent(t *testing.T) {
|
||||||
|
pidPath := filepath.Join(t.TempDir(), "pid")
|
||||||
|
sendErr := errors.New("failed to send Started")
|
||||||
|
var processPID int
|
||||||
|
|
||||||
|
_, result := startExecTest(t, &ExecRequest_Command{
|
||||||
|
Name: execTestShell,
|
||||||
|
Args: []string{"-c", `printf %d "$$" > "$PID_FILE"; exec sleep 30`},
|
||||||
|
Env: map[string]string{"PID_FILE": pidPath},
|
||||||
|
}, func(stream *execTestStream) {
|
||||||
|
stream.sendHook = func(response *ExecResponse) error {
|
||||||
|
if response.GetStarted() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
processPID, err = waitForExecTestPID(pidPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendErr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
require.ErrorIs(t, receiveExecResult(t, result), sendErr)
|
||||||
|
require.ErrorIs(t, syscall.Kill(processPID, 0), syscall.ESRCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startExecTest(
|
||||||
|
t *testing.T,
|
||||||
|
command *ExecRequest_Command,
|
||||||
|
configure ...func(*execTestStream),
|
||||||
|
) (*execTestStream, <-chan error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
stream := newExecTestStream(ctx)
|
||||||
|
for _, configureStream := range configure {
|
||||||
|
configureStream(stream)
|
||||||
|
}
|
||||||
|
result := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
result <- (&RPC{}).Exec(stream)
|
||||||
|
}()
|
||||||
|
stream.requests <- &ExecRequest{
|
||||||
|
Type: &ExecRequest_Command_{Command: command},
|
||||||
|
}
|
||||||
|
return stream, result
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveExecResponse(t *testing.T, stream *execTestStream) *ExecResponse {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case response := <-stream.responses:
|
||||||
|
return response
|
||||||
|
case <-time.After(execTestTimeout):
|
||||||
|
t.Fatal("timed out waiting for exec response")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveExecResult(t *testing.T, result <-chan error) error {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-result:
|
||||||
|
return err
|
||||||
|
case <-time.After(execTestTimeout):
|
||||||
|
t.Fatal("timed out waiting for Exec to return")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExecTestPID(path string) (int, error) {
|
||||||
|
deadline := time.Now().Add(execTestTimeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
//nolint:gosec // path is created under t.TempDir by the test
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
return strconv.Atoi(string(data))
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,16 @@ service Agent {
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExecRequest {
|
message ExecRequest {
|
||||||
|
message SendSignal {
|
||||||
|
enum Signal {
|
||||||
|
SIGNAL_UNSPECIFIED = 0;
|
||||||
|
SIGNAL_SIGTERM = 1;
|
||||||
|
SIGNAL_SIGKILL = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Signal signal = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message Command {
|
message Command {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
repeated string args = 2;
|
repeated string args = 2;
|
||||||
|
|
@ -25,6 +35,7 @@ message ExecRequest {
|
||||||
Command command = 1;
|
Command command = 1;
|
||||||
IOChunk standard_input = 2;
|
IOChunk standard_input = 2;
|
||||||
TerminalSize terminal_resize = 3;
|
TerminalSize terminal_resize = 3;
|
||||||
|
SendSignal send_signal = 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +44,15 @@ message ExecResponse {
|
||||||
int32 code = 1;
|
int32 code = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Started {
|
||||||
|
// nothing for now
|
||||||
|
}
|
||||||
|
|
||||||
oneof type {
|
oneof type {
|
||||||
Exit exit = 1;
|
Exit exit = 1;
|
||||||
IOChunk standard_output = 2;
|
IOChunk standard_output = 2;
|
||||||
IOChunk standard_error = 3;
|
IOChunk standard_error = 3;
|
||||||
|
Started started = 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue