@theia/dev-container 1.72.0-next.59 → 1.73.0-next.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.
Files changed (90) hide show
  1. package/lib/electron-browser/container-connection-contribution.d.ts +5 -2
  2. package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
  3. package/lib/electron-browser/container-connection-contribution.js +130 -30
  4. package/lib/electron-browser/container-connection-contribution.js.map +1 -1
  5. package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
  6. package/lib/electron-browser/container-output-provider.js +3 -1
  7. package/lib/electron-browser/container-output-provider.js.map +1 -1
  8. package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
  9. package/lib/electron-browser/dev-container-frontend-module.js +5 -0
  10. package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
  11. package/lib/electron-browser/dev-container-startup-contribution.d.ts +15 -0
  12. package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -0
  13. package/lib/electron-browser/dev-container-startup-contribution.js +94 -0
  14. package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -0
  15. package/lib/electron-common/dev-container-preferences.d.ts +12 -0
  16. package/lib/electron-common/dev-container-preferences.d.ts.map +1 -0
  17. package/lib/electron-common/dev-container-preferences.js +44 -0
  18. package/lib/electron-common/dev-container-preferences.js.map +1 -0
  19. package/lib/electron-common/remote-container-connection-provider.d.ts +20 -1
  20. package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
  21. package/lib/electron-node/dev-container-backend-module.d.ts.map +1 -1
  22. package/lib/electron-node/dev-container-backend-module.js +4 -0
  23. package/lib/electron-node/dev-container-backend-module.js.map +1 -1
  24. package/lib/electron-node/dev-container-cli-contribution.d.ts +19 -0
  25. package/lib/electron-node/dev-container-cli-contribution.d.ts.map +1 -0
  26. package/lib/electron-node/dev-container-cli-contribution.js +66 -0
  27. package/lib/electron-node/dev-container-cli-contribution.js.map +1 -0
  28. package/lib/electron-node/dev-container-cli-contribution.spec.d.ts +2 -0
  29. package/lib/electron-node/dev-container-cli-contribution.spec.d.ts.map +1 -0
  30. package/lib/electron-node/dev-container-cli-contribution.spec.js +91 -0
  31. package/lib/electron-node/dev-container-cli-contribution.spec.js.map +1 -0
  32. package/lib/electron-node/dev-container-file-service.d.ts +4 -4
  33. package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
  34. package/lib/electron-node/dev-container-file-service.js +9 -9
  35. package/lib/electron-node/dev-container-file-service.js.map +1 -1
  36. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +6 -2
  37. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts.map +1 -1
  38. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js +24 -4
  39. package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js.map +1 -1
  40. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts +7 -6
  41. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts.map +1 -1
  42. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +4 -9
  43. package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
  44. package/lib/electron-node/devcontainer-util.d.ts +19 -0
  45. package/lib/electron-node/devcontainer-util.d.ts.map +1 -0
  46. package/lib/electron-node/devcontainer-util.js +48 -0
  47. package/lib/electron-node/devcontainer-util.js.map +1 -0
  48. package/lib/electron-node/devcontainer-util.spec.d.ts +2 -0
  49. package/lib/electron-node/devcontainer-util.spec.d.ts.map +1 -0
  50. package/lib/electron-node/devcontainer-util.spec.js +128 -0
  51. package/lib/electron-node/devcontainer-util.spec.js.map +1 -0
  52. package/lib/electron-node/docker-container-service.d.ts +3 -3
  53. package/lib/electron-node/docker-container-service.d.ts.map +1 -1
  54. package/lib/electron-node/docker-container-service.js +3 -4
  55. package/lib/electron-node/docker-container-service.js.map +1 -1
  56. package/lib/electron-node/remote-container-connection-provider.d.ts +27 -66
  57. package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
  58. package/lib/electron-node/remote-container-connection-provider.js +269 -311
  59. package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
  60. package/lib/electron-node/remote-docker-container-connection.d.ts +50 -0
  61. package/lib/electron-node/remote-docker-container-connection.d.ts.map +1 -0
  62. package/lib/electron-node/remote-docker-container-connection.js +239 -0
  63. package/lib/electron-node/remote-docker-container-connection.js.map +1 -0
  64. package/lib/electron-node/remote-docker-container-connection.spec.d.ts +2 -0
  65. package/lib/electron-node/remote-docker-container-connection.spec.d.ts.map +1 -0
  66. package/lib/electron-node/remote-docker-container-connection.spec.js +217 -0
  67. package/lib/electron-node/remote-docker-container-connection.spec.js.map +1 -0
  68. package/package.json +7 -7
  69. package/src/electron-browser/container-connection-contribution.ts +155 -38
  70. package/src/electron-browser/container-output-provider.ts +3 -1
  71. package/src/electron-browser/dev-container-frontend-module.ts +6 -0
  72. package/src/electron-browser/dev-container-startup-contribution.ts +99 -0
  73. package/src/electron-common/dev-container-preferences.ts +53 -0
  74. package/src/electron-common/remote-container-connection-provider.ts +23 -1
  75. package/src/electron-node/dev-container-backend-module.ts +5 -0
  76. package/src/electron-node/dev-container-cli-contribution.spec.ts +106 -0
  77. package/src/electron-node/dev-container-cli-contribution.ts +68 -0
  78. package/src/electron-node/dev-container-file-service.ts +10 -10
  79. package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +29 -5
  80. package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +11 -11
  81. package/src/electron-node/devcontainer-util.spec.ts +154 -0
  82. package/src/electron-node/devcontainer-util.ts +49 -0
  83. package/src/electron-node/docker-container-service.ts +6 -7
  84. package/src/electron-node/remote-container-connection-provider.ts +274 -366
  85. package/src/electron-node/{remote-container-connection-provider.spec.ts → remote-docker-container-connection.spec.ts} +105 -4
  86. package/src/electron-node/remote-docker-container-connection.ts +290 -0
  87. package/lib/electron-node/remote-container-connection-provider.spec.d.ts +0 -2
  88. package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +0 -1
  89. package/lib/electron-node/remote-container-connection-provider.spec.js +0 -131
  90. package/lib/electron-node/remote-container-connection-provider.spec.js.map +0 -1
