diff --git a/dev-packages/application-manager/src/generator/webpack-generator.ts b/dev-packages/application-manager/src/generator/webpack-generator.ts index 0eeb0b38dc269..2db2258a524c6 100644 --- a/dev-packages/application-manager/src/generator/webpack-generator.ts +++ b/dev-packages/application-manager/src/generator/webpack-generator.ts @@ -437,6 +437,8 @@ const config = { ${this.ifPackage('@theia/git', () => `// Ensure the git locator process can the started 'git-locator-host': require.resolve('@theia/git/lib/node/git-locator/git-locator-host'),`)} ${this.ifElectron("'electron-main': require.resolve('./src-gen/backend/electron-main'),")} + ${this.ifPackage('@theia/dev-container', () => `// VS Code Dev-Container communication: + 'dev-container-server': require.resolve('@theia/dev-container/lib/dev-container-server/dev-container-server'),`)} ...commonJsLibraries }, module: { diff --git a/packages/dev-container/src/dev-container-server/dev-container-server.ts b/packages/dev-container/src/dev-container-server/dev-container-server.ts new file mode 100644 index 0000000000000..0e477150539a3 --- /dev/null +++ b/packages/dev-container/src/dev-container-server/dev-container-server.ts @@ -0,0 +1,53 @@ +// ***************************************************************************** +// Copyright (C) 2024 Typefox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { createConnection } from 'net'; +import { stdin, argv, stdout } from 'process'; + +/** + * this node.js Program is supposed to be executed by an docker exec session inside a docker container. + * It uses a tty session to listen on stdin and send on stdout all communication with the theia backend running inside the container. + */ + +let backendPort: number | undefined = undefined; +argv.slice(2).forEach(arg => { + if (arg.startsWith('-target-port')) { + backendPort = parseInt(arg.split('=')[1]); + } +}); + +if (!backendPort) { + throw new Error('please start with -target-port={port number}'); +} +if (stdin.isTTY) { + stdin.setRawMode(true); +} +const connection = createConnection(backendPort, '0.0.0.0'); + +connection.pipe(stdout); +stdin.pipe(connection); + +connection.on('error', error => { + console.error('connection error', error); +}); + +connection.on('close', () => { + console.log('connection closed'); + process.exit(0); +}); + +// keep the process running +setInterval(() => { }, 1 << 30); diff --git a/packages/dev-container/src/electron-node/docker-container-service.ts b/packages/dev-container/src/electron-node/docker-container-service.ts index eee84127f0bf4..59fa62585317a 100644 --- a/packages/dev-container/src/electron-node/docker-container-service.ts +++ b/packages/dev-container/src/electron-node/docker-container-service.ts @@ -81,11 +81,11 @@ export class DockerContainerService { const containerCreateOptions: Docker.ContainerCreateOptions = { Tty: true, ExposedPorts: { - [`${port}/tcp`]: {}, + // [`${port}/tcp`]: {}, }, HostConfig: { PortBindings: { - [`${port}/tcp`]: [{ HostPort: '0' }], + // [`${port}/tcp`]: [{ HostPort: '0' }], }, Mounts: [{ Source: workspace.path.toString(), diff --git a/packages/dev-container/src/electron-node/remote-container-connection-provider.ts b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts index 97b6921d4af99..7b1bf7e069c53 100644 --- a/packages/dev-container/src/electron-node/remote-container-connection-provider.ts +++ b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts @@ -17,7 +17,7 @@ import * as net from 'net'; import { ContainerConnectionOptions, ContainerConnectionResult, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider'; import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types'; -import { RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service'; +import { RemoteSetupResult, RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service'; import { RemoteConnectionService } from '@theia/remote/lib/electron-node/remote-connection-service'; import { RemoteProxyServerProvider } from '@theia/remote/lib/electron-node/remote-proxy-server-provider'; import { Emitter, Event, MessageService } from '@theia/core'; @@ -70,11 +70,13 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection report('Connecting to remote system...'); const remote = await this.createContainerConnection(container, dockerConnection, port); - await this.remoteSetup.setup({ + const result = await this.remoteSetup.setup({ connection: remote, report, nodeDownloadTemplate: options.nodeDownloadTemplate }); + remote.remoteSetupResult = result; + const registration = this.remoteConnectionService.register(remote); const server = await this.serverProvider.getProxyServer(socket => { remote.forwardOut(socket); @@ -107,7 +109,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection type: 'container', docker, container, - port + port, })); } @@ -142,6 +144,8 @@ export class RemoteDockerContainerConnection implements RemoteConnection { containerInfo: Docker.ContainerInspectInfo | undefined; + remoteSetupResult: RemoteSetupResult; + protected activeTerminalSession: ContainerTerminalSession | undefined; protected readonly onDidDisconnectEmitter = new Emitter(); @@ -159,13 +163,20 @@ export class RemoteDockerContainerConnection implements RemoteConnection { } async forwardOut(socket: Socket): Promise { - if (!this.containerInfo) { - this.containerInfo = await this.container.inspect(); + const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`; + const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`; + try { + const ttySession = await this.container.exec({ + Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${this.remotePort}`], + AttachStdin: true, AttachStdout: true, AttachStderr: true + }); + const stream = await ttySession.start({ hijack: true, stdin: true }); + + socket.pipe(stream); + ttySession.modem.demuxStream(stream, socket, socket); + } catch (e) { + console.error(e); } - const portMapping = this.containerInfo.NetworkSettings.Ports[`${this.remotePort}/tcp`][0]; - const connectSocket = new Socket({ readable: true, writable: true }).connect(parseInt(portMapping.HostPort), portMapping.HostIp); - socket.pipe(connectSocket); - connectSocket.pipe(socket); } async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise { diff --git a/packages/remote/src/electron-node/setup/remote-setup-service.ts b/packages/remote/src/electron-node/setup/remote-setup-service.ts index 546695d9ecc15..951f62429da8f 100644 --- a/packages/remote/src/electron-node/setup/remote-setup-service.ts +++ b/packages/remote/src/electron-node/setup/remote-setup-service.ts @@ -29,6 +29,11 @@ export interface RemoteSetupOptions { nodeDownloadTemplate?: string; } +export interface RemoteSetupResult { + applicationDirectory: string; + nodeDirectory: string; +} + @injectable() export class RemoteSetupService { @@ -47,7 +52,7 @@ export class RemoteSetupService { @inject(ApplicationPackage) protected readonly applicationPackage: ApplicationPackage; - async setup(options: RemoteSetupOptions): Promise { + async setup(options: RemoteSetupOptions): Promise { const { connection, report, @@ -86,6 +91,10 @@ export class RemoteSetupService { report('Starting application on remote...'); const port = await this.startApplication(connection, platform, applicationDirectory, remoteNodeDirectory); connection.remotePort = port; + return { + applicationDirectory: libDir, + nodeDirectory: remoteNodeDirectory + }; } protected async startApplication(connection: RemoteConnection, platform: RemotePlatform, remotePath: string, nodeDir: string): Promise {