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/examples/browser/package.json b/examples/browser/package.json
index 9da407c65339e..daa632e7b1e47 100644
--- a/examples/browser/package.json
+++ b/examples/browser/package.json
@@ -27,6 +27,7 @@
"@theia/console": "1.47.0",
"@theia/core": "1.47.0",
"@theia/debug": "1.47.0",
+ "@theia/dev-container": "1.47.0",
"@theia/editor": "1.47.0",
"@theia/editor-preview": "1.47.0",
"@theia/file-search": "1.47.0",
diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json
index 0debed71dc386..8318b446c62cb 100644
--- a/examples/browser/tsconfig.json
+++ b/examples/browser/tsconfig.json
@@ -23,6 +23,9 @@
{
"path": "../../packages/debug"
},
+ {
+ "path": "../../packages/dev-container"
+ },
{
"path": "../../packages/editor"
},
diff --git a/examples/electron/package.json b/examples/electron/package.json
index 19c9ba0c978b3..2e04a9ffcf28b 100644
--- a/examples/electron/package.json
+++ b/examples/electron/package.json
@@ -27,6 +27,7 @@
"@theia/console": "1.47.0",
"@theia/core": "1.47.0",
"@theia/debug": "1.47.0",
+ "@theia/dev-container": "1.47.0",
"@theia/editor": "1.47.0",
"@theia/editor-preview": "1.47.0",
"@theia/electron": "1.47.0",
diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json
index c1c637281a2a3..a5073361ded87 100644
--- a/examples/electron/tsconfig.json
+++ b/examples/electron/tsconfig.json
@@ -26,6 +26,9 @@
{
"path": "../../packages/debug"
},
+ {
+ "path": "../../packages/dev-container"
+ },
{
"path": "../../packages/editor"
},
diff --git a/packages/core/src/browser/window/window-service.ts b/packages/core/src/browser/window/window-service.ts
index 34adb43737c22..694046fe910a2 100644
--- a/packages/core/src/browser/window/window-service.ts
+++ b/packages/core/src/browser/window/window-service.ts
@@ -18,6 +18,11 @@ import { StopReason } from '../../common/frontend-application-state';
import { Event } from '../../common/event';
import { NewWindowOptions, WindowSearchParams } from '../../common/window';
+export interface WindowReloadOptions {
+ search?: WindowSearchParams,
+ hash?: string
+}
+
/**
* Service for opening new browser windows.
*/
@@ -35,7 +40,7 @@ export interface WindowService {
* Opens a new default window.
* - In electron and in the browser it will open the default window without a pre-defined content.
*/
- openNewDefaultWindow(params?: WindowSearchParams): void;
+ openNewDefaultWindow(params?: WindowReloadOptions): void;
/**
* Fires when the `window` unloads. The unload event is inevitable. On this event, the frontend application can save its state and release resource.
@@ -64,5 +69,5 @@ export interface WindowService {
/**
* Reloads the window according to platform.
*/
- reload(params?: WindowSearchParams): void;
+ reload(params?: WindowReloadOptions): void;
}
diff --git a/packages/core/src/electron-browser/window/electron-window-service.ts b/packages/core/src/electron-browser/window/electron-window-service.ts
index 7777063b67e0c..f2ec5d6ae2edc 100644
--- a/packages/core/src/electron-browser/window/electron-window-service.ts
+++ b/packages/core/src/electron-browser/window/electron-window-service.ts
@@ -21,6 +21,7 @@ import { ElectronMainWindowService } from '../../electron-common/electron-main-w
import { ElectronWindowPreferences } from './electron-window-preferences';
import { ConnectionCloseService } from '../../common/messaging/connection-management';
import { FrontendIdProvider } from '../../browser/messaging/frontend-id-provider';
+import { WindowReloadOptions } from '../../browser/window/window-service';
@injectable()
export class ElectronWindowService extends DefaultWindowService {
@@ -86,12 +87,20 @@ export class ElectronWindowService extends DefaultWindowService {
}
}
- override reload(params?: WindowSearchParams): void {
+ override reload(params?: WindowReloadOptions): void {
if (params) {
- const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&');
- location.search = query;
+ const newLocation = new URL(location.href);
+ if (params.search) {
+ const query = Object.entries(params.search).map(([name, value]) => `${name}=${value}`).join('&');
+ newLocation.search = query;
+ }
+ if (params.hash) {
+ newLocation.hash = '#' + params.hash;
+ }
+ location.assign(newLocation);
} else {
window.electronTheiaCore.requestReload();
}
}
}
+
diff --git a/packages/dev-container/.eslintrc.js b/packages/dev-container/.eslintrc.js
new file mode 100644
index 0000000000000..13089943582b6
--- /dev/null
+++ b/packages/dev-container/.eslintrc.js
@@ -0,0 +1,10 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: [
+ '../../configs/build.eslintrc.json'
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: 'tsconfig.json'
+ }
+};
diff --git a/packages/dev-container/README.md b/packages/dev-container/README.md
new file mode 100644
index 0000000000000..b9ce2d06af2e5
--- /dev/null
+++ b/packages/dev-container/README.md
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
ECLIPSE THEIA - DEV-CONTAINER EXTENSION
+
+
+
+
+
+## Description
+
+The `@theia/dev-container` extension provides functionality to create, start and connect to development containers similiar to the
+[vscode Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
+
+The full devcontainer.json Schema can be found [here](https://containers.dev/implementors/json_reference/).
+Currently only a small number of configuration file properties are implemented. Those include the following:
+- name
+- Image
+- dockerfile/build.dockerfile
+- build.context
+- location
+- forwardPorts
+- mounts
+
+see `main-container-creation-contributions.ts` for how to implementations or how to implement additional ones.
+
+
+## Additional Information
+
+- [Theia - GitHub](https://github.com/eclipse-theia/theia)
+- [Theia - Website](https://theia-ide.org/)
+
+## License
+
+- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
+- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
+
+## Trademark
+"Theia" is a trademark of the Eclipse Foundation
+https://www.eclipse.org/theia
diff --git a/packages/dev-container/package.json b/packages/dev-container/package.json
new file mode 100644
index 0000000000000..246fa87cc3add
--- /dev/null
+++ b/packages/dev-container/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@theia/dev-container",
+ "version": "1.47.0",
+ "description": "Theia - Editor Preview Extension",
+ "dependencies": {
+ "@theia/core": "1.47.0",
+ "@theia/output": "1.47.0",
+ "@theia/remote": "1.47.0",
+ "@theia/workspace": "1.47.0",
+ "dockerode": "^4.0.2",
+ "uuid": "^8.0.0",
+ "jsonc-parser": "^2.2.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "theiaExtensions": [
+ {
+ "frontendElectron": "lib/electron-browser/dev-container-frontend-module",
+ "backendElectron": "lib/electron-node/dev-container-backend-module"
+ }
+ ],
+ "keywords": [
+ "theia-extension"
+ ],
+ "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eclipse-theia/theia.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eclipse-theia/theia/issues"
+ },
+ "homepage": "https://github.com/eclipse-theia/theia",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "build": "theiaext build",
+ "clean": "theiaext clean",
+ "compile": "theiaext compile",
+ "lint": "theiaext lint",
+ "test": "theiaext test",
+ "watch": "theiaext watch"
+ },
+ "devDependencies": {
+ "@theia/ext-scripts": "1.47.0",
+ "@types/dockerode": "^3.3.23"
+ },
+ "nyc": {
+ "extends": "../../configs/nyc.json"
+ }
+}
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-browser/container-connection-contribution.ts b/packages/dev-container/src/electron-browser/container-connection-contribution.ts
new file mode 100644
index 0000000000000..415ada831f733
--- /dev/null
+++ b/packages/dev-container/src/electron-browser/container-connection-contribution.ts
@@ -0,0 +1,103 @@
+// *****************************************************************************
+// 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 { inject, injectable } from '@theia/core/shared/inversify';
+import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
+import { LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
+import { RemotePreferences } from '@theia/remote/lib/electron-browser/remote-preferences';
+import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
+import { Command, QuickInputService } from '@theia/core';
+import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
+import { ContainerOutputProvider } from './container-output-provider';
+
+export namespace RemoteContainerCommands {
+ export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
+ id: 'dev-container:reopen-in-container',
+ label: 'Reopen in Container',
+ category: 'Dev Container'
+ }, 'theia/dev-container/connect');
+}
+
+const LAST_USED_CONTAINER = 'lastUsedContainer';
+@injectable()
+export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution {
+
+ @inject(RemoteContainerConnectionProvider)
+ protected readonly connectionProvider: RemoteContainerConnectionProvider;
+
+ @inject(RemotePreferences)
+ protected readonly remotePreferences: RemotePreferences;
+
+ @inject(WorkspaceStorageService)
+ protected readonly workspaceStorageService: WorkspaceStorageService;
+
+ @inject(WorkspaceService)
+ protected readonly workspaceService: WorkspaceService;
+
+ @inject(QuickInputService)
+ protected readonly quickInputService: QuickInputService;
+
+ @inject(ContainerOutputProvider)
+ protected readonly containerOutputProvider: ContainerOutputProvider;
+
+ registerRemoteCommands(registry: RemoteRegistry): void {
+ registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
+ execute: () => this.openInContainer()
+ });
+ }
+
+ async openInContainer(): Promise {
+ const devcontainerFile = await this.getOrSelectDevcontainerFile();
+ if (!devcontainerFile) {
+ return;
+ }
+ const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile}`;
+ const lastContainerInfo = await this.workspaceStorageService.getData(lastContainerInfoKey);
+
+ this.containerOutputProvider.openChannel();
+
+ const connectionResult = await this.connectionProvider.connectToContainer({
+ nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
+ lastContainerInfo,
+ devcontainerFile
+ });
+
+ this.workspaceStorageService.setData(lastContainerInfoKey, {
+ id: connectionResult.containerId,
+ lastUsed: Date.now()
+ });
+
+ this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
+ }
+
+ async getOrSelectDevcontainerFile(): Promise {
+ const devcontainerFiles = await this.connectionProvider.getDevContainerFiles();
+
+ if (devcontainerFiles.length === 1) {
+ return devcontainerFiles[0].path;
+ }
+
+ return (await this.quickInputService.pick(devcontainerFiles.map(file => ({
+ type: 'item',
+ label: file.name,
+ description: file.path,
+ file: file.path,
+ })), {
+ title: 'Select a devcontainer.json file'
+ }))?.file;
+ }
+
+}
diff --git a/packages/dev-container/src/electron-browser/container-output-provider.ts b/packages/dev-container/src/electron-browser/container-output-provider.ts
new file mode 100644
index 0000000000000..f0891c943d085
--- /dev/null
+++ b/packages/dev-container/src/electron-browser/container-output-provider.ts
@@ -0,0 +1,36 @@
+// *****************************************************************************
+// 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 { injectable, inject } from '@theia/core/shared/inversify';
+import { OutputChannel, OutputChannelManager } from '@theia/output/lib/browser/output-channel';
+
+@injectable()
+export class ContainerOutputProvider implements ContainerOutputProvider {
+
+ @inject(OutputChannelManager)
+ protected readonly outputChannelManager: OutputChannelManager;
+
+ protected currentChannel?: OutputChannel;
+
+ openChannel(): void {
+ this.currentChannel = this.outputChannelManager.getChannel('Container');
+ this.currentChannel.show();
+ };
+
+ onRemoteOutput(output: string): void {
+ this.currentChannel?.appendLine(output);
+ }
+}
diff --git a/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts b/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts
new file mode 100644
index 0000000000000..77cdd79844b72
--- /dev/null
+++ b/packages/dev-container/src/electron-browser/dev-container-frontend-module.ts
@@ -0,0 +1,33 @@
+// *****************************************************************************
+// 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 { ContainerModule } from '@theia/core/shared/inversify';
+import { RemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
+import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
+import { ContainerConnectionContribution } from './container-connection-contribution';
+import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
+import { ContainerOutputProvider } from './container-output-provider';
+
+export default new ContainerModule(bind => {
+ bind(ContainerConnectionContribution).toSelf().inSingletonScope();
+ bind(RemoteRegistryContribution).toService(ContainerConnectionContribution);
+
+ bind(ContainerOutputProvider).toSelf().inSingletonScope();
+
+ bind(RemoteContainerConnectionProvider).toDynamicValue(ctx => {
+ const outputProvider = ctx.container.get(ContainerOutputProvider);
+ return ServiceConnectionProvider.createLocalProxy(ctx.container, RemoteContainerConnectionProviderPath, outputProvider);
+ }).inSingletonScope();
+});
diff --git a/packages/dev-container/src/electron-common/container-output-provider.ts b/packages/dev-container/src/electron-common/container-output-provider.ts
new file mode 100644
index 0000000000000..ae7d1b712e788
--- /dev/null
+++ b/packages/dev-container/src/electron-common/container-output-provider.ts
@@ -0,0 +1,19 @@
+// *****************************************************************************
+// 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
+// *****************************************************************************
+
+export interface ContainerOutputProvider {
+ onRemoteOutput(output: string): void;
+}
diff --git a/packages/dev-container/src/electron-common/remote-container-connection-provider.ts b/packages/dev-container/src/electron-common/remote-container-connection-provider.ts
new file mode 100644
index 0000000000000..236cb3113cd90
--- /dev/null
+++ b/packages/dev-container/src/electron-common/remote-container-connection-provider.ts
@@ -0,0 +1,49 @@
+// *****************************************************************************
+// 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 { RpcServer } from '@theia/core';
+import { ContainerOutputProvider } from './container-output-provider';
+
+// *****************************************************************************
+export const RemoteContainerConnectionProviderPath = '/remote/container';
+
+export const RemoteContainerConnectionProvider = Symbol('RemoteContainerConnectionProvider');
+
+export interface ContainerConnectionOptions {
+ nodeDownloadTemplate?: string;
+ lastContainerInfo?: LastContainerInfo
+ devcontainerFile: string;
+}
+
+export interface LastContainerInfo {
+ id: string;
+ lastUsed: number;
+}
+
+export interface ContainerConnectionResult {
+ port: string;
+ workspacePath: string;
+ containerId: string;
+}
+
+export interface DevContainerFile {
+ name: string;
+ path: string;
+}
+
+export interface RemoteContainerConnectionProvider extends RpcServer {
+ connectToContainer(options: ContainerConnectionOptions): Promise;
+ getDevContainerFiles(): Promise;
+}
diff --git a/packages/dev-container/src/electron-node/dev-container-backend-module.ts b/packages/dev-container/src/electron-node/dev-container-backend-module.ts
new file mode 100644
index 0000000000000..cac6e5e74ebf4
--- /dev/null
+++ b/packages/dev-container/src/electron-node/dev-container-backend-module.ts
@@ -0,0 +1,47 @@
+// *****************************************************************************
+// 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 { ContainerModule } from '@theia/core/shared/inversify';
+import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
+import { DevContainerConnectionProvider } from './remote-container-connection-provider';
+import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
+import { ContainerCreationContribution, DockerContainerService } from './docker-container-service';
+import { bindContributionProvider, ConnectionHandler, RpcConnectionHandler } from '@theia/core';
+import { registerContainerCreationContributions } from './devcontainer-contributions/main-container-creation-contributions';
+import { DevContainerFileService } from './dev-container-file-service';
+import { ContainerOutputProvider } from '../electron-common/container-output-provider';
+
+export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
+ bindContributionProvider(bind, ContainerCreationContribution);
+ registerContainerCreationContributions(bind);
+
+ bind(DevContainerConnectionProvider).toSelf().inSingletonScope();
+ bind(RemoteContainerConnectionProvider).toService(DevContainerConnectionProvider);
+ bind(ConnectionHandler).toDynamicValue(ctx =>
+ new RpcConnectionHandler(RemoteContainerConnectionProviderPath, client => {
+ const server = ctx.container.get(RemoteContainerConnectionProvider);
+ server.setClient(client);
+ client.onDidCloseConnection(() => server.dispose());
+ return server;
+ }));
+});
+
+export default new ContainerModule((bind, unbind, isBound, rebind) => {
+ bind(DockerContainerService).toSelf().inSingletonScope();
+ bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule);
+
+ bind(DevContainerFileService).toSelf().inSingletonScope();
+});
diff --git a/packages/dev-container/src/electron-node/dev-container-file-service.ts b/packages/dev-container/src/electron-node/dev-container-file-service.ts
new file mode 100644
index 0000000000000..00f59186f7710
--- /dev/null
+++ b/packages/dev-container/src/electron-node/dev-container-file-service.ts
@@ -0,0 +1,72 @@
+// *****************************************************************************
+// 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 { inject, injectable } from '@theia/core/shared/inversify';
+import { WorkspaceServer } from '@theia/workspace/lib/common';
+import { DevContainerFile } from '../electron-common/remote-container-connection-provider';
+import { DevContainerConfiguration } from './devcontainer-file';
+import { parse } from 'jsonc-parser';
+import * as fs from '@theia/core/shared/fs-extra';
+import { Path, URI } from '@theia/core';
+
+@injectable()
+export class DevContainerFileService {
+
+ @inject(WorkspaceServer)
+ protected readonly workspaceServer: WorkspaceServer;
+
+ async getConfiguration(path: string): Promise {
+ const configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration;
+ if (!configuration) {
+ throw new Error(`devcontainer file ${path} could not be parsed`);
+ }
+
+ configuration.location = path;
+ return configuration;
+ }
+
+ async getAvailableFiles(): Promise {
+ const workspace = await this.workspaceServer.getMostRecentlyUsedWorkspace();
+ if (!workspace) {
+ return [];
+ }
+
+ const devcontainerPath = new URI(workspace).path.join('.devcontainer').fsPath();
+
+ return (await this.searchForDevontainerJsonFiles(devcontainerPath, 1)).map(file => ({
+ name: parse(fs.readFileSync(file, 'utf-8')).name ?? 'devcontainer',
+ path: file
+ }));
+
+ }
+
+ protected async searchForDevontainerJsonFiles(directory: string, depth: number): Promise {
+ if (depth < 0) {
+ return [];
+ }
+ const filesPaths = (await fs.readdir(directory)).map(file => new Path(directory).join(file).fsPath());
+
+ const devcontainerFiles = [];
+ for (const file of filesPaths) {
+ if (file.endsWith('devcontainer.json')) {
+ devcontainerFiles.push(file);
+ } else if ((await fs.stat(file)).isDirectory()) {
+ devcontainerFiles.push(...await this.searchForDevontainerJsonFiles(file, depth - 1));
+ }
+ }
+ return devcontainerFiles;
+ }
+}
diff --git a/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts b/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts
new file mode 100644
index 0000000000000..c0c91473201f0
--- /dev/null
+++ b/packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts
@@ -0,0 +1,140 @@
+// *****************************************************************************
+// 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 * as Docker from 'dockerode';
+import { injectable, interfaces } from '@theia/core/shared/inversify';
+import { ContainerCreationContribution } from '../docker-container-service';
+import { DevContainerConfiguration, DockerfileContainer, ImageContainer, NonComposeContainerBase } from '../devcontainer-file';
+import { Path } from '@theia/core';
+import { ContainerOutputProvider } from '../../electron-common/container-output-provider';
+
+export function registerContainerCreationContributions(bind: interfaces.Bind): void {
+ bind(ContainerCreationContribution).to(ImageFileContribution).inSingletonScope();
+ bind(ContainerCreationContribution).to(DockerFileContribution).inSingletonScope();
+ bind(ContainerCreationContribution).to(ForwardPortsContribution).inSingletonScope();
+ bind(ContainerCreationContribution).to(MountsContribution).inSingletonScope();
+}
+
+@injectable()
+export class ImageFileContribution implements ContainerCreationContribution {
+ async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: ImageContainer,
+ api: Docker, outputprovider: ContainerOutputProvider): Promise {
+ if (containerConfig.image) {
+ await new Promise((res, rej) => api.pull(containerConfig.image, {}, (err, stream) => {
+ if (err) {
+ rej(err);
+ } else {
+ api.modem.followProgress(stream, (error, output) => error ?
+ rej(error) :
+ res(), progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress)));
+ }
+ }));
+ createOptions.Image = containerConfig.image;
+ }
+ }
+}
+
+@injectable()
+export class DockerFileContribution implements ContainerCreationContribution {
+ async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DockerfileContainer,
+ api: Docker, outputprovider: ContainerOutputProvider): Promise {
+ // check if dockerfile container
+ if (containerConfig.dockerFile || containerConfig.build?.dockerfile) {
+ const dockerfile = (containerConfig.dockerFile ?? containerConfig.build?.dockerfile) as string;
+ const buildStream = await api.buildImage({
+ context: containerConfig.context ?? new Path(containerConfig.location as string).dir.fsPath(),
+ src: [dockerfile],
+ } as Docker.ImageBuildContext, {
+ buildargs: containerConfig.build?.args
+ });
+ // TODO probably have some console windows showing the output of the build
+ const imageId = await new Promise((res, rej) => api.modem.followProgress(buildStream, (err, outputs) => {
+ if (err) {
+ rej(err);
+ } else {
+ for (let i = outputs.length - 1; i >= 0; i--) {
+ if (outputs[i].aux?.ID) {
+ res(outputs[i].aux.ID);
+ return;
+ }
+ }
+ }
+ }, progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress))));
+ createOptions.Image = imageId;
+ }
+ }
+}
+
+@injectable()
+export class ForwardPortsContribution implements ContainerCreationContribution {
+ async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise {
+ if (!containerConfig.forwardPorts) {
+ return;
+ }
+
+ for (const port of containerConfig.forwardPorts) {
+ let portKey: string;
+ let hostPort: string;
+ if (typeof port === 'string') {
+ const parts = port.split(':');
+ portKey = isNaN(+parts[0]) ? parts[0] : `${parts[0]}/tcp`;
+ hostPort = parts[1] ?? parts[0];
+ } else {
+ portKey = `${port}/tcp`;
+ hostPort = port.toString();
+ }
+ createOptions.ExposedPorts![portKey] = {};
+ createOptions.HostConfig!.PortBindings[portKey] = [{ HostPort: hostPort }];
+ }
+
+ }
+
+}
+
+@injectable()
+export class MountsContribution implements ContainerCreationContribution {
+ async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise {
+ if (!containerConfig.mounts) {
+ return;
+ }
+
+ createOptions.HostConfig!.Mounts!.push(...(containerConfig as NonComposeContainerBase)?.mounts
+ ?.map(mount => typeof mount === 'string' ?
+ this.parseMountString(mount) :
+ { Source: mount.source, Target: mount.target, Type: mount.type ?? 'bind' }) ?? []);
+ }
+
+ parseMountString(mount: string): Docker.MountSettings {
+ const parts = mount.split(',');
+ return {
+ Source: parts.find(part => part.startsWith('source=') || part.startsWith('src='))?.split('=')[1]!,
+ Target: parts.find(part => part.startsWith('target=') || part.startsWith('dst='))?.split('=')[1]!,
+ Type: (parts.find(part => part.startsWith('type='))?.split('=')[1] ?? 'bind') as Docker.MountType
+ };
+ }
+}
+
+export namespace OutputHelper {
+ export interface Progress {
+ id?: string;
+ stream: string;
+ status?: string;
+ progress?: string;
+ }
+
+ export function parseProgress(progress: Progress): string {
+ return progress.stream ?? progress.progress ?? progress.status ?? '';
+ }
+}
diff --git a/packages/dev-container/src/electron-node/devcontainer-file.ts b/packages/dev-container/src/electron-node/devcontainer-file.ts
new file mode 100644
index 0000000000000..5bd948dd17a90
--- /dev/null
+++ b/packages/dev-container/src/electron-node/devcontainer-file.ts
@@ -0,0 +1,380 @@
+// *****************************************************************************
+// 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
+// *****************************************************************************
+
+/**
+ * Defines a dev container
+ * type generated from https://containers.dev/implementors/json_schema/ and modified
+ */
+export type DevContainerConfiguration = (((DockerfileContainer | ImageContainer) & (NonComposeContainerBase)) | ComposeContainer) & DevContainerCommon & { location?: string };
+
+export type DockerfileContainer = {
+ /**
+ * Docker build-related options.
+ */
+ build: {
+ /**
+ * The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file.
+ */
+ dockerfile: string
+ /**
+ * The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file.
+ */
+ context?: string
+ } & BuildOptions
+ [k: string]: unknown
+} | {
+ /**
+ * The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file.
+ */
+ dockerFile: string
+ /**
+ * The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file.
+ */
+ context?: string
+
+ /**
+ * Docker build-related options.
+ */
+ build?: {
+ /**
+ * Target stage in a multi-stage build.
+ */
+ target?: string
+ /**
+ * Build arguments.
+ */
+ args?: {
+ [k: string]: string
+ }
+ /**
+ * The image to consider as a cache. Use an array to specify multiple images.
+ */
+ cacheFrom?: string | string[]
+ [k: string]: unknown
+ }
+ [k: string]: unknown
+};
+
+export interface BuildOptions {
+ /**
+ * Target stage in a multi-stage build.
+ */
+ target?: string
+ /**
+ * Build arguments.
+ */
+ args?: {
+ [k: string]: string
+ }
+ /**
+ * The image to consider as a cache. Use an array to specify multiple images.
+ */
+ cacheFrom?: string | string[]
+ [k: string]: unknown
+}
+export interface ImageContainer {
+ /**
+ * The docker image that will be used to create the container.
+ */
+ image: string
+ [k: string]: unknown
+}
+
+export interface NonComposeContainerBase {
+ /**
+ * Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to
+ * the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. '8000:8010'.
+ */
+ appPort?: number | string | (number | string)[]
+ /**
+ * Container environment variables.
+ */
+ containerEnv?: {
+ [k: string]: string
+ }
+ /**
+ * The user the container will be started with. The default is the user on the Docker image.
+ */
+ containerUser?: string
+ /**
+ * Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.
+ */
+ mounts?: (string | MountConfig)[]
+ /**
+ * The arguments required when starting in the container.
+ */
+ runArgs?: string[]
+ /**
+ * Action to take when the user disconnects from the container in their editor. The default is to stop the container.
+ */
+ shutdownAction?: 'none' | 'stopContainer'
+ /**
+ * Whether to overwrite the command specified in the image. The default is true.
+ */
+ overrideCommand?: boolean
+ /**
+ * The path of the workspace folder inside the container.
+ */
+ workspaceFolder?: string
+ /**
+ * The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project.
+ */
+ workspaceMount?: string
+ [k: string]: unknown
+}
+
+export interface ComposeContainer {
+ /**
+ * The name of the docker-compose file(s) used to start the services.
+ */
+ dockerComposeFile: string | string[]
+ /**
+ * The service you want to work on. This is considered the primary container for your dev environment which your editor will connect to.
+ */
+ service: string
+ /**
+ * An array of services that should be started and stopped.
+ */
+ runServices?: string[]
+ /**
+ * The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml.
+ */
+ workspaceFolder: string
+ /**
+ * Action to take when the user disconnects from the primary container in their editor. The default is to stop all of the compose containers.
+ */
+ shutdownAction?: 'none' | 'stopCompose'
+ /**
+ * Whether to overwrite the command specified in the image. The default is false.
+ */
+ overrideCommand?: boolean
+ [k: string]: unknown
+}
+
+export interface DevContainerCommon {
+ /**
+ * A name for the dev container which can be displayed to the user.
+ */
+ name?: string
+ /**
+ * Features to add to the dev container.
+ */
+ features?: {
+ [k: string]: unknown
+ }
+ /**
+ * Array consisting of the Feature id (without the semantic version) of Features in the order the user wants them to be installed.
+ */
+ overrideFeatureInstallOrder?: string[]
+ /**
+ * Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format 'host:port_number'.
+ */
+ forwardPorts?: (number | string)[]
+ portsAttributes?: {
+ /**
+ * A port, range of ports (ex. '40000-55000'), or regular expression (ex. '.+\\/server.js').
+ * For a port number or range, the attributes will apply to that port number or range of port numbers.
+ * Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.
+ *
+ * This interface was referenced by `undefined`'s JSON-Schema definition
+ * via the `patternProperty` '(^\d+(-\d+)?$)|(.+)'.
+ */
+ [k: string]: {
+ /**
+ * Defines the action that occurs when the port is discovered for automatic forwarding
+ */
+ onAutoForward?:
+ | 'notify'
+ | 'openBrowser'
+ | 'openBrowserOnce'
+ | 'openPreview'
+ | 'silent'
+ | 'ignore'
+ /**
+ * Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.
+ */
+ elevateIfNeeded?: boolean
+ /**
+ * Label that will be shown in the UI for this port.
+ */
+ label?: string
+ requireLocalPort?: boolean
+ /**
+ * The protocol to use when forwarding this port.
+ */
+ protocol?: 'http' | 'https'
+ [k: string]: unknown
+ }
+ }
+ otherPortsAttributes?: {
+ /**
+ * Defines the action that occurs when the port is discovered for automatic forwarding
+ */
+ onAutoForward?:
+ | 'notify'
+ | 'openBrowser'
+ | 'openPreview'
+ | 'silent'
+ | 'ignore'
+ /**
+ * Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.
+ */
+ elevateIfNeeded?: boolean
+ /**
+ * Label that will be shown in the UI for this port.
+ */
+ label?: string
+ requireLocalPort?: boolean
+ /**
+ * The protocol to use when forwarding this port.
+ */
+ protocol?: 'http' | 'https'
+ }
+ /**
+ * Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder.
+ */
+ updateRemoteUserUID?: boolean
+ /**
+ * Remote environment variables to set for processes spawned in the container including lifecycle scripts and any remote editor/IDE server process.
+ */
+ remoteEnv?: {
+ [k: string]: string | null
+ }
+ /**
+ * The username to use for spawning processes in the container including lifecycle scripts and any remote editor/IDE server process.
+ * The default is the same user as the container.
+ */
+ remoteUser?: string
+ /**
+ * A command to run locally before anything else. This command is run before 'onCreateCommand'.
+ * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
+ */
+ initializeCommand?: string | string[]
+ /**
+ * A command to run when creating the container. This command is run after 'initializeCommand' and before 'updateContentCommand'.
+ * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
+ */
+ onCreateCommand?:
+ | string
+ | string[]
+ | {
+ [k: string]: string | string[]
+ }
+ /**
+ * A command to run when creating the container and rerun when the workspace content was updated while creating the container.
+ * This command is run after 'onCreateCommand' and before 'postCreateCommand'. If this is a single string, it will be run in a shell.
+ * If this is an array of strings, it will be run as a single command without shell.
+ */
+ updateContentCommand?:
+ | string
+ | string[]
+ | {
+ [k: string]: string | string[]
+ }
+ /**
+ * A command to run after creating the container. This command is run after 'updateContentCommand' and before 'postStartCommand'.
+ * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
+ */
+ postCreateCommand?:
+ | string
+ | string[]
+ | {
+ [k: string]: string | string[]
+ }
+ /**
+ * A command to run after starting the container. This command is run after 'postCreateCommand' and before 'postAttachCommand'.
+ * If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
+ */
+ postStartCommand?:
+ | string
+ | string[]
+ | {
+ [k: string]: string | string[]
+ }
+ /**
+ * A command to run when attaching to the container. This command is run after 'postStartCommand'. If this is a single string, it will be run in a shell.
+ * If this is an array of strings, it will be run as a single command without shell.
+ */
+ postAttachCommand?:
+ | string
+ | string[]
+ | {
+ [k: string]: string | string[]
+ }
+ /**
+ * The user command to wait for before continuing execution in the background while the UI is starting up. The default is 'updateContentCommand'.
+ */
+ waitFor?:
+ | 'initializeCommand'
+ | 'onCreateCommand'
+ | 'updateContentCommand'
+ | 'postCreateCommand'
+ | 'postStartCommand'
+ /**
+ * User environment probe to run. The default is 'loginInteractiveShell'.
+ */
+ userEnvProbe?:
+ | 'none'
+ | 'loginShell'
+ | 'loginInteractiveShell'
+ | 'interactiveShell'
+ /**
+ * Host hardware requirements.
+ */
+ hostRequirements?: {
+ /**
+ * Number of required CPUs.
+ */
+ cpus?: number
+ /**
+ * Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
+ */
+ memory?: string
+ /**
+ * Amount of required disk space in bytes. Supports units tb, gb, mb and kb.
+ */
+ storage?: string
+ gpu?:
+ | (true | false | 'optional')
+ | {
+ /**
+ * Number of required cores.
+ */
+ cores?: number
+ /**
+ * Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
+ */
+ memory?: string
+ }
+ [k: string]: unknown
+ }
+ /**
+ * Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations.
+ */
+ customizations?: {
+ [k: string]: unknown
+ }
+ additionalProperties?: {
+ [k: string]: unknown
+ }
+ [k: string]: unknown
+}
+
+export interface MountConfig {
+ source: string,
+ target: string,
+ type: 'volume' | 'bind',
+}
diff --git a/packages/dev-container/src/electron-node/docker-container-service.ts b/packages/dev-container/src/electron-node/docker-container-service.ts
new file mode 100644
index 0000000000000..3626e7c00b762
--- /dev/null
+++ b/packages/dev-container/src/electron-node/docker-container-service.ts
@@ -0,0 +1,124 @@
+// *****************************************************************************
+// 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 { ContributionProvider, URI } from '@theia/core';
+import { inject, injectable, named } from '@theia/core/shared/inversify';
+import { WorkspaceServer } from '@theia/workspace/lib/common';
+import * as fs from '@theia/core/shared/fs-extra';
+import * as Docker from 'dockerode';
+import { LastContainerInfo } from '../electron-common/remote-container-connection-provider';
+import { DevContainerConfiguration } from './devcontainer-file';
+import { DevContainerFileService } from './dev-container-file-service';
+import { ContainerOutputProvider } from '../electron-common/container-output-provider';
+
+export const ContainerCreationContribution = Symbol('ContainerCreationContributions');
+
+export interface ContainerCreationContribution {
+ handleContainerCreation(createOptions: Docker.ContainerCreateOptions,
+ containerConfig: DevContainerConfiguration,
+ api: Docker,
+ outputProvider?: ContainerOutputProvider): Promise;
+}
+
+@injectable()
+export class DockerContainerService {
+
+ @inject(WorkspaceServer)
+ protected readonly workspaceServer: WorkspaceServer;
+
+ @inject(ContributionProvider) @named(ContainerCreationContribution)
+ protected readonly containerCreationContributions: ContributionProvider;
+
+ @inject(DevContainerFileService)
+ protected readonly devContainerFileService: DevContainerFileService;
+
+ async getOrCreateContainer(docker: Docker, devcontainerFile: string,
+ lastContainerInfo?: LastContainerInfo, outputProvider?: ContainerOutputProvider): Promise {
+ let container;
+
+ const workspace = new URI(await this.workspaceServer.getMostRecentlyUsedWorkspace());
+
+ if (lastContainerInfo && fs.statSync(devcontainerFile).mtimeMs < lastContainerInfo.lastUsed) {
+ try {
+ container = docker.getContainer(lastContainerInfo.id);
+ if ((await container.inspect()).State.Running) {
+ await container.restart();
+ } else {
+ await container.start();
+ }
+ } catch (e) {
+ container = undefined;
+ console.warn('DevContainer: could not find last used container');
+ }
+ }
+ if (!container) {
+ container = await this.buildContainer(docker, devcontainerFile, workspace, outputProvider);
+ }
+ return container;
+ }
+
+ protected async buildContainer(docker: Docker, devcontainerFile: string, workspace: URI, outputProvider?: ContainerOutputProvider): Promise {
+ const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile);
+
+ if (!devcontainerConfig) {
+ // TODO add ability for user to create new config
+ throw new Error('No devcontainer.json');
+ }
+
+ const containerCreateOptions: Docker.ContainerCreateOptions = {
+ Tty: true,
+ ExposedPorts: {},
+ HostConfig: {
+ PortBindings: {},
+ Mounts: [{
+ Source: workspace.path.toString(),
+ Target: `/workspaces/${workspace.path.name}`,
+ Type: 'bind'
+ }],
+ },
+ };
+
+ for (const containerCreateContrib of this.containerCreationContributions.getContributions()) {
+ await containerCreateContrib.handleContainerCreation(containerCreateOptions, devcontainerConfig, docker, outputProvider);
+ }
+
+ // TODO add more config
+ const container = await docker.createContainer(containerCreateOptions);
+ await container.start();
+
+ return container;
+ }
+
+ protected getPortBindings(forwardPorts: (string | number)[]): { exposedPorts: {}, portBindings: {} } {
+ const res: { exposedPorts: { [key: string]: {} }, portBindings: { [key: string]: {} } } = { exposedPorts: {}, portBindings: {} };
+ for (const port of forwardPorts) {
+ let portKey: string;
+ let hostPort: string;
+ if (typeof port === 'string') {
+ const parts = port.split(':');
+ portKey = isNaN(+parts[0]) ? parts[0] : `${parts[0]}/tcp`;
+ hostPort = parts[1] ?? parts[0];
+ } else {
+ portKey = `${port}/tcp`;
+ hostPort = port.toString();
+ }
+ res.exposedPorts[portKey] = {};
+ res.portBindings[portKey] = [{ HostPort: hostPort }];
+ }
+
+ return res;
+ }
+}
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
new file mode 100644
index 0000000000000..6c80b89e57c7b
--- /dev/null
+++ b/packages/dev-container/src/electron-node/remote-container-connection-provider.ts
@@ -0,0 +1,287 @@
+// *****************************************************************************
+// 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 * as net from 'net';
+import {
+ ContainerConnectionOptions, ContainerConnectionResult,
+ DevContainerFile, RemoteContainerConnectionProvider
+} from '../electron-common/remote-container-connection-provider';
+import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types';
+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, generateUuid, MessageService, RpcServer } from '@theia/core';
+import { Socket } from 'net';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import * as Docker from 'dockerode';
+import { DockerContainerService } from './docker-container-service';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { WriteStream } from 'tty';
+import { PassThrough } from 'stream';
+import { exec } from 'child_process';
+import { DevContainerFileService } from './dev-container-file-service';
+import { ContainerOutputProvider } from '../electron-common/container-output-provider';
+
+@injectable()
+export class DevContainerConnectionProvider implements RemoteContainerConnectionProvider, RpcServer {
+
+ @inject(RemoteConnectionService)
+ protected readonly remoteConnectionService: RemoteConnectionService;
+
+ @inject(RemoteSetupService)
+ protected readonly remoteSetup: RemoteSetupService;
+
+ @inject(MessageService)
+ protected readonly messageService: MessageService;
+
+ @inject(RemoteProxyServerProvider)
+ protected readonly serverProvider: RemoteProxyServerProvider;
+
+ @inject(DockerContainerService)
+ protected readonly containerService: DockerContainerService;
+
+ @inject(DevContainerFileService)
+ protected readonly devContainerFileService: DevContainerFileService;
+
+ protected outputProvider: ContainerOutputProvider | undefined;
+
+ setClient(client: ContainerOutputProvider): void {
+ this.outputProvider = client;
+ }
+
+ async connectToContainer(options: ContainerConnectionOptions): Promise {
+ const dockerConnection = new Docker();
+ const version = await dockerConnection.version().catch(() => undefined);
+
+ if (!version) {
+ this.messageService.error('Docker Daemon is not running');
+ throw new Error('Docker is not running');
+ }
+
+ // create container
+ const progress = await this.messageService.showProgress({
+ text: 'Creating container',
+ });
+ try {
+ const container = await this.containerService.getOrCreateContainer(dockerConnection, options.devcontainerFile, options.lastContainerInfo, this.outputProvider);
+
+ // create actual connection
+ const report: RemoteStatusReport = message => progress.report({ message });
+ report('Connecting to remote system...');
+
+ const remote = await this.createContainerConnection(container, dockerConnection);
+ 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);
+ });
+ remote.onDidDisconnect(() => {
+ server.close();
+ registration.dispose();
+ });
+ const localPort = (server.address() as net.AddressInfo).port;
+ remote.localPort = localPort;
+ return {
+ containerId: container.id,
+ workspacePath: (await container.inspect()).Mounts[0].Destination,
+ port: localPort.toString(),
+ };
+ } catch (e) {
+ this.messageService.error(e.message);
+ console.error(e);
+ throw e;
+ } finally {
+ progress.cancel();
+ }
+ }
+
+ getDevContainerFiles(): Promise {
+ return this.devContainerFileService.getAvailableFiles();
+ }
+
+ async createContainerConnection(container: Docker.Container, docker: Docker): Promise {
+ return Promise.resolve(new RemoteDockerContainerConnection({
+ id: generateUuid(),
+ name: 'dev-container',
+ type: 'container',
+ docker,
+ container,
+ }));
+ }
+
+ dispose(): void {
+
+ }
+
+}
+
+export interface RemoteContainerConnectionOptions {
+ id: string;
+ name: string;
+ type: string;
+ docker: Docker;
+ container: Docker.Container;
+}
+
+interface ContainerTerminalSession {
+ execution: Docker.Exec,
+ stdout: WriteStream,
+ stderr: WriteStream,
+ executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
+}
+
+export class RemoteDockerContainerConnection implements RemoteConnection {
+
+ id: string;
+ name: string;
+ type: string;
+ localPort: number;
+ remotePort: number;
+
+ docker: Docker;
+ container: Docker.Container;
+
+ containerInfo: Docker.ContainerInspectInfo | undefined;
+
+ remoteSetupResult: RemoteSetupResult;
+
+ protected activeTerminalSession: ContainerTerminalSession | undefined;
+
+ protected readonly onDidDisconnectEmitter = new Emitter();
+ onDidDisconnect: Event = this.onDidDisconnectEmitter.event;
+
+ constructor(options: RemoteContainerConnectionOptions) {
+ this.id = options.id;
+ this.type = options.type;
+ this.name = options.name;
+ this.onDidDisconnect(() => this.dispose());
+
+ this.docker = options.docker;
+ this.container = options.container;
+ }
+
+ async forwardOut(socket: Socket): Promise {
+ 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);
+ }
+ }
+
+ async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise {
+ // return (await this.getOrCreateTerminalSession()).executeCommand(cmd, args);
+ const deferred = new Deferred();
+ try {
+ // TODO add windows container support
+ const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true });
+ let stdoutBuffer = '';
+ let stderrBuffer = '';
+ const stream = await execution?.start({});
+ const stdout = new PassThrough();
+ stdout.on('data', (chunk: Buffer) => {
+ stdoutBuffer += chunk.toString();
+ });
+ const stderr = new PassThrough();
+ stderr.on('data', (chunk: Buffer) => {
+ stderrBuffer += chunk.toString();
+ });
+ execution.modem.demuxStream(stream, stdout, stderr);
+ stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }));
+ } catch (e) {
+ deferred.reject(e);
+ }
+ return deferred.promise;
+ }
+
+ async execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise {
+ const deferred = new Deferred();
+ try {
+ // TODO add windows container support
+ const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true });
+ let stdoutBuffer = '';
+ let stderrBuffer = '';
+ const stream = await execution?.start({});
+ stream.on('close', () => {
+ if (deferred.state === 'unresolved') {
+ deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
+ }
+ });
+ const stdout = new PassThrough();
+ stdout.on('data', (data: Buffer) => {
+ if (deferred.state === 'unresolved') {
+ stdoutBuffer += data.toString();
+
+ if (tester(stdoutBuffer, stderrBuffer)) {
+ deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
+ }
+ }
+ });
+ const stderr = new PassThrough();
+ stderr.on('data', (data: Buffer) => {
+ if (deferred.state === 'unresolved') {
+ stderrBuffer += data.toString();
+
+ if (tester(stdoutBuffer, stderrBuffer)) {
+ deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
+ }
+ }
+ });
+ execution.modem.demuxStream(stream, stdout, stderr);
+ } catch (e) {
+ deferred.reject(e);
+ }
+ return deferred.promise;
+ }
+
+ async copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise {
+ const deferred = new Deferred();
+ const process = exec(`docker cp -qa ${localPath.toString()} ${this.container.id}:${remotePath}`);
+
+ let stderr = '';
+ process.stderr?.on('data', data => {
+ stderr += data.toString();
+ });
+ process.on('close', code => {
+ if (code === 0) {
+ deferred.resolve();
+ } else {
+ deferred.reject(stderr);
+ }
+ });
+ return deferred.promise;
+ }
+
+ dispose(): void {
+ this.container.stop();
+ }
+
+}
diff --git a/packages/dev-container/src/package.spec.ts b/packages/dev-container/src/package.spec.ts
new file mode 100644
index 0000000000000..759142013d5a6
--- /dev/null
+++ b/packages/dev-container/src/package.spec.ts
@@ -0,0 +1,28 @@
+// *****************************************************************************
+// Copyright (C) 2017 Ericsson 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
+// *****************************************************************************
+
+/* note: this bogus test file is required so that
+ we are able to run mocha unit tests on this
+ package, without having any actual unit tests in it.
+ This way a coverage report will be generated,
+ showing 0% coverage, instead of no report.
+ This file can be removed once we have real unit
+ tests in place. */
+
+describe('dev-container package', () => {
+
+ it('support code coverage statistics', () => true);
+});
diff --git a/packages/dev-container/tsconfig.json b/packages/dev-container/tsconfig.json
new file mode 100644
index 0000000000000..d7f2cbfb079c4
--- /dev/null
+++ b/packages/dev-container/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "extends": "../../configs/base.tsconfig",
+ "compilerOptions": {
+ "composite": true,
+ "rootDir": "src",
+ "outDir": "lib"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../core"
+ },
+ {
+ "path": "../output"
+ },
+ {
+ "path": "../remote"
+ },
+ {
+ "path": "../workspace"
+ }
+ ]
+}
diff --git a/packages/remote/package.json b/packages/remote/package.json
index 6ca865c3c6b39..5a287e315834f 100644
--- a/packages/remote/package.json
+++ b/packages/remote/package.json
@@ -12,7 +12,7 @@
"decompress-unzip": "^4.0.1",
"express-http-proxy": "^1.6.3",
"glob": "^8.1.0",
- "ssh2": "^1.12.0",
+ "ssh2": "^1.15.0",
"ssh2-sftp-client": "^9.1.0",
"socket.io": "^4.5.3",
"socket.io-client": "^4.5.3",
diff --git a/packages/remote/src/electron-browser/remote-frontend-contribution.ts b/packages/remote/src/electron-browser/remote-frontend-contribution.ts
index 9db5ed2a9f199..8e8d2f13d2fde 100644
--- a/packages/remote/src/electron-browser/remote-frontend-contribution.ts
+++ b/packages/remote/src/electron-browser/remote-frontend-contribution.ts
@@ -108,9 +108,7 @@ export class RemoteFrontendContribution implements CommandContribution, Frontend
protected disconnectRemote(): void {
const port = new URLSearchParams(location.search).get('localPort');
if (port) {
- this.windowService.reload({
- port
- });
+ this.windowService.reload({ search: { port } });
}
}
diff --git a/packages/remote/src/electron-browser/remote-registry-contribution.ts b/packages/remote/src/electron-browser/remote-registry-contribution.ts
index c936aeb833886..735b612f29564 100644
--- a/packages/remote/src/electron-browser/remote-registry-contribution.ts
+++ b/packages/remote/src/electron-browser/remote-registry-contribution.ts
@@ -16,8 +16,7 @@
import { Command, CommandHandler, Emitter, Event } from '@theia/core';
import { inject, injectable } from '@theia/core/shared/inversify';
-import { WindowService } from '@theia/core/lib/browser/window/window-service';
-import { WindowSearchParams } from '@theia/core/lib/common/window';
+import { WindowService, WindowReloadOptions } from '@theia/core/lib/browser/window/window-service';
export const RemoteRegistryContribution = Symbol('RemoteRegistryContribution');
@@ -33,15 +32,19 @@ export abstract class AbstractRemoteRegistryContribution implements RemoteRegist
abstract registerRemoteCommands(registry: RemoteRegistry): void;
- protected openRemote(port: string, newWindow: boolean): void {
+ protected openRemote(port: string, newWindow: boolean, workspace?: string): void {
const searchParams = new URLSearchParams(location.search);
const localPort = searchParams.get('localPort') || searchParams.get('port');
- const options: WindowSearchParams = {
- port
+ const options: WindowReloadOptions = {
+ search: { port }
};
if (localPort) {
- options.localPort = localPort;
+ options.search!.localPort = localPort;
}
+ if (workspace) {
+ options.hash = workspace;
+ }
+
if (newWindow) {
this.windowService.openNewDefaultWindow(options);
} else {
diff --git a/packages/remote/src/electron-node/remote-types.ts b/packages/remote/src/electron-node/remote-types.ts
index 7499fd682004d..e50811629d0cc 100644
--- a/packages/remote/src/electron-node/remote-types.ts
+++ b/packages/remote/src/electron-node/remote-types.ts
@@ -50,7 +50,20 @@ export interface RemoteConnection extends Disposable {
remotePort: number;
onDidDisconnect: Event;
forwardOut(socket: net.Socket): void;
+
+ /**
+ * execute a single command on the remote machine
+ */
exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise;
+
+ /**
+ * execute a command on the remote machine and wait for a specific output
+ * @param tester function which returns true if the output is as expected
+ */
execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise;
+
+ /**
+ * copy files from local to remote
+ */
copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): 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 16aee1c0ae085..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,12 +91,16 @@ 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 {
const nodeExecutable = this.scriptService.joinPath(platform, nodeDir, ...(platform.os === OS.Type.Windows ? ['node.exe'] : ['bin', 'node']));
const mainJsFile = this.scriptService.joinPath(platform, remotePath, 'lib', 'backend', 'main.js');
- const localAddressRegex = /listening on http:\/\/127.0.0.1:(\d+)/;
+ const localAddressRegex = /listening on http:\/\/0.0.0.0:(\d+)/;
let prefix = '';
if (platform.os === OS.Type.Windows) {
// We might to switch to PowerShell beforehand on Windows
@@ -101,7 +110,7 @@ export class RemoteSetupService {
// This way, our current working directory is set as expected
const result = await connection.execPartial(`${prefix}cd "${remotePath}";${nodeExecutable}`,
stdout => localAddressRegex.test(stdout),
- [mainJsFile, '--hostname=127.0.0.1', '--port=0', '--remote']);
+ [mainJsFile, '--hostname=0.0.0.0', `--port=${connection.remotePort ?? 0}`, '--remote']);
const match = localAddressRegex.exec(result.stdout);
if (!match) {
diff --git a/tsconfig.json b/tsconfig.json
index 3b0eb7ae410c0..fec10e328dcec 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -69,6 +69,9 @@
{
"path": "packages/debug"
},
+ {
+ "path": "packages/dev-container"
+ },
{
"path": "packages/editor"
},
diff --git a/yarn.lock b/yarn.lock
index 1098b78f1f71a..7ffee9d042681 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -977,6 +977,11 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
+"@balena/dockerignore@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d"
+ integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==
+
"@discoveryjs/json-ext@^0.5.0":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@@ -1860,6 +1865,22 @@
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-3.5.8.tgz#24434e47c2ef1cbcdf96afa43c6ea2fd8e4add93"
integrity sha512-CZ5vepL87+M8PxRIvJjR181Erahch2w7Jev/XJm+Iot/SOvJh8QqH/N79b+vsKtYF6fFzoPieiiq2c5tzmXR9A==
+"@types/docker-modem@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87"
+ integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==
+ dependencies:
+ "@types/node" "*"
+ "@types/ssh2" "*"
+
+"@types/dockerode@^3.3.23":
+ version "3.3.23"
+ resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.23.tgz#07b2084013d01e14d5d97856446f4d9c9f27c223"
+ integrity sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==
+ dependencies:
+ "@types/docker-modem" "*"
+ "@types/node" "*"
+
"@types/dompurify@^2.2.2":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9"
@@ -4724,6 +4745,25 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
+docker-modem@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-5.0.3.tgz#50c06f11285289f58112b5c4c4d89824541c41d0"
+ integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==
+ dependencies:
+ debug "^4.1.1"
+ readable-stream "^3.5.0"
+ split-ca "^1.0.1"
+ ssh2 "^1.15.0"
+
+dockerode@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-4.0.2.tgz#dedc8529a1db3ac46d186f5912389899bc309f7d"
+ integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==
+ dependencies:
+ "@balena/dockerignore" "^1.0.2"
+ docker-modem "^5.0.3"
+ tar-fs "~2.0.1"
+
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -9934,7 +9974,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
+readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
@@ -10759,6 +10799,11 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
+split-ca@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6"
+ integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==
+
split2@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f"
@@ -10799,7 +10844,7 @@ ssh2-sftp-client@^9.1.0:
promise-retry "^2.0.1"
ssh2 "^1.12.0"
-ssh2@^1.12.0, ssh2@^1.5.0:
+ssh2@^1.12.0, ssh2@^1.15.0, ssh2@^1.5.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b"
integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==
@@ -11112,6 +11157,16 @@ tar-fs@^1.16.2:
pump "^1.0.0"
tar-stream "^1.1.2"
+tar-fs@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2"
+ integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==
+ dependencies:
+ chownr "^1.1.1"
+ mkdirp-classic "^0.5.2"
+ pump "^3.0.0"
+ tar-stream "^2.0.0"
+
tar-stream@^1.1.2, tar-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
@@ -11125,7 +11180,7 @@ tar-stream@^1.1.2, tar-stream@^1.5.2:
to-buffer "^1.1.1"
xtend "^4.0.0"
-tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0:
+tar-stream@^2.0.0, tar-stream@^2.1.4, tar-stream@^2.2.0, tar-stream@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
@@ -11771,7 +11826,7 @@ uuid@^7.0.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
-uuid@^8.3.2:
+uuid@^8.0.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==