@theia/plugin-dev 1.34.2 → 1.34.3

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 (53) hide show
  1. package/LICENSE +641 -641
  2. package/README.md +31 -31
  3. package/lib/browser/hosted-plugin-controller.d.ts +74 -74
  4. package/lib/browser/hosted-plugin-controller.js +352 -352
  5. package/lib/browser/hosted-plugin-frontend-contribution.d.ts +6 -6
  6. package/lib/browser/hosted-plugin-frontend-contribution.js +56 -56
  7. package/lib/browser/hosted-plugin-informer.d.ts +25 -25
  8. package/lib/browser/hosted-plugin-informer.js +102 -102
  9. package/lib/browser/hosted-plugin-log-viewer.d.ts +14 -14
  10. package/lib/browser/hosted-plugin-log-viewer.js +69 -69
  11. package/lib/browser/hosted-plugin-manager-client.d.ts +79 -79
  12. package/lib/browser/hosted-plugin-manager-client.js +407 -407
  13. package/lib/browser/hosted-plugin-preferences.d.ts +13 -13
  14. package/lib/browser/hosted-plugin-preferences.js +60 -60
  15. package/lib/browser/plugin-dev-frontend-module.d.ts +3 -3
  16. package/lib/browser/plugin-dev-frontend-module.js +42 -42
  17. package/lib/common/index.d.ts +2 -2
  18. package/lib/common/index.js +31 -31
  19. package/lib/common/plugin-dev-protocol.d.ts +24 -24
  20. package/lib/common/plugin-dev-protocol.js +20 -20
  21. package/lib/node/hosted-instance-manager.d.ts +102 -102
  22. package/lib/node/hosted-instance-manager.js +316 -316
  23. package/lib/node/hosted-plugin-reader.d.ts +11 -11
  24. package/lib/node/hosted-plugin-reader.js +68 -68
  25. package/lib/node/hosted-plugin-uri-postprocessor.d.ts +6 -6
  26. package/lib/node/hosted-plugin-uri-postprocessor.js +27 -27
  27. package/lib/node/hosted-plugins-manager.d.ts +41 -41
  28. package/lib/node/hosted-plugins-manager.js +118 -118
  29. package/lib/node/plugin-dev-backend-module.d.ts +4 -4
  30. package/lib/node/plugin-dev-backend-module.js +53 -53
  31. package/lib/node/plugin-dev-service.d.ts +25 -25
  32. package/lib/node/plugin-dev-service.js +108 -108
  33. package/lib/node-electron/plugin-dev-electron-backend-module.d.ts +3 -3
  34. package/lib/node-electron/plugin-dev-electron-backend-module.js +28 -28
  35. package/lib/package.spec.js +25 -25
  36. package/package.json +9 -9
  37. package/src/browser/hosted-plugin-controller.ts +356 -356
  38. package/src/browser/hosted-plugin-frontend-contribution.ts +45 -45
  39. package/src/browser/hosted-plugin-informer.ts +93 -93
  40. package/src/browser/hosted-plugin-log-viewer.ts +52 -52
  41. package/src/browser/hosted-plugin-manager-client.ts +426 -426
  42. package/src/browser/hosted-plugin-preferences.ts +71 -71
  43. package/src/browser/plugin-dev-frontend-module.ts +45 -45
  44. package/src/common/index.ts +21 -21
  45. package/src/common/plugin-dev-protocol.ts +45 -45
  46. package/src/node/hosted-instance-manager.ts +381 -381
  47. package/src/node/hosted-plugin-reader.ts +58 -58
  48. package/src/node/hosted-plugin-uri-postprocessor.ts +32 -32
  49. package/src/node/hosted-plugins-manager.ts +146 -146
  50. package/src/node/plugin-dev-backend-module.ts +54 -54
  51. package/src/node/plugin-dev-service.ts +107 -107
  52. package/src/node-electron/plugin-dev-electron-backend-module.ts +29 -29
  53. package/src/package.spec.ts +28 -28
