@theia/dev-container 1.71.0-next.72 → 1.71.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/README.md +1 -0
- package/lib/electron-browser/container-connection-contribution.d.ts +8 -0
- package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
- package/lib/electron-browser/container-connection-contribution.js +158 -6
- package/lib/electron-browser/container-connection-contribution.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 +3 -0
- package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
- package/lib/electron-browser/dev-container-suggestion-contribution.d.ts +16 -0
- package/lib/electron-browser/dev-container-suggestion-contribution.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-suggestion-contribution.js +96 -0
- package/lib/electron-browser/dev-container-suggestion-contribution.js.map +1 -0
- package/lib/electron-common/remote-container-connection-provider.d.ts +9 -0
- package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
- package/lib/electron-node/dev-container-file-service.js +4 -6
- package/lib/electron-node/dev-container-file-service.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.js +7 -1
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.d.ts +2 -0
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.d.ts.map +1 -0
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.js +421 -0
- package/lib/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.js.map +1 -0
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +28 -1
- 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 +304 -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.map +1 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +0 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
- package/lib/electron-node/devcontainer-file.d.ts +8 -1
- package/lib/electron-node/devcontainer-file.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-file.js +14 -0
- package/lib/electron-node/devcontainer-file.js.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.d.ts +6 -1
- package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.js +112 -4
- package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts +2 -0
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +1 -0
- package/lib/electron-node/remote-container-connection-provider.spec.js +131 -0
- package/lib/electron-node/remote-container-connection-provider.spec.js.map +1 -0
- package/package.json +7 -7
- package/src/electron-browser/container-connection-contribution.ts +173 -7
- package/src/electron-browser/dev-container-frontend-module.ts +4 -0
- package/src/electron-browser/dev-container-suggestion-contribution.ts +93 -0
- package/src/electron-common/remote-container-connection-provider.ts +10 -0
- package/src/electron-node/dev-container-file-service.ts +4 -6
- package/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.spec.ts +519 -0
- package/src/electron-node/devcontainer-contributions/cli-enhancing-creation-contributions.ts +7 -1
- package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +323 -5
- package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +0 -1
- package/src/electron-node/devcontainer-file.ts +13 -1
- package/src/electron-node/remote-container-connection-provider.spec.ts +152 -0
- package/src/electron-node/remote-container-connection-provider.ts +121 -5
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
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
19
|
import { DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
|
20
20
|
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
|
|
@@ -32,9 +32,29 @@ export namespace RemoteContainerCommands {
|
|
|
32
32
|
label: 'Reopen in Container',
|
|
33
33
|
category: 'Dev Container'
|
|
34
34
|
}, 'theia/remote/dev-container/connect');
|
|
35
|
+
|
|
36
|
+
export const ATTACH_TO_CONTAINER = Command.toLocalizedCommand({
|
|
37
|
+
id: 'dev-container:attach-to-container',
|
|
38
|
+
label: 'Attach to Running Container',
|
|
39
|
+
category: 'Dev Container'
|
|
40
|
+
}, 'theia/remote/dev-container/attach');
|
|
41
|
+
|
|
42
|
+
export const REBUILD_CONTAINER = Command.toLocalizedCommand({
|
|
43
|
+
id: 'dev-container:rebuild-container',
|
|
44
|
+
label: 'Rebuild Container',
|
|
45
|
+
category: 'Dev Container'
|
|
46
|
+
}, 'theia/remote/dev-container/rebuild');
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
const LAST_USED_CONTAINER = 'lastUsedContainer';
|
|
50
|
+
const ACTIVE_DEV_CONTAINER_CONTEXT = 'activeDevContainerContext';
|
|
51
|
+
|
|
52
|
+
interface DevContainerContext {
|
|
53
|
+
devcontainerFilePath: string;
|
|
54
|
+
devcontainerFileName: string;
|
|
55
|
+
hostWorkspacePath: string;
|
|
56
|
+
containerId: string;
|
|
57
|
+
}
|
|
38
58
|
@injectable()
|
|
39
59
|
export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution implements WorkspaceOpenHandlerContribution {
|
|
40
60
|
|
|
@@ -65,28 +85,85 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
65
85
|
@inject(ContainerOutputProvider)
|
|
66
86
|
protected readonly containerOutputProvider: ContainerOutputProvider;
|
|
67
87
|
|
|
88
|
+
protected hasDevContainerFiles = false;
|
|
89
|
+
|
|
90
|
+
@postConstruct()
|
|
91
|
+
protected init(): void {
|
|
92
|
+
// Mark that we're in a remote session. sessionStorage survives page
|
|
93
|
+
// reloads (disconnect) but is cleared on window close (restart).
|
|
94
|
+
// This lets canHandle() distinguish disconnect from restart.
|
|
95
|
+
if (this.isRemoteSession()) {
|
|
96
|
+
sessionStorage.setItem('devcontainer:wasRemote', 'true');
|
|
97
|
+
}
|
|
98
|
+
this.workspaceService.ready.then(() => this.checkForDevContainerFiles());
|
|
99
|
+
this.workspaceService.onWorkspaceChanged(() => this.checkForDevContainerFiles());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected async checkForDevContainerFiles(): Promise<void> {
|
|
103
|
+
if (this.isRemoteSession()) {
|
|
104
|
+
this.hasDevContainerFiles = true;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const workspace = this.workspaceService.workspace;
|
|
108
|
+
if (!workspace) {
|
|
109
|
+
this.hasDevContainerFiles = false;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const files = await this.connectionProvider.getDevContainerFiles(workspace.resource.path.toString());
|
|
114
|
+
this.hasDevContainerFiles = files.length > 0;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Failed to check for devcontainer files, assume none exist
|
|
117
|
+
this.hasDevContainerFiles = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
68
121
|
registerRemoteCommands(registry: RemoteRegistry): void {
|
|
69
122
|
registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
|
|
70
|
-
execute: () => this.openInContainer()
|
|
123
|
+
execute: () => this.openInContainer(),
|
|
124
|
+
isVisible: () => !this.isRemoteSession() && this.hasDevContainerFiles
|
|
71
125
|
});
|
|
126
|
+
registry.registerCommand(RemoteContainerCommands.ATTACH_TO_CONTAINER, {
|
|
127
|
+
execute: () => this.attachToContainer()
|
|
128
|
+
});
|
|
129
|
+
registry.registerCommand(RemoteContainerCommands.REBUILD_CONTAINER, {
|
|
130
|
+
execute: () => this.rebuildContainer(),
|
|
131
|
+
isVisible: () => this.isRemoteSession()
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
protected isRemoteSession(): boolean {
|
|
136
|
+
return new URLSearchParams(window.location.search).has('localPort');
|
|
72
137
|
}
|
|
73
138
|
|
|
74
139
|
canHandle(uri: URI): MaybePromise<boolean> {
|
|
75
|
-
|
|
140
|
+
if (uri.scheme !== DEV_CONTAINER_WORKSPACE_SCHEME) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
// After disconnect (reload), sessionStorage still has the flag from
|
|
144
|
+
// the remote session's init. Skip auto-reopen so the user gets their
|
|
145
|
+
// local workspace. After restart (close+open), sessionStorage is
|
|
146
|
+
// cleared, so auto-reopen works.
|
|
147
|
+
const wasRemote = sessionStorage.getItem('devcontainer:wasRemote');
|
|
148
|
+
if (wasRemote) {
|
|
149
|
+
sessionStorage.removeItem('devcontainer:wasRemote');
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
76
153
|
}
|
|
77
154
|
|
|
78
155
|
async openWorkspace(uri: URI, options?: WorkspaceInput | undefined): Promise<void> {
|
|
79
156
|
const filePath = new URLSearchParams(uri.query).get(DEV_CONTAINER_PATH_QUERY);
|
|
80
157
|
|
|
81
158
|
if (!filePath) {
|
|
82
|
-
throw new Error('No devcontainer file specified for workspace');
|
|
159
|
+
throw new Error(nls.localize('theia/dev-container/noDevcontainerFileSpecified', 'No devcontainer file specified for workspace'));
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(uri.path.toString());
|
|
86
163
|
const devcontainerFile = devcontainerFiles.find(file => file.path === filePath);
|
|
87
164
|
|
|
88
165
|
if (!devcontainerFile) {
|
|
89
|
-
throw new Error(
|
|
166
|
+
throw new Error(nls.localize('theia/dev-container/devcontainerFileNotFound', 'Devcontainer file at {0} not found in workspace', filePath));
|
|
90
167
|
}
|
|
91
168
|
|
|
92
169
|
return this.doOpenInContainer(devcontainerFile, uri.path.toString());
|
|
@@ -110,17 +187,98 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
110
187
|
this.doOpenInContainer(devcontainerFile);
|
|
111
188
|
}
|
|
112
189
|
|
|
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
|
+
async rebuildContainer(): Promise<void> {
|
|
218
|
+
this.containerOutputProvider.openChannel();
|
|
219
|
+
const progress = await this.messageService.showProgress({
|
|
220
|
+
text: nls.localize('theia/remote/dev-container/rebuilding', 'Rebuilding dev container')
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// When inside a remote container, read the stored context instead of
|
|
225
|
+
// scanning the filesystem (the RPC goes to the local backend which
|
|
226
|
+
// doesn't have the container's workspace path).
|
|
227
|
+
const ctx = await this.storageService.getData<DevContainerContext | undefined>(ACTIVE_DEV_CONTAINER_CONTEXT);
|
|
228
|
+
if (ctx) {
|
|
229
|
+
progress.report({ message: nls.localize('theia/dev-container/removingOldContainer', 'Removing old container...') });
|
|
230
|
+
try {
|
|
231
|
+
await this.connectionProvider.removeContainer(ctx.containerId);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
// Container may already be gone, ignore error
|
|
234
|
+
}
|
|
235
|
+
const lastContainerKey = `${LAST_USED_CONTAINER}:${ctx.devcontainerFilePath}`;
|
|
236
|
+
await this.storageService.setData(lastContainerKey, undefined);
|
|
237
|
+
progress.cancel();
|
|
238
|
+
this.doOpenInContainer(
|
|
239
|
+
{ path: ctx.devcontainerFilePath, name: ctx.devcontainerFileName },
|
|
240
|
+
ctx.hostWorkspacePath
|
|
241
|
+
);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Fallback: local workspace — scan for devcontainer files
|
|
246
|
+
const devcontainerFile = await this.getOrSelectDevcontainerFile();
|
|
247
|
+
if (!devcontainerFile) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
|
|
251
|
+
const lastContainerInfo = await this.storageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
|
|
252
|
+
if (lastContainerInfo) {
|
|
253
|
+
progress.report({ message: nls.localize('theia/dev-container/removingOldContainer', 'Removing old container...') });
|
|
254
|
+
try {
|
|
255
|
+
await this.connectionProvider.removeContainer(lastContainerInfo.id);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Container may already be gone, ignore error
|
|
258
|
+
}
|
|
259
|
+
await this.storageService.setData(lastContainerInfoKey, undefined);
|
|
260
|
+
}
|
|
261
|
+
progress.cancel();
|
|
262
|
+
this.doOpenInContainer(devcontainerFile);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
progress.cancel();
|
|
265
|
+
this.messageService.error(nls.localize('theia/dev-container/failedToRebuild', 'Failed to rebuild container: {0}', (e as Error).message));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
113
269
|
async doOpenInContainer(devcontainerFile: DevContainerFile, workspacePath?: string): Promise<void> {
|
|
114
270
|
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
|
|
115
271
|
const lastContainerInfo = await this.storageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
|
|
116
272
|
|
|
117
273
|
this.containerOutputProvider.openChannel();
|
|
118
274
|
|
|
275
|
+
const hostWorkspacePath = workspacePath ?? this.workspaceService.workspace?.resource.path.toString();
|
|
276
|
+
|
|
119
277
|
const connectionResult = await this.connectionProvider.connectToContainer({
|
|
120
278
|
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
|
|
121
279
|
lastContainerInfo,
|
|
122
280
|
devcontainerFile: devcontainerFile.path,
|
|
123
|
-
workspacePath:
|
|
281
|
+
workspacePath: hostWorkspacePath
|
|
124
282
|
});
|
|
125
283
|
|
|
126
284
|
this.storageService.setData<LastContainerInfo>(lastContainerInfoKey, {
|
|
@@ -128,8 +286,16 @@ export class ContainerConnectionContribution extends AbstractRemoteRegistryContr
|
|
|
128
286
|
lastUsed: Date.now()
|
|
129
287
|
});
|
|
130
288
|
|
|
289
|
+
// Store full context so rebuild works from inside the container
|
|
290
|
+
this.storageService.setData<DevContainerContext>(ACTIVE_DEV_CONTAINER_CONTEXT, {
|
|
291
|
+
devcontainerFilePath: devcontainerFile.path,
|
|
292
|
+
devcontainerFileName: devcontainerFile.name,
|
|
293
|
+
hostWorkspacePath: hostWorkspacePath ?? '',
|
|
294
|
+
containerId: connectionResult.containerId,
|
|
295
|
+
});
|
|
296
|
+
|
|
131
297
|
this.workspaceServer.setMostRecentlyUsedWorkspace(
|
|
132
|
-
`${DEV_CONTAINER_WORKSPACE_SCHEME}:${
|
|
298
|
+
`${DEV_CONTAINER_WORKSPACE_SCHEME}:${hostWorkspacePath}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
|
|
133
299
|
|
|
134
300
|
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
|
|
135
301
|
}
|
|
@@ -23,6 +23,7 @@ import { ContainerInfoContribution } from './container-info-contribution';
|
|
|
23
23
|
import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
|
|
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
|
+
import { DevContainerSuggestionContribution } from './dev-container-suggestion-contribution';
|
|
26
27
|
|
|
27
28
|
export default new ContainerModule(bind => {
|
|
28
29
|
bind(ContainerConnectionContribution).toSelf().inSingletonScope();
|
|
@@ -40,4 +41,7 @@ export default new ContainerModule(bind => {
|
|
|
40
41
|
bind(FrontendApplicationContribution).toService(ContainerInfoContribution);
|
|
41
42
|
bind(WindowTitleContribution).toService(ContainerInfoContribution);
|
|
42
43
|
bind(LabelProviderContribution).toService(ContainerInfoContribution);
|
|
44
|
+
|
|
45
|
+
bind(DevContainerSuggestionContribution).toSelf().inSingletonScope();
|
|
46
|
+
bind(FrontendApplicationContribution).toService(DevContainerSuggestionContribution);
|
|
43
47
|
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource 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 { CommandService, MessageService, nls } from '@theia/core';
|
|
19
|
+
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
|
|
20
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
|
21
|
+
import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
|
22
|
+
import { RemoteContainerCommands } from './container-connection-contribution';
|
|
23
|
+
import { RemoteStatusService } from '@theia/remote/lib/electron-common/remote-status-service';
|
|
24
|
+
|
|
25
|
+
const DONT_SHOW_AGAIN_KEY = 'dev-container.suggestion.dontShowAgain';
|
|
26
|
+
|
|
27
|
+
@injectable()
|
|
28
|
+
export class DevContainerSuggestionContribution implements FrontendApplicationContribution {
|
|
29
|
+
|
|
30
|
+
@inject(WorkspaceService)
|
|
31
|
+
protected readonly workspaceService: WorkspaceService;
|
|
32
|
+
|
|
33
|
+
@inject(RemoteContainerConnectionProvider)
|
|
34
|
+
protected readonly connectionProvider: RemoteContainerConnectionProvider;
|
|
35
|
+
|
|
36
|
+
@inject(MessageService)
|
|
37
|
+
protected readonly messageService: MessageService;
|
|
38
|
+
|
|
39
|
+
@inject(CommandService)
|
|
40
|
+
protected readonly commandService: CommandService;
|
|
41
|
+
|
|
42
|
+
@inject(RemoteStatusService)
|
|
43
|
+
protected readonly remoteStatusService: RemoteStatusService;
|
|
44
|
+
|
|
45
|
+
@inject(LocalStorageService)
|
|
46
|
+
protected readonly storageService: LocalStorageService;
|
|
47
|
+
|
|
48
|
+
onStart(): void {
|
|
49
|
+
this.checkForDevContainer();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected async checkForDevContainer(): Promise<void> {
|
|
53
|
+
const containerPort = parseInt(new URLSearchParams(location.search).get('port') ?? '0');
|
|
54
|
+
if (containerPort > 0) {
|
|
55
|
+
const status = await this.remoteStatusService.getStatus(containerPort);
|
|
56
|
+
if (status?.alive) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dontShowAgain = await this.storageService.getData<boolean>(DONT_SHOW_AGAIN_KEY);
|
|
62
|
+
if (dontShowAgain) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await this.workspaceService.ready;
|
|
67
|
+
const workspace = this.workspaceService.workspace;
|
|
68
|
+
if (!workspace) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(workspace.resource.path.toString());
|
|
74
|
+
if (devcontainerFiles.length > 0) {
|
|
75
|
+
const reopenAction = nls.localize('theia/remote/dev-container/reopenInContainer', 'Reopen in Container');
|
|
76
|
+
const dontShowAgainAction = nls.localizeByDefault("Don't Show Again");
|
|
77
|
+
const result = await this.messageService.info(
|
|
78
|
+
nls.localize('theia/remote/dev-container/suggestion',
|
|
79
|
+
'This workspace has a dev container configuration. Would you like to reopen it in a container?'),
|
|
80
|
+
reopenAction,
|
|
81
|
+
dontShowAgainAction
|
|
82
|
+
);
|
|
83
|
+
if (result === reopenAction) {
|
|
84
|
+
this.commandService.executeCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER.id);
|
|
85
|
+
} else if (result === dontShowAgainAction) {
|
|
86
|
+
await this.storageService.setData(DONT_SHOW_AGAIN_KEY, true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Silently ignore if we can't check for devcontainer files
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -45,8 +45,18 @@ export interface DevContainerFile {
|
|
|
45
45
|
path: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface RunningContainerInfo {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
image: string;
|
|
52
|
+
status: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOutputProvider> {
|
|
49
56
|
connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult>;
|
|
50
57
|
getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]>;
|
|
51
58
|
getCurrentContainerInfo(port: number): Promise<ContainerInspectInfo | undefined>;
|
|
59
|
+
listRunningContainers(): Promise<RunningContainerInfo[]>;
|
|
60
|
+
attachToContainer(containerId: string): Promise<ContainerConnectionResult>;
|
|
61
|
+
removeContainer(containerId: string): Promise<void>;
|
|
52
62
|
}
|
|
@@ -23,7 +23,7 @@ import * as fs from '@theia/core/shared/fs-extra';
|
|
|
23
23
|
import { ContributionProvider, Path, URI } from '@theia/core';
|
|
24
24
|
import { VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
|
|
25
25
|
|
|
26
|
-
const VARIABLE_REGEX =
|
|
26
|
+
const VARIABLE_REGEX = /\$\{(.+?)(?::(.+?))?\}/g;
|
|
27
27
|
|
|
28
28
|
@injectable()
|
|
29
29
|
export class DevContainerFileService {
|
|
@@ -35,16 +35,14 @@ export class DevContainerFileService {
|
|
|
35
35
|
protected readonly variableResolverContributions: ContributionProvider<VariableResolverContribution>;
|
|
36
36
|
|
|
37
37
|
protected resolveVariable(value: string): string {
|
|
38
|
-
|
|
39
|
-
if (match) {
|
|
40
|
-
const [, type, variable] = match;
|
|
38
|
+
return value.replace(VARIABLE_REGEX, (match, type, variable) => {
|
|
41
39
|
for (const contribution of this.variableResolverContributions.getContributions()) {
|
|
42
40
|
if (contribution.canResolve(type)) {
|
|
43
41
|
return contribution.resolve(variable ?? type);
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
return match;
|
|
45
|
+
});
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
protected resolveVariablesRecursively<T>(obj: T): T {
|