@theia/plugin-dev 1.45.0 → 1.46.0-next.72

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