@@ -15,26 +15,28 @@
15
15
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
16
  // *****************************************************************************
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.RemoteDockerContainerConnection = exports.DevContainerConnectionProvider = void 0;
18
+ exports.DevContainerConnectionProvider = exports.RemoteDockerContainerConnection = void 0;
19
19
  const tslib_1 = require("tslib");
20
- const remote_setup_service_1 = require("@theia/remote/lib/electron-node/setup/remote-setup-service");
20
+ const stream_1 = require("stream");
21
21
  const remote_connection_service_1 = require("@theia/remote/lib/electron-node/remote-connection-service");
22
+ const remote_setup_service_1 = require("@theia/remote/lib/electron-node/setup/remote-setup-service");
22
23
  const remote_proxy_server_provider_1 = require("@theia/remote/lib/electron-node/remote-proxy-server-provider");
23
24
  const core_1 = require("@theia/core");
24
25
  const inversify_1 = require("@theia/core/shared/inversify");
25
26
  const Docker = require("dockerode");
26
- const docker_container_service_1 = require("./docker-container-service");
27
- const promise_util_1 = require("@theia/core/lib/common/promise-util");
28
- const stream_1 = require("stream");
29
- const child_process_1 = require("child_process");
30
27
  const dev_container_file_service_1 = require("./dev-container-file-service");
