@theia/dev-container 1.72.0-next.59 → 1.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/electron-browser/container-connection-contribution.d.ts +5 -2
- package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
- package/lib/electron-browser/container-connection-contribution.js +130 -30
- package/lib/electron-browser/container-connection-contribution.js.map +1 -1
- package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
- package/lib/electron-browser/container-output-provider.js +3 -1
- package/lib/electron-browser/container-output-provider.js.map +1 -1
- package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
- package/lib/electron-browser/dev-container-frontend-module.js +5 -0
- package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
- package/lib/electron-browser/dev-container-startup-contribution.d.ts +15 -0
- package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-startup-contribution.js +94 -0
- package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -0
- package/lib/electron-common/dev-container-preferences.d.ts +12 -0
- package/lib/electron-common/dev-container-preferences.d.ts.map +1 -0
- package/lib/electron-common/dev-container-preferences.js +44 -0
- package/lib/electron-common/dev-container-preferences.js.map +1 -0
- package/lib/electron-common/remote-container-connection-provider.d.ts +20 -1
- package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/dev-container-backend-module.d.ts.map +1 -1
- package/lib/electron-node/dev-container-backend-module.js +4 -0
- package/lib/electron-node/dev-container-backend-module.js.map +1 -1
- package/lib/electron-node/dev-container-cli-contribution.d.ts +19 -0
- package/lib/electron-node/dev-container-cli-contribution.d.ts.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.js +66 -0
- package/lib/electron-node/dev-container-cli-contribution.js.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.d.ts +2 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.d.ts.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.js +91 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.js.map +1 -0
- package/lib/electron-node/dev-container-file-service.d.ts +4 -4
- package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
- package/lib/electron-node/dev-container-file-service.js +9 -9
- package/lib/electron-node/dev-container-file-service.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +6 -2
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js +24 -4
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts +7 -6
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +4 -9
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
- package/lib/electron-node/devcontainer-util.d.ts +19 -0
- package/lib/electron-node/devcontainer-util.d.ts.map +1 -0
- package/lib/electron-node/devcontainer-util.js +48 -0
- package/lib/electron-node/devcontainer-util.js.map +1 -0
- package/lib/electron-node/devcontainer-util.spec.d.ts +2 -0
- package/lib/electron-node/devcontainer-util.spec.d.ts.map +1 -0
- package/lib/electron-node/devcontainer-util.spec.js +128 -0
- package/lib/electron-node/devcontainer-util.spec.js.map +1 -0
- package/lib/electron-node/docker-container-service.d.ts +3 -3
- package/lib/electron-node/docker-container-service.d.ts.map +1 -1
- package/lib/electron-node/docker-container-service.js +3 -4
- package/lib/electron-node/docker-container-service.js.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.d.ts +27 -66
- package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.js +269 -311
- package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
- package/lib/electron-node/remote-docker-container-connection.d.ts +50 -0
- package/lib/electron-node/remote-docker-container-connection.d.ts.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.js +239 -0
- package/lib/electron-node/remote-docker-container-connection.js.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.spec.d.ts +2 -0
- package/lib/electron-node/remote-docker-container-connection.spec.d.ts.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.spec.js +217 -0
- package/lib/electron-node/remote-docker-container-connection.spec.js.map +1 -0
- package/package.json +7 -7
- package/src/electron-browser/container-connection-contribution.ts +155 -38
- package/src/electron-browser/container-output-provider.ts +3 -1
- package/src/electron-browser/dev-container-frontend-module.ts +6 -0
- package/src/electron-browser/dev-container-startup-contribution.ts +99 -0
- package/src/electron-common/dev-container-preferences.ts +53 -0
- package/src/electron-common/remote-container-connection-provider.ts +23 -1
- package/src/electron-node/dev-container-backend-module.ts +5 -0
- package/src/electron-node/dev-container-cli-contribution.spec.ts +106 -0
- package/src/electron-node/dev-container-cli-contribution.ts +68 -0
- package/src/electron-node/dev-container-file-service.ts +10 -10
- package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +29 -5
- package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +11 -11
- package/src/electron-node/devcontainer-util.spec.ts +154 -0
- package/src/electron-node/devcontainer-util.ts +49 -0
- package/src/electron-node/docker-container-service.ts +6 -7
- package/src/electron-node/remote-container-connection-provider.ts +274 -366
- package/src/electron-node/{remote-container-connection-provider.spec.ts → remote-docker-container-connection.spec.ts} +105 -4
- package/src/electron-node/remote-docker-container-connection.ts +290 -0
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts +0 -2
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +0 -1
- package/lib/electron-node/remote-container-connection-provider.spec.js +0 -131
- package/lib/electron-node/remote-container-connection-provider.spec.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/dev-container",
|
|
3
|
-
"version": "1.72.0
|
|
3
|
+
"version": "1.72.0",
|
|
4
4
|
"description": "Theia - Editor Preview Extension",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@theia/core": "1.72.0
|
|
7
|
-
"@theia/output": "1.72.0
|
|
8
|
-
"@theia/remote": "1.72.0
|
|
9
|
-
"@theia/workspace": "1.72.0
|
|
6
|
+
"@theia/core": "1.72.0",
|
|
7
|
+
"@theia/output": "1.72.0",
|
|
8
|
+
"@theia/remote": "1.72.0",
|
|
9
|
+
"@theia/workspace": "1.72.0",
|
|
10
10
|
"dockerode": "^4.0.12",
|
|
11
11
|
"jsonc-parser": "^3.3.1",
|
|
12
12
|
"uuid": "^8.3.2"
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"watch": "theiaext watch"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@theia/ext-scripts": "1.
|
|
48
|
+
"@theia/ext-scripts": "1.72.0",
|
|
49
49
|
"@types/dockerode": "^3.3.47"
|
|
50
50
|
},
|
|
51
51
|
"nyc": {
|
|
52
52
|
"extends": "../../configs/nyc.json"
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "5bc20bc672aa732fb7e05234cf63c1b514868896"
|
|
55
55
|
}
|
|
@@ -16,15 +16,20 @@
|
|
|
16
16
|
|
|
17
17
|
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
|
18
18
|
import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider,
|
|
21
|
+
RunningContainerInfo, WorkspaceCandidate
|
|
22
|
+
} from '../electron-common/remote-container-connection-provider';
|
|
20
23
|
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
|
|
21
|
-
import { Command, MaybePromise, MessageService, nls, QuickInputService, URI } from '@theia/core';
|
|
24
|
+
import { Command, ILogger, MaybePromise, MessageService, nls, QuickInputService, URI } from '@theia/core';
|
|
22
25
|
import { WorkspaceInput, WorkspaceOpenHandlerContribution, WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
23
26
|
import { ContainerOutputProvider } from './container-output-provider';
|
|
24
27
|
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
|
25
28
|
import { DEV_CONTAINER_PATH_QUERY, DEV_CONTAINER_WORKSPACE_SCHEME } from '../electron-common/dev-container-workspaces';
|
|
26
29
|
import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
|
|
27
30
|
import { LocalStorageService } from '@theia/core/lib/browser';
|
|
31
|
+
import { ConfirmDialog, Dialog } from '@theia/core/lib/browser/dialogs';
|
|
32
|
+
import { DevContainerPreferences } from '../electron-common/dev-container-preferences';
|
|
28
33
|
|
|
29
34
|
export namespace RemoteContainerCommands {
|
|
30
35
|
export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
|
|
@@ -32,7 +37,6 @@ export namespace RemoteContainerCommands {
|
|
|
32
37
|
label: 'Reopen in Container',
|
|
33
38
|
category: 'Dev Container'
|
|
34
39
|
}, 'theia/remote/dev-container/connect');
|
|
35
|
-
|
|
36
40
|
export const ATTACH_TO_CONTAINER = Command.toLocalizedCommand({
|
|
37
41
|
id: 'dev-container:attach-to-container',
|
|
38
42
|
label: 'Attach to Running Container',
|
|
@@ -85,6 +89,12 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
85
89
|
@inject(ContainerOutputProvider)
|
|
86
90
|
protected readonly containerOutputProvider: ContainerOutputProvider;
|
|
87
91
|
|
|
92
|
+
@inject(DevContainerPreferences)
|
|
93
|
+
protected readonly devContainerPreferences: DevContainerPreferences;
|
|
94
|
+
|
|
95
|
+
@inject(ILogger)
|
|
96
|
+
protected readonly logger: ILogger;
|
|
97
|
+
|
|
88
98
|
protected hasDevContainerFiles = false;
|
|
89
99
|
|
|
90
100
|
@postConstruct()
|
|
@@ -123,13 +133,13 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
123
133
|
execute: () => this.openInContainer(),
|
|
124
134
|
isVisible: () => !this.isRemoteSession() && this.hasDevContainerFiles
|
|
125
135
|
});
|
|
126
|
-
registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
|
|
127
|
-
execute: () => this.attachToContainer()
|
|
128
|
-
});
|
|
129
136
|
registry.registerCommand(RemoteContainerCommands.REBUILD_CONTAINER, {
|
|
130
137
|
execute: () => this.rebuildContainer(),
|
|
131
138
|
isVisible: () => this.isRemoteSession()
|
|
132
139
|
});
|
|
140
|
+
registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
|
|
141
|
+
execute: () => this.attachToRunningContainer()
|
|
142
|
+
});
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
protected isRemoteSession(): boolean {
|
|
@@ -187,33 +197,6 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
187
197
|
this.doOpenInContainer(devcontainerFile);
|
|
188
198
|
}
|
|
189
199
|
|
|
190
|
-
async attachToContainer(): Promise<void> {
|
|
191
|
-
const containers = await this.connectionProvider.listRunningContainers();
|
|
192
|
-
if (containers.length === 0) {
|
|
193
|
-
this.messageService.info(nls.localize('theia/remote/dev-container/noRunningContainers', 'No running containers found.'));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const selected = await this.quickInputService.pick(containers.map(container => ({
|
|
198
|
-
type: 'item' as const,
|
|
199
|
-
label: container.name || container.id.substring(0, 12),
|
|
200
|
-
description: container.image,
|
|
201
|
-
detail: container.status,
|
|
202
|
-
container
|
|
203
|
-
})), {
|
|
204
|
-
title: nls.localize('theia/remote/dev-container/selectContainer', 'Select a running container to attach to')
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (!selected) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
this.containerOutputProvider.openChannel();
|
|
212
|
-
|
|
213
|
-
const connectionResult = await this.connectionProvider.attachToContainer(selected.container.id);
|
|
214
|
-
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
200
|
async rebuildContainer(): Promise<void> {
|
|
218
201
|
this.containerOutputProvider.openChannel();
|
|
219
202
|
const progress = await this.messageService.showProgress({
|
|
@@ -230,7 +213,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
230
213
|
try {
|
|
231
214
|
await this.connectionProvider.removeContainer(ctx.containerId);
|
|
232
215
|
} catch (error) {
|
|
233
|
-
|
|
216
|
+
this.logger.debug('Container removal failed (may already be gone):', error);
|
|
234
217
|
}
|
|
235
218
|
const lastContainerKey = `${LAST_USED_CONTAINER}:${ctx.devcontainerFilePath}`;
|
|
236
219
|
await this.storageService.setData(lastContainerKey, undefined);
|
|
@@ -254,7 +237,7 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
254
237
|
try {
|
|
255
238
|
await this.connectionProvider.removeContainer(lastContainerInfo.id);
|
|
256
239
|
} catch (error) {
|
|
257
|
-
|
|
240
|
+
this.logger.debug('Container removal failed (may already be gone):', error);
|
|
258
241
|
}
|
|
259
242
|
await this.storageService.setData(lastContainerInfoKey, undefined);
|
|
260
243
|
}
|
|
@@ -281,25 +264,159 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
281
264
|
workspacePath: hostWorkspacePath
|
|
282
265
|
});
|
|
283
266
|
|
|
284
|
-
this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
|
|
267
|
+
await this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
|
|
285
268
|
id: connectionResult.containerId,
|
|
286
269
|
lastUsed: Date.now()
|
|
287
270
|
});
|
|
288
271
|
|
|
289
272
|
// Store full context so rebuild works from inside the container
|
|
290
|
-
this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
|
|
273
|
+
await this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
|
|
291
274
|
devcontainerFilePath: devcontainerFile.path,
|
|
292
275
|
devcontainerFileName: devcontainerFile.name,
|
|
293
276
|
hostWorkspacePath: hostWorkspacePath ?? '',
|
|
294
277
|
containerId: connectionResult.containerId,
|
|
295
278
|
});
|
|
296
279
|
|
|
297
|
-
this.workspaceServer.setMostRecentlyUsedWorkspace(
|
|
280
|
+
await this.workspaceServer.setMostRecentlyUsedWorkspace(
|
|
298
281
|
`${DEV_CONTAINER_WORKSPACE_SCHEME}:${hostWorkspacePath}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
|
|
299
282
|
|
|
300
283
|
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
|
|
301
284
|
}
|
|
302
285
|
|
|
286
|
+
async attachToRunningContainer(): Promise<void> {
|
|
287
|
+
try {
|
|
288
|
+
const confirmed = await new ConfirmDialog({
|
|
289
|
+
title: nls.localize('theia/remote/dev-container/attachWarningTitle', 'Attach to Container'),
|
|
290
|
+
msg: nls.localize('theia/remote/dev-container/attachWarning',
|
|
291
|
+
'Attaching to a container will execute code inside it. Only attach to containers whose origin you trust.'),
|
|
292
|
+
ok: nls.localizeByDefault('Continue'),
|
|
293
|
+
cancel: Dialog.CANCEL,
|
|
294
|
+
}).open();
|
|
295
|
+
if (!confirmed) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const containers = await this.connectionProvider.listRunningContainers();
|
|
300
|
+
if (containers.length === 0) {
|
|
301
|
+
this.messageService.info(
|
|
302
|
+
nls.localize('theia/remote/dev-container/noRunningContainers', 'No running containers found.')
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const selectedItem = await this.quickInputService.pick(containers.map(container => ({
|
|
308
|
+
type: 'item' as const,
|
|
309
|
+
label: container.name,
|
|
310
|
+
description: container.image,
|
|
311
|
+
detail: container.status,
|
|
312
|
+
container
|
|
313
|
+
})), {
|
|
314
|
+
title: nls.localize('theia/remote/dev-container/selectContainer', 'Select a running container to attach to')
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (!selectedItem) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const selectedContainer: RunningContainerInfo = selectedItem.container;
|
|
322
|
+
|
|
323
|
+
const candidates: WorkspaceCandidate[] = await this.connectionProvider.getWorkspaceCandidates(selectedContainer.id);
|
|
324
|
+
|
|
325
|
+
const customPathId = '__custom_path__';
|
|
326
|
+
const pathItem = await this.quickInputService.pick([
|
|
327
|
+
...candidates.map(candidate => ({
|
|
328
|
+
type: 'item' as const,
|
|
329
|
+
label: candidate.path,
|
|
330
|
+
description: candidate.source
|
|
331
|
+
})),
|
|
332
|
+
{
|
|
333
|
+
type: 'item' as const,
|
|
334
|
+
id: customPathId,
|
|
335
|
+
label: nls.localize('theia/remote/dev-container/enterCustomPath', 'Enter custom path...'),
|
|
336
|
+
description: ''
|
|
337
|
+
}
|
|
338
|
+
], {
|
|
339
|
+
title: nls.localize('theia/remote/dev-container/selectWorkspacePath', 'Select a workspace path inside the container')
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (!pathItem) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let selectedPath: string | undefined;
|
|
347
|
+
if ('id' in pathItem && pathItem.id === customPathId) {
|
|
348
|
+
selectedPath = await this.quickInputService.input({
|
|
349
|
+
prompt: nls.localize('theia/remote/dev-container/enterWorkspacePath', 'Enter the workspace path inside the container'),
|
|
350
|
+
value: '/'
|
|
351
|
+
});
|
|
352
|
+
} else {
|
|
353
|
+
selectedPath = pathItem.label;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!selectedPath) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let devcontainerFile: string | undefined;
|
|
361
|
+
const applyConfigPref = this.devContainerPreferences['devcontainer.attach.applyFoundConfig'];
|
|
362
|
+
|
|
363
|
+
if (applyConfigPref !== 'never') {
|
|
364
|
+
const foundConfig = await this.connectionProvider.scanForDevContainerConfig(selectedContainer.id, selectedPath);
|
|
365
|
+
if (foundConfig) {
|
|
366
|
+
if (applyConfigPref === 'always') {
|
|
367
|
+
devcontainerFile = foundConfig;
|
|
368
|
+
} else {
|
|
369
|
+
// 'ask'
|
|
370
|
+
const applyConfig = await new ConfirmDialog({
|
|
371
|
+
title: nls.localize('theia/remote/dev-container/foundConfigTitle', 'Dev Container Configuration Found'),
|
|
372
|
+
msg: nls.localize('theia/remote/dev-container/foundConfigMsg',
|
|
373
|
+
'A devcontainer.json was found at {0}. Would you like to apply its post-attach settings (extensions, port forwarding, etc.)?',
|
|
374
|
+
foundConfig),
|
|
375
|
+
ok: nls.localizeByDefault('Apply'),
|
|
376
|
+
cancel: Dialog.CANCEL,
|
|
377
|
+
}).open();
|
|
378
|
+
if (applyConfig) {
|
|
379
|
+
devcontainerFile = foundConfig;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.containerOutputProvider.openChannel();
|
|
386
|
+
|
|
387
|
+
const progress = await this.messageService.showProgress({
|
|
388
|
+
text: nls.localize('theia/remote/dev-container/attaching', 'Attaching to container')
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const connectionResult = await this.connectionProvider.attachToContainer({
|
|
393
|
+
containerId: selectedContainer.id,
|
|
394
|
+
workspacePath: selectedPath,
|
|
395
|
+
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
|
|
396
|
+
devcontainerFile,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Store context so rebuild works from inside the container
|
|
400
|
+
await this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
|
|
401
|
+
devcontainerFilePath: devcontainerFile ?? '',
|
|
402
|
+
devcontainerFileName: devcontainerFile ? URI.fromFilePath(devcontainerFile).path.base : '',
|
|
403
|
+
hostWorkspacePath: '',
|
|
404
|
+
containerId: connectionResult.containerId,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
|
|
408
|
+
} finally {
|
|
409
|
+
progress.cancel();
|
|
410
|
+
}
|
|
411
|
+
} catch (e) {
|
|
412
|
+
this.messageService.error(nls.localize(
|
|
413
|
+
'theia/remote/dev-container/attachError',
|
|
414
|
+
'Failed to attach to container: {0}',
|
|
415
|
+
e instanceof Error ? e.message : String(e)
|
|
416
|
+
));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
303
420
|
async getOrSelectDevcontainerFile(): Promise<DevContainerFile | undefined> {
|
|
304
421
|
const workspace = this.workspaceService.workspace;
|
|
305
422
|
if (!workspace) {
|
|
@@ -24,6 +24,8 @@ import { FrontendApplicationContribution, LabelProviderContribution } from '@the
|
|
|
24
24
|
import { WorkspaceOpenHandlerContribution } from '@theia/workspace/lib/browser/workspace-service';
|
|
25
25
|
import { WindowTitleContribution } from '@theia/core/lib/browser/window/window-title-service';
|
|
26
26
|
import { DevContainerSuggestionContribution } from './dev-container-suggestion-contribution';
|
|
27
|
+
import { bindDevContainerPreferences } from '../electron-common/dev-container-preferences';
|
|
28
|
+
import { DevContainerStartupContribution } from './dev-container-startup-contribution';
|
|
27
29
|
|
|
28
30
|
export default new ContainerModule(bind => {
|
|
29
31
|
bind(ContainerConnectionContribution).toSelf().inSingletonScope();
|
|
@@ -31,6 +33,7 @@ export default new ContainerModule(bind => {
|
|
|
31
33
|
bind(WorkspaceOpenHandlerContribution).toService(ContainerConnectionContribution);
|
|
32
34
|
|
|
33
35
|
bind(ContainerOutputProvider).toSelf().inSingletonScope();
|
|
36
|
+
bindDevContainerPreferences(bind);
|
|
34
37
|
|
|
35
38
|
bind(RemoteContainerConnectionProvider).toDynamicValue(ctx => {
|
|
36
39
|
const outputProvider = ctx.container.get(ContainerOutputProvider);
|
|
@@ -44,4 +47,7 @@ export default new ContainerModule(bind => {
|
|
|
44
47
|
|
|
45
48
|
bind(DevContainerSuggestionContribution).toSelf().inSingletonScope();
|
|
46
49
|
bind(FrontendApplicationContribution).toService(DevContainerSuggestionContribution);
|
|
50
|
+
|
|
51
|
+
bind(DevContainerStartupContribution).toSelf().inSingletonScope();
|
|
52
|
+
bind(FrontendApplicationContribution).toService(DevContainerStartupContribution);
|
|
47
53
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH 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 { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
|
19
|
+
import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
|
20
|
+
import { AbstractRemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
|
|
21
|
+
import { ILogger, MessageService, nls } from '@theia/core';
|
|
22
|
+
import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
|
|
23
|
+
|
|
24
|
+
@injectable()
|
|
25
|
+
export class DevContainerStartupContribution extends AbstractRemoteRegistryContribution implements FrontendApplicationContribution {
|
|
26
|
+
|
|
27
|
+
@inject(RemoteContainerConnectionProvider)
|
|
28
|
+
protected readonly connectionProvider: RemoteContainerConnectionProvider;
|
|
29
|
+
|
|
30
|
+
@inject(ILogger)
|
|
31
|
+
protected readonly logger: ILogger;
|
|
32
|
+
|
|
33
|
+
@inject(MessageService)
|
|
34
|
+
protected readonly messageService: MessageService;
|
|
35
|
+
|
|
36
|
+
@inject(RemotePreferences)
|
|
37
|
+
protected readonly remotePreferences: RemotePreferences;
|
|
38
|
+
|
|
39
|
+
registerRemoteCommands(): void {
|
|
40
|
+
// no commands to register — this contribution only handles startup
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
onStart(): void {
|
|
44
|
+
this.handleStartupAttach();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected async handleStartupAttach(): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const args = await this.connectionProvider.getAttachContainerArgs();
|
|
50
|
+
if (!args) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { containerId, scanForDevJson } = args;
|
|
55
|
+
this.logger.info(`CLI: --attach-container ${containerId}, initiating attach from frontend...`);
|
|
56
|
+
|
|
57
|
+
const containers = await this.connectionProvider.listRunningContainers();
|
|
58
|
+
// Match by ID prefix (either direction — user may pass a short prefix or a full 64-char ID
|
|
59
|
+
// while listRunningContainers returns 12-char truncated IDs) or exact name.
|
|
60
|
+
const matches = containers.filter(c => c.id.startsWith(containerId) || containerId.startsWith(c.id) || c.name === containerId);
|
|
61
|
+
if (matches.length > 1) {
|
|
62
|
+
this.logger.warn(`CLI: container identifier "${containerId}" matches ${matches.length} containers, using first match: ${matches[0].name || matches[0].id}`);
|
|
63
|
+
}
|
|
64
|
+
const target = matches[0];
|
|
65
|
+
|
|
66
|
+
if (!target) {
|
|
67
|
+
const msg = nls.localize('theia/remote/dev-container/cliContainerNotFound',
|
|
68
|
+
'Container "{0}" not found or not running.', containerId);
|
|
69
|
+
this.logger.error(`CLI: ${msg}`);
|
|
70
|
+
this.messageService.error(msg);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const candidates = await this.connectionProvider.getWorkspaceCandidates(target.id);
|
|
75
|
+
const workspacePath = candidates.length > 0 ? candidates[0].path : '/';
|
|
76
|
+
|
|
77
|
+
const devcontainerFile = scanForDevJson
|
|
78
|
+
? await this.connectionProvider.scanForDevContainerConfig(target.id, workspacePath)
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
81
|
+
const result = await this.connectionProvider.attachToContainer({
|
|
82
|
+
containerId: target.id,
|
|
83
|
+
workspacePath,
|
|
84
|
+
devcontainerFile,
|
|
85
|
+
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.logger.info(`CLI: startup attach ready, proxy on port ${result.port}, workspace: ${result.workspacePath}`);
|
|
89
|
+
this.openRemote(result.port, false, result.workspacePath);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
this.logger.error('CLI: Failed to attach to container during startup:', e);
|
|
92
|
+
this.messageService.error(nls.localize(
|
|
93
|
+
'theia/remote/dev-container/cliAttachError',
|
|
94
|
+
'Failed to attach to container: {0}',
|
|
95
|
+
e instanceof Error ? e.message : String(e)
|
|
96
|
+
));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH 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 { interfaces } from '@theia/core/shared/inversify';
|
|
18
|
+
import { PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
|
|
19
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
20
|
+
import { PreferenceProxyFactory } from '@theia/core/lib/common/preferences/injectable-preference-proxy';
|
|
21
|
+
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
|
22
|
+
|
|
23
|
+
export const DevContainerPreferenceSchema: PreferenceSchema = {
|
|
24
|
+
properties: {
|
|
25
|
+
'devcontainer.attach.applyFoundConfig': {
|
|
26
|
+
type: 'string',
|
|
27
|
+
enum: ['always', 'ask', 'never'],
|
|
28
|
+
default: 'ask',
|
|
29
|
+
markdownDescription: nls.localize(
|
|
30
|
+
'theia/devContainer/attach/applyFoundConfig',
|
|
31
|
+
'Controls whether to apply a devcontainer.json found inside a container when attaching. '
|
|
32
|
+
+ 'Extensions, settings, port forwarding, and post-attach commands from the configuration will be applied.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface DevContainerPreferenceConfiguration {
|
|
39
|
+
'devcontainer.attach.applyFoundConfig': 'always' | 'ask' | 'never';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DevContainerPreferenceContribution = Symbol('DevContainerPreferenceContribution');
|
|
43
|
+
export const DevContainerPreferences = Symbol('DevContainerPreferences');
|
|
44
|
+
export type DevContainerPreferences = PreferenceProxy<DevContainerPreferenceConfiguration>;
|
|
45
|
+
|
|
46
|
+
export function bindDevContainerPreferences(bind: interfaces.Bind): void {
|
|
47
|
+
bind(DevContainerPreferences).toDynamicValue(ctx => {
|
|
48
|
+
const factory = ctx.container.get<PreferenceProxyFactory>(PreferenceProxyFactory);
|
|
49
|
+
return factory(DevContainerPreferenceSchema);
|
|
50
|
+
}).inSingletonScope();
|
|
51
|
+
bind(DevContainerPreferenceContribution).toConstantValue({ schema: DevContainerPreferenceSchema });
|
|
52
|
+
bind(PreferenceContribution).toService(DevContainerPreferenceContribution);
|
|
53
|
+
}
|
|
@@ -50,6 +50,25 @@ export interface RunningContainerInfo {
|
|
|
50
50
|
name: string;
|
|
51
51
|
image: string;
|
|
52
52
|
status: string;
|
|
53
|
+
created: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WorkspaceCandidate {
|
|
57
|
+
path: string;
|
|
58
|
+
/** Describes where this candidate came from */
|
|
59
|
+
source: 'working-dir' | 'bind-mount' | 'devcontainer-label' | 'fallback';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AttachContainerOptions {
|
|
63
|
+
containerId: string;
|
|
64
|
+
workspacePath: string;
|
|
65
|
+
nodeDownloadTemplate?: string;
|
|
66
|
+
devcontainerFile?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AttachContainerArgs {
|
|
70
|
+
containerId: string;
|
|
71
|
+
scanForDevJson: boolean;
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOutputProvider> {
|
|
@@ -57,6 +76,9 @@ export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOu
|
|
|
57
76
|
getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]>;
|
|
58
77
|
getCurrentContainerInfo(port: number): Promise<ContainerInspectInfo | undefined>;
|
|
59
78
|
listRunningContainers(): Promise<RunningContainerInfo[]>;
|
|
60
|
-
|
|
79
|
+
getWorkspaceCandidates(containerId: string): Promise<WorkspaceCandidate[]>;
|
|
80
|
+
scanForDevContainerConfig(containerId: string, workspacePath: string): Promise<string | undefined>;
|
|
81
|
+
getAttachContainerArgs(): Promise<AttachContainerArgs | undefined>;
|
|
82
|
+
attachToContainer(options: AttachContainerOptions): Promise<ContainerConnectionResult>;
|
|
61
83
|
removeContainer(containerId: string): Promise<void>;
|
|
62
84
|
}
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
18
|
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
|
19
|
+
import { DevContainerCliContribution } from './dev-container-cli-contribution';
|
|
20
|
+
import { CliContribution } from '@theia/core/lib/node';
|
|
19
21
|
import { DevContainerConnectionProvider } from './remote-container-connection-provider';
|
|
20
22
|
import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
|
|
21
23
|
import { ContainerCreationContribution, DockerContainerService } from './docker-container-service';
|
|
@@ -66,4 +68,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|
|
66
68
|
|
|
67
69
|
bind(DevContainerWorkspaceHandler).toSelf().inSingletonScope();
|
|
68
70
|
bind(WorkspaceHandlerContribution).toService(DevContainerWorkspaceHandler);
|
|
71
|
+
|
|
72
|
+
bind(DevContainerCliContribution).toSelf().inSingletonScope();
|
|
73
|
+
bind(CliContribution).toService(DevContainerCliContribution);
|
|
69
74
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH 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 { expect } from 'chai';
|
|
18
|
+
import { DevContainerCliContribution } from './dev-container-cli-contribution';
|
|
19
|
+
import { Arguments } from '@theia/core/shared/yargs';
|
|
20
|
+
|
|
21
|
+
function createArgs(overrides: Record<string, unknown> = {}): Arguments {
|
|
22
|
+
return {
|
|
23
|
+
_: [],
|
|
24
|
+
'$0': '',
|
|
25
|
+
...overrides
|
|
26
|
+
} as unknown as Arguments;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('DevContainerCliContribution', () => {
|
|
30
|
+
|
|
31
|
+
describe('setArguments / getAttachContainerId', () => {
|
|
32
|
+
|
|
33
|
+
it('should return undefined when --attach-container not provided', () => {
|
|
34
|
+
const contribution = new DevContainerCliContribution();
|
|
35
|
+
contribution.setArguments(createArgs());
|
|
36
|
+
expect(contribution.getAttachContainerId()).to.be.undefined;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should stash container ID from --attach-container', () => {
|
|
40
|
+
const contribution = new DevContainerCliContribution();
|
|
41
|
+
contribution.setArguments(createArgs({ 'attach-container': 'abc123' }));
|
|
42
|
+
expect(contribution.getAttachContainerId()).to.equal('abc123');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should convert numeric container ID to string', () => {
|
|
46
|
+
const contribution = new DevContainerCliContribution();
|
|
47
|
+
contribution.setArguments(createArgs({ 'attach-container': 12345 }));
|
|
48
|
+
expect(contribution.getAttachContainerId()).to.equal('12345');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should ignore empty string --attach-container', () => {
|
|
52
|
+
const contribution = new DevContainerCliContribution();
|
|
53
|
+
contribution.setArguments(createArgs({ 'attach-container': '' }));
|
|
54
|
+
expect(contribution.getAttachContainerId()).to.be.undefined;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should ignore whitespace-only --attach-container', () => {
|
|
58
|
+
const contribution = new DevContainerCliContribution();
|
|
59
|
+
contribution.setArguments(createArgs({ 'attach-container': ' ' }));
|
|
60
|
+
expect(contribution.getAttachContainerId()).to.be.undefined;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('consumeAttachContainerId', () => {
|
|
65
|
+
|
|
66
|
+
it('should return the container ID and clear it', () => {
|
|
67
|
+
const contribution = new DevContainerCliContribution();
|
|
68
|
+
contribution.setArguments(createArgs({ 'attach-container': 'abc123' }));
|
|
69
|
+
expect(contribution.consumeAttachContainerId()).to.equal('abc123');
|
|
70
|
+
expect(contribution.consumeAttachContainerId()).to.be.undefined;
|
|
71
|
+
expect(contribution.getAttachContainerId()).to.be.undefined;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return undefined when no ID was set', () => {
|
|
75
|
+
const contribution = new DevContainerCliContribution();
|
|
76
|
+
contribution.setArguments(createArgs());
|
|
77
|
+
expect(contribution.consumeAttachContainerId()).to.be.undefined;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('shouldScanForDevJson', () => {
|
|
82
|
+
|
|
83
|
+
it('should default to true', () => {
|
|
84
|
+
const contribution = new DevContainerCliContribution();
|
|
85
|
+
expect(contribution.shouldScanForDevJson()).to.be.true;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should remain true when --dev-json is true', () => {
|
|
89
|
+
const contribution = new DevContainerCliContribution();
|
|
90
|
+
contribution.setArguments(createArgs({ 'dev-json': true }));
|
|
91
|
+
expect(contribution.shouldScanForDevJson()).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should be false when --dev-json is false', () => {
|
|
95
|
+
const contribution = new DevContainerCliContribution();
|
|
96
|
+
contribution.setArguments(createArgs({ 'dev-json': false }));
|
|
97
|
+
expect(contribution.shouldScanForDevJson()).to.be.false;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should be true when --dev-json is undefined', () => {
|
|
101
|
+
const contribution = new DevContainerCliContribution();
|
|
102
|
+
contribution.setArguments(createArgs({ 'dev-json': undefined }));
|
|
103
|
+
expect(contribution.shouldScanForDevJson()).to.be.true;
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|