@theia/dev-container 1.76.0-next.7 → 1.77.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 +2 -0
- package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
- package/lib/electron-browser/container-connection-contribution.js +12 -1
- package/lib/electron-browser/container-connection-contribution.js.map +1 -1
- package/lib/electron-browser/container-output-provider.d.ts +5 -0
- package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
- package/lib/electron-browser/container-output-provider.js +11 -0
- package/lib/electron-browser/container-output-provider.js.map +1 -1
- package/lib/electron-browser/dev-container-attach-screen.d.ts +52 -0
- package/lib/electron-browser/dev-container-attach-screen.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-attach-screen.js +142 -0
- package/lib/electron-browser/dev-container-attach-screen.js.map +1 -0
- package/lib/electron-browser/dev-container-attach-screen.spec.d.ts +2 -0
- package/lib/electron-browser/dev-container-attach-screen.spec.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-attach-screen.spec.js +51 -0
- package/lib/electron-browser/dev-container-attach-screen.spec.js.map +1 -0
- package/lib/electron-browser/dev-container-frontend-module.d.ts +1 -0
- package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
- package/lib/electron-browser/dev-container-frontend-module.js +3 -0
- package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
- package/lib/electron-browser/dev-container-startup-contribution.d.ts +31 -1
- package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -1
- package/lib/electron-browser/dev-container-startup-contribution.js +114 -9
- package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -1
- package/lib/electron-browser/dev-container-startup-contribution.spec.d.ts +2 -0
- package/lib/electron-browser/dev-container-startup-contribution.spec.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-startup-contribution.spec.js +199 -0
- package/lib/electron-browser/dev-container-startup-contribution.spec.js.map +1 -0
- package/lib/electron-common/container-output-provider.d.ts +6 -0
- package/lib/electron-common/container-output-provider.d.ts.map +1 -1
- package/lib/electron-common/remote-container-connection-provider.d.ts +10 -0
- package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-main/dev-container-claim-contribution.d.ts +13 -0
- package/lib/electron-main/dev-container-claim-contribution.d.ts.map +1 -0
- package/lib/electron-main/dev-container-claim-contribution.js +38 -0
- package/lib/electron-main/dev-container-claim-contribution.js.map +1 -0
- package/lib/electron-main/dev-container-electron-main-module.d.ts +4 -0
- package/lib/electron-main/dev-container-electron-main-module.d.ts.map +1 -0
- package/lib/electron-main/dev-container-electron-main-module.js +25 -0
- package/lib/electron-main/dev-container-electron-main-module.js.map +1 -0
- package/lib/electron-node/remote-container-connection-provider.d.ts +1 -1
- package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.js +10 -5
- package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
- package/package.json +9 -8
- package/src/electron-browser/container-connection-contribution.ts +11 -1
- package/src/electron-browser/container-output-provider.ts +11 -0
- package/src/electron-browser/dev-container-attach-screen.spec.ts +60 -0
- package/src/electron-browser/dev-container-attach-screen.tsx +180 -0
- package/src/electron-browser/dev-container-frontend-module.ts +3 -0
- package/src/electron-browser/dev-container-startup-contribution.spec.ts +245 -0
- package/src/electron-browser/dev-container-startup-contribution.ts +113 -15
- package/src/electron-browser/style/dev-container-attach-screen.css +75 -0
- package/src/electron-common/container-output-provider.ts +6 -0
- package/src/electron-common/remote-container-connection-provider.ts +10 -0
- package/src/electron-main/dev-container-claim-contribution.ts +34 -0
- package/src/electron-main/dev-container-electron-main-module.ts +24 -0
- package/src/electron-node/remote-container-connection-provider.ts +10 -5
|
@@ -0,0 +1,245 @@
|
|
|
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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
|
|
21
|
+
import { expect } from 'chai';
|
|
22
|
+
import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
|
|
23
|
+
|
|
24
|
+
const launchArgs = (values: Record<string, string[]>): LaunchArguments => ({ values, negated: [] });
|
|
25
|
+
import { AttachContainerArgs, ContainerConnectionResult, RunningContainerInfo } from '../electron-common/remote-container-connection-provider';
|
|
26
|
+
import { DevContainerStartupContribution } from './dev-container-startup-contribution';
|
|
27
|
+
import type { AttachScreenErrorActions } from './dev-container-attach-screen';
|
|
28
|
+
|
|
29
|
+
disableJSDOM();
|
|
30
|
+
|
|
31
|
+
class TestDevContainerStartupContribution extends DevContainerStartupContribution {
|
|
32
|
+
|
|
33
|
+
forwardedArgs: LaunchArguments | undefined;
|
|
34
|
+
backendCalls = 0;
|
|
35
|
+
backendArgs: AttachContainerArgs | undefined;
|
|
36
|
+
|
|
37
|
+
resolve(): Promise<AttachContainerArgs | undefined> {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
(this as any).launchArgs = { getLaunchArgs: () => this.forwardedArgs };
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
(this as any).connectionProvider = {
|
|
42
|
+
getAttachContainerArgs: async () => {
|
|
43
|
+
this.backendCalls++;
|
|
44
|
+
return this.backendArgs;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return this.resolveAttachArgs();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('DevContainerStartupContribution#resolveAttachArgs', () => {
|
|
52
|
+
|
|
53
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
54
|
+
after(() => disableJSDOM());
|
|
55
|
+
|
|
56
|
+
let contribution: TestDevContainerStartupContribution;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
contribution = new TestDevContainerStartupContribution();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reads the container from the backend on a cold-start window (no forwarded options)', async () => {
|
|
63
|
+
contribution.forwardedArgs = undefined;
|
|
64
|
+
contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
|
|
65
|
+
|
|
66
|
+
const result = await contribution.resolve();
|
|
67
|
+
|
|
68
|
+
expect(result).to.deep.equal({ containerId: 'A', scanForDevJson: true });
|
|
69
|
+
expect(contribution.backendCalls).to.equal(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reads the container from the forwarded options on a second-instance window', async () => {
|
|
73
|
+
contribution.forwardedArgs = launchArgs({ 'attach-container': ['B'] });
|
|
74
|
+
contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
|
|
75
|
+
|
|
76
|
+
const result = await contribution.resolve();
|
|
77
|
+
|
|
78
|
+
expect(result).to.deep.equal({ containerId: 'B', scanForDevJson: true });
|
|
79
|
+
// The stale backend value (from the first launch) must not be consulted.
|
|
80
|
+
expect(contribution.backendCalls).to.equal(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('honors --no-dev-json in the forwarded options', async () => {
|
|
84
|
+
contribution.forwardedArgs = { values: { 'attach-container': ['B'] }, negated: ['dev-json'] };
|
|
85
|
+
|
|
86
|
+
const result = await contribution.resolve();
|
|
87
|
+
|
|
88
|
+
expect(result).to.deep.equal({ containerId: 'B', scanForDevJson: false });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('does not attach for a second-instance window without --attach-container', async () => {
|
|
92
|
+
contribution.forwardedArgs = launchArgs({ 'session-preference': ['foo=1'] });
|
|
93
|
+
contribution.backendArgs = { containerId: 'A', scanForDevJson: true };
|
|
94
|
+
|
|
95
|
+
const result = await contribution.resolve();
|
|
96
|
+
|
|
97
|
+
expect(result).to.be.undefined;
|
|
98
|
+
expect(contribution.backendCalls).to.equal(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
class AttachHarness extends DevContainerStartupContribution {
|
|
103
|
+
stages: string[] = [];
|
|
104
|
+
errors: Array<{ message: string, actions: AttachScreenErrorActions }> = [];
|
|
105
|
+
disposeCount = 0;
|
|
106
|
+
showCount = 0;
|
|
107
|
+
openRemoteCalls: string[] = [];
|
|
108
|
+
protected statusListener: ((message: string) => void) | undefined;
|
|
109
|
+
|
|
110
|
+
setup(opts: { containers?: RunningContainerInfo[], attachResult?: ContainerConnectionResult | Error, statusDuringAttach?: string }): void {
|
|
111
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
112
|
+
(this as any).logger = { info: () => { }, warn: () => { }, error: () => { } };
|
|
113
|
+
(this as any).attachScreen = {
|
|
114
|
+
showAttaching: () => { this.showCount++; },
|
|
115
|
+
reportStage: (message: string) => this.stages.push(message),
|
|
116
|
+
reportError: (message: string, actions: AttachScreenErrorActions) => this.errors.push({ message, actions }),
|
|
117
|
+
dispose: () => { this.disposeCount++; }
|
|
118
|
+
};
|
|
119
|
+
(this as any).containerOutputProvider = {
|
|
120
|
+
onDidReportStatus: (cb: (message: string) => void) => {
|
|
121
|
+
this.statusListener = cb;
|
|
122
|
+
return { dispose: () => { this.statusListener = undefined; } };
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
(this as any).remotePreferences = {};
|
|
126
|
+
(this as any).remoteCliArgsCollector = { collect: async () => [] };
|
|
127
|
+
(this as any).connectionProvider = {
|
|
128
|
+
listRunningContainers: async () => opts.containers ?? [],
|
|
129
|
+
getWorkspaceCandidates: async () => [],
|
|
130
|
+
scanForDevContainerConfig: async () => undefined,
|
|
131
|
+
attachToContainer: async () => {
|
|
132
|
+
if (opts.statusDuringAttach) {
|
|
133
|
+
this.statusListener?.(opts.statusDuringAttach);
|
|
134
|
+
}
|
|
135
|
+
if (opts.attachResult instanceof Error) {
|
|
136
|
+
throw opts.attachResult;
|
|
137
|
+
}
|
|
138
|
+
return opts.attachResult!;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
protected override openRemote(port: string): void {
|
|
145
|
+
this.openRemoteCalls.push(port);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
run(args: AttachContainerArgs): Promise<void> {
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
150
|
+
return (this as any).runStartupAttach(args);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('DevContainerStartupContribution#runStartupAttach', () => {
|
|
155
|
+
|
|
156
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
157
|
+
after(() => disableJSDOM());
|
|
158
|
+
|
|
159
|
+
const runningB: RunningContainerInfo = { id: 'abc123def456', name: 'B', image: 'img', status: 'running', created: 0 };
|
|
160
|
+
|
|
161
|
+
it('reloads into the container on success without showing an error', async () => {
|
|
162
|
+
const harness = new AttachHarness();
|
|
163
|
+
harness.setup({ containers: [runningB], attachResult: { port: '9000', workspacePath: '/w', containerId: 'abc123def456' } });
|
|
164
|
+
|
|
165
|
+
await harness.run({ containerId: 'B', scanForDevJson: false });
|
|
166
|
+
|
|
167
|
+
expect(harness.openRemoteCalls).to.deep.equal(['9000']);
|
|
168
|
+
expect(harness.errors).to.have.lengthOf(0);
|
|
169
|
+
expect(harness.showCount).to.be.greaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('forwards backend status messages to the attach screen', async () => {
|
|
173
|
+
const harness = new AttachHarness();
|
|
174
|
+
harness.setup({
|
|
175
|
+
containers: [runningB],
|
|
176
|
+
attachResult: { port: '9000', workspacePath: '/w', containerId: 'abc123def456' },
|
|
177
|
+
statusDuringAttach: 'Starting application on remote...'
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await harness.run({ containerId: 'B', scanForDevJson: false });
|
|
181
|
+
|
|
182
|
+
expect(harness.stages).to.include('Starting application on remote...');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('shows an error with retry/close when the container is not found, without reloading', async () => {
|
|
186
|
+
const harness = new AttachHarness();
|
|
187
|
+
harness.setup({ containers: [] });
|
|
188
|
+
|
|
189
|
+
await harness.run({ containerId: 'missing', scanForDevJson: false });
|
|
190
|
+
|
|
191
|
+
expect(harness.openRemoteCalls).to.have.lengthOf(0);
|
|
192
|
+
expect(harness.errors).to.have.lengthOf(1);
|
|
193
|
+
expect(harness.errors[0].message).to.match(/not found/i);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('retry re-runs the attach; close dismisses the screen', async () => {
|
|
197
|
+
const harness = new AttachHarness();
|
|
198
|
+
harness.setup({ containers: [] });
|
|
199
|
+
|
|
200
|
+
await harness.run({ containerId: 'missing', scanForDevJson: false });
|
|
201
|
+
expect(harness.errors).to.have.lengthOf(1);
|
|
202
|
+
|
|
203
|
+
await harness.errors[0].actions.retry();
|
|
204
|
+
expect(harness.errors).to.have.lengthOf(2);
|
|
205
|
+
|
|
206
|
+
harness.errors[1].actions.close();
|
|
207
|
+
expect(harness.disposeCount).to.be.greaterThan(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
class StartupHarness extends DevContainerStartupContribution {
|
|
212
|
+
errors: string[] = [];
|
|
213
|
+
|
|
214
|
+
setup(): void {
|
|
215
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
216
|
+
(this as any).logger = { info: () => { }, warn: () => { }, error: () => { } };
|
|
217
|
+
(this as any).launchArgs = { getLaunchArgs: () => undefined };
|
|
218
|
+
(this as any).attachScreen = { isAttached: false, dispose: () => { } };
|
|
219
|
+
(this as any).messageService = { error: (message: string) => this.errors.push(message) };
|
|
220
|
+
(this as any).connectionProvider = {
|
|
221
|
+
getAttachContainerArgs: async () => { throw new Error('backend unreachable'); }
|
|
222
|
+
};
|
|
223
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
run(): Promise<void> {
|
|
227
|
+
return this.handleStartupAttach();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
describe('DevContainerStartupContribution#handleStartupAttach', () => {
|
|
232
|
+
|
|
233
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
234
|
+
after(() => disableJSDOM());
|
|
235
|
+
|
|
236
|
+
it('reports a failure to resolve the attach arguments on a cold start', async () => {
|
|
237
|
+
const harness = new StartupHarness();
|
|
238
|
+
harness.setup();
|
|
239
|
+
|
|
240
|
+
await harness.run();
|
|
241
|
+
|
|
242
|
+
expect(harness.errors).to.have.lengthOf(1);
|
|
243
|
+
expect(harness.errors[0]).to.contain('backend unreachable');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -16,10 +16,15 @@
|
|
|
16
16
|
|
|
17
17
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
18
18
|
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
|
19
|
-
import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
|
19
|
+
import { AttachContainerArgs, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
|
20
20
|
import { AbstractRemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
|
|
21
21
|
import { ILogger, MessageService, nls } from '@theia/core';
|
|
22
|
+
import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
|
|
23
|
+
import { WindowLaunchArgs } from '@theia/core/lib/browser/window/window-launch-args';
|
|
22
24
|
import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
|
|
25
|
+
import { ContainerOutputProvider } from './container-output-provider';
|
|
26
|
+
import { DevContainerAttachScreen } from './dev-container-attach-screen';
|
|
27
|
+
import { RemoteCliArgsCollector } from '@theia/remote/lib/electron-browser/remote-cli-args-collector';
|
|
23
28
|
|
|
24
29
|
@injectable()
|
|
25
30
|
export class DevContainerStartupContribution extends AbstractRemoteRegistryContribution implements FrontendApplicationContribution {
|
|
@@ -36,23 +41,71 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
|
|
|
36
41
|
@inject(RemotePreferences)
|
|
37
42
|
protected readonly remotePreferences: RemotePreferences;
|
|
38
43
|
|
|
44
|
+
@inject(RemoteCliArgsCollector)
|
|
45
|
+
protected readonly remoteCliArgsCollector: RemoteCliArgsCollector;
|
|
46
|
+
|
|
47
|
+
@inject(ContainerOutputProvider)
|
|
48
|
+
protected readonly containerOutputProvider: ContainerOutputProvider;
|
|
49
|
+
|
|
50
|
+
@inject(DevContainerAttachScreen)
|
|
51
|
+
protected readonly attachScreen: DevContainerAttachScreen;
|
|
52
|
+
|
|
53
|
+
@inject(WindowLaunchArgs)
|
|
54
|
+
protected readonly launchArgs: WindowLaunchArgs;
|
|
55
|
+
|
|
39
56
|
registerRemoteCommands(): void {
|
|
40
57
|
// no commands to register — this contribution only handles startup
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
onStart(): void {
|
|
61
|
+
// For a CLI-driven attach the window is opened empty and reloads into the container once the
|
|
62
|
+
// attach completes. Show the "attaching" screen from the first paint so the user never
|
|
63
|
+
// interacts with the transient local window, before the (possibly RPC-bound) container id is
|
|
64
|
+
// even resolved.
|
|
65
|
+
if (this.isAttachPending()) {
|
|
66
|
+
this.attachScreen.showAttaching();
|
|
67
|
+
}
|
|
44
68
|
this.handleStartupAttach();
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
protected async handleStartupAttach(): Promise<void> {
|
|
72
|
+
let args: AttachContainerArgs | undefined;
|
|
48
73
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
args = await this.resolveAttachArgs();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// A rejection here (e.g. failing to redeem the launch args or reach the backend) must not
|
|
77
|
+
// escape as an unhandled rejection and leave the attach screen stuck on "Preparing to attach".
|
|
78
|
+
this.logger.error('CLI: Failed to resolve attach arguments during startup:', e);
|
|
79
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
80
|
+
if (this.isAttachPending()) {
|
|
81
|
+
this.attachScreen.reportError(message, {
|
|
82
|
+
retry: () => this.handleStartupAttach(),
|
|
83
|
+
close: () => this.disposeAttachScreen()
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
// No attach screen is up, so fall back to a notification.
|
|
87
|
+
this.disposeAttachScreen();
|
|
88
|
+
this.messageService.error(nls.localize('theia/remote/dev-container/cliAttachError', 'Failed to attach to container: {0}', message));
|
|
52
89
|
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!args) {
|
|
93
|
+
// Not attaching after all (e.g. a second-instance window without --attach-container):
|
|
94
|
+
// make sure a preliminary screen is not left up.
|
|
95
|
+
this.disposeAttachScreen();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.runStartupAttach(args);
|
|
99
|
+
}
|
|
53
100
|
|
|
54
|
-
|
|
101
|
+
protected async runStartupAttach(args: AttachContainerArgs): Promise<void> {
|
|
102
|
+
const { containerId, scanForDevJson } = args;
|
|
103
|
+
this.attachScreen.showAttaching(containerId);
|
|
104
|
+
// Surface the backend's live status messages (RemoteStatusReport) on the attach screen.
|
|
105
|
+
const statusSubscription = this.containerOutputProvider.onDidReportStatus(message => this.attachScreen.reportStage(message));
|
|
106
|
+
try {
|
|
55
107
|
this.logger.info(`CLI: --attach-container ${containerId}, initiating attach from frontend...`);
|
|
108
|
+
this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/locating', 'Locating container {0}...', containerId));
|
|
56
109
|
|
|
57
110
|
const containers = await this.connectionProvider.listRunningContainers();
|
|
58
111
|
// Match by ID prefix (either direction — user may pass a short prefix or a full 64-char ID
|
|
@@ -64,13 +117,11 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
|
|
|
64
117
|
const target = matches[0];
|
|
65
118
|
|
|
66
119
|
if (!target) {
|
|
67
|
-
|
|
68
|
-
'Container "{0}" not found or not running.', containerId);
|
|
69
|
-
this.logger.error(`CLI: ${msg}`);
|
|
70
|
-
this.messageService.error(msg);
|
|
71
|
-
return;
|
|
120
|
+
throw new Error(nls.localize('theia/remote/dev-container/cliContainerNotFound',
|
|
121
|
+
'Container "{0}" not found or not running.', containerId));
|
|
72
122
|
}
|
|
73
123
|
|
|
124
|
+
this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/preparingWorkspace', 'Preparing workspace...'));
|
|
74
125
|
const candidates = await this.connectionProvider.getWorkspaceCandidates(target.id);
|
|
75
126
|
const workspacePath = candidates.length > 0 ? candidates[0].path : '/';
|
|
76
127
|
|
|
@@ -78,22 +129,69 @@ export class DevContainerStartupContribution extends AbstractRemoteRegistryContr
|
|
|
78
129
|
? await this.connectionProvider.scanForDevContainerConfig(target.id, workspacePath)
|
|
79
130
|
: undefined;
|
|
80
131
|
|
|
132
|
+
this.attachScreen.reportStage(nls.localize('theia/remote/dev-container/attachScreen/starting', 'Starting the application inside the container...'));
|
|
81
133
|
const result = await this.connectionProvider.attachToContainer({
|
|
82
134
|
containerId: target.id,
|
|
83
135
|
workspacePath,
|
|
84
136
|
devcontainerFile,
|
|
85
137
|
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
|
|
138
|
+
additionalArgs: await this.remoteCliArgsCollector.collect(),
|
|
86
139
|
});
|
|
87
140
|
|
|
88
141
|
this.logger.info(`CLI: startup attach ready, proxy on port ${result.port}, workspace: ${result.workspacePath}`);
|
|
142
|
+
// Reloads the window into the container; the attach screen is torn down by the navigation.
|
|
89
143
|
this.openRemote(result.port, false, result.workspacePath);
|
|
90
144
|
} catch (e) {
|
|
91
145
|
this.logger.error('CLI: Failed to attach to container during startup:', e);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
146
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
147
|
+
this.attachScreen.reportError(message, {
|
|
148
|
+
retry: () => this.runStartupAttach(args),
|
|
149
|
+
close: () => this.attachScreen.dispose()
|
|
150
|
+
});
|
|
151
|
+
} finally {
|
|
152
|
+
statusSubscription.dispose();
|
|
97
153
|
}
|
|
98
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Whether this window was opened for a CLI-driven attach. Decided from the window's own launch
|
|
158
|
+
* options, which are available synchronously from the first paint, rather than from a flag in
|
|
159
|
+
* the URL: only `@theia/dev-container` knows what `--attach-container` means.
|
|
160
|
+
*/
|
|
161
|
+
protected isAttachPending(): boolean {
|
|
162
|
+
const args = this.launchArgs.getLaunchArgs();
|
|
163
|
+
return args !== undefined && LaunchArguments.lastValue(args, 'attach-container') !== undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Disposes the attach screen, but only when it is actually on screen. The screen is a singleton,
|
|
168
|
+
* so disposing it on the many windows that never attach would leave a disposed widget behind
|
|
169
|
+
* that a later {@link DevContainerAttachScreen.showAttaching} could no longer attach.
|
|
170
|
+
*/
|
|
171
|
+
protected disposeAttachScreen(): void {
|
|
172
|
+
if (this.attachScreen.isAttached) {
|
|
173
|
+
this.attachScreen.dispose();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the container to attach to on startup.
|
|
179
|
+
*
|
|
180
|
+
* A forwarded (second-instance) launch carries its CLI options, provided by the trusted main
|
|
181
|
+
* process. For such windows those options are the only source, because the shared backend still
|
|
182
|
+
* reflects the original cold-start launch and cannot distinguish between windows. A cold-start
|
|
183
|
+
* window has no forwarded options and reads them from the backend instead.
|
|
184
|
+
*/
|
|
185
|
+
protected async resolveAttachArgs(): Promise<AttachContainerArgs | undefined> {
|
|
186
|
+
const forwarded = this.launchArgs.getLaunchArgs();
|
|
187
|
+
if (forwarded !== undefined) {
|
|
188
|
+
const containerId = LaunchArguments.lastValue(forwarded, 'attach-container');
|
|
189
|
+
if (!containerId) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return { containerId, scanForDevJson: !LaunchArguments.isNegated(forwarded, 'dev-json') };
|
|
193
|
+
}
|
|
194
|
+
return this.connectionProvider.getAttachContainerArgs();
|
|
195
|
+
}
|
|
196
|
+
|
|
99
197
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
.theia-dev-container-attach-screen {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: 10000;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
background-color: var(--theia-editor-background);
|
|
25
|
+
color: var(--theia-foreground);
|
|
26
|
+
font-family: var(--theia-ui-font-family);
|
|
27
|
+
font-size: var(--theia-ui-font-size1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.theia-dev-container-attach-screen-content {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
align-items: center;
|
|
34
|
+
text-align: center;
|
|
35
|
+
max-width: 480px;
|
|
36
|
+
padding: calc(var(--theia-ui-padding) * 4);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.theia-dev-container-attach-screen-logo {
|
|
40
|
+
max-width: 240px;
|
|
41
|
+
max-height: 96px;
|
|
42
|
+
margin-bottom: calc(var(--theia-ui-padding) * 4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.theia-dev-container-attach-screen-spinner {
|
|
46
|
+
font-size: calc(var(--theia-ui-font-size1) * 2.5);
|
|
47
|
+
color: var(--theia-icon-foreground);
|
|
48
|
+
margin-bottom: calc(var(--theia-ui-padding) * 3);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.theia-dev-container-attach-screen-title {
|
|
52
|
+
font-size: var(--theia-ui-font-size3);
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
margin: 0 0 var(--theia-ui-padding) 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.theia-dev-container-attach-screen-stage {
|
|
58
|
+
margin-bottom: calc(var(--theia-ui-padding) * 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.theia-dev-container-attach-screen-note {
|
|
62
|
+
color: var(--theia-descriptionForeground);
|
|
63
|
+
font-size: var(--theia-ui-font-size0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.theia-dev-container-attach-screen-alert {
|
|
67
|
+
margin-bottom: calc(var(--theia-ui-padding) * 3);
|
|
68
|
+
text-align: left;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.theia-dev-container-attach-screen-actions {
|
|
72
|
+
display: flex;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
gap: calc(var(--theia-ui-padding) * 2);
|
|
75
|
+
}
|
|
@@ -16,4 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
export interface ContainerOutputProvider {
|
|
18
18
|
onRemoteOutput(output: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Receives curated status messages (from {@link RemoteStatusReport}) describing the current
|
|
21
|
+
* attach phase, e.g. "Starting application on remote...". Used to drive the "attaching" screen.
|
|
22
|
+
* Optional so that existing implementers of this interface keep compiling.
|
|
23
|
+
*/
|
|
24
|
+
onRemoteStatus?(message: string): void;
|
|
19
25
|
}
|
|
@@ -27,6 +27,11 @@ export interface ContainerConnectionOptions {
|
|
|
27
27
|
lastContainerInfo?: LastContainerInfo
|
|
28
28
|
devcontainerFile: string;
|
|
29
29
|
workspacePath?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Additional CLI arguments to pass to the container's remote backend, e.g. the
|
|
32
|
+
* `--session-preference` values forwarded to a second-instance window.
|
|
33
|
+
*/
|
|
34
|
+
additionalArgs?: string[];
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
export interface LastContainerInfo {
|
|
@@ -64,6 +69,11 @@ export interface AttachContainerOptions {
|
|
|
64
69
|
workspacePath: string;
|
|
65
70
|
nodeDownloadTemplate?: string;
|
|
66
71
|
devcontainerFile?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Additional CLI arguments to pass to the container's remote backend, e.g. the
|
|
74
|
+
* `--session-preference` values forwarded to a second-instance window.
|
|
75
|
+
*/
|
|
76
|
+
additionalArgs?: string[];
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
export interface AttachContainerArgs {
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
|
|
19
|
+
import { LaunchArguments } from '@theia/core/lib/common/launch-arguments';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Claims windows opened for a CLI `--attach-container` launch. Such a window is opened empty and
|
|
23
|
+
* reloads into the container once the attach completes; claiming it lets core open an empty window
|
|
24
|
+
* (instead of restoring the last workspace) and flag it so the dev-container frontend can show its
|
|
25
|
+
* "attaching" screen from the first paint. Keeps the knowledge of `--attach-container` in
|
|
26
|
+
* `@theia/dev-container` rather than in core.
|
|
27
|
+
*/
|
|
28
|
+
@injectable()
|
|
29
|
+
export class DevContainerClaimContribution implements ElectronMainApplicationContribution {
|
|
30
|
+
|
|
31
|
+
claimsWindow(args: LaunchArguments): boolean {
|
|
32
|
+
return LaunchArguments.lastValue(args, 'attach-container') !== undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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 { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
|
|
19
|
+
import { DevContainerClaimContribution } from './dev-container-claim-contribution';
|
|
20
|
+
|
|
21
|
+
export default new ContainerModule(bind => {
|
|
22
|
+
bind(DevContainerClaimContribution).toSelf().inSingletonScope();
|
|
23
|
+
bind(ElectronMainApplicationContribution).toService(DevContainerClaimContribution);
|
|
24
|
+
});
|
|
@@ -255,7 +255,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
255
255
|
|
|
256
256
|
report('Connecting to remote system...');
|
|
257
257
|
|
|
258
|
-
const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report);
|
|
258
|
+
const result = await this.setupRemoteConnection(container, docker, devContainerConfig, options.nodeDownloadTemplate, report, options.additionalArgs);
|
|
259
259
|
remote = result.remote;
|
|
260
260
|
|
|
261
261
|
await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider, context);
|
|
@@ -296,7 +296,11 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
296
296
|
async attachToContainer(options: AttachContainerOptions): Promise<ContainerConnectionResult> {
|
|
297
297
|
const progress = await this.messageService.showProgress({ text: 'Attaching to container' });
|
|
298
298
|
try {
|
|
299
|
-
const report: RemoteStatusReport = message =>
|
|
299
|
+
const report: RemoteStatusReport = message => {
|
|
300
|
+
progress.report({ message });
|
|
301
|
+
// Mirror the status to the frontend so the CLI "attaching" screen can show live progress.
|
|
302
|
+
this.outputProvider?.onRemoteStatus?.(message);
|
|
303
|
+
};
|
|
300
304
|
const docker = await this.createDockerConnection();
|
|
301
305
|
const container = docker.getContainer(options.containerId);
|
|
302
306
|
|
|
@@ -318,7 +322,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
318
322
|
try {
|
|
319
323
|
report('Connecting to remote system...');
|
|
320
324
|
|
|
321
|
-
const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report);
|
|
325
|
+
const result = await this.setupRemoteConnection(container, docker, config, options.nodeDownloadTemplate, report, options.additionalArgs);
|
|
322
326
|
remote = result.remote;
|
|
323
327
|
|
|
324
328
|
if (options.devcontainerFile) {
|
|
@@ -378,7 +382,7 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
378
382
|
*/
|
|
379
383
|
protected async setupRemoteConnection(
|
|
380
384
|
container: Docker.Container, docker: Docker, config: DevContainerConfiguration,
|
|
381
|
-
nodeDownloadTemplate: string | undefined, report: RemoteStatusReport
|
|
385
|
+
nodeDownloadTemplate: string | undefined, report: RemoteStatusReport, additionalArgs?: string[]
|
|
382
386
|
): Promise<{ localPort: number; remote: RemoteDockerContainerConnection }> {
|
|
383
387
|
const remote = new RemoteDockerContainerConnection({
|
|
384
388
|
id: generateUuid(),
|
|
@@ -395,7 +399,8 @@ export class DevContainerConnectionProvider implements RemoteContainerConnection
|
|
|
395
399
|
result = await this.remoteSetup.setup({
|
|
396
400
|
connection: remote,
|
|
397
401
|
report,
|
|
398
|
-
nodeDownloadTemplate
|
|
402
|
+
nodeDownloadTemplate,
|
|
403
|
+
additionalArgs
|
|
399
404
|
});
|
|
400
405
|
} catch (e) {
|
|
401
406
|
remote.dispose();
|