@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.
- 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,7 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { expect } from 'chai';
|
|
18
|
-
import { RemoteDockerContainerConnection } from './remote-container-connection
|
|
18
|
+
import { RemoteDockerContainerConnection } from './remote-docker-container-connection';
|
|
19
19
|
import { DevContainerConfiguration } from './devcontainer-file';
|
|
20
20
|
import { ILogger } from '@theia/core';
|
|
21
21
|
import * as Docker from 'dockerode';
|
|
@@ -24,11 +24,14 @@ class TestableDockerContainerConnection extends RemoteDockerContainerConnection
|
|
|
24
24
|
public testGetRemoteEnv(): string[] | undefined {
|
|
25
25
|
return this.getRemoteEnv();
|
|
26
26
|
}
|
|
27
|
+
public testBuildShellCommand(cmd: string, args?: string[]): string {
|
|
28
|
+
return this.buildShellCommand(cmd, args);
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
function createConnection(config: DevContainerConfiguration): TestableDockerContainerConnection {
|
|
30
33
|
const mockDocker = {
|
|
31
|
-
getEvents: () => Promise.resolve({ on: () => { } })
|
|
34
|
+
getEvents: () => Promise.resolve({ on: () => { }, destroy: () => { } })
|
|
32
35
|
} as unknown as Docker;
|
|
33
36
|
const mockContainer = {} as unknown as Docker.Container;
|
|
34
37
|
const mockLogger = {} as ILogger;
|
|
@@ -102,8 +105,7 @@ describe('RemoteDockerContainerConnection', () => {
|
|
|
102
105
|
}
|
|
103
106
|
} as DevContainerConfiguration);
|
|
104
107
|
|
|
105
|
-
|
|
106
|
-
expect(env).to.have.lengthOf(0);
|
|
108
|
+
expect(connection.testGetRemoteEnv()).to.be.undefined;
|
|
107
109
|
});
|
|
108
110
|
|
|
109
111
|
it('should handle values containing equals signs', () => {
|
|
@@ -149,4 +151,103 @@ describe('RemoteDockerContainerConnection', () => {
|
|
|
149
151
|
expect(env).to.include('SPACED=hello world');
|
|
150
152
|
});
|
|
151
153
|
});
|
|
154
|
+
|
|
155
|
+
describe('buildShellCommand', () => {
|
|
156
|
+
|
|
157
|
+
it('should return cmd unchanged when no args are provided', () => {
|
|
158
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
159
|
+
expect(connection.testBuildShellCommand('echo')).to.equal('echo');
|
|
160
|
+
expect(connection.testBuildShellCommand('echo', [])).to.equal('echo');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should strong-quote arguments to prevent shell expansion', () => {
|
|
164
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
165
|
+
expect(connection.testBuildShellCommand('echo', ['hello world'])).to.equal("echo 'hello world'");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle arguments with dollar signs', () => {
|
|
169
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
170
|
+
expect(connection.testBuildShellCommand('echo', ['$HOME'])).to.equal("echo '$HOME'");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle arguments with backticks', () => {
|
|
174
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
175
|
+
expect(connection.testBuildShellCommand('echo', ['`whoami`'])).to.equal("echo '`whoami`'");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle arguments with single quotes', () => {
|
|
179
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
180
|
+
// Single quotes inside strong-quoted strings are handled by breaking out and using double-quoted quote
|
|
181
|
+
expect(connection.testBuildShellCommand('echo', ["it's"])).to.equal('echo \'it\'"\'"\'s\'');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle arguments with double quotes', () => {
|
|
185
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
186
|
+
expect(connection.testBuildShellCommand('echo', ['say "hi"'])).to.equal("echo 'say \"hi\"'");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle multiple arguments', () => {
|
|
190
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
191
|
+
expect(connection.testBuildShellCommand('node', ['server.js', '--port=8080'])).to.equal("node 'server.js' '--port=8080'");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle arguments with newlines', () => {
|
|
195
|
+
const connection = createConnection({ image: 'test' } as DevContainerConfiguration);
|
|
196
|
+
const result = connection.testBuildShellCommand('echo', ['line1\nline2']);
|
|
197
|
+
// Literal newline is preserved inside single quotes
|
|
198
|
+
expect(result).to.equal("echo 'line1\nline2'");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('shutdownContainer', () => {
|
|
203
|
+
|
|
204
|
+
function createConnectionWithConfig(config: Partial<DevContainerConfiguration>): { connection: RemoteDockerContainerConnection; stopCalled: () => boolean } {
|
|
205
|
+
const state = { stopCalled: false };
|
|
206
|
+
const mockDocker = {
|
|
207
|
+
getEvents: () => Promise.resolve({ on: () => { }, destroy: () => { } })
|
|
208
|
+
} as unknown as Docker;
|
|
209
|
+
const mockContainer = {
|
|
210
|
+
id: 'test-container-id',
|
|
211
|
+
stop: () => { state.stopCalled = true; return Promise.resolve(); }
|
|
212
|
+
} as unknown as Docker.Container;
|
|
213
|
+
const mockLogger = {} as ILogger;
|
|
214
|
+
const connection = new RemoteDockerContainerConnection({
|
|
215
|
+
id: 'test-id',
|
|
216
|
+
name: 'test',
|
|
217
|
+
type: 'Dev Container',
|
|
218
|
+
docker: mockDocker,
|
|
219
|
+
container: mockContainer,
|
|
220
|
+
config: config as DevContainerConfiguration,
|
|
221
|
+
logger: mockLogger
|
|
222
|
+
});
|
|
223
|
+
return { connection, stopCalled: () => state.stopCalled };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
it('should not stop container when shutdownAction is none', async () => {
|
|
227
|
+
const { connection, stopCalled } = createConnectionWithConfig({ shutdownAction: 'none' });
|
|
228
|
+
await connection.dispose();
|
|
229
|
+
expect(stopCalled()).to.equal(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should stop container when shutdownAction is stopContainer', async () => {
|
|
233
|
+
const { connection, stopCalled } = createConnectionWithConfig({ shutdownAction: 'stopContainer' });
|
|
234
|
+
await connection.dispose();
|
|
235
|
+
expect(stopCalled()).to.equal(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should default to stopContainer when shutdownAction is not set and no dockerComposeFile', async () => {
|
|
239
|
+
const { connection, stopCalled } = createConnectionWithConfig({});
|
|
240
|
+
await connection.dispose();
|
|
241
|
+
expect(stopCalled()).to.equal(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should not stop container when shutdownAction is none even with dockerComposeFile', async () => {
|
|
245
|
+
const { connection, stopCalled } = createConnectionWithConfig({
|
|
246
|
+
shutdownAction: 'none',
|
|
247
|
+
dockerComposeFile: 'docker-compose.yml'
|
|
248
|
+
});
|
|
249
|
+
await connection.dispose();
|
|
250
|
+
expect(stopCalled()).to.equal(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
152
253
|
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester } from '@theia/remote/lib/electron-node/remote-types';
|
|
18
|
+
import { RemoteSetupResult } from '@theia/remote/lib/electron-node/setup/remote-setup-service';
|
|
19
|
+
import { Emitter, Event, ILogger } from '@theia/core';
|
|
20
|
+
import { BashQuotingFunctions, ShellQuoting, createShellCommandLine } from '@theia/core/lib/common/shell-quoting';
|
|
21
|
+
import { Socket } from 'net';
|
|
22
|
+
import * as Docker from 'dockerode';
|
|
23
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
24
|
+
import { PassThrough } from 'stream';
|
|
25
|
+
import { execFile, execFileSync } from 'child_process';
|
|
26
|
+
import { DevContainerConfiguration } from './devcontainer-file';
|
|
27
|
+
import { resolveComposeFilePath } from './docker-compose/compose-service';
|
|
28
|
+
|
|
29
|
+
export interface RemoteContainerConnectionOptions {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
docker: Docker;
|
|
34
|
+
container: Docker.Container;
|
|
35
|
+
config: DevContainerConfiguration;
|
|
36
|
+
logger: ILogger;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class RemoteDockerContainerConnection implements RemoteConnection {
|
|
40
|
+
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
type: string;
|
|
44
|
+
localPort: number;
|
|
45
|
+
remotePort: number;
|
|
46
|
+
|
|
47
|
+
docker: Docker;
|
|
48
|
+
container: Docker.Container;
|
|
49
|
+
|
|
50
|
+
remoteSetupResult!: RemoteSetupResult;
|
|
51
|
+
|
|
52
|
+
protected readonly logger: ILogger;
|
|
53
|
+
|
|
54
|
+
protected config: DevContainerConfiguration;
|
|
55
|
+
|
|
56
|
+
protected dockerEventStream: NodeJS.ReadableStream | undefined;
|
|
57
|
+
|
|
58
|
+
protected readonly onDidDisconnectEmitter = new Emitter<void>();
|
|
59
|
+
onDidDisconnect: Event<void> = this.onDidDisconnectEmitter.event;
|
|
60
|
+
|
|
61
|
+
constructor(options: RemoteContainerConnectionOptions) {
|
|
62
|
+
this.id = options.id;
|
|
63
|
+
this.type = options.type;
|
|
64
|
+
this.name = options.name;
|
|
65
|
+
|
|
66
|
+
this.docker = options.docker;
|
|
67
|
+
this.container = options.container;
|
|
68
|
+
|
|
69
|
+
this.config = options.config;
|
|
70
|
+
this.logger = options.logger;
|
|
71
|
+
|
|
72
|
+
this.docker.getEvents({ filters: { container: [this.container.id], event: ['stop'] } }).then(stream => {
|
|
73
|
+
this.dockerEventStream = stream;
|
|
74
|
+
stream.on('data', () => this.onDidDisconnectEmitter.fire());
|
|
75
|
+
}).catch(e => {
|
|
76
|
+
this.logger.error('Failed to register Docker event listener:', e);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
protected getRemoteEnv(): string[] | undefined {
|
|
81
|
+
const remoteEnv = this.config.remoteEnv;
|
|
82
|
+
if (!remoteEnv || Object.keys(remoteEnv).length === 0) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const entries = Object.entries(remoteEnv)
|
|
86
|
+
.filter(([, value]) => value !== undefined)
|
|
87
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
88
|
+
return entries.length > 0 ? entries : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Builds a shell command string safe for use with `sh -c`.
|
|
93
|
+
* Arguments are strong-quoted (single quotes) using {@link BashQuotingFunctions}
|
|
94
|
+
* so that no shell expansion occurs inside them.
|
|
95
|
+
*
|
|
96
|
+
* **`cmd` is not escaped** and must only contain trusted, internally-constructed
|
|
97
|
+
* values. All untrusted input must be passed via `args`.
|
|
98
|
+
*/
|
|
99
|
+
protected buildShellCommand(cmd: string, args?: string[]): string {
|
|
100
|
+
if (!args || args.length === 0) {
|
|
101
|
+
return cmd;
|
|
102
|
+
}
|
|
103
|
+
return `${cmd} ${createShellCommandLine(args.map(a => ({ value: a, quoting: ShellQuoting.Strong })), BashQuotingFunctions)}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async forwardOut(socket: Socket, port?: number): Promise<void> {
|
|
107
|
+
const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`;
|
|
108
|
+
const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`;
|
|
109
|
+
try {
|
|
110
|
+
const ttySession = await this.container.exec({
|
|
111
|
+
Cmd: ['sh', '-c', this.buildShellCommand(node, [devContainerServer, `-target-port=${port ?? this.remotePort}`])],
|
|
112
|
+
Env: this.getRemoteEnv(),
|
|
113
|
+
AttachStdin: true, AttachStdout: true, AttachStderr: true
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const stream = await ttySession.start({ hijack: true, stdin: true });
|
|
117
|
+
|
|
118
|
+
socket.pipe(stream);
|
|
119
|
+
ttySession.modem.demuxStream(stream, socket, socket);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
this.logger.error('Failed to forward socket:', e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise<RemoteExecResult> {
|
|
126
|
+
const deferred = new Deferred<RemoteExecResult>();
|
|
127
|
+
try {
|
|
128
|
+
// TODO add windows container support
|
|
129
|
+
const execution = await this.container.exec({
|
|
130
|
+
Cmd: ['sh', '-c', this.buildShellCommand(cmd, args)], Env: this.getRemoteEnv(), AttachStdout: true, AttachStderr: true
|
|
131
|
+
});
|
|
132
|
+
let stdoutBuffer = '';
|
|
133
|
+
let stderrBuffer = '';
|
|
134
|
+
const stream = await execution?.start({});
|
|
135
|
+
const stdout = new PassThrough();
|
|
136
|
+
stdout.on('data', (chunk: Buffer) => {
|
|
137
|
+
stdoutBuffer += chunk.toString();
|
|
138
|
+
});
|
|
139
|
+
const stderr = new PassThrough();
|
|
140
|
+
stderr.on('data', (chunk: Buffer) => {
|
|
141
|
+
stderrBuffer += chunk.toString();
|
|
142
|
+
});
|
|
143
|
+
execution.modem.demuxStream(stream, stdout, stderr);
|
|
144
|
+
stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }));
|
|
145
|
+
} catch (e) {
|
|
146
|
+
deferred.reject(e);
|
|
147
|
+
}
|
|
148
|
+
return deferred.promise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise<RemoteExecResult> {
|
|
152
|
+
const deferred = new Deferred<RemoteExecResult>();
|
|
153
|
+
try {
|
|
154
|
+
// TODO add windows container support
|
|
155
|
+
const execution = await this.container.exec({
|
|
156
|
+
Cmd: ['sh', '-c', this.buildShellCommand(cmd, args)], Env: this.getRemoteEnv(), AttachStdout: true, AttachStderr: true
|
|
157
|
+
});
|
|
158
|
+
let stdoutBuffer = '';
|
|
159
|
+
let stderrBuffer = '';
|
|
160
|
+
const stream = await execution?.start({});
|
|
161
|
+
|
|
162
|
+
const cleanupStreams = (): void => {
|
|
163
|
+
stdout.destroy();
|
|
164
|
+
stderr.destroy();
|
|
165
|
+
stream.destroy();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
stream.on('close', () => {
|
|
169
|
+
if (deferred.state === 'unresolved') {
|
|
170
|
+
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const stdout = new PassThrough();
|
|
174
|
+
stdout.on('data', (data: Buffer) => {
|
|
175
|
+
this.logger.debug('REMOTE STDOUT:', data.toString());
|
|
176
|
+
if (deferred.state === 'unresolved') {
|
|
177
|
+
stdoutBuffer += data.toString();
|
|
178
|
+
|
|
179
|
+
if (tester(stdoutBuffer, stderrBuffer)) {
|
|
180
|
+
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
181
|
+
cleanupStreams();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
const stderr = new PassThrough();
|
|
186
|
+
stderr.on('data', (data: Buffer) => {
|
|
187
|
+
this.logger.debug('REMOTE STDERR:', data.toString());
|
|
188
|
+
if (deferred.state === 'unresolved') {
|
|
189
|
+
stderrBuffer += data.toString();
|
|
190
|
+
|
|
191
|
+
if (tester(stdoutBuffer, stderrBuffer)) {
|
|
192
|
+
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
|
193
|
+
cleanupStreams();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
execution.modem.demuxStream(stream, stdout, stderr);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
deferred.reject(e);
|
|
200
|
+
}
|
|
201
|
+
return deferred.promise;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
getDockerHost(): string[] {
|
|
205
|
+
const dockerHost = process.env.DOCKER_HOST;
|
|
206
|
+
try {
|
|
207
|
+
if (dockerHost) {
|
|
208
|
+
const dockerHostURL = new URL(dockerHost);
|
|
209
|
+
if (dockerHostURL.protocol === 'http:' || dockerHostURL.protocol === 'https:') {
|
|
210
|
+
dockerHostURL.protocol = 'tcp:';
|
|
211
|
+
}
|
|
212
|
+
return ['-H', dockerHostURL.href];
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
this.logger.error('Failed to parse DOCKER_HOST:', e);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async copy(localPath: string, remotePath: string): Promise<void> {
|
|
222
|
+
const deferred = new Deferred<void>();
|
|
223
|
+
const hostArgs = this.getDockerHost();
|
|
224
|
+
|
|
225
|
+
const subprocess = execFile('docker', [...hostArgs, 'cp', '-a', localPath, `${this.container.id}:${remotePath}`]);
|
|
226
|
+
|
|
227
|
+
let stderr = '';
|
|
228
|
+
subprocess.stderr?.on('data', data => {
|
|
229
|
+
stderr += data.toString();
|
|
230
|
+
});
|
|
231
|
+
subprocess.on('close', code => {
|
|
232
|
+
if (code === 0) {
|
|
233
|
+
deferred.resolve();
|
|
234
|
+
} else {
|
|
235
|
+
deferred.reject(stderr);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
return deferred.promise;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
disposeSync(): void {
|
|
242
|
+
// cant use dockerode here since this needs to happen on one tick
|
|
243
|
+
this.shutdownContainer(true);
|
|
244
|
+
this.onDidDisconnectEmitter.dispose();
|
|
245
|
+
if (this.dockerEventStream) {
|
|
246
|
+
(this.dockerEventStream as import('stream').Readable).destroy();
|
|
247
|
+
this.dockerEventStream = undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async dispose(): Promise<void> {
|
|
252
|
+
await this.shutdownContainer(false);
|
|
253
|
+
this.onDidDisconnectEmitter.dispose();
|
|
254
|
+
if (this.dockerEventStream) {
|
|
255
|
+
(this.dockerEventStream as import('stream').Readable).destroy();
|
|
256
|
+
this.dockerEventStream = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
protected async shutdownContainer(sync: boolean): Promise<unknown> {
|
|
261
|
+
const hostArgs = this.getDockerHost();
|
|
262
|
+
|
|
263
|
+
const shutdownAction = this.config.shutdownAction ?? (this.config.dockerComposeFile ? 'stopCompose' : 'stopContainer');
|
|
264
|
+
|
|
265
|
+
if (shutdownAction === 'none') {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (shutdownAction === 'stopContainer') {
|
|
270
|
+
return sync ? execFileSync('docker', [...hostArgs, 'stop', this.container.id]) : this.container.stop();
|
|
271
|
+
} else if (shutdownAction === 'stopCompose') {
|
|
272
|
+
if (!this.config.dockerComposeFile) {
|
|
273
|
+
this.logger.warn('shutdownAction is stopCompose but dockerComposeFile is not defined, falling back to stopContainer');
|
|
274
|
+
return sync ? execFileSync('docker', [...hostArgs, 'stop', this.container.id]) : this.container.stop();
|
|
275
|
+
}
|
|
276
|
+
const composeFilePath = resolveComposeFilePath(this.config);
|
|
277
|
+
return sync ? execFileSync('docker', [...hostArgs, 'compose', '-f', composeFilePath, 'stop']) :
|
|
278
|
+
new Promise<void>((res, rej) => execFile('docker', [...hostArgs, 'compose', '-f', composeFilePath, 'stop'], err => {
|
|
279
|
+
if (err) {
|
|
280
|
+
this.logger.error('Failed to stop compose:', err);
|
|
281
|
+
rej(err);
|
|
282
|
+
} else {
|
|
283
|
+
res();
|
|
284
|
+
}
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"remote-container-connection-provider.spec.d.ts","sourceRoot":"","sources":["../../src/electron-node/remote-container-connection-provider.spec.ts"],"names":[],"mappings":""}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// *****************************************************************************
|
|
3
|
-
// Copyright (C) 2026 EclipseSource and others.
|
|
4
|
-
//
|
|
5
|
-
// This program and the accompanying materials are made available under the
|
|
6
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
-
//
|
|
9
|
-
// This Source Code may also be made available under the following Secondary
|
|
10
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
-
// with the GNU Classpath Exception which is available at
|
|
13
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
-
//
|
|
15
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
-
// *****************************************************************************
|
|
17
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
const chai_1 = require("chai");
|
|
19
|
-
const remote_container_connection_provider_1 = require("./remote-container-connection-provider");
|
|
20
|
-
class TestableDockerContainerConnection extends remote_container_connection_provider_1.RemoteDockerContainerConnection {
|
|
21
|
-
testGetRemoteEnv() {
|
|
22
|
-
return this.getRemoteEnv();
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function createConnection(config) {
|
|
26
|
-
const mockDocker = {
|
|
27
|
-
getEvents: () => Promise.resolve({ on: () => { } })
|
|
28
|
-
};
|
|
29
|
-
const mockContainer = {};
|
|
30
|
-
const mockLogger = {};
|
|
31
|
-
return new TestableDockerContainerConnection({
|
|
32
|
-
id: 'test-id',
|
|
33
|
-
name: 'test',
|
|
34
|
-
type: 'Dev Container',
|
|
35
|
-
docker: mockDocker,
|
|
36
|
-
container: mockContainer,
|
|
37
|
-
config,
|
|
38
|
-
logger: mockLogger
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
describe('RemoteDockerContainerConnection', () => {
|
|
42
|
-
describe('getRemoteEnv', () => {
|
|
43
|
-
it('should return undefined when remoteEnv is not set', () => {
|
|
44
|
-
const connection = createConnection({
|
|
45
|
-
image: 'test'
|
|
46
|
-
});
|
|
47
|
-
(0, chai_1.expect)(connection.testGetRemoteEnv()).to.be.undefined;
|
|
48
|
-
});
|
|
49
|
-
it('should return undefined when remoteEnv is empty', () => {
|
|
50
|
-
const connection = createConnection({
|
|
51
|
-
image: 'test',
|
|
52
|
-
remoteEnv: {}
|
|
53
|
-
});
|
|
54
|
-
(0, chai_1.expect)(connection.testGetRemoteEnv()).to.be.undefined;
|
|
55
|
-
});
|
|
56
|
-
it('should convert remoteEnv entries to KEY=value format', () => {
|
|
57
|
-
const connection = createConnection({
|
|
58
|
-
image: 'test',
|
|
59
|
-
remoteEnv: {
|
|
60
|
-
'MY_VAR': 'my_value',
|
|
61
|
-
'ANOTHER_VAR': 'another_value'
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
const env = connection.testGetRemoteEnv();
|
|
65
|
-
(0, chai_1.expect)(env).to.have.lengthOf(2);
|
|
66
|
-
(0, chai_1.expect)(env).to.include('MY_VAR=my_value');
|
|
67
|
-
(0, chai_1.expect)(env).to.include('ANOTHER_VAR=another_value');
|
|
68
|
-
});
|
|
69
|
-
it('should filter out entries with undefined values', () => {
|
|
70
|
-
const connection = createConnection({
|
|
71
|
-
image: 'test',
|
|
72
|
-
remoteEnv: {
|
|
73
|
-
'KEEP_VAR': 'value',
|
|
74
|
-
'REMOVE_VAR': undefined
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
const env = connection.testGetRemoteEnv();
|
|
78
|
-
(0, chai_1.expect)(env).to.have.lengthOf(1);
|
|
79
|
-
(0, chai_1.expect)(env).to.include('KEEP_VAR=value');
|
|
80
|
-
});
|
|
81
|
-
it('should return undefined when all entries have undefined values', () => {
|
|
82
|
-
const connection = createConnection({
|
|
83
|
-
image: 'test',
|
|
84
|
-
remoteEnv: {
|
|
85
|
-
'VAR1': undefined,
|
|
86
|
-
'VAR2': undefined
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
const env = connection.testGetRemoteEnv();
|
|
90
|
-
(0, chai_1.expect)(env).to.have.lengthOf(0);
|
|
91
|
-
});
|
|
92
|
-
it('should handle values containing equals signs', () => {
|
|
93
|
-
const connection = createConnection({
|
|
94
|
-
image: 'test',
|
|
95
|
-
remoteEnv: {
|
|
96
|
-
'CONNECTION_STRING': 'host=localhost;port=5432'
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
const env = connection.testGetRemoteEnv();
|
|
100
|
-
(0, chai_1.expect)(env).to.have.lengthOf(1);
|
|
101
|
-
(0, chai_1.expect)(env).to.include('CONNECTION_STRING=host=localhost;port=5432');
|
|
102
|
-
});
|
|
103
|
-
it('should handle empty string values', () => {
|
|
104
|
-
const connection = createConnection({
|
|
105
|
-
image: 'test',
|
|
106
|
-
remoteEnv: {
|
|
107
|
-
'EMPTY_VAR': ''
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
const env = connection.testGetRemoteEnv();
|
|
111
|
-
(0, chai_1.expect)(env).to.have.lengthOf(1);
|
|
112
|
-
(0, chai_1.expect)(env).to.include('EMPTY_VAR=');
|
|
113
|
-
});
|
|
114
|
-
it('should handle values with special characters', () => {
|
|
115
|
-
const connection = createConnection({
|
|
116
|
-
image: 'test',
|
|
117
|
-
remoteEnv: {
|
|
118
|
-
'PATH_EXTRA': '/usr/local/bin:/custom/path',
|
|
119
|
-
'QUOTED': 'hello "world"',
|
|
120
|
-
'SPACED': 'hello world'
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
const env = connection.testGetRemoteEnv();
|
|
124
|
-
(0, chai_1.expect)(env).to.have.lengthOf(3);
|
|
125
|
-
(0, chai_1.expect)(env).to.include('PATH_EXTRA=/usr/local/bin:/custom/path');
|
|
126
|
-
(0, chai_1.expect)(env).to.include('QUOTED=hello "world"');
|
|
127
|
-
(0, chai_1.expect)(env).to.include('SPACED=hello world');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
//# sourceMappingURL=remote-container-connection-provider.spec.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"remote-container-connection-provider.spec.js","sourceRoot":"","sources":["../../src/electron-node/remote-container-connection-provider.spec.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,+CAA+C;AAC/C,EAAE;AACF,2EAA2E;AAC3E,mEAAmE;AACnE,wCAAwC;AACxC,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,6EAA6E;AAC7E,yDAAyD;AACzD,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;;AAEhF,+BAA8B;AAC9B,iGAAyF;AAKzF,MAAM,iCAAkC,SAAQ,sEAA+B;IACpE,gBAAgB;QACnB,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;IAC/B,CAAC;CACJ;AAED,SAAS,gBAAgB,CAAC,MAAiC;IACvD,MAAM,UAAU,GAAG;QACf,SAAS,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;KACjC,CAAC;IACvB,MAAM,aAAa,GAAG,EAAiC,CAAC;IACxD,MAAM,UAAU,GAAG,EAAa,CAAC;IACjC,OAAO,IAAI,iCAAiC,CAAC;QACzC,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,UAAU;QAClB,SAAS,EAAE,aAAa;QACxB,MAAM;QACN,MAAM,EAAE,UAAU;KACrB,CAAC,CAAC;AACP,CAAC;AAED,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAE7C,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAE1B,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;YACzD,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;aACa,CAAC,CAAC;YAEhC,IAAA,aAAM,EAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;YACvD,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE,EAAE;aACa,CAAC,CAAC;YAEhC,IAAA,aAAM,EAAC,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC;QAC1D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;YAC5D,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,QAAQ,EAAE,UAAU;oBACpB,aAAa,EAAE,eAAe;iBACjC;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAChC,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;QACxD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;YACvD,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,UAAU,EAAE,OAAO;oBACnB,YAAY,EAAE,SAAS;iBAC1B;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAChC,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;YACtE,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,MAAM,EAAE,SAAS;oBACjB,MAAM,EAAE,SAAS;iBACpB;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACpD,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,mBAAmB,EAAE,0BAA0B;iBAClD;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAChC,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC;QACzE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;YACzC,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,WAAW,EAAE,EAAE;iBAClB;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAChC,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;YACpD,MAAM,UAAU,GAAG,gBAAgB,CAAC;gBAChC,KAAK,EAAE,MAAM;gBACb,SAAS,EAAE;oBACP,YAAY,EAAE,6BAA6B;oBAC3C,QAAQ,EAAE,eAAe;oBACzB,QAAQ,EAAE,aAAa;iBAC1B;aACyB,CAAC,CAAC;YAEhC,MAAM,GAAG,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC1C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YAChC,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,wCAAwC,CAAC,CAAC;YACjE,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;YAC/C,IAAA,aAAM,EAAC,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
|