Skip to content

Commit 401d4b9

Browse files
authored
Dev-Container support (#13372)
* basics for dev-container support Signed-off-by: Jonah Iden <[email protected]> * basic creating and connecting to container working Signed-off-by: Jonah Iden <[email protected]> * open workspace when opening container Signed-off-by: Jonah Iden <[email protected]> * save and reuse last USed container per workspace Signed-off-by: Jonah Iden <[email protected]> * restart container if running Signed-off-by: Jonah Iden <[email protected]> * better container creation extension features Signed-off-by: Jonah Iden <[email protected]> * added dockerfile support Signed-off-by: Jonah Iden <[email protected]> * rebuild container if devcontainer.json has been changed since last use Signed-off-by: Jonah Iden <[email protected]> * fix build Signed-off-by: Jonah Iden <[email protected]> * fixed checking if container needs rebuild Signed-off-by: Jonah Iden <[email protected]> * working port forwarding via exec instance Signed-off-by: Jonah Iden <[email protected]> * review changes Signed-off-by: Jonah Iden <[email protected]> * fix import Signed-off-by: Jonah Iden <[email protected]> * smaller fixes and added support for multiple devcontainer configuration files Signed-off-by: Jonah Iden <[email protected]> * basic output window for devcontainer build Signed-off-by: Jonah Iden <[email protected]> * smaller review changes and nicer dockerfile.json detection code Signed-off-by: Jonah Iden <[email protected]> * fixed build and docuemented implemented devcontainer.json properties Signed-off-by: Jonah Iden <[email protected]> --------- Signed-off-by: Jonah Iden <[email protected]>
1 parent 92cbb85 commit 401d4b9

31 files changed

+1630
-22
lines changed

dev-packages/application-manager/src/generator/webpack-generator.ts

+2
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ const config = {
437437
${this.ifPackage('@theia/git', () => `// Ensure the git locator process can the started
438438
'git-locator-host': require.resolve('@theia/git/lib/node/git-locator/git-locator-host'),`)}
439439
${this.ifElectron("'electron-main': require.resolve('./src-gen/backend/electron-main'),")}
440+
${this.ifPackage('@theia/dev-container', () => `// VS Code Dev-Container communication:
441+
'dev-container-server': require.resolve('@theia/dev-container/lib/dev-container-server/dev-container-server'),`)}
440442
...commonJsLibraries
441443
},
442444
module: {

examples/browser/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@theia/console": "1.47.0",
2828
"@theia/core": "1.47.0",
2929
"@theia/debug": "1.47.0",
30+
"@theia/dev-container": "1.47.0",
3031
"@theia/editor": "1.47.0",
3132
"@theia/editor-preview": "1.47.0",
3233
"@theia/file-search": "1.47.0",

examples/browser/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
{
2424
"path": "../../packages/debug"
2525
},
26+
{
27+
"path": "../../packages/dev-container"
28+
},
2629
{
2730
"path": "../../packages/editor"
2831
},

examples/electron/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@theia/console": "1.47.0",
2828
"@theia/core": "1.47.0",
2929
"@theia/debug": "1.47.0",
30+
"@theia/dev-container": "1.47.0",
3031
"@theia/editor": "1.47.0",
3132
"@theia/editor-preview": "1.47.0",
3233
"@theia/electron": "1.47.0",

examples/electron/tsconfig.json

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
{
2727
"path": "../../packages/debug"
2828
},
29+
{
30+
"path": "../../packages/dev-container"
31+
},
2932
{
3033
"path": "../../packages/editor"
3134
},

packages/core/src/browser/window/window-service.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import { StopReason } from '../../common/frontend-application-state';
1818
import { Event } from '../../common/event';
1919
import { NewWindowOptions, WindowSearchParams } from '../../common/window';
2020

21+
export interface WindowReloadOptions {
22+
search?: WindowSearchParams,
23+
hash?: string
24+
}
25+
2126
/**
2227
* Service for opening new browser windows.
2328
*/
@@ -35,7 +40,7 @@ export interface WindowService {
3540
* Opens a new default window.
3641
* - In electron and in the browser it will open the default window without a pre-defined content.
3742
*/
38-
openNewDefaultWindow(params?: WindowSearchParams): void;
43+
openNewDefaultWindow(params?: WindowReloadOptions): void;
3944

4045
/**
4146
* 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 {
6469
/**
6570
* Reloads the window according to platform.
6671
*/
67-
reload(params?: WindowSearchParams): void;
72+
reload(params?: WindowReloadOptions): void;
6873
}

packages/core/src/electron-browser/window/electron-window-service.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ElectronMainWindowService } from '../../electron-common/electron-main-w
2121
import { ElectronWindowPreferences } from './electron-window-preferences';
2222
import { ConnectionCloseService } from '../../common/messaging/connection-management';
2323
import { FrontendIdProvider } from '../../browser/messaging/frontend-id-provider';
24+
import { WindowReloadOptions } from '../../browser/window/window-service';
2425

2526
@injectable()
2627
export class ElectronWindowService extends DefaultWindowService {
@@ -86,12 +87,20 @@ export class ElectronWindowService extends DefaultWindowService {
8687
}
8788
}
8889

89-
override reload(params?: WindowSearchParams): void {
90+
override reload(params?: WindowReloadOptions): void {
9091
if (params) {
91-
const query = Object.entries(params).map(([name, value]) => `${name}=${value}`).join('&');
92-
location.search = query;
92+
const newLocation = new URL(location.href);
93+
if (params.search) {
94+
const query = Object.entries(params.search).map(([name, value]) => `${name}=${value}`).join('&');
95+
newLocation.search = query;
96+
}
97+
if (params.hash) {
98+
newLocation.hash = '#' + params.hash;
99+
}
100+
location.assign(newLocation);
93101
} else {
94102
window.electronTheiaCore.requestReload();
95103
}
96104
}
97105
}
106+

packages/dev-container/.eslintrc.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** @type {import('eslint').Linter.Config} */
2+
module.exports = {
3+
extends: [
4+
'../../configs/build.eslintrc.json'
5+
],
6+
parserOptions: {
7+
tsconfigRootDir: __dirname,
8+
project: 'tsconfig.json'
9+
}
10+
};

packages/dev-container/README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<div align='center'>
2+
3+
<br />
4+
5+
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
6+
7+
<h2>ECLIPSE THEIA - DEV-CONTAINER EXTENSION</h2>
8+
9+
<hr />
10+
11+
</div>
12+
13+
## Description
14+
15+
The `@theia/dev-container` extension provides functionality to create, start and connect to development containers similiar to the
16+
[vscode Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
17+
18+
The full devcontainer.json Schema can be found [here](https://containers.dev/implementors/json_reference/).
19+
Currently only a small number of configuration file properties are implemented. Those include the following:
20+
- name
21+
- Image
22+
- dockerfile/build.dockerfile
23+
- build.context
24+
- location
25+
- forwardPorts
26+
- mounts
27+
28+
see `main-container-creation-contributions.ts` for how to implementations or how to implement additional ones.
29+
30+
31+
## Additional Information
32+
33+
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
34+
- [Theia - Website](https://theia-ide.org/)
35+
36+
## License
37+
38+
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
39+
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
40+
41+
## Trademark
42+
"Theia" is a trademark of the Eclipse Foundation
43+
https://www.eclipse.org/theia

packages/dev-container/package.json

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@theia/dev-container",
3+
"version": "1.47.0",
4+
"description": "Theia - Editor Preview Extension",
5+
"dependencies": {
6+
"@theia/core": "1.47.0",
7+
"@theia/output": "1.47.0",
8+
"@theia/remote": "1.47.0",
9+
"@theia/workspace": "1.47.0",
10+
"dockerode": "^4.0.2",
11+
"uuid": "^8.0.0",
12+
"jsonc-parser": "^2.2.0"
13+
},
14+
"publishConfig": {
15+
"access": "public"
16+
},
17+
"theiaExtensions": [
18+
{
19+
"frontendElectron": "lib/electron-browser/dev-container-frontend-module",
20+
"backendElectron": "lib/electron-node/dev-container-backend-module"
21+
}
22+
],
23+
"keywords": [
24+
"theia-extension"
25+
],
26+
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
27+
"repository": {
28+
"type": "git",
29+
"url": "https://github.com/eclipse-theia/theia.git"
30+
},
31+
"bugs": {
32+
"url": "https://github.com/eclipse-theia/theia/issues"
33+
},
34+
"homepage": "https://github.com/eclipse-theia/theia",
35+
"files": [
36+
"lib",
37+
"src"
38+
],
39+
"scripts": {
40+
"build": "theiaext build",
41+
"clean": "theiaext clean",
42+
"compile": "theiaext compile",
43+
"lint": "theiaext lint",
44+
"test": "theiaext test",
45+
"watch": "theiaext watch"
46+
},
47+
"devDependencies": {
48+
"@theia/ext-scripts": "1.47.0",
49+
"@types/dockerode": "^3.3.23"
50+
},
51+
"nyc": {
52+
"extends": "../../configs/nyc.json"
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 Typefox and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { createConnection } from 'net';
18+
import { stdin, argv, stdout } from 'process';
19+
20+
/**
21+
* this node.js Program is supposed to be executed by an docker exec session inside a docker container.
22+
* It uses a tty session to listen on stdin and send on stdout all communication with the theia backend running inside the container.
23+
*/
24+
25+
let backendPort: number | undefined = undefined;
26+
argv.slice(2).forEach(arg => {
27+
if (arg.startsWith('-target-port')) {
28+
backendPort = parseInt(arg.split('=')[1]);
29+
}
30+
});
31+
32+
if (!backendPort) {
33+
throw new Error('please start with -target-port={port number}');
34+
}
35+
if (stdin.isTTY) {
36+
stdin.setRawMode(true);
37+
}
38+
const connection = createConnection(backendPort, '0.0.0.0');
39+
40+
connection.pipe(stdout);
41+
stdin.pipe(connection);
42+
43+
connection.on('error', error => {
44+
console.error('connection error', error);
45+
});
46+
47+
connection.on('close', () => {
48+
console.log('connection closed');
49+
process.exit(0);
50+
});
51+
52+
// keep the process running
53+
setInterval(() => { }, 1 << 30);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2024 Typefox and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { inject, injectable } from '@theia/core/shared/inversify';
18+
import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
19+
import { LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
20+
import { RemotePreferences } from '@theia/remote/lib/electron-browser/remote-preferences';
21+
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
22+
import { Command, QuickInputService } from '@theia/core';
23+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
24+
import { ContainerOutputProvider } from './container-output-provider';
25+
26+
export namespace RemoteContainerCommands {
27+
export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
28+
id: 'dev-container:reopen-in-container',
29+
label: 'Reopen in Container',
30+
category: 'Dev Container'
31+
}, 'theia/dev-container/connect');
32+
}
33+
34+
const LAST_USED_CONTAINER = 'lastUsedContainer';
35+
@injectable()
36+
export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution {
37+
38+
@inject(RemoteContainerConnectionProvider)
39+
protected readonly connectionProvider: RemoteContainerConnectionProvider;
40+
41+
@inject(RemotePreferences)
42+
protected readonly remotePreferences: RemotePreferences;
43+
44+
@inject(WorkspaceStorageService)
45+
protected readonly workspaceStorageService: WorkspaceStorageService;
46+
47+
@inject(WorkspaceService)
48+
protected readonly workspaceService: WorkspaceService;
49+
50+
@inject(QuickInputService)
51+
protected readonly quickInputService: QuickInputService;
52+
53+
@inject(ContainerOutputProvider)
54+
protected readonly containerOutputProvider: ContainerOutputProvider;
55+
56+
registerRemoteCommands(registry: RemoteRegistry): void {
57+
registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
58+
execute: () => this.openInContainer()
59+
});
60+
}
61+
62+
async openInContainer(): Promise<void> {
63+
const devcontainerFile = await this.getOrSelectDevcontainerFile();
64+
if (!devcontainerFile) {
65+
return;
66+
}
67+
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile}`;
68+
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
69+
70+
this.containerOutputProvider.openChannel();
71+
72+
const connectionResult = await this.connectionProvider.connectToContainer({
73+
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
74+
lastContainerInfo,
75+
devcontainerFile
76+
});
77+
78+
this.workspaceStorageService.setData<LastContainerInfo>(lastContainerInfoKey, {
79+
id: connectionResult.containerId,
80+
lastUsed: Date.now()
81+
});
82+
83+
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
84+
}
85+
86+
async getOrSelectDevcontainerFile(): Promise<string | undefined> {
87+
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles();
88+
89+
if (devcontainerFiles.length === 1) {
90+
return devcontainerFiles[0].path;
91+
}
92+
93+
return (await this.quickInputService.pick(devcontainerFiles.map(file => ({
94+
type: 'item',
95+
label: file.name,
96+
description: file.path,
97+
file: file.path,
98+
})), {
99+
title: 'Select a devcontainer.json file'
100+
}))?.file;
101+
}
102+
103+
}

0 commit comments

Comments
 (0)