@theia/core 1.72.0-next.42 → 1.72.0-next.46
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 +63 -6
- 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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const tslib_1 = require("tslib");
|
|
19
|
+
const chai_1 = require("chai");
|
|
20
|
+
const sinon = require("sinon");
|
|
21
|
+
const inversify_1 = require("inversify");
|
|
22
|
+
const common_1 = require("../common");
|
|
23
|
+
const promise_util_1 = require("../common/promise-util");
|
|
24
|
+
const mock_logger_1 = require("../common/test/mock-logger");
|
|
25
|
+
const node_stopwatch_1 = require("./performance/node-stopwatch");
|
|
26
|
+
const process_utils_1 = require("./process-utils");
|
|
27
|
+
const backend_application_1 = require("./backend-application");
|
|
28
|
+
const cli_1 = require("./cli");
|
|
29
|
+
/**
|
|
30
|
+
* Test subclass that exposes the protected `gracefulShutdown` for direct testing.
|
|
31
|
+
*/
|
|
32
|
+
let TestBackendApplication = class TestBackendApplication extends backend_application_1.BackendApplication {
|
|
33
|
+
invokeGracefulShutdown() {
|
|
34
|
+
return this.gracefulShutdown();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
TestBackendApplication = tslib_1.__decorate([
|
|
38
|
+
(0, inversify_1.injectable)()
|
|
39
|
+
], TestBackendApplication);
|
|
40
|
+
// All process events on which `BackendApplication` installs listeners in its
|
|
41
|
+
// constructor. We snapshot and restore them around each test to avoid leaking
|
|
42
|
+
// listeners across tests (and triggering `MaxListenersExceededWarning`).
|
|
43
|
+
const PROCESS_EVENTS = ['SIGINT', 'SIGTERM', 'SIGPIPE', 'exit', 'uncaughtException'];
|
|
44
|
+
describe('BackendApplication', () => {
|
|
45
|
+
let sandbox;
|
|
46
|
+
let exitStub;
|
|
47
|
+
let savedListeners;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
sandbox = sinon.createSandbox();
|
|
50
|
+
// Snapshot any existing listeners so we can restore after each test
|
|
51
|
+
// (BackendApplication installs its own as part of construction).
|
|
52
|
+
savedListeners = {};
|
|
53
|
+
for (const evt of PROCESS_EVENTS) {
|
|
54
|
+
savedListeners[evt] = [...process.listeners(evt)];
|
|
55
|
+
process.removeAllListeners(evt);
|
|
56
|
+
}
|
|
57
|
+
exitStub = sandbox.stub(process, 'exit');
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
for (const evt of PROCESS_EVENTS) {
|
|
61
|
+
process.removeAllListeners(evt);
|
|
62
|
+
for (const listener of savedListeners[evt] ?? []) {
|
|
63
|
+
process.on(evt, listener);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
sandbox.restore();
|
|
67
|
+
});
|
|
68
|
+
function createTestContainer() {
|
|
69
|
+
const container = new inversify_1.Container();
|
|
70
|
+
container.bind(backend_application_1.RootContainer).toConstantValue(container);
|
|
71
|
+
container.bind(common_1.ILogger).to(mock_logger_1.MockLogger).inSingletonScope();
|
|
72
|
+
container.bind(common_1.Stopwatch).to(node_stopwatch_1.NodeStopwatch).inSingletonScope();
|
|
73
|
+
container.bind(process_utils_1.ProcessUtils).toSelf().inSingletonScope();
|
|
74
|
+
container.bind(backend_application_1.BackendApplicationCliContribution).toSelf().inSingletonScope();
|
|
75
|
+
container.bind(cli_1.CliContribution).toService(backend_application_1.BackendApplicationCliContribution);
|
|
76
|
+
(0, common_1.bindContributionProvider)(container, backend_application_1.BackendApplicationContribution);
|
|
77
|
+
container.bind(TestBackendApplication).toSelf().inSingletonScope();
|
|
78
|
+
container.bind(backend_application_1.BackendApplication).toService(TestBackendApplication);
|
|
79
|
+
return container;
|
|
80
|
+
}
|
|
81
|
+
describe('graceful shutdown', () => {
|
|
82
|
+
it('runs @preDestroy on root-scoped singletons before exiting with code 1', async () => {
|
|
83
|
+
let canaryDisposed = false;
|
|
84
|
+
let Canary = class Canary {
|
|
85
|
+
onPreDestroy() {
|
|
86
|
+
canaryDisposed = true;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
tslib_1.__decorate([
|
|
90
|
+
(0, inversify_1.preDestroy)(),
|
|
91
|
+
tslib_1.__metadata("design:type", Function),
|
|
92
|
+
tslib_1.__metadata("design:paramtypes", []),
|
|
93
|
+
tslib_1.__metadata("design:returntype", void 0)
|
|
94
|
+
], Canary.prototype, "onPreDestroy", null);
|
|
95
|
+
Canary = tslib_1.__decorate([
|
|
96
|
+
(0, inversify_1.injectable)()
|
|
97
|
+
], Canary);
|
|
98
|
+
const container = createTestContainer();
|
|
99
|
+
const canaryModule = new inversify_1.ContainerModule(bind => {
|
|
100
|
+
bind(Canary).toSelf().inSingletonScope();
|
|
101
|
+
});
|
|
102
|
+
container.load(canaryModule);
|
|
103
|
+
container.get(Canary);
|
|
104
|
+
const app = container.get(TestBackendApplication);
|
|
105
|
+
await app.invokeGracefulShutdown();
|
|
106
|
+
(0, chai_1.expect)(canaryDisposed, '@preDestroy was not invoked on root-scoped singleton').to.be.true;
|
|
107
|
+
(0, chai_1.expect)(exitStub.calledOnceWith(1), 'process.exit(1) was not called exactly once').to.be.true;
|
|
108
|
+
});
|
|
109
|
+
it('is idempotent: a second invocation does not unbind the container twice', async () => {
|
|
110
|
+
const container = createTestContainer();
|
|
111
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
112
|
+
const app = container.get(TestBackendApplication);
|
|
113
|
+
await app.invokeGracefulShutdown();
|
|
114
|
+
await app.invokeGracefulShutdown();
|
|
115
|
+
(0, chai_1.expect)(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
|
|
116
|
+
(0, chai_1.expect)(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
|
|
117
|
+
});
|
|
118
|
+
it('still exits if container cleanup rejects', async () => {
|
|
119
|
+
const container = createTestContainer();
|
|
120
|
+
const cleanupError = new Error('cleanup boom');
|
|
121
|
+
sandbox.stub(container, 'unbindAllAsync').rejects(cleanupError);
|
|
122
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
123
|
+
const app = container.get(TestBackendApplication);
|
|
124
|
+
await app.invokeGracefulShutdown();
|
|
125
|
+
(0, chai_1.expect)(exitStub.calledOnceWith(1), 'process.exit(1) was not called').to.be.true;
|
|
126
|
+
(0, chai_1.expect)(warnStub.calledOnce, 'a warning should be logged when cleanup rejects').to.be.true;
|
|
127
|
+
(0, chai_1.expect)(warnStub.firstCall.args[0]).to.match(/cleanup boom/);
|
|
128
|
+
});
|
|
129
|
+
it('exits even when container cleanup hangs past the timeout', async () => {
|
|
130
|
+
const clock = sandbox.useFakeTimers();
|
|
131
|
+
const container = createTestContainer();
|
|
132
|
+
sandbox.stub(container, 'unbindAllAsync').returns(new Promise(() => { }));
|
|
133
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
134
|
+
const app = container.get(TestBackendApplication);
|
|
135
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
136
|
+
await clock.tickAsync(5001);
|
|
137
|
+
await shutdownPromise;
|
|
138
|
+
(0, chai_1.expect)(exitStub.calledOnceWith(1), 'process.exit(1) was not called after timeout').to.be.true;
|
|
139
|
+
(0, chai_1.expect)(warnStub.calledOnce, 'a warning should be logged on timeout').to.be.true;
|
|
140
|
+
(0, chai_1.expect)(warnStub.firstCall.args[0]).to.match(/timed out/);
|
|
141
|
+
});
|
|
142
|
+
it('awaits async onStop contributions before unbinding the container', async () => {
|
|
143
|
+
const container = createTestContainer();
|
|
144
|
+
const onStopDeferred = new promise_util_1.Deferred();
|
|
145
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
146
|
+
onStop: () => onStopDeferred.promise
|
|
147
|
+
});
|
|
148
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
149
|
+
const app = container.get(TestBackendApplication);
|
|
150
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
151
|
+
(0, chai_1.expect)(unbindSpy.called, 'unbindAllAsync should not run before onStop resolves').to.be.false;
|
|
152
|
+
onStopDeferred.resolve();
|
|
153
|
+
await shutdownPromise;
|
|
154
|
+
(0, chai_1.expect)(unbindSpy.calledOnce, 'unbindAllAsync should run once onStop completes').to.be.true;
|
|
155
|
+
(0, chai_1.expect)(exitStub.calledOnceWith(1)).to.be.true;
|
|
156
|
+
});
|
|
157
|
+
it('invokes onStop hooks while injected services are still resolvable', async () => {
|
|
158
|
+
let Helper = class Helper {
|
|
159
|
+
constructor() {
|
|
160
|
+
this.value = 'still-bound';
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
Helper = tslib_1.__decorate([
|
|
164
|
+
(0, inversify_1.injectable)()
|
|
165
|
+
], Helper);
|
|
166
|
+
const container = createTestContainer();
|
|
167
|
+
container.bind(Helper).toSelf().inSingletonScope();
|
|
168
|
+
let observed;
|
|
169
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
170
|
+
onStop: () => {
|
|
171
|
+
observed = container.get(Helper).value;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const app = container.get(TestBackendApplication);
|
|
175
|
+
await app.invokeGracefulShutdown();
|
|
176
|
+
(0, chai_1.expect)(observed, 'onStop should observe injected services that are still bound').to.equal('still-bound');
|
|
177
|
+
});
|
|
178
|
+
it('proceeds with shutdown when onStop hooks exceed the timeout', async () => {
|
|
179
|
+
const clock = sandbox.useFakeTimers();
|
|
180
|
+
const container = createTestContainer();
|
|
181
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
182
|
+
onStop: () => new Promise(() => { })
|
|
183
|
+
});
|
|
184
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
185
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
186
|
+
const app = container.get(TestBackendApplication);
|
|
187
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
188
|
+
await clock.tickAsync(5001);
|
|
189
|
+
await shutdownPromise;
|
|
190
|
+
(0, chai_1.expect)(warnStub.calledOnce, 'a warning should be logged on onStop timeout').to.be.true;
|
|
191
|
+
(0, chai_1.expect)(warnStub.firstCall.args[0]).to.match(/Stopping backend contributions/);
|
|
192
|
+
(0, chai_1.expect)(unbindSpy.calledOnce, 'unbind should still run after onStop times out').to.be.true;
|
|
193
|
+
(0, chai_1.expect)(exitStub.calledOnceWith(1)).to.be.true;
|
|
194
|
+
});
|
|
195
|
+
it('runs all contributions even when one onStop rejects', async () => {
|
|
196
|
+
const container = createTestContainer();
|
|
197
|
+
let secondRan = false;
|
|
198
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
199
|
+
onStop: async () => { throw new Error('boom'); }
|
|
200
|
+
});
|
|
201
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
202
|
+
onStop: () => { secondRan = true; }
|
|
203
|
+
});
|
|
204
|
+
const errorStub = sandbox.stub(console, 'error');
|
|
205
|
+
const app = container.get(TestBackendApplication);
|
|
206
|
+
await app.invokeGracefulShutdown();
|
|
207
|
+
(0, chai_1.expect)(secondRan, 'second contribution should still be stopped after first rejects').to.be.true;
|
|
208
|
+
(0, chai_1.expect)(errorStub.calledWithMatch('Could not stop contribution')).to.be.true;
|
|
209
|
+
});
|
|
210
|
+
it('is idempotent when a contribution re-enters graceful shutdown', async () => {
|
|
211
|
+
const container = createTestContainer();
|
|
212
|
+
// Indirected through a holder so the contribution closure can refer to the
|
|
213
|
+
// application instance that is constructed after the binding is recorded.
|
|
214
|
+
const appHolder = {};
|
|
215
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({
|
|
216
|
+
onStop: () => appHolder.current.invokeGracefulShutdown()
|
|
217
|
+
});
|
|
218
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
219
|
+
appHolder.current = container.get(TestBackendApplication);
|
|
220
|
+
await appHolder.current.invokeGracefulShutdown();
|
|
221
|
+
(0, chai_1.expect)(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
|
|
222
|
+
(0, chai_1.expect)(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('process exit handler', () => {
|
|
226
|
+
it('does not re-invoke contributions after graceful shutdown ran them', async () => {
|
|
227
|
+
const container = createTestContainer();
|
|
228
|
+
const onStopSpy = sandbox.spy();
|
|
229
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
|
|
230
|
+
const terminateStub = sandbox.stub(process_utils_1.ProcessUtils.prototype, 'terminateProcessTree');
|
|
231
|
+
const app = container.get(TestBackendApplication);
|
|
232
|
+
const exitListener = process.listeners('exit')[0];
|
|
233
|
+
await app.invokeGracefulShutdown();
|
|
234
|
+
(0, chai_1.expect)(onStopSpy.callCount, 'contribution onStop should fire from gracefulShutdown').to.equal(1);
|
|
235
|
+
exitListener();
|
|
236
|
+
(0, chai_1.expect)(onStopSpy.callCount, 'contribution onStop should not be invoked a second time').to.equal(1);
|
|
237
|
+
(0, chai_1.expect)(terminateStub.called, 'terminateProcessTree should be invoked by the exit handler').to.be.true;
|
|
238
|
+
});
|
|
239
|
+
it('invokes contributions synchronously when graceful shutdown was bypassed', () => {
|
|
240
|
+
const container = createTestContainer();
|
|
241
|
+
const onStopSpy = sandbox.spy();
|
|
242
|
+
container.bind(backend_application_1.BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
|
|
243
|
+
const terminateStub = sandbox.stub(process_utils_1.ProcessUtils.prototype, 'terminateProcessTree');
|
|
244
|
+
container.get(TestBackendApplication);
|
|
245
|
+
const exitListener = process.listeners('exit')[0];
|
|
246
|
+
exitListener();
|
|
247
|
+
(0, chai_1.expect)(onStopSpy.calledOnce, 'sync exit path should still invoke contributions').to.be.true;
|
|
248
|
+
(0, chai_1.expect)(terminateStub.called, 'terminateProcessTree should be invoked').to.be.true;
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
//# sourceMappingURL=backend-application.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend-application.spec.js","sourceRoot":"","sources":["../../src/node/backend-application.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theia/core",
|
|
3
|
-
"version": "1.72.0-next.
|
|
3
|
+
"version": "1.72.0-next.46+128342e9b",
|
|
4
4
|
"description": "Theia is a cloud & desktop IDE framework implemented in TypeScript.",
|
|
5
5
|
"main": "lib/common/index.js",
|
|
6
6
|
"typings": "lib/common/index.d.ts",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"@lumino/virtualdom": "^2.0.4",
|
|
18
18
|
"@lumino/widgets": "2.7.5",
|
|
19
19
|
"@parcel/watcher": "^2.5.6",
|
|
20
|
-
"@theia/application-package": "1.72.0-next.
|
|
21
|
-
"@theia/request": "1.72.0-next.
|
|
20
|
+
"@theia/application-package": "1.72.0-next.46+128342e9b",
|
|
21
|
+
"@theia/request": "1.72.0-next.46+128342e9b",
|
|
22
22
|
"@types/body-parser": "^1.19.6",
|
|
23
23
|
"@types/express": "^4.17.25",
|
|
24
24
|
"@types/fs-extra": "^4.0.15",
|
|
@@ -221,5 +221,5 @@
|
|
|
221
221
|
"nyc": {
|
|
222
222
|
"extends": "../../configs/nyc.json"
|
|
223
223
|
},
|
|
224
|
-
"gitHead": "
|
|
224
|
+
"gitHead": "128342e9ba53c273de5ef8a5bc9b93dbc0b931d4"
|
|
225
225
|
}
|
|
@@ -148,6 +148,10 @@ class DecorationProviderWrapper {
|
|
|
148
148
|
const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => {
|
|
149
149
|
if (this.data.get(uri) === request) {
|
|
150
150
|
this.keepItem(uri, data);
|
|
151
|
+
// Notify subscribers that a lazily-fetched decoration is now
|
|
152
|
+
// available so renderers can re-query. Without this, decorations
|
|
153
|
+
// dropped by event truncation never reach the UI.
|
|
154
|
+
this.onDidChangeDecorationsEmitter.fire(this.decorations);
|
|
151
155
|
}
|
|
152
156
|
}).catch(err => {
|
|
153
157
|
if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled') && this.data.get(uri) === request) {
|
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
PreferenceProviderProvider,
|
|
35
35
|
PreferenceProvider
|
|
36
36
|
} from '../common';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution,
|
|
39
|
+
BackendApplicationServer, BackendApplicationPath, RootContainer
|
|
40
|
+
} from './backend-application';
|
|
38
41
|
import { CliManager, CliContribution } from './cli';
|
|
39
42
|
import { IPCConnectionProvider } from './messaging';
|
|
40
43
|
import { ApplicationServerImpl } from './application-server';
|
|
@@ -87,6 +90,13 @@ export const backendApplicationModule = new ContainerModule(bind => {
|
|
|
87
90
|
bind(CliContribution).toService(BackendApplicationCliContribution);
|
|
88
91
|
|
|
89
92
|
bind(BackendApplication).toSelf().inSingletonScope();
|
|
93
|
+
bind(RootContainer).toDynamicValue(({ container }) => {
|
|
94
|
+
let root = container;
|
|
95
|
+
while (root.parent) {
|
|
96
|
+
root = root.parent;
|
|
97
|
+
}
|
|
98
|
+
return root;
|
|
99
|
+
}).inSingletonScope();
|
|
90
100
|
bindRootContributionProvider(bind, BackendApplicationContribution);
|
|
91
101
|
// Bind the BackendApplicationServer as a BackendApplicationContribution
|
|
92
102
|
// and fallback to an empty contribution if never bound.
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource 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 { expect } from 'chai';
|
|
18
|
+
import * as sinon from 'sinon';
|
|
19
|
+
import { Container, ContainerModule, injectable, preDestroy } from 'inversify';
|
|
20
|
+
import { bindContributionProvider, ILogger, Stopwatch } from '../common';
|
|
21
|
+
import { Deferred } from '../common/promise-util';
|
|
22
|
+
import { MockLogger } from '../common/test/mock-logger';
|
|
23
|
+
import { NodeStopwatch } from './performance/node-stopwatch';
|
|
24
|
+
import { ProcessUtils } from './process-utils';
|
|
25
|
+
import {
|
|
26
|
+
BackendApplication,
|
|
27
|
+
BackendApplicationCliContribution,
|
|
28
|
+
BackendApplicationContribution,
|
|
29
|
+
RootContainer
|
|
30
|
+
} from './backend-application';
|
|
31
|
+
import { CliContribution } from './cli';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Test subclass that exposes the protected `gracefulShutdown` for direct testing.
|
|
35
|
+
*/
|
|
36
|
+
@injectable()
|
|
37
|
+
class TestBackendApplication extends BackendApplication {
|
|
38
|
+
public invokeGracefulShutdown(): Promise<void> {
|
|
39
|
+
return this.gracefulShutdown();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// All process events on which `BackendApplication` installs listeners in its
|
|
44
|
+
// constructor. We snapshot and restore them around each test to avoid leaking
|
|
45
|
+
// listeners across tests (and triggering `MaxListenersExceededWarning`).
|
|
46
|
+
const PROCESS_EVENTS = ['SIGINT', 'SIGTERM', 'SIGPIPE', 'exit', 'uncaughtException'] as const;
|
|
47
|
+
type ProcessEventName = typeof PROCESS_EVENTS[number];
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
type AnyListener = (...args: any[]) => void;
|
|
50
|
+
|
|
51
|
+
describe('BackendApplication', () => {
|
|
52
|
+
let sandbox: sinon.SinonSandbox;
|
|
53
|
+
let exitStub: sinon.SinonStub;
|
|
54
|
+
let savedListeners: Partial<Record<ProcessEventName, AnyListener[]>>;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
sandbox = sinon.createSandbox();
|
|
58
|
+
|
|
59
|
+
// Snapshot any existing listeners so we can restore after each test
|
|
60
|
+
// (BackendApplication installs its own as part of construction).
|
|
61
|
+
savedListeners = {};
|
|
62
|
+
for (const evt of PROCESS_EVENTS) {
|
|
63
|
+
savedListeners[evt] = [...process.listeners(evt as NodeJS.Signals)] as AnyListener[];
|
|
64
|
+
process.removeAllListeners(evt);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
exitStub = sandbox.stub(process, 'exit');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
for (const evt of PROCESS_EVENTS) {
|
|
72
|
+
process.removeAllListeners(evt);
|
|
73
|
+
for (const listener of savedListeners[evt] ?? []) {
|
|
74
|
+
process.on(evt, listener);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sandbox.restore();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function createTestContainer(): Container {
|
|
82
|
+
const container = new Container();
|
|
83
|
+
|
|
84
|
+
container.bind(RootContainer).toConstantValue(container);
|
|
85
|
+
|
|
86
|
+
container.bind(ILogger).to(MockLogger).inSingletonScope();
|
|
87
|
+
container.bind(Stopwatch).to(NodeStopwatch).inSingletonScope();
|
|
88
|
+
container.bind(ProcessUtils).toSelf().inSingletonScope();
|
|
89
|
+
container.bind(BackendApplicationCliContribution).toSelf().inSingletonScope();
|
|
90
|
+
container.bind(CliContribution).toService(BackendApplicationCliContribution);
|
|
91
|
+
bindContributionProvider(container, BackendApplicationContribution);
|
|
92
|
+
|
|
93
|
+
container.bind(TestBackendApplication).toSelf().inSingletonScope();
|
|
94
|
+
container.bind(BackendApplication).toService(TestBackendApplication);
|
|
95
|
+
|
|
96
|
+
return container;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('graceful shutdown', () => {
|
|
100
|
+
|
|
101
|
+
it('runs @preDestroy on root-scoped singletons before exiting with code 1', async () => {
|
|
102
|
+
let canaryDisposed = false;
|
|
103
|
+
|
|
104
|
+
@injectable()
|
|
105
|
+
class Canary {
|
|
106
|
+
@preDestroy()
|
|
107
|
+
protected onPreDestroy(): void {
|
|
108
|
+
canaryDisposed = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const container = createTestContainer();
|
|
113
|
+
const canaryModule = new ContainerModule(bind => {
|
|
114
|
+
bind(Canary).toSelf().inSingletonScope();
|
|
115
|
+
});
|
|
116
|
+
container.load(canaryModule);
|
|
117
|
+
container.get(Canary);
|
|
118
|
+
|
|
119
|
+
const app = container.get(TestBackendApplication);
|
|
120
|
+
|
|
121
|
+
await app.invokeGracefulShutdown();
|
|
122
|
+
|
|
123
|
+
expect(canaryDisposed, '@preDestroy was not invoked on root-scoped singleton').to.be.true;
|
|
124
|
+
expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called exactly once').to.be.true;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('is idempotent: a second invocation does not unbind the container twice', async () => {
|
|
128
|
+
const container = createTestContainer();
|
|
129
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
130
|
+
|
|
131
|
+
const app = container.get(TestBackendApplication);
|
|
132
|
+
|
|
133
|
+
await app.invokeGracefulShutdown();
|
|
134
|
+
await app.invokeGracefulShutdown();
|
|
135
|
+
|
|
136
|
+
expect(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
|
|
137
|
+
expect(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('still exits if container cleanup rejects', async () => {
|
|
141
|
+
const container = createTestContainer();
|
|
142
|
+
const cleanupError = new Error('cleanup boom');
|
|
143
|
+
sandbox.stub(container, 'unbindAllAsync').rejects(cleanupError);
|
|
144
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
145
|
+
|
|
146
|
+
const app = container.get(TestBackendApplication);
|
|
147
|
+
|
|
148
|
+
await app.invokeGracefulShutdown();
|
|
149
|
+
|
|
150
|
+
expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called').to.be.true;
|
|
151
|
+
expect(warnStub.calledOnce, 'a warning should be logged when cleanup rejects').to.be.true;
|
|
152
|
+
expect(warnStub.firstCall.args[0]).to.match(/cleanup boom/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('exits even when container cleanup hangs past the timeout', async () => {
|
|
156
|
+
const clock = sandbox.useFakeTimers();
|
|
157
|
+
const container = createTestContainer();
|
|
158
|
+
sandbox.stub(container, 'unbindAllAsync').returns(new Promise<void>(() => { /* never */ }));
|
|
159
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
160
|
+
|
|
161
|
+
const app = container.get(TestBackendApplication);
|
|
162
|
+
|
|
163
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
164
|
+
|
|
165
|
+
await clock.tickAsync(5001);
|
|
166
|
+
await shutdownPromise;
|
|
167
|
+
|
|
168
|
+
expect(exitStub.calledOnceWith(1), 'process.exit(1) was not called after timeout').to.be.true;
|
|
169
|
+
expect(warnStub.calledOnce, 'a warning should be logged on timeout').to.be.true;
|
|
170
|
+
expect(warnStub.firstCall.args[0]).to.match(/timed out/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('awaits async onStop contributions before unbinding the container', async () => {
|
|
174
|
+
const container = createTestContainer();
|
|
175
|
+
const onStopDeferred = new Deferred<void>();
|
|
176
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
177
|
+
onStop: () => onStopDeferred.promise
|
|
178
|
+
});
|
|
179
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
180
|
+
|
|
181
|
+
const app = container.get(TestBackendApplication);
|
|
182
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
183
|
+
|
|
184
|
+
expect(unbindSpy.called, 'unbindAllAsync should not run before onStop resolves').to.be.false;
|
|
185
|
+
|
|
186
|
+
onStopDeferred.resolve();
|
|
187
|
+
await shutdownPromise;
|
|
188
|
+
|
|
189
|
+
expect(unbindSpy.calledOnce, 'unbindAllAsync should run once onStop completes').to.be.true;
|
|
190
|
+
expect(exitStub.calledOnceWith(1)).to.be.true;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('invokes onStop hooks while injected services are still resolvable', async () => {
|
|
194
|
+
@injectable()
|
|
195
|
+
class Helper {
|
|
196
|
+
readonly value = 'still-bound';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const container = createTestContainer();
|
|
200
|
+
container.bind(Helper).toSelf().inSingletonScope();
|
|
201
|
+
|
|
202
|
+
let observed: string | undefined;
|
|
203
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
204
|
+
onStop: () => {
|
|
205
|
+
observed = container.get(Helper).value;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const app = container.get(TestBackendApplication);
|
|
210
|
+
await app.invokeGracefulShutdown();
|
|
211
|
+
|
|
212
|
+
expect(observed, 'onStop should observe injected services that are still bound').to.equal('still-bound');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('proceeds with shutdown when onStop hooks exceed the timeout', async () => {
|
|
216
|
+
const clock = sandbox.useFakeTimers();
|
|
217
|
+
const container = createTestContainer();
|
|
218
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
219
|
+
onStop: () => new Promise<void>(() => { /* never */ })
|
|
220
|
+
});
|
|
221
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
222
|
+
const warnStub = sandbox.stub(console, 'warn');
|
|
223
|
+
|
|
224
|
+
const app = container.get(TestBackendApplication);
|
|
225
|
+
const shutdownPromise = app.invokeGracefulShutdown();
|
|
226
|
+
|
|
227
|
+
await clock.tickAsync(5001);
|
|
228
|
+
await shutdownPromise;
|
|
229
|
+
|
|
230
|
+
expect(warnStub.calledOnce, 'a warning should be logged on onStop timeout').to.be.true;
|
|
231
|
+
expect(warnStub.firstCall.args[0]).to.match(/Stopping backend contributions/);
|
|
232
|
+
expect(unbindSpy.calledOnce, 'unbind should still run after onStop times out').to.be.true;
|
|
233
|
+
expect(exitStub.calledOnceWith(1)).to.be.true;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('runs all contributions even when one onStop rejects', async () => {
|
|
237
|
+
const container = createTestContainer();
|
|
238
|
+
let secondRan = false;
|
|
239
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
240
|
+
onStop: async () => { throw new Error('boom'); }
|
|
241
|
+
});
|
|
242
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
243
|
+
onStop: () => { secondRan = true; }
|
|
244
|
+
});
|
|
245
|
+
const errorStub = sandbox.stub(console, 'error');
|
|
246
|
+
|
|
247
|
+
const app = container.get(TestBackendApplication);
|
|
248
|
+
await app.invokeGracefulShutdown();
|
|
249
|
+
|
|
250
|
+
expect(secondRan, 'second contribution should still be stopped after first rejects').to.be.true;
|
|
251
|
+
expect(errorStub.calledWithMatch('Could not stop contribution')).to.be.true;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('is idempotent when a contribution re-enters graceful shutdown', async () => {
|
|
255
|
+
const container = createTestContainer();
|
|
256
|
+
// Indirected through a holder so the contribution closure can refer to the
|
|
257
|
+
// application instance that is constructed after the binding is recorded.
|
|
258
|
+
const appHolder: { current?: TestBackendApplication } = {};
|
|
259
|
+
container.bind(BackendApplicationContribution).toConstantValue({
|
|
260
|
+
onStop: () => appHolder.current!.invokeGracefulShutdown()
|
|
261
|
+
});
|
|
262
|
+
const unbindSpy = sandbox.spy(container, 'unbindAllAsync');
|
|
263
|
+
|
|
264
|
+
appHolder.current = container.get(TestBackendApplication);
|
|
265
|
+
await appHolder.current.invokeGracefulShutdown();
|
|
266
|
+
|
|
267
|
+
expect(unbindSpy.callCount, 'unbindAllAsync should be called only once').to.equal(1);
|
|
268
|
+
expect(exitStub.callCount, 'process.exit should be called only once').to.equal(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('process exit handler', () => {
|
|
274
|
+
|
|
275
|
+
it('does not re-invoke contributions after graceful shutdown ran them', async () => {
|
|
276
|
+
const container = createTestContainer();
|
|
277
|
+
const onStopSpy = sandbox.spy();
|
|
278
|
+
container.bind(BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
|
|
279
|
+
const terminateStub = sandbox.stub(ProcessUtils.prototype, 'terminateProcessTree');
|
|
280
|
+
|
|
281
|
+
const app = container.get(TestBackendApplication);
|
|
282
|
+
const exitListener = process.listeners('exit')[0] as () => void;
|
|
283
|
+
|
|
284
|
+
await app.invokeGracefulShutdown();
|
|
285
|
+
expect(onStopSpy.callCount, 'contribution onStop should fire from gracefulShutdown').to.equal(1);
|
|
286
|
+
|
|
287
|
+
exitListener();
|
|
288
|
+
|
|
289
|
+
expect(onStopSpy.callCount, 'contribution onStop should not be invoked a second time').to.equal(1);
|
|
290
|
+
expect(terminateStub.called, 'terminateProcessTree should be invoked by the exit handler').to.be.true;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('invokes contributions synchronously when graceful shutdown was bypassed', () => {
|
|
294
|
+
const container = createTestContainer();
|
|
295
|
+
const onStopSpy = sandbox.spy();
|
|
296
|
+
container.bind(BackendApplicationContribution).toConstantValue({ onStop: onStopSpy });
|
|
297
|
+
const terminateStub = sandbox.stub(ProcessUtils.prototype, 'terminateProcessTree');
|
|
298
|
+
|
|
299
|
+
container.get(TestBackendApplication);
|
|
300
|
+
const exitListener = process.listeners('exit')[0] as () => void;
|
|
301
|
+
|
|
302
|
+
exitListener();
|
|
303
|
+
|
|
304
|
+
expect(onStopSpy.calledOnce, 'sync exit path should still invoke contributions').to.be.true;
|
|
305
|
+
expect(terminateStub.called, 'terminateProcessTree should be invoked').to.be.true;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
});
|