@@ -1,426 +1,426 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
- import URI from '@theia/core/lib/common/uri';
19
- import { Path } from '@theia/core/lib/common/path';
20
- import { MessageService, Command, Emitter, Event } from '@theia/core/lib/common';
21
- import { LabelProvider, isNative, AbstractDialog } from '@theia/core/lib/browser';
22
- import { WindowService } from '@theia/core/lib/browser/window/window-service';
23
- import { WorkspaceService } from '@theia/workspace/lib/browser';
24
- import { FileDialogService } from '@theia/filesystem/lib/browser';
25
- import { PluginDebugConfiguration, PluginDevServer } from '../common/plugin-dev-protocol';
26
- import { LaunchVSCodeArgument, LaunchVSCodeRequest, LaunchVSCodeResult } from '@theia/debug/lib/browser/debug-contribution';
27
- import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
28
- import { HostedPluginPreferences } from './hosted-plugin-preferences';
29
- import { FileService } from '@theia/filesystem/lib/browser/file-service';
30
- import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
31
- import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
32
- import { nls } from '@theia/core/lib/common/nls';
33
-
34
- /**
35
- * Commands to control Hosted plugin instances.
36
- */
37
- export namespace HostedPluginCommands {
38
- const HOSTED_PLUGIN_CATEGORY_KEY = 'theia/plugin-dev/hostedPlugin';
39
- const HOSTED_PLUGIN_CATEGORY = 'Hosted Plugin';
40
- export const START = Command.toLocalizedCommand({
41
- id: 'hosted-plugin:start',
42
- category: HOSTED_PLUGIN_CATEGORY,
43
- label: 'Start Instance'
44
- }, 'theia/plugin-dev/startInstance', HOSTED_PLUGIN_CATEGORY_KEY);
45
-
46
- export const DEBUG = Command.toLocalizedCommand({
47
- id: 'hosted-plugin:debug',
48
- category: HOSTED_PLUGIN_CATEGORY,
49
- label: 'Debug Instance'
50
- }, 'theia/plugin-dev/debugInstance', HOSTED_PLUGIN_CATEGORY_KEY);
51
-
52
- export const STOP = Command.toLocalizedCommand({
53
- id: 'hosted-plugin:stop',
54
- category: HOSTED_PLUGIN_CATEGORY,
55
- label: 'Stop Instance'
56
- }, 'theia/plugin-dev/stopInstance', HOSTED_PLUGIN_CATEGORY_KEY);
57
-
58
- export const RESTART = Command.toLocalizedCommand({
59
- id: 'hosted-plugin:restart',
60
- category: HOSTED_PLUGIN_CATEGORY,
61
- label: 'Restart Instance'
62
- }, 'theia/plugin-dev/restartInstance', HOSTED_PLUGIN_CATEGORY_KEY);
63
-
64
- export const SELECT_PATH = Command.toLocalizedCommand({
65
- id: 'hosted-plugin:select-path',
66
- category: HOSTED_PLUGIN_CATEGORY,
67
- label: 'Select Path'
68
- }, 'theia/plugin-dev/selectPath', HOSTED_PLUGIN_CATEGORY_KEY);
69
- }
70
-
71
- /**
72
- * Available states of hosted plugin instance.
73
- */
74
- export enum HostedInstanceState {
75
- STOPPED = 'stopped',
76
- STARTING = 'starting',
77
- RUNNING = 'running',
78
- STOPPING = 'stopping',
79
- FAILED = 'failed'
80
- }
81
-
82
- export interface HostedInstanceData {
83
- state: HostedInstanceState;
84
- pluginLocation: URI;
85
- }
86
-
87
- /**
88
- * Responsible for UI to set up and control Hosted Plugin Instance.
89
- */
90
- @injectable()
91
- export class HostedPluginManagerClient {
92
- private openNewTabAskDialog: OpenHostedInstanceLinkDialog;
93
-
94
- private connection: DebugSessionConnection;
95
-
96
- // path to the plugin on the file system
97
- protected pluginLocation: URI | undefined;
98
-
99
- // URL to the running plugin instance
100
- protected pluginInstanceURL: string | undefined;
101
-
102
- protected isDebug = false;
103
-
104
- protected readonly stateChanged = new Emitter<HostedInstanceData>();
105
-
106
- get onStateChanged(): Event<HostedInstanceData> {
107
- return this.stateChanged.event;
108
- }
109
-
110
- @inject(PluginDevServer)
111
- protected readonly hostedPluginServer: PluginDevServer;
112
- @inject(MessageService)
113
- protected readonly messageService: MessageService;
114
- @inject(LabelProvider)
115
- protected readonly labelProvider: LabelProvider;
116
- @inject(WindowService)
117
- protected readonly windowService: WindowService;
118
- @inject(FileService)
119
- protected readonly fileService: FileService;
120
- @inject(EnvVariablesServer)
121
- protected readonly environments: EnvVariablesServer;
122
- @inject(WorkspaceService)
123
- protected readonly workspaceService: WorkspaceService;
124
- @inject(DebugSessionManager)
125
- protected readonly debugSessionManager: DebugSessionManager;
126
- @inject(HostedPluginPreferences)
127
- protected readonly hostedPluginPreferences: HostedPluginPreferences;
128
- @inject(FileDialogService)
129
- protected readonly fileDialogService: FileDialogService;
130
-
131
- @postConstruct()
132
- protected async init(): Promise<void> {
133
- this.openNewTabAskDialog = new OpenHostedInstanceLinkDialog(this.windowService);
134
-
135
- // is needed for case when page is loaded when hosted instance is already running.
136
- if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
137
- this.pluginLocation = new URI(await this.hostedPluginServer.getHostedPluginURI());
138
- }
139
- }
140
-
141
- get lastPluginLocation(): string | undefined {
142
- if (this.pluginLocation) {
143
- return this.pluginLocation.toString();
144
- }
145
- return undefined;
146
- }
147
-
148
- async start(debugConfig?: PluginDebugConfiguration): Promise<void> {
149
- if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
150
- this.messageService.warn(nls.localize('theia/plugin-dev/alreadyRunning', 'Hosted instance is already running.'));
151
- return;
152
- }
153
-
154
- if (!this.pluginLocation) {
155
- await this.selectPluginPath();
156
- if (!this.pluginLocation) {
157
- // selection was cancelled
158
- return;
159
- }
160
- }
161
-
162
- try {
163
- this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation });
164
- this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
165
-
166
- if (debugConfig) {
167
- this.isDebug = true;
168
- this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation.toString(), debugConfig);
169
- } else {
170
- this.isDebug = false;
171
- this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation.toString());
172
- }
173
- await this.openPluginWindow();
174
-
175
- this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
176
- this.stateChanged.fire({ state: HostedInstanceState.RUNNING, pluginLocation: this.pluginLocation });
177
- } catch (error) {
178
- this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(error)));
179
- this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation });
180
- this.stop();
181
- }
182
- }
183
-
184
- async debug(config?: PluginDebugConfiguration): Promise<string | undefined> {
185
- await this.start(this.setDebugConfig(config));
186
- await this.startDebugSessionManager();
187
-
188
- return this.pluginInstanceURL;
189
- }
190
-
191
- async startDebugSessionManager(): Promise<void> {
192
- let outFiles: string[] | undefined = undefined;
193
- if (this.pluginLocation && this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].length > 0) {
194
- const fsPath = await this.fileService.fsPath(this.pluginLocation);
195
- if (fsPath) {
196
- outFiles = this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].map(outFile =>
197
- outFile.replace('${pluginPath}', new Path(fsPath).toString())
198
- );
199
- }
200
- }
201
- const name = nls.localize('theia/plugin-dev/hostedPlugin', 'Hosted Plugin');
202
- await this.debugSessionManager.start({
203
- name,
204
- configuration: {
205
- type: 'node',
206
- request: 'attach',
207
- timeout: 30000,
208
- name,
209
- smartStep: true,
210
- sourceMaps: !!outFiles,
211
- outFiles
212
- }
213
- });
214
- }
215
-
216
- async stop(checkRunning: boolean = true): Promise<void> {
217
- if (checkRunning && !await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
218
- this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
219
- return;
220
- }
221
- try {
222
- this.stateChanged.fire({ state: HostedInstanceState.STOPPING, pluginLocation: this.pluginLocation! });
223
- await this.hostedPluginServer.terminateHostedPluginInstance();
224
- this.messageService.info((this.pluginInstanceURL
225
- ? nls.localize('theia/plugin-dev/instanceTerminated', '{0} has been terminated', this.pluginInstanceURL)
226
- : nls.localize('theia/plugin-dev/unknownTerminated', 'The instance has been terminated')));
227
- this.stateChanged.fire({ state: HostedInstanceState.STOPPED, pluginLocation: this.pluginLocation! });
228
- } catch (error) {
229
- this.messageService.error(this.getErrorMessage(error));
230
- }
231
- }
232
-
233
- async restart(): Promise<void> {
234
- if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
235
- await this.stop(false);
236
-
237
- this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
238
-
239
- // It takes some time before OS released all resources e.g. port.
240
- // Keep trying to run hosted instance with delay.
241
- this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation! });
242
- let lastError;
243
- for (let tries = 0; tries < 15; tries++) {
244
- try {
245
- if (this.isDebug) {
246
- this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation!.toString(), {
247
- debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode']
248
- });
249
- await this.startDebugSessionManager();
250
- } else {
251
- this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation!.toString());
252
- }
253
- await this.openPluginWindow();
254
- this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
255
- this.stateChanged.fire({
256
- state: HostedInstanceState.RUNNING,
257
- pluginLocation: this.pluginLocation!
258
- });
259
- return;
260
- } catch (error) {
261
- lastError = error;
262
- await new Promise(resolve => setTimeout(resolve, 500));
263
- }
264
- }
265
- this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(lastError)));
266
- this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation! });
267
- this.stop();
268
- } else {
269
- this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
270
- this.start();
271
- }
272
- }
273
-
274
- /**
275
- * Creates directory choose dialog and set selected folder into pluginLocation field.
276
- */
277
- async selectPluginPath(): Promise<void> {
278
- const workspaceFolder = (await this.workspaceService.roots)[0] || await this.fileService.resolve(new URI(await this.environments.getHomeDirUri()));
279
- if (!workspaceFolder) {
280
- throw new Error('Unable to find the root');
281
- }
282
-
283
- const result = await this.fileDialogService.showOpenDialog({
284
- title: HostedPluginCommands.SELECT_PATH.label!,
285
- openLabel: nls.localize('theia/plugin-dev/select', 'Select'),
286
- canSelectFiles: false,
287
- canSelectFolders: true,
288
- canSelectMany: false
289
- }, workspaceFolder);
290
-
291
- if (result) {
292
- if (await this.hostedPluginServer.isPluginValid(result.toString())) {
293
- this.pluginLocation = result;
294
- this.messageService.info(nls.localize('theia/plugin-dev/pluginFolder', 'Plugin folder is set to: {0}', this.labelProvider.getLongName(result)));
295
- } else {
296
- this.messageService.error(nls.localize('theia/plugin-dev/noValidPlugin', 'Specified folder does not contain valid plugin.'));
297
- }
298
- }
299
- }
300
-
301
- register(configType: string, connection: DebugSessionConnection): void {
302
- if (configType === 'pwa-extensionHost') {
303
- this.connection = connection;
304
- this.connection.onRequest('launchVSCode', (request: LaunchVSCodeRequest) => this.launchVSCode(request));
305
-
306
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
- this.connection.on('exited', async (args: any) => {
308
- await this.stop();
309
- });
310
- }
311
- }
312
-
313
- /**
314
- * Opens window with URL to the running plugin instance.
315
- */
316
- protected async openPluginWindow(): Promise<void> {
317
- // do nothing for electron browser
318
- if (isNative) {
319
- return;
320
- }
321
-
322
- if (this.pluginInstanceURL) {
323
- try {
324
- this.windowService.openNewWindow(this.pluginInstanceURL);
325
- } catch (err) {
326
- // browser blocked opening of a new tab
327
- this.openNewTabAskDialog.showOpenNewTabAskDialog(this.pluginInstanceURL);
328
- }
329
- }
330
- }
331
-
332
- protected async launchVSCode({ arguments: { args } }: LaunchVSCodeRequest): Promise<LaunchVSCodeResult> {
333
- let result = {};
334
- let instanceURI;
335
-
336
- const sessions = this.debugSessionManager.sessions.filter(session => session.id !== this.connection.sessionId);
337
-
338
- /* if `launchVSCode` is invoked and sessions do not exist - it means that `start` debug was invoked.
339
- if `launchVSCode` is invoked and sessions do exist - it means that `restartSessions()` was invoked,
340
- which invoked `this.sendRequest('restart', {})`, which restarted `vscode-builtin-js-debug` plugin which is
341
- connected to first session (sessions[0]), which means that other existing (child) sessions need to be terminated
342
- and new ones will be created by running `startDebugSessionManager()`
343
- */
344
- if (sessions.length > 0) {
345
- sessions.forEach(session => this.debugSessionManager.terminateSession(session));
346
- await this.startDebugSessionManager();
347
- instanceURI = this.pluginInstanceURL;
348
- } else {
349
- instanceURI = await this.debug(this.getDebugPluginConfig(args));
350
- }
351
-
352
- if (instanceURI) {
353
- const instanceURL = new URL(instanceURI);
354
- if (instanceURL.port) {
355
- result = Object.assign(result, { rendererDebugPort: instanceURL.port });
356
- }
357
- }
358
- return result;
359
- }
360
-
361
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
- protected getErrorMessage(error: any): string {
363
- return error?.message?.substring(error.message.indexOf(':') + 1) || '';
364
- }
365
-
366
- private setDebugConfig(config?: PluginDebugConfiguration): PluginDebugConfiguration {
367
- config = Object.assign(config || {}, { debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'] });
368
- if (config.pluginLocation) {
369
- this.pluginLocation = new URI((!config.pluginLocation.startsWith('/') ? '/' : '') + config.pluginLocation.replace(/\\/g, '/')).withScheme('file');
370
- }
371
- return config;
372
- }
373
-
374
- private getDebugPluginConfig(args: LaunchVSCodeArgument[]): PluginDebugConfiguration {
375
- let pluginLocation;
376
- for (const arg of args) {
377
- if (arg?.prefix === '--extensionDevelopmentPath=') {
378
- pluginLocation = arg.path;
379
- }
380
- }
381
-
382
- return {
383
- pluginLocation
384
- };
385
- }
386
- }
387
-
388
- class OpenHostedInstanceLinkDialog extends AbstractDialog<string> {
389
- protected readonly windowService: WindowService;
390
- protected readonly openButton: HTMLButtonElement;
391
- protected readonly messageNode: HTMLDivElement;
392
- protected readonly linkNode: HTMLAnchorElement;
393
- value: string;
394
-
395
- constructor(windowService: WindowService) {
396
- super({
397
- title: nls.localize('theia/plugin-dev/preventedNewTab', 'Your browser prevented opening of a new tab')
398
- });
399
- this.windowService = windowService;
400
-
401
- this.linkNode = document.createElement('a');
402
- this.linkNode.target = '_blank';
403
- this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
404
- this.contentNode.appendChild(this.linkNode);
405
-
406
- const messageNode = document.createElement('div');
407
- messageNode.innerText = nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:') + ' ';
408
- messageNode.appendChild(this.linkNode);
409
- this.contentNode.appendChild(messageNode);
410
-
411
- this.appendCloseButton();
412
- this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
413
- }
414
-
415
- showOpenNewTabAskDialog(uri: string): void {
416
- this.value = uri;
417
-
418
- this.linkNode.textContent = uri;
419
- this.linkNode.href = uri;
420
- this.openButton.onclick = () => {
421
- this.windowService.openNewWindow(uri);
422
- };
423
-
424
- this.open();
425
- }
426
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2018 Red Hat, Inc. 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 WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
18
+ import URI from '@theia/core/lib/common/uri';
19
+ import { Path } from '@theia/core/lib/common/path';
20
+ import { MessageService, Command, Emitter, Event } from '@theia/core/lib/common';
21
+ import { LabelProvider, isNative, AbstractDialog } from '@theia/core/lib/browser';
22
+ import { WindowService } from '@theia/core/lib/browser/window/window-service';
23
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
24
+ import { FileDialogService } from '@theia/filesystem/lib/browser';
25
+ import { PluginDebugConfiguration, PluginDevServer } from '../common/plugin-dev-protocol';
26
+ import { LaunchVSCodeArgument, LaunchVSCodeRequest, LaunchVSCodeResult } from '@theia/debug/lib/browser/debug-contribution';
27
+ import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
28
+ import { HostedPluginPreferences } from './hosted-plugin-preferences';
29
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
30
+ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
31
+ import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
32
+ import { nls } from '@theia/core/lib/common/nls';
33
+
34
+ /**
35
+ * Commands to control Hosted plugin instances.
36
+ */
37
+ export namespace HostedPluginCommands {
38
+ const HOSTED_PLUGIN_CATEGORY_KEY = 'theia/plugin-dev/hostedPlugin';
39
+ const HOSTED_PLUGIN_CATEGORY = 'Hosted Plugin';
40
+ export const START = Command.toLocalizedCommand({
41
+ id: 'hosted-plugin:start',
42
+ category: HOSTED_PLUGIN_CATEGORY,
43
+ label: 'Start Instance'
44
+ }, 'theia/plugin-dev/startInstance', HOSTED_PLUGIN_CATEGORY_KEY);
45
+
46
+ export const DEBUG = Command.toLocalizedCommand({
47
+ id: 'hosted-plugin:debug',
48
+ category: HOSTED_PLUGIN_CATEGORY,
49
+ label: 'Debug Instance'
50
+ }, 'theia/plugin-dev/debugInstance', HOSTED_PLUGIN_CATEGORY_KEY);
51
+
52
+ export const STOP = Command.toLocalizedCommand({
53
+ id: 'hosted-plugin:stop',
54
+ category: HOSTED_PLUGIN_CATEGORY,
55
+ label: 'Stop Instance'
56
+ }, 'theia/plugin-dev/stopInstance', HOSTED_PLUGIN_CATEGORY_KEY);
57
+
58
+ export const RESTART = Command.toLocalizedCommand({
59
+ id: 'hosted-plugin:restart',
60
+ category: HOSTED_PLUGIN_CATEGORY,
61
+ label: 'Restart Instance'
62
+ }, 'theia/plugin-dev/restartInstance', HOSTED_PLUGIN_CATEGORY_KEY);
63
+
64
+ export const SELECT_PATH = Command.toLocalizedCommand({
65
+ id: 'hosted-plugin:select-path',
66
+ category: HOSTED_PLUGIN_CATEGORY,
67
+ label: 'Select Path'
68
+ }, 'theia/plugin-dev/selectPath', HOSTED_PLUGIN_CATEGORY_KEY);
69
+ }
70
+
71
+ /**
72
+ * Available states of hosted plugin instance.
73
+ */
74
+ export enum HostedInstanceState {
75
+ STOPPED = 'stopped',
76
+ STARTING = 'starting',
77
+ RUNNING = 'running',
78
+ STOPPING = 'stopping',
79
+ FAILED = 'failed'
80
+ }
81
+
82
+ export interface HostedInstanceData {
83
+ state: HostedInstanceState;
84
+ pluginLocation: URI;
85
+ }
86
+
87
+ /**
88
+ * Responsible for UI to set up and control Hosted Plugin Instance.
89
+ */
90
+ @injectable()
91
+ export class HostedPluginManagerClient {
92
+ private openNewTabAskDialog: OpenHostedInstanceLinkDialog;
93
+
94
+ private connection: DebugSessionConnection;
95
+
96
+ // path to the plugin on the file system
97
+ protected pluginLocation: URI | undefined;
98
+
99
+ // URL to the running plugin instance
100
+ protected pluginInstanceURL: string | undefined;
101
+
102
+ protected isDebug = false;
103
+
104
+ protected readonly stateChanged = new Emitter<HostedInstanceData>();
105
+
106
+ get onStateChanged(): Event<HostedInstanceData> {
107
+ return this.stateChanged.event;
108
+ }
109
+
110
+ @inject(PluginDevServer)
111
+ protected readonly hostedPluginServer: PluginDevServer;
112
+ @inject(MessageService)
113
+ protected readonly messageService: MessageService;
114
+ @inject(LabelProvider)
115
+ protected readonly labelProvider: LabelProvider;
116
+ @inject(WindowService)
117
+ protected readonly windowService: WindowService;
118
+ @inject(FileService)
119
+ protected readonly fileService: FileService;
120
+ @inject(EnvVariablesServer)
121
+ protected readonly environments: EnvVariablesServer;
122
+ @inject(WorkspaceService)
123
+ protected readonly workspaceService: WorkspaceService;
124
+ @inject(DebugSessionManager)
125
+ protected readonly debugSessionManager: DebugSessionManager;
126
+ @inject(HostedPluginPreferences)
127
+ protected readonly hostedPluginPreferences: HostedPluginPreferences;
128
+ @inject(FileDialogService)
129
+ protected readonly fileDialogService: FileDialogService;
130
+
131
+ @postConstruct()
132
+ protected async init(): Promise<void> {
133
+ this.openNewTabAskDialog = new OpenHostedInstanceLinkDialog(this.windowService);
134
+
135
+ // is needed for case when page is loaded when hosted instance is already running.
136
+ if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
137
+ this.pluginLocation = new URI(await this.hostedPluginServer.getHostedPluginURI());
138
+ }
139
+ }
140
+
141
+ get lastPluginLocation(): string | undefined {
142
+ if (this.pluginLocation) {
143
+ return this.pluginLocation.toString();
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ async start(debugConfig?: PluginDebugConfiguration): Promise<void> {
149
+ if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
150
+ this.messageService.warn(nls.localize('theia/plugin-dev/alreadyRunning', 'Hosted instance is already running.'));
151
+ return;
152
+ }
153
+
154
+ if (!this.pluginLocation) {
155
+ await this.selectPluginPath();
156
+ if (!this.pluginLocation) {
157
+ // selection was cancelled
158
+ return;
159
+ }
160
+ }
161
+
162
+ try {
163
+ this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation });
164
+ this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
165
+
166
+ if (debugConfig) {
167
+ this.isDebug = true;
168
+ this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation.toString(), debugConfig);
169
+ } else {
170
+ this.isDebug = false;
171
+ this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation.toString());
172
+ }
173
+ await this.openPluginWindow();
174
+
175
+ this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
176
+ this.stateChanged.fire({ state: HostedInstanceState.RUNNING, pluginLocation: this.pluginLocation });
177
+ } catch (error) {
178
+ this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(error)));
179
+ this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation });
180
+ this.stop();
181
+ }
182
+ }
183
+
184
+ async debug(config?: PluginDebugConfiguration): Promise<string | undefined> {
185
+ await this.start(this.setDebugConfig(config));
186
+ await this.startDebugSessionManager();
187
+
188
+ return this.pluginInstanceURL;
189
+ }
190
+
191
+ async startDebugSessionManager(): Promise<void> {
192
+ let outFiles: string[] | undefined = undefined;
193
+ if (this.pluginLocation && this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].length > 0) {
194
+ const fsPath = await this.fileService.fsPath(this.pluginLocation);
195
+ if (fsPath) {
196
+ outFiles = this.hostedPluginPreferences['hosted-plugin.launchOutFiles'].map(outFile =>
197
+ outFile.replace('${pluginPath}', new Path(fsPath).toString())
198
+ );
199
+ }
200
+ }
201
+ const name = nls.localize('theia/plugin-dev/hostedPlugin', 'Hosted Plugin');
202
+ await this.debugSessionManager.start({
203
+ name,
204
+ configuration: {
205
+ type: 'node',
206
+ request: 'attach',
207
+ timeout: 30000,
208
+ name,
209
+ smartStep: true,
210
+ sourceMaps: !!outFiles,
211
+ outFiles
212
+ }
213
+ });
214
+ }
215
+
216
+ async stop(checkRunning: boolean = true): Promise<void> {
217
+ if (checkRunning && !await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
218
+ this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
219
+ return;
220
+ }
221
+ try {
222
+ this.stateChanged.fire({ state: HostedInstanceState.STOPPING, pluginLocation: this.pluginLocation! });
223
+ await this.hostedPluginServer.terminateHostedPluginInstance();
224
+ this.messageService.info((this.pluginInstanceURL
225
+ ? nls.localize('theia/plugin-dev/instanceTerminated', '{0} has been terminated', this.pluginInstanceURL)
226
+ : nls.localize('theia/plugin-dev/unknownTerminated', 'The instance has been terminated')));
227
+ this.stateChanged.fire({ state: HostedInstanceState.STOPPED, pluginLocation: this.pluginLocation! });
228
+ } catch (error) {
229
+ this.messageService.error(this.getErrorMessage(error));
230
+ }
231
+ }
232
+
233
+ async restart(): Promise<void> {
234
+ if (await this.hostedPluginServer.isHostedPluginInstanceRunning()) {
235
+ await this.stop(false);
236
+
237
+ this.messageService.info(nls.localize('theia/plugin-dev/starting', 'Starting hosted instance server ...'));
238
+
239
+ // It takes some time before OS released all resources e.g. port.
240
+ // Keep trying to run hosted instance with delay.
241
+ this.stateChanged.fire({ state: HostedInstanceState.STARTING, pluginLocation: this.pluginLocation! });
242
+ let lastError;
243
+ for (let tries = 0; tries < 15; tries++) {
244
+ try {
245
+ if (this.isDebug) {
246
+ this.pluginInstanceURL = await this.hostedPluginServer.runDebugHostedPluginInstance(this.pluginLocation!.toString(), {
247
+ debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode']
248
+ });
249
+ await this.startDebugSessionManager();
250
+ } else {
251
+ this.pluginInstanceURL = await this.hostedPluginServer.runHostedPluginInstance(this.pluginLocation!.toString());
252
+ }
253
+ await this.openPluginWindow();
254
+ this.messageService.info(`${nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:')} ${this.pluginInstanceURL}`);
255
+ this.stateChanged.fire({
256
+ state: HostedInstanceState.RUNNING,
257
+ pluginLocation: this.pluginLocation!
258
+ });
259
+ return;
260
+ } catch (error) {
261
+ lastError = error;
262
+ await new Promise(resolve => setTimeout(resolve, 500));
263
+ }
264
+ }
265
+ this.messageService.error(nls.localize('theia/plugin-dev/failed', 'Failed to run hosted plugin instance: {0}', this.getErrorMessage(lastError)));
266
+ this.stateChanged.fire({ state: HostedInstanceState.FAILED, pluginLocation: this.pluginLocation! });
267
+ this.stop();
268
+ } else {
269
+ this.messageService.warn(nls.localize('theia/plugin-dev/notRunning', 'Hosted instance is not running.'));
270
+ this.start();
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Creates directory choose dialog and set selected folder into pluginLocation field.
276
+ */
277
+ async selectPluginPath(): Promise<void> {
278
+ const workspaceFolder = (await this.workspaceService.roots)[0] || await this.fileService.resolve(new URI(await this.environments.getHomeDirUri()));
279
+ if (!workspaceFolder) {
280
+ throw new Error('Unable to find the root');
281
+ }
282
+
283
+ const result = await this.fileDialogService.showOpenDialog({
284
+ title: HostedPluginCommands.SELECT_PATH.label!,
285
+ openLabel: nls.localize('theia/plugin-dev/select', 'Select'),
286
+ canSelectFiles: false,
287
+ canSelectFolders: true,
288
+ canSelectMany: false
289
+ }, workspaceFolder);
290
+
291
+ if (result) {
292
+ if (await this.hostedPluginServer.isPluginValid(result.toString())) {
293
+ this.pluginLocation = result;
294
+ this.messageService.info(nls.localize('theia/plugin-dev/pluginFolder', 'Plugin folder is set to: {0}', this.labelProvider.getLongName(result)));
295
+ } else {
296
+ this.messageService.error(nls.localize('theia/plugin-dev/noValidPlugin', 'Specified folder does not contain valid plugin.'));
297
+ }
298
+ }
299
+ }
300
+
301
+ register(configType: string, connection: DebugSessionConnection): void {
302
+ if (configType === 'pwa-extensionHost') {
303
+ this.connection = connection;
304
+ this.connection.onRequest('launchVSCode', (request: LaunchVSCodeRequest) => this.launchVSCode(request));
305
+
306
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
+ this.connection.on('exited', async (args: any) => {
308
+ await this.stop();
309
+ });
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Opens window with URL to the running plugin instance.
315
+ */
316
+ protected async openPluginWindow(): Promise<void> {
317
+ // do nothing for electron browser
318
+ if (isNative) {
319
+ return;
320
+ }
321
+
322
+ if (this.pluginInstanceURL) {
323
+ try {
324
+ this.windowService.openNewWindow(this.pluginInstanceURL);
325
+ } catch (err) {
326
+ // browser blocked opening of a new tab
327
+ this.openNewTabAskDialog.showOpenNewTabAskDialog(this.pluginInstanceURL);
328
+ }
329
+ }
330
+ }
331
+
332
+ protected async launchVSCode({ arguments: { args } }: LaunchVSCodeRequest): Promise<LaunchVSCodeResult> {
333
+ let result = {};
334
+ let instanceURI;
335
+
336
+ const sessions = this.debugSessionManager.sessions.filter(session => session.id !== this.connection.sessionId);
337
+
338
+ /* if `launchVSCode` is invoked and sessions do not exist - it means that `start` debug was invoked.
339
+ if `launchVSCode` is invoked and sessions do exist - it means that `restartSessions()` was invoked,
340
+ which invoked `this.sendRequest('restart', {})`, which restarted `vscode-builtin-js-debug` plugin which is
341
+ connected to first session (sessions[0]), which means that other existing (child) sessions need to be terminated
342
+ and new ones will be created by running `startDebugSessionManager()`
343
+ */
344
+ if (sessions.length > 0) {
345
+ sessions.forEach(session => this.debugSessionManager.terminateSession(session));
346
+ await this.startDebugSessionManager();
347
+ instanceURI = this.pluginInstanceURL;
348
+ } else {
349
+ instanceURI = await this.debug(this.getDebugPluginConfig(args));
350
+ }
351
+
352
+ if (instanceURI) {
353
+ const instanceURL = new URL(instanceURI);
354
+ if (instanceURL.port) {
355
+ result = Object.assign(result, { rendererDebugPort: instanceURL.port });
356
+ }
357
+ }
358
+ return result;
359
+ }
360
+
361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
362
+ protected getErrorMessage(error: any): string {
363
+ return error?.message?.substring(error.message.indexOf(':') + 1) || '';
364
+ }
365
+
366
+ private setDebugConfig(config?: PluginDebugConfiguration): PluginDebugConfiguration {
367
+ config = Object.assign(config || {}, { debugMode: this.hostedPluginPreferences['hosted-plugin.debugMode'] });
368
+ if (config.pluginLocation) {
369
+ this.pluginLocation = new URI((!config.pluginLocation.startsWith('/') ? '/' : '') + config.pluginLocation.replace(/\\/g, '/')).withScheme('file');
370
+ }
371
+ return config;
372
+ }
373
+
374
+ private getDebugPluginConfig(args: LaunchVSCodeArgument[]): PluginDebugConfiguration {
375
+ let pluginLocation;
376
+ for (const arg of args) {
377
+ if (arg?.prefix === '--extensionDevelopmentPath=') {
378
+ pluginLocation = arg.path;
379
+ }
380
+ }
381
+
382
+ return {
383
+ pluginLocation
384
+ };
385
+ }
386
+ }
387
+
388
+ class OpenHostedInstanceLinkDialog extends AbstractDialog<string> {
389
+ protected readonly windowService: WindowService;
390
+ protected readonly openButton: HTMLButtonElement;
391
+ protected readonly messageNode: HTMLDivElement;
392
+ protected readonly linkNode: HTMLAnchorElement;
393
+ value: string;
394
+
395
+ constructor(windowService: WindowService) {
396
+ super({
397
+ title: nls.localize('theia/plugin-dev/preventedNewTab', 'Your browser prevented opening of a new tab')
398
+ });
399
+ this.windowService = windowService;
400
+
401
+ this.linkNode = document.createElement('a');
402
+ this.linkNode.target = '_blank';
403
+ this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
404
+ this.contentNode.appendChild(this.linkNode);
405
+
406
+ const messageNode = document.createElement('div');
407
+ messageNode.innerText = nls.localize('theia/plugin-dev/running', 'Hosted instance is running at:') + ' ';
408
+ messageNode.appendChild(this.linkNode);
409
+ this.contentNode.appendChild(messageNode);
410
+
411
+ this.appendCloseButton();
412
+ this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
413
+ }
414
+
415
+ showOpenNewTabAskDialog(uri: string): void {
416
+ this.value = uri;
417
+
418
+ this.linkNode.textContent = uri;
419
+ this.linkNode.href = uri;
420
+ this.openButton.onclick = () => {
421
+ this.windowService.openNewWindow(uri);
422
+ };
423
+
424
+ this.open();
425
+ }
426
+ }