@chidchanun/bcp 0.1.26 → 0.1.28
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/README.md +151 -523
- package/docs/README.md +69 -82
- package/docs/production-hardening.md +152 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/releases/0.1.28.md +134 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +42 -2
- package/packages/server/src/hardening-proxy.ts +393 -0
- package/packages/server/src/production-hardening.ts +374 -0
- package/packages/server/src/standalone-production-runtime-v7.ts +299 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type * as http from "node:http";
|
|
2
|
+
|
|
3
|
+
export interface ProductionHardeningConfig {
|
|
4
|
+
requestTimeoutMs: number;
|
|
5
|
+
headersTimeoutMs: number;
|
|
6
|
+
keepAliveTimeoutMs: number;
|
|
7
|
+
shutdownTimeoutMs: number;
|
|
8
|
+
trustProxy: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GracefulCloseOptions {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ShutdownHookOptions {
|
|
16
|
+
name?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ShutdownHook =
|
|
20
|
+
() => void | Promise<void>;
|
|
21
|
+
|
|
22
|
+
interface RegisteredShutdownHook {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
hook: ShutdownHook;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_REQUEST_TIMEOUT_MS =
|
|
29
|
+
120_000;
|
|
30
|
+
const DEFAULT_HEADERS_TIMEOUT_MS =
|
|
31
|
+
66_000;
|
|
32
|
+
const DEFAULT_KEEP_ALIVE_TIMEOUT_MS =
|
|
33
|
+
65_000;
|
|
34
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS =
|
|
35
|
+
10_000;
|
|
36
|
+
|
|
37
|
+
let nextShutdownHookId =
|
|
38
|
+
1;
|
|
39
|
+
const shutdownHooks =
|
|
40
|
+
new Map<
|
|
41
|
+
number,
|
|
42
|
+
RegisteredShutdownHook
|
|
43
|
+
>();
|
|
44
|
+
|
|
45
|
+
export function getProductionHardeningConfig(
|
|
46
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
47
|
+
): ProductionHardeningConfig {
|
|
48
|
+
const keepAliveTimeoutMs =
|
|
49
|
+
parsePositiveInteger(
|
|
50
|
+
environment.BCP_KEEP_ALIVE_TIMEOUT_MS,
|
|
51
|
+
"BCP_KEEP_ALIVE_TIMEOUT_MS"
|
|
52
|
+
) ??
|
|
53
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT_MS;
|
|
54
|
+
const headersTimeoutMs =
|
|
55
|
+
parsePositiveInteger(
|
|
56
|
+
environment.BCP_HEADERS_TIMEOUT_MS,
|
|
57
|
+
"BCP_HEADERS_TIMEOUT_MS"
|
|
58
|
+
) ??
|
|
59
|
+
DEFAULT_HEADERS_TIMEOUT_MS;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
headersTimeoutMs <=
|
|
63
|
+
keepAliveTimeoutMs
|
|
64
|
+
) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"BCP Framework: BCP_HEADERS_TIMEOUT_MS must be greater than BCP_KEEP_ALIVE_TIMEOUT_MS."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
requestTimeoutMs:
|
|
72
|
+
parsePositiveInteger(
|
|
73
|
+
environment.BCP_REQUEST_TIMEOUT_MS,
|
|
74
|
+
"BCP_REQUEST_TIMEOUT_MS"
|
|
75
|
+
) ??
|
|
76
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
77
|
+
headersTimeoutMs,
|
|
78
|
+
keepAliveTimeoutMs,
|
|
79
|
+
shutdownTimeoutMs:
|
|
80
|
+
parsePositiveInteger(
|
|
81
|
+
environment.BCP_SHUTDOWN_TIMEOUT_MS,
|
|
82
|
+
"BCP_SHUTDOWN_TIMEOUT_MS"
|
|
83
|
+
) ??
|
|
84
|
+
DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
85
|
+
trustProxy:
|
|
86
|
+
parseBoolean(
|
|
87
|
+
environment.BCP_TRUST_PROXY,
|
|
88
|
+
"BCP_TRUST_PROXY"
|
|
89
|
+
) ??
|
|
90
|
+
false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function applyProductionHttpServerHardening(
|
|
95
|
+
server: http.Server,
|
|
96
|
+
config:
|
|
97
|
+
ProductionHardeningConfig =
|
|
98
|
+
getProductionHardeningConfig()
|
|
99
|
+
): void {
|
|
100
|
+
server.requestTimeout =
|
|
101
|
+
config.requestTimeoutMs;
|
|
102
|
+
server.headersTimeout =
|
|
103
|
+
config.headersTimeoutMs;
|
|
104
|
+
server.keepAliveTimeout =
|
|
105
|
+
config.keepAliveTimeoutMs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function closeHttpServerGracefully(
|
|
109
|
+
server: http.Server,
|
|
110
|
+
options: GracefulCloseOptions = {}
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
if (!server.listening) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const timeoutMs =
|
|
117
|
+
options.timeoutMs ??
|
|
118
|
+
getProductionHardeningConfig()
|
|
119
|
+
.shutdownTimeoutMs;
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
!Number.isSafeInteger(
|
|
123
|
+
timeoutMs
|
|
124
|
+
) ||
|
|
125
|
+
timeoutMs <= 0
|
|
126
|
+
) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
"BCP Framework: graceful shutdown timeout must be a positive safe integer."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await new Promise<void>(
|
|
133
|
+
(
|
|
134
|
+
resolve,
|
|
135
|
+
reject
|
|
136
|
+
) => {
|
|
137
|
+
let settled =
|
|
138
|
+
false;
|
|
139
|
+
let timer:
|
|
140
|
+
NodeJS.Timeout | null =
|
|
141
|
+
null;
|
|
142
|
+
const settle =
|
|
143
|
+
(
|
|
144
|
+
error?: Error
|
|
145
|
+
) => {
|
|
146
|
+
if (settled) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
settled =
|
|
151
|
+
true;
|
|
152
|
+
|
|
153
|
+
if (timer) {
|
|
154
|
+
clearTimeout(
|
|
155
|
+
timer
|
|
156
|
+
);
|
|
157
|
+
timer =
|
|
158
|
+
null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (error) {
|
|
162
|
+
reject(
|
|
163
|
+
error
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
timer =
|
|
172
|
+
setTimeout(
|
|
173
|
+
() => {
|
|
174
|
+
try {
|
|
175
|
+
server.closeAllConnections?.();
|
|
176
|
+
} catch {
|
|
177
|
+
// The close callback remains authoritative.
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
timeoutMs
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
timer.unref?.();
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
server.closeIdleConnections?.();
|
|
187
|
+
server.close(
|
|
188
|
+
(error) => {
|
|
189
|
+
settle(
|
|
190
|
+
error ??
|
|
191
|
+
undefined
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
settle(
|
|
197
|
+
error instanceof Error
|
|
198
|
+
? error
|
|
199
|
+
: new Error(
|
|
200
|
+
String(error)
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function registerShutdownHook(
|
|
209
|
+
hook: ShutdownHook,
|
|
210
|
+
options: ShutdownHookOptions = {}
|
|
211
|
+
): () => void {
|
|
212
|
+
if (
|
|
213
|
+
typeof hook !==
|
|
214
|
+
"function"
|
|
215
|
+
) {
|
|
216
|
+
throw new TypeError(
|
|
217
|
+
"BCP Framework: shutdown hook must be a function."
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const id =
|
|
222
|
+
nextShutdownHookId++;
|
|
223
|
+
const name =
|
|
224
|
+
normalizeHookName(
|
|
225
|
+
options.name,
|
|
226
|
+
id
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
shutdownHooks.set(
|
|
230
|
+
id,
|
|
231
|
+
{
|
|
232
|
+
id,
|
|
233
|
+
name,
|
|
234
|
+
hook,
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
shutdownHooks.delete(
|
|
240
|
+
id
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function runShutdownHooks(): Promise<void> {
|
|
246
|
+
const registered =
|
|
247
|
+
Array.from(
|
|
248
|
+
shutdownHooks.values()
|
|
249
|
+
).reverse();
|
|
250
|
+
const failures:
|
|
251
|
+
Error[] = [];
|
|
252
|
+
|
|
253
|
+
shutdownHooks.clear();
|
|
254
|
+
|
|
255
|
+
for (const item of registered) {
|
|
256
|
+
try {
|
|
257
|
+
await item.hook();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
failures.push(
|
|
260
|
+
new Error(
|
|
261
|
+
`BCP Framework: shutdown hook "${item.name}" failed. ${error instanceof Error ? error.message : String(error)}`
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
failures.length > 0
|
|
269
|
+
) {
|
|
270
|
+
throw new AggregateError(
|
|
271
|
+
failures,
|
|
272
|
+
"BCP Framework: one or more shutdown hooks failed."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function isTrustedProxyEnabled(
|
|
278
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
279
|
+
): boolean {
|
|
280
|
+
return getProductionHardeningConfig(
|
|
281
|
+
environment
|
|
282
|
+
).trustProxy;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeHookName(
|
|
286
|
+
value: string | undefined,
|
|
287
|
+
id: number
|
|
288
|
+
): string {
|
|
289
|
+
const normalized =
|
|
290
|
+
value?.trim();
|
|
291
|
+
|
|
292
|
+
if (!normalized) {
|
|
293
|
+
return `hook-${id}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
normalized.length > 128 ||
|
|
298
|
+
/[\r\n]/.test(
|
|
299
|
+
normalized
|
|
300
|
+
)
|
|
301
|
+
) {
|
|
302
|
+
throw new TypeError(
|
|
303
|
+
"BCP Framework: shutdown hook name must be at most 128 characters without line breaks."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return normalized;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parsePositiveInteger(
|
|
311
|
+
value: string | undefined,
|
|
312
|
+
name: string
|
|
313
|
+
): number | undefined {
|
|
314
|
+
if (value === undefined) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const normalized =
|
|
319
|
+
value.trim();
|
|
320
|
+
const parsed =
|
|
321
|
+
Number(
|
|
322
|
+
normalized
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
normalized.length === 0 ||
|
|
327
|
+
!Number.isSafeInteger(
|
|
328
|
+
parsed
|
|
329
|
+
) ||
|
|
330
|
+
parsed <= 0
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`BCP Framework: ${name} must be a positive safe integer.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return parsed;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseBoolean(
|
|
341
|
+
value: string | undefined,
|
|
342
|
+
name: string
|
|
343
|
+
): boolean | undefined {
|
|
344
|
+
if (value === undefined) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const normalized =
|
|
349
|
+
value
|
|
350
|
+
.trim()
|
|
351
|
+
.toLowerCase();
|
|
352
|
+
|
|
353
|
+
if (
|
|
354
|
+
normalized === "1" ||
|
|
355
|
+
normalized === "true" ||
|
|
356
|
+
normalized === "yes" ||
|
|
357
|
+
normalized === "on"
|
|
358
|
+
) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
normalized === "0" ||
|
|
364
|
+
normalized === "false" ||
|
|
365
|
+
normalized === "no" ||
|
|
366
|
+
normalized === "off"
|
|
367
|
+
) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
throw new Error(
|
|
372
|
+
`BCP Framework: ${name} must be one of 1, 0, true, false, yes, no, on or off.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createHardeningProxy,
|
|
5
|
+
} from "./hardening-proxy.js";
|
|
6
|
+
import {
|
|
7
|
+
getProductionHardeningConfig,
|
|
8
|
+
runShutdownHooks,
|
|
9
|
+
} from "./production-hardening.js";
|
|
10
|
+
import {
|
|
11
|
+
createStandaloneProductionServer as createBaseStandaloneProductionServer,
|
|
12
|
+
type StandaloneApiRoute,
|
|
13
|
+
type StandalonePageRoute,
|
|
14
|
+
type StandaloneProductionServerOptions as BaseStandaloneProductionServerOptions,
|
|
15
|
+
} from "./standalone-production-runtime-v6.js";
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
StandaloneApiRoute,
|
|
19
|
+
StandalonePageRoute,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface StandaloneProductionServerOptions
|
|
23
|
+
extends BaseStandaloneProductionServerOptions {}
|
|
24
|
+
|
|
25
|
+
export function createStandaloneProductionServer(
|
|
26
|
+
options: StandaloneProductionServerOptions
|
|
27
|
+
) {
|
|
28
|
+
let baseServer:
|
|
29
|
+
ReturnType<
|
|
30
|
+
typeof createBaseStandaloneProductionServer
|
|
31
|
+
> | null =
|
|
32
|
+
null;
|
|
33
|
+
let hardeningProxy:
|
|
34
|
+
ReturnType<
|
|
35
|
+
typeof createHardeningProxy
|
|
36
|
+
> | null =
|
|
37
|
+
null;
|
|
38
|
+
let started =
|
|
39
|
+
false;
|
|
40
|
+
let stopping =
|
|
41
|
+
false;
|
|
42
|
+
|
|
43
|
+
const handleSigterm =
|
|
44
|
+
() => {
|
|
45
|
+
void shutdownFromSignal(
|
|
46
|
+
"SIGTERM"
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
const handleSigint =
|
|
50
|
+
() => {
|
|
51
|
+
void shutdownFromSignal(
|
|
52
|
+
"SIGINT"
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
async function start(): Promise<void> {
|
|
57
|
+
if (started) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const config =
|
|
62
|
+
getProductionHardeningConfig();
|
|
63
|
+
const internalPort =
|
|
64
|
+
await findFreePort();
|
|
65
|
+
|
|
66
|
+
baseServer =
|
|
67
|
+
createBaseStandaloneProductionServer({
|
|
68
|
+
...options,
|
|
69
|
+
port:
|
|
70
|
+
internalPort,
|
|
71
|
+
hostname:
|
|
72
|
+
"127.0.0.1",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await baseServer.start();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
hardeningProxy =
|
|
79
|
+
createHardeningProxy({
|
|
80
|
+
port:
|
|
81
|
+
options.port,
|
|
82
|
+
hostname:
|
|
83
|
+
options.hostname,
|
|
84
|
+
upstreamPort:
|
|
85
|
+
internalPort,
|
|
86
|
+
upstreamHostname:
|
|
87
|
+
"127.0.0.1",
|
|
88
|
+
});
|
|
89
|
+
await hardeningProxy.start();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
await baseServer.stop();
|
|
92
|
+
baseServer =
|
|
93
|
+
null;
|
|
94
|
+
hardeningProxy =
|
|
95
|
+
null;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
process.once(
|
|
100
|
+
"SIGTERM",
|
|
101
|
+
handleSigterm
|
|
102
|
+
);
|
|
103
|
+
process.once(
|
|
104
|
+
"SIGINT",
|
|
105
|
+
handleSigint
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
started =
|
|
109
|
+
true;
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
`[BCP Runtime] Hardening gateway: http://${options.hostname}:${options.port}`
|
|
113
|
+
);
|
|
114
|
+
console.log(
|
|
115
|
+
`[BCP Runtime] Request timeout: ${config.requestTimeoutMs}ms | Shutdown timeout: ${config.shutdownTimeoutMs}ms | Trust proxy: ${config.trustProxy ? "on" : "off"}`
|
|
116
|
+
);
|
|
117
|
+
console.log("");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function stop(): Promise<void> {
|
|
121
|
+
if (stopping) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
stopping =
|
|
126
|
+
true;
|
|
127
|
+
|
|
128
|
+
process.off(
|
|
129
|
+
"SIGTERM",
|
|
130
|
+
handleSigterm
|
|
131
|
+
);
|
|
132
|
+
process.off(
|
|
133
|
+
"SIGINT",
|
|
134
|
+
handleSigint
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const failures:
|
|
138
|
+
unknown[] = [];
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (hardeningProxy) {
|
|
142
|
+
await hardeningProxy.stop();
|
|
143
|
+
hardeningProxy =
|
|
144
|
+
null;
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
failures.push(
|
|
148
|
+
error
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await runShutdownHooks();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
failures.push(
|
|
156
|
+
error
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
if (baseServer) {
|
|
162
|
+
await baseServer.stop();
|
|
163
|
+
baseServer =
|
|
164
|
+
null;
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
failures.push(
|
|
168
|
+
error
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
started =
|
|
173
|
+
false;
|
|
174
|
+
stopping =
|
|
175
|
+
false;
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
failures.length > 0
|
|
179
|
+
) {
|
|
180
|
+
throw new AggregateError(
|
|
181
|
+
failures,
|
|
182
|
+
"BCP Framework: production shutdown completed with errors."
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function shutdownFromSignal(
|
|
188
|
+
signal: NodeJS.Signals
|
|
189
|
+
): Promise<void> {
|
|
190
|
+
if (stopping) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(
|
|
195
|
+
`[BCP Runtime] ${signal} received. Starting graceful shutdown...`
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await stop();
|
|
200
|
+
console.log(
|
|
201
|
+
"[BCP Runtime] Graceful shutdown complete."
|
|
202
|
+
);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
process.exitCode =
|
|
205
|
+
1;
|
|
206
|
+
console.error(
|
|
207
|
+
"[BCP Runtime] Graceful shutdown failed:",
|
|
208
|
+
error
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
start,
|
|
215
|
+
stop,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function findFreePort(): Promise<number> {
|
|
220
|
+
const server =
|
|
221
|
+
net.createServer();
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await new Promise<void>(
|
|
225
|
+
(
|
|
226
|
+
resolve,
|
|
227
|
+
reject
|
|
228
|
+
) => {
|
|
229
|
+
const onError =
|
|
230
|
+
(error: Error) => {
|
|
231
|
+
server.off(
|
|
232
|
+
"listening",
|
|
233
|
+
onListening
|
|
234
|
+
);
|
|
235
|
+
reject(
|
|
236
|
+
error
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
const onListening =
|
|
240
|
+
() => {
|
|
241
|
+
server.off(
|
|
242
|
+
"error",
|
|
243
|
+
onError
|
|
244
|
+
);
|
|
245
|
+
resolve();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
server.once(
|
|
249
|
+
"error",
|
|
250
|
+
onError
|
|
251
|
+
);
|
|
252
|
+
server.once(
|
|
253
|
+
"listening",
|
|
254
|
+
onListening
|
|
255
|
+
);
|
|
256
|
+
server.listen(
|
|
257
|
+
0,
|
|
258
|
+
"127.0.0.1"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const address =
|
|
264
|
+
server.address();
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
!address ||
|
|
268
|
+
typeof address ===
|
|
269
|
+
"string"
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"BCP Framework: could not allocate production hardening port."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return address.port;
|
|
277
|
+
} finally {
|
|
278
|
+
if (server.listening) {
|
|
279
|
+
await new Promise<void>(
|
|
280
|
+
(
|
|
281
|
+
resolve,
|
|
282
|
+
reject
|
|
283
|
+
) => {
|
|
284
|
+
server.close(
|
|
285
|
+
(error) => {
|
|
286
|
+
if (error) {
|
|
287
|
+
reject(
|
|
288
|
+
error
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
resolve();
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|