@theia/core 1.72.0-next.42 → 1.72.0-next.45
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/browser/catalog.json +11 -5
- package/lib/browser/decorations-service.d.ts.map +1 -1
- package/lib/browser/decorations-service.js +4 -0
- package/lib/browser/decorations-service.js.map +1 -1
- package/lib/node/backend-application-module.d.ts.map +1 -1
- package/lib/node/backend-application-module.js +7 -0
- package/lib/node/backend-application-module.js.map +1 -1
- package/lib/node/backend-application.d.ts +43 -3
- package/lib/node/backend-application.d.ts.map +1 -1
- package/lib/node/backend-application.js +82 -15
- package/lib/node/backend-application.js.map +1 -1
- package/lib/node/backend-application.spec.d.ts +2 -0
- package/lib/node/backend-application.spec.d.ts.map +1 -0
- package/lib/node/backend-application.spec.js +252 -0
- package/lib/node/backend-application.spec.js.map +1 -0
- package/package.json +4 -4
- package/src/browser/decorations-service.ts +4 -0
- package/src/node/backend-application-module.ts +11 -1
- package/src/node/backend-application.spec.ts +310 -0
- package/src/node/backend-application.ts +104 -19
|
@@ -21,10 +21,10 @@ import * as https from 'https';
|
|
|
21
21
|
import * as express from 'express';
|
|
22
22
|
import * as yargs from 'yargs';
|
|
23
23
|
import * as fs from 'fs-extra';
|
|
24
|
-
import { inject, named, injectable, postConstruct } from 'inversify';
|
|
24
|
+
import { inject, named, injectable, type interfaces, postConstruct } from 'inversify';
|
|
25
25
|
import { ContributionProvider, LogLevel, MaybePromise, MeasurementContext, Stopwatch } from '../common';
|
|
26
26
|
import { CliContribution } from './cli';
|
|
27
|
-
import { Deferred } from '../common/promise-util';
|
|
27
|
+
import { Deferred, timeoutReject } from '../common/promise-util';
|
|
28
28
|
import { environment } from '../common/index';
|
|
29
29
|
import { AddressInfo } from 'net';
|
|
30
30
|
import { ProcessUtils } from './process-utils';
|
|
@@ -35,12 +35,19 @@ import { ProcessUtils } from './process-utils';
|
|
|
35
35
|
*/
|
|
36
36
|
export const BackendApplicationPath = process.env.THEIA_APP_PROJECT_PATH || process.cwd();
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Private injection token for the backend's root Inversify {@link Container}.
|
|
40
|
+
*/
|
|
41
|
+
export const RootContainer = Symbol('RootContainer');
|
|
42
|
+
|
|
38
43
|
export type DnsResultOrder = 'ipv4first' | 'verbatim' | 'nodeDefault';
|
|
39
44
|
|
|
40
45
|
const APP_PROJECT_PATH = 'app-project-path';
|
|
41
46
|
|
|
42
47
|
const TIMER_WARNING_THRESHOLD = 50;
|
|
43
48
|
|
|
49
|
+
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
50
|
+
|
|
44
51
|
const DEFAULT_PORT = environment.electron.is() ? 0 : 3000;
|
|
45
52
|
const DEFAULT_HOST = 'localhost';
|
|
46
53
|
const DEFAULT_SSL = false;
|
|
@@ -103,12 +110,23 @@ export interface BackendApplicationContribution {
|
|
|
103
110
|
onStart?(server: http.Server | https.Server): MaybePromise<void>;
|
|
104
111
|
|
|
105
112
|
/**
|
|
106
|
-
* Called when the backend application shuts down.
|
|
107
|
-
*
|
|
113
|
+
* Called when the backend application shuts down.
|
|
114
|
+
*
|
|
115
|
+
* When shutdown is initiated via `SIGINT`/`SIGTERM`, contributions are dispatched
|
|
116
|
+
* in parallel and any returned promise is awaited up to `SHUTDOWN_TIMEOUT_MS`
|
|
117
|
+
* milliseconds while injected services from the root container are still
|
|
118
|
+
* resolvable.
|
|
119
|
+
*
|
|
120
|
+
* On synchronous-exit fallback paths (uncaught exceptions, server bind failures,
|
|
121
|
+
* or normal process exit), the hook is invoked synchronously and any returned
|
|
122
|
+
* promise is discarded. Implementations should be resilient to either path.
|
|
123
|
+
*
|
|
124
|
+
* Contributions must be independent of one another during stop because they are
|
|
125
|
+
* dispatched in parallel.
|
|
108
126
|
*
|
|
109
127
|
* @param app the express application.
|
|
110
128
|
*/
|
|
111
|
-
onStop?(app?: express.Application): void
|
|
129
|
+
onStop?(app?: express.Application): MaybePromise<void>;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
@injectable()
|
|
@@ -162,8 +180,14 @@ export class BackendApplication {
|
|
|
162
180
|
@inject(Stopwatch)
|
|
163
181
|
protected readonly stopwatch: Stopwatch;
|
|
164
182
|
|
|
183
|
+
@inject(RootContainer)
|
|
184
|
+
protected readonly rootContainer: interfaces.Container;
|
|
185
|
+
|
|
165
186
|
private _configured: Promise<void>;
|
|
166
187
|
|
|
188
|
+
private stoppedContributions = false;
|
|
189
|
+
private shuttingDown = false;
|
|
190
|
+
|
|
167
191
|
private settlementContext?: MeasurementContext<BackendApplicationContribution>;
|
|
168
192
|
|
|
169
193
|
constructor(
|
|
@@ -179,18 +203,15 @@ export class BackendApplication {
|
|
|
179
203
|
process.on('SIGPIPE', () => {
|
|
180
204
|
console.error(new Error('Unexpected SIGPIPE'));
|
|
181
205
|
});
|
|
182
|
-
|
|
183
|
-
* Kill the current process tree on exit.
|
|
184
|
-
*/
|
|
185
|
-
function signalHandler(signal: NodeJS.Signals): never {
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
206
|
+
|
|
188
207
|
// Handles normal process termination.
|
|
189
208
|
process.on('exit', () => this.onStop());
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
209
|
+
|
|
210
|
+
// Handles `Ctrl+C` and `kill pid`. Delegates to gracefulShutdown so that
|
|
211
|
+
// root-scoped singletons get their @preDestroy hooks invoked before exit.
|
|
212
|
+
const onSignal = () => { this.gracefulShutdown().catch(err => console.error(err)); };
|
|
213
|
+
process.on('SIGINT', onSignal);
|
|
214
|
+
process.on('SIGTERM', onSignal);
|
|
194
215
|
}
|
|
195
216
|
|
|
196
217
|
protected async initialize(): Promise<void> {
|
|
@@ -332,18 +353,82 @@ export class BackendApplication {
|
|
|
332
353
|
: `${scheme}://${address}:${port}`;
|
|
333
354
|
}
|
|
334
355
|
|
|
335
|
-
|
|
356
|
+
/**
|
|
357
|
+
* Performs an asynchronous shutdown of the backend in two phases:
|
|
358
|
+
*
|
|
359
|
+
* 1. Contributions' {@link BackendApplicationContribution.onStop onStop} hooks are
|
|
360
|
+
* dispatched in parallel and awaited so that they can still resolve services
|
|
361
|
+
* from the root Inversify container while it is bound.
|
|
362
|
+
* 2. All services in the root container are unbound, running their `@preDestroy`
|
|
363
|
+
* hooks.
|
|
364
|
+
*
|
|
365
|
+
* Each phase has its own {@link SHUTDOWN_TIMEOUT_MS} budget to avoid hanging on a
|
|
366
|
+
* misbehaving hook. Late-resolving promises from a timed-out phase may still
|
|
367
|
+
* settle in the background and could log noisily or interact with a partly
|
|
368
|
+
* unbound container; this is accepted because the process is exiting.
|
|
369
|
+
*
|
|
370
|
+
* Idempotent: a second invocation is a no-op. Exits the process with code 1 so
|
|
371
|
+
* that the `process.on('exit')` handler runs for fallback cleanup such as
|
|
372
|
+
* {@link ProcessUtils.terminateProcessTree}; the exit handler does not re-invoke
|
|
373
|
+
* contribution `onStop()` hooks that this method already dispatched.
|
|
374
|
+
*/
|
|
375
|
+
protected async gracefulShutdown(): Promise<void> {
|
|
376
|
+
if (this.shuttingDown) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.shuttingDown = true;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await Promise.race([
|
|
383
|
+
this.stopContributions(),
|
|
384
|
+
timeoutReject<void>(SHUTDOWN_TIMEOUT_MS, `Stopping backend contributions timed out after ${SHUTDOWN_TIMEOUT_MS}ms`)
|
|
385
|
+
]);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
388
|
+
console.warn(`Backend contributions cleanup failed: ${message}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await Promise.race([
|
|
393
|
+
this.rootContainer.unbindAllAsync(),
|
|
394
|
+
timeoutReject<void>(SHUTDOWN_TIMEOUT_MS, `Container unbind timed out after ${SHUTDOWN_TIMEOUT_MS}ms`)
|
|
395
|
+
]);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
398
|
+
console.warn(`Backend root container cleanup failed: ${message}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
protected async stopContributions(): Promise<void> {
|
|
405
|
+
if (this.stoppedContributions) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
this.stoppedContributions = true;
|
|
336
409
|
console.info('>>> Stopping backend contributions...');
|
|
337
|
-
|
|
410
|
+
// The `async` wrapper converts a synchronous throw inside a non-async
|
|
411
|
+
// contribution's `onStop` into a rejected promise so the per-contribution
|
|
412
|
+
// try/catch can handle it; otherwise `Promise.all` would abort.
|
|
413
|
+
await Promise.all(this.contributionsProvider.getContributions().map(async contrib => {
|
|
338
414
|
if (contrib.onStop) {
|
|
339
415
|
try {
|
|
340
|
-
contrib.onStop(this.app);
|
|
416
|
+
await contrib.onStop(this.app);
|
|
341
417
|
} catch (error) {
|
|
342
418
|
console.error('Could not stop contribution', error);
|
|
343
419
|
}
|
|
344
420
|
}
|
|
345
|
-
}
|
|
421
|
+
}));
|
|
346
422
|
console.info('<<< All backend contributions have been stopped.');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
protected onStop(): void {
|
|
426
|
+
// Deliberate fire-and-forget of an async `stopContributions`()` call.
|
|
427
|
+
// It invokes each contribution's `onStop` synchronously up to its
|
|
428
|
+
// first `await`, so any synchronous cleanup runs before
|
|
429
|
+
// `terminateProcessTree`. Any returned promises are abandoned because
|
|
430
|
+
// the `'exit'` event does not yield back to the event loop.
|
|
431
|
+
this.stopContributions();
|
|
347
432
|
this.processUtils.terminateProcessTree(process.pid);
|
|
348
433
|
}
|
|
349
434
|
|