diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index 0e80db99534f1..621b70899b948 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,11 +1,10 @@ FROM gitpod/workspace-full:latest -FROM gitpod/workspace-full:latest - +# We use latest major version of Node.js distributed VS Code. (see about dialog in your local VS Code) RUN bash -c ". .nvm/nvm.sh \ - && nvm install 12.18.3 \ - && nvm use 12.18.3 \ - && nvm alias default 12.18.3" + && nvm install 12 \ + && nvm use 12 \ + && nvm alias default 12" RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix diff --git a/extensions/gitpod/src/extension.ts b/extensions/gitpod/src/extension.ts index c220438853e83..f3d1bb9ebe830 100644 --- a/extensions/gitpod/src/extension.ts +++ b/extensions/gitpod/src/extension.ts @@ -5,18 +5,19 @@ require('reflect-metadata'); import { GitpodClient, GitpodServer, GitpodServiceImpl } from '@gitpod/gitpod-protocol/lib/gitpod-service'; import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory'; +import { NavigatorContext, PullRequestContext } from '@gitpod/gitpod-protocol/lib/protocol'; import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url'; import * as workspaceInstance from '@gitpod/gitpod-protocol/lib/workspace-instance'; import { ControlServiceClient } from '@gitpod/supervisor-api-grpc/lib/control_grpc_pb'; -import { ExposePortRequest, ExposePortResponse } from '@gitpod/supervisor-api-grpc/lib/control_pb'; +import { ExposePortRequest } from '@gitpod/supervisor-api-grpc/lib/control_pb'; import { InfoServiceClient } from '@gitpod/supervisor-api-grpc/lib/info_grpc_pb'; -import { WorkspaceInfoRequest, WorkspaceInfoResponse } from '@gitpod/supervisor-api-grpc/lib/info_pb'; +import { WorkspaceInfoRequest } from '@gitpod/supervisor-api-grpc/lib/info_pb'; import { NotificationServiceClient } from '@gitpod/supervisor-api-grpc/lib/notification_grpc_pb'; import { NotifyRequest, NotifyResponse, RespondRequest, SubscribeRequest, SubscribeResponse } from '@gitpod/supervisor-api-grpc/lib/notification_pb'; import { StatusServiceClient } from '@gitpod/supervisor-api-grpc/lib/status_grpc_pb'; import { ExposedPortInfo, OnPortExposedAction, PortsStatus, PortsStatusRequest, PortsStatusResponse, PortVisibility } from '@gitpod/supervisor-api-grpc/lib/status_pb'; import { TokenServiceClient } from '@gitpod/supervisor-api-grpc/lib/token_grpc_pb'; -import { GetTokenRequest, GetTokenResponse } from '@gitpod/supervisor-api-grpc/lib/token_pb'; +import { GetTokenRequest } from '@gitpod/supervisor-api-grpc/lib/token_pb'; import * as grpc from '@grpc/grpc-js'; import * as fs from 'fs'; import type * as keytarType from 'keytar'; @@ -29,17 +30,29 @@ import * as vscode from 'vscode'; import { ConsoleLogger, listen as doListen } from 'vscode-ws-jsonrpc'; import { GitpodPluginModel } from './gitpod-plugin-model'; import WebSocket = require('ws'); -import { NavigatorContext, PullRequestContext } from '@gitpod/gitpod-protocol/lib/protocol'; export async function activate(context: vscode.ExtensionContext) { const pendingActivate: Promise[] = []; + + const supervisorDeadlines = { + long: 30 * 1000, + normal: 15 * 1000, + short: 5 * 1000 + }; const supervisorAddr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; - const statusServiceClient = new StatusServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const controlServiceClient = new ControlServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const notificationServiceClient = new NotificationServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const tokenServiceClient = new TokenServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const infoServiceClient = new InfoServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const workspaceInfoResponse = await util.promisify(infoServiceClient.workspaceInfo.bind(infoServiceClient))(new WorkspaceInfoRequest()); + const supervisorClientOptions: Partial = { + 'grpc.primary_user_agent': `${vscode.env.appName}/${vscode.version} ${context.extensionId}/${context.extensionVersion}`, + }; + const supervisorMetadata = new grpc.Metadata(); + const statusServiceClient = new StatusServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); + const controlServiceClient = new ControlServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); + const notificationServiceClient = new NotificationServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); + const tokenServiceClient = new TokenServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); + const infoServiceClient = new InfoServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); + + const workspaceInfoResponse = await util.promisify(infoServiceClient.workspaceInfo.bind(infoServiceClient, new WorkspaceInfoRequest(), supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); const checkoutLocation = workspaceInfoResponse.getCheckoutLocation(); const workspaceId = workspaceInfoResponse.getWorkspaceId(); const gitpodHost = workspaceInfoResponse.getGitpodHost(); @@ -67,7 +80,9 @@ export async function activate(context: vscode.ExtensionContext) { for (const scope of gitpodScopes) { getTokenRequest.addScope(scope); } - const getTokenResponse = await util.promisify(tokenServiceClient.getToken.bind(tokenServiceClient))(getTokenRequest); + const getTokenResponse = await util.promisify(tokenServiceClient.getToken.bind(tokenServiceClient, getTokenRequest, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); return getTokenResponse.getToken(); })(); (async () => { @@ -353,7 +368,7 @@ export async function activate(context: vscode.ExtensionContext) { try { const req = new PortsStatusRequest(); req.setObserve(true); - const evts = statusServiceClient.portsStatus(req); + const evts = statusServiceClient.portsStatus(req, supervisorMetadata); stopUpdates = evts.cancel.bind(evts); await new Promise((resolve, reject) => { @@ -402,7 +417,9 @@ export async function activate(context: vscode.ExtensionContext) { const request = new ExposePortRequest(); request.setPort(portNumber); request.setTargetPort(portNumber); - await util.promisify(controlServiceClient.exposePort).bind(controlServiceClient)(request); + await util.promisify(controlServiceClient.exposePort.bind(controlServiceClient, request, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.normal + }))(); } } catch (e) { reject(e); @@ -640,7 +657,9 @@ export async function activate(context: vscode.ExtensionContext) { getTokenRequest.addScope(scope); } } - const getTokenResponse = await util.promisify(tokenServiceClient.getToken.bind(tokenServiceClient))(getTokenRequest); + const getTokenResponse = await util.promisify(tokenServiceClient.getToken.bind(tokenServiceClient, getTokenRequest, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); const accessToken = getTokenResponse.getToken(); gitHubSession = await resolveAuthenticationSession({ id: gitHubSessionID, @@ -738,7 +757,7 @@ export async function activate(context: vscode.ExtensionContext) { while (run) { try { console.info('connecting to notification service'); - const evts = notificationServiceClient.subscribe(new SubscribeRequest()); + const evts = notificationServiceClient.subscribe(new SubscribeRequest(), supervisorMetadata); stopUpdates = evts.cancel.bind(evts); await new Promise((resolve, reject) => { @@ -769,7 +788,9 @@ export async function activate(context: vscode.ExtensionContext) { respondRequest.setResponse(notifyResponse); respondRequest.setRequestid(result.getRequestid()); console.info('sending notification response', request); - notificationServiceClient.respond(respondRequest, (error, _) => { + notificationServiceClient.respond(respondRequest, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.normal + }, (error, _) => { if (error?.code !== grpc.status.DEADLINE_EXCEEDED) { reject(error); } diff --git a/extensions/gitpod/yarn.lock b/extensions/gitpod/yarn.lock index 69a9b3dddf814..e41e6f00690df 100644 --- a/extensions/gitpod/yarn.lock +++ b/extensions/gitpod/yarn.lock @@ -30,11 +30,11 @@ google-protobuf "^3.8.0-rc.1" "@grpc/grpc-js@^1.1.5", "@grpc/grpc-js@latest": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.4.tgz#04f0bbefb2636296d17e821f3d52152fbe2f6989" - integrity sha512-z+EI20HYHLd3/uERtwOqP8Q4EPhGbz5RKUpiyo6xPWfR3pcjpf8sfNvY9XytDQ4xo1wNz7NqH1kh2UBonwzbfg== + version "1.2.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.11.tgz#68faa56bded64844294dc6429185503376f05ff1" + integrity sha512-DZqx3nHBm2OGY7NKq4sppDEfx4nBAsQH/d/H/yxo/+BwpVLWLGs+OorpwQ+Fqd6EgpDEoi4MhqndjGUeLl/5GA== dependencies: - "@types/node" "^12.12.47" + "@types/node" ">=12.12.47" google-auth-library "^6.1.1" semver "^6.2.0" @@ -51,16 +51,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== +"@types/node@>=12.12.47": + version "14.14.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313" + integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== + "@types/node@^10.12.21": version "10.17.35" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== -"@types/node@^12.12.47": - version "12.12.62" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.62.tgz#733923d73669188d35950253dd18a21570085d2b" - integrity sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg== - "@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" diff --git a/remote/yarn.lock b/remote/yarn.lock index dab33a079f9fe..a28d105998e37 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -11,18 +11,18 @@ google-protobuf "^3.8.0-rc.1" "@grpc/grpc-js@^1.1.5", "@grpc/grpc-js@latest": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.4.tgz#04f0bbefb2636296d17e821f3d52152fbe2f6989" - integrity sha512-z+EI20HYHLd3/uERtwOqP8Q4EPhGbz5RKUpiyo6xPWfR3pcjpf8sfNvY9XytDQ4xo1wNz7NqH1kh2UBonwzbfg== + version "1.2.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.11.tgz#68faa56bded64844294dc6429185503376f05ff1" + integrity sha512-DZqx3nHBm2OGY7NKq4sppDEfx4nBAsQH/d/H/yxo/+BwpVLWLGs+OorpwQ+Fqd6EgpDEoi4MhqndjGUeLl/5GA== dependencies: - "@types/node" "^12.12.47" + "@types/node" ">=12.12.47" google-auth-library "^6.1.1" semver "^6.2.0" -"@types/node@^12.12.47": - version "12.19.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.13.tgz#888e2b34159fb91496589484ec169618212b51b7" - integrity sha512-qdixo2f0U7z6m0UJUugTJqVF94GNDkdgQhfBtMs8t5898JE7G/D2kJYw4rc1nzjIPLVAsDkY2MdABnLAP5lM1w== +"@types/node@>=12.12.47": + version "14.14.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313" + integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== abort-controller@^3.0.0: version "3.0.0" diff --git a/src/vs/gitpod/node/remoteTerminalChannelServer.ts b/src/vs/gitpod/node/remoteTerminalChannelServer.ts index 4d43ba5833118..9aaafcac5a307 100644 --- a/src/vs/gitpod/node/remoteTerminalChannelServer.ts +++ b/src/vs/gitpod/node/remoteTerminalChannelServer.ts @@ -3,8 +3,7 @@ *--------------------------------------------------------------------------------------------*/ import { TaskStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; -import { TerminalServiceClient } from '@gitpod/supervisor-api-grpc/lib/terminal_grpc_pb'; -import { ListTerminalsRequest, ListTerminalsResponse } from '@gitpod/supervisor-api-grpc/lib/terminal_pb'; +import { ListTerminalsRequest } from '@gitpod/supervisor-api-grpc/lib/terminal_pb'; import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; @@ -16,6 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IRawURITransformer, transformIncomingURIs, transformOutgoingURIs, URITransformer } from 'vs/base/common/uriIpc'; import { getSystemShellSync } from 'vs/base/node/shell'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { supervisorDeadlines, supervisorMetadata, terminalServiceClient } from 'vs/gitpod/node/supervisor-client'; import { OpenSupervisorTerminalProcessOptions, SupervisorTerminalProcess } from 'vs/gitpod/node/supervisorTerminalProcess'; import { ILogService } from 'vs/platform/log/common/log'; import product from 'vs/platform/product/common/product'; @@ -27,7 +27,7 @@ import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, IGetTerminalCwdArguments, IGetTerminalInitialCwdArguments, IOnTerminalProcessEventArguments, IRemoteTerminalDescriptionDto, IResizeTerminalProcessArguments, ISendCharCountToTerminalProcessArguments, ISendInputToTerminalProcessArguments, IShutdownTerminalProcessArguments, IStartTerminalProcessArguments, IWorkspaceFolderData } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, IGetTerminalCwdArguments, IGetTerminalInitialCwdArguments, IOnTerminalProcessEventArguments, IResizeTerminalProcessArguments, ISendCharCountToTerminalProcessArguments, ISendInputToTerminalProcessArguments, IShutdownTerminalProcessArguments, IStartTerminalProcessArguments, IWorkspaceFolderData } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { IRemoteTerminalAttachTarget } from 'vs/workbench/contrib/terminal/common/terminal'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; @@ -129,7 +129,6 @@ export class RemoteTerminalChannelServer implements IServerChannel IRawURITransformer, - private terminalServiceClient: TerminalServiceClient, private logService: ILogService, private synchingTasks: Promise> ) { } @@ -143,7 +142,6 @@ export class RemoteTerminalChannelServer implements IServerChannel(this.terminalServiceClient.list.bind(this.terminalServiceClient))(new ListTerminalsRequest()); - for (const terminal of response.getTerminalsList()) { - const alias = terminal.getAlias(); - const id = this.aliasToId.get(alias); - const annotations = terminal.getAnnotationsMap(); - const workspaceId = annotations.get('workspaceId') || ''; - const workspaceName = annotations.get('workspaceName') || ''; - const shouldPersistTerminal = tasks.has(alias) || Boolean(annotations.get('shouldPersistTerminal')); - let terminalProcess: SupervisorTerminalProcess | undefined; - if (!id) { - terminalProcess = this.createTerminalProcess( - terminal.getInitialWorkdir(), - workspaceId, - workspaceName, - shouldPersistTerminal - ); - this.terminalProcesses.set(terminalProcess.id, terminalProcess); - - terminalProcess.alias = alias; - this.attachTerminalProcess(terminalProcess); - } else { - terminalProcess = this.terminalProcesses.get(id); - } - if (!terminalProcess) { - continue; - } - - result.push({ - id: terminalProcess.id, - cwd: terminal.getCurrentWorkdir(), - pid: terminal.getPid(), - title: terminal.getTitle(), - workspaceId, - workspaceName, - isOrphan: true - }); - } - + const state = await this.sync(); + const result: IRemoteTerminalAttachTarget[] = [...state.terminals.values()]; return result; } catch (e) { this.logService.error('code server: failed to list remote terminals:', e); @@ -405,125 +365,7 @@ export class RemoteTerminalChannelServer implements IServerChannel(this.terminalServiceClient.list.bind(this.terminalServiceClient))(new ListTerminalsRequest()); - const workspaceTerminals = new Set(); - const targets = new Map(); - for (const terminal of response.getTerminalsList()) { - const alias = terminal.getAlias(); - const id = this.aliasToId.get(alias); - const annotations = terminal.getAnnotationsMap(); - const workspaceId = annotations.get('workspaceId') || ''; - const workspaceName = annotations.get('workspaceName') || ''; - const shouldPersistTerminal = tasks.has(alias) || Boolean(annotations.get('shouldPersistTerminal')); - let terminalProcess: SupervisorTerminalProcess | undefined; - if (!id) { - terminalProcess = this.createTerminalProcess( - terminal.getInitialWorkdir(), - workspaceId, - workspaceName, - shouldPersistTerminal - ); - this.terminalProcesses.set(terminalProcess.id, terminalProcess); - - terminalProcess.alias = alias; - this.attachTerminalProcess(terminalProcess); - } else { - terminalProcess = this.terminalProcesses.get(id); - } - if (!terminalProcess) { - continue; - } - - if (workspaceId === args.workspaceId) { - workspaceTerminals.add(terminalProcess.id); - } - if (workspaceId === args.workspaceId || tasks.has(alias)) { - targets.set(terminalProcess.id, { - id: terminalProcess.id, - cwd: terminal.getCurrentWorkdir(), - pid: terminal.getPid(), - title: terminal.getTitle(), - workspaceId, - workspaceName, - isOrphan: true - }); - } - } - - const result: ITerminalsLayoutInfo = { tabs: [] }; - if (this.layoutInfo.has(args.workspaceId)) { - // restoring layout - for (const tab of this.layoutInfo.get(args.workspaceId)!) { - result.tabs.push({ - ...tab, - terminals: tab.terminals.map(terminal => { - const target = targets.get(terminal.terminal) || null; - return { - ...terminal, - terminal: target - }; - }) - }); - } - } else { - // initial layout - type Tab = IRawTerminalTabLayoutInfo; - let currentTab: Tab | undefined; - let currentTerminal: IRemoteTerminalAttachTarget | undefined; - const layoutTerminal = (terminal: IRemoteTerminalAttachTarget, mode: TerminalOpenMode = defaultOpenMode) => { - if (!currentTab) { - currentTab = { - isActive: false, - activePersistentTerminalId: terminal.id, - terminals: [{ relativeSize: 1, terminal }] - }; - result.tabs.push(currentTab); - } else if (mode === 'tab-after' || mode === 'tab-before') { - const tab: Tab = { - isActive: false, - activePersistentTerminalId: terminal.id, - terminals: [{ relativeSize: 1, terminal }] - }; - const currentIndex = result.tabs.indexOf(currentTab); - const direction = mode === 'tab-after' ? 1 : -1; - result.tabs.splice(currentIndex + direction, 0, tab); - currentTab = tab; - } else { - currentTab.activePersistentTerminalId = terminal.id; - let currentIndex = -1; - const relativeSize = 1 / (currentTab.terminals.length + 1); - currentTab.terminals.forEach((info, index) => { - info.relativeSize = relativeSize; - if (info.terminal === currentTerminal) { - currentIndex = index; - } - }); - const direction = (mode === 'split-right' || mode === 'split-bottom') ? 1 : -1; - currentTab.terminals.splice(currentIndex + direction, 0, { relativeSize, terminal }); - } - currentTerminal = terminal; - }; - for (const [alias, status] of tasks) { - const id = this.aliasToId.get(alias); - const terminal = typeof id === 'number' && targets.get(id); - if (terminal) { - layoutTerminal(terminal, asTerminalOpenMode(status.getPresentation()?.getOpenMode())); - } - } - for (const id of workspaceTerminals) { - const terminal = targets.get(id); - if (terminal) { - layoutTerminal(terminal); - } - } - if (currentTab) { - currentTab.isActive = true; - } - } - + const result = await this.getTerminalLayoutInfo(arg as IGetTerminalLayoutInfoArgs); return result; } catch (e) { this.logService.error('code server: failed to get terminal layout info:', e); @@ -550,4 +392,140 @@ export class RemoteTerminalChannelServer implements IServerChannel { + const { tasks, terminals: targets } = await this.sync(arg); + const result: ITerminalsLayoutInfo = { tabs: [] }; + if (this.layoutInfo.has(arg.workspaceId)) { + // restoring layout + for (const tab of this.layoutInfo.get(arg.workspaceId)!) { + result.tabs.push({ + ...tab, + terminals: tab.terminals.map(terminal => { + const target = targets.get(terminal.terminal) || null; + return { + ...terminal, + terminal: target + }; + }) + }); + } + } else { + // initial layout + type Tab = IRawTerminalTabLayoutInfo; + let currentTab: Tab | undefined; + let currentTerminal: IRemoteTerminalAttachTarget | undefined; + const layoutTerminal = (terminal: IRemoteTerminalAttachTarget, mode: TerminalOpenMode = defaultOpenMode) => { + if (!currentTab) { + currentTab = { + isActive: false, + activePersistentTerminalId: terminal.id, + terminals: [{ relativeSize: 1, terminal }] + }; + result.tabs.push(currentTab); + } else if (mode === 'tab-after' || mode === 'tab-before') { + const tab: Tab = { + isActive: false, + activePersistentTerminalId: terminal.id, + terminals: [{ relativeSize: 1, terminal }] + }; + const currentIndex = result.tabs.indexOf(currentTab); + const direction = mode === 'tab-after' ? 1 : -1; + result.tabs.splice(currentIndex + direction, 0, tab); + currentTab = tab; + } else { + currentTab.activePersistentTerminalId = terminal.id; + let currentIndex = -1; + const relativeSize = 1 / (currentTab.terminals.length + 1); + currentTab.terminals.forEach((info, index) => { + info.relativeSize = relativeSize; + if (info.terminal === currentTerminal) { + currentIndex = index; + } + }); + const direction = (mode === 'split-right' || mode === 'split-bottom') ? 1 : -1; + currentTab.terminals.splice(currentIndex + direction, 0, { relativeSize, terminal }); + } + currentTerminal = terminal; + }; + for (const [alias, status] of tasks) { + const id = this.aliasToId.get(alias); + if (typeof id !== 'number') { + continue; + } + const terminal = targets.get(id); + if (terminal) { + targets.delete(id); + layoutTerminal(terminal, asTerminalOpenMode(status.getPresentation()?.getOpenMode())); + } + } + for (const id of targets.keys()) { + const terminal = targets.get(id); + if (terminal) { + layoutTerminal(terminal); + } + } + if (currentTab) { + currentTab.isActive = true; + } + } + + return result; + } + + private async sync(arg?: IGetTerminalLayoutInfoArgs): Promise<{ + tasks: Map, + terminals: Map + }> { + const tasks = await this.synchingTasks; + try { + const response = await util.promisify(terminalServiceClient.list.bind(terminalServiceClient, new ListTerminalsRequest(), supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); + for (const terminal of response.getTerminalsList()) { + const alias = terminal.getAlias(); + const id = this.aliasToId.get(alias); + const annotations = terminal.getAnnotationsMap(); + const workspaceId = annotations.get('workspaceId') || ''; + const workspaceName = annotations.get('workspaceName') || ''; + const shouldPersistTerminal = tasks.has(alias) || Boolean(annotations.get('shouldPersistTerminal')); + if (id) { + const terminalProcess = this.terminalProcesses.get(id); + if (terminalProcess) { + terminalProcess.syncState = terminal.toObject(); + } + } else { + const terminalProcess = this.createTerminalProcess( + terminal.getInitialWorkdir(), + workspaceId, + workspaceName, + shouldPersistTerminal + ); + + terminalProcess.syncState = terminal.toObject(); + this.attachTerminalProcess(terminalProcess); + } + } + } catch (e) { + console.error('code server: failed to sync terminals:', e); + } + const terminals = new Map(); + for (const terminal of this.terminalProcesses.values()) { + if (terminal.syncState && (!arg || ( + arg.workspaceId === terminal.workspaceId || (terminal.alias && tasks.has(terminal.alias))) + )) { + terminals.set(terminal.id, { + id: terminal.id, + cwd: terminal.syncState.currentWorkdir, + pid: terminal.syncState.pid, + title: terminal.syncState.title, + workspaceId: terminal.workspaceId, + workspaceName: terminal.workspaceName, + isOrphan: true + }); + } + } + return { tasks, terminals }; + } + } diff --git a/src/vs/gitpod/node/server.ts b/src/vs/gitpod/node/server.ts index 1810174bc7dfd..87050f7d1920e 100644 --- a/src/vs/gitpod/node/server.ts +++ b/src/vs/gitpod/node/server.ts @@ -3,20 +3,18 @@ *--------------------------------------------------------------------------------------------*/ import type { ResolvedPlugins } from '@gitpod/gitpod-protocol'; -import { StatusServiceClient } from '@gitpod/supervisor-api-grpc/lib/status_grpc_pb'; +import { WorkspaceInfoRequest } from '@gitpod/supervisor-api-grpc/lib/info_pb'; import { TasksStatusRequest, TasksStatusResponse, TaskState, TaskStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; -import { TerminalServiceClient } from '@gitpod/supervisor-api-grpc/lib/terminal_grpc_pb'; -import { InfoServiceClient } from '@gitpod/supervisor-api-grpc/lib/info_grpc_pb'; import * as grpc from '@grpc/grpc-js'; import * as cp from 'child_process'; import * as crypto from 'crypto'; import * as fs from 'fs'; -import * as util from 'util'; import * as http from 'http'; import * as net from 'net'; import * as os from 'os'; import * as path from 'path'; import * as url from 'url'; +import * as util from 'util'; import { getPathFromAmdModule } from 'vs/base/common/amd'; import { RunOnceScheduler } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -38,6 +36,7 @@ import { ClientConnectionEvent, IPCServer, IServerChannel } from 'vs/base/parts/ import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { RemoteTerminalChannelServer } from 'vs/gitpod/node/remoteTerminalChannelServer'; +import { infoServiceClient, statusServiceClient, supervisorDeadlines, supervisorMetadata } from 'vs/gitpod/node/supervisor-client'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; @@ -77,7 +76,6 @@ import { ExtensionScanner, ExtensionScannerInput, IExtensionReference } from 'vs import { IGetEnvironmentDataArguments, IRemoteAgentEnvironmentDTO, IScanExtensionsArguments, IScanSingleExtensionArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { WorkspaceInfoRequest, WorkspaceInfoResponse } from '@gitpod/supervisor-api-grpc/lib/info_pb'; const uriTransformerPath = path.join(__dirname, '../../../gitpodUriTransformer'); const rawURITransformerFactory: (remoteAuthority: string) => IRawURITransformer = require.__$__nodeRequire(uriTransformerPath); @@ -209,7 +207,6 @@ function serveError(req: http.IncomingMessage, res: http.ServerResponse, errorCo } async function installExtensionsFromServer( - infoServiceClient: InfoServiceClient, extensionGalleryService: IExtensionGalleryService, extensionManagementService: IExtensionManagementService, requestService: IRequestService, @@ -222,7 +219,9 @@ async function installExtensionsFromServer( // 'eamodio.gitlens' ]; try { - const workspaceInfoResponse = await util.promisify(infoServiceClient.workspaceInfo.bind(infoServiceClient))(new WorkspaceInfoRequest()); + const workspaceInfoResponse = await util.promisify(infoServiceClient.workspaceInfo.bind(infoServiceClient, new WorkspaceInfoRequest(), supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); const workspaceContextUrl = URI.parse(workspaceInfoResponse.getWorkspaceContextUrl()); if (/github\.com/i.test(workspaceContextUrl.authority)) { ids.push('github.vscode-pull-request-github'); @@ -438,11 +437,6 @@ async function main(): Promise { } channelServer.registerChannel('remoteextensionsenvironment', new RemoteExtensionsEnvironment()); - const supervisorAddr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; - const infoServiceClient = new InfoServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const terminalServiceClient = new TerminalServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const statusServiceClient = new StatusServiceClient(supervisorAddr, grpc.credentials.createInsecure()); - const synchingTasks = (async () => { const tasks = new Map(); logService.info('code server: synching tasks...'); @@ -451,7 +445,7 @@ async function main(): Promise { try { const req = new TasksStatusRequest(); req.setObserve(true); - const stream = statusServiceClient.tasksStatus(req); + const stream = statusServiceClient.tasksStatus(req, supervisorMetadata); await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); @@ -480,7 +474,6 @@ async function main(): Promise { channelServer.registerChannel('remoteterminal', new RemoteTerminalChannelServer( rawURITransformerFactory, - terminalServiceClient, logService, synchingTasks )); @@ -663,7 +656,6 @@ async function main(): Promise { const extensionManagementService = accessor.get(IExtensionManagementService); channelServer.registerChannel('extensions', new ExtensionManagementChannel(extensionManagementService, requestContext => new URITransformer(rawURITransformerFactory(requestContext)))); installExtensionsFromServer( - infoServiceClient, extensionGalleryService, extensionManagementService, accessor.get(IRequestService), diff --git a/src/vs/gitpod/node/supervisor-client.ts b/src/vs/gitpod/node/supervisor-client.ts new file mode 100644 index 0000000000000..18f1975afe901 --- /dev/null +++ b/src/vs/gitpod/node/supervisor-client.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Typefox. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { InfoServiceClient } from '@gitpod/supervisor-api-grpc/lib/info_grpc_pb'; +import { StatusServiceClient } from '@gitpod/supervisor-api-grpc/lib/status_grpc_pb'; +import { TerminalServiceClient } from '@gitpod/supervisor-api-grpc/lib/terminal_grpc_pb'; +import * as grpc from '@grpc/grpc-js'; +import product from 'vs/platform/product/common/product'; + +export const supervisorDeadlines = { + long: 30 * 1000, + normal: 15 * 1000, + short: 5 * 1000 +}; +export const supervisorMetadata = new grpc.Metadata(); +const supervisorClientOptions: Partial = { + 'grpc.primary_user_agent': `${product.nameLong}/${product.version} Server`, +}; +const supervisorAddr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; +export const infoServiceClient = new InfoServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); +export const terminalServiceClient = new TerminalServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); +export const statusServiceClient = new StatusServiceClient(supervisorAddr, grpc.credentials.createInsecure(), supervisorClientOptions); diff --git a/src/vs/gitpod/node/supervisorTerminalProcess.ts b/src/vs/gitpod/node/supervisorTerminalProcess.ts index 1cbbe38d6ad05..dab8b434dda65 100644 --- a/src/vs/gitpod/node/supervisorTerminalProcess.ts +++ b/src/vs/gitpod/node/supervisorTerminalProcess.ts @@ -2,16 +2,17 @@ * Copyright (c) Typefox. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { TerminalServiceClient } from '@gitpod/supervisor-api-grpc/lib/terminal_grpc_pb'; -import { GetTerminalRequest, ListenTerminalRequest, ListenTerminalResponse, OpenTerminalRequest, OpenTerminalResponse, SetTerminalSizeRequest, ShutdownTerminalRequest, Terminal, TerminalSize, WriteTerminalRequest } from '@gitpod/supervisor-api-grpc/lib/terminal_pb'; +import { GetTerminalRequest, ListenTerminalRequest, ListenTerminalResponse, OpenTerminalRequest, SetTerminalSizeRequest, ShutdownTerminalRequest, Terminal, TerminalSize, WriteTerminalRequest } from '@gitpod/supervisor-api-grpc/lib/terminal_pb'; import { status } from '@grpc/grpc-js'; import * as util from 'util'; import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; +import { supervisorDeadlines, supervisorMetadata, terminalServiceClient } from 'vs/gitpod/node/supervisor-client'; import { ILogService } from 'vs/platform/log/common/log'; import { ITerminalChildProcess, ITerminalLaunchError } from 'vs/platform/terminal/common/terminal'; -import { IRemoteTerminalProcessEvent, IRemoteTerminalProcessReplayEvent, ReplayEntry } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; +import { IRemoteTerminalProcessEvent, IRemoteTerminalProcessReplayEvent } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; export interface OpenSupervisorTerminalProcessOptions { shell: string @@ -28,7 +29,12 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi private exitCode: number | undefined; private closeTimeout: any; - alias?: string; + syncState: Terminal.AsObject | undefined; + get alias(): string | undefined { + return this.syncState?.alias; + } + + private readonly _recorder = new TerminalRecorder(1, 1); private readonly _onProcessData = this.add(new Emitter()); get onProcessData(): Event { return this._onProcessData.event; } @@ -54,10 +60,9 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi constructor( readonly id: number, - private readonly terminalServiceClient: TerminalServiceClient, private initialCwd: string, - private readonly workspaceId: string, - private readonly workspaceName: string, + readonly workspaceId: string, + readonly workspaceName: string, readonly shouldPersist: boolean, private readonly logService: ILogService, private readonly openOptions?: OpenSupervisorTerminalProcessOptions @@ -101,8 +106,11 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi request.getAnnotationsMap().set('workspaceName', this.workspaceName); request.getAnnotationsMap().set('shouldPersistTerminal', String(this.shouldPersist)); request.setSize(this.toSize(this.openOptions.cols, this.openOptions.rows)); - const response = await util.promisify(this.terminalServiceClient.open.bind(this.terminalServiceClient))(request); - this.alias = response.getTerminal()!.getAlias(); + + const response = await util.promisify(terminalServiceClient.open.bind(terminalServiceClient, request, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.long + }))(); + this.syncState = response.getTerminal()!.toObject(); this.initialCwd = response.getTerminal()!.getCurrentWorkdir() || response.getTerminal()!.getInitialWorkdir(); this._onProcessReady.fire({ pid: response.getTerminal()!.getPid(), cwd: this.initialCwd }); @@ -164,7 +172,7 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi } const request = new ListenTerminalRequest(); request.setAlias(alias); - const stream = this.terminalServiceClient.listen(request); + const stream = terminalServiceClient.listen(request, supervisorMetadata); this.stopListen = stream.cancel.bind(stream); stream.on('end', resolve); stream.on('error', reject); @@ -203,7 +211,7 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi } if (notFound) { this.shutdownImmediate(); - } else if (typeof exitCode !== undefined) { + } else if (typeof exitCode === 'number') { this.exitCode = exitCode; this.shutdownGracefully(); } @@ -229,7 +237,9 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi try { const request = new ShutdownTerminalRequest(); request.setAlias(this.alias); - await util.promisify(this.terminalServiceClient.shutdown.bind(this.terminalServiceClient))(request); + await util.promisify(terminalServiceClient.shutdown.bind(terminalServiceClient, request, supervisorMetadata, { + deadline: Date.now() + supervisorDeadlines.short + }))(); } catch (e) { if (e && e.code === status.NOT_FOUND) { // Swallow, the pty has already been killed @@ -256,7 +266,7 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi const request = new WriteTerminalRequest(); request.setAlias(this.alias); request.setStdin(Buffer.from(data)); - this.terminalServiceClient.write(request, e => { + terminalServiceClient.write(request, supervisorMetadata, { deadline: Date.now() + supervisorDeadlines.short }, e => { if (e && e.code !== status.NOT_FOUND) { this.logService.error(`code server: ${this.id}:${this.alias} terminal: write failed:`, e); } @@ -275,7 +285,7 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi request.setAlias(this.alias); request.setSize(size); request.setForce(true); - this.terminalServiceClient.setSize(request, e => { + terminalServiceClient.setSize(request, supervisorMetadata, { deadline: Date.now() + supervisorDeadlines.short }, e => { if (e && e.code !== status.NOT_FOUND) { this.logService.error(`code server: ${this.id}:${this.alias} terminal: resize failed:`, e); } @@ -293,7 +303,7 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi try { const request = new GetTerminalRequest(); request.setAlias(this.alias); - const terminal = await util.promisify(this.terminalServiceClient.get.bind(this.terminalServiceClient))(request); + const terminal = await util.promisify(terminalServiceClient.get.bind(terminalServiceClient, request, supervisorMetadata, { deadline: Date.now() + supervisorDeadlines.short }))(); return terminal.getCurrentWorkdir(); } catch { return this.initialCwd; @@ -310,50 +320,25 @@ export class SupervisorTerminalProcess extends DisposableStore implements ITermi clearTimeout(this.closeTimeout); this.shutdownGracefully(); } - this.record(data); + this._recorder.recordData(data); } private toSize(cols: number, rows: number): TerminalSize | undefined { if (typeof cols !== 'number' || typeof rows !== 'number' || isNaN(cols) || isNaN(rows)) { return undefined; } - this.recording.cols = Math.max(cols, 1); - this.recording.rows = Math.max(rows, 1); const size = new TerminalSize(); - size.setCols(this.recording.cols); - size.setRows(this.recording.rows); + size.setCols(Math.max(cols, 1)); + size.setRows(Math.max(rows, 1)); + this._recorder.recordResize(size.getCols(), size.getRows()); return size; } - private recording: ReplayEntry = { - data: '', - cols: 0, - rows: 0 - }; - private readonly maxRecodingSize = 256 << 10; - private recordingOffset = 0; private replay(): IRemoteTerminalProcessReplayEvent { return { type: 'replay', - events: [this.recording] + events: this._recorder.generateReplayEvent().events }; } - private record(buf: string): void { - // If the buffer is larger than ours, then we only care - // about the last size bytes anyways - if (buf.length > this.maxRecodingSize) { - buf = buf.substr(buf.length - this.maxRecodingSize); - } - - // Copy in place - const remainingOffset = this.maxRecodingSize - this.recordingOffset; - this.recording.data = this.recording.data.substr(0, this.recordingOffset) + buf; - if (buf.length > remainingOffset) { - this.recording.data = buf.substr(0, remainingOffset) + this.recording.data.substr(remainingOffset); - } - - // Update location of the cursor - this.recordingOffset = (this.recordingOffset + buf.length) % this.maxRecodingSize; - } } diff --git a/yarn.lock b/yarn.lock index 836e8cb30cec1..4c08a490e8013 100644 --- a/yarn.lock +++ b/yarn.lock @@ -214,9 +214,9 @@ google-protobuf "^3.8.0-rc.1" "@grpc/grpc-js@^1.1.5", "@grpc/grpc-js@latest": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.7.tgz#d291c23ed4dfd794366d91c016e9bef2b5f7663e" - integrity sha512-hBkR/vZTodu/dA/kcKpiQtPQdjMbpfKv7RKfEByT5/7qOQNpIh2O6Sr1aldLMzstFqmGrufmR7XTc56VCMH7LA== + version "1.2.11" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.2.11.tgz#68faa56bded64844294dc6429185503376f05ff1" + integrity sha512-DZqx3nHBm2OGY7NKq4sppDEfx4nBAsQH/d/H/yxo/+BwpVLWLGs+OorpwQ+Fqd6EgpDEoi4MhqndjGUeLl/5GA== dependencies: "@types/node" ">=12.12.47" google-auth-library "^6.1.1"