Skip to content

Commit

Permalink
Dev-Container support (#13372)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
jonah-iden authored Mar 15, 2024
1 parent 92cbb85 commit 401d4b9
Show file tree
Hide file tree
Showing 31 changed files with 1,630 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions examples/browser/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{
"path": "../../packages/debug"
},
{
"path": "../../packages/dev-container"
},
{
"path": "../../packages/editor"
},
Expand Down
1 change: 1 addition & 0 deletions examples/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions examples/electron/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
{
"path": "../../packages/debug"
},
{
"path": "../../packages/dev-container"
},
{
"path": "../../packages/editor"
},
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/browser/window/window-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -64,5 +69,5 @@ export interface WindowService {
/**
* Reloads the window according to platform.
*/
reload(params?: WindowSearchParams): void;
reload(params?: WindowReloadOptions): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
}
}
}

10 changes: 10 additions & 0 deletions packages/dev-container/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};
43 changes: 43 additions & 0 deletions packages/dev-container/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<div align='center'>

<br />

<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />

<h2>ECLIPSE THEIA - DEV-CONTAINER EXTENSION</h2>

<hr />

</div>

## 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
54 changes: 54 additions & 0 deletions packages/dev-container/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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<void> {
const devcontainerFile = await this.getOrSelectDevcontainerFile();
if (!devcontainerFile) {
return;
}
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile}`;
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);

this.containerOutputProvider.openChannel();

const connectionResult = await this.connectionProvider.connectToContainer({
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
lastContainerInfo,
devcontainerFile
});

this.workspaceStorageService.setData<LastContainerInfo>(lastContainerInfoKey, {
id: connectionResult.containerId,
lastUsed: Date.now()
});

this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
}

async getOrSelectDevcontainerFile(): Promise<string | undefined> {
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;
}

}
Loading

0 comments on commit 401d4b9

Please sign in to comment.