@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
|
@@ -15,27 +15,28 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import * as net from 'net';
|
|
18
|
+
import { PassThrough } from 'stream';
|
|
18
19
|
import {
|
|
19
|
-
ContainerConnectionOptions, ContainerConnectionResult,
|
|
20
|
-
DevContainerFile, RemoteContainerConnectionProvider, RunningContainerInfo
|
|
20
|
+
AttachContainerArgs, AttachContainerOptions, ContainerConnectionOptions, ContainerConnectionResult,
|
|
21
|
+
DevContainerFile, RemoteContainerConnectionProvider, RunningContainerInfo, WorkspaceCandidate
|
|
21
22
|
} from '../electron-common/remote-container-connection-provider';
|
|
22
|
-
import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types';
|
|
23
|
-
import { RemoteSetupResult, RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service';
|
|
24
23
|
import { RemoteConnectionService } from '@theia/remote/lib/electron-node/remote-connection-service';
|
|
24
|
+
import { RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service';
|
|
25
25
|
import { RemoteProxyServerProvider } from '@theia/remote/lib/electron-node/remote-proxy-server-provider';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
26
|
+
import { RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types';
|
|
27
|
+
import { RpcServer, ILogger, MessageService, generateUuid, URI } from '@theia/core';
|
|
28
28
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
29
29
|
import * as Docker from 'dockerode';
|
|
30
|
-
import { DockerContainerService } from './docker-container-service';
|
|
31
|
-
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
32
|
-
import { WriteStream } from 'tty';
|
|
33
|
-
import { PassThrough } from 'stream';
|
|
34
|
-
import { exec, execSync } from 'child_process';
|
|
35
30
|
import { DevContainerFileService } from './dev-container-file-service';
|
|
31
|
+
import { DockerContainerService } from './docker-container-service';
|
|
36
32
|
import { ContainerOutputProvider } from '../electron-common/container-output-provider';
|
|
33
|
+
import { DevContainerCliContribution } from './dev-container-cli-contribution';
|
|
34
|
+
import { RemoteDockerContainerConnection } from './remote-docker-container-connection';
|
|
35
|
+
// Re-export for backward compatibility — these types were moved to remote-docker-container-connection.ts
|
|
36
|
+
export { RemoteDockerContainerConnection, RemoteContainerConnectionOptions } from './remote-docker-container-connection';
|
|
37
37
|
import { DevContainerConfiguration } from './devcontainer-file';
|
|
38
|
-
import {
|
|
38
|
+
import { getWorkspaceMounts, inferWorkspacePath } from './devcontainer-util';
|
|
39
|
+
import { VariableContext } from './devcontainer-contributions/variable-resolver-contribution';
|
|
39
40
|
|
|
40
41
|
@injectable()
|
|
41
42
|
export class DevContainerConnectionProvider implements RemoteContainerConnectionProvider, RpcServer<ContainerOutputProvider> {
|
|
@@ -46,9 +47,6 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
46
47
|
@inject(RemoteSetupService)
|
|
47
48
|
protected readonly remoteSetup: RemoteSetupService;
|
|
48
49
|
|
|
49
|
-
@inject(MessageService)
|
|
50
|
-
protected readonly messageService: MessageService;
|
|
51
|
-
|
|
52
50
|
@inject(RemoteProxyServerProvider)
|
|
53
51
|
protected readonly serverProvider: RemoteProxyServerProvider;
|
|
54
52
|
|
|
@@ -58,19 +56,22 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
58
56
|
@inject(DevContainerFileService)
|
|
59
57
|
protected readonly devContainerFileService: DevContainerFileService;
|
|
60
58
|
|
|
61
|
-
@inject(
|
|
62
|
-
protected readonly
|
|
59
|
+
@inject(DevContainerCliContribution)
|
|
60
|
+
protected readonly cliContribution: DevContainerCliContribution;
|
|
63
61
|
|
|
64
62
|
@inject(ILogger)
|
|
65
63
|
protected readonly logger: ILogger;
|
|
66
64
|
|
|
65
|
+
@inject(MessageService)
|
|
66
|
+
protected readonly messageService: MessageService;
|
|
67
|
+
|
|
67
68
|
protected outputProvider: ContainerOutputProvider | undefined;
|
|
68
69
|
|
|
69
70
|
setClient(client: ContainerOutputProvider): void {
|
|
70
71
|
this.outputProvider = client;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
async
|
|
74
|
+
async createDockerConnection(): Promise<Docker> {
|
|
74
75
|
const dockerOptions: Docker.DockerOptions = {};
|
|
75
76
|
const dockerHost = process.env.DOCKER_HOST;
|
|
76
77
|
|
|
@@ -96,62 +97,181 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
96
97
|
}
|
|
97
98
|
} catch (_) {
|
|
98
99
|
this.logger.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
|
|
99
|
-
this.messageService.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const dockerConnection = new Docker(dockerOptions);
|
|
103
|
-
|
|
103
|
+
await dockerConnection.version()
|
|
104
104
|
.catch(e => {
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
this.logger.error('Docker Error:', e);
|
|
106
|
+
throw new Error(`Docker is not available: ${e.message ?? e}`);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
return dockerConnection;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async listRunningContainers(docker?: Docker): Promise<RunningContainerInfo[]> {
|
|
113
|
+
docker ??= await this.createDockerConnection();
|
|
114
|
+
const containers = await docker.listContainers({ all: false });
|
|
115
|
+
|
|
116
|
+
return containers.map(container => ({
|
|
117
|
+
id: container.Id.substring(0, 12),
|
|
118
|
+
name: (container.Names[0] || '').replace(/^\//, ''),
|
|
119
|
+
image: container.Image,
|
|
120
|
+
status: container.Status,
|
|
121
|
+
created: container.Created
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getWorkspaceCandidates(containerId: string, docker?: Docker): Promise<WorkspaceCandidate[]> {
|
|
126
|
+
docker ??= await this.createDockerConnection();
|
|
127
|
+
const container = docker.getContainer(containerId);
|
|
128
|
+
const info = await container.inspect();
|
|
129
|
+
const candidates: WorkspaceCandidate[] = [];
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
|
|
132
|
+
// Check for devcontainer metadata label (set by tools that created the container)
|
|
133
|
+
const metadataLabel = info.Config.Labels?.['devcontainer.metadata'];
|
|
134
|
+
if (metadataLabel) {
|
|
135
|
+
try {
|
|
136
|
+
const metadata = JSON.parse(metadataLabel);
|
|
137
|
+
if (Array.isArray(metadata)) {
|
|
138
|
+
for (const entry of metadata) {
|
|
139
|
+
if (entry.remoteWorkspaceFolder && !seen.has(entry.remoteWorkspaceFolder)) {
|
|
140
|
+
seen.add(entry.remoteWorkspaceFolder);
|
|
141
|
+
candidates.push({ path: entry.remoteWorkspaceFolder, source: 'devcontainer-label' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore malformed metadata
|
|
147
|
+
}
|
|
112
148
|
}
|
|
113
149
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
const localFolderLabel = info.Config.Labels?.['devcontainer.local_folder'];
|
|
151
|
+
if (localFolderLabel) {
|
|
152
|
+
const basename = URI.fromFilePath(localFolderLabel).path.base;
|
|
153
|
+
if (basename) {
|
|
154
|
+
const workspacePath = `/workspaces/${basename}`;
|
|
155
|
+
if (!seen.has(workspacePath)) {
|
|
156
|
+
seen.add(workspacePath);
|
|
157
|
+
candidates.push({ path: workspacePath, source: 'devcontainer-label' });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
if (info.Config.WorkingDir && info.Config.WorkingDir !== '/' && !seen.has(info.Config.WorkingDir)) {
|
|
163
|
+
seen.add(info.Config.WorkingDir);
|
|
164
|
+
candidates.push({ path: info.Config.WorkingDir, source: 'working-dir' });
|
|
165
|
+
}
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
for (const mount of getWorkspaceMounts(info.Mounts ?? [])) {
|
|
168
|
+
if (!seen.has(mount.Destination)) {
|
|
169
|
+
seen.add(mount.Destination);
|
|
170
|
+
candidates.push({ path: mount.Destination, source: 'bind-mount' });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!seen.has('/')) {
|
|
175
|
+
candidates.push({ path: '/', source: 'fallback' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return candidates;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async scanForDevContainerConfig(containerId: string, workspacePath: string, docker?: Docker): Promise<string | undefined> {
|
|
182
|
+
docker ??= await this.createDockerConnection();
|
|
183
|
+
const container = docker.getContainer(containerId);
|
|
184
|
+
|
|
185
|
+
// Search all three standard devcontainer.json locations:
|
|
186
|
+
// <workspace>/.devcontainer/devcontainer.json
|
|
187
|
+
// <workspace>/.devcontainer.json
|
|
188
|
+
// <workspace>/.devcontainer/<subfolder>/devcontainer.json
|
|
189
|
+
// Uses a single find command to avoid multiple exec round-trips.
|
|
190
|
+
try {
|
|
191
|
+
// Use Cmd array form instead of sh -c to avoid shell injection.
|
|
192
|
+
// stderr (e.g. "No such file or directory" when .devcontainer doesn't exist)
|
|
193
|
+
// is demuxed to a separate stream and discarded.
|
|
194
|
+
const execution = await container.exec({
|
|
195
|
+
Cmd: [
|
|
196
|
+
'find',
|
|
197
|
+
`${workspacePath}/.devcontainer`, `${workspacePath}/.devcontainer.json`,
|
|
198
|
+
'-maxdepth', '2',
|
|
199
|
+
'(', '-name', 'devcontainer.json', '-o', '-name', '.devcontainer.json', ')',
|
|
200
|
+
'-type', 'f'
|
|
201
|
+
],
|
|
202
|
+
AttachStdout: true,
|
|
203
|
+
AttachStderr: true
|
|
131
204
|
});
|
|
132
|
-
remote.remoteSetupResult = result;
|
|
133
205
|
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
206
|
+
let stdout = '';
|
|
207
|
+
const stream = await execution.start({});
|
|
208
|
+
const stdoutPassthrough = new PassThrough();
|
|
209
|
+
const stderrPassthrough = new PassThrough();
|
|
210
|
+
stdoutPassthrough.on('data', (chunk: Buffer) => {
|
|
211
|
+
stdout += chunk.toString();
|
|
137
212
|
});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
213
|
+
execution.modem.demuxStream(stream, stdoutPassthrough, stderrPassthrough);
|
|
214
|
+
|
|
215
|
+
await new Promise<void>((resolve, reject) => {
|
|
216
|
+
stream.on('end', () => resolve());
|
|
217
|
+
stream.on('error', reject);
|
|
141
218
|
});
|
|
142
|
-
|
|
143
|
-
|
|
219
|
+
stdoutPassthrough.destroy();
|
|
220
|
+
stderrPassthrough.destroy();
|
|
144
221
|
|
|
145
|
-
|
|
222
|
+
const found = stdout.trim().split('\n').filter(line => line.length > 0);
|
|
223
|
+
if (found.length === 0) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
227
|
+
// Prefer the standard location, then root-level, then subfolder configs
|
|
228
|
+
const standardPath = `${workspacePath}/.devcontainer/devcontainer.json`;
|
|
229
|
+
const rootPath = `${workspacePath}/.devcontainer.json`;
|
|
230
|
+
for (const preferred of [standardPath, rootPath]) {
|
|
231
|
+
if (found.includes(preferred)) {
|
|
232
|
+
return preferred;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Return the first subfolder config found
|
|
236
|
+
return found[0];
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// find/sh might not be available in minimal containers
|
|
239
|
+
this.logger.debug('Failed to scan for devcontainer.json in container:', e);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult> {
|
|
245
|
+
const progress = await this.messageService.showProgress({ text: 'Creating container' });
|
|
246
|
+
try {
|
|
247
|
+
const report: RemoteStatusReport = message => progress.report({ message });
|
|
248
|
+
const docker = await this.createDockerConnection();
|
|
249
|
+
|
|
250
|
+
let remote: RemoteDockerContainerConnection | undefined;
|
|
251
|
+
try {
|
|
252
|
+
const container = await this.containerService.getOrCreateContainer(docker, options, this.outputProvider);
|
|
253
|
+
const context: VariableContext = { containerId: container.id };
|
|
254
|
+
const devContainerConfig = await this.devContainerFileService.getConfiguration(options.devcontainerFile, context);
|
|
255
|
+
|
|
256
|
+
report('Connecting to remote system...');
|
|
257
|
+
|
|
258
|
+
const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report);
|
|
259
|
+
remote = result.remote;
|
|
260
|
+
|
|
261
|
+
await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
containerId: container.id,
|
|
265
|
+
workspacePath: devContainerConfig.workspaceFolder ?? inferWorkspacePath(await container.inspect()),
|
|
266
|
+
port: result.localPort.toString(),
|
|
267
|
+
};
|
|
268
|
+
} catch (e) {
|
|
269
|
+
remote?.dispose();
|
|
270
|
+
this.logger.error(e);
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
152
273
|
} catch (e) {
|
|
153
274
|
this.messageService.error(e.message);
|
|
154
|
-
console.error(e);
|
|
155
275
|
throw e;
|
|
156
276
|
} finally {
|
|
157
277
|
progress.cancel();
|
|
@@ -162,363 +282,151 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
162
282
|
return this.devContainerFileService.getAvailableFiles(workspacePath);
|
|
163
283
|
}
|
|
164
284
|
|
|
165
|
-
async
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
name: config.name ?? 'dev-container',
|
|
169
|
-
type: 'Dev Container',
|
|
170
|
-
docker,
|
|
171
|
-
container,
|
|
172
|
-
config,
|
|
173
|
-
logger: this.logger
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async getCurrentContainerInfo(port: number): Promise<Docker.ContainerInspectInfo | undefined> {
|
|
178
|
-
const connection = this.remoteConnectionService.getConnectionFromPort(port);
|
|
179
|
-
if (!connection || !(connection instanceof RemoteDockerContainerConnection)) {
|
|
285
|
+
async getAttachContainerArgs(): Promise<AttachContainerArgs | undefined> {
|
|
286
|
+
const containerId = this.cliContribution.consumeAttachContainerId();
|
|
287
|
+
if (!containerId) {
|
|
180
288
|
return undefined;
|
|
181
289
|
}
|
|
182
|
-
return
|
|
290
|
+
return {
|
|
291
|
+
containerId,
|
|
292
|
+
scanForDevJson: this.cliContribution.shouldScanForDevJson()
|
|
293
|
+
};
|
|
183
294
|
}
|
|
184
295
|
|
|
185
|
-
async
|
|
296
|
+
async attachToContainer(options: AttachContainerOptions): Promise<ContainerConnectionResult> {
|
|
297
|
+
const progress = await this.messageService.showProgress({ text: 'Attaching to container' });
|
|
186
298
|
try {
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
id: container.Id,
|
|
191
|
-
name: (container.Names[0] ?? '').replace(/^\//, ''),
|
|
192
|
-
image: container.Image,
|
|
193
|
-
status: container.Status
|
|
194
|
-
}));
|
|
195
|
-
} catch (e) {
|
|
196
|
-
console.error('Failed to list running containers:', e);
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
}
|
|
299
|
+
const report: RemoteStatusReport = message => progress.report({ message });
|
|
300
|
+
const docker = await this.createDockerConnection();
|
|
301
|
+
const container = docker.getContainer(options.containerId);
|
|
200
302
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
303
|
+
const containerInfo = await container.inspect();
|
|
304
|
+
if (!containerInfo.State.Running) {
|
|
305
|
+
throw new Error(`Container ${options.containerId} is not running`);
|
|
306
|
+
}
|
|
205
307
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
id: generateUuid(),
|
|
215
|
-
name: containerInfo.Name.replace(/^\//, ''),
|
|
216
|
-
type: 'Dev Container',
|
|
217
|
-
docker,
|
|
218
|
-
container,
|
|
219
|
-
config: DevContainerConfiguration.empty(),
|
|
220
|
-
logger: this.logger
|
|
221
|
-
});
|
|
308
|
+
const context: VariableContext = { containerId: options.containerId };
|
|
309
|
+
let config: DevContainerConfiguration;
|
|
310
|
+
if (options.devcontainerFile) {
|
|
311
|
+
config = await this.devContainerFileService.getConfiguration(options.devcontainerFile, context);
|
|
312
|
+
config = { ...config, shutdownAction: 'none' };
|
|
313
|
+
} else {
|
|
314
|
+
config = { name: containerInfo.Name.replace(/^\//, ''), shutdownAction: 'none' } as DevContainerConfiguration;
|
|
315
|
+
}
|
|
222
316
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
report
|
|
226
|
-
});
|
|
227
|
-
remote.remoteSetupResult = result;
|
|
317
|
+
let remote: RemoteDockerContainerConnection | undefined;
|
|
318
|
+
try {
|
|
319
|
+
report('Connecting to remote system...');
|
|
228
320
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
remote.forwardOut(socket);
|
|
232
|
-
});
|
|
233
|
-
remote.onDidDisconnect(() => {
|
|
234
|
-
server.close();
|
|
235
|
-
registration.dispose();
|
|
236
|
-
});
|
|
237
|
-
const localPort = (server.address() as net.AddressInfo).port;
|
|
238
|
-
remote.localPort = localPort;
|
|
321
|
+
const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report);
|
|
322
|
+
remote = result.remote;
|
|
239
323
|
|
|
240
|
-
|
|
324
|
+
if (options.devcontainerFile) {
|
|
325
|
+
await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
|
|
326
|
+
}
|
|
241
327
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
328
|
+
return {
|
|
329
|
+
containerId: container.id,
|
|
330
|
+
workspacePath: options.workspacePath,
|
|
331
|
+
port: result.localPort.toString(),
|
|
332
|
+
};
|
|
333
|
+
} catch (e) {
|
|
334
|
+
remote?.dispose();
|
|
335
|
+
this.logger.error(e);
|
|
336
|
+
throw e;
|
|
337
|
+
}
|
|
247
338
|
} catch (e) {
|
|
248
339
|
this.messageService.error(e.message);
|
|
249
|
-
console.error(e);
|
|
250
340
|
throw e;
|
|
251
341
|
} finally {
|
|
252
342
|
progress.cancel();
|
|
253
343
|
}
|
|
254
344
|
}
|
|
255
345
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
m.Destination !== '/tmp/host_gitconfig'
|
|
263
|
-
);
|
|
264
|
-
return (workspaceMount?.Destination ?? containerInfo.Config.WorkingDir) || '/';
|
|
346
|
+
async getCurrentContainerInfo(port: number): Promise<Docker.ContainerInspectInfo | undefined> {
|
|
347
|
+
const connection = this.remoteConnectionService.getConnectionFromPort(port);
|
|
348
|
+
if (!connection || !(connection instanceof RemoteDockerContainerConnection)) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
return connection.container.inspect();
|
|
265
352
|
}
|
|
266
353
|
|
|
267
354
|
async removeContainer(containerId: string): Promise<void> {
|
|
355
|
+
return this.doRemoveContainer(containerId);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
protected async doRemoveContainer(containerId: string, docker?: Docker): Promise<void> {
|
|
359
|
+
docker ??= await this.createDockerConnection();
|
|
360
|
+
const container = docker.getContainer(containerId);
|
|
268
361
|
try {
|
|
269
|
-
const docker = new Docker();
|
|
270
|
-
const container = docker.getContainer(containerId);
|
|
271
362
|
const info = await container.inspect();
|
|
272
363
|
if (info.State.Running) {
|
|
273
364
|
await container.stop();
|
|
274
365
|
}
|
|
275
366
|
await container.remove();
|
|
276
367
|
} catch (e) {
|
|
277
|
-
|
|
368
|
+
this.logger.error('Failed to remove container:', e);
|
|
278
369
|
throw e;
|
|
279
370
|
}
|
|
280
371
|
}
|
|
281
372
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
stdout: WriteStream,
|
|
301
|
-
stderr: WriteStream,
|
|
302
|
-
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
interface ContainerTerminalSession {
|
|
306
|
-
execution: Docker.Exec,
|
|
307
|
-
stdout: WriteStream,
|
|
308
|
-
stderr: WriteStream,
|
|
309
|
-
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
export class RemoteDockerContainerConnection implements RemoteConnection {
|
|
313
|
-
|
|
314
|
-
id: string;
|
|
315
|
-
name: string;
|
|
316
|
-
type: string;
|
|
317
|
-
localPort: number;
|
|
318
|
-
remotePort: number;
|
|
319
|
-
|
|
320
|
-
docker: Docker;
|
|
321
|
-
container: Docker.Container;
|
|
322
|
-
|
|
323
|
-
remoteSetupResult: RemoteSetupResult;
|
|
324
|
-
|
|
325
|
-
protected readonly logger: ILogger;
|
|
326
|
-
|
|
327
|
-
protected config: DevContainerConfiguration;
|
|
328
|
-
|
|
329
|
-
protected activeTerminalSession: ContainerTerminalSession | undefined;
|
|
330
|
-
|
|
331
|
-
protected readonly onDidDisconnectEmitter = new Emitter<void>();
|
|
332
|
-
onDidDisconnect: Event<void> = this.onDidDisconnectEmitter.event;
|
|
333
|
-
|
|
334
|
-
constructor(options: RemoteContainerConnectionOptions) {
|
|
335
|
-
this.id = options.id;
|
|
336
|
-
this.type = options.type;
|
|
337
|
-
this.name = options.name;
|
|
338
|
-
|
|
339
|
-
this.docker = options.docker;
|
|
340
|
-
this.container = options.container;
|
|
341
|
-
|
|
342
|
-
this.config = options.config;
|
|
343
|
-
|
|
344
|
-
this.docker.getEvents({ filters: { container: [this.container.id], event: ['stop'] } }).then(stream => {
|
|
345
|
-
stream.on('data', () => this.onDidDisconnectEmitter.fire());
|
|
373
|
+
/**
|
|
374
|
+
* Creates a remote connection, runs setup (injecting the Theia backend into the container),
|
|
375
|
+
* registers the connection, and starts a local proxy server.
|
|
376
|
+
*
|
|
377
|
+
* @returns the local proxy port and the remote connection
|
|
378
|
+
*/
|
|
379
|
+
protected async setupRemoteConnection(
|
|
380
|
+
container: Docker.Container, docker: Docker, config: DevContainerConfiguration,
|
|
381
|
+
nodeDownloadTemplate: string | undefined, report: RemoteStatusReport
|
|
382
|
+
): Promise<{ localPort: number; remote: RemoteDockerContainerConnection }> {
|
|
383
|
+
const remote = new RemoteDockerContainerConnection({
|
|
384
|
+
id: generateUuid(),
|
|
385
|
+
name: config.name ?? 'dev-container',
|
|
386
|
+
type: 'Dev Container',
|
|
387
|
+
docker,
|
|
388
|
+
container,
|
|
389
|
+
config,
|
|
390
|
+
logger: this.logger
|
|
346
391
|
});
|
|
347
392
|
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
protected getRemoteEnv(): string[] | undefined {
|
|
352
|
-
const remoteEnv = this.config.remoteEnv;
|
|
353
|
-
if (!remoteEnv || Object.keys(remoteEnv).length === 0) {
|
|
354
|
-
return undefined;
|
|
355
|
-
}
|
|
356
|
-
return Object.entries(remoteEnv)
|
|
357
|
-
.filter(([, value]) => value !== undefined)
|
|
358
|
-
.map(([key, value]) => `${key}=${value}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async forwardOut(socket: Socket, port?: number): Promise<void> {
|
|
362
|
-
const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`;
|
|
363
|
-
const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`;
|
|
364
|
-
try {
|
|
365
|
-
const ttySession = await this.container.exec({
|
|
366
|
-
Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`],
|
|
367
|
-
Env: this.getRemoteEnv(),
|
|
368
|
-
AttachStdin: true, AttachStdout: true, AttachStderr: true
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
const stream = await ttySession.start({ hijack: true, stdin: true });
|
|
372
|
-
|
|
373
|
-
socket.pipe(stream);
|
|
374
|
-
ttySession.modem.demuxStream(stream, socket, socket);
|
|
375
|
-
} catch (e) {
|
|
376
|
-
console.error(e);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise<RemoteExecResult> {
|
|
381
|
-
// return (await this.getOrCreateTerminalSession()).executeCommand(cmd, args);
|
|
382
|
-
const deferred = new Deferred<RemoteExecResult>();
|
|
393
|
+
let result;
|
|
383
394
|
try {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
let stdoutBuffer = '';
|
|
389
|
-
let stderrBuffer = '';
|
|
390
|
-
const stream = await execution?.start({});
|
|
391
|
-
const stdout = new PassThrough();
|
|
392
|
-
stdout.on('data', (chunk: Buffer) => {
|
|
393
|
-
stdoutBuffer += chunk.toString();
|
|
394
|
-
});
|
|
395
|
-
const stderr = new PassThrough();
|
|
396
|
-
stderr.on('data', (chunk: Buffer) => {
|
|
397
|
-
stderrBuffer += chunk.toString();
|
|
395
|
+
result = await this.remoteSetup.setup({
|
|
396
|
+
connection: remote,
|
|
397
|
+
report,
|
|
398
|
+
nodeDownloadTemplate
|
|
398
399
|
});
|
|
399
|
-
execution.modem.demuxStream(stream, stdout, stderr);
|
|
400
|
-
stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }));
|
|
401
400
|
} catch (e) {
|
|
402
|
-
|
|
401
|
+
remote.dispose();
|
|
402
|
+
throw e;
|
|
403
403
|
}
|
|
404
|
-
|
|
405
|
-
}
|
|
404
|
+
remote.remoteSetupResult = result;
|
|
406
405
|
|
|
407
|
-
|
|
408
|
-
|
|
406
|
+
let registration: { dispose(): void } | undefined;
|
|
407
|
+
let server: net.Server | undefined;
|
|
409
408
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
});
|
|
414
|
-
let stdoutBuffer = '';
|
|
415
|
-
let stderrBuffer = '';
|
|
416
|
-
const stream = await execution?.start({});
|
|
417
|
-
stream.on('close', () => {
|
|
418
|
-
if (deferred.state === 'unresolved') {
|
|
419
|
-
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
const stdout = new PassThrough();
|
|
423
|
-
stdout.on('data', (data: Buffer) => {
|
|
424
|
-
this.logger.debug('REMOTE STDOUT:', data.toString());
|
|
425
|
-
if (deferred.state === 'unresolved') {
|
|
426
|
-
stdoutBuffer += data.toString();
|
|
427
|
-
|
|
428
|
-
if (tester(stdoutBuffer, stderrBuffer)) {
|
|
429
|
-
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
const stderr = new PassThrough();
|
|
434
|
-
stderr.on('data', (data: Buffer) => {
|
|
435
|
-
this.logger.debug('REMOTE STDERR:', data.toString());
|
|
436
|
-
if (deferred.state === 'unresolved') {
|
|
437
|
-
stderrBuffer += data.toString();
|
|
438
|
-
|
|
439
|
-
if (tester(stdoutBuffer, stderrBuffer)) {
|
|
440
|
-
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
409
|
+
registration = this.remoteConnectionService.register(remote);
|
|
410
|
+
server = await this.serverProvider.getProxyServer(socket => {
|
|
411
|
+
remote.forwardOut(socket);
|
|
443
412
|
});
|
|
444
|
-
execution.modem.demuxStream(stream, stdout, stderr);
|
|
445
413
|
} catch (e) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
getDockerHost(): string {
|
|
452
|
-
const dockerHost = process.env.DOCKER_HOST;
|
|
453
|
-
let remoteHost = '';
|
|
454
|
-
try {
|
|
455
|
-
if (dockerHost) {
|
|
456
|
-
const dockerHostURL = new URL(dockerHost);
|
|
457
|
-
if (dockerHostURL.protocol === 'http:' || dockerHostURL.protocol === 'https:') {
|
|
458
|
-
dockerHostURL.protocol = 'tcp:';
|
|
459
|
-
}
|
|
460
|
-
remoteHost = `-H ${dockerHostURL.href} `;
|
|
461
|
-
}
|
|
462
|
-
} catch (e) {
|
|
463
|
-
console.error(e);
|
|
414
|
+
server?.close();
|
|
415
|
+
registration?.dispose();
|
|
416
|
+
remote.dispose();
|
|
417
|
+
throw e;
|
|
464
418
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
async copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise<void> {
|
|
470
|
-
const deferred = new Deferred<void>();
|
|
471
|
-
const remoteHost = this.getDockerHost();
|
|
472
|
-
|
|
473
|
-
const subprocess = exec(`docker ${remoteHost}cp -a ${localPath.toString()} ${this.container.id}:${remotePath}`);
|
|
474
|
-
|
|
475
|
-
let stderr = '';
|
|
476
|
-
subprocess.stderr?.on('data', data => {
|
|
477
|
-
stderr += data.toString();
|
|
478
|
-
});
|
|
479
|
-
subprocess.on('close', code => {
|
|
480
|
-
if (code === 0) {
|
|
481
|
-
deferred.resolve();
|
|
482
|
-
} else {
|
|
483
|
-
deferred.reject(stderr);
|
|
484
|
-
}
|
|
419
|
+
remote.onDidDisconnect(() => {
|
|
420
|
+
server.close();
|
|
421
|
+
registration.dispose();
|
|
485
422
|
});
|
|
486
|
-
return deferred.promise;
|
|
487
|
-
}
|
|
488
423
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
424
|
+
const localPort = (server.address() as net.AddressInfo).port;
|
|
425
|
+
remote.localPort = localPort;
|
|
426
|
+
return { localPort, remote };
|
|
492
427
|
}
|
|
493
428
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
protected async shutdownContainer(sync: boolean): Promise<unknown> {
|
|
499
|
-
const remoteHost = this.getDockerHost();
|
|
500
|
-
|
|
501
|
-
const shutdownAction = this.config.shutdownAction ?? (this.config.dockerComposeFile ? 'stopCompose' : 'stopContainer');
|
|
502
|
-
|
|
503
|
-
if (shutdownAction === 'stopContainer') {
|
|
504
|
-
return sync ? execSync(`docker ${remoteHost}stop ${this.container.id}`) : this.container.stop();
|
|
505
|
-
} else if (shutdownAction === 'stopCompose') {
|
|
506
|
-
if (!this.config.dockerComposeFile) {
|
|
507
|
-
console.warn('shutdownAction is stopCompose but dockerComposeFile is not defined, falling back to stopContainer');
|
|
508
|
-
return sync ? execSync(`docker ${remoteHost}stop ${this.container.id}`) : this.container.stop();
|
|
509
|
-
}
|
|
510
|
-
const composeFilePath = resolveComposeFilePath(this.config);
|
|
511
|
-
return sync ? execSync(`docker ${remoteHost}compose -f ${composeFilePath} stop`) :
|
|
512
|
-
new Promise<void>((res, rej) => exec(`docker ${remoteHost}compose -f ${composeFilePath} stop`, err => {
|
|
513
|
-
if (err) {
|
|
514
|
-
console.error(err);
|
|
515
|
-
rej(err);
|
|
516
|
-
} else {
|
|
517
|
-
res();
|
|
518
|
-
}
|
|
519
|
-
}));
|
|
520
|
-
}
|
|
521
|
-
|
|
429
|
+
dispose(): void {
|
|
430
|
+
this.outputProvider = undefined;
|
|
522
431
|
}
|
|
523
|
-
|
|
524
432
|
}
|