Skip to content

Commit 4a18a6f

Browse files
committed
smaller fixes and added support for multiple devcontainer configuration files
Signed-off-by: Jonah Iden <[email protected]>
1 parent b45fb37 commit 4a18a6f

File tree

7 files changed

+137
-40
lines changed

7 files changed

+137
-40
lines changed

packages/dev-container/src/electron-browser/container-connection-contribution.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remot
1919
import { LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
2020
import { RemotePreferences } from '@theia/remote/lib/electron-browser/remote-preferences';
2121
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
22-
import { Command } from '@theia/core';
22+
import { Command, QuickInputService } from '@theia/core';
23+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
2324

2425
export namespace RemoteContainerCommands {
2526
export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
@@ -40,7 +41,13 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
4041
protected readonly remotePreferences: RemotePreferences;
4142

4243
@inject(WorkspaceStorageService)
43-
private workspaceStorageService: WorkspaceStorageService;
44+
protected readonly workspaceStorageService: WorkspaceStorageService;
45+
46+
@inject(WorkspaceService)
47+
protected readonly workspaceService: WorkspaceService;
48+
49+
@inject(QuickInputService)
50+
protected readonly quickInputService: QuickInputService;
4451

4552
registerRemoteCommands(registry: RemoteRegistry): void {
4653
registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
@@ -50,20 +57,41 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
5057
}
5158

5259
async openInContainer(): Promise<void> {
53-
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(LAST_USED_CONTAINER);
60+
const devcontainerFile = await this.getOrSelectDevcontainerFile();
61+
if (!devcontainerFile) {
62+
return;
63+
}
64+
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile}`;
65+
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
5466

5567
const connectionResult = await this.connectionProvider.connectToContainer({
5668
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
57-
lastContainerInfo
69+
lastContainerInfo,
70+
devcontainerFile
5871
});
5972

60-
this.workspaceStorageService.setData<LastContainerInfo>(LAST_USED_CONTAINER, {
73+
this.workspaceStorageService.setData<LastContainerInfo>(lastContainerInfoKey, {
6174
id: connectionResult.containerId,
62-
port: connectionResult.containerPort,
6375
lastUsed: Date.now()
6476
});
6577

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

81+
async getOrSelectDevcontainerFile(): Promise<string | undefined> {
82+
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles();
83+
84+
if (devcontainerFiles.length === 1) {
85+
return devcontainerFiles[0].path;
86+
}
87+
88+
return (await this.quickInputService.pick(devcontainerFiles.map(file => ({
89+
type: 'item',
90+
label: file.name,
91+
description: file.path,
92+
file: file.path,
93+
})), {
94+
title: 'Select a devcontainer.json file'
95+
}))?.file;
96+
}
6997
}

packages/dev-container/src/electron-common/remote-container-connection-provider.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,25 @@ export const RemoteContainerConnectionProvider = Symbol('RemoteContainerConnecti
2020
export interface ContainerConnectionOptions {
2121
nodeDownloadTemplate?: string;
2222
lastContainerInfo?: LastContainerInfo
23+
devcontainerFile: string;
2324
}
2425

2526
export interface LastContainerInfo {
2627
id: string;
27-
port: number;
2828
lastUsed: number;
2929
}
3030

3131
export interface ContainerConnectionResult {
3232
port: string;
3333
workspacePath: string;
3434
containerId: string;
35-
containerPort: number;
35+
}
36+
37+
export interface DevContainerFile {
38+
name: string;
39+
path: string;
3640
}
3741
export interface RemoteContainerConnectionProvider {
3842
connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult>;
43+
getDevContainerFiles(): Promise<DevContainerFile[]>;
3944
}

packages/dev-container/src/electron-node/dev-container-backend-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPat
2121
import { ContainerCreationContribution, DockerContainerService } from './docker-container-service';
2222
import { bindContributionProvider } from '@theia/core';
2323
import { registerContainerCreationContributions } from './devcontainer-contributions/main-container-creation-contributions';
24+
import { DevContainerFileService } from './dev-container-file-service';
2425

2526
export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
2627
bindContributionProvider(bind, ContainerCreationContribution);
@@ -34,4 +35,6 @@ export const remoteConnectionModule = ConnectionContainerModule.create(({ bind,
3435
export default new ContainerModule((bind, unbind, isBound, rebind) => {
3536
bind(DockerContainerService).toSelf().inSingletonScope();
3637
bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule);
38+
39+
bind(DevContainerFileService).toSelf().inSingletonScope();
3740
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 { WorkspaceServer } from '@theia/workspace/lib/common';
19+
import { DevContainerFile } from '../electron-common/remote-container-connection-provider';
20+
import { DevContainerConfiguration } from './devcontainer-file';
21+
import { parse } from 'jsonc-parser';
22+
import * as fs from '@theia/core/shared/fs-extra';
23+
import { URI } from '@theia/core';
24+
25+
@injectable()
26+
export class DevContainerFileService {
27+
28+
@inject(WorkspaceServer)
29+
protected readonly workspaceServer: WorkspaceServer;
30+
31+
async getConfiguration(path: string): Promise<DevContainerConfiguration> {
32+
const configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration;
33+
if (!configuration) {
34+
throw new Error(`devcontainer file ${path} could not be parsed`);
35+
}
36+
37+
configuration.location = path;
38+
return configuration;
39+
}
40+
41+
async getAvailableFiles(): Promise<DevContainerFile[]> {
42+
const workspace = await this.workspaceServer.getMostRecentlyUsedWorkspace();
43+
if (!workspace) {
44+
return [];
45+
}
46+
47+
const devcontainerPath = new URI(workspace).path.join('.devcontainer');
48+
49+
const files = await fs.readdir(devcontainerPath.fsPath());
50+
return files.flatMap(filename => {
51+
const fileStat = fs.statSync(devcontainerPath.join(filename).fsPath());
52+
return fileStat.isDirectory() ?
53+
fs.readdirSync(devcontainerPath.join(filename).fsPath()).map(inner => devcontainerPath.join(filename).join(inner).fsPath()) :
54+
[devcontainerPath.join(filename).fsPath()];
55+
})
56+
.filter(file => file.endsWith('devcontainer.json'))
57+
.map(file => ({
58+
name: parse(fs.readFileSync(file, 'utf-8')).name ?? 'devcontainer',
59+
path: file
60+
}));
61+
62+
}
63+
}

packages/dev-container/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as Docker from 'dockerode';
1717
import { injectable, interfaces } from '@theia/core/shared/inversify';
1818
import { ContainerCreationContribution } from '../docker-container-service';
1919
import { DevContainerConfiguration, DockerfileContainer, ImageContainer } from '../devcontainer-file';
20+
import { Path } from '@theia/core';
2021

2122
export function registerContainerCreationContributions(bind: interfaces.Bind): void {
2223
bind(ContainerCreationContribution).to(ImageFileContribution).inSingletonScope();
@@ -43,7 +44,7 @@ export class DockerFileContribution implements ContainerCreationContribution {
4344
if (containerConfig.dockerFile || containerConfig.build?.dockerfile) {
4445
const dockerfile = (containerConfig.dockerFile ?? containerConfig.build?.dockerfile) as string;
4546
const buildStream = await api.buildImage({
46-
context: containerConfig.context ?? containerConfig.location,
47+
context: containerConfig.context ?? new Path(containerConfig.location as string).dir.fsPath(),
4748
src: [dockerfile],
4849
} as Docker.ImageBuildContext, {
4950
buildargs: containerConfig.build?.args

packages/dev-container/src/electron-node/docker-container-service.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
import { ContributionProvider, URI } from '@theia/core';
1818
import { inject, injectable, named } from '@theia/core/shared/inversify';
1919
import { WorkspaceServer } from '@theia/workspace/lib/common';
20-
import { parse } from 'jsonc-parser';
2120
import * as fs from '@theia/core/shared/fs-extra';
2221
import * as Docker from 'dockerode';
2322
import { LastContainerInfo } from '../electron-common/remote-container-connection-provider';
2423
import { DevContainerConfiguration } from './devcontainer-file';
24+
import { DevContainerFileService } from './dev-container-file-service';
2525

2626
export const ContainerCreationContribution = Symbol('ContainerCreationContributions');
2727

@@ -38,40 +38,35 @@ export class DockerContainerService {
3838
@inject(ContributionProvider) @named(ContainerCreationContribution)
3939
protected readonly containerCreationContributions: ContributionProvider<ContainerCreationContribution>;
4040

41-
async getOrCreateContainer(docker: Docker, lastContainerInfo?: LastContainerInfo): Promise<[Docker.Container, number]> {
42-
let port = Math.floor(Math.random() * (49151 - 10000)) + 10000;
41+
@inject(DevContainerFileService)
42+
protected readonly devContainerFileService: DevContainerFileService;
43+
44+
async getOrCreateContainer(docker: Docker, devcontainerFile: string, lastContainerInfo?: LastContainerInfo): Promise<Docker.Container> {
4345
let container;
4446

4547
const workspace = new URI(await this.workspaceServer.getMostRecentlyUsedWorkspace());
46-
if (!workspace) {
47-
throw new Error('No workspace');
48-
}
49-
50-
const devcontainerFile = workspace.resolve('.devcontainer/devcontainer.json');
5148

52-
if (lastContainerInfo && fs.statSync(devcontainerFile.path.fsPath()).mtimeMs < lastContainerInfo.lastUsed) {
49+
if (lastContainerInfo && fs.statSync(devcontainerFile).mtimeMs < lastContainerInfo.lastUsed) {
5350
try {
5451
container = docker.getContainer(lastContainerInfo.id);
5552
if ((await container.inspect()).State.Running) {
5653
await container.restart();
5754
} else {
5855
await container.start();
5956
}
60-
port = lastContainerInfo.port;
6157
} catch (e) {
6258
container = undefined;
6359
console.warn('DevContainer: could not find last used container');
6460
}
6561
}
6662
if (!container) {
67-
container = await this.buildContainer(docker, port, devcontainerFile, workspace);
63+
container = await this.buildContainer(docker, devcontainerFile, workspace);
6864
}
69-
return [container, port];
65+
return container;
7066
}
7167

72-
protected async buildContainer(docker: Docker, port: number, devcontainerFile: URI, workspace: URI): Promise<Docker.Container> {
73-
const devcontainerConfig = parse(await fs.readFile(devcontainerFile.path.fsPath(), 'utf-8').catch(() => '0')) as DevContainerConfiguration;
74-
devcontainerConfig.location = devcontainerFile.path.dir.fsPath();
68+
protected async buildContainer(docker: Docker, devcontainerFile: string, workspace: URI): Promise<Docker.Container> {
69+
const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile);
7570

7671
if (!devcontainerConfig) {
7772
// TODO add ability for user to create new config
@@ -80,13 +75,9 @@ export class DockerContainerService {
8075

8176
const containerCreateOptions: Docker.ContainerCreateOptions = {
8277
Tty: true,
83-
ExposedPorts: {
84-
// [`${port}/tcp`]: {},
85-
},
78+
ExposedPorts: {},
8679
HostConfig: {
87-
PortBindings: {
88-
// [`${port}/tcp`]: [{ HostPort: '0' }],
89-
},
80+
PortBindings: {},
9081
Mounts: [{
9182
Source: workspace.path.toString(),
9283
Target: `/workspaces/${workspace.path.name}`,
@@ -101,8 +92,7 @@ export class DockerContainerService {
10192

10293
// TODO add more config
10394
const container = await docker.createContainer(containerCreateOptions);
104-
const start = await container.start();
105-
console.log(start);
95+
await container.start();
10696

10797
return container;
10898
}

packages/dev-container/src/electron-node/remote-container-connection-provider.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
// *****************************************************************************
1616

1717
import * as net from 'net';
18-
import { ContainerConnectionOptions, ContainerConnectionResult, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
18+
import {
19+
ContainerConnectionOptions, ContainerConnectionResult,
20+
DevContainerFile, RemoteContainerConnectionProvider
21+
} from '../electron-common/remote-container-connection-provider';
1922
import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types';
2023
import { RemoteSetupResult, RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service';
2124
import { RemoteConnectionService } from '@theia/remote/lib/electron-node/remote-connection-service';
@@ -29,6 +32,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
2932
import { WriteStream } from 'tty';
3033
import { PassThrough } from 'stream';
3134
import { exec } from 'child_process';
35+
import { DevContainerFileService } from './dev-container-file-service';
3236

3337
@injectable()
3438
export class DevContainerConnectionProvider implements RemoteContainerConnectionProvider {
@@ -48,6 +52,9 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
4852
@inject(DockerContainerService)
4953
protected readonly containerService: DockerContainerService;
5054

55+
@inject(DevContainerFileService)
56+
protected readonly devContainerFileService: DevContainerFileService;
57+
5158
async connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult> {
5259
const dockerConnection = new Docker();
5360
const version = await dockerConnection.version().catch(() => undefined);
@@ -62,13 +69,13 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
6269
text: 'create container',
6370
});
6471
try {
65-
const [container, port] = await this.containerService.getOrCreateContainer(dockerConnection, options.lastContainerInfo);
72+
const container = await this.containerService.getOrCreateContainer(dockerConnection, options.devcontainerFile, options.lastContainerInfo);
6673

6774
// create actual connection
6875
const report: RemoteStatusReport = message => progress.report({ message });
6976
report('Connecting to remote system...');
7077

71-
const remote = await this.createContainerConnection(container, dockerConnection, port);
78+
const remote = await this.createContainerConnection(container, dockerConnection);
7279
const result = await this.remoteSetup.setup({
7380
connection: remote,
7481
report,
@@ -88,7 +95,6 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
8895
remote.localPort = localPort;
8996
return {
9097
containerId: container.id,
91-
containerPort: port,
9298
workspacePath: (await container.inspect()).Mounts[0].Destination,
9399
port: localPort.toString(),
94100
};
@@ -101,14 +107,17 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
101107
}
102108
}
103109

104-
async createContainerConnection(container: Docker.Container, docker: Docker, port: number): Promise<RemoteDockerContainerConnection> {
110+
getDevContainerFiles(): Promise<DevContainerFile[]> {
111+
return this.devContainerFileService.getAvailableFiles();
112+
}
113+
114+
async createContainerConnection(container: Docker.Container, docker: Docker): Promise<RemoteDockerContainerConnection> {
105115
return Promise.resolve(new RemoteDockerContainerConnection({
106116
id: generateUuid(),
107117
name: 'dev-container',
108118
type: 'container',
109119
docker,
110120
container,
111-
port,
112121
}));
113122
}
114123

@@ -120,7 +129,6 @@ export interface RemoteContainerConnectionOptions {
120129
type: string;
121130
docker: Docker;
122131
container: Docker.Container;
123-
port: number;
124132
}
125133

126134
interface ContainerTerminalSession {
@@ -158,7 +166,6 @@ export class RemoteDockerContainerConnection implements RemoteConnection {
158166

159167
this.docker = options.docker;
160168
this.container = options.container;
161-
this.remotePort = options.port;
162169
}
163170

164171
async forwardOut(socket: Socket): Promise<void> {

0 commit comments

Comments
 (0)