From 75b97c7a6d08e6ca13074ae1313a098c64c57580 Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Tue, 13 Oct 2020 09:00:00 +0000 Subject: [PATCH] [supervisor] tasks support --- components/ide/code/leeway.Dockerfile | 2 +- components/supervisor-api/go/status.pb.go | 385 ++++++++++++++-- components/supervisor-api/go/status.pb.gw.go | 125 ++++++ components/supervisor-api/go/terminal.pb.go | 228 +++++++--- .../supervisor-api/go/terminal.pb.gw.go | 101 +++++ components/supervisor-api/status.proto | 35 ++ components/supervisor-api/terminal.proto | 17 +- .../supervisor/pkg/supervisor/config.go | 30 ++ .../supervisor/pkg/supervisor/services.go | 40 ++ .../supervisor/pkg/supervisor/supervisor.go | 3 + components/supervisor/pkg/supervisor/tasks.go | 414 ++++++++++++++++++ .../supervisor/pkg/terminal/ring-buffer.go | 96 ++++ components/supervisor/pkg/terminal/service.go | 14 +- .../supervisor/pkg/terminal/terminal.go | 62 ++- .../supervisor/pkg/terminal/terminal_test.go | 71 +++ .../packages/gitpod-extension/package.json | 18 +- .../src/browser/gitpod-frontend-module.ts | 9 +- .../browser/gitpod-shell-layout-restorer.ts | 135 +----- .../src/browser/gitpod-task-contribution.ts | 143 ++++++ .../src/browser/gitpod-terminal-widget.ts | 23 +- .../src/common/content-ready-service.ts | 49 --- .../src/common/gitpod-info.ts | 15 - .../src/common/gitpod-task-protocol.ts | 45 ++ .../src/node/content-ready-service-server.ts | 47 -- .../src/node/gitpod-backend-module.ts | 56 +-- .../src/node/gitpod-info-backend.ts | 10 +- .../src/node/gitpod-task-server-impl.ts | 130 ++++++ .../src/node/gitpod-task-starter.ts | 393 ----------------- components/ws-manager/pkg/manager/headless.go | 42 +- yarn.lock | 25 +- 30 files changed, 1927 insertions(+), 836 deletions(-) create mode 100644 components/supervisor/pkg/supervisor/tasks.go create mode 100644 components/supervisor/pkg/terminal/ring-buffer.go create mode 100644 components/supervisor/pkg/terminal/terminal_test.go create mode 100644 components/theia/packages/gitpod-extension/src/browser/gitpod-task-contribution.ts delete mode 100644 components/theia/packages/gitpod-extension/src/common/content-ready-service.ts create mode 100644 components/theia/packages/gitpod-extension/src/common/gitpod-task-protocol.ts delete mode 100644 components/theia/packages/gitpod-extension/src/node/content-ready-service-server.ts create mode 100644 components/theia/packages/gitpod-extension/src/node/gitpod-task-server-impl.ts delete mode 100644 components/theia/packages/gitpod-extension/src/node/gitpod-task-starter.ts diff --git a/components/ide/code/leeway.Dockerfile b/components/ide/code/leeway.Dockerfile index fae0190f5dfedb..ce3a3f4bc118e5 100644 --- a/components/ide/code/leeway.Dockerfile +++ b/components/ide/code/leeway.Dockerfile @@ -28,7 +28,7 @@ RUN sudo apt-get update \ && sudo apt-get clean -y \ && rm -rf /var/lib/apt/lists/* -ENV GP_CODE_COMMIT 074132acc5f9cf4aa27bbde1e57500e083414769 +ENV GP_CODE_COMMIT 2b9a3dee974c2ee8cf881f1f4d796ea4cdd2e158 RUN git clone https://github.com/gitpod-io/vscode.git --branch gp-code --single-branch gp-code WORKDIR /gp-code RUN git reset --hard $GP_CODE_COMMIT diff --git a/components/supervisor-api/go/status.pb.go b/components/supervisor-api/go/status.pb.go index 551714aa159152..5b1591adcc36a0 100644 --- a/components/supervisor-api/go/status.pb.go +++ b/components/supervisor-api/go/status.pb.go @@ -57,6 +57,34 @@ func (ContentSource) EnumDescriptor() ([]byte, []int) { return fileDescriptor_dfe4fce6682daf5b, []int{0} } +type TaskState int32 + +const ( + TaskState_opening TaskState = 0 + TaskState_running TaskState = 1 + TaskState_closed TaskState = 2 +) + +var TaskState_name = map[int32]string{ + 0: "opening", + 1: "running", + 2: "closed", +} + +var TaskState_value = map[string]int32{ + "opening": 0, + "running": 1, + "closed": 2, +} + +func (x TaskState) String() string { + return proto.EnumName(TaskState_name, int32(x)) +} + +func (TaskState) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_dfe4fce6682daf5b, []int{1} +} + type SupervisorStatusRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -499,8 +527,207 @@ func (m *PortsStatus) GetGlobalPort() uint32 { return 0 } +type TasksStatusRequest struct { + // if observe is true, we'll return a stream of changes rather than just the + // current state of affairs. + Observe bool `protobuf:"varint,1,opt,name=observe,proto3" json:"observe,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TasksStatusRequest) Reset() { *m = TasksStatusRequest{} } +func (m *TasksStatusRequest) String() string { return proto.CompactTextString(m) } +func (*TasksStatusRequest) ProtoMessage() {} +func (*TasksStatusRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_dfe4fce6682daf5b, []int{11} +} + +func (m *TasksStatusRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TasksStatusRequest.Unmarshal(m, b) +} +func (m *TasksStatusRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TasksStatusRequest.Marshal(b, m, deterministic) +} +func (m *TasksStatusRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_TasksStatusRequest.Merge(m, src) +} +func (m *TasksStatusRequest) XXX_Size() int { + return xxx_messageInfo_TasksStatusRequest.Size(m) +} +func (m *TasksStatusRequest) XXX_DiscardUnknown() { + xxx_messageInfo_TasksStatusRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_TasksStatusRequest proto.InternalMessageInfo + +func (m *TasksStatusRequest) GetObserve() bool { + if m != nil { + return m.Observe + } + return false +} + +type TasksStatusResponse struct { + Tasks []*TaskStatus `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TasksStatusResponse) Reset() { *m = TasksStatusResponse{} } +func (m *TasksStatusResponse) String() string { return proto.CompactTextString(m) } +func (*TasksStatusResponse) ProtoMessage() {} +func (*TasksStatusResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_dfe4fce6682daf5b, []int{12} +} + +func (m *TasksStatusResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TasksStatusResponse.Unmarshal(m, b) +} +func (m *TasksStatusResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TasksStatusResponse.Marshal(b, m, deterministic) +} +func (m *TasksStatusResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_TasksStatusResponse.Merge(m, src) +} +func (m *TasksStatusResponse) XXX_Size() int { + return xxx_messageInfo_TasksStatusResponse.Size(m) +} +func (m *TasksStatusResponse) XXX_DiscardUnknown() { + xxx_messageInfo_TasksStatusResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_TasksStatusResponse proto.InternalMessageInfo + +func (m *TasksStatusResponse) GetTasks() []*TaskStatus { + if m != nil { + return m.Tasks + } + return nil +} + +type TaskStatus struct { + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + State TaskState `protobuf:"varint,2,opt,name=state,proto3,enum=supervisor.TaskState" json:"state,omitempty"` + Terminal string `protobuf:"bytes,3,opt,name=terminal,proto3" json:"terminal,omitempty"` + Presentation *TaskPresentation `protobuf:"bytes,4,opt,name=presentation,proto3" json:"presentation,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TaskStatus) Reset() { *m = TaskStatus{} } +func (m *TaskStatus) String() string { return proto.CompactTextString(m) } +func (*TaskStatus) ProtoMessage() {} +func (*TaskStatus) Descriptor() ([]byte, []int) { + return fileDescriptor_dfe4fce6682daf5b, []int{13} +} + +func (m *TaskStatus) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TaskStatus.Unmarshal(m, b) +} +func (m *TaskStatus) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TaskStatus.Marshal(b, m, deterministic) +} +func (m *TaskStatus) XXX_Merge(src proto.Message) { + xxx_messageInfo_TaskStatus.Merge(m, src) +} +func (m *TaskStatus) XXX_Size() int { + return xxx_messageInfo_TaskStatus.Size(m) +} +func (m *TaskStatus) XXX_DiscardUnknown() { + xxx_messageInfo_TaskStatus.DiscardUnknown(m) +} + +var xxx_messageInfo_TaskStatus proto.InternalMessageInfo + +func (m *TaskStatus) GetId() string { + if m != nil { + return m.Id + } + return "" +} + +func (m *TaskStatus) GetState() TaskState { + if m != nil { + return m.State + } + return TaskState_opening +} + +func (m *TaskStatus) GetTerminal() string { + if m != nil { + return m.Terminal + } + return "" +} + +func (m *TaskStatus) GetPresentation() *TaskPresentation { + if m != nil { + return m.Presentation + } + return nil +} + +type TaskPresentation struct { + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + OpenIn string `protobuf:"bytes,2,opt,name=open_in,json=openIn,proto3" json:"open_in,omitempty"` + OpenMode string `protobuf:"bytes,3,opt,name=open_mode,json=openMode,proto3" json:"open_mode,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *TaskPresentation) Reset() { *m = TaskPresentation{} } +func (m *TaskPresentation) String() string { return proto.CompactTextString(m) } +func (*TaskPresentation) ProtoMessage() {} +func (*TaskPresentation) Descriptor() ([]byte, []int) { + return fileDescriptor_dfe4fce6682daf5b, []int{14} +} + +func (m *TaskPresentation) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_TaskPresentation.Unmarshal(m, b) +} +func (m *TaskPresentation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_TaskPresentation.Marshal(b, m, deterministic) +} +func (m *TaskPresentation) XXX_Merge(src proto.Message) { + xxx_messageInfo_TaskPresentation.Merge(m, src) +} +func (m *TaskPresentation) XXX_Size() int { + return xxx_messageInfo_TaskPresentation.Size(m) +} +func (m *TaskPresentation) XXX_DiscardUnknown() { + xxx_messageInfo_TaskPresentation.DiscardUnknown(m) +} + +var xxx_messageInfo_TaskPresentation proto.InternalMessageInfo + +func (m *TaskPresentation) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *TaskPresentation) GetOpenIn() string { + if m != nil { + return m.OpenIn + } + return "" +} + +func (m *TaskPresentation) GetOpenMode() string { + if m != nil { + return m.OpenMode + } + return "" +} + func init() { proto.RegisterEnum("supervisor.ContentSource", ContentSource_name, ContentSource_value) + proto.RegisterEnum("supervisor.TaskState", TaskState_name, TaskState_value) proto.RegisterType((*SupervisorStatusRequest)(nil), "supervisor.SupervisorStatusRequest") proto.RegisterType((*SupervisorStatusResponse)(nil), "supervisor.SupervisorStatusResponse") proto.RegisterType((*IDEStatusRequest)(nil), "supervisor.IDEStatusRequest") @@ -512,6 +739,10 @@ func init() { proto.RegisterType((*PortsStatusRequest)(nil), "supervisor.PortsStatusRequest") proto.RegisterType((*PortsStatusResponse)(nil), "supervisor.PortsStatusResponse") proto.RegisterType((*PortsStatus)(nil), "supervisor.PortsStatus") + proto.RegisterType((*TasksStatusRequest)(nil), "supervisor.TasksStatusRequest") + proto.RegisterType((*TasksStatusResponse)(nil), "supervisor.TasksStatusResponse") + proto.RegisterType((*TaskStatus)(nil), "supervisor.TaskStatus") + proto.RegisterType((*TaskPresentation)(nil), "supervisor.TaskPresentation") } func init() { @@ -519,44 +750,57 @@ func init() { } var fileDescriptor_dfe4fce6682daf5b = []byte{ - // 590 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x54, 0xcd, 0x6e, 0xd3, 0x40, - 0x10, 0xc6, 0x2e, 0x2d, 0x74, 0xd2, 0xa4, 0xee, 0xb4, 0x55, 0xd3, 0x28, 0x21, 0xa9, 0xc3, 0x4f, - 0x1a, 0x89, 0x98, 0xa6, 0x27, 0x84, 0x7a, 0x48, 0x53, 0x0e, 0x1c, 0x90, 0x50, 0x72, 0xcb, 0x25, - 0x5a, 0xbb, 0x4b, 0x6a, 0xc5, 0x78, 0x8d, 0xbd, 0x0e, 0x42, 0x85, 0x0b, 0x3c, 0x02, 0x42, 0x3c, - 0x08, 0x8f, 0xc2, 0x2b, 0xf0, 0x20, 0xc8, 0xeb, 0x4d, 0xb3, 0x4e, 0xea, 0x72, 0x89, 0xbc, 0xdf, - 0x7c, 0x3b, 0xdf, 0xb7, 0xa3, 0x6f, 0x02, 0x5b, 0x11, 0x27, 0x3c, 0x8e, 0x3a, 0x41, 0xc8, 0x38, - 0x43, 0x88, 0xe2, 0x80, 0x86, 0x33, 0x37, 0x62, 0x61, 0xa5, 0x3a, 0x61, 0x6c, 0xe2, 0x51, 0x8b, - 0x04, 0xae, 0x45, 0x7c, 0x9f, 0x71, 0xc2, 0x5d, 0xe6, 0x4b, 0xa6, 0x79, 0x08, 0x07, 0xc3, 0x1b, - 0xee, 0x50, 0xf4, 0x18, 0xd0, 0x8f, 0x31, 0x8d, 0xb8, 0xd9, 0x86, 0xf2, 0x6a, 0x29, 0x0a, 0x98, - 0x1f, 0x51, 0x2c, 0x81, 0xce, 0xa6, 0x65, 0xad, 0xa1, 0xb5, 0x1e, 0x0e, 0x74, 0x36, 0x35, 0x9f, - 0x82, 0xf1, 0xe6, 0xe2, 0x75, 0xe6, 0x3e, 0x22, 0xdc, 0xff, 0x44, 0x5c, 0x2e, 0x59, 0xe2, 0xdb, - 0x6c, 0xc2, 0x8e, 0xc2, 0xcb, 0x69, 0xd6, 0x86, 0xbd, 0x3e, 0xf3, 0x39, 0xf5, 0xf9, 0xff, 0x1b, - 0x5e, 0xc1, 0xfe, 0x12, 0x57, 0x36, 0xad, 0xc2, 0x26, 0x99, 0x11, 0xd7, 0x23, 0xb6, 0x47, 0xe5, - 0x8d, 0x05, 0x80, 0x27, 0xb0, 0x11, 0xb1, 0x38, 0x74, 0x68, 0x59, 0x6f, 0x68, 0xad, 0x52, 0xf7, - 0xb0, 0xb3, 0x98, 0x58, 0x67, 0xde, 0x50, 0x10, 0x06, 0x92, 0x68, 0xee, 0xc3, 0xee, 0x39, 0x71, - 0xa6, 0x71, 0x90, 0x9d, 0x52, 0x0f, 0xf6, 0xb2, 0xb0, 0xd4, 0x3f, 0x06, 0xc3, 0x21, 0x3e, 0x09, - 0x3f, 0x8f, 0x97, 0x6d, 0x6c, 0xa7, 0x78, 0x6f, 0x0e, 0x9b, 0x1d, 0xc0, 0x77, 0x2c, 0xe4, 0x51, - 0xf6, 0xb5, 0x65, 0x78, 0xc0, 0xec, 0x88, 0x86, 0xb3, 0xf9, 0xbd, 0xf9, 0xd1, 0xbc, 0x80, 0xdd, - 0x0c, 0x5f, 0x2a, 0x3e, 0x87, 0xf5, 0x20, 0x81, 0xcb, 0x5a, 0x63, 0xad, 0x55, 0xe8, 0x1e, 0xa8, - 0x4f, 0x52, 0xf9, 0x29, 0xcb, 0x7c, 0x0b, 0x05, 0x05, 0xc5, 0x1a, 0x80, 0xc7, 0x1c, 0xe2, 0x8d, - 0x93, 0xaa, 0x50, 0x2c, 0x0e, 0x36, 0x05, 0x92, 0xb0, 0xb0, 0x0e, 0x85, 0x89, 0xc7, 0xec, 0x79, - 0x5d, 0x17, 0x75, 0x48, 0xa1, 0x84, 0xd0, 0xee, 0x43, 0x31, 0x33, 0x37, 0x2c, 0x01, 0xbc, 0x0f, - 0xd9, 0x87, 0x31, 0xe3, 0x57, 0x34, 0x34, 0xee, 0xe1, 0x36, 0x14, 0xc4, 0xd9, 0x16, 0xd3, 0x32, - 0x34, 0xdc, 0x81, 0xa2, 0x00, 0x82, 0x90, 0xda, 0xb1, 0xeb, 0x5d, 0x1a, 0x7a, 0xf7, 0xf7, 0x3a, - 0x14, 0x53, 0x3f, 0xc3, 0xc4, 0xb9, 0x43, 0xf1, 0x0b, 0x18, 0xcb, 0x21, 0xc4, 0xa6, 0xfa, 0xb2, - 0x9c, 0xf4, 0x56, 0x1e, 0xdf, 0x4d, 0x4a, 0x67, 0x66, 0xd6, 0xbe, 0xfd, 0xf9, 0xfb, 0x43, 0x3f, - 0xc0, 0x7d, 0x6b, 0x76, 0x62, 0xa5, 0x2b, 0x64, 0x2d, 0xee, 0xe1, 0x77, 0x0d, 0x36, 0x6f, 0xf2, - 0x8a, 0x55, 0xb5, 0xe5, 0x72, 0xdc, 0x2b, 0xb5, 0x9c, 0xaa, 0x54, 0x7a, 0x29, 0x94, 0x4e, 0xb1, - 0xa4, 0x28, 0xb9, 0x97, 0x74, 0x74, 0x84, 0xf5, 0x2c, 0x62, 0x25, 0xb9, 0xb6, 0xae, 0x93, 0xdf, - 0x33, 0x1e, 0xc6, 0xf4, 0x2b, 0xfe, 0xd2, 0x16, 0xb3, 0x4d, 0x9d, 0x34, 0x6e, 0x8b, 0x6b, 0xc6, - 0xcd, 0xd1, 0x1d, 0x0c, 0xe9, 0xa8, 0x27, 0x1c, 0xbd, 0x42, 0x54, 0xf4, 0x9d, 0x94, 0x39, 0x7a, - 0x82, 0xcd, 0x55, 0x74, 0xd5, 0x99, 0x07, 0x5b, 0x6a, 0xf8, 0xb1, 0xae, 0xaa, 0xde, 0xb2, 0x2d, - 0x95, 0x46, 0x3e, 0x41, 0xba, 0x3a, 0x14, 0xae, 0x76, 0x71, 0x47, 0xd1, 0x4f, 0x23, 0x83, 0x3f, - 0xb5, 0x6c, 0x64, 0x1f, 0xe5, 0x25, 0x5c, 0x8a, 0xd5, 0x73, 0xeb, 0x52, 0xab, 0x2f, 0xb4, 0xce, - 0xd0, 0x50, 0xb4, 0xc4, 0x72, 0x8c, 0x8e, 0xf1, 0xd9, 0x32, 0x66, 0xc9, 0xf5, 0xb3, 0xae, 0xe5, - 0x47, 0x3a, 0x83, 0x17, 0xda, 0xf9, 0xfa, 0x68, 0x8d, 0x04, 0xae, 0xbd, 0x21, 0xfe, 0x51, 0x4f, - 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x1b, 0x56, 0xfe, 0xff, 0x8b, 0x05, 0x00, 0x00, + // 789 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x55, 0xcd, 0x6e, 0xf2, 0x46, + 0x14, 0x8d, 0x9d, 0x40, 0xc2, 0x25, 0x10, 0xe7, 0x12, 0x0a, 0xa1, 0x49, 0x21, 0x4e, 0x7f, 0x08, + 0x6d, 0x71, 0x43, 0x56, 0x55, 0x15, 0xa9, 0x09, 0xe9, 0x22, 0x8b, 0x48, 0x91, 0xd3, 0x15, 0xaa, + 0x84, 0x06, 0x98, 0x12, 0x0b, 0xe3, 0x71, 0xfd, 0x43, 0x55, 0xa5, 0xdd, 0xb4, 0x8f, 0x50, 0x55, + 0x7d, 0x84, 0x3e, 0x4c, 0x97, 0x7d, 0x85, 0x3e, 0xc8, 0xa7, 0x19, 0x8f, 0xc1, 0xe6, 0x27, 0xdf, + 0xf7, 0x6d, 0x90, 0xe7, 0xcc, 0x99, 0x7b, 0xce, 0xbd, 0x1e, 0x1f, 0x60, 0xdf, 0x0f, 0x48, 0x10, + 0xfa, 0x6d, 0xd7, 0x63, 0x01, 0x43, 0xf0, 0x43, 0x97, 0x7a, 0x33, 0xcb, 0x67, 0x5e, 0xed, 0x64, + 0xcc, 0xd8, 0xd8, 0xa6, 0x06, 0x71, 0x2d, 0x83, 0x38, 0x0e, 0x0b, 0x48, 0x60, 0x31, 0x47, 0x32, + 0xf5, 0x63, 0xa8, 0x3c, 0xcd, 0xb9, 0x4f, 0xa2, 0x86, 0x49, 0x7f, 0x0a, 0xa9, 0x1f, 0xe8, 0x2d, + 0xa8, 0xae, 0x6e, 0xf9, 0x2e, 0x73, 0x7c, 0x8a, 0x45, 0x50, 0xd9, 0xa4, 0xaa, 0x34, 0x94, 0xe6, + 0x9e, 0xa9, 0xb2, 0x89, 0xfe, 0x29, 0x68, 0xf7, 0x77, 0xdf, 0xa5, 0xce, 0x23, 0xc2, 0xce, 0xcf, + 0xc4, 0x0a, 0x24, 0x4b, 0x3c, 0xeb, 0xe7, 0x70, 0x98, 0xe0, 0x6d, 0x28, 0xd6, 0x82, 0xa3, 0x2e, + 0x73, 0x02, 0xea, 0x04, 0x6f, 0x2f, 0xf8, 0x0c, 0xe5, 0x25, 0xae, 0x2c, 0x7a, 0x02, 0x39, 0x32, + 0x23, 0x96, 0x4d, 0x06, 0x36, 0x95, 0x27, 0x16, 0x00, 0x5e, 0x42, 0xd6, 0x67, 0xa1, 0x37, 0xa4, + 0x55, 0xb5, 0xa1, 0x34, 0x8b, 0x9d, 0xe3, 0xf6, 0x62, 0x62, 0xed, 0xb8, 0xa0, 0x20, 0x98, 0x92, + 0xa8, 0x97, 0xa1, 0x74, 0x4b, 0x86, 0x93, 0xd0, 0x4d, 0x4f, 0xe9, 0x06, 0x8e, 0xd2, 0xb0, 0xd4, + 0xbf, 0x00, 0x6d, 0x48, 0x1c, 0xe2, 0xfd, 0xd2, 0x5f, 0xb6, 0x71, 0x10, 0xe1, 0x37, 0x31, 0xac, + 0xb7, 0x01, 0x1f, 0x99, 0x17, 0xf8, 0xe9, 0x6e, 0xab, 0xb0, 0xcb, 0x06, 0x3e, 0xf5, 0x66, 0xf1, + 0xb9, 0x78, 0xa9, 0xdf, 0x41, 0x29, 0xc5, 0x97, 0x8a, 0x5f, 0x42, 0xc6, 0xe5, 0x70, 0x55, 0x69, + 0x6c, 0x37, 0xf3, 0x9d, 0x4a, 0xb2, 0xa5, 0x24, 0x3f, 0x62, 0xe9, 0x0f, 0x90, 0x4f, 0xa0, 0x78, + 0x0a, 0x60, 0xb3, 0x21, 0xb1, 0xfb, 0x7c, 0x57, 0x28, 0x16, 0xcc, 0x9c, 0x40, 0x38, 0x0b, 0xeb, + 0x90, 0x1f, 0xdb, 0x6c, 0x10, 0xef, 0xab, 0x62, 0x1f, 0x22, 0x88, 0x13, 0x78, 0x13, 0xdf, 0x13, + 0x7f, 0xf2, 0xce, 0x4d, 0x74, 0xa1, 0x94, 0xe2, 0xcb, 0x26, 0xbe, 0x80, 0x4c, 0xc0, 0x61, 0xd9, + 0xc4, 0x07, 0xc9, 0x26, 0x38, 0x3f, 0xee, 0x41, 0x90, 0xf4, 0x7f, 0x14, 0x80, 0x05, 0xca, 0x2f, + 0x92, 0x35, 0x12, 0x42, 0x39, 0x53, 0xb5, 0x46, 0xf8, 0x39, 0x64, 0xf8, 0x67, 0x11, 0xbf, 0xe4, + 0xf2, 0xba, 0x62, 0xd4, 0x8c, 0x38, 0x58, 0x83, 0xbd, 0x80, 0x7a, 0x53, 0xcb, 0x21, 0x76, 0x75, + 0x5b, 0x94, 0x98, 0xaf, 0xf1, 0x5b, 0xd8, 0x77, 0x3d, 0xea, 0x53, 0x27, 0xfa, 0x78, 0xaa, 0x3b, + 0x0d, 0xa5, 0x99, 0xef, 0x9c, 0x2c, 0xd7, 0x7b, 0x4c, 0x70, 0xcc, 0xd4, 0x09, 0xfd, 0x07, 0xd0, + 0x96, 0x19, 0xfc, 0x3e, 0x3b, 0x64, 0x4a, 0xa5, 0x61, 0xf1, 0x8c, 0x15, 0xd8, 0x65, 0x2e, 0x75, + 0xfa, 0x96, 0x23, 0x4c, 0xe7, 0xcc, 0x2c, 0x5f, 0xde, 0x3b, 0xf8, 0x21, 0xe4, 0xc4, 0xc6, 0x94, + 0x8d, 0x68, 0xec, 0x8f, 0x03, 0x0f, 0x6c, 0x44, 0x5b, 0x5d, 0x28, 0xa4, 0x2e, 0x2d, 0x16, 0x01, + 0x7e, 0xf4, 0xd8, 0xb4, 0xcf, 0x82, 0x67, 0xea, 0x69, 0x5b, 0x78, 0x00, 0x79, 0xb1, 0x1e, 0x88, + 0xab, 0xaa, 0x29, 0x78, 0x08, 0x05, 0x01, 0xb8, 0x1e, 0x1d, 0x84, 0x96, 0x3d, 0xd2, 0xd4, 0xd6, + 0x25, 0xe4, 0xe6, 0x43, 0xc1, 0x7c, 0xe4, 0xc3, 0x72, 0xc6, 0xda, 0x16, 0x5f, 0x78, 0xa1, 0x23, + 0x16, 0x0a, 0x02, 0x64, 0x87, 0x36, 0xf3, 0xe9, 0x48, 0x53, 0x3b, 0xff, 0x66, 0xa1, 0x10, 0xcd, + 0xfe, 0x89, 0xcf, 0x61, 0x48, 0xf1, 0x57, 0xd0, 0x96, 0x43, 0x03, 0xcf, 0x93, 0x73, 0xda, 0x90, + 0x36, 0xb5, 0x8f, 0x5f, 0x27, 0x45, 0xd7, 0x43, 0x3f, 0xfd, 0xfd, 0xbf, 0xff, 0xff, 0x54, 0x2b, + 0x58, 0x36, 0x66, 0x97, 0x46, 0x14, 0x79, 0xc6, 0xe2, 0x1c, 0xfe, 0xa1, 0x40, 0x6e, 0x9e, 0x2f, + 0x98, 0x7a, 0x3f, 0xcb, 0xf1, 0x54, 0x3b, 0xdd, 0xb0, 0x2b, 0x95, 0xbe, 0x16, 0x4a, 0x57, 0x58, + 0x4c, 0x28, 0x59, 0x23, 0xda, 0x3b, 0xc3, 0x7a, 0x1a, 0x31, 0x78, 0x0e, 0x19, 0x2f, 0xfc, 0xf7, + 0x3a, 0xf0, 0x42, 0xfa, 0x1b, 0xfe, 0xad, 0x2c, 0x5e, 0x47, 0xe4, 0xa4, 0xb1, 0x2e, 0x5e, 0x52, + 0x6e, 0xce, 0x5e, 0x61, 0x48, 0x47, 0x37, 0xc2, 0xd1, 0x37, 0x88, 0x09, 0xfd, 0x61, 0xc4, 0xec, + 0x7d, 0x82, 0xe7, 0xab, 0xe8, 0xaa, 0x33, 0x1b, 0xf6, 0x93, 0x61, 0x85, 0xf5, 0xa4, 0xea, 0x9a, + 0x74, 0xab, 0x35, 0x36, 0x13, 0xa4, 0xab, 0x63, 0xe1, 0xaa, 0x84, 0x87, 0x09, 0xfd, 0xe8, 0x96, + 0xe1, 0x5f, 0x4a, 0x3a, 0x62, 0x3e, 0xda, 0x94, 0x48, 0x52, 0xac, 0xbe, 0x71, 0x5f, 0x6a, 0x75, + 0x85, 0xd6, 0x35, 0x6a, 0x09, 0x2d, 0x11, 0x66, 0xbd, 0x0b, 0xfc, 0x6c, 0x19, 0x33, 0x64, 0xd2, + 0x18, 0x2f, 0xf2, 0x21, 0x9a, 0xc1, 0x57, 0x8a, 0xf0, 0x95, 0xc8, 0x9e, 0xb4, 0xaf, 0xd5, 0x10, + 0x4b, 0xfb, 0x5a, 0x13, 0x5a, 0x6b, 0x7d, 0x89, 0x80, 0x7a, 0x2f, 0x5f, 0xb7, 0x99, 0xde, 0x36, + 0x71, 0xad, 0x41, 0x56, 0xfc, 0x33, 0x5f, 0xbd, 0x09, 0x00, 0x00, 0xff, 0xff, 0xff, 0x0f, 0x93, + 0x74, 0xd3, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -584,6 +828,8 @@ type StatusServiceClient interface { BackupStatus(ctx context.Context, in *BackupStatusRequest, opts ...grpc.CallOption) (*BackupStatusResponse, error) // PortsStatus provides feedback about the network ports currently in use. PortsStatus(ctx context.Context, in *PortsStatusRequest, opts ...grpc.CallOption) (StatusService_PortsStatusClient, error) + // TasksStatus provides tasks status information. + TasksStatus(ctx context.Context, in *TasksStatusRequest, opts ...grpc.CallOption) (StatusService_TasksStatusClient, error) } type statusServiceClient struct { @@ -662,6 +908,38 @@ func (x *statusServicePortsStatusClient) Recv() (*PortsStatusResponse, error) { return m, nil } +func (c *statusServiceClient) TasksStatus(ctx context.Context, in *TasksStatusRequest, opts ...grpc.CallOption) (StatusService_TasksStatusClient, error) { + stream, err := c.cc.NewStream(ctx, &_StatusService_serviceDesc.Streams[1], "/supervisor.StatusService/TasksStatus", opts...) + if err != nil { + return nil, err + } + x := &statusServiceTasksStatusClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type StatusService_TasksStatusClient interface { + Recv() (*TasksStatusResponse, error) + grpc.ClientStream +} + +type statusServiceTasksStatusClient struct { + grpc.ClientStream +} + +func (x *statusServiceTasksStatusClient) Recv() (*TasksStatusResponse, error) { + m := new(TasksStatusResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // StatusServiceServer is the server API for StatusService service. type StatusServiceServer interface { // SupervisorStatus returns once supervisor is running. @@ -677,6 +955,8 @@ type StatusServiceServer interface { BackupStatus(context.Context, *BackupStatusRequest) (*BackupStatusResponse, error) // PortsStatus provides feedback about the network ports currently in use. PortsStatus(*PortsStatusRequest, StatusService_PortsStatusServer) error + // TasksStatus provides tasks status information. + TasksStatus(*TasksStatusRequest, StatusService_TasksStatusServer) error } // UnimplementedStatusServiceServer can be embedded to have forward compatible implementations. @@ -698,6 +978,9 @@ func (*UnimplementedStatusServiceServer) BackupStatus(ctx context.Context, req * func (*UnimplementedStatusServiceServer) PortsStatus(req *PortsStatusRequest, srv StatusService_PortsStatusServer) error { return status.Errorf(codes.Unimplemented, "method PortsStatus not implemented") } +func (*UnimplementedStatusServiceServer) TasksStatus(req *TasksStatusRequest, srv StatusService_TasksStatusServer) error { + return status.Errorf(codes.Unimplemented, "method TasksStatus not implemented") +} func RegisterStatusServiceServer(s *grpc.Server, srv StatusServiceServer) { s.RegisterService(&_StatusService_serviceDesc, srv) @@ -796,6 +1079,27 @@ func (x *statusServicePortsStatusServer) Send(m *PortsStatusResponse) error { return x.ServerStream.SendMsg(m) } +func _StatusService_TasksStatus_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(TasksStatusRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StatusServiceServer).TasksStatus(m, &statusServiceTasksStatusServer{stream}) +} + +type StatusService_TasksStatusServer interface { + Send(*TasksStatusResponse) error + grpc.ServerStream +} + +type statusServiceTasksStatusServer struct { + grpc.ServerStream +} + +func (x *statusServiceTasksStatusServer) Send(m *TasksStatusResponse) error { + return x.ServerStream.SendMsg(m) +} + var _StatusService_serviceDesc = grpc.ServiceDesc{ ServiceName: "supervisor.StatusService", HandlerType: (*StatusServiceServer)(nil), @@ -823,6 +1127,11 @@ var _StatusService_serviceDesc = grpc.ServiceDesc{ Handler: _StatusService_PortsStatus_Handler, ServerStreams: true, }, + { + StreamName: "TasksStatus", + Handler: _StatusService_TasksStatus_Handler, + ServerStreams: true, + }, }, Metadata: "status.proto", } diff --git a/components/supervisor-api/go/status.pb.gw.go b/components/supervisor-api/go/status.pb.gw.go index cf52dbbd15df9c..e629621164fbd8 100644 --- a/components/supervisor-api/go/status.pb.gw.go +++ b/components/supervisor-api/go/status.pb.gw.go @@ -316,6 +316,69 @@ func request_StatusService_PortsStatus_1(ctx context.Context, marshaler runtime. } +var ( + filter_StatusService_TasksStatus_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_StatusService_TasksStatus_0(ctx context.Context, marshaler runtime.Marshaler, client StatusServiceClient, req *http.Request, pathParams map[string]string) (StatusService_TasksStatusClient, runtime.ServerMetadata, error) { + var protoReq TasksStatusRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_StatusService_TasksStatus_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + stream, err := client.TasksStatus(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +func request_StatusService_TasksStatus_1(ctx context.Context, marshaler runtime.Marshaler, client StatusServiceClient, req *http.Request, pathParams map[string]string) (StatusService_TasksStatusClient, runtime.ServerMetadata, error) { + var protoReq TasksStatusRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["observe"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "observe") + } + + protoReq.Observe, err = runtime.Bool(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "observe", err) + } + + stream, err := client.TasksStatus(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + // RegisterStatusServiceHandlerServer registers the http handlers for service StatusService to "mux". // UnaryRPC :call StatusServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -474,6 +537,20 @@ func RegisterStatusServiceHandlerServer(ctx context.Context, mux *runtime.ServeM return }) + mux.Handle("GET", pattern_StatusService_TasksStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + mux.Handle("GET", pattern_StatusService_TasksStatus_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + return nil } @@ -675,6 +752,46 @@ func RegisterStatusServiceHandlerClient(ctx context.Context, mux *runtime.ServeM }) + mux.Handle("GET", pattern_StatusService_TasksStatus_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_StatusService_TasksStatus_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_StatusService_TasksStatus_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_StatusService_TasksStatus_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_StatusService_TasksStatus_1(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_StatusService_TasksStatus_1(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -694,6 +811,10 @@ var ( pattern_StatusService_PortsStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "status", "ports"}, "", runtime.AssumeColonVerbOpt(false))) pattern_StatusService_PortsStatus_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 4, 1, 5, 3}, []string{"v1", "status", "ports", "observe", "true"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_StatusService_TasksStatus_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "status", "tasks"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_StatusService_TasksStatus_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 4, 1, 5, 3}, []string{"v1", "status", "ports", "observe", "true"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( @@ -712,4 +833,8 @@ var ( forward_StatusService_PortsStatus_0 = runtime.ForwardResponseStream forward_StatusService_PortsStatus_1 = runtime.ForwardResponseStream + + forward_StatusService_TasksStatus_0 = runtime.ForwardResponseStream + + forward_StatusService_TasksStatus_1 = runtime.ForwardResponseStream ) diff --git a/components/supervisor-api/go/terminal.pb.go b/components/supervisor-api/go/terminal.pb.go index 67cab9083874cd..f12ef7c0dba36f 100644 --- a/components/supervisor-api/go/terminal.pb.go +++ b/components/supervisor-api/go/terminal.pb.go @@ -30,9 +30,10 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type OpenTerminalRequest struct { - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Env map[string]string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *OpenTerminalRequest) Reset() { *m = OpenTerminalRequest{} } @@ -60,6 +61,13 @@ func (m *OpenTerminalRequest) XXX_DiscardUnknown() { var xxx_messageInfo_OpenTerminalRequest proto.InternalMessageInfo +func (m *OpenTerminalRequest) GetEnv() map[string]string { + if m != nil { + return m.Env + } + return nil +} + type OpenTerminalResponse struct { Alias string `protobuf:"bytes,1,opt,name=alias,proto3" json:"alias,omitempty"` // starter_token can be used to change the terminal size if there are @@ -109,6 +117,76 @@ func (m *OpenTerminalResponse) GetStarterToken() string { return "" } +type CloseTerminalRequest struct { + Alias string `protobuf:"bytes,1,opt,name=alias,proto3" json:"alias,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CloseTerminalRequest) Reset() { *m = CloseTerminalRequest{} } +func (m *CloseTerminalRequest) String() string { return proto.CompactTextString(m) } +func (*CloseTerminalRequest) ProtoMessage() {} +func (*CloseTerminalRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_ff8b8260c8ef16ad, []int{2} +} + +func (m *CloseTerminalRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CloseTerminalRequest.Unmarshal(m, b) +} +func (m *CloseTerminalRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CloseTerminalRequest.Marshal(b, m, deterministic) +} +func (m *CloseTerminalRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CloseTerminalRequest.Merge(m, src) +} +func (m *CloseTerminalRequest) XXX_Size() int { + return xxx_messageInfo_CloseTerminalRequest.Size(m) +} +func (m *CloseTerminalRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CloseTerminalRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CloseTerminalRequest proto.InternalMessageInfo + +func (m *CloseTerminalRequest) GetAlias() string { + if m != nil { + return m.Alias + } + return "" +} + +type CloseTerminalResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CloseTerminalResponse) Reset() { *m = CloseTerminalResponse{} } +func (m *CloseTerminalResponse) String() string { return proto.CompactTextString(m) } +func (*CloseTerminalResponse) ProtoMessage() {} +func (*CloseTerminalResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_ff8b8260c8ef16ad, []int{3} +} + +func (m *CloseTerminalResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CloseTerminalResponse.Unmarshal(m, b) +} +func (m *CloseTerminalResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CloseTerminalResponse.Marshal(b, m, deterministic) +} +func (m *CloseTerminalResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_CloseTerminalResponse.Merge(m, src) +} +func (m *CloseTerminalResponse) XXX_Size() int { + return xxx_messageInfo_CloseTerminalResponse.Size(m) +} +func (m *CloseTerminalResponse) XXX_DiscardUnknown() { + xxx_messageInfo_CloseTerminalResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_CloseTerminalResponse proto.InternalMessageInfo + type ListTerminalsRequest struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -119,7 +197,7 @@ func (m *ListTerminalsRequest) Reset() { *m = ListTerminalsRequest{} } func (m *ListTerminalsRequest) String() string { return proto.CompactTextString(m) } func (*ListTerminalsRequest) ProtoMessage() {} func (*ListTerminalsRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{2} + return fileDescriptor_ff8b8260c8ef16ad, []int{4} } func (m *ListTerminalsRequest) XXX_Unmarshal(b []byte) error { @@ -151,7 +229,7 @@ func (m *ListTerminalsResponse) Reset() { *m = ListTerminalsResponse{} } func (m *ListTerminalsResponse) String() string { return proto.CompactTextString(m) } func (*ListTerminalsResponse) ProtoMessage() {} func (*ListTerminalsResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{3} + return fileDescriptor_ff8b8260c8ef16ad, []int{5} } func (m *ListTerminalsResponse) XXX_Unmarshal(b []byte) error { @@ -192,7 +270,7 @@ func (m *ListTerminalsResponse_Terminal) Reset() { *m = ListTerminalsRes func (m *ListTerminalsResponse_Terminal) String() string { return proto.CompactTextString(m) } func (*ListTerminalsResponse_Terminal) ProtoMessage() {} func (*ListTerminalsResponse_Terminal) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{3, 0} + return fileDescriptor_ff8b8260c8ef16ad, []int{5, 0} } func (m *ListTerminalsResponse_Terminal) XXX_Unmarshal(b []byte) error { @@ -245,7 +323,7 @@ func (m *ListenTerminalRequest) Reset() { *m = ListenTerminalRequest{} } func (m *ListenTerminalRequest) String() string { return proto.CompactTextString(m) } func (*ListenTerminalRequest) ProtoMessage() {} func (*ListenTerminalRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{4} + return fileDescriptor_ff8b8260c8ef16ad, []int{6} } func (m *ListenTerminalRequest) XXX_Unmarshal(b []byte) error { @@ -287,7 +365,7 @@ func (m *ListenTerminalResponse) Reset() { *m = ListenTerminalResponse{} func (m *ListenTerminalResponse) String() string { return proto.CompactTextString(m) } func (*ListenTerminalResponse) ProtoMessage() {} func (*ListenTerminalResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{5} + return fileDescriptor_ff8b8260c8ef16ad, []int{7} } func (m *ListenTerminalResponse) XXX_Unmarshal(b []byte) error { @@ -365,7 +443,7 @@ func (m *WriteTerminalRequest) Reset() { *m = WriteTerminalRequest{} } func (m *WriteTerminalRequest) String() string { return proto.CompactTextString(m) } func (*WriteTerminalRequest) ProtoMessage() {} func (*WriteTerminalRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{6} + return fileDescriptor_ff8b8260c8ef16ad, []int{8} } func (m *WriteTerminalRequest) XXX_Unmarshal(b []byte) error { @@ -411,7 +489,7 @@ func (m *WriteTerminalResponse) Reset() { *m = WriteTerminalResponse{} } func (m *WriteTerminalResponse) String() string { return proto.CompactTextString(m) } func (*WriteTerminalResponse) ProtoMessage() {} func (*WriteTerminalResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{7} + return fileDescriptor_ff8b8260c8ef16ad, []int{9} } func (m *WriteTerminalResponse) XXX_Unmarshal(b []byte) error { @@ -463,7 +541,7 @@ func (m *SetTerminalSizeRequest) Reset() { *m = SetTerminalSizeRequest{} func (m *SetTerminalSizeRequest) String() string { return proto.CompactTextString(m) } func (*SetTerminalSizeRequest) ProtoMessage() {} func (*SetTerminalSizeRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{8} + return fileDescriptor_ff8b8260c8ef16ad, []int{10} } func (m *SetTerminalSizeRequest) XXX_Unmarshal(b []byte) error { @@ -574,7 +652,7 @@ func (m *SetTerminalSizeResponse) Reset() { *m = SetTerminalSizeResponse func (m *SetTerminalSizeResponse) String() string { return proto.CompactTextString(m) } func (*SetTerminalSizeResponse) ProtoMessage() {} func (*SetTerminalSizeResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_ff8b8260c8ef16ad, []int{9} + return fileDescriptor_ff8b8260c8ef16ad, []int{11} } func (m *SetTerminalSizeResponse) XXX_Unmarshal(b []byte) error { @@ -597,7 +675,10 @@ var xxx_messageInfo_SetTerminalSizeResponse proto.InternalMessageInfo func init() { proto.RegisterType((*OpenTerminalRequest)(nil), "supervisor.OpenTerminalRequest") + proto.RegisterMapType((map[string]string)(nil), "supervisor.OpenTerminalRequest.EnvEntry") proto.RegisterType((*OpenTerminalResponse)(nil), "supervisor.OpenTerminalResponse") + proto.RegisterType((*CloseTerminalRequest)(nil), "supervisor.CloseTerminalRequest") + proto.RegisterType((*CloseTerminalResponse)(nil), "supervisor.CloseTerminalResponse") proto.RegisterType((*ListTerminalsRequest)(nil), "supervisor.ListTerminalsRequest") proto.RegisterType((*ListTerminalsResponse)(nil), "supervisor.ListTerminalsResponse") proto.RegisterType((*ListTerminalsResponse_Terminal)(nil), "supervisor.ListTerminalsResponse.Terminal") @@ -614,45 +695,50 @@ func init() { } var fileDescriptor_ff8b8260c8ef16ad = []byte{ - // 594 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x54, 0xcd, 0x72, 0x12, 0x41, - 0x10, 0x0e, 0xe1, 0x27, 0xa4, 0x25, 0x5a, 0x4e, 0x00, 0x37, 0x6b, 0x2c, 0x71, 0xb8, 0x50, 0x56, - 0xc9, 0x6a, 0xbc, 0x7a, 0xe2, 0x44, 0x95, 0x56, 0x89, 0x0b, 0x65, 0xaa, 0xbc, 0xa4, 0x36, 0x30, - 0x81, 0x29, 0x97, 0x9d, 0x75, 0xa6, 0x81, 0x44, 0xcb, 0x8b, 0x07, 0x5f, 0xc0, 0x47, 0xf1, 0x25, - 0xbc, 0xfb, 0x0a, 0x3e, 0x88, 0x35, 0x33, 0xbb, 0x24, 0xc0, 0x06, 0xbd, 0xed, 0xf7, 0x75, 0x4f, - 0x7f, 0xdd, 0xd3, 0xdf, 0x2c, 0xdc, 0x45, 0x26, 0xa7, 0x3c, 0x0a, 0xc2, 0x76, 0x2c, 0x05, 0x0a, - 0x02, 0x6a, 0x16, 0x33, 0x39, 0xe7, 0x4a, 0x48, 0xf7, 0x78, 0x2c, 0xc4, 0x38, 0x64, 0x5e, 0x10, - 0x73, 0x2f, 0x88, 0x22, 0x81, 0x01, 0x72, 0x11, 0x29, 0x9b, 0x49, 0x6b, 0x70, 0xf8, 0x36, 0x66, - 0xd1, 0x20, 0x39, 0xef, 0xb3, 0x4f, 0x33, 0xa6, 0x90, 0xbe, 0x83, 0xea, 0x2a, 0xad, 0x62, 0x11, - 0x29, 0x46, 0xaa, 0x50, 0x0c, 0x42, 0x1e, 0x28, 0x27, 0xd7, 0xc8, 0xb5, 0xf6, 0x7d, 0x0b, 0x48, - 0x13, 0x0e, 0x14, 0x06, 0x12, 0x99, 0x3c, 0x43, 0xf1, 0x91, 0x45, 0xce, 0xae, 0x89, 0x56, 0x12, - 0x72, 0xa0, 0x39, 0x5a, 0x87, 0xea, 0x1b, 0xae, 0x30, 0x2d, 0xa9, 0x52, 0xa9, 0x9f, 0x39, 0xa8, - 0xad, 0x05, 0x12, 0xb1, 0x2e, 0xec, 0xa7, 0x73, 0x69, 0xc1, 0x7c, 0xeb, 0xce, 0xc9, 0xd3, 0xf6, - 0xf5, 0x64, 0xed, 0xcc, 0x53, 0xed, 0x65, 0xcf, 0xd7, 0x87, 0xdd, 0x1e, 0x94, 0x53, 0xfa, 0x96, - 0x11, 0x1c, 0xd8, 0x1b, 0x8a, 0xe9, 0x34, 0x88, 0x46, 0xce, 0x6e, 0x23, 0xdf, 0xda, 0xf7, 0x53, - 0xa8, 0xf3, 0x91, 0x63, 0xc8, 0x9c, 0xbc, 0xcd, 0x37, 0x80, 0x3e, 0xb3, 0x4d, 0x6f, 0xdc, 0x5c, - 0x76, 0x79, 0xfa, 0x1e, 0xea, 0xeb, 0xe9, 0xc9, 0x90, 0x0e, 0x94, 0x14, 0x8e, 0xc4, 0x0c, 0xcd, - 0x81, 0x4a, 0x77, 0xc7, 0x4f, 0x70, 0x12, 0x61, 0x52, 0x9a, 0xeb, 0x4c, 0x23, 0x4c, 0xca, 0x4e, - 0x19, 0x4a, 0x62, 0x86, 0xf1, 0x0c, 0x69, 0x07, 0xaa, 0xa7, 0x92, 0x23, 0xfb, 0xaf, 0x2e, 0x34, - 0xab, 0x70, 0xc4, 0xed, 0x7e, 0x2a, 0xbe, 0x05, 0xf4, 0x15, 0xd4, 0xd6, 0x6a, 0x24, 0xad, 0x35, - 0xe1, 0xe0, 0xfc, 0x0a, 0x99, 0x3a, 0x5b, 0x48, 0x8e, 0xc8, 0x22, 0x53, 0xec, 0xc0, 0xaf, 0x18, - 0xf2, 0xd4, 0x72, 0xf4, 0x57, 0x0e, 0xea, 0x7d, 0xb6, 0xdc, 0x43, 0x9f, 0x7f, 0x66, 0xdb, 0x9b, - 0xa8, 0x43, 0xf1, 0x86, 0x49, 0xba, 0x3b, 0xbe, 0x85, 0x9a, 0xbf, 0x10, 0x72, 0x68, 0xef, 0xb9, - 0xac, 0x79, 0x03, 0x09, 0x81, 0x82, 0x14, 0x0b, 0xe5, 0x14, 0x8c, 0xb8, 0xf9, 0xd6, 0xdc, 0x50, - 0x84, 0xca, 0x29, 0x5a, 0x4e, 0x7f, 0xeb, 0x0d, 0x2e, 0xf8, 0x08, 0x27, 0xbd, 0x4b, 0xa7, 0x64, - 0xe8, 0x14, 0x12, 0x17, 0xca, 0x13, 0xc6, 0xc7, 0x13, 0xec, 0x5d, 0x3a, 0x7b, 0x26, 0xb4, 0xc4, - 0x1d, 0x80, 0x72, 0x2c, 0xb9, 0x90, 0x1c, 0xaf, 0xe8, 0x11, 0x3c, 0xd8, 0x98, 0xc4, 0x5e, 0xc5, - 0xc9, 0xf7, 0x02, 0xdc, 0x5b, 0x06, 0xb4, 0xff, 0x86, 0x8c, 0xbc, 0x86, 0x82, 0x7e, 0x23, 0xe4, - 0xf1, 0x4d, 0x4f, 0x66, 0x3c, 0x26, 0xb7, 0x71, 0x7b, 0x82, 0x2d, 0x4f, 0x77, 0xc8, 0x05, 0x14, - 0xb4, 0x41, 0x48, 0x63, 0x8b, 0xc1, 0x6d, 0xb5, 0x27, 0xff, 0x7c, 0x02, 0xf4, 0xe8, 0xdb, 0xef, - 0x3f, 0x3f, 0x76, 0x0f, 0xc9, 0x7d, 0x6f, 0xfe, 0xc2, 0x4b, 0x5f, 0x81, 0x17, 0xea, 0xfa, 0x73, - 0x28, 0x59, 0x23, 0x92, 0x8d, 0x3a, 0x9b, 0x8d, 0xd3, 0x6d, 0x29, 0x89, 0x56, 0xd3, 0x68, 0x3d, - 0x22, 0x0f, 0x37, 0xb4, 0x58, 0xe4, 0x7d, 0x31, 0x2b, 0xff, 0xfa, 0x3c, 0x47, 0x62, 0x28, 0x1a, - 0x93, 0xad, 0x0e, 0x98, 0xe5, 0xdd, 0xd5, 0x01, 0x33, 0x9d, 0x49, 0xa9, 0x11, 0x3d, 0xa6, 0xee, - 0x8a, 0xa8, 0xb6, 0x29, 0x4b, 0x35, 0xc9, 0x00, 0xf6, 0xfa, 0x0c, 0xf5, 0x16, 0xc9, 0xca, 0x1c, - 0xd9, 0x66, 0x75, 0x9b, 0x5b, 0x73, 0xd2, 0x3d, 0x75, 0x8a, 0x1f, 0xf2, 0x41, 0xcc, 0xcf, 0x4b, - 0xe6, 0xef, 0xf9, 0xf2, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x72, 0x49, 0xb4, 0x06, 0x79, 0x05, - 0x00, 0x00, + // 686 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x55, 0x4d, 0x4f, 0x13, 0x41, + 0x18, 0x66, 0xfb, 0x45, 0x79, 0x2d, 0x7e, 0x0c, 0xa5, 0x2c, 0x2b, 0x46, 0x9c, 0x5e, 0x88, 0xd1, + 0x56, 0x31, 0x31, 0x86, 0x78, 0xaa, 0x21, 0x21, 0xd1, 0x44, 0x5c, 0x88, 0x24, 0x5e, 0xc8, 0xd2, + 0x0e, 0x30, 0x61, 0x99, 0x59, 0x67, 0xa6, 0x85, 0x6a, 0xbc, 0x78, 0xf3, 0xec, 0x4f, 0xf1, 0x4f, + 0x78, 0xf7, 0x2f, 0xf8, 0x23, 0x3c, 0x9a, 0xf9, 0xd8, 0x96, 0x6d, 0x97, 0xea, 0x6d, 0xdf, 0x67, + 0x9e, 0x79, 0xde, 0x79, 0x3f, 0x9e, 0x2c, 0xdc, 0x54, 0x44, 0x9c, 0x53, 0x16, 0xc5, 0xad, 0x44, + 0x70, 0xc5, 0x11, 0xc8, 0x7e, 0x42, 0xc4, 0x80, 0x4a, 0x2e, 0x82, 0xb5, 0x13, 0xce, 0x4f, 0x62, + 0xd2, 0x8e, 0x12, 0xda, 0x8e, 0x18, 0xe3, 0x2a, 0x52, 0x94, 0x33, 0x69, 0x99, 0xf8, 0x9b, 0x07, + 0x4b, 0x6f, 0x13, 0xc2, 0xf6, 0x9d, 0x40, 0x48, 0x3e, 0xf6, 0x89, 0x54, 0x68, 0x0b, 0x8a, 0x84, + 0x0d, 0xfc, 0xc2, 0x7a, 0x71, 0xe3, 0xc6, 0xe6, 0x46, 0x6b, 0xac, 0xd7, 0xca, 0x61, 0xb7, 0xb6, + 0xd9, 0x60, 0x9b, 0x29, 0x31, 0x0c, 0xf5, 0xa5, 0xe0, 0x39, 0x54, 0x53, 0x00, 0xdd, 0x86, 0xe2, + 0x19, 0x19, 0xfa, 0xde, 0xba, 0xb7, 0xb1, 0x10, 0xea, 0x4f, 0x54, 0x87, 0xf2, 0x20, 0x8a, 0xfb, + 0xc4, 0x2f, 0x18, 0xcc, 0x06, 0x5b, 0x85, 0x17, 0x1e, 0x7e, 0x07, 0xf5, 0xac, 0xb8, 0x4c, 0x38, + 0x93, 0x44, 0xdf, 0x88, 0x62, 0x1a, 0x49, 0xa7, 0x62, 0x03, 0xd4, 0x84, 0x45, 0xa9, 0x22, 0xa1, + 0x88, 0x38, 0x54, 0xfc, 0x8c, 0x30, 0xa7, 0x57, 0x73, 0xe0, 0xbe, 0xc6, 0xf0, 0x23, 0xa8, 0xbf, + 0x8a, 0xb9, 0x24, 0x93, 0xe5, 0xe5, 0x4a, 0xe2, 0x15, 0x58, 0x9e, 0x60, 0xdb, 0x17, 0xe0, 0x06, + 0xd4, 0xdf, 0x50, 0xa9, 0x52, 0x5c, 0x3a, 0x19, 0xfc, 0xc3, 0x83, 0xe5, 0x89, 0x03, 0xf7, 0xe6, + 0x1d, 0x58, 0x48, 0x67, 0xa2, 0x93, 0xe8, 0x2e, 0x3e, 0xbc, 0xda, 0xc5, 0xdc, 0x5b, 0xad, 0x51, + 0xe2, 0xf1, 0xe5, 0x60, 0x17, 0xaa, 0x29, 0x7c, 0x4d, 0x27, 0x7c, 0x98, 0xef, 0xf2, 0xf3, 0xf3, + 0x88, 0xf5, 0xcc, 0xbc, 0x16, 0xc2, 0x34, 0xd4, 0x7c, 0x45, 0x55, 0x4c, 0xfc, 0xa2, 0xe5, 0x9b, + 0x00, 0x3f, 0xb6, 0x8f, 0x9e, 0x1e, 0x7a, 0x7e, 0x57, 0xde, 0x43, 0x63, 0x92, 0xee, 0x8a, 0xf4, + 0xa1, 0x22, 0x55, 0x8f, 0xf7, 0x95, 0xb9, 0x50, 0xdb, 0x99, 0x0b, 0x5d, 0xec, 0x4e, 0x88, 0x10, + 0x66, 0x2a, 0xe9, 0x09, 0x11, 0xa2, 0x53, 0x85, 0x0a, 0xef, 0xab, 0xa4, 0xaf, 0x70, 0x07, 0xea, + 0x07, 0x82, 0xaa, 0xff, 0x9b, 0x8d, 0x46, 0xa5, 0xea, 0x51, 0x3b, 0xe6, 0x5a, 0x68, 0x03, 0xfc, + 0x12, 0x96, 0x27, 0x34, 0xdc, 0xd3, 0x9a, 0xb0, 0x78, 0x34, 0x54, 0x44, 0x1e, 0x5e, 0x08, 0xaa, + 0x14, 0x61, 0x46, 0x6c, 0x31, 0xac, 0x19, 0xf0, 0xc0, 0x62, 0xf8, 0xa7, 0x07, 0x8d, 0x3d, 0x32, + 0x9a, 0xc3, 0x1e, 0xfd, 0x44, 0x66, 0x3f, 0xa2, 0x01, 0xe5, 0x2b, 0xbb, 0xb6, 0x33, 0x17, 0xda, + 0x50, 0xe3, 0xc7, 0x5c, 0x74, 0x6d, 0x9f, 0xab, 0x1a, 0x37, 0x21, 0x42, 0x50, 0x12, 0xfc, 0x42, + 0xfa, 0x25, 0x93, 0xdc, 0x7c, 0x6b, 0xac, 0xcb, 0x63, 0xe9, 0x97, 0x2d, 0xa6, 0xbf, 0xf5, 0x04, + 0x2f, 0x68, 0x4f, 0x9d, 0xee, 0x5e, 0xfa, 0x15, 0x03, 0xa7, 0x21, 0x0a, 0xa0, 0x7a, 0x4a, 0xe8, + 0xc9, 0xa9, 0xda, 0xbd, 0xf4, 0xe7, 0xcd, 0xd1, 0x28, 0xee, 0x00, 0x54, 0x13, 0x41, 0xb9, 0xa0, + 0x6a, 0x88, 0x57, 0x61, 0x65, 0xaa, 0x12, 0xdb, 0x8a, 0xcd, 0x3f, 0x25, 0xb8, 0x35, 0x3a, 0xd0, + 0xfb, 0xd7, 0x25, 0xe8, 0x35, 0x94, 0xb4, 0xd5, 0xd0, 0xfd, 0x7f, 0x38, 0x3b, 0x58, 0xbf, 0x9e, + 0xe0, 0xbc, 0x31, 0x87, 0x12, 0x28, 0x1b, 0xdb, 0xa0, 0x0c, 0x39, 0xcf, 0x77, 0xc1, 0x83, 0x19, + 0x0c, 0xa7, 0x87, 0xbf, 0xfe, 0xfa, 0xfd, 0xbd, 0xb0, 0x86, 0x82, 0xf6, 0xe0, 0x69, 0x3b, 0xb5, + 0x41, 0xbb, 0xab, 0xb9, 0xed, 0xcf, 0x66, 0x0c, 0x5f, 0xd0, 0x31, 0x94, 0xf4, 0x4a, 0x66, 0x13, + 0xe6, 0x39, 0x34, 0x9b, 0x30, 0xd7, 0x74, 0x78, 0xd5, 0x24, 0x5c, 0x42, 0x77, 0x32, 0x09, 0x63, + 0xad, 0x3f, 0x80, 0x8a, 0x5d, 0x7d, 0x34, 0xa5, 0x33, 0xdd, 0x2a, 0x3c, 0x8b, 0xe2, 0x72, 0x35, + 0x4d, 0xae, 0x7b, 0xe8, 0xee, 0x54, 0x2e, 0xc2, 0xd2, 0xea, 0x9e, 0x78, 0xba, 0xa3, 0x66, 0xad, + 0xb3, 0x05, 0xe6, 0xb9, 0x25, 0x5b, 0x60, 0xae, 0x17, 0xd2, 0x8e, 0xe2, 0x6c, 0x47, 0xb5, 0x31, + 0xc6, 0x1d, 0xdd, 0x87, 0xf9, 0x3d, 0xa2, 0xf4, 0xde, 0xa0, 0x4c, 0x1d, 0xf9, 0xf6, 0x08, 0x9a, + 0x33, 0x39, 0xe9, 0x66, 0x74, 0xca, 0x1f, 0x8a, 0x51, 0x42, 0x8f, 0x2a, 0xe6, 0x5f, 0xf3, 0xec, + 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf9, 0x30, 0xbf, 0x84, 0xa7, 0x06, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -669,6 +755,9 @@ const _ = grpc.SupportPackageIsVersion6 type TerminalServiceClient interface { // Open opens a new terminal running the login shell Open(ctx context.Context, in *OpenTerminalRequest, opts ...grpc.CallOption) (*OpenTerminalResponse, error) + // Close closes a terminal for the given alias, SIGKILL'ing all child processes + // before closing the pseudo-terminal. + Close(ctx context.Context, in *CloseTerminalRequest, opts ...grpc.CallOption) (*CloseTerminalResponse, error) // List lists all open terminals List(ctx context.Context, in *ListTerminalsRequest, opts ...grpc.CallOption) (*ListTerminalsResponse, error) // Listen listens to a terminal @@ -696,6 +785,15 @@ func (c *terminalServiceClient) Open(ctx context.Context, in *OpenTerminalReques return out, nil } +func (c *terminalServiceClient) Close(ctx context.Context, in *CloseTerminalRequest, opts ...grpc.CallOption) (*CloseTerminalResponse, error) { + out := new(CloseTerminalResponse) + err := c.cc.Invoke(ctx, "/supervisor.TerminalService/Close", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *terminalServiceClient) List(ctx context.Context, in *ListTerminalsRequest, opts ...grpc.CallOption) (*ListTerminalsResponse, error) { out := new(ListTerminalsResponse) err := c.cc.Invoke(ctx, "/supervisor.TerminalService/List", in, out, opts...) @@ -759,6 +857,9 @@ func (c *terminalServiceClient) SetSize(ctx context.Context, in *SetTerminalSize type TerminalServiceServer interface { // Open opens a new terminal running the login shell Open(context.Context, *OpenTerminalRequest) (*OpenTerminalResponse, error) + // Close closes a terminal for the given alias, SIGKILL'ing all child processes + // before closing the pseudo-terminal. + Close(context.Context, *CloseTerminalRequest) (*CloseTerminalResponse, error) // List lists all open terminals List(context.Context, *ListTerminalsRequest) (*ListTerminalsResponse, error) // Listen listens to a terminal @@ -776,6 +877,9 @@ type UnimplementedTerminalServiceServer struct { func (*UnimplementedTerminalServiceServer) Open(ctx context.Context, req *OpenTerminalRequest) (*OpenTerminalResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Open not implemented") } +func (*UnimplementedTerminalServiceServer) Close(ctx context.Context, req *CloseTerminalRequest) (*CloseTerminalResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Close not implemented") +} func (*UnimplementedTerminalServiceServer) List(ctx context.Context, req *ListTerminalsRequest) (*ListTerminalsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method List not implemented") } @@ -811,6 +915,24 @@ func _TerminalService_Open_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _TerminalService_Close_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseTerminalRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TerminalServiceServer).Close(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/supervisor.TerminalService/Close", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TerminalServiceServer).Close(ctx, req.(*CloseTerminalRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _TerminalService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListTerminalsRequest) if err := dec(in); err != nil { @@ -894,6 +1016,10 @@ var _TerminalService_serviceDesc = grpc.ServiceDesc{ MethodName: "Open", Handler: _TerminalService_Open_Handler, }, + { + MethodName: "Close", + Handler: _TerminalService_Close_Handler, + }, { MethodName: "List", Handler: _TerminalService_List_Handler, diff --git a/components/supervisor-api/go/terminal.pb.gw.go b/components/supervisor-api/go/terminal.pb.gw.go index 414e253e542ebc..6efe509c500db8 100644 --- a/components/supervisor-api/go/terminal.pb.gw.go +++ b/components/supervisor-api/go/terminal.pb.gw.go @@ -37,6 +37,60 @@ var _ = utilities.NewDoubleArray var _ = descriptor.ForMessage var _ = metadata.Join +func request_TerminalService_Close_0(ctx context.Context, marshaler runtime.Marshaler, client TerminalServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CloseTerminalRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["alias"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "alias") + } + + protoReq.Alias, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "alias", err) + } + + msg, err := client.Close(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_TerminalService_Close_0(ctx context.Context, marshaler runtime.Marshaler, server TerminalServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq CloseTerminalRequest + var metadata runtime.ServerMetadata + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["alias"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "alias") + } + + protoReq.Alias, err = runtime.String(val) + + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "alias", err) + } + + msg, err := server.Close(ctx, &protoReq) + return msg, metadata, err + +} + func request_TerminalService_List_0(ctx context.Context, marshaler runtime.Marshaler, client TerminalServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq ListTerminalsRequest var metadata runtime.ServerMetadata @@ -168,6 +222,29 @@ func local_request_TerminalService_Write_0(ctx context.Context, marshaler runtim // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterTerminalServiceHandlerFromEndpoint instead. func RegisterTerminalServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server TerminalServiceServer) error { + mux.Handle("GET", pattern_TerminalService_Close_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_TerminalService_Close_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_TerminalService_Close_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_TerminalService_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -262,6 +339,26 @@ func RegisterTerminalServiceHandler(ctx context.Context, mux *runtime.ServeMux, // "TerminalServiceClient" to call the correct interceptors. func RegisterTerminalServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client TerminalServiceClient) error { + mux.Handle("GET", pattern_TerminalService_Close_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_TerminalService_Close_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_TerminalService_Close_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("GET", pattern_TerminalService_List_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -326,6 +423,8 @@ func RegisterTerminalServiceHandlerClient(ctx context.Context, mux *runtime.Serv } var ( + pattern_TerminalService_Close_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "terminal", "close", "alias"}, "", runtime.AssumeColonVerbOpt(false))) + pattern_TerminalService_List_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "terminal", "list"}, "", runtime.AssumeColonVerbOpt(false))) pattern_TerminalService_Listen_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "terminal", "listen", "alias"}, "", runtime.AssumeColonVerbOpt(false))) @@ -334,6 +433,8 @@ var ( ) var ( + forward_TerminalService_Close_0 = runtime.ForwardResponseMessage + forward_TerminalService_List_0 = runtime.ForwardResponseMessage forward_TerminalService_Listen_0 = runtime.ForwardResponseStream diff --git a/components/supervisor-api/status.proto b/components/supervisor-api/status.proto index d6d090ebf39ecd..a34effca0144e9 100644 --- a/components/supervisor-api/status.proto +++ b/components/supervisor-api/status.proto @@ -60,6 +60,16 @@ service StatusService { }; } + // TasksStatus provides tasks status information. + rpc TasksStatus(TasksStatusRequest) returns (stream TasksStatusResponse) { + option (google.api.http) = { + get: "/v1/status/tasks" + additional_bindings { + get: "/v1/status/ports/observe/{observe=true}", + } + }; + } + } message SupervisorStatusRequest {} @@ -119,3 +129,28 @@ message PortsStatus { uint32 global_port = 2; } + +message TasksStatusRequest { + // if observe is true, we'll return a stream of changes rather than just the + // current state of affairs. + bool observe = 1; +} +message TasksStatusResponse { + repeated TaskStatus tasks = 1; +} +message TaskStatus { + string id = 1; + TaskState state = 2; + string terminal = 3; + TaskPresentation presentation = 4; +} +enum TaskState { + opening = 0; + running = 1; + closed = 2; +} +message TaskPresentation { + string name = 1; + string open_in = 2; + string open_mode = 3; +} diff --git a/components/supervisor-api/terminal.proto b/components/supervisor-api/terminal.proto index d4d8a9094c4789..7c3c5d2eedb7ab 100644 --- a/components/supervisor-api/terminal.proto +++ b/components/supervisor-api/terminal.proto @@ -13,6 +13,14 @@ option go_package = "api"; service TerminalService { // Open opens a new terminal running the login shell rpc Open(OpenTerminalRequest) returns (OpenTerminalResponse) {} + + // Close closes a terminal for the given alias, SIGKILL'ing all child processes + // before closing the pseudo-terminal. + rpc Close(CloseTerminalRequest) returns (CloseTerminalResponse) { + option (google.api.http) = { + get: "/v1/terminal/close/{alias}" + }; + } // List lists all open terminals rpc List(ListTerminalsRequest) returns (ListTerminalsResponse) { @@ -39,7 +47,9 @@ service TerminalService { rpc SetSize(SetTerminalSizeRequest) returns (SetTerminalSizeResponse) {} } -message OpenTerminalRequest {} +message OpenTerminalRequest { + map env = 2; +} message OpenTerminalResponse { string alias = 1; @@ -48,6 +58,11 @@ message OpenTerminalResponse { string starter_token = 2; } +message CloseTerminalRequest { + string alias = 1; +} +message CloseTerminalResponse {} + message ListTerminalsRequest {} message ListTerminalsResponse { message Terminal { diff --git a/components/supervisor/pkg/supervisor/config.go b/components/supervisor/pkg/supervisor/config.go index b4583b10ec946b..93fb604659d78c 100644 --- a/components/supervisor/pkg/supervisor/config.go +++ b/components/supervisor/pkg/supervisor/config.go @@ -183,6 +183,12 @@ type WorkspaceConfig struct { // GitpodHost points to the Gitpod API server we're to talk to GitpodHost string `env:"GITPOD_HOST"` + + // GitpodTasks is the task configuration of the workspace + GitpodTasks *string `env:"GITPOD_TASKS"` + + // GitpodHeadless controls whether the workspace is running headless + GitpodHeadless *string `env:"GITPOD_HEADLESS"` } // WorkspaceGitpodToken is a list of tokens that should be added to supervisor's token service @@ -191,6 +197,18 @@ type WorkspaceGitpodToken struct { TokenOTS string `json:"tokenOTS"` } +// TaskConfig defines gitpod task shape +type TaskConfig struct { + Name *string `json:"name,omitempty"` + Before *string `json:"before,omitempty"` + Init *string `json:"init,omitempty"` + Prebuild *string `json:"prebuild,omitempty"` + Command *string `json:"command,omitempty"` + Env *map[string]string `json:"env,omitempty"` + OpenIn *string `json:"openIn,omitempty"` + OpenMode *string `json:"openMode,omitempty"` +} + // Validate validates this configuration func (c WorkspaceConfig) Validate() error { if !(0 < c.IDEPort && c.IDEPort <= math.MaxUint16) { @@ -270,6 +288,18 @@ func (c WorkspaceConfig) GitpodAPIEndpoint() (endpoint, host string, err error) return } +// getGitpodTasks parses gitpod tasks +func (c WorkspaceConfig) getGitpodTasks() (tasks *[]TaskConfig, err error) { + if c.GitpodTasks == nil { + return + } + err = json.Unmarshal([]byte(*c.GitpodTasks), &tasks) + if err != nil { + return nil, fmt.Errorf("cannot parse tasks: %w", err) + } + return +} + // GetConfig loads the supervisor configuration func GetConfig() (*Config, error) { static, err := loadStaticConfigFromFile() diff --git a/components/supervisor/pkg/supervisor/services.go b/components/supervisor/pkg/supervisor/services.go index f001cc4ab5950c..5448edd2be29a4 100644 --- a/components/supervisor/pkg/supervisor/services.go +++ b/components/supervisor/pkg/supervisor/services.go @@ -41,6 +41,7 @@ type RegisterableRESTService interface { type statusService struct { IWH *backup.InWorkspaceHelper Ports *portsManager + Tasks *tasksManager IDEReady <-chan struct{} } @@ -149,6 +150,45 @@ func (s *statusService) PortsStatus(req *api.PortsStatusRequest, srv api.StatusS } } +func (s *statusService) TasksStatus(req *api.TasksStatusRequest, srv api.StatusService_TasksStatusServer) error { + select { + case <-srv.Context().Done(): + return nil + case <-s.Tasks.ready: + } + + err := srv.Send(&api.TasksStatusResponse{ + Tasks: s.Tasks.getStatus(), + }) + if err != nil { + return err + } + if !req.Observe { + return nil + } + + sub := s.Tasks.Subscribe() + if sub == nil { + return status.Error(codes.ResourceExhausted, "too many subscriptions") + } + defer sub.Close() + + for { + select { + case <-srv.Context().Done(): + return nil + case update := <-sub.Updates(): + if update == nil { + return nil + } + err := srv.Send(&api.TasksStatusResponse{Tasks: update}) + if err != nil { + return err + } + } + } +} + // RegistrableTokenService can register the token service type RegistrableTokenService struct { Service api.TokenServiceServer diff --git a/components/supervisor/pkg/supervisor/supervisor.go b/components/supervisor/pkg/supervisor/supervisor.go index f30b66ebe82b52..ae9b50297e6df0 100644 --- a/components/supervisor/pkg/supervisor/supervisor.go +++ b/components/supervisor/pkg/supervisor/supervisor.go @@ -123,6 +123,7 @@ func Run(options ...RunOption) { termMuxSrv = terminal.NewMuxTerminalService(termMux) uidmapCanary = ndeapi.NewInWorkspaceHelper() ) + taskManager := newTasksManager(cfg, termMuxSrv, iwh) termMuxSrv.DefaultWorkdir = cfg.RepoRoot @@ -131,6 +132,7 @@ func Run(options ...RunOption) { apiServices = append(apiServices, &statusService{ IWH: iwh, Ports: portMgmt, + Tasks: taskManager, IDEReady: ideReady, }) apiServices = append(apiServices, termMuxSrv) @@ -146,6 +148,7 @@ func Run(options ...RunOption) { go startContentInit(ctx, cfg, &wg, iwh) go startAPIEndpoint(ctx, cfg, &wg, apiServices) go portMgmt.Run(ctx, &wg) + go taskManager.Run(ctx, &wg) if cfg.PreventMetadataAccess { go func() { diff --git a/components/supervisor/pkg/supervisor/tasks.go b/components/supervisor/pkg/supervisor/tasks.go new file mode 100644 index 00000000000000..211d3a669341e0 --- /dev/null +++ b/components/supervisor/pkg/supervisor/tasks.go @@ -0,0 +1,414 @@ +// Copyright (c) 2020 TypeFox GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package supervisor + +import ( + "bufio" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/gitpod-io/gitpod/common-go/log" + csapi "github.com/gitpod-io/gitpod/content-service/api" + "github.com/gitpod-io/gitpod/supervisor/api" + "github.com/gitpod-io/gitpod/supervisor/pkg/backup" + "github.com/gitpod-io/gitpod/supervisor/pkg/terminal" +) + +type runContext struct { + contentSource csapi.WorkspaceInitSource + headless bool + tasks []*task +} + +type tasksSubscription struct { + updates chan []*api.TaskStatus + Close func() error +} + +func (sub *tasksSubscription) Updates() <-chan []*api.TaskStatus { + return sub.updates +} + +func (tm *tasksManager) Subscribe() *tasksSubscription { + tm.mu.Lock() + defer tm.mu.Unlock() + + if len(tm.subscriptions) > maxSubscriptions { + return nil + } + + sub := &tasksSubscription{updates: make(chan []*api.TaskStatus, 5)} + sub.Close = func() error { + tm.mu.Lock() + defer tm.mu.Unlock() + + // We can safely close the channel here even though we're not the + // producer writing to it, because we're holding mu. + close(sub.updates) + delete(tm.subscriptions, sub) + + return nil + } + tm.subscriptions[sub] = struct{}{} + + return sub +} + +type task struct { + api.TaskStatus + config TaskConfig + command string + prebuildChan chan bool +} + +type tasksManager struct { + config *Config + tasks map[string]*task + subscriptions map[*tasksSubscription]struct{} + mu sync.RWMutex + ready chan struct{} + terminalService *terminal.MuxTerminalService + contentState backup.ContentState +} + +func newTasksManager(config *Config, terminalService *terminal.MuxTerminalService, contentState backup.ContentState) *tasksManager { + return &tasksManager{ + config: config, + terminalService: terminalService, + contentState: contentState, + tasks: make(map[string]*task), + subscriptions: make(map[*tasksSubscription]struct{}), + ready: make(chan struct{}), + } +} + +func (tm *tasksManager) getStatus() []*api.TaskStatus { + tm.mu.RLock() + defer tm.mu.RUnlock() + + i := 0 + status := make([]*api.TaskStatus, len(tm.tasks)) + for _, task := range tm.tasks { + status[i] = &task.TaskStatus + i++ + } + return status +} + +func (tm *tasksManager) updateState(doUpdate func() *task) { + tm.mu.Lock() + defer tm.mu.Unlock() + + updated := doUpdate() + if updated == nil { + return + } + updates := make([]*api.TaskStatus, 1) + updates[0] = &updated.TaskStatus + for sub := range tm.subscriptions { + select { + case sub.updates <- updates: + default: + log.Warn("cannot to push tasks update to a subscriber") + } + } +} + +func (tm *tasksManager) setTaskState(t *task, newState api.TaskState) { + tm.updateState(func() *task { + if t.State == newState { + return nil + } + t.State = newState + return t + }) +} + +func (tm *tasksManager) init(ctx context.Context) *runContext { + defer close(tm.ready) + + tasks, err := tm.config.getGitpodTasks() + if err != nil { + log.WithError(err).Fatal() + return nil + } + if tasks == nil { + log.Info("no gitpod tasks found") + return nil + } + + select { + case <-ctx.Done(): + return nil + case <-tm.contentState.ContentReady(): + } + + contentSource, _ := tm.contentState.ContentSource() + headless := tm.config.GitpodHeadless != nil && *tm.config.GitpodHeadless == "true" + runContext := &runContext{ + contentSource: contentSource, + headless: headless, + tasks: make([]*task, 0), + } + + for i, config := range *tasks { + id := strconv.Itoa(i) + presentation := &api.TaskPresentation{} + if config.Name != nil { + presentation.Name = *config.Name + } else { + presentation.Name = tm.terminalService.DefaultWorkdir + } + if config.OpenIn != nil { + presentation.OpenIn = *config.OpenIn + } + if config.OpenMode != nil { + presentation.OpenMode = *config.OpenMode + } + task := &task{ + TaskStatus: api.TaskStatus{ + Id: id, + State: api.TaskState_opening, + Presentation: presentation, + }, + config: config, + } + task.command = task.getCommand(runContext) + if task.command == "" { + task.State = api.TaskState_closed + } else { + runContext.tasks = append(runContext.tasks, task) + } + tm.tasks[id] = task + } + return runContext +} + +func (tm *tasksManager) Run(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + runContext := tm.init(ctx) + if runContext == nil { + return + } + if len(runContext.tasks) == 0 { + log.Info("no gitpod tasks to run") + return + } + + for _, t := range runContext.tasks { + taskLog := log.WithField("command", t.command) + taskLog.Info("starting a task terminal...") + openRequest := &api.OpenTerminalRequest{} + if t.config.Env != nil { + openRequest.Env = *t.config.Env + } + resp, err := tm.terminalService.Open(ctx, openRequest) + if err != nil { + taskLog.WithError(err).Fatal("cannot open new task terminal") + tm.setTaskState(t, api.TaskState_closed) + continue + } + + taskLog = taskLog.WithField("terminal", resp.Alias) + terminal, ok := tm.terminalService.Mux.Get(resp.Alias) + if !ok { + taskLog.Fatal("cannot find a task terminal") + tm.setTaskState(t, api.TaskState_closed) + continue + } + + taskLog.Info("task terminal has been started") + tm.updateState(func() *task { + t.Terminal = resp.Alias + t.State = api.TaskState_running + return t + }) + + go func(t *task) { + terminal.Command.Process.Wait() + taskLog.Info("task terminal has been closed") + tm.setTaskState(t, api.TaskState_closed) + }(t) + + if runContext.headless { + tm.watch(t, terminal) + } + terminal.PTY.Write([]byte(t.command + "\r\n")) + } + + if runContext.headless { + tm.report(ctx) + } +} + +func (task *task) getCommand(context *runContext) string { + commands := task.getCommands(context) + command := composeCommand(composeCommandOptions{ + commands: commands, + format: "{\r\n%s\r\n}", + sep: " && ", + }) + + if strings.TrimSpace(command) == "" { + return "" + } + + if context.headless { + // it's important that prebuild tasks exit eventually + // also, we need to save the log output in the workspace + return command + "; exit" + } + + histfile := "/workspace/.gitpod/cmd-" + task.Id + histfileCommands := commands + if context.contentSource == csapi.WorkspaceInitFromPrebuild { + histfileCommands = []*string{task.config.Before, task.config.Init, task.config.Prebuild, task.config.Command} + } + err := ioutil.WriteFile(histfile, []byte(composeCommand(composeCommandOptions{ + commands: histfileCommands, + format: "%s\r\n", + })), 0644) + if err != nil { + log.WithField("histfile", histfile).WithError(err).Fatal("cannot write histfile") + return command + } + // the space at beginning of the HISTFILE command prevents the HISTFILE command itself from appearing in + // the bash history. + return " HISTFILE=" + histfile + " history -r; " + command +} + +func (task *task) getCommands(context *runContext) []*string { + if context.headless { + // prebuild + return []*string{task.config.Before, task.config.Init, task.config.Prebuild} + } + if context.contentSource == csapi.WorkspaceInitFromPrebuild { + // prebuilt + prebuildLogFileName := task.prebuildLogFileName() + legacyPrebuildLogFileName := "/workspace/.prebuild-log-" + task.Id + printlogs := "[ -r " + legacyPrebuildLogFileName + " ] && cat " + legacyPrebuildLogFileName + "; [ -r " + prebuildLogFileName + " ] && cat " + prebuildLogFileName + "; true" + return []*string{task.config.Before, &printlogs, task.config.Command} + } + if context.contentSource == csapi.WorkspaceInitFromBackup { + // restart + return []*string{task.config.Before, task.config.Command} + } + // init + return []*string{task.config.Before, task.config.Init, task.config.Command} + +} + +func (task *task) prebuildLogFileName() string { + return "/workspace/.gitpod/prebuild-log-" + task.Id +} + +func (tm *tasksManager) watch(task *task, terminal *terminal.Term) { + var ( + workspaceLog = log.WithField("component", "workspace") + stdout = terminal.Stdout.Listen() + start = time.Now() + ) + task.prebuildChan = make(chan bool) + go func() { + success := false + defer func() { + task.prebuildChan <- success + }() + + fileName := task.prebuildLogFileName() + file, err := os.Create(fileName) + if err != nil { + workspaceLog.WithError(err).Fatal("cannot create a prebuild log file") + return + } + defer file.Close() + + fileWriter := bufio.NewWriter(file) + + workspaceLog.Info("Writing build output to " + fileName) + + buf := make([]byte, 4096) + for { + n, err := stdout.Read(buf) + if err == io.EOF { + elapsed := time.Since(start) + duration := "" + if elapsed >= 1*time.Minute { + elapsedInMinutes := strconv.Itoa(int(elapsed.Minutes())) + duration = "๐ŸŽ‰ You just saved " + elapsedInMinutes + " minute" + if elapsedInMinutes != "1" { + duration += "s" + } + duration += " of watching your code build.\r\n" + } + data := string(buf[:n]) + fileWriter.Write(buf[:n]) + workspaceLog.WithField("type", "workspaceTaskOutput").WithField("data", data).Info() + + endMessage := "\r\n๐ŸŒ This task ran as part of a workspace prebuild.\r\n" + duration + "\r\n" + fileWriter.WriteString(endMessage) + workspaceLog.WithField("type", "workspaceTaskOutput").WithField("data", endMessage).Info() + + fileWriter.Flush() + success = true + break + } + if err != nil { + workspaceLog.WithError(err).Fatal("cannot read from a task terminal") + return + } + data := string(buf[:n]) + fileWriter.Write(buf[:n]) + workspaceLog.WithField("type", "workspaceTaskOutput").WithField("data", data).Info() + } + }() +} + +func (tm *tasksManager) report(ctx context.Context) { + workspaceLog := log.WithField("component", "workspace") + ok := true + for _, task := range tm.tasks { + if task.prebuildChan != nil { + select { + case <-ctx.Done(): + return + case prebuildOk := <-task.prebuildChan: + if !prebuildOk { + ok = false + } + } + } + } + workspaceLog.WithField("type", "workspaceTaskOutput").WithField("data", "๐Ÿš› uploading prebuilt workspace").Info() + if !ok { + workspaceLog.WithField("type", "workspaceTaskFailed").WithField("error", "one of the tasks failed with non-zero exit code").Info() + return + } + workspaceLog.WithField("type", "workspaceTaskDone").Info() +} + +type composeCommandOptions struct { + commands []*string + format string + sep string +} + +func composeCommand(options composeCommandOptions) string { + var commands []string + for _, command := range options.commands { + if command != nil && strings.TrimSpace(*command) != "" { + commands = append(commands, fmt.Sprintf(options.format, *command)) + } + } + return strings.Join(commands, options.sep) +} diff --git a/components/supervisor/pkg/terminal/ring-buffer.go b/components/supervisor/pkg/terminal/ring-buffer.go new file mode 100644 index 00000000000000..3e5a147fa7d05b --- /dev/null +++ b/components/supervisor/pkg/terminal/ring-buffer.go @@ -0,0 +1,96 @@ +// Copyright (c) 2020 TypeFox GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package terminal + +import ( + "fmt" +) + +// RingBuffer implements a ring buffer. It is a fixed size, +// and new writes overwrite older data, such that for a buffer +// of size N, for any amount of writes, only the last N bytes +// are retained. +type RingBuffer struct { + data []byte + size int64 + writeCursor int64 + written int64 +} + +// NewRingBuffer creates a new buffer of a given size. The size +// must be greater than 0. +func NewRingBuffer(size int64) (*RingBuffer, error) { + if size <= 0 { + return nil, fmt.Errorf("Size must be positive") + } + + b := &RingBuffer{ + size: size, + data: make([]byte, size), + } + return b, nil +} + +// Write writes up to len(buf) bytes to the internal ring, +// overriding older data if necessary. +func (b *RingBuffer) Write(buf []byte) (int, error) { + // Account for total bytes written + n := len(buf) + b.written += int64(n) + + // If the buffer is larger than ours, then we only care + // about the last size bytes anyways + if int64(n) > b.size { + buf = buf[int64(n)-b.size:] + } + + // Copy in place + remain := b.size - b.writeCursor + copy(b.data[b.writeCursor:], buf) + if int64(len(buf)) > remain { + copy(b.data, buf[remain:]) + } + + // Update location of the cursor + b.writeCursor = ((b.writeCursor + int64(len(buf))) % b.size) + return n, nil +} + +// Size returns the size of the buffer +func (b *RingBuffer) Size() int64 { + return b.size +} + +// TotalWritten provides the total number of bytes written +func (b *RingBuffer) TotalWritten() int64 { + return b.written +} + +// Bytes provides a slice of the bytes written. This +// slice should not be written to. +func (b *RingBuffer) Bytes() []byte { + switch { + case b.written >= b.size && b.writeCursor == 0: + return b.data + case b.written > b.size: + out := make([]byte, b.size) + copy(out, b.data[b.writeCursor:]) + copy(out[b.size-b.writeCursor:], b.data[:b.writeCursor]) + return out + default: + return b.data[:b.writeCursor] + } +} + +// Reset resets the buffer so it has no content. +func (b *RingBuffer) Reset() { + b.writeCursor = 0 + b.written = 0 +} + +// String returns the contents of the buffer as a string +func (b *RingBuffer) String() string { + return string(b.Bytes()) +} diff --git a/components/supervisor/pkg/terminal/service.go b/components/supervisor/pkg/terminal/service.go index 0b46ced912f912..3692c327d1ef5f 100644 --- a/components/supervisor/pkg/terminal/service.go +++ b/components/supervisor/pkg/terminal/service.go @@ -35,7 +35,7 @@ type MuxTerminalService struct { DefaultWorkdir string LoginShell []string - tokens map[*term]string + tokens map[*Term]string } // RegisterGRPC registers a gRPC service @@ -53,6 +53,9 @@ func (srv *MuxTerminalService) Open(ctx context.Context, req *api.OpenTerminalRe cmd := exec.Command(srv.LoginShell[0], srv.LoginShell[1:]...) cmd.Dir = srv.DefaultWorkdir cmd.Env = append(os.Environ(), "TERM=xterm-color") + for key, value := range req.Env { + cmd.Env = append(cmd.Env, key+"="+value) + } alias, err := srv.Mux.Start(cmd) if err != nil { return nil, status.Error(codes.Internal, err.Error()) @@ -71,6 +74,15 @@ func (srv *MuxTerminalService) Open(ctx context.Context, req *api.OpenTerminalRe }, nil } +// Close closes a terminal for the given alias +func (srv *MuxTerminalService) Close(ctx context.Context, req *api.CloseTerminalRequest) (*api.CloseTerminalResponse, error) { + err := srv.Mux.Close(req.Alias) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + return &api.CloseTerminalResponse{}, nil +} + // List lists all open terminals func (srv *MuxTerminalService) List(ctx context.Context, req *api.ListTerminalsRequest) (*api.ListTerminalsResponse, error) { srv.Mux.mu.RLock() diff --git a/components/supervisor/pkg/terminal/terminal.go b/components/supervisor/pkg/terminal/terminal.go index 76b75c78419903..35f8c3ad08433b 100644 --- a/components/supervisor/pkg/terminal/terminal.go +++ b/components/supervisor/pkg/terminal/terminal.go @@ -21,16 +21,24 @@ import ( // NewMux creates a new terminal mux func NewMux() *Mux { return &Mux{ - terms: make(map[string]*term), + terms: make(map[string]*Term), } } // Mux can mux pseudo-terminals type Mux struct { - terms map[string]*term + terms map[string]*Term mu sync.RWMutex } +// Get returns a terminal for the given alias +func (m *Mux) Get(alias string) (*Term, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + term, ok := m.terms[alias] + return term, ok +} + // Start starts a new command in its own pseudo-terminal and returns an alias // for that pseudo terminal. func (m *Mux) Start(cmd *exec.Cmd) (alias string, err error) { @@ -94,16 +102,29 @@ func (m *Mux) Close(alias string) error { return nil } -func newTerm(pty *os.File, cmd *exec.Cmd) (*term, error) { +// terminalBacklogSize is the number of bytes of output we'll store in RAM for each terminal. +// The higher this number is, the better the UX, but the higher the resource requirements are. +// For now we assume an average of five terminals per workspace, which makes this consume 1MiB of RAM. +const terminalBacklogSize = 256 << 10 + +func newTerm(pty *os.File, cmd *exec.Cmd) (*Term, error) { token, err := uuid.NewRandom() if err != nil { return nil, err } - res := &term{ + recorder, err := NewRingBuffer(terminalBacklogSize) + if err != nil { + return nil, err + } + + res := &Term{ PTY: pty, Command: cmd, - Stdout: &multiWriter{listener: make(map[*multiWriterListener]struct{})}, + Stdout: &multiWriter{ + listener: make(map[*multiWriterListener]struct{}), + recorder: recorder, + }, StarterToken: token.String(), } @@ -111,7 +132,8 @@ func newTerm(pty *os.File, cmd *exec.Cmd) (*term, error) { return res, nil } -type term struct { +// Term is a pseudo-terminal +type Term struct { PTY *os.File Command *exec.Cmd Title string @@ -123,8 +145,11 @@ type term struct { // multiWriter is like io.MultiWriter, except that we can listener at runtime. type multiWriter struct { closed bool - mu sync.Mutex + mu sync.RWMutex listener map[*multiWriterListener]struct{} + // ring buffer to record last 256kb of pty output + // new listener is initialized with the latest recodring first + recorder *RingBuffer } type multiWriterListener struct { @@ -134,6 +159,7 @@ type multiWriterListener struct { once sync.Once closeChan chan struct{} cchan chan []byte + done chan struct{} } func (l *multiWriterListener) Close() error { @@ -156,19 +182,29 @@ func (mw *multiWriter) Listen() *multiWriterListener { defer mw.mu.Unlock() r, w := io.Pipe() - cchan, closeChan := make(chan []byte), make(chan struct{}, 1) + cchan, done, closeChan := make(chan []byte), make(chan struct{}, 1), make(chan struct{}, 1) res := &multiWriterListener{ Reader: r, cchan: cchan, + done: done, closeChan: closeChan, } go func() { + mw.mu.RLock() + recording := mw.recorder.Bytes() + mw.mu.RUnlock() + w.Write(recording) + // copy bytes from channel to writer. // Note: we close the writer independently of the write operation s.t. we don't // block the closing because the write's blocking. for b := range cchan { - _, err := w.Write(b) + n, err := w.Write(b) + done <- struct{}{} + if err == nil && n != len(b) { + err = io.ErrShortWrite + } if err != nil { log.WithError(err).Error("terminal listener droped out") res.Close() @@ -195,6 +231,8 @@ func (mw *multiWriter) Write(p []byte) (n int, err error) { mw.mu.Lock() defer mw.mu.Unlock() + mw.recorder.Write(p) + for lstr := range mw.listener { if lstr.closed { continue @@ -205,6 +243,12 @@ func (mw *multiWriter) Write(p []byte) (n int, err error) { case <-time.After(5 * time.Second): lstr.Close() } + + select { + case <-lstr.done: + case <-time.After(5 * time.Second): + lstr.Close() + } } return len(p), nil } diff --git a/components/supervisor/pkg/terminal/terminal_test.go b/components/supervisor/pkg/terminal/terminal_test.go new file mode 100644 index 00000000000000..a84faf4e9959b0 --- /dev/null +++ b/components/supervisor/pkg/terminal/terminal_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2020 TypeFox GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package terminal + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + "time" + + "github.com/gitpod-io/gitpod/supervisor/api" + "github.com/google/go-cmp/cmp" +) + +func TestTerminals(t *testing.T) { + tests := []struct { + Desc string + Stdin []string + Delay time.Duration + Expectation func(terminal *Term) string + }{ + { + Desc: "recorded output should be equals read output", + Stdin: []string{ + "echo \"yarn\"", + "echo \"gp sync-done init\"", + "echo \"yarn --cwd theia-training watch\"", + "history", + }, + Delay: 1000 * time.Millisecond, + Expectation: func(terminal *Term) string { + return string(terminal.Stdout.recorder.Bytes()) + }, + }, + } + for _, test := range tests { + t.Run(test.Desc, func(t *testing.T) { + terminalService := NewMuxTerminalService(NewMux()) + resp, err := terminalService.Open(context.Background(), &api.OpenTerminalRequest{}) + if err != nil { + t.Fatal(err) + } + terminal, ok := terminalService.Mux.Get(resp.Alias) + if !ok { + t.Fatal("no terminal") + } + stdoutOutput := bytes.NewBuffer(nil) + go io.Copy(stdoutOutput, terminal.Stdout.Listen()) + + for _, stdin := range test.Stdin { + terminal.PTY.Write([]byte(stdin + "\r\n")) + time.Sleep(test.Delay) + } + + err = terminalService.Mux.Close(resp.Alias) + if err != nil { + t.Fatal(err) + } + + expectation := strings.Split(test.Expectation(terminal), "\r\n") + actual := strings.Split(string(stdoutOutput.Bytes()), "\r\n") + if diff := cmp.Diff(expectation, actual); diff != "" { + t.Errorf("unexpected output (-want +got):\n%s", diff) + } + }) + } +} diff --git a/components/theia/packages/gitpod-extension/package.json b/components/theia/packages/gitpod-extension/package.json index f183c544768a1f..ba744abd7c804d 100644 --- a/components/theia/packages/gitpod-extension/package.json +++ b/components/theia/packages/gitpod-extension/package.json @@ -11,6 +11,9 @@ "styles/*" ], "dependencies": { + "@gitpod/gitpod-protocol": "0.1.5", + "@gitpod/supervisor-api-grpc": "0.1.5", + "@grpc/grpc-js": "^1.1.5", "@octokit/rest": "~16.43.1", "@theia/core": "next", "@theia/filesystem": "next", @@ -27,13 +30,12 @@ "@theia/userstorage": "next", "@theia/vsx-registry": "next", "@theia/workspace": "next", - "@gitpod/gitpod-protocol": "0.1.5", - "@gitpod/supervisor-api-grpc": "0.1.5", "bitbucket": "^2.1.0", "decompress": "^4.2.0", "diff": "^3.4.0", "filenamify": "^4.1.0", "gitlab": "^14.2.2", + "google-protobuf": "^3.8.0-rc.1", "install": "^0.13.0", "js-cookie": "^2.2.1", "json-query": "^2.2.2", @@ -43,12 +45,11 @@ "octicons": "^7.1.0", "ovsx": "0.1.0-next.e000fdb", "prom-client": "^10.2.0", + "ps-tree": "^1.2.0", "react": "^16.4.1", "react-dom": "^16.4.1", "requestretry": "^4.0.0", - "yaml": "^1.5.1", - "google-protobuf": "^3.8.0-rc.1", - "@grpc/grpc-js": "^1.1.5" + "yaml": "^1.5.1" }, "devDependencies": { "@types/chai": "^4.1.2", @@ -57,15 +58,16 @@ "@types/filenamify": "^2.0.2", "@types/js-cookie": "^2.2.4", "@types/json-query": "^2.2.0", + "@types/ps-tree": "^1.1.0", "@types/requestretry": "^1.12.4", "@types/tmp": "^0.2.0", "@types/yaml": "^1.0.2", "chai": "^4.1.2", + "mocha": "^5.0.0", "mocha-typescript": "^1.1.17", "rimraf": "^2.6.2", - "typescript": "^3.9.3", - "mocha": "^5.0.0", - "ts-node": "<7.0.0" + "ts-node": "<7.0.0", + "typescript": "^3.9.3" }, "scripts": { "prepare": "yarn run clean && yarn run build", diff --git a/components/theia/packages/gitpod-extension/src/browser/gitpod-frontend-module.ts b/components/theia/packages/gitpod-extension/src/browser/gitpod-frontend-module.ts index fc57b0e3b47ab5..6b062f84b98ce6 100644 --- a/components/theia/packages/gitpod-extension/src/browser/gitpod-frontend-module.ts +++ b/components/theia/packages/gitpod-extension/src/browser/gitpod-frontend-module.ts @@ -45,7 +45,6 @@ import { GitpodPreviewLinkNormalizer } from "./user-message/GitpodPreviewLinkNor import { PreviewLinkNormalizer } from "@theia/preview/lib/browser/preview-link-normalizer"; import { GitpodMenuModelRegistry } from "./gitpod-menu"; import { WaitForContentContribution } from './waitfor-content-contribution'; -import { ContentReadyServiceServer, ContentReadyService } from '../common/content-ready-service'; import { GitpodWebSocketConnectionProvider } from './gitpod-ws-connection-provider'; import { GitHostWatcher } from './git-host-watcher'; import { GitpodExternalUriService } from './gitpod-external-uri-service'; @@ -65,6 +64,8 @@ import { GitpodUserStorageProvider } from './gitpod-user-storage-provider'; import { Emitter } from '@theia/core/lib/common/event'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { GitpodTaskContribution } from './gitpod-task-contribution'; +import { GitpodTaskServer, gitpodTaskServicePath } from '../common/gitpod-task-protocol'; @injectable() class GitpodFrontendApplication extends FrontendApplication { @@ -125,6 +126,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(GitpodOpenContext); rebind(InitialGitHubDataProvider).toService(GitpodOpenContext); + bind(GitpodTaskServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, gitpodTaskServicePath)).inSingletonScope(); + bind(FrontendApplicationContribution).to(GitpodTaskContribution).inSingletonScope(); + bind(GitpodShareWidget).toSelf().inSingletonScope(); bind(GitpodShareDialog).toSelf().inSingletonScope(); bind(GitpodShareDialogProps).toConstantValue({ title: 'Share Workspace' }); @@ -159,9 +163,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(CliServiceContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(CliServiceContribution); - bind(ContentReadyServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, ContentReadyService.SERVICE_PATH)).inSingletonScope(); - bind(ContentReadyService).toSelf().inSingletonScope(); - bind(GitpodPortsService).toSelf().inSingletonScope(); bind(UserMessageContribution).toSelf().inSingletonScope(); diff --git a/components/theia/packages/gitpod-extension/src/browser/gitpod-shell-layout-restorer.ts b/components/theia/packages/gitpod-extension/src/browser/gitpod-shell-layout-restorer.ts index 4edf8973a52912..a55452dca0c9c9 100644 --- a/components/theia/packages/gitpod-extension/src/browser/gitpod-shell-layout-restorer.ts +++ b/components/theia/packages/gitpod-extension/src/browser/gitpod-shell-layout-restorer.ts @@ -4,17 +4,11 @@ * See License-AGPL.txt in the project root for license information. */ -import { ShellLayoutRestorer, WidgetManager, StorageService, FrontendApplication, ApplicationShell } from "@theia/core/lib/browser"; -import { injectable, inject } from "inversify"; import { ILogger } from "@theia/core"; -import { GitpodServiceProvider } from "./gitpod-service-provider"; -import { GitpodInfoService, TerminalProcessInfo } from "../common/gitpod-info"; +import { FrontendApplication, ShellLayoutRestorer, StorageService, WidgetManager } from "@theia/core/lib/browser"; import { Deferred } from "@theia/core/lib/common/promise-util"; -import { TerminalWidget } from "@theia/terminal/lib/browser/base/terminal-widget"; -import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions } from '@theia/terminal/lib/browser/terminal-widget-impl'; -import { WorkspaceService } from "@theia/workspace/lib/browser"; -import { TaskConfig } from "@gitpod/gitpod-protocol"; -import { GitpodTerminalWidget } from "./gitpod-terminal-widget"; +import { inject, injectable } from "inversify"; +import { GitpodServiceProvider } from "./gitpod-service-provider"; import { getWorkspaceID } from "./utils"; const workspaceIdPlaceHolder = '<ยงwsid$>'; @@ -28,16 +22,13 @@ export class GitpodLayoutRestorer extends ShellLayoutRestorer { private layoutData = new Deferred(); protected application: FrontendApplication; - private cwd: string | undefined; constructor( @inject(WidgetManager) protected widgetManager: WidgetManager, @inject(ILogger) protected logger: ILogger, @inject(StorageService) protected storageService: StorageService, - @inject(GitpodServiceProvider) protected serviceProvider: GitpodServiceProvider, - @inject(GitpodInfoService) protected infoProvider: GitpodInfoService, - @inject(WorkspaceService) protected workspaceService: WorkspaceService - ) { + @inject(GitpodServiceProvider) protected serviceProvider: GitpodServiceProvider + ) { super(widgetManager, logger, storageService); const service = this.serviceProvider.getService() const workspaceId = getWorkspaceID(); @@ -46,7 +37,7 @@ export class GitpodLayoutRestorer extends ShellLayoutRestorer { const replaced = layout && replaceAll(layout, workspaceIdPlaceHolder, workspaceId); this.layoutData.resolve(replaced); }); - + } public captureLayout(): string { @@ -58,113 +49,17 @@ export class GitpodLayoutRestorer extends ShellLayoutRestorer { public async restoreLayout(app: FrontendApplication): Promise { this.application = app; - const restored = await super.restoreLayout(app); - try { - if (!restored) { - const serializedLayoutData = await this.layoutData.promise; - if (!serializedLayoutData) { - return false; - } - const layoutData = await this.inflate(serializedLayoutData); - await app.shell.setLayoutData(layoutData); - return true; + const restored = await super.restoreLayout(app); + if (!restored) { + const serializedLayoutData = await this.layoutData.promise; + if (!serializedLayoutData) { + return false; } - return restored; - } finally { - this.initializeTerminals(app.shell); + const layoutData = await this.inflate(serializedLayoutData); + await app.shell.setLayoutData(layoutData); + return true; } + return restored; } - - protected async initializeTerminals(shell: ApplicationShell) { - const infos = await this.infoProvider.getTerminalProcessInfos(); - const roots = await this.workspaceService.roots; - this.cwd = roots[0] && roots[0].resource.path.toString() || undefined; - this.doInitializeTerminals(shell, infos); - } - - protected doInitializeTerminals(shell: ApplicationShell, infos: TerminalProcessInfo[]) { - interface TerminalConnection { - processId?: number, - info?: TerminalProcessInfo, - terminal?: TerminalWidget - } - const terminals: GitpodTerminalWidget[] = shell.widgets.filter(w => w instanceof GitpodTerminalWidget).map(t => t as GitpodTerminalWidget); - const usedTerminals = new Set(); - const allConnections : TerminalConnection[] = []; - - for (const info of infos) { - allConnections.push({ - processId: info.processId, - info - }) - } - // associate direct matches by processid - for (const terminal of terminals) { - const processId = (terminal as GitpodTerminalWidget).getTerminalId(); - if (processId !== undefined) { - let connection = allConnections.find(i => i.processId === processId); - if (connection && !connection.terminal) { - connection.terminal = terminal; - usedTerminals.add(terminal); - } - } - } - // associate other terminals - for (const terminal of terminals) { - if (!usedTerminals.has(terminal)) { - const connection = allConnections.find(con => !con.terminal) - if (connection) { - connection.terminal = terminal; - } else { - const processId = (terminal as GitpodTerminalWidget).getTerminalId(); - allConnections.push({ - processId, - terminal - }); - } - } - } - // start all - for (const c of allConnections) { - if (c.info && c.terminal) { - c.terminal.start(c.info.processId); - c.terminal.title.label = this.computeTitle(c.info.task); - } else if (c.info) { - const options: ApplicationShell.WidgetOptions = { - area: c.info.task.openIn || 'bottom', - mode: c.info.task.openMode || 'tab-after' - } - const title = this.computeTitle(c.info.task); - this.newTerminalFor(shell, c.info.processId, options, title); - } else if (c.terminal) { - c.terminal.start(); - c.terminal.title.label = this.computeTitle(); - } - } - // if there is no terminal at all, lets start one - if (terminals.length === 0 && infos.length === 0) { - const title = this.computeTitle(); - this.newTerminalFor(shell, 0, { area: 'bottom' }, title); - } - } - - protected computeTitle(task?: TaskConfig): string { - if (task && task.name) { - return task.name; - } - return this.cwd || 'Terminal'; - } - - protected async newTerminalFor(shell: ApplicationShell, processId: number, openOptions: ApplicationShell.WidgetOptions, title: string): Promise { - const widget = await this.widgetManager.getOrCreateWidget(TERMINAL_WIDGET_FACTORY_ID, { - title, - useServerTitle: false, - created: new Date().toISOString() - }); - shell.addWidget(widget, openOptions); - shell.activateWidget(widget.id); - // If a backend process with this id is there it will use it. Or create a fresh one. - widget.start(processId); - } } diff --git a/components/theia/packages/gitpod-extension/src/browser/gitpod-task-contribution.ts b/components/theia/packages/gitpod-extension/src/browser/gitpod-task-contribution.ts new file mode 100644 index 00000000000000..9f0e28efd3b9da --- /dev/null +++ b/components/theia/packages/gitpod-extension/src/browser/gitpod-task-contribution.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2020 TypeFox GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { injectable, inject, postConstruct } from 'inversify'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { TerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { GitpodTerminalWidget } from './gitpod-terminal-widget'; +import { GitpodTaskState, GitpodTaskServer, GitpodTask } from '../common/gitpod-task-protocol'; +import { IBaseTerminalServer } from '@theia/terminal/lib/common/base-terminal-protocol'; +import { Emitter } from '@theia/core'; + +interface GitpodTaskTerminalWidget extends GitpodTerminalWidget { + readonly kind: 'gitpod-task' + /** undefined if not running */ + remoteTerminal?: string +} +namespace GitpodTaskTerminalWidget { + const idPrefix = 'gitpod-task-terminal' + export function is(terminal: TerminalWidget): terminal is GitpodTaskTerminalWidget { + return terminal.kind === 'gitpod-task'; + } + export function toTerminalId(id: string): string { + return idPrefix + ':' + id; + } + export function getTaskId(terminal: GitpodTaskTerminalWidget): string { + return terminal.id.split(':')[1]; + } +} + +@injectable() +export class GitpodTaskContribution implements FrontendApplicationContribution { + + @inject(TerminalFrontendContribution) + private readonly terminals: TerminalFrontendContribution; + + @inject(GitpodTaskServer) + private readonly server: GitpodTaskServer; + + private readonly onDidChangeEmitter = new Emitter(); + private readonly onDidChange = this.onDidChangeEmitter.event; + + private readonly taskTerminals = new Map(); + + @postConstruct() + protected init(): void { + // register client before connection is opened + this.server.setClient({ + onDidChange: ({ updated }) => this.onDidChangeEmitter.fire(updated) + }); + this.terminals.onDidCreateTerminal(terminal => { + if (GitpodTaskTerminalWidget.is(terminal)) { + this.taskTerminals.set(terminal.id, terminal); + terminal.onDidDispose(() => + this.taskTerminals.delete(terminal.id) + ); + terminal.onTerminalDidClose(() => { + if (terminal.remoteTerminal) { + fetch(window.location.protocol + '//' + window.location.host + '/_supervisor/v1/terminal/close/' + terminal.remoteTerminal, { + credentials: "include" + }) + } + }); + } + }); + } + + async onDidInitializeLayout(): Promise { + const tasks = await this.server.getTasks(); + let ref: TerminalWidget | undefined; + for (const task of tasks) { + if (task.state == GitpodTaskState.CLOSED) { + continue; + } + try { + const id = GitpodTaskTerminalWidget.toTerminalId(task.id); + let terminal = this.taskTerminals.get(id); + if (!terminal) { + terminal = await this.terminals.newTerminal({ + id, + kind: 'gitpod-task', + title: task.presentation!.name, + useServerTitle: false + }) as GitpodTaskTerminalWidget; + await terminal.start(); + this.terminals.activateTerminal(terminal, { + ref, + area: task.presentation.openIn || 'bottom', + mode: task.presentation.openMode || 'tab-after' + }); + } else if (!IBaseTerminalServer.validateId(terminal.terminalId)) { + await terminal.start(); + } + if (terminal) { + ref = terminal; + } + } catch (e) { + console.error('Failed to start Gitpod task terminal:', e); + } + } + this.updateTerminals(tasks); + this.onDidChange(tasks => this.updateTerminals(tasks)); + + // if there is no terminal at all, lets start one + if (!this.terminals.all.length) { + const terminal = await this.terminals.newTerminal({}); + terminal.start(); + this.terminals.open(terminal); + } + } + + protected async updateTerminals(tasks: GitpodTask[]): Promise { + for (const task of tasks) { + try { + const id = GitpodTaskTerminalWidget.toTerminalId(task.id); + const terminal = this.taskTerminals.get(id); + if (!terminal) { + continue; + } + if (task.state === GitpodTaskState.CLOSED) { + delete terminal.remoteTerminal; + terminal.dispose(); + continue; + } + if (task.state !== GitpodTaskState.RUNNING) { + continue; + } + terminal.remoteTerminal = task.terminal; + if (task.terminal) { + await this.server.attach({ + terminalId: terminal.terminalId, + remoteTerminal: task.terminal + }); + } + } catch (e) { + console.error('Failed to update Gitpod task terminal:', e); + } + } + } +} \ No newline at end of file diff --git a/components/theia/packages/gitpod-extension/src/browser/gitpod-terminal-widget.ts b/components/theia/packages/gitpod-extension/src/browser/gitpod-terminal-widget.ts index 4262060dd1edf0..cf9d2e2de662d3 100644 --- a/components/theia/packages/gitpod-extension/src/browser/gitpod-terminal-widget.ts +++ b/components/theia/packages/gitpod-extension/src/browser/gitpod-terminal-widget.ts @@ -34,27 +34,6 @@ export class GitpodTerminalWidget extends TerminalWidgetImpl { } } - // on restoreState, let's not immediately start the terminal but keep the state and wait for an external start call - private oldState: any; - restoreState(oldState: object) { - this.oldState = oldState; - } - - public getTerminalId(): number | undefined { - if (this.terminalId) { - return this.terminalId; - } - return this.oldState && this.oldState.terminalId; - } - - async start(id?: number): Promise { - if (id === undefined && this.oldState !== undefined) { - super.restoreState(this.oldState); - return this.terminalId; - } - return super.start(id); - } - } export namespace GitpodTerminalWidget { @@ -95,7 +74,7 @@ export namespace GitpodTerminalWidget { return url.port; } - export function toURL(uri: string) : URL | undefined { + export function toURL(uri: string): URL | undefined { let url; try { if (!uri.startsWith("http")) { diff --git a/components/theia/packages/gitpod-extension/src/common/content-ready-service.ts b/components/theia/packages/gitpod-extension/src/common/content-ready-service.ts deleted file mode 100644 index 24248821de276f..00000000000000 --- a/components/theia/packages/gitpod-extension/src/common/content-ready-service.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2020 TypeFox GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License-AGPL.txt in the project root for license information. - */ - -import { inject, injectable, postConstruct } from "inversify"; -import { JsonRpcServer } from "@theia/core"; -import { Deferred } from "@theia/core/lib/common/promise-util"; - -export interface ContentReadyServiceClient { - onContentReady(): Promise; -} - -export const ContentReadyServiceServer = Symbol('ContentReadyServiceServer'); -export interface ContentReadyServiceServer extends JsonRpcServer { - markContentReady(): void; - isContentReady(): boolean; - disposeClient(client: ContentReadyServiceClient): void; -} - -@injectable() -export class ContentReadyService { - - @inject(ContentReadyServiceServer) private readonly server: ContentReadyServiceServer; - - protected contentReady = new Deferred(); - - get readyPromise() { - return this.contentReady.promise; - } - - @postConstruct() - init(): void { - const onContentReady: () => Promise = () => { - this.contentReady.resolve(); - return this.contentReady.promise; - }; - this.server.setClient({ onContentReady }); - if (this.server.isContentReady()) { - // content was ready before we registered the listener - onContentReady(); - } - } -} - -export namespace ContentReadyService { - export const SERVICE_PATH = '/services/content-ready-service'; -} diff --git a/components/theia/packages/gitpod-extension/src/common/gitpod-info.ts b/components/theia/packages/gitpod-extension/src/common/gitpod-info.ts index 5ba57a2db0c0e0..5708408aa7793d 100644 --- a/components/theia/packages/gitpod-extension/src/common/gitpod-info.ts +++ b/components/theia/packages/gitpod-extension/src/common/gitpod-info.ts @@ -4,24 +4,14 @@ * See License-AGPL.txt in the project root for license information. */ -import { TaskConfig } from "@gitpod/gitpod-protocol"; - export const gitpodInfoPath = '/services/gitpodInfoPath'; export const GitpodInfoService = Symbol('GitpodInfoService'); export interface GitpodInfoService { getInfo() : Promise - getTerminalProcessInfos(): Promise; -} - -export interface TerminalProcessInfo { - processId: number - task: TaskConfig } -export type SnapshotBucketId = string; - export interface GitpodInfo { workspaceId: string; instanceId: string; @@ -29,8 +19,3 @@ export interface GitpodInfo { interval: number; repoRoot: string; } - -export namespace GitpodInfo { - export const SERVICE_PATH = '/gitpod/info'; - export const TERMINAL_INFOS_PATH = '/gitpod/terminalnfos'; -} diff --git a/components/theia/packages/gitpod-extension/src/common/gitpod-task-protocol.ts b/components/theia/packages/gitpod-extension/src/common/gitpod-task-protocol.ts new file mode 100644 index 00000000000000..f62c5392d29db2 --- /dev/null +++ b/components/theia/packages/gitpod-extension/src/common/gitpod-task-protocol.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2020 TypeFox GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { JsonRpcServer } from "@theia/core"; +import type { ApplicationShell } from "@theia/core/lib/browser"; + +export const enum GitpodTaskState { + OPENING = 0, + RUNNING = 1, + CLOSED = 2, +} + +export interface GitpodTask { + id: string + state: GitpodTaskState + terminal?: string + presentation: { + name: string + openIn?: ApplicationShell.WidgetOptions['area'] + openMode?: ApplicationShell.WidgetOptions['mode'] + } +} + +export const gitpodTaskServicePath = "/services/gitpodTasks"; + +export const GitpodTaskServer = Symbol('GitpodTaskServer'); +export interface GitpodTaskServer extends JsonRpcServer { + getTasks(): Promise; + attach(params: AttachTaskTerminalParams): Promise +} +export interface GitpodTaskClient { + onDidChange(event: DidChangeGitpodTasksEvent): void +} + +export interface DidChangeGitpodTasksEvent { + updated: GitpodTask[] +} + +export interface AttachTaskTerminalParams { + terminalId: number + remoteTerminal: string +} \ No newline at end of file diff --git a/components/theia/packages/gitpod-extension/src/node/content-ready-service-server.ts b/components/theia/packages/gitpod-extension/src/node/content-ready-service-server.ts deleted file mode 100644 index 8612c76d855519..00000000000000 --- a/components/theia/packages/gitpod-extension/src/node/content-ready-service-server.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2020 TypeFox GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License-AGPL.txt in the project root for license information. - */ - -import { ContentReadyServiceServer, ContentReadyServiceClient } from "../common/content-ready-service"; -import { injectable } from "inversify"; - -@injectable() -export class ContentReadyServiceServerImpl implements ContentReadyServiceServer { - protected contentReady: boolean = false; - protected clients: ContentReadyServiceClient[] = []; - - setClient(client: ContentReadyServiceClient): void { - this.clients.push(client); - - if (this.contentReady) { - client.onContentReady(); - } - } - - disposeClient(client: ContentReadyServiceClient): void { - const idx = this.clients.indexOf(client); - if (idx > -1) { - this.clients.splice(idx, 1); - } - - if (this.clients.length == 0) { - this.dispose(); - } - } - - dispose(): void { - - } - - markContentReady(): void { - this.contentReady = true; - this.clients.forEach(c => c.onContentReady()); - } - - isContentReady(): boolean { - return this.contentReady; - } - -} \ No newline at end of file diff --git a/components/theia/packages/gitpod-extension/src/node/gitpod-backend-module.ts b/components/theia/packages/gitpod-extension/src/node/gitpod-backend-module.ts index dbd7ba3803f2c4..b25a5a57c6e9fb 100644 --- a/components/theia/packages/gitpod-extension/src/node/gitpod-backend-module.ts +++ b/components/theia/packages/gitpod-extension/src/node/gitpod-backend-module.ts @@ -11,7 +11,6 @@ import { ContainerModule } from "inversify"; import { GitPodExpressService } from './gitpod-express-service'; import { BackendApplicationContribution } from "@theia/core/lib/node/backend-application"; -import { GitpodTaskStarter, TheiaLocalTaskStarter, WorkspaceReadyTaskStarter } from './gitpod-task-starter'; import { GitpodFileParser } from '@gitpod/gitpod-protocol/lib/gitpod-file-parser'; import { GitpodInfoProviderNodeImpl } from "./gitpod-info-backend"; @@ -37,15 +36,13 @@ import { GitpodPluginLocatorClient } from "./extensions/gitpod-plugin-locator-cl import { HostedPluginReader } from "@theia/plugin-ext/lib/hosted/node/plugin-reader"; import { GitpodPluginReader } from "./extensions/gitpod-plugin-reader"; import { gitpodInfoPath } from "../common/gitpod-info"; -import { ContentReadyServiceServer, ContentReadyServiceClient, ContentReadyService } from "../common/content-ready-service"; -import { ContentReadyServiceServerImpl } from "./content-ready-service-server"; -import { Deferred } from "@theia/core/lib/common/promise-util"; import { OpenVSXExtensionProviderImpl } from "./extensions/openvsx-extension-provider-impl"; import { openVSXExtensionProviderPath } from "../common/openvsx-extension-provider"; import { EnvVariablesServer } from "@theia/core/lib/common/env-variables"; -import { RemoteFileSystemServer, FileSystemProviderServer } from "@theia/filesystem/lib/common/remote-file-system-provider"; import { SupervisorServedPortsServiceImpl } from "./supervisor-serverd-ports-service"; import { SupervisorClientProvider } from "./supervisor-client-provider"; +import { GitpodTaskServer, GitpodTaskClient, gitpodTaskServicePath } from "../common/gitpod-task-protocol"; +import { GitpodTaskServerImpl } from "./gitpod-task-server-impl"; export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(ShellProcess).to(GitpodShellProcess).inTransientScope(); @@ -53,13 +50,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(ILoggerServer).to(JsonConsoleLoggerServer).inSingletonScope(); bind(GitpodFileParser).toSelf().inSingletonScope(); - bind(GitpodTaskStarter).toSelf().inSingletonScope(); - bind(BackendApplicationContribution).to(WorkspaceReadyTaskStarter).inSingletonScope(); - - if (process.env['THEIA_LOCAL']) { - // in theia local mode, no signal is coming from syncd, so we want to launch the tasks on start. - bind(BackendApplicationContribution).to(TheiaLocalTaskStarter); - } bind(GitPodExpressService).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(GitPodExpressService); @@ -74,6 +64,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { return server; }) ).inSingletonScope(); + bind(GitpodTaskServer).to(GitpodTaskServerImpl).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(context => + new JsonRpcConnectionHandler(gitpodTaskServicePath, client => { + const server = context.container.get(GitpodTaskServer); + server.setClient(client); + client.onDidCloseConnection(() => server.disposeClient(client)); + return server; + }) + ).inSingletonScope(); bind(CliServiceServer).to(CliServiceServerImpl).inSingletonScope(); bind(ConnectionHandler).toDynamicValue(context => @@ -87,43 +86,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { if (!process.env['THEIA_LOCAL']) { rebind(EnvVariablesServer).to(GitpodEnvVariablesServer).inSingletonScope(); - rebind(RemoteFileSystemServer).toDynamicValue(context => { - const server = context.container.get(FileSystemProviderServer); - const contentReadyServer: ContentReadyServiceServer = context.container.get(ContentReadyServiceServer); - const ready = new Deferred(); - contentReadyServer.setClient({ - onContentReady() { - ready.resolve(); - return Promise.resolve(); - } - }); - const get: (target: any, prop: any) => any = (target: any, prop: any) => { - const orig = target[prop]; - if (typeof orig === 'function') { - return (...args: any[]) => { - return ready.promise.then(() => { - return orig.apply(target, args); - }); - } - } - return orig; - }; - return new Proxy(server, { get }); - }); } rebind(HostedPluginReader).to(GitpodPluginReader).inSingletonScope(); - bind(ContentReadyServiceServer).to(ContentReadyServiceServerImpl).inSingletonScope(); - bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(ContentReadyService.SERVICE_PATH, client => { - const server = context.container.get(ContentReadyServiceServer); - server.setClient(client); - client.onDidCloseConnection(() => server.disposeClient(client)); - return server; - }) - ).inSingletonScope(); - bind(GitpodPluginDeployerHandler).toSelf().inSingletonScope(); rebind(HostedPluginDeployerHandler).toService(GitpodPluginDeployerHandler); bind(GitpodPluginDeployer).toSelf().inSingletonScope(); diff --git a/components/theia/packages/gitpod-extension/src/node/gitpod-info-backend.ts b/components/theia/packages/gitpod-extension/src/node/gitpod-info-backend.ts index 0919c8c523943d..cb64f48d9e8912 100644 --- a/components/theia/packages/gitpod-extension/src/node/gitpod-info-backend.ts +++ b/components/theia/packages/gitpod-extension/src/node/gitpod-info-backend.ts @@ -4,14 +4,11 @@ * See License-AGPL.txt in the project root for license information. */ -import { injectable, inject } from "inversify"; -import { GitpodInfo, GitpodInfoService, TerminalProcessInfo } from "../common/gitpod-info"; -import { GitpodTaskStarter } from "./gitpod-task-starter"; +import { injectable } from "inversify"; +import { GitpodInfo, GitpodInfoService } from "../common/gitpod-info"; @injectable() export class GitpodInfoProviderNodeImpl implements GitpodInfoService { - @inject(GitpodTaskStarter) protected taskStarter: GitpodTaskStarter; - private info: GitpodInfo = { host: process.env.GITPOD_HOST || 'http://localhost:3000', // workspaceId: process.env.GITPOD_WORKSPACE_ID || 'a12-321', // Issue workspace @@ -25,7 +22,4 @@ export class GitpodInfoProviderNodeImpl implements GitpodInfoService { return this.info; } - async getTerminalProcessInfos(): Promise { - return this.taskStarter.terminalProcessInfos; - } } \ No newline at end of file diff --git a/components/theia/packages/gitpod-extension/src/node/gitpod-task-server-impl.ts b/components/theia/packages/gitpod-extension/src/node/gitpod-task-server-impl.ts new file mode 100644 index 00000000000000..f7c35f47358c5c --- /dev/null +++ b/components/theia/packages/gitpod-extension/src/node/gitpod-task-server-impl.ts @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2020 TypeFox GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { TasksStatusRequest, TasksStatusResponse } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { ApplicationShell } from '@theia/core/lib/browser'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { ProcessManager, TerminalProcess } from '@theia/process/lib/node'; +import * as fs from 'fs'; +import { inject, injectable, postConstruct } from 'inversify'; +import * as path from 'path'; +import * as util from 'util'; +import { AttachTaskTerminalParams, GitpodTask, GitpodTaskClient, GitpodTaskServer } from '../common/gitpod-task-protocol'; +import { SupervisorClientProvider } from './supervisor-client-provider'; +import psTree = require('ps-tree'); + +@injectable() +export class GitpodTaskServerImpl implements GitpodTaskServer { + + protected run = true; + protected stopUpdates: (() => void) | undefined; + + private readonly clients = new Set(); + + private readonly tasks = new Map(); + private readonly deferredReady = new Deferred(); + private readonly supervisorBin = (async () => { + let supervisor = '/.supervisor/supervisor'; + try { + await util.promisify(fs.stat)(supervisor); + } catch (e) { + supervisor = '/theia/supervisor'; + try { + await util.promisify(fs.stat)(supervisor); + } catch { + throw e; + } + } + return supervisor; + })(); + + @inject(ProcessManager) + protected readonly processManager: ProcessManager; + + @inject(SupervisorClientProvider) + private readonly supervisorClientProvider: SupervisorClientProvider; + + @postConstruct() + async start(): Promise { + const client = await this.supervisorClientProvider.getStatusClient(); + while (this.run) { + try { + const req = new TasksStatusRequest(); + req.setObserve(true); + const evts = client.tasksStatus(req); + this.stopUpdates = evts.cancel; + + await new Promise((resolve, reject) => { + evts.on("close", resolve); + evts.on("error", reject); + evts.on("data", (response: TasksStatusResponse) => { + const updated: GitpodTask[] = []; + for (const task of response.getTasksList()) { + const openIn = task.getPresentation()!.getOpenIn(); + const openMode = task.getPresentation()!.getOpenMode(); + const update: GitpodTask = { + id: task.getId(), + state: task.getState() as number, + terminal: task.getTerminal(), + presentation: { + name: task.getPresentation()!.getName(), + // grpc inserts empty strings for optional properties of string type :( + openIn: !!openIn ? openIn as ApplicationShell.WidgetOptions['area'] | undefined : undefined, + openMode: !!openMode ? openMode as ApplicationShell.WidgetOptions['mode'] | undefined : undefined + } + } + this.tasks.set(task.getId(), update); + updated.push(update); + } + this.deferredReady.resolve(); + for (const client of this.clients) { + client.onDidChange({ updated }); + } + }); + }); + } catch (err) { + console.error("cannot maintain connection to supervisor", err); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + } + + async getTasks(): Promise { + await this.deferredReady.promise; + return [...this.tasks.values()]; + } + + async attach({ terminalId, remoteTerminal }: AttachTaskTerminalParams): Promise { + const terminalProcess = this.processManager.get(terminalId); + if (!(terminalProcess instanceof TerminalProcess)) { + return; + } + const [supervisorBin, children] = await Promise.all([ + this.supervisorBin, + util.promisify(psTree)(terminalProcess.pid) + ]); + const supervisorCommand = path.basename(supervisorBin); + if (children.some(child => child.COMMAND === supervisorCommand)) { + return; + } + terminalProcess.write(`${supervisorBin} terminal attach -ir ${remoteTerminal}\r\n`); + } + + setClient(client: GitpodTaskClient): void { + this.clients.add(client); + } + disposeClient(client: GitpodTaskClient): void { + this.clients.delete(client); + } + + dispose(): void { + this.run = false; + if (!!this.stopUpdates) { + this.stopUpdates(); + } + } + +} \ No newline at end of file diff --git a/components/theia/packages/gitpod-extension/src/node/gitpod-task-starter.ts b/components/theia/packages/gitpod-extension/src/node/gitpod-task-starter.ts deleted file mode 100644 index 955ef146347112..00000000000000 --- a/components/theia/packages/gitpod-extension/src/node/gitpod-task-starter.ts +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Copyright (c) 2020 TypeFox GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License-AGPL.txt in the project root for license information. - */ - -import { injectable, inject } from "inversify"; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ILogger } from "@theia/core/lib/common"; -import { TerminalProcess, ProcessManager } from "@theia/process/lib/node"; -import { IShellTerminalServer } from '@theia/terminal/lib/common/shell-terminal-protocol'; -import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol'; -import { TaskConfig } from '@gitpod/gitpod-protocol/lib/protocol'; -import { WorkspaceReadyMessage, WorkspaceInitSource } from '@gitpod/gitpod-protocol/lib/wsready'; -import { TerminalProcessInfo } from "../common/gitpod-info"; -import { BackendApplicationContribution } from "@theia/core/lib/node"; -import { GitpodFileParser } from '@gitpod/gitpod-protocol/lib/gitpod-file-parser'; -import { FileUri } from '@theia/core/lib/node/file-uri'; -import URI from "@theia/core/lib/common/uri"; -import { TheiaHeadlessLogType } from '@gitpod/gitpod-protocol/lib/headless-workspace-log'; -import { Transform } from "stream"; -import * as moment from 'moment'; -import { ContentReadyServiceServer } from "../common/content-ready-service"; - -import * as jsoncparser from 'jsonc-parser'; -import { UTF8 } from "@theia/core/lib/common/encodings"; - -interface TaskConfigWithPrebuild extends TaskConfig { - printlogs?: string -} - -export type StartPhase = 'init' | 'restart' | 'prebuild' | 'prebuilt'; - -@injectable() -export class GitpodTaskStarter { - @inject(ILogger) protected readonly logger: ILogger; - @inject(ProcessManager) protected readonly processManager: ProcessManager; - @inject(IShellTerminalServer) protected readonly shellTerminalServer: IShellTerminalServer; - @inject(WorkspaceServer) protected readonly workspaceServer: WorkspaceServer; - readonly terminalProcessInfos: TerminalProcessInfo[] = []; - @inject(GitpodFileParser) protected readonly gitpodFileParser: GitpodFileParser; - - async start(startPhase: StartPhase) { - const headlessTasks: Promise[] = []; - try { - const previousUri = await this.workspaceServer.getMostRecentlyUsedWorkspace(); - const workspaceRoot = previousUri && FileUri.fsPath(new URI(previousUri)) || process.env.THEIA_WORKSPACE_ROOT; - const rootPath = await this.resolveWorkspacePath(workspaceRoot); - const workspaceTasks = await this.getWorkspaceTasks(rootPath); - const isHeadless = process.env.GITPOD_HEADLESS === 'true'; - if (isHeadless) { - this.logger.info(`Running headless and thus ignoring original start phase: ${JSON.stringify(startPhase)}. Using 'prebuild' instead.`); - startPhase = 'prebuild'; - } - - let idx = 0; - this.logger.info(`Starting tasks in ${JSON.stringify(startPhase)} from ${JSON.stringify(workspaceTasks)}`); - for (const task of workspaceTasks) { - const path = rootPath; - const taskcfg = this.addPrebuiltTasks(task, idx); - - const taskCommand = await this.buildCmdStr(taskcfg, startPhase, idx); - if (isHeadless && !taskCommand) { - // we have nothing to do here - continue; - } - - const term = await this.startTerminalWith(path, taskCommand, task.env || {}); - this.terminalProcessInfos.push({ - processId: term.id, - task - }); - - if (isHeadless) { - this.logger.info(`Running ${taskCommand}`) - headlessTasks.push(this.watchHeadlessTask(term, rootPath)); - } - idx += 1; - } - - if (isHeadless) { - this.publishHeadlessTaskState(headlessTasks); - } - } catch (err) { - this.logger.info(err.message); - } - - } - - protected async resolveWorkspacePath(workspaceRoot?: string): Promise { - if (!workspaceRoot) { - console.error('No workspace root'); - } else { - try { - const stat = await fs.stat(workspaceRoot) - if (stat.isDirectory()) { - return workspaceRoot; - } else { - const path = await this.getWorkspacePathFromFile(FileUri.create(workspaceRoot).toString()); - if (path) { - return path; - } - } - } catch (err) { - console.error("Couldn't load workspace root", err); - } - } - return "/workspace"; - } - - protected async getWorkspacePathFromFile(fileUri: string): Promise { - const content = await fs.readFile(FileUri.fsPath(fileUri), UTF8); - const strippedContent = jsoncparser.stripComments(content); - const data = jsoncparser.parse(strippedContent); - if (data && Array.isArray(data['folders'])) { - for (const candidate of data['folders']) { - if (typeof candidate['path'] === 'string') { - const configPath = candidate['path']; - const relativeUri = new URI(fileUri).parent.resolve(configPath); - try { - const folderStat = await fs.stat(FileUri.fsPath(relativeUri)); - if (folderStat.isDirectory()) { - return relativeUri.toString(); - } - } catch { - /* no-op */ - } - } - } - } - console.error(`Workspace config doesn't contain a valid workspace root.`, JSON.stringify(data)); - return undefined; - } - - protected addPrebuiltTasks(task: TaskConfig, index: number): TaskConfigWithPrebuild { - const legacyFilename = `/workspace/.prebuild-log-${index}`; - const fileName = `/workspace/.gitpod/prebuild-log-${index}`; - return { - ...task, - printlogs: `[ -r ${legacyFilename} ] && cat ${legacyFilename}; [ -r ${fileName} ] && cat ${fileName}; true`, - } - } - - protected async getWorkspaceTasks(rootPath: string): Promise { - try { - try { - const gitpodFile = path.join(rootPath, '.gitpod.yml'); - if (fs.existsSync(gitpodFile)) { - const contents = await fs.readFile(gitpodFile); - const parseResult = this.gitpodFileParser.parse(contents.toString()); - if (!parseResult.validationErrors || parseResult.validationErrors.length === 0) { - if (parseResult.config.tasks) { - return parseResult.config.tasks; - } - } - } - } catch (err) { - this.logger.info("Failed to parse tasks from local .gitpod.yml.", { err }); - } - const values = JSON.parse(process.env.GITPOD_TASKS || ''); - if (Array.isArray(values)) { - return values.filter(v => TaskConfig.is(v)); - } - } catch (err) { - this.logger.info("Failed to parse workspace tasks.", { err }); - } - return []; - } - - protected async startTerminalWith(cwd: string, command: string | undefined, env: { [env: string]: string }): Promise { - const terminalId = await this.shellTerminalServer.create({ - rootURI: cwd, - env - }); - const termProcess = this.processManager.get(terminalId) as TerminalProcess; - if (command) { - // let's wait for data (prompt), before sending the command. - const out = termProcess.createOutputStream(); - out.once('data', () => termProcess.write(command + '\n')); - } - - this.logger.info("Started terminal %s with command '%s'.", terminalId, command); - return termProcess; - } - - protected getCommands(task: TaskConfigWithPrebuild, startPhase: StartPhase | 'mock-prebuilt'): string[] { - function isCommand(cmd?: string): cmd is string { - return !!(cmd && cmd.trim().length > 0); - } - - const phaseToTask: { [P in (StartPhase | "mock-prebuilt")]: string[] } = { - 'init': ['before', 'init', 'command'], - - 'restart': ['before', 'command'], - - // we're starting a new prebuild - 'prebuild': ['before', 'init', 'prebuild'], - - // this workspace was initialized from a previously run prebuild - 'prebuilt': ['before', 'printlogs', 'command'], - - // this workspace was initialized from a prebuild and we need the commands as if we ran all of them at once - 'mock-prebuilt': ['before', 'init', 'prebuild', 'command'], - }; - - const commands = phaseToTask[startPhase] - .map(t => (task as any)[t]) - .filter(isCommand); - return commands; - } - - protected async buildCmdStr(task: TaskConfigWithPrebuild, startPhase: StartPhase, index: number): Promise { - const commands = this.getCommands(task, startPhase); - let command = commands - .map(c => `{\n${c}\n}`) - .join(" && "); - if (startPhase === "prebuild") { - // it's important that prebuild tasks exit eventually - // also, we need to save the log output in the workspace - if (command.trim().length > 0) { - command += '; exit'; - } else { - command = 'exit'; - } - } else if (command.trim().length > 0) { - const fn = `/workspace/.gitpod/cmd-${index}`; - await fs.writeFile(fn, this.getCommands(task, startPhase == 'prebuilt' ? 'mock-prebuilt' : startPhase).join("\r\n") + "\r\n"); - - // the space at beginning of the HISTFILE command prevents the HISTFILE command itself from appearing in - // the bash history. - const histcmd = ` HISTFILE=${fn} history -r`; - command = histcmd + '; ' + command; - } - - if (command.trim().length === 0) { - return undefined; - } else { - return command; - } - } - - /** - * This function listens on the tasks and forwards its output/events to the logger. - * The ws-monitor listens to this log output and interpretes it using the HeadlessWorkspaceLog labels. - */ - protected watchHeadlessTask(term: TerminalProcess, rootPath: string): Promise { - // at this point we're already running. Let's not wait for the file to open, but buffer what's - // been output before. - const logFile = `/workspace/.gitpod/prebuild-log-${term.id}`; - this.logger.info(`Writing build output to ${logFile}`); - let logfileStream = fs.createWriteStream(logFile, { flags: 'w' }); - - const start = Date.now(); - const output = term.createOutputStream(); - output. - pipe(new PrebuiltExitTransform(() => { - const now = Date.now(); - if (now - start < 60 * 1000) { - return ""; - } - - return `๐ŸŽ‰ You just saved ${moment(start).to(now, true)} of watching your code build.\n`; - })). - pipe(logfileStream); - output.on('data', async data => { - this.logger.info({ type: TheiaHeadlessLogType.TaskLogLabel, data: data.toString() }); - }); - - return new Promise((resolve, reject) => { - term.onExit(async (e) => { - /* Not sure that's such a brilliant idea. I don't know how the term output - * stream pipe behaves when we close the underlying stream. The rationale is - * that I want ensure that the log file is synced to disk before taking the - * snapshot. - */ - logfileStream.close(); - - this.logger.info(`Terminal process exited with code ${e.code}`); - resolve(e.code); - }); - }) - } - - protected async publishHeadlessTaskState(tasks: Promise[]) { - try { - /* TODO: here we could decide if we want to publish a snapshot even if one of - * of the tasks returned with a non-zero exit code (i.e. the promise - * resolves to false). - */ - const taskExitCodes = await Promise.all(tasks); - const hasMonZeroExitCodes = !!taskExitCodes.find(s => s !== 0); - - this.logger.info({ type: TheiaHeadlessLogType.TaskLogLabel, data: "๐Ÿš› uploading prebuilt workspace" }); - if (hasMonZeroExitCodes) { - this.logger.info({ type: TheiaHeadlessLogType.TaskFailedLabel, error: "one of the tasks failed with non-zero exit code" }); - } else { - this.logger.info({ type: TheiaHeadlessLogType.TaskSuccessfulLabel }); - } - } catch (err) { - this.logger.info({ type: TheiaHeadlessLogType.TaskFailedLabel, error: `Tasks failed: ${err}` }); - } - } -} - -@injectable() -export class TheiaLocalTaskStarter implements BackendApplicationContribution { - - @inject(GitpodTaskStarter) starter: GitpodTaskStarter; - onStart() { - this.starter.start((process.env.GITPOD_START_MODE || "init") as StartPhase); - } -} - -@injectable() -export class WorkspaceReadyTaskStarter implements BackendApplicationContribution { - @inject(GitpodTaskStarter) starter: GitpodTaskStarter; - @inject(ILogger) protected readonly logger: ILogger; - @inject(ContentReadyServiceServer) protected readonly contentReadyService: ContentReadyServiceServer; - - async onStart() { - const readyFilename = "/workspace/.gitpod/ready"; - - this.logger.info("waiting for workspace content to become available"); - const readyFileExists = () => new Promise((resolve, reject) => { - this.logger.debug(`checking if ready file (${readyFilename}) exists`); - try { - fs.exists(readyFilename, resolve); - } catch (err) { - reject(err); - } - }); - while (true) { - if (await readyFileExists()) { - break - } - - // we really do this polling forerver (or until the workspace content gets ready), as we do not - // want to impose another workspace startup timeout. Some repositories can really take up to 30-45 minutes - // to check out. Worst case do we poll this file until the workspace is stopped again by wsman - // because the content did not get ready in time. - await new Promise(tryAgain => setTimeout(() => tryAgain(), 1000)); - } - - let message: WorkspaceReadyMessage = { - source: WorkspaceInitSource.WorkspaceInitFromOther - }; - try { - message = await fs.readJson(readyFilename); - } catch (err) { - this.logger.error(`cannot read workspace ready file: ${err}`); - } - - let phase: StartPhase; - if (message.source == "from-backup") { - phase = "restart"; - } else if (message.source == "from-prebuild") { - phase = "prebuilt"; - } else { - phase = "init"; - } - - this.logger.info("workspace content is ready - starting tasks", { phase, message }); - this.starter.start(phase); - this.contentReadyService.markContentReady(); - } - -} - - -class PrebuiltExitTransform extends Transform { - - constructor(protected readonly durationProvider: () => string) { - super(); - } - - // this is a poor mans version of this transformation. It assumes that the chuck contains - // all the lines required to detect the exit condition. That's not neccesarily true. - // A better implementation would buffer lines if required. Let' see how well this one works - // in practice though. - _transform(chunk: any, encoding: string, callback: Function): void { - const rows: string[] = chunk.toString().split("\n"); - - for (let i = 0; i < rows.length - 1; i++) { - if (rows[i].trim() == "exit" && rows[i + 1].trim() == "") { - rows[i] = `\n๐ŸŒ This task ran as part of a workspace prebuild.\n${this.durationProvider()}\n` - } - } - this.push(rows.join("\n")); - callback(); - } - -} \ No newline at end of file diff --git a/components/ws-manager/pkg/manager/headless.go b/components/ws-manager/pkg/manager/headless.go index 326c8290be91da..1fd76e7c1606a8 100644 --- a/components/ws-manager/pkg/manager/headless.go +++ b/components/ws-manager/pkg/manager/headless.go @@ -89,17 +89,22 @@ func (hl *HeadlessListener) Listen(ctx context.Context, pod *corev1.Pod) error { } func (hl *HeadlessListener) handleLogLine(pod *corev1.Pod, line string) (continueListening bool) { - var originalMsg theiaLogMessage + var taskMsg taskLogMessage + var originalMsg workspaceLogMessage err := json.Unmarshal([]byte(line), &originalMsg) if err != nil { - return true - } - - if originalMsg.Component != "workspace" { - return true + var legacyOriginalMsg theiaLogMessage + err := json.Unmarshal([]byte(line), &legacyOriginalMsg) + if err != nil || legacyOriginalMsg.Component != "workspace" { + return true + } + taskMsg = legacyOriginalMsg.Message + } else { + if originalMsg.Component != "workspace" { + return true + } + taskMsg = originalMsg.taskLogMessage } - - taskMsg := originalMsg.Message if taskMsg.Type == "workspaceTaskOutput" { hl.OnHeadlessLog(pod, taskMsg.Data) return true @@ -115,16 +120,24 @@ func (hl *HeadlessListener) handleLogLine(pod *corev1.Pod, line string) (continu return true } -type theiaLogMessage struct { - Message theiaTaskLogMessage `json:"message"` - Component string `json:"component"` -} - -type theiaTaskLogMessage struct { +type taskLogMessage struct { Type string `json:"type"` Data string `json:"data"` } +type workspaceLogMessage struct { + taskLogMessage + Component string `json:"component"` +} + +//region backward compatibility +type theiaLogMessage struct { + Message taskLogMessage `json:"message"` + Component string `json:"component"` +} + +//endregion + const ( // timeout in seconds, default 3 seconds. Actual timeout is this number multiplied by the number of retries. listenerTimeout = 3 * time.Second @@ -161,7 +174,6 @@ func (hl *HeadlessListener) listenAndRetry(ctx context.Context, pod *corev1.Pod, if len(l) == 0 { continue } - lastLineReadChan <- l } diff --git a/yarn.lock b/yarn.lock index 0442031f52d6b3..cd9a67d069b8c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3399,6 +3399,11 @@ version "15.5.6" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.6.tgz#9c03d3fed70a8d517c191b7734da2879b50ca26c" +"@types/ps-tree@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/ps-tree/-/ps-tree-1.1.0.tgz#7e2034e8ccdc16f6b0ced7a88529ebcb3b1dc424" + integrity sha512-rm5GU5sefQpg2d/DQ+fMDZnl9aPiJjJ9FYA12isIocNTZqu9VDZRgCRBx3oYFEdmDpmPmY4hxxmY/+1a84Rtzg== + "@types/puppeteer@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-2.0.0.tgz#82c04f93367e2d3396e371a71be1167332148838" @@ -18439,18 +18444,20 @@ utils-merge@1.0.1, utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: +uuid@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== uuid@^2.0.1: version "2.0.3" - resolved "http://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== +uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^7.0.1: version "7.0.3" @@ -18458,9 +18465,9 @@ uuid@^7.0.1: integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== uuid@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== v8-compile-cache@^1.1.2: version "1.1.2"