31
- const devcontainer_file_1 = require("./devcontainer-file");
32
- const compose_service_1 = require("./docker-compose/compose-service");
28
+ const docker_container_service_1 = require("./docker-container-service");
29
+ const dev_container_cli_contribution_1 = require("./dev-container-cli-contribution");
30
+ const remote_docker_container_connection_1 = require("./remote-docker-container-connection");
31
+ // Re-export for backward compatibility — these types were moved to remote-docker-container-connection.ts
32
+ var remote_docker_container_connection_2 = require("./remote-docker-container-connection");
33
+ Object.defineProperty(exports, "RemoteDockerContainerConnection", { enumerable: true, get: function () { return remote_docker_container_connection_2.RemoteDockerContainerConnection; } });
34
+ const devcontainer_util_1 = require("./devcontainer-util");
33
35
  let DevContainerConnectionProvider = class DevContainerConnectionProvider {
34
36
  setClient(client) {
35
37
  this.outputProvider = client;
36
38
  }
37
- async connectToContainer(options) {
39
+ async createDockerConnection() {
38
40
  const dockerOptions = {};
39
41
  const dockerHost = process.env.DOCKER_HOST;
40
42
  try {
@@ -63,55 +65,162 @@ let DevContainerConnectionProvider = class DevContainerConnectionProvider {
63
65
  }
64
66
  catch (_) {
65
67
  this.logger.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
66
- this.messageService.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
67
68
  }
68
69
  const dockerConnection = new Docker(dockerOptions);
69
- const version = await dockerConnection.version()
70
+ await dockerConnection.version()
70
71
  .catch(e => {
71
- console.error('Docker Error:', e);
72
- this.messageService.error('Docker Error: ' + e.message);
72
+ this.logger.error('Docker Error:', e);
73
+ throw new Error(`Docker is not available: ${e.message ?? e}`);
73
74
  });
74
- if (!version) {
75
- this.messageService.error('Docker Daemon is not running');
76
- throw new Error('Docker is not running');
75
+ return dockerConnection;
76
+ }
77
+ async listRunningContainers(docker) {
78
+ docker ??= await this.createDockerConnection();
79
+ const containers = await docker.listContainers({ all: false });
80
+ return containers.map(container => ({
81
+ id: container.Id.substring(0, 12),
82
+ name: (container.Names[0] || '').replace(/^\//, ''),
83
+ image: container.Image,
84
+ status: container.Status,
85
+ created: container.Created
86
+ }));
87
+ }
88
+ async getWorkspaceCandidates(containerId, docker) {
89
+ docker ??= await this.createDockerConnection();
90
+ const container = docker.getContainer(containerId);
91
+ const info = await container.inspect();
92
+ const candidates = [];
93
+ const seen = new Set();
94
+ // Check for devcontainer metadata label (set by tools that created the container)
95
+ const metadataLabel = info.Config.Labels?.['devcontainer.metadata'];
96
+ if (metadataLabel) {
97
+ try {
98
+ const metadata = JSON.parse(metadataLabel);
99
+ if (Array.isArray(metadata)) {
100
+ for (const entry of metadata) {
101
+ if (entry.remoteWorkspaceFolder && !seen.has(entry.remoteWorkspaceFolder)) {
102
+ seen.add(entry.remoteWorkspaceFolder);
103
+ candidates.push({ path: entry.remoteWorkspaceFolder, source: 'devcontainer-label' });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ catch {
109
+ // ignore malformed metadata
110
+ }
77
111
  }
78
- // create container
79
- const progress = await this.messageService.showProgress({
80
- text: 'Creating container',
81
- });
112
+ const localFolderLabel = info.Config.Labels?.['devcontainer.local_folder'];
113
+ if (localFolderLabel) {
114
+ const basename = core_1.URI.fromFilePath(localFolderLabel).path.base;
115
+ if (basename) {
116
+ const workspacePath = `/workspaces/${basename}`;
117
+ if (!seen.has(workspacePath)) {
118
+ seen.add(workspacePath);
119
+ candidates.push({ path: workspacePath, source: 'devcontainer-label' });
120
+ }
121
+ }
122
+ }
123
+ if (info.Config.WorkingDir && info.Config.WorkingDir !== '/' && !seen.has(info.Config.WorkingDir)) {
124
+ seen.add(info.Config.WorkingDir);
125
+ candidates.push({ path: info.Config.WorkingDir, source: 'working-dir' });
126
+ }
127
+ for (const mount of (0, devcontainer_util_1.getWorkspaceMounts)(info.Mounts ?? [])) {
128
+ if (!seen.has(mount.Destination)) {
129
+ seen.add(mount.Destination);
130
+ candidates.push({ path: mount.Destination, source: 'bind-mount' });
131
+ }
132
+ }
133
+ if (!seen.has('/')) {
134
+ candidates.push({ path: '/', source: 'fallback' });
135
+ }
136
+ return candidates;
137
+ }
138
+ async scanForDevContainerConfig(containerId, workspacePath, docker) {
139
+ docker ??= await this.createDockerConnection();
140
+ const container = docker.getContainer(containerId);
141
+ // Search all three standard devcontainer.json locations:
142
+ // <workspace>/.devcontainer/devcontainer.json
143
+ // <workspace>/.devcontainer.json
144
+ // <workspace>/.devcontainer/<subfolder>/devcontainer.json
145
+ // Uses a single find command to avoid multiple exec round-trips.
82
146
  try {
83
- const container = await this.containerService.getOrCreateContainer(dockerConnection, options, this.outputProvider);
84
- const devContainerConfig = await this.devContainerFileService.getConfiguration(options.devcontainerFile);
85
- // create actual connection
86
- const report = message => progress.report({ message });
87
- report('Connecting to remote system...');
88
- const remote = await this.createContainerConnection(container, dockerConnection, devContainerConfig);
89
- const result = await this.remoteSetup.setup({
90
- connection: remote,
91
- report,
92
- nodeDownloadTemplate: options.nodeDownloadTemplate
147
+ // Use Cmd array form instead of sh -c to avoid shell injection.
148
+ // stderr (e.g. "No such file or directory" when .devcontainer doesn't exist)
149
+ // is demuxed to a separate stream and discarded.
150
+ const execution = await container.exec({
151
+ Cmd: [
152
+ 'find',
153
+ `${workspacePath}/.devcontainer`, `${workspacePath}/.devcontainer.json`,
154
+ '-maxdepth', '2',
155
+ '(', '-name', 'devcontainer.json', '-o', '-name', '.devcontainer.json', ')',
156
+ '-type', 'f'
157
+ ],
158
+ AttachStdout: true,
159
+ AttachStderr: true
93
160
  });
94
- remote.remoteSetupResult = result;
95
- const registration = this.remoteConnectionService.register(remote);
96
- const server = await this.serverProvider.getProxyServer(socket => {
97
- remote.forwardOut(socket);
161
+ let stdout = '';
162
+ const stream = await execution.start({});
163
+ const stdoutPassthrough = new stream_1.PassThrough();
164
+ const stderrPassthrough = new stream_1.PassThrough();
165
+ stdoutPassthrough.on('data', (chunk) => {
166
+ stdout += chunk.toString();
98
167
  });
99
- remote.onDidDisconnect(() => {
100
- server.close();
101
- registration.dispose();
168
+ execution.modem.demuxStream(stream, stdoutPassthrough, stderrPassthrough);
169
+ await new Promise((resolve, reject) => {
170
+ stream.on('end', () => resolve());
171
+ stream.on('error', reject);
102
172
  });
103
- const localPort = server.address().port;
104
- remote.localPort = localPort;
105
- await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider);
106
- return {
107
- containerId: container.id,
108
- workspacePath: devContainerConfig.workspaceFolder ?? this.inferWorkspacePath(await container.inspect()),
109
- port: localPort.toString(),
110
- };
173
+ stdoutPassthrough.destroy();
174
+ stderrPassthrough.destroy();
175
+ const found = stdout.trim().split('\n').filter(line => line.length > 0);
176
+ if (found.length === 0) {
177
+ return undefined;
178
+ }
179
+ // Prefer the standard location, then root-level, then subfolder configs
180
+ const standardPath = `${workspacePath}/.devcontainer/devcontainer.json`;
181
+ const rootPath = `${workspacePath}/.devcontainer.json`;
182
+ for (const preferred of [standardPath, rootPath]) {
183
+ if (found.includes(preferred)) {
184
+ return preferred;
185
+ }
186
+ }
187
+ // Return the first subfolder config found
188
+ return found[0];
189
+ }
190
+ catch (e) {
191
+ // find/sh might not be available in minimal containers
192
+ this.logger.debug('Failed to scan for devcontainer.json in container:', e);
193
+ return undefined;
194
+ }
195
+ }
196
+ async connectToContainer(options) {
197
+ const progress = await this.messageService.showProgress({ text: 'Creating container' });
198
+ try {
199
+ const report = message => progress.report({ message });
200
+ const docker = await this.createDockerConnection();
201
+ let remote;
202
+ try {
203
+ const container = await this.containerService.getOrCreateContainer(docker, options, this.outputProvider);
204
+ const context = { containerId: container.id };
205
+ const devContainerConfig = await this.devContainerFileService.getConfiguration(options.devcontainerFile, context);
206
+ report('Connecting to remote system...');
207
+ const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report);
208
+ remote = result.remote;
209
+ await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
210
+ return {
211
+ containerId: container.id,
212
+ workspacePath: devContainerConfig.workspaceFolder ?? (0, devcontainer_util_1.inferWorkspacePath)(await container.inspect()),
213
+ port: result.localPort.toString(),
214
+ };
215
+ }
216
+ catch (e) {
217
+ remote?.dispose();
218
+ this.logger.error(e);
219
+ throw e;
220
+ }
111
221
  }
112
222
  catch (e) {
113
223
  this.messageService.error(e.message);
114
- console.error(e);
115
224
  throw e;
116
225
  }
117
226
  finally {
@@ -121,102 +230,77 @@ let DevContainerConnectionProvider = class DevContainerConnectionProvider {
121
230
  getDevContainerFiles(workspacePath) {
122
231
  return this.devContainerFileService.getAvailableFiles(workspacePath);
123
232
  }
124
- async createContainerConnection(container, docker, config) {
125
- return Promise.resolve(new RemoteDockerContainerConnection({
126
- id: (0, core_1.generateUuid)(),
127
- name: config.name ?? 'dev-container',
128
- type: 'Dev Container',
129
- docker,
130
- container,
131
- config,
132
- logger: this.logger
133
- }));
134
- }
135
- async getCurrentContainerInfo(port) {
136
- const connection = this.remoteConnectionService.getConnectionFromPort(port);
137
- if (!connection || !(connection instanceof RemoteDockerContainerConnection)) {
233
+ async getAttachContainerArgs() {
234
+ const containerId = this.cliContribution.consumeAttachContainerId();
235
+ if (!containerId) {
138
236
  return undefined;
139
237
  }
140
- return connection.container.inspect();
141
- }
142
- async listRunningContainers() {
143
- try {
144
- const docker = new Docker();
145
- const containers = await docker.listContainers({ all: false });
146
- return containers.map(container => ({
147
- id: container.Id,
148
- name: (container.Names[0] ?? '').replace(/^\//, ''),
149
- image: container.Image,
150
- status: container.Status
151
- }));
152
- }
153
- catch (e) {
154
- console.error('Failed to list running containers:', e);
155
- return [];
156
- }
238
+ return {
239
+ containerId,
240
+ scanForDevJson: this.cliContribution.shouldScanForDevJson()
241
+ };
157
242
  }
158
- async attachToContainer(containerId) {
159
- const docker = new Docker();
160
- const container = docker.getContainer(containerId);
161
- const containerInfo = await container.inspect();
162
- const progress = await this.messageService.showProgress({
163
- text: 'Attaching to container',
164
- });
243
+ async attachToContainer(options) {
244
+ const progress = await this.messageService.showProgress({ text: 'Attaching to container' });
165
245
  try {
166
246
  const report = message => progress.report({ message });
167
- report('Connecting to remote system...');
168
- const remote = new RemoteDockerContainerConnection({
169
- id: (0, core_1.generateUuid)(),
170
- name: containerInfo.Name.replace(/^\//, ''),
171
- type: 'Dev Container',
172
- docker,
173
- container,
174
- config: devcontainer_file_1.DevContainerConfiguration.empty(),
175
- logger: this.logger
176
- });
177
- const result = await this.remoteSetup.setup({
178
- connection: remote,
179
- report,
180
- });
181
- remote.remoteSetupResult = result;
182
- const registration = this.remoteConnectionService.register(remote);
183
- const server = await this.serverProvider.getProxyServer(socket => {
184
- remote.forwardOut(socket);
185
- });
186
- remote.onDidDisconnect(() => {
187
- server.close();
188
- registration.dispose();
189
- });
190
- const localPort = server.address().port;
191
- remote.localPort = localPort;
192
- const workspacePath = this.inferWorkspacePath(containerInfo);
193
- return {
194
- containerId: container.id,
195
- workspacePath,
196
- port: localPort.toString(),
197
- };
247
+ const docker = await this.createDockerConnection();
248
+ const container = docker.getContainer(options.containerId);
249
+ const containerInfo = await container.inspect();
250
+ if (!containerInfo.State.Running) {
251
+ throw new Error(`Container ${options.containerId} is not running`);
252
+ }
253
+ const context = { containerId: options.containerId };
254
+ let config;
255
+ if (options.devcontainerFile) {
256
+ config = await this.devContainerFileService.getConfiguration(options.devcontainerFile, context);
257
+ config = { ...config, shutdownAction: 'none' };
258
+ }
259
+ else {
260
+ config = { name: containerInfo.Name.replace(/^\//, ''), shutdownAction: 'none' };
261
+ }
262
+ let remote;
263
+ try {
264
+ report('Connecting to remote system...');
265
+ const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report);
266
+ remote = result.remote;
267
+ if (options.devcontainerFile) {
268
+ await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
269
+ }
270
+ return {
271
+ containerId: container.id,
272
+ workspacePath: options.workspacePath,
273
+ port: result.localPort.toString(),
274
+ };
275
+ }
276
+ catch (e) {
277
+ remote?.dispose();
278
+ this.logger.error(e);
279
+ throw e;
280
+ }
198
281
  }
199
282
  catch (e) {
200
283
  this.messageService.error(e.message);
201
- console.error(e);
202
284
  throw e;
203
285
  }
204
286
  finally {
205
287
  progress.cancel();
206
288
  }
207
289
  }
208
- inferWorkspacePath(containerInfo) {
209
- // Skip mounts that are injected by HostConfigSharingContribution
210
- // (SSH dir, gitconfig) these are not workspace mounts.
211
- const workspaceMount = containerInfo.Mounts.find(m => !m.Destination.endsWith('/.ssh') &&
212
- !m.Destination.endsWith('/.gitconfig') &&
213
- m.Destination !== '/tmp/host_gitconfig');
214
- return (workspaceMount?.Destination ?? containerInfo.Config.WorkingDir) || '/';
290
+ async getCurrentContainerInfo(port) {
291
+ const connection = this.remoteConnectionService.getConnectionFromPort(port);
292
+ if (!connection || !(connection instanceof remote_docker_container_connection_1.RemoteDockerContainerConnection)) {
293
+ return undefined;
294
+ }
295
+ return connection.container.inspect();
215
296
  }
216
297
  async removeContainer(containerId) {
298
+ return this.doRemoveContainer(containerId);
299
+ }
300
+ async doRemoveContainer(containerId, docker) {
301
+ docker ??= await this.createDockerConnection();
302
+ const container = docker.getContainer(containerId);
217
303
  try {
218
- const docker = new Docker();
219
- const container = docker.getContainer(containerId);
220
304
  const info = await container.inspect();
221
305
  if (info.State.Running) {
222
306
  await container.stop();
@@ -224,11 +308,63 @@ let DevContainerConnectionProvider = class DevContainerConnectionProvider {
224
308
  await container.remove();
225
309
  }
226
310
  catch (e) {
227
- console.error('Failed to remove container:', e);
311
+ this.logger.error('Failed to remove container:', e);
228
312
  throw e;
229
313
  }
230
314
  }
315
+ /**
316
+ * Creates a remote connection, runs setup (injecting the Theia backend into the container),
317
+ * registers the connection, and starts a local proxy server.
318
+ *
319
+ * @returns the local proxy port and the remote connection
320
+ */
321
+ async setupRemoteConnection(container, docker, config, nodeDownloadTemplate, report) {
322
+ const remote = new remote_docker_container_connection_1.RemoteDockerContainerConnection({
323
+ id: (0, core_1.generateUuid)(),
324
+ name: config.name ?? 'dev-container',
325
+ type: 'Dev Container',
326
+ docker,
327
+ container,
328
+ config,
329
+ logger: this.logger
330
+ });
331
+ let result;
332
+ try {
333
+ result = await this.remoteSetup.setup({
334
+ connection: remote,
335
+ report,
336
+ nodeDownloadTemplate
337
+ });
338
+ }
339
+ catch (e) {
340
+ remote.dispose();
341
+ throw e;
342
+ }
343
+ remote.remoteSetupResult = result;
344
+ let registration;
345
+ let server;
346
+ try {
347
+ registration = this.remoteConnectionService.register(remote);
348
+ server = await this.serverProvider.getProxyServer(socket => {
349
+ remote.forwardOut(socket);
350
+ });
351
+ }
352
+ catch (e) {
353
+ server?.close();
354
+ registration?.dispose();
355
+ remote.dispose();
356
+ throw e;
357
+ }
358
+ remote.onDidDisconnect(() => {
359
+ server.close();
360
+ registration.dispose();
361
+ });
362
+ const localPort = server.address().port;
363
+ remote.localPort = localPort;
364
+ return { localPort, remote };
365
+ }
231
366
  dispose() {
367
+ this.outputProvider = undefined;
232
368
  }
233
369
  };
234
370
  exports.DevContainerConnectionProvider = DevContainerConnectionProvider;
@@ -240,10 +376,6 @@ tslib_1.__decorate([
240
376
  (0, inversify_1.inject)(remote_setup_service_1.RemoteSetupService),
241
377
  tslib_1.__metadata("design:type", remote_setup_service_1.RemoteSetupService)
242
378
  ], DevContainerConnectionProvider.prototype, "remoteSetup", void 0);
243
- tslib_1.__decorate([
244
- (0, inversify_1.inject)(core_1.MessageService),
245
- tslib_1.__metadata("design:type", core_1.MessageService)
246
- ], DevContainerConnectionProvider.prototype, "messageService", void 0);
247
379
  tslib_1.__decorate([
248
380
  (0, inversify_1.inject)(remote_proxy_server_provider_1.RemoteProxyServerProvider),
249
381
  tslib_1.__metadata("design:type", remote_proxy_server_provider_1.RemoteProxyServerProvider)
@@ -257,192 +389,18 @@ tslib_1.__decorate([
257
389
  tslib_1.__metadata("design:type", dev_container_file_service_1.DevContainerFileService)
258
390
  ], DevContainerConnectionProvider.prototype, "devContainerFileService", void 0);
259
391
  tslib_1.__decorate([
260
- (0, inversify_1.inject)(remote_connection_service_1.RemoteConnectionService),
261
- tslib_1.__metadata("design:type", remote_connection_service_1.RemoteConnectionService)
262
- ], DevContainerConnectionProvider.prototype, "remoteService", void 0);
392
+ (0, inversify_1.inject)(dev_container_cli_contribution_1.DevContainerCliContribution),
393
+ tslib_1.__metadata("design:type", dev_container_cli_contribution_1.DevContainerCliContribution)
394
+ ], DevContainerConnectionProvider.prototype, "cliContribution", void 0);
263
395
  tslib_1.__decorate([
264
396
  (0, inversify_1.inject)(core_1.ILogger),
265
397
  tslib_1.__metadata("design:type", Object)
266
398
  ], DevContainerConnectionProvider.prototype, "logger", void 0);
399
+ tslib_1.__decorate([
400
+ (0, inversify_1.inject)(core_1.MessageService),
401
+ tslib_1.__metadata("design:type", core_1.MessageService)
402
+ ], DevContainerConnectionProvider.prototype, "messageService", void 0);
267
403
  exports.DevContainerConnectionProvider = DevContainerConnectionProvider = tslib_1.__decorate([
268
404
  (0, inversify_1.injectable)()
269
405
  ], DevContainerConnectionProvider);
270
- class RemoteDockerContainerConnection {
271
- constructor(options) {
272
- this.onDidDisconnectEmitter = new core_1.Emitter();
273
- this.onDidDisconnect = this.onDidDisconnectEmitter.event;
274
- this.id = options.id;
275
- this.type = options.type;
276
- this.name = options.name;
277
- this.docker = options.docker;
278
- this.container = options.container;
279
- this.config = options.config;
280
- this.docker.getEvents({ filters: { container: [this.container.id], event: ['stop'] } }).then(stream => {
281
- stream.on('data', () => this.onDidDisconnectEmitter.fire());
282
- });
283
- this.logger = options.logger;
284
- }
285
- getRemoteEnv() {
286
- const remoteEnv = this.config.remoteEnv;
287
- if (!remoteEnv || Object.keys(remoteEnv).length === 0) {
288
- return undefined;
289
- }
290
- return Object.entries(remoteEnv)
291
- .filter(([, value]) => value !== undefined)
292
- .map(([key, value]) => `${key}=${value}`);
293
- }
294
- async forwardOut(socket, port) {
295
- const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`;
296
- const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`;
297
- try {
298
- const ttySession = await this.container.exec({
299
- Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`],
300
- Env: this.getRemoteEnv(),
301
- AttachStdin: true, AttachStdout: true, AttachStderr: true
302
- });
303
- const stream = await ttySession.start({ hijack: true, stdin: true });
304
- socket.pipe(stream);
305
- ttySession.modem.demuxStream(stream, socket, socket);
306
- }
307
- catch (e) {
308
- console.error(e);
309
- }
310
- }
311
- async exec(cmd, args, options) {
312
- // return (await this.getOrCreateTerminalSession()).executeCommand(cmd, args);
313
- const deferred = new promise_util_1.Deferred();
314
- try {
315
- // TODO add windows container support
316
- const execution = await this.container.exec({
317
- Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], Env: this.getRemoteEnv(), AttachStdout: true, AttachStderr: true
318
- });
319
- let stdoutBuffer = '';
320
- let stderrBuffer = '';
321
- const stream = await execution?.start({});
322
- const stdout = new stream_1.PassThrough();
323
- stdout.on('data', (chunk) => {
324
- stdoutBuffer += chunk.toString();
325
- });
326
- const stderr = new stream_1.PassThrough();
327
- stderr.on('data', (chunk) => {
328
- stderrBuffer += chunk.toString();
329
- });
330
- execution.modem.demuxStream(stream, stdout, stderr);
331
- stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }));
332
- }
333
- catch (e) {
334
- deferred.reject(e);
335
- }
336
- return deferred.promise;
337
- }
338
- async execPartial(cmd, tester, args, options) {
339
- const deferred = new promise_util_1.Deferred();
340
- try {
341
- // TODO add windows container support
342
- const execution = await this.container.exec({
343
- Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], Env: this.getRemoteEnv(), AttachStdout: true, AttachStderr: true
344
- });
345
- let stdoutBuffer = '';
346
- let stderrBuffer = '';
347
- const stream = await execution?.start({});
348
- stream.on('close', () => {
349
- if (deferred.state === 'unresolved') {
350
- deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
351
- }
352
- });
353
- const stdout = new stream_1.PassThrough();
354
- stdout.on('data', (data) => {
355
- this.logger.debug('REMOTE STDOUT:', data.toString());
356
- if (deferred.state === 'unresolved') {
357
- stdoutBuffer += data.toString();
358
- if (tester(stdoutBuffer, stderrBuffer)) {
359
- deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
360
- }
361
- }
362
- });
363
- const stderr = new stream_1.PassThrough();
364
- stderr.on('data', (data) => {
365
- this.logger.debug('REMOTE STDERR:', data.toString());
366
- if (deferred.state === 'unresolved') {
367
- stderrBuffer += data.toString();
368
- if (tester(stdoutBuffer, stderrBuffer)) {
369
- deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
370
- }
371
- }
372
- });
373
- execution.modem.demuxStream(stream, stdout, stderr);
374
- }
375
- catch (e) {
376
- deferred.reject(e);
377
- }
378
- return deferred.promise;
379
- }
380
- getDockerHost() {
381
- const dockerHost = process.env.DOCKER_HOST;
382
- let remoteHost = '';
383
- try {
384
- if (dockerHost) {
385
- const dockerHostURL = new URL(dockerHost);
386
- if (dockerHostURL.protocol === 'http:' || dockerHostURL.protocol === 'https:') {
387
- dockerHostURL.protocol = 'tcp:';
388
- }
389
- remoteHost = `-H ${dockerHostURL.href} `;
390
- }
391
- }
392
- catch (e) {
393
- console.error(e);
394
- }
395
- return remoteHost;
396
- }
397
- async copy(localPath, remotePath) {
398
- const deferred = new promise_util_1.Deferred();
399
- const remoteHost = this.getDockerHost();
400
- const subprocess = (0, child_process_1.exec)(`docker ${remoteHost}cp -a ${localPath.toString()} ${this.container.id}:${remotePath}`);
401
- let stderr = '';
402
- subprocess.stderr?.on('data', data => {
403
- stderr += data.toString();
404
- });
405
- subprocess.on('close', code => {
406
- if (code === 0) {
407
- deferred.resolve();
408
- }
409
- else {
410
- deferred.reject(stderr);
411
- }
412
- });
413
- return deferred.promise;
414
- }
415
- disposeSync() {
416
- // cant use dockerrode here since this needs to happen on one tick
417
- this.shutdownContainer(true);
418
- }
419
- async dispose() {
420
- await this.shutdownContainer(false);
421
- }
422
- async shutdownContainer(sync) {
423
- const remoteHost = this.getDockerHost();
424
- const shutdownAction = this.config.shutdownAction ?? (this.config.dockerComposeFile ? 'stopCompose' : 'stopContainer');
425
- if (shutdownAction === 'stopContainer') {
426
- return sync ? (0, child_process_1.execSync)(`docker ${remoteHost}stop ${this.container.id}`) : this.container.stop();
427
- }
428
- else if (shutdownAction === 'stopCompose') {
429
- if (!this.config.dockerComposeFile) {
430
- console.warn('shutdownAction is stopCompose but dockerComposeFile is not defined, falling back to stopContainer');
431
- return sync ? (0, child_process_1.execSync)(`docker ${remoteHost}stop ${this.container.id}`) : this.container.stop();
432
- }
433
- const composeFilePath = (0, compose_service_1.resolveComposeFilePath)(this.config);
434
- return sync ? (0, child_process_1.execSync)(`docker ${remoteHost}compose -f ${composeFilePath} stop`) :
435
- new Promise((res, rej) => (0, child_process_1.exec)(`docker ${remoteHost}compose -f ${composeFilePath} stop`, err => {
436
- if (err) {
437
- console.error(err);
438
- rej(err);
439
- }
440
- else {
441
- res();
442
- }
443
- }));
444
- }
445
- }
446
- }
447
- exports.RemoteDockerContainerConnection = RemoteDockerContainerConnection;
448
406
  //# sourceMappingURL=remote-container-connection-provider.js.map