/
backend-application.ts
344 lines (297 loc) · 13.1 KB
/
backend-application.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import * as express from 'express';
import * as yargs from 'yargs';
import * as fs from 'fs-extra';
import { performance, PerformanceObserver } from 'perf_hooks';
import { inject, named, injectable, postConstruct } from 'inversify';
import { ContributionProvider, MaybePromise } from '../common';
import { CliContribution } from './cli';
import { Deferred } from '../common/promise-util';
import { environment } from '../common/index';
import { AddressInfo } from 'net';
import { ApplicationPackage } from '@theia/application-package';
export const BackendApplicationContribution = Symbol('BackendApplicationContribution');
/**
* Contribution for hooking into the backend lifecycle.
*/
export interface BackendApplicationContribution {
/**
* Called during the initialization of the backend application.
* Use this for functionality which has to run as early as possible.
*
* The implementation may be async, however it will still block the
* initialization step until it's resolved.
*
* @returns either `undefined` or a Promise resolving to `undefined`.
*/
initialize?(): MaybePromise<void>;
/**
* Called after the initialization of the backend application is complete.
* Use this to configure the Express app before it is started, for example
* to offer additional endpoints.
*
* The implementation may be async, however it will still block the
* configuration step until it's resolved.
*
* @param app the express application to configure.
*
* @returns either `undefined` or a Promise resolving to `undefined`.
*/
configure?(app: express.Application): MaybePromise<void>;
/**
* Called right after the server for the Express app is started.
* Use this to additionally configure the server or as ready-signal for your service.
*
* The implementation may be async, however it will still block the
* startup step until it's resolved.
*
* @param server the backend server running the express app.
*
* @returns either `undefined` or a Promise resolving to `undefined`.
*/
onStart?(server: http.Server | https.Server): MaybePromise<void>;
/**
* Called when the backend application shuts down. Contributions must perform only synchronous operations.
* Any kind of additional asynchronous work queued in the event loop will be ignored and abandoned.
*
* @param app the express application.
*/
onStop?(app?: express.Application): void;
}
const defaultPort = environment.electron.is() ? 0 : 3000;
const defaultHost = 'localhost';
const defaultSSL = false;
const appProjectPath = 'app-project-path';
const TIMER_WARNING_THRESHOLD = 50;
@injectable()
export class BackendApplicationCliContribution implements CliContribution {
port: number;
hostname: string | undefined;
ssl: boolean | undefined;
cert: string | undefined;
certkey: string | undefined;
projectPath: string;
configure(conf: yargs.Argv): void {
conf.option('port', { alias: 'p', description: 'The port the backend server listens on.', type: 'number', default: defaultPort });
conf.option('hostname', { alias: 'h', description: 'The allowed hostname for connections.', type: 'string', default: defaultHost });
conf.option('ssl', { description: 'Use SSL (HTTPS), cert and certkey must also be set', type: 'boolean', default: defaultSSL });
conf.option('cert', { description: 'Path to SSL certificate.', type: 'string' });
conf.option('certkey', { description: 'Path to SSL certificate key.', type: 'string' });
conf.option(appProjectPath, { description: 'Sets the application project directory', default: this.appProjectPath() });
}
setArguments(args: yargs.Arguments): void {
this.port = args.port as number;
this.hostname = args.hostname as string;
this.ssl = args.ssl as boolean;
this.cert = args.cert as string;
this.certkey = args.certkey as string;
this.projectPath = args[appProjectPath] as string;
}
protected appProjectPath(): string {
if (environment.electron.is()) {
if (process.env.THEIA_APP_PROJECT_PATH) {
return process.env.THEIA_APP_PROJECT_PATH;
}
throw new Error('The \'THEIA_APP_PROJECT_PATH\' environment variable must be set when running in electron.');
}
return process.cwd();
}
}
/**
* The main entry point for Theia applications.
*/
@injectable()
export class BackendApplication {
protected readonly app: express.Application = express();
@inject(ApplicationPackage)
protected readonly applicationPackage: ApplicationPackage;
private readonly _performanceObserver: PerformanceObserver;
constructor(
@inject(ContributionProvider) @named(BackendApplicationContribution)
protected readonly contributionsProvider: ContributionProvider<BackendApplicationContribution>,
@inject(BackendApplicationCliContribution) protected readonly cliParams: BackendApplicationCliContribution
) {
process.on('uncaughtException', error => {
if (error) {
console.error('Uncaught Exception: ', error.toString());
if (error.stack) {
console.error(error.stack);
}
}
});
// Workaround for Electron not installing a handler to ignore SIGPIPE error
// (https://github.com/electron/electron/issues/13254)
process.on('SIGPIPE', () => {
console.error(new Error('Unexpected SIGPIPE'));
});
// Handles normal process termination.
process.on('exit', () => this.onStop());
// Handles `Ctrl+C`.
process.on('SIGINT', () => process.exit(0));
// Handles `kill pid`.
process.on('SIGTERM', () => process.exit(0));
// Create performance observer
this._performanceObserver = new PerformanceObserver(list => {
for (const item of list.getEntries()) {
const contribution = `Backend ${item.name}`;
if (item.duration > TIMER_WARNING_THRESHOLD) {
console.warn(`${contribution} is slow, took: ${item.duration.toFixed(1)} ms`);
} else {
console.debug(`${contribution} took: ${item.duration.toFixed(1)} ms`);
}
}
});
this._performanceObserver.observe({
entryTypes: ['measure']
});
this.initialize();
}
protected async initialize(): Promise<void> {
for (const contribution of this.contributionsProvider.getContributions()) {
if (contribution.initialize) {
try {
await this.measure(contribution.constructor.name + '.initialize',
() => contribution.initialize!()
);
} catch (error) {
console.error('Could not initialize contribution', error);
}
}
}
}
@postConstruct()
protected async configure(): Promise<void> {
this.app.get('*.js', this.serveGzipped.bind(this, 'text/javascript'));
this.app.get('*.js.map', this.serveGzipped.bind(this, 'application/json'));
this.app.get('*.css', this.serveGzipped.bind(this, 'text/css'));
this.app.get('*.wasm', this.serveGzipped.bind(this, 'application/wasm'));
this.app.get('*.gif', this.serveGzipped.bind(this, 'image/gif'));
this.app.get('*.png', this.serveGzipped.bind(this, 'image/png'));
this.app.get('*.svg', this.serveGzipped.bind(this, 'image/svg+xml'));
for (const contribution of this.contributionsProvider.getContributions()) {
if (contribution.configure) {
try {
await this.measure(contribution.constructor.name + '.configure',
() => contribution.configure!(this.app)
);
} catch (error) {
console.error('Could not configure contribution', error);
}
}
}
}
use(...handlers: express.Handler[]): void {
this.app.use(...handlers);
}
async start(aPort?: number, aHostname?: string): Promise<http.Server | https.Server> {
const hostname = aHostname !== undefined ? aHostname : this.cliParams.hostname;
const port = aPort !== undefined ? aPort : this.cliParams.port;
const deferred = new Deferred<http.Server | https.Server>();
let server: http.Server | https.Server;
if (this.cliParams.ssl) {
if (this.cliParams.cert === undefined) {
throw new Error('Missing --cert option, see --help for usage');
}
if (this.cliParams.certkey === undefined) {
throw new Error('Missing --certkey option, see --help for usage');
}
let key: Buffer;
let cert: Buffer;
try {
key = await fs.readFile(this.cliParams.certkey as string);
} catch (err) {
console.error("Can't read certificate key");
throw err;
}
try {
cert = await fs.readFile(this.cliParams.cert as string);
} catch (err) {
console.error("Can't read certificate");
throw err;
}
server = https.createServer({ key, cert }, this.app);
} else {
server = http.createServer(this.app);
}
server.on('error', error => {
deferred.reject(error);
/* The backend might run in a separate process,
* so we defer `process.exit` to let time for logging in the parent process */
setTimeout(process.exit, 0, 1);
});
server.listen(port, hostname, () => {
const scheme = this.cliParams.ssl ? 'https' : 'http';
console.info(`Theia app listening on ${scheme}://${hostname || 'localhost'}:${(server.address() as AddressInfo).port}.`);
deferred.resolve(server);
});
/* Allow any number of websocket servers. */
server.setMaxListeners(0);
for (const contribution of this.contributionsProvider.getContributions()) {
if (contribution.onStart) {
try {
await this.measure(contribution.constructor.name + '.onStart',
() => contribution.onStart!(server)
);
} catch (error) {
console.error('Could not start contribution', error);
}
}
}
return deferred.promise;
}
protected onStop(): void {
console.info('>>> Stopping backend contributions...');
for (const contrib of this.contributionsProvider.getContributions()) {
if (contrib.onStop) {
try {
contrib.onStop(this.app);
} catch (error) {
console.error('Could not stop contribution', error);
}
}
}
console.info('<<< All backend contributions have been stopped.');
this._performanceObserver.disconnect();
}
protected async serveGzipped(contentType: string, req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> {
const acceptedEncodings = req.acceptsEncodings();
const gzUrl = `${req.url}.gz`;
const gzPath = path.join(this.applicationPackage.projectPath, 'lib', gzUrl);
if (acceptedEncodings.indexOf('gzip') === -1 || !(await fs.pathExists(gzPath))) {
next();
return;
}
req.url = gzUrl;
res.set('Content-Encoding', 'gzip');
res.set('Content-Type', contentType);
next();
}
protected async measure<T>(name: string, fn: () => MaybePromise<T>): Promise<T> {
const startMark = name + '-start';
const endMark = name + '-end';
performance.mark(startMark);
const result = await fn();
performance.mark(endMark);
performance.measure(name, startMark, endMark);
// Observer should immediately log the measurement, so we can clear it
performance.clearMarks(name);
return result;
}
}