@milaboratories/pl-deployments 2.17.18 → 3.0.0
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/dist/local/pl.cjs +56 -4
- package/dist/local/pl.cjs.map +1 -1
- package/dist/local/pl.d.ts +30 -2
- package/dist/local/pl.d.ts.map +1 -1
- package/dist/local/pl.js +57 -5
- package/dist/local/pl.js.map +1 -1
- package/package.json +5 -4
- package/src/local/pl.test.ts +7 -7
- package/src/local/pl.ts +92 -2
package/dist/local/pl.cjs
CHANGED
|
@@ -12,6 +12,7 @@ let node_os = require("node:os");
|
|
|
12
12
|
node_os = require_runtime.__toESM(node_os);
|
|
13
13
|
let _milaboratories_pl_http = require("@milaboratories/pl-http");
|
|
14
14
|
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
15
|
+
let _milaboratories_pl_healthcheck = require("@milaboratories/pl-healthcheck");
|
|
15
16
|
//#region src/local/pl.ts
|
|
16
17
|
const LocalConfigYaml = "config-local.yaml";
|
|
17
18
|
/**
|
|
@@ -25,11 +26,12 @@ var LocalPl = class {
|
|
|
25
26
|
nRuns = 0;
|
|
26
27
|
lastRunHistory = {};
|
|
27
28
|
wasStopped = false;
|
|
28
|
-
constructor(logger, workingDir, startOptions, initialStartHistory, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
|
|
29
|
+
constructor(logger, workingDir, startOptions, initialStartHistory, grpcPort, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
|
|
29
30
|
this.logger = logger;
|
|
30
31
|
this.workingDir = workingDir;
|
|
31
32
|
this.startOptions = startOptions;
|
|
32
33
|
this.initialStartHistory = initialStartHistory;
|
|
34
|
+
this.grpcPort = grpcPort;
|
|
33
35
|
this.onClose = onClose;
|
|
34
36
|
this.onError = onError;
|
|
35
37
|
this.onCloseAndError = onCloseAndError;
|
|
@@ -71,8 +73,58 @@ var LocalPl = class {
|
|
|
71
73
|
stopped() {
|
|
72
74
|
return this.wasStopped;
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Polls the OS until the backend process is observed alive.
|
|
78
|
+
* Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.
|
|
79
|
+
*/
|
|
80
|
+
async isAlive(opts) {
|
|
81
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
|
|
82
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
83
|
+
while (true) {
|
|
84
|
+
if (Date.now() >= deadline) throw new Error("Timeout waiting for backend process to be alive");
|
|
85
|
+
await (0, _milaboratories_ts_helpers.sleep)(pollMs);
|
|
86
|
+
if (!this.pid) continue;
|
|
87
|
+
if (await require_process.isProcessAlive(this.pid)) return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.
|
|
92
|
+
* Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.
|
|
93
|
+
*
|
|
94
|
+
* Calling this method is an explicit statement that the caller wants a real
|
|
95
|
+
* readiness signal; if `grpcPort` is undefined we throw rather than silently
|
|
96
|
+
* degrade to a process-only check (use {@link isAlive} for that).
|
|
97
|
+
*/
|
|
98
|
+
async isReady(opts) {
|
|
99
|
+
const grpcPort = opts?.grpcPort ?? this.grpcPort;
|
|
100
|
+
if (grpcPort === void 0) throw new Error("isReady requires grpcPort to be configured — caller asked for a real readiness check but no gRPC port is available");
|
|
101
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
|
|
102
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
103
|
+
const requestTimeoutMs = Math.max(500, Math.min(pollMs, 2e3));
|
|
104
|
+
let processAlive = false;
|
|
105
|
+
let wasAlive = false;
|
|
106
|
+
let isFirstCheck = true;
|
|
107
|
+
while (true) {
|
|
108
|
+
if (Date.now() >= deadline) {
|
|
109
|
+
if (!processAlive) throw new Error("Timeout waiting for backend process to be alive");
|
|
110
|
+
throw new Error("Timeout waiting for backend to be ready");
|
|
111
|
+
}
|
|
112
|
+
if (!isFirstCheck) await (0, _milaboratories_ts_helpers.sleep)(pollMs);
|
|
113
|
+
isFirstCheck = false;
|
|
114
|
+
if (!this.pid) continue;
|
|
115
|
+
processAlive = await require_process.isProcessAlive(this.pid);
|
|
116
|
+
wasAlive = wasAlive || processAlive;
|
|
117
|
+
if (!processAlive) {
|
|
118
|
+
if (wasAlive) throw new Error("Backend process died while waiting for readiness");
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
if (await (0, _milaboratories_pl_healthcheck.plHealthCheck)({
|
|
123
|
+
address: `127.0.0.1:${grpcPort}`,
|
|
124
|
+
defaultRequestTimeout: requestTimeoutMs
|
|
125
|
+
}) === _milaboratories_pl_healthcheck.ServingStatus.SERVING) return;
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
76
128
|
}
|
|
77
129
|
debugInfo() {
|
|
78
130
|
return {
|
|
@@ -127,7 +179,7 @@ Only Basic auth is supported by the backend.`);
|
|
|
127
179
|
args: processOpts.args,
|
|
128
180
|
cwd: processOpts.opts.cwd
|
|
129
181
|
});
|
|
130
|
-
const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
|
|
182
|
+
const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.grpcPort, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
|
|
131
183
|
await pl.start();
|
|
132
184
|
return pl;
|
|
133
185
|
});
|
package/dist/local/pl.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pl.cjs","names":["withTrace","processRun","filePid","writePid","processWaitStopped","isProcessAlive","os","fsp","resolveLocalPlBinaryPath","readPid","processStop","newDefaultPlBinarySource"],"sources":["../../src/local/pl.ts"],"sourcesContent":["import type { ProcessOptions } from \"./process\";\nimport { isProcessAlive, processStop, processWaitStopped, processRun } from \"./process\";\nimport type { PlBinarySource } from \"../common/pl_binary\";\nimport { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from \"../common/pl_binary\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { filePid, readPid, writePid } from \"./pid\";\nimport type { Trace } from \"./trace\";\nimport { withTrace } from \"./trace\";\nimport upath from \"upath\";\nimport fsp from \"node:fs/promises\";\nimport type { Required } from \"utility-types\";\nimport * as os from \"node:os\";\nimport type { ProxySettings } from \"@milaboratories/pl-http\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\n\nexport const LocalConfigYaml = \"config-local.yaml\";\n\n/**\n * Represents a local running pl-core,\n * and has methods to start, check if it's running, stop and wait for stopping it.\n * Also, a hook on pl-core closed can be provided.\n */\nexport class LocalPl {\n private instance?: ChildProcess;\n public pid?: number;\n private nRuns: number = 0;\n private lastRunHistory: Trace = {};\n private wasStopped = false;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly workingDir: string,\n private readonly startOptions: ProcessOptions,\n private readonly initialStartHistory: Trace,\n private readonly onClose?: (pl: LocalPl) => Promise<void>,\n private readonly onError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>,\n ) {}\n\n async start() {\n await withTrace(this.logger, async (trace, t) => {\n this.wasStopped = false;\n const instance = processRun(this.logger, this.startOptions);\n instance.on(\"error\", (e: any) => {\n this.logger.error(\n `error '${e}', while running platforma, started opts: ${JSON.stringify(this.debugInfo())}`,\n );\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onError !== undefined) void this.onError(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n instance.on(\"close\", () => {\n this.logger.warn(`platforma was closed, started opts: ${JSON.stringify(this.debugInfo())}`);\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onClose !== undefined) void this.onClose(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n\n trace(\"started\", true);\n\n const pidFile = trace(\"pidFile\", filePid(this.workingDir));\n trace(\"pid\", notEmpty(instance.pid));\n trace(\"pidWritten\", await writePid(pidFile, notEmpty(instance.pid)));\n\n this.nRuns++;\n this.instance = instance;\n this.pid = instance.pid;\n this.lastRunHistory = t;\n });\n }\n\n stop() {\n // TODO use this.instance to stop the process\n this.wasStopped = true;\n processStop(notEmpty(this.pid));\n }\n\n async waitStopped() {\n await processWaitStopped(notEmpty(this.pid), 15000);\n }\n\n stopped() {\n return this.wasStopped;\n }\n\n async isAlive(): Promise<boolean> {\n return await isProcessAlive(notEmpty(this.pid));\n }\n\n debugInfo() {\n return {\n lastRunHistory: this.lastRunHistory,\n nRuns: this.nRuns,\n pid: this.pid,\n workingDir: this.workingDir,\n initialStartHistory: this.initialStartHistory,\n wasStopped: this.wasStopped,\n };\n }\n}\n\n/** Options to start a local pl-core. */\nexport type LocalPlOptions = {\n /** From what directory start a process. */\n readonly workingDir: string;\n /** A string representation of yaml config. */\n readonly config: string;\n /** How to get a binary, download it or get an existing one (default: download latest version) */\n readonly plBinary?: PlBinarySource;\n /** Additional options for a process, environments, stdout, stderr etc. */\n readonly spawnOptions?: SpawnOptions;\n /**\n * If the previous pl-core was started from the same directory,\n * we can check if it's still running and then stop it before starting a new one.\n * (default: true)\n */\n readonly closeOld?: boolean;\n /**\n * Proxy settings to use to fetch the binary and pass it down\n * as a HTTPS_PROXY/HTTP_PROXY environment variable;\n * Backend only supports Basic auth.\n */\n readonly proxy?: ProxySettings;\n\n readonly onClose?: (pl: LocalPl) => Promise<void>;\n readonly onError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>;\n};\n\nexport type LocalPlOptionsFull = Required<LocalPlOptions, \"plBinary\" | \"spawnOptions\" | \"closeOld\">;\n\n/**\n * Starts pl-core, if the option was provided downloads a binary, reads license environments etc.\n */\nexport async function localPlatformaInit(logger: MiLogger, _ops: LocalPlOptions): Promise<LocalPl> {\n // filling-in default values\n\n // Backend could consume a lot of CPU power,\n // we want to keep at least a couple for UI and other apps to work.\n const numCpu = Math.max(os.cpus().length - 2, 1);\n const ops = mergeDefaultOps(_ops, numCpu);\n\n return await withTrace(logger, async (trace, t) => {\n trace(\"startOptions\", { ...ops, config: \"too wordy\" });\n\n const workDir = upath.resolve(ops.workingDir);\n\n if (ops.closeOld) {\n trace(\"closeOld\", await localPlatformaReadPidAndStop(logger, workDir));\n }\n\n const configPath = upath.join(workDir, LocalConfigYaml);\n\n logger.info(`writing configuration '${configPath}'...`);\n await fsp.writeFile(configPath, ops.config);\n\n const plBinPath = upath.join(workDir, \"binaries\");\n const baseBinaryPath = await resolveLocalPlBinaryPath({\n logger,\n downloadDir: plBinPath,\n src: ops.plBinary,\n dispatcher: defaultHttpDispatcher(ops.proxy),\n });\n const binaryPath = trace(\"binaryPath\", upath.join(\"binaries\", baseBinaryPath));\n\n const env = { ...process.env };\n\n if (ops.proxy?.url) {\n const url = new URL(ops.proxy.url);\n if (ops.proxy.auth) {\n const parsed = parseHttpAuth(ops.proxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`\\\nUnsupported auth scheme: ${parsed.scheme}. \\\nOnly Basic auth is supported by the backend.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n env.http_proxy = url.toString();\n env.https_proxy = url.toString();\n }\n\n const processOpts = plProcessOps(binaryPath, configPath, ops, workDir, env);\n trace(\"processOpts\", {\n cmd: processOpts.cmd,\n args: processOpts.args,\n cwd: processOpts.opts.cwd,\n });\n\n const pl = new LocalPl(\n logger,\n ops.workingDir,\n processOpts,\n t,\n ops.onClose,\n ops.onError,\n ops.onCloseAndError,\n ops.onCloseAndErrorNoStop,\n );\n await pl.start();\n\n return pl;\n });\n}\n\n/** Reads a pid of the old pl-core if it was started in the same working directory,\n * and closes it. */\nasync function localPlatformaReadPidAndStop(\n logger: MiLogger,\n workingDir: string,\n): Promise<Record<string, any>> {\n return await withTrace(logger, async (trace, t) => {\n const file = trace(\"pidFilePath\", filePid(workingDir));\n\n const oldPid = trace(\"pid\", await readPid(file));\n const alive = trace(\"wasAlive\", await isProcessAlive(oldPid));\n\n if (oldPid !== undefined && alive) {\n trace(\"stopped\", processStop(oldPid));\n try {\n trace(\"waitStopped\", await processWaitStopped(oldPid, 15_000)); // larger, that 10s we provide to backend in config.\n } catch {\n trace(\"forceStopped\", processStop(oldPid, true));\n trace(\"waitForceStopped\", await processWaitStopped(oldPid, 5_000));\n }\n }\n\n return t;\n });\n}\n\n/** Gets default options for the whole init process\n * and overrides them with the provided options. */\nexport function mergeDefaultOps(ops: LocalPlOptions, numCpu: number): LocalPlOptionsFull {\n const result: {\n plBinary: PlBinarySource;\n spawnOptions: SpawnOptions;\n closeOld: boolean;\n } = {\n plBinary: newDefaultPlBinarySource(),\n spawnOptions: {\n env: {\n GOMAXPROCS: String(numCpu),\n },\n },\n closeOld: true,\n };\n\n if (ops.spawnOptions?.env) {\n result.spawnOptions.env = { ...result.spawnOptions.env, ...ops.spawnOptions.env };\n }\n\n if (ops.spawnOptions) {\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.spawnOptions = { ...result.spawnOptions, ...withoutEnv };\n }\n\n const withoutSpawnOps = { ...ops };\n delete withoutSpawnOps[\"spawnOptions\"];\n\n return { ...result, ...withoutSpawnOps };\n}\n\n/** Gets default options for a platforma local binary\n * and overrides them with the provided options. */\nexport function plProcessOps(\n binaryPath: any,\n configPath: string,\n ops: LocalPlOptionsFull,\n workDir: string,\n defaultEnv: Record<string, string | undefined>,\n): ProcessOptions {\n const result: ProcessOptions = {\n cmd: binaryPath,\n args: [\"--config\", configPath],\n opts: {\n env: { ...defaultEnv },\n cwd: workDir,\n stdio: [\"pipe\", \"ignore\", \"inherit\"],\n windowsHide: true, // hide a terminal on Windows\n },\n };\n\n if (ops.spawnOptions?.env) {\n result.opts.env = { ...result.opts.env, ...ops.spawnOptions.env };\n }\n\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.opts = { ...result.opts, ...withoutEnv };\n\n return result;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pl.cjs","names":["withTrace","processRun","filePid","writePid","processWaitStopped","isProcessAlive","ServingStatus","os","fsp","resolveLocalPlBinaryPath","readPid","processStop","newDefaultPlBinarySource"],"sources":["../../src/local/pl.ts"],"sourcesContent":["import type { ProcessOptions } from \"./process\";\nimport { isProcessAlive, processStop, processWaitStopped, processRun } from \"./process\";\nimport type { PlBinarySource } from \"../common/pl_binary\";\nimport { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from \"../common/pl_binary\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { filePid, readPid, writePid } from \"./pid\";\nimport type { Trace } from \"./trace\";\nimport { withTrace } from \"./trace\";\nimport upath from \"upath\";\nimport fsp from \"node:fs/promises\";\nimport type { Required } from \"utility-types\";\nimport * as os from \"node:os\";\nimport { sleep } from \"@milaboratories/ts-helpers\";\nimport type { ProxySettings } from \"@milaboratories/pl-http\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\nimport { plHealthCheck, ServingStatus } from \"@milaboratories/pl-healthcheck\";\n\nexport const LocalConfigYaml = \"config-local.yaml\";\n\n/**\n * Represents a local running pl-core,\n * and has methods to start, check if it's running, stop and wait for stopping it.\n * Also, a hook on pl-core closed can be provided.\n */\nexport class LocalPl {\n private instance?: ChildProcess;\n public pid?: number;\n private nRuns: number = 0;\n private lastRunHistory: Trace = {};\n private wasStopped = false;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly workingDir: string,\n private readonly startOptions: ProcessOptions,\n private readonly initialStartHistory: Trace,\n private readonly grpcPort?: number,\n private readonly onClose?: (pl: LocalPl) => Promise<void>,\n private readonly onError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>,\n ) {}\n\n async start() {\n await withTrace(this.logger, async (trace, t) => {\n this.wasStopped = false;\n const instance = processRun(this.logger, this.startOptions);\n instance.on(\"error\", (e: any) => {\n this.logger.error(\n `error '${e}', while running platforma, started opts: ${JSON.stringify(this.debugInfo())}`,\n );\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onError !== undefined) void this.onError(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n instance.on(\"close\", () => {\n this.logger.warn(`platforma was closed, started opts: ${JSON.stringify(this.debugInfo())}`);\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onClose !== undefined) void this.onClose(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n\n trace(\"started\", true);\n\n const pidFile = trace(\"pidFile\", filePid(this.workingDir));\n trace(\"pid\", notEmpty(instance.pid));\n trace(\"pidWritten\", await writePid(pidFile, notEmpty(instance.pid)));\n\n this.nRuns++;\n this.instance = instance;\n this.pid = instance.pid;\n this.lastRunHistory = t;\n });\n }\n\n stop() {\n // TODO use this.instance to stop the process\n this.wasStopped = true;\n processStop(notEmpty(this.pid));\n }\n\n async waitStopped() {\n await processWaitStopped(notEmpty(this.pid), 15000);\n }\n\n stopped() {\n return this.wasStopped;\n }\n\n /**\n * Polls the OS until the backend process is observed alive.\n * Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.\n */\n async isAlive(opts?: { timeoutMs?: number; pollIntervalMs?: number }): Promise<void> {\n const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);\n const pollMs = opts?.pollIntervalMs ?? 500;\n\n while (true) {\n if (Date.now() >= deadline) {\n throw new Error(\"Timeout waiting for backend process to be alive\");\n }\n await sleep(pollMs);\n if (!this.pid) continue;\n if (await isProcessAlive(this.pid)) return;\n }\n }\n\n /**\n * Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.\n * Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.\n *\n * Calling this method is an explicit statement that the caller wants a real\n * readiness signal; if `grpcPort` is undefined we throw rather than silently\n * degrade to a process-only check (use {@link isAlive} for that).\n */\n async isReady(opts?: {\n grpcPort?: number;\n timeoutMs?: number;\n pollIntervalMs?: number;\n }): Promise<void> {\n const grpcPort = opts?.grpcPort ?? this.grpcPort;\n\n if (grpcPort === undefined) {\n throw new Error(\n \"isReady requires grpcPort to be configured — caller asked for a real readiness check but no gRPC port is available\",\n );\n }\n\n const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);\n const pollMs = opts?.pollIntervalMs ?? 500;\n const requestTimeoutMs = Math.max(500, Math.min(pollMs, 2_000));\n\n let processAlive = false;\n let wasAlive = false;\n let isFirstCheck = true;\n\n while (true) {\n if (Date.now() >= deadline) {\n if (!processAlive) {\n throw new Error(\"Timeout waiting for backend process to be alive\");\n }\n throw new Error(\"Timeout waiting for backend to be ready\");\n }\n\n if (!isFirstCheck) await sleep(pollMs);\n isFirstCheck = false;\n\n if (!this.pid) continue;\n\n processAlive = await isProcessAlive(this.pid);\n wasAlive = wasAlive || processAlive;\n if (!processAlive) {\n if (wasAlive) {\n // saw the process alive at least once, now it's gone — definitive failure\n throw new Error(\"Backend process died while waiting for readiness\");\n }\n continue;\n }\n\n try {\n const status = await plHealthCheck({\n address: `127.0.0.1:${grpcPort}`,\n defaultRequestTimeout: requestTimeoutMs,\n });\n if (status === ServingStatus.SERVING) return;\n } catch {\n // not yet serving\n }\n }\n }\n\n debugInfo() {\n return {\n lastRunHistory: this.lastRunHistory,\n nRuns: this.nRuns,\n pid: this.pid,\n workingDir: this.workingDir,\n initialStartHistory: this.initialStartHistory,\n wasStopped: this.wasStopped,\n };\n }\n}\n\n/** Options to start a local pl-core. */\nexport type LocalPlOptions = {\n /** From what directory start a process. */\n readonly workingDir: string;\n /** A string representation of yaml config. */\n readonly config: string;\n /** How to get a binary, download it or get an existing one (default: download latest version) */\n readonly plBinary?: PlBinarySource;\n /** Additional options for a process, environments, stdout, stderr etc. */\n readonly spawnOptions?: SpawnOptions;\n /**\n * If the previous pl-core was started from the same directory,\n * we can check if it's still running and then stop it before starting a new one.\n * (default: true)\n */\n readonly closeOld?: boolean;\n /**\n * Proxy settings to use to fetch the binary and pass it down\n * as a HTTPS_PROXY/HTTP_PROXY environment variable;\n * Backend only supports Basic auth.\n */\n readonly proxy?: ProxySettings;\n\n /**\n * gRPC port the backend listens on (always on 127.0.0.1 for local).\n * Required by {@link LocalPl.isReady}, which polls\n * `grpc.health.v1.Health/Check` against this port to distinguish a process\n * that is alive but still initializing from one actually serving gRPC.\n */\n readonly grpcPort?: number;\n\n readonly onClose?: (pl: LocalPl) => Promise<void>;\n readonly onError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>;\n};\n\nexport type LocalPlOptionsFull = Required<LocalPlOptions, \"plBinary\" | \"spawnOptions\" | \"closeOld\">;\n\n/**\n * Starts pl-core, if the option was provided downloads a binary, reads license environments etc.\n */\nexport async function localPlatformaInit(logger: MiLogger, _ops: LocalPlOptions): Promise<LocalPl> {\n // filling-in default values\n\n // Backend could consume a lot of CPU power,\n // we want to keep at least a couple for UI and other apps to work.\n const numCpu = Math.max(os.cpus().length - 2, 1);\n const ops = mergeDefaultOps(_ops, numCpu);\n\n return await withTrace(logger, async (trace, t) => {\n trace(\"startOptions\", { ...ops, config: \"too wordy\" });\n\n const workDir = upath.resolve(ops.workingDir);\n\n if (ops.closeOld) {\n trace(\"closeOld\", await localPlatformaReadPidAndStop(logger, workDir));\n }\n\n const configPath = upath.join(workDir, LocalConfigYaml);\n\n logger.info(`writing configuration '${configPath}'...`);\n await fsp.writeFile(configPath, ops.config);\n\n const plBinPath = upath.join(workDir, \"binaries\");\n const baseBinaryPath = await resolveLocalPlBinaryPath({\n logger,\n downloadDir: plBinPath,\n src: ops.plBinary,\n dispatcher: defaultHttpDispatcher(ops.proxy),\n });\n const binaryPath = trace(\"binaryPath\", upath.join(\"binaries\", baseBinaryPath));\n\n const env = { ...process.env };\n\n if (ops.proxy?.url) {\n const url = new URL(ops.proxy.url);\n if (ops.proxy.auth) {\n const parsed = parseHttpAuth(ops.proxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`\\\nUnsupported auth scheme: ${parsed.scheme}. \\\nOnly Basic auth is supported by the backend.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n env.http_proxy = url.toString();\n env.https_proxy = url.toString();\n }\n\n const processOpts = plProcessOps(binaryPath, configPath, ops, workDir, env);\n trace(\"processOpts\", {\n cmd: processOpts.cmd,\n args: processOpts.args,\n cwd: processOpts.opts.cwd,\n });\n\n const pl = new LocalPl(\n logger,\n ops.workingDir,\n processOpts,\n t,\n ops.grpcPort,\n ops.onClose,\n ops.onError,\n ops.onCloseAndError,\n ops.onCloseAndErrorNoStop,\n );\n await pl.start();\n\n return pl;\n });\n}\n\n/** Reads a pid of the old pl-core if it was started in the same working directory,\n * and closes it. */\nasync function localPlatformaReadPidAndStop(\n logger: MiLogger,\n workingDir: string,\n): Promise<Record<string, any>> {\n return await withTrace(logger, async (trace, t) => {\n const file = trace(\"pidFilePath\", filePid(workingDir));\n\n const oldPid = trace(\"pid\", await readPid(file));\n const alive = trace(\"wasAlive\", await isProcessAlive(oldPid));\n\n if (oldPid !== undefined && alive) {\n trace(\"stopped\", processStop(oldPid));\n try {\n trace(\"waitStopped\", await processWaitStopped(oldPid, 15_000)); // larger, that 10s we provide to backend in config.\n } catch {\n trace(\"forceStopped\", processStop(oldPid, true));\n trace(\"waitForceStopped\", await processWaitStopped(oldPid, 5_000));\n }\n }\n\n return t;\n });\n}\n\n/** Gets default options for the whole init process\n * and overrides them with the provided options. */\nexport function mergeDefaultOps(ops: LocalPlOptions, numCpu: number): LocalPlOptionsFull {\n const result: {\n plBinary: PlBinarySource;\n spawnOptions: SpawnOptions;\n closeOld: boolean;\n } = {\n plBinary: newDefaultPlBinarySource(),\n spawnOptions: {\n env: {\n GOMAXPROCS: String(numCpu),\n },\n },\n closeOld: true,\n };\n\n if (ops.spawnOptions?.env) {\n result.spawnOptions.env = { ...result.spawnOptions.env, ...ops.spawnOptions.env };\n }\n\n if (ops.spawnOptions) {\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.spawnOptions = { ...result.spawnOptions, ...withoutEnv };\n }\n\n const withoutSpawnOps = { ...ops };\n delete withoutSpawnOps[\"spawnOptions\"];\n\n return { ...result, ...withoutSpawnOps };\n}\n\n/** Gets default options for a platforma local binary\n * and overrides them with the provided options. */\nexport function plProcessOps(\n binaryPath: any,\n configPath: string,\n ops: LocalPlOptionsFull,\n workDir: string,\n defaultEnv: Record<string, string | undefined>,\n): ProcessOptions {\n const result: ProcessOptions = {\n cmd: binaryPath,\n args: [\"--config\", configPath],\n opts: {\n env: { ...defaultEnv },\n cwd: workDir,\n stdio: [\"pipe\", \"ignore\", \"inherit\"],\n windowsHide: true, // hide a terminal on Windows\n },\n };\n\n if (ops.spawnOptions?.env) {\n result.opts.env = { ...result.opts.env, ...ops.spawnOptions.env };\n }\n\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.opts = { ...result.opts, ...withoutEnv };\n\n return result;\n}\n"],"mappings":"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"}
|
package/dist/local/pl.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare class LocalPl {
|
|
|
18
18
|
private readonly workingDir;
|
|
19
19
|
private readonly startOptions;
|
|
20
20
|
private readonly initialStartHistory;
|
|
21
|
+
private readonly grpcPort?;
|
|
21
22
|
private readonly onClose?;
|
|
22
23
|
private readonly onError?;
|
|
23
24
|
private readonly onCloseAndError?;
|
|
@@ -27,12 +28,32 @@ declare class LocalPl {
|
|
|
27
28
|
private nRuns;
|
|
28
29
|
private lastRunHistory;
|
|
29
30
|
private wasStopped;
|
|
30
|
-
constructor(logger: MiLogger, workingDir: string, startOptions: ProcessOptions, initialStartHistory: Trace, onClose?: ((pl: LocalPl) => Promise<void>) | undefined, onError?: ((pl: LocalPl) => Promise<void>) | undefined, onCloseAndError?: ((pl: LocalPl) => Promise<void>) | undefined, onCloseAndErrorNoStop?: ((pl: LocalPl) => Promise<void>) | undefined);
|
|
31
|
+
constructor(logger: MiLogger, workingDir: string, startOptions: ProcessOptions, initialStartHistory: Trace, grpcPort?: number | undefined, onClose?: ((pl: LocalPl) => Promise<void>) | undefined, onError?: ((pl: LocalPl) => Promise<void>) | undefined, onCloseAndError?: ((pl: LocalPl) => Promise<void>) | undefined, onCloseAndErrorNoStop?: ((pl: LocalPl) => Promise<void>) | undefined);
|
|
31
32
|
start(): Promise<void>;
|
|
32
33
|
stop(): void;
|
|
33
34
|
waitStopped(): Promise<void>;
|
|
34
35
|
stopped(): boolean;
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Polls the OS until the backend process is observed alive.
|
|
38
|
+
* Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.
|
|
39
|
+
*/
|
|
40
|
+
isAlive(opts?: {
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
pollIntervalMs?: number;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.
|
|
46
|
+
* Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.
|
|
47
|
+
*
|
|
48
|
+
* Calling this method is an explicit statement that the caller wants a real
|
|
49
|
+
* readiness signal; if `grpcPort` is undefined we throw rather than silently
|
|
50
|
+
* degrade to a process-only check (use {@link isAlive} for that).
|
|
51
|
+
*/
|
|
52
|
+
isReady(opts?: {
|
|
53
|
+
grpcPort?: number;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
pollIntervalMs?: number;
|
|
56
|
+
}): Promise<void>;
|
|
36
57
|
debugInfo(): {
|
|
37
58
|
lastRunHistory: Trace;
|
|
38
59
|
nRuns: number;
|
|
@@ -60,6 +81,13 @@ type LocalPlOptions = {
|
|
|
60
81
|
* Backend only supports Basic auth.
|
|
61
82
|
*/
|
|
62
83
|
readonly proxy?: ProxySettings;
|
|
84
|
+
/**
|
|
85
|
+
* gRPC port the backend listens on (always on 127.0.0.1 for local).
|
|
86
|
+
* Required by {@link LocalPl.isReady}, which polls
|
|
87
|
+
* `grpc.health.v1.Health/Check` against this port to distinguish a process
|
|
88
|
+
* that is alive but still initializing from one actually serving gRPC.
|
|
89
|
+
*/
|
|
90
|
+
readonly grpcPort?: number;
|
|
63
91
|
readonly onClose?: (pl: LocalPl) => Promise<void>;
|
|
64
92
|
readonly onError?: (pl: LocalPl) => Promise<void>;
|
|
65
93
|
readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;
|
package/dist/local/pl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pl.d.ts","names":[],"sources":["../../src/local/pl.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"pl.d.ts","names":[],"sources":["../../src/local/pl.ts"],"mappings":";;;;;;;;;cAoBa,eAAA;;AAAb;;;;cAOa,OAAA;EAAA,iBAQQ,MAAA;EAAA,iBACA,UAAA;EAAA,iBACA,YAAA;EAAA,iBACA,mBAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,eAAA;EAAA,iBACA,qBAAA;EAAA,QAfX,QAAA;EACD,GAAA;EAAA,QACC,KAAA;EAAA,QACA,cAAA;EAAA,QACA,UAAA;cAGW,MAAA,EAAQ,QAAA,EACR,UAAA,UACA,YAAA,EAAc,cAAA,EACd,mBAAA,EAAqB,KAAA,EACrB,QAAA,uBACA,OAAA,KAAW,EAAA,EAAI,OAAA,KAAY,OAAA,qBAC3B,OAAA,KAAW,EAAA,EAAI,OAAA,KAAY,OAAA,qBAC3B,eAAA,KAAmB,EAAA,EAAI,OAAA,KAAY,OAAA,qBACnC,qBAAA,KAAyB,EAAA,EAAI,OAAA,KAAY,OAAA;EAGtD,KAAA,CAAA,GAAK,OAAA;EAsCX,IAAA,CAAA;EAMM,WAAA,CAAA,GAAW,OAAA;EAIjB,OAAA,CAAA;;;;;EAQM,OAAA,CAAQ,IAAA;IAAS,SAAA;IAAoB,cAAA;EAAA,IAA4B,OAAA;EA9DpD;;;;;;;;EAoFb,OAAA,CAAQ,IAAA;IACZ,QAAA;IACA,SAAA;IACA,cAAA;EAAA,IACE,OAAA;EAoDJ,SAAA,CAAA;;;;;;;;;;KAaU,cAAA;EAxJoC,oDA0JrC,UAAA,UAzJiC;EAAA,SA2JjC,MAAA,UA3J6C;EAAA,SA6J7C,QAAA,GAAW,cAAA,EA5J4B;EAAA,SA8JvC,YAAA,GAAe,YAAA;EA9JoC;;;;;EAAA,SAoKnD,QAAA;EArHQ;;;;;EAAA,SA2HR,KAAA,GAAQ,aAAA;EA/GsD;;;;;;EAAA,SAuH9D,QAAA;EAAA,SAEA,OAAA,IAAW,EAAA,EAAI,OAAA,KAAY,OAAA;EAAA,SAC3B,OAAA,IAAW,EAAA,EAAI,OAAA,KAAY,OAAA;EAAA,SAC3B,eAAA,IAAmB,EAAA,EAAI,OAAA,KAAY,OAAA;EAAA,SACnC,qBAAA,IAAyB,EAAA,EAAI,OAAA,KAAY,OAAA;AAAA;AAAA,KAGxC,kBAAA,GAAqB,QAAA,CAAS,cAAA;;;;iBAKpB,kBAAA,CAAmB,MAAA,EAAQ,QAAA,EAAU,IAAA,EAAM,cAAA,GAAiB,OAAA,CAAQ,OAAA;;AAzC1F;iBA8IgB,eAAA,CAAgB,GAAA,EAAK,cAAA,EAAgB,MAAA,WAAiB,kBAAA;;;iBAiCtD,YAAA,CACd,UAAA,OACA,UAAA,UACA,GAAA,EAAK,kBAAA,EACL,OAAA,UACA,UAAA,EAAY,MAAA,+BACX,cAAA"}
|
package/dist/local/pl.js
CHANGED
|
@@ -2,12 +2,13 @@ import { isProcessAlive, processRun, processStop, processWaitStopped } from "./p
|
|
|
2
2
|
import { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from "../common/pl_binary.js";
|
|
3
3
|
import { filePid, readPid, writePid } from "./pid.js";
|
|
4
4
|
import { withTrace } from "./trace.js";
|
|
5
|
-
import { notEmpty } from "@milaboratories/ts-helpers";
|
|
5
|
+
import { notEmpty, sleep } from "@milaboratories/ts-helpers";
|
|
6
6
|
import fsp from "node:fs/promises";
|
|
7
7
|
import upath from "upath";
|
|
8
8
|
import * as os$1 from "node:os";
|
|
9
9
|
import { defaultHttpDispatcher } from "@milaboratories/pl-http";
|
|
10
10
|
import { parseHttpAuth } from "@milaboratories/pl-model-common";
|
|
11
|
+
import { ServingStatus, plHealthCheck } from "@milaboratories/pl-healthcheck";
|
|
11
12
|
//#region src/local/pl.ts
|
|
12
13
|
const LocalConfigYaml = "config-local.yaml";
|
|
13
14
|
/**
|
|
@@ -21,11 +22,12 @@ var LocalPl = class {
|
|
|
21
22
|
nRuns = 0;
|
|
22
23
|
lastRunHistory = {};
|
|
23
24
|
wasStopped = false;
|
|
24
|
-
constructor(logger, workingDir, startOptions, initialStartHistory, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
|
|
25
|
+
constructor(logger, workingDir, startOptions, initialStartHistory, grpcPort, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
|
|
25
26
|
this.logger = logger;
|
|
26
27
|
this.workingDir = workingDir;
|
|
27
28
|
this.startOptions = startOptions;
|
|
28
29
|
this.initialStartHistory = initialStartHistory;
|
|
30
|
+
this.grpcPort = grpcPort;
|
|
29
31
|
this.onClose = onClose;
|
|
30
32
|
this.onError = onError;
|
|
31
33
|
this.onCloseAndError = onCloseAndError;
|
|
@@ -67,8 +69,58 @@ var LocalPl = class {
|
|
|
67
69
|
stopped() {
|
|
68
70
|
return this.wasStopped;
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Polls the OS until the backend process is observed alive.
|
|
74
|
+
* Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.
|
|
75
|
+
*/
|
|
76
|
+
async isAlive(opts) {
|
|
77
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
|
|
78
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
79
|
+
while (true) {
|
|
80
|
+
if (Date.now() >= deadline) throw new Error("Timeout waiting for backend process to be alive");
|
|
81
|
+
await sleep(pollMs);
|
|
82
|
+
if (!this.pid) continue;
|
|
83
|
+
if (await isProcessAlive(this.pid)) return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.
|
|
88
|
+
* Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.
|
|
89
|
+
*
|
|
90
|
+
* Calling this method is an explicit statement that the caller wants a real
|
|
91
|
+
* readiness signal; if `grpcPort` is undefined we throw rather than silently
|
|
92
|
+
* degrade to a process-only check (use {@link isAlive} for that).
|
|
93
|
+
*/
|
|
94
|
+
async isReady(opts) {
|
|
95
|
+
const grpcPort = opts?.grpcPort ?? this.grpcPort;
|
|
96
|
+
if (grpcPort === void 0) throw new Error("isReady requires grpcPort to be configured — caller asked for a real readiness check but no gRPC port is available");
|
|
97
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 12e4);
|
|
98
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
99
|
+
const requestTimeoutMs = Math.max(500, Math.min(pollMs, 2e3));
|
|
100
|
+
let processAlive = false;
|
|
101
|
+
let wasAlive = false;
|
|
102
|
+
let isFirstCheck = true;
|
|
103
|
+
while (true) {
|
|
104
|
+
if (Date.now() >= deadline) {
|
|
105
|
+
if (!processAlive) throw new Error("Timeout waiting for backend process to be alive");
|
|
106
|
+
throw new Error("Timeout waiting for backend to be ready");
|
|
107
|
+
}
|
|
108
|
+
if (!isFirstCheck) await sleep(pollMs);
|
|
109
|
+
isFirstCheck = false;
|
|
110
|
+
if (!this.pid) continue;
|
|
111
|
+
processAlive = await isProcessAlive(this.pid);
|
|
112
|
+
wasAlive = wasAlive || processAlive;
|
|
113
|
+
if (!processAlive) {
|
|
114
|
+
if (wasAlive) throw new Error("Backend process died while waiting for readiness");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
if (await plHealthCheck({
|
|
119
|
+
address: `127.0.0.1:${grpcPort}`,
|
|
120
|
+
defaultRequestTimeout: requestTimeoutMs
|
|
121
|
+
}) === ServingStatus.SERVING) return;
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
72
124
|
}
|
|
73
125
|
debugInfo() {
|
|
74
126
|
return {
|
|
@@ -123,7 +175,7 @@ Only Basic auth is supported by the backend.`);
|
|
|
123
175
|
args: processOpts.args,
|
|
124
176
|
cwd: processOpts.opts.cwd
|
|
125
177
|
});
|
|
126
|
-
const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
|
|
178
|
+
const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.grpcPort, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
|
|
127
179
|
await pl.start();
|
|
128
180
|
return pl;
|
|
129
181
|
});
|
package/dist/local/pl.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pl.js","names":["os"],"sources":["../../src/local/pl.ts"],"sourcesContent":["import type { ProcessOptions } from \"./process\";\nimport { isProcessAlive, processStop, processWaitStopped, processRun } from \"./process\";\nimport type { PlBinarySource } from \"../common/pl_binary\";\nimport { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from \"../common/pl_binary\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { filePid, readPid, writePid } from \"./pid\";\nimport type { Trace } from \"./trace\";\nimport { withTrace } from \"./trace\";\nimport upath from \"upath\";\nimport fsp from \"node:fs/promises\";\nimport type { Required } from \"utility-types\";\nimport * as os from \"node:os\";\nimport type { ProxySettings } from \"@milaboratories/pl-http\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\n\nexport const LocalConfigYaml = \"config-local.yaml\";\n\n/**\n * Represents a local running pl-core,\n * and has methods to start, check if it's running, stop and wait for stopping it.\n * Also, a hook on pl-core closed can be provided.\n */\nexport class LocalPl {\n private instance?: ChildProcess;\n public pid?: number;\n private nRuns: number = 0;\n private lastRunHistory: Trace = {};\n private wasStopped = false;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly workingDir: string,\n private readonly startOptions: ProcessOptions,\n private readonly initialStartHistory: Trace,\n private readonly onClose?: (pl: LocalPl) => Promise<void>,\n private readonly onError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>,\n ) {}\n\n async start() {\n await withTrace(this.logger, async (trace, t) => {\n this.wasStopped = false;\n const instance = processRun(this.logger, this.startOptions);\n instance.on(\"error\", (e: any) => {\n this.logger.error(\n `error '${e}', while running platforma, started opts: ${JSON.stringify(this.debugInfo())}`,\n );\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onError !== undefined) void this.onError(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n instance.on(\"close\", () => {\n this.logger.warn(`platforma was closed, started opts: ${JSON.stringify(this.debugInfo())}`);\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onClose !== undefined) void this.onClose(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n\n trace(\"started\", true);\n\n const pidFile = trace(\"pidFile\", filePid(this.workingDir));\n trace(\"pid\", notEmpty(instance.pid));\n trace(\"pidWritten\", await writePid(pidFile, notEmpty(instance.pid)));\n\n this.nRuns++;\n this.instance = instance;\n this.pid = instance.pid;\n this.lastRunHistory = t;\n });\n }\n\n stop() {\n // TODO use this.instance to stop the process\n this.wasStopped = true;\n processStop(notEmpty(this.pid));\n }\n\n async waitStopped() {\n await processWaitStopped(notEmpty(this.pid), 15000);\n }\n\n stopped() {\n return this.wasStopped;\n }\n\n async isAlive(): Promise<boolean> {\n return await isProcessAlive(notEmpty(this.pid));\n }\n\n debugInfo() {\n return {\n lastRunHistory: this.lastRunHistory,\n nRuns: this.nRuns,\n pid: this.pid,\n workingDir: this.workingDir,\n initialStartHistory: this.initialStartHistory,\n wasStopped: this.wasStopped,\n };\n }\n}\n\n/** Options to start a local pl-core. */\nexport type LocalPlOptions = {\n /** From what directory start a process. */\n readonly workingDir: string;\n /** A string representation of yaml config. */\n readonly config: string;\n /** How to get a binary, download it or get an existing one (default: download latest version) */\n readonly plBinary?: PlBinarySource;\n /** Additional options for a process, environments, stdout, stderr etc. */\n readonly spawnOptions?: SpawnOptions;\n /**\n * If the previous pl-core was started from the same directory,\n * we can check if it's still running and then stop it before starting a new one.\n * (default: true)\n */\n readonly closeOld?: boolean;\n /**\n * Proxy settings to use to fetch the binary and pass it down\n * as a HTTPS_PROXY/HTTP_PROXY environment variable;\n * Backend only supports Basic auth.\n */\n readonly proxy?: ProxySettings;\n\n readonly onClose?: (pl: LocalPl) => Promise<void>;\n readonly onError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>;\n};\n\nexport type LocalPlOptionsFull = Required<LocalPlOptions, \"plBinary\" | \"spawnOptions\" | \"closeOld\">;\n\n/**\n * Starts pl-core, if the option was provided downloads a binary, reads license environments etc.\n */\nexport async function localPlatformaInit(logger: MiLogger, _ops: LocalPlOptions): Promise<LocalPl> {\n // filling-in default values\n\n // Backend could consume a lot of CPU power,\n // we want to keep at least a couple for UI and other apps to work.\n const numCpu = Math.max(os.cpus().length - 2, 1);\n const ops = mergeDefaultOps(_ops, numCpu);\n\n return await withTrace(logger, async (trace, t) => {\n trace(\"startOptions\", { ...ops, config: \"too wordy\" });\n\n const workDir = upath.resolve(ops.workingDir);\n\n if (ops.closeOld) {\n trace(\"closeOld\", await localPlatformaReadPidAndStop(logger, workDir));\n }\n\n const configPath = upath.join(workDir, LocalConfigYaml);\n\n logger.info(`writing configuration '${configPath}'...`);\n await fsp.writeFile(configPath, ops.config);\n\n const plBinPath = upath.join(workDir, \"binaries\");\n const baseBinaryPath = await resolveLocalPlBinaryPath({\n logger,\n downloadDir: plBinPath,\n src: ops.plBinary,\n dispatcher: defaultHttpDispatcher(ops.proxy),\n });\n const binaryPath = trace(\"binaryPath\", upath.join(\"binaries\", baseBinaryPath));\n\n const env = { ...process.env };\n\n if (ops.proxy?.url) {\n const url = new URL(ops.proxy.url);\n if (ops.proxy.auth) {\n const parsed = parseHttpAuth(ops.proxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`\\\nUnsupported auth scheme: ${parsed.scheme}. \\\nOnly Basic auth is supported by the backend.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n env.http_proxy = url.toString();\n env.https_proxy = url.toString();\n }\n\n const processOpts = plProcessOps(binaryPath, configPath, ops, workDir, env);\n trace(\"processOpts\", {\n cmd: processOpts.cmd,\n args: processOpts.args,\n cwd: processOpts.opts.cwd,\n });\n\n const pl = new LocalPl(\n logger,\n ops.workingDir,\n processOpts,\n t,\n ops.onClose,\n ops.onError,\n ops.onCloseAndError,\n ops.onCloseAndErrorNoStop,\n );\n await pl.start();\n\n return pl;\n });\n}\n\n/** Reads a pid of the old pl-core if it was started in the same working directory,\n * and closes it. */\nasync function localPlatformaReadPidAndStop(\n logger: MiLogger,\n workingDir: string,\n): Promise<Record<string, any>> {\n return await withTrace(logger, async (trace, t) => {\n const file = trace(\"pidFilePath\", filePid(workingDir));\n\n const oldPid = trace(\"pid\", await readPid(file));\n const alive = trace(\"wasAlive\", await isProcessAlive(oldPid));\n\n if (oldPid !== undefined && alive) {\n trace(\"stopped\", processStop(oldPid));\n try {\n trace(\"waitStopped\", await processWaitStopped(oldPid, 15_000)); // larger, that 10s we provide to backend in config.\n } catch {\n trace(\"forceStopped\", processStop(oldPid, true));\n trace(\"waitForceStopped\", await processWaitStopped(oldPid, 5_000));\n }\n }\n\n return t;\n });\n}\n\n/** Gets default options for the whole init process\n * and overrides them with the provided options. */\nexport function mergeDefaultOps(ops: LocalPlOptions, numCpu: number): LocalPlOptionsFull {\n const result: {\n plBinary: PlBinarySource;\n spawnOptions: SpawnOptions;\n closeOld: boolean;\n } = {\n plBinary: newDefaultPlBinarySource(),\n spawnOptions: {\n env: {\n GOMAXPROCS: String(numCpu),\n },\n },\n closeOld: true,\n };\n\n if (ops.spawnOptions?.env) {\n result.spawnOptions.env = { ...result.spawnOptions.env, ...ops.spawnOptions.env };\n }\n\n if (ops.spawnOptions) {\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.spawnOptions = { ...result.spawnOptions, ...withoutEnv };\n }\n\n const withoutSpawnOps = { ...ops };\n delete withoutSpawnOps[\"spawnOptions\"];\n\n return { ...result, ...withoutSpawnOps };\n}\n\n/** Gets default options for a platforma local binary\n * and overrides them with the provided options. */\nexport function plProcessOps(\n binaryPath: any,\n configPath: string,\n ops: LocalPlOptionsFull,\n workDir: string,\n defaultEnv: Record<string, string | undefined>,\n): ProcessOptions {\n const result: ProcessOptions = {\n cmd: binaryPath,\n args: [\"--config\", configPath],\n opts: {\n env: { ...defaultEnv },\n cwd: workDir,\n stdio: [\"pipe\", \"ignore\", \"inherit\"],\n windowsHide: true, // hide a terminal on Windows\n },\n };\n\n if (ops.spawnOptions?.env) {\n result.opts.env = { ...result.opts.env, ...ops.spawnOptions.env };\n }\n\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.opts = { ...result.opts, ...withoutEnv };\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;AAkBA,MAAa,kBAAkB;;;;;;AAO/B,IAAa,UAAb,MAAqB;CACnB;CACA;CACA,QAAwB;CACxB,iBAAgC,EAAE;CAClC,aAAqB;CAErB,YACE,QACA,YACA,cACA,qBACA,SACA,SACA,iBACA,uBACA;AARiB,OAAA,SAAA;AACA,OAAA,aAAA;AACA,OAAA,eAAA;AACA,OAAA,sBAAA;AACA,OAAA,UAAA;AACA,OAAA,UAAA;AACA,OAAA,kBAAA;AACA,OAAA,wBAAA;;CAGnB,MAAM,QAAQ;AACZ,QAAM,UAAU,KAAK,QAAQ,OAAO,OAAO,MAAM;AAC/C,QAAK,aAAa;GAClB,MAAM,WAAW,WAAW,KAAK,QAAQ,KAAK,aAAa;AAC3D,YAAS,GAAG,UAAU,MAAW;AAC/B,SAAK,OAAO,MACV,UAAU,EAAE,4CAA4C,KAAK,UAAU,KAAK,WAAW,CAAC,GACzF;AAGD,QAAI,KAAK,YAAY,KAAA,EAAgB,MAAK,QAAQ,KAAK;AACvD,QAAI,KAAK,oBAAoB,KAAA,EAAgB,MAAK,gBAAgB,KAAK;AACvE,QAAI,KAAK,0BAA0B,KAAA,KAAa,CAAC,KAAK,WAC/C,MAAK,sBAAsB,KAAK;KACvC;AACF,YAAS,GAAG,eAAe;AACzB,SAAK,OAAO,KAAK,uCAAuC,KAAK,UAAU,KAAK,WAAW,CAAC,GAAG;AAG3F,QAAI,KAAK,YAAY,KAAA,EAAgB,MAAK,QAAQ,KAAK;AACvD,QAAI,KAAK,oBAAoB,KAAA,EAAgB,MAAK,gBAAgB,KAAK;AACvE,QAAI,KAAK,0BAA0B,KAAA,KAAa,CAAC,KAAK,WAC/C,MAAK,sBAAsB,KAAK;KACvC;AAEF,SAAM,WAAW,KAAK;GAEtB,MAAM,UAAU,MAAM,WAAW,QAAQ,KAAK,WAAW,CAAC;AAC1D,SAAM,OAAO,SAAS,SAAS,IAAI,CAAC;AACpC,SAAM,cAAc,MAAM,SAAS,SAAS,SAAS,SAAS,IAAI,CAAC,CAAC;AAEpE,QAAK;AACL,QAAK,WAAW;AAChB,QAAK,MAAM,SAAS;AACpB,QAAK,iBAAiB;IACtB;;CAGJ,OAAO;AAEL,OAAK,aAAa;AAClB,cAAY,SAAS,KAAK,IAAI,CAAC;;CAGjC,MAAM,cAAc;AAClB,QAAM,mBAAmB,SAAS,KAAK,IAAI,EAAE,KAAM;;CAGrD,UAAU;AACR,SAAO,KAAK;;CAGd,MAAM,UAA4B;AAChC,SAAO,MAAM,eAAe,SAAS,KAAK,IAAI,CAAC;;CAGjD,YAAY;AACV,SAAO;GACL,gBAAgB,KAAK;GACrB,OAAO,KAAK;GACZ,KAAK,KAAK;GACV,YAAY,KAAK;GACjB,qBAAqB,KAAK;GAC1B,YAAY,KAAK;GAClB;;;;;;AAsCL,eAAsB,mBAAmB,QAAkB,MAAwC;CAMjG,MAAM,MAAM,gBAAgB,MADb,KAAK,IAAIA,KAAG,MAAM,CAAC,SAAS,GAAG,EAAE,CACP;AAEzC,QAAO,MAAM,UAAU,QAAQ,OAAO,OAAO,MAAM;AACjD,QAAM,gBAAgB;GAAE,GAAG;GAAK,QAAQ;GAAa,CAAC;EAEtD,MAAM,UAAU,MAAM,QAAQ,IAAI,WAAW;AAE7C,MAAI,IAAI,SACN,OAAM,YAAY,MAAM,6BAA6B,QAAQ,QAAQ,CAAC;EAGxE,MAAM,aAAa,MAAM,KAAK,SAAS,gBAAgB;AAEvD,SAAO,KAAK,0BAA0B,WAAW,MAAM;AACvD,QAAM,IAAI,UAAU,YAAY,IAAI,OAAO;EAG3C,MAAM,iBAAiB,MAAM,yBAAyB;GACpD;GACA,aAHgB,MAAM,KAAK,SAAS,WAAW;GAI/C,KAAK,IAAI;GACT,YAAY,sBAAsB,IAAI,MAAM;GAC7C,CAAC;EACF,MAAM,aAAa,MAAM,cAAc,MAAM,KAAK,YAAY,eAAe,CAAC;EAE9E,MAAM,MAAM,EAAE,GAAG,QAAQ,KAAK;AAE9B,MAAI,IAAI,OAAO,KAAK;GAClB,MAAM,MAAM,IAAI,IAAI,IAAI,MAAM,IAAI;AAClC,OAAI,IAAI,MAAM,MAAM;IAClB,MAAM,SAAS,cAAc,IAAI,MAAM,KAAK;AAC5C,QAAI,OAAO,WAAW,QACpB,OAAM,IAAI,MAAM;2BACC,OAAO,OAAO;8CACK;AAEtC,QAAI,WAAW,OAAO;AACtB,QAAI,WAAW,OAAO;;AAExB,OAAI,aAAa,IAAI,UAAU;AAC/B,OAAI,cAAc,IAAI,UAAU;;EAGlC,MAAM,cAAc,aAAa,YAAY,YAAY,KAAK,SAAS,IAAI;AAC3E,QAAM,eAAe;GACnB,KAAK,YAAY;GACjB,MAAM,YAAY;GAClB,KAAK,YAAY,KAAK;GACvB,CAAC;EAEF,MAAM,KAAK,IAAI,QACb,QACA,IAAI,YACJ,aACA,GACA,IAAI,SACJ,IAAI,SACJ,IAAI,iBACJ,IAAI,sBACL;AACD,QAAM,GAAG,OAAO;AAEhB,SAAO;GACP;;;;AAKJ,eAAe,6BACb,QACA,YAC8B;AAC9B,QAAO,MAAM,UAAU,QAAQ,OAAO,OAAO,MAAM;EAGjD,MAAM,SAAS,MAAM,OAAO,MAAM,QAFrB,MAAM,eAAe,QAAQ,WAAW,CAAC,CAEP,CAAC;EAChD,MAAM,QAAQ,MAAM,YAAY,MAAM,eAAe,OAAO,CAAC;AAE7D,MAAI,WAAW,KAAA,KAAa,OAAO;AACjC,SAAM,WAAW,YAAY,OAAO,CAAC;AACrC,OAAI;AACF,UAAM,eAAe,MAAM,mBAAmB,QAAQ,KAAO,CAAC;WACxD;AACN,UAAM,gBAAgB,YAAY,QAAQ,KAAK,CAAC;AAChD,UAAM,oBAAoB,MAAM,mBAAmB,QAAQ,IAAM,CAAC;;;AAItE,SAAO;GACP;;;;AAKJ,SAAgB,gBAAgB,KAAqB,QAAoC;CACvF,MAAM,SAIF;EACF,UAAU,0BAA0B;EACpC,cAAc,EACZ,KAAK,EACH,YAAY,OAAO,OAAO,EAC3B,EACF;EACD,UAAU;EACX;AAED,KAAI,IAAI,cAAc,IACpB,QAAO,aAAa,MAAM;EAAE,GAAG,OAAO,aAAa;EAAK,GAAG,IAAI,aAAa;EAAK;AAGnF,KAAI,IAAI,cAAc;EACpB,MAAM,aAAa,EAAE,GAAG,IAAI,cAAc;AAC1C,SAAO,WAAW;AAClB,SAAO,eAAe;GAAE,GAAG,OAAO;GAAc,GAAG;GAAY;;CAGjE,MAAM,kBAAkB,EAAE,GAAG,KAAK;AAClC,QAAO,gBAAgB;AAEvB,QAAO;EAAE,GAAG;EAAQ,GAAG;EAAiB;;;;AAK1C,SAAgB,aACd,YACA,YACA,KACA,SACA,YACgB;CAChB,MAAM,SAAyB;EAC7B,KAAK;EACL,MAAM,CAAC,YAAY,WAAW;EAC9B,MAAM;GACJ,KAAK,EAAE,GAAG,YAAY;GACtB,KAAK;GACL,OAAO;IAAC;IAAQ;IAAU;IAAU;GACpC,aAAa;GACd;EACF;AAED,KAAI,IAAI,cAAc,IACpB,QAAO,KAAK,MAAM;EAAE,GAAG,OAAO,KAAK;EAAK,GAAG,IAAI,aAAa;EAAK;CAGnE,MAAM,aAAa,EAAE,GAAG,IAAI,cAAc;AAC1C,QAAO,WAAW;AAClB,QAAO,OAAO;EAAE,GAAG,OAAO;EAAM,GAAG;EAAY;AAE/C,QAAO"}
|
|
1
|
+
{"version":3,"file":"pl.js","names":["os"],"sources":["../../src/local/pl.ts"],"sourcesContent":["import type { ProcessOptions } from \"./process\";\nimport { isProcessAlive, processStop, processWaitStopped, processRun } from \"./process\";\nimport type { PlBinarySource } from \"../common/pl_binary\";\nimport { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from \"../common/pl_binary\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { ChildProcess, SpawnOptions } from \"node:child_process\";\nimport { filePid, readPid, writePid } from \"./pid\";\nimport type { Trace } from \"./trace\";\nimport { withTrace } from \"./trace\";\nimport upath from \"upath\";\nimport fsp from \"node:fs/promises\";\nimport type { Required } from \"utility-types\";\nimport * as os from \"node:os\";\nimport { sleep } from \"@milaboratories/ts-helpers\";\nimport type { ProxySettings } from \"@milaboratories/pl-http\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\nimport { plHealthCheck, ServingStatus } from \"@milaboratories/pl-healthcheck\";\n\nexport const LocalConfigYaml = \"config-local.yaml\";\n\n/**\n * Represents a local running pl-core,\n * and has methods to start, check if it's running, stop and wait for stopping it.\n * Also, a hook on pl-core closed can be provided.\n */\nexport class LocalPl {\n private instance?: ChildProcess;\n public pid?: number;\n private nRuns: number = 0;\n private lastRunHistory: Trace = {};\n private wasStopped = false;\n\n constructor(\n private readonly logger: MiLogger,\n private readonly workingDir: string,\n private readonly startOptions: ProcessOptions,\n private readonly initialStartHistory: Trace,\n private readonly grpcPort?: number,\n private readonly onClose?: (pl: LocalPl) => Promise<void>,\n private readonly onError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,\n private readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>,\n ) {}\n\n async start() {\n await withTrace(this.logger, async (trace, t) => {\n this.wasStopped = false;\n const instance = processRun(this.logger, this.startOptions);\n instance.on(\"error\", (e: any) => {\n this.logger.error(\n `error '${e}', while running platforma, started opts: ${JSON.stringify(this.debugInfo())}`,\n );\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onError !== undefined) void this.onError(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n instance.on(\"close\", () => {\n this.logger.warn(`platforma was closed, started opts: ${JSON.stringify(this.debugInfo())}`);\n\n // keep in mind there are no awaits here, it will be asynchronous\n if (this.onClose !== undefined) void this.onClose(this);\n if (this.onCloseAndError !== undefined) void this.onCloseAndError(this);\n if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)\n void this.onCloseAndErrorNoStop(this);\n });\n\n trace(\"started\", true);\n\n const pidFile = trace(\"pidFile\", filePid(this.workingDir));\n trace(\"pid\", notEmpty(instance.pid));\n trace(\"pidWritten\", await writePid(pidFile, notEmpty(instance.pid)));\n\n this.nRuns++;\n this.instance = instance;\n this.pid = instance.pid;\n this.lastRunHistory = t;\n });\n }\n\n stop() {\n // TODO use this.instance to stop the process\n this.wasStopped = true;\n processStop(notEmpty(this.pid));\n }\n\n async waitStopped() {\n await processWaitStopped(notEmpty(this.pid), 15000);\n }\n\n stopped() {\n return this.wasStopped;\n }\n\n /**\n * Polls the OS until the backend process is observed alive.\n * Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.\n */\n async isAlive(opts?: { timeoutMs?: number; pollIntervalMs?: number }): Promise<void> {\n const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);\n const pollMs = opts?.pollIntervalMs ?? 500;\n\n while (true) {\n if (Date.now() >= deadline) {\n throw new Error(\"Timeout waiting for backend process to be alive\");\n }\n await sleep(pollMs);\n if (!this.pid) continue;\n if (await isProcessAlive(this.pid)) return;\n }\n }\n\n /**\n * Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.\n * Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.\n *\n * Calling this method is an explicit statement that the caller wants a real\n * readiness signal; if `grpcPort` is undefined we throw rather than silently\n * degrade to a process-only check (use {@link isAlive} for that).\n */\n async isReady(opts?: {\n grpcPort?: number;\n timeoutMs?: number;\n pollIntervalMs?: number;\n }): Promise<void> {\n const grpcPort = opts?.grpcPort ?? this.grpcPort;\n\n if (grpcPort === undefined) {\n throw new Error(\n \"isReady requires grpcPort to be configured — caller asked for a real readiness check but no gRPC port is available\",\n );\n }\n\n const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);\n const pollMs = opts?.pollIntervalMs ?? 500;\n const requestTimeoutMs = Math.max(500, Math.min(pollMs, 2_000));\n\n let processAlive = false;\n let wasAlive = false;\n let isFirstCheck = true;\n\n while (true) {\n if (Date.now() >= deadline) {\n if (!processAlive) {\n throw new Error(\"Timeout waiting for backend process to be alive\");\n }\n throw new Error(\"Timeout waiting for backend to be ready\");\n }\n\n if (!isFirstCheck) await sleep(pollMs);\n isFirstCheck = false;\n\n if (!this.pid) continue;\n\n processAlive = await isProcessAlive(this.pid);\n wasAlive = wasAlive || processAlive;\n if (!processAlive) {\n if (wasAlive) {\n // saw the process alive at least once, now it's gone — definitive failure\n throw new Error(\"Backend process died while waiting for readiness\");\n }\n continue;\n }\n\n try {\n const status = await plHealthCheck({\n address: `127.0.0.1:${grpcPort}`,\n defaultRequestTimeout: requestTimeoutMs,\n });\n if (status === ServingStatus.SERVING) return;\n } catch {\n // not yet serving\n }\n }\n }\n\n debugInfo() {\n return {\n lastRunHistory: this.lastRunHistory,\n nRuns: this.nRuns,\n pid: this.pid,\n workingDir: this.workingDir,\n initialStartHistory: this.initialStartHistory,\n wasStopped: this.wasStopped,\n };\n }\n}\n\n/** Options to start a local pl-core. */\nexport type LocalPlOptions = {\n /** From what directory start a process. */\n readonly workingDir: string;\n /** A string representation of yaml config. */\n readonly config: string;\n /** How to get a binary, download it or get an existing one (default: download latest version) */\n readonly plBinary?: PlBinarySource;\n /** Additional options for a process, environments, stdout, stderr etc. */\n readonly spawnOptions?: SpawnOptions;\n /**\n * If the previous pl-core was started from the same directory,\n * we can check if it's still running and then stop it before starting a new one.\n * (default: true)\n */\n readonly closeOld?: boolean;\n /**\n * Proxy settings to use to fetch the binary and pass it down\n * as a HTTPS_PROXY/HTTP_PROXY environment variable;\n * Backend only supports Basic auth.\n */\n readonly proxy?: ProxySettings;\n\n /**\n * gRPC port the backend listens on (always on 127.0.0.1 for local).\n * Required by {@link LocalPl.isReady}, which polls\n * `grpc.health.v1.Health/Check` against this port to distinguish a process\n * that is alive but still initializing from one actually serving gRPC.\n */\n readonly grpcPort?: number;\n\n readonly onClose?: (pl: LocalPl) => Promise<void>;\n readonly onError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;\n readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>;\n};\n\nexport type LocalPlOptionsFull = Required<LocalPlOptions, \"plBinary\" | \"spawnOptions\" | \"closeOld\">;\n\n/**\n * Starts pl-core, if the option was provided downloads a binary, reads license environments etc.\n */\nexport async function localPlatformaInit(logger: MiLogger, _ops: LocalPlOptions): Promise<LocalPl> {\n // filling-in default values\n\n // Backend could consume a lot of CPU power,\n // we want to keep at least a couple for UI and other apps to work.\n const numCpu = Math.max(os.cpus().length - 2, 1);\n const ops = mergeDefaultOps(_ops, numCpu);\n\n return await withTrace(logger, async (trace, t) => {\n trace(\"startOptions\", { ...ops, config: \"too wordy\" });\n\n const workDir = upath.resolve(ops.workingDir);\n\n if (ops.closeOld) {\n trace(\"closeOld\", await localPlatformaReadPidAndStop(logger, workDir));\n }\n\n const configPath = upath.join(workDir, LocalConfigYaml);\n\n logger.info(`writing configuration '${configPath}'...`);\n await fsp.writeFile(configPath, ops.config);\n\n const plBinPath = upath.join(workDir, \"binaries\");\n const baseBinaryPath = await resolveLocalPlBinaryPath({\n logger,\n downloadDir: plBinPath,\n src: ops.plBinary,\n dispatcher: defaultHttpDispatcher(ops.proxy),\n });\n const binaryPath = trace(\"binaryPath\", upath.join(\"binaries\", baseBinaryPath));\n\n const env = { ...process.env };\n\n if (ops.proxy?.url) {\n const url = new URL(ops.proxy.url);\n if (ops.proxy.auth) {\n const parsed = parseHttpAuth(ops.proxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`\\\nUnsupported auth scheme: ${parsed.scheme}. \\\nOnly Basic auth is supported by the backend.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n env.http_proxy = url.toString();\n env.https_proxy = url.toString();\n }\n\n const processOpts = plProcessOps(binaryPath, configPath, ops, workDir, env);\n trace(\"processOpts\", {\n cmd: processOpts.cmd,\n args: processOpts.args,\n cwd: processOpts.opts.cwd,\n });\n\n const pl = new LocalPl(\n logger,\n ops.workingDir,\n processOpts,\n t,\n ops.grpcPort,\n ops.onClose,\n ops.onError,\n ops.onCloseAndError,\n ops.onCloseAndErrorNoStop,\n );\n await pl.start();\n\n return pl;\n });\n}\n\n/** Reads a pid of the old pl-core if it was started in the same working directory,\n * and closes it. */\nasync function localPlatformaReadPidAndStop(\n logger: MiLogger,\n workingDir: string,\n): Promise<Record<string, any>> {\n return await withTrace(logger, async (trace, t) => {\n const file = trace(\"pidFilePath\", filePid(workingDir));\n\n const oldPid = trace(\"pid\", await readPid(file));\n const alive = trace(\"wasAlive\", await isProcessAlive(oldPid));\n\n if (oldPid !== undefined && alive) {\n trace(\"stopped\", processStop(oldPid));\n try {\n trace(\"waitStopped\", await processWaitStopped(oldPid, 15_000)); // larger, that 10s we provide to backend in config.\n } catch {\n trace(\"forceStopped\", processStop(oldPid, true));\n trace(\"waitForceStopped\", await processWaitStopped(oldPid, 5_000));\n }\n }\n\n return t;\n });\n}\n\n/** Gets default options for the whole init process\n * and overrides them with the provided options. */\nexport function mergeDefaultOps(ops: LocalPlOptions, numCpu: number): LocalPlOptionsFull {\n const result: {\n plBinary: PlBinarySource;\n spawnOptions: SpawnOptions;\n closeOld: boolean;\n } = {\n plBinary: newDefaultPlBinarySource(),\n spawnOptions: {\n env: {\n GOMAXPROCS: String(numCpu),\n },\n },\n closeOld: true,\n };\n\n if (ops.spawnOptions?.env) {\n result.spawnOptions.env = { ...result.spawnOptions.env, ...ops.spawnOptions.env };\n }\n\n if (ops.spawnOptions) {\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.spawnOptions = { ...result.spawnOptions, ...withoutEnv };\n }\n\n const withoutSpawnOps = { ...ops };\n delete withoutSpawnOps[\"spawnOptions\"];\n\n return { ...result, ...withoutSpawnOps };\n}\n\n/** Gets default options for a platforma local binary\n * and overrides them with the provided options. */\nexport function plProcessOps(\n binaryPath: any,\n configPath: string,\n ops: LocalPlOptionsFull,\n workDir: string,\n defaultEnv: Record<string, string | undefined>,\n): ProcessOptions {\n const result: ProcessOptions = {\n cmd: binaryPath,\n args: [\"--config\", configPath],\n opts: {\n env: { ...defaultEnv },\n cwd: workDir,\n stdio: [\"pipe\", \"ignore\", \"inherit\"],\n windowsHide: true, // hide a terminal on Windows\n },\n };\n\n if (ops.spawnOptions?.env) {\n result.opts.env = { ...result.opts.env, ...ops.spawnOptions.env };\n }\n\n const withoutEnv = { ...ops.spawnOptions };\n delete withoutEnv[\"env\"];\n result.opts = { ...result.opts, ...withoutEnv };\n\n return result;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-deployments",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "MiLaboratories Platforma Backend code service run wrapper",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"upath": "^2.0.1",
|
|
33
33
|
"yaml": "^2.8.0",
|
|
34
34
|
"zod": "~3.25.76",
|
|
35
|
+
"@milaboratories/pl-config": "1.8.1",
|
|
35
36
|
"@milaboratories/pl-http": "1.2.4",
|
|
37
|
+
"@milaboratories/pl-healthcheck": "1.0.0",
|
|
36
38
|
"@milaboratories/pl-model-common": "1.42.0",
|
|
37
|
-
"@milaboratories/ts-helpers": "1.8.2"
|
|
38
|
-
"@milaboratories/pl-config": "1.8.1"
|
|
39
|
+
"@milaboratories/ts-helpers": "1.8.2"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@types/decompress": "^4.2.7",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"typescript": "~5.9.3",
|
|
47
48
|
"utility-types": "^3.11.0",
|
|
48
49
|
"vitest": "^4.1.3",
|
|
49
|
-
"@milaboratories/ts-builder": "1.
|
|
50
|
+
"@milaboratories/ts-builder": "1.5.0",
|
|
50
51
|
"@milaboratories/ts-configs": "1.2.3",
|
|
51
52
|
"@milaboratories/build-configs": "2.0.0"
|
|
52
53
|
},
|
package/src/local/pl.test.ts
CHANGED
|
@@ -32,7 +32,7 @@ test(
|
|
|
32
32
|
|
|
33
33
|
console.log(`Platforma: %o`, pl.debugInfo());
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
await pl.isAlive();
|
|
36
36
|
expect(pl.pid).not.toBeUndefined();
|
|
37
37
|
|
|
38
38
|
pl.stop();
|
|
@@ -57,14 +57,14 @@ test(
|
|
|
57
57
|
await sleep(5000);
|
|
58
58
|
console.log(`OldPlatforma: %o`, oldPl.debugInfo());
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
await oldPl.isAlive();
|
|
61
61
|
const newPl = await localPlatformaInit(logger, options);
|
|
62
|
-
expect(
|
|
62
|
+
await expect(oldPl.isAlive({ timeoutMs: 1000 })).rejects.toThrow();
|
|
63
63
|
await sleep(5000);
|
|
64
64
|
|
|
65
65
|
console.log(`NewPlatforma: %o`, newPl.debugInfo());
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
await newPl.isAlive();
|
|
68
68
|
expect(newPl.pid).not.toBeUndefined();
|
|
69
69
|
newPl.stop();
|
|
70
70
|
await newPl.waitStopped();
|
|
@@ -87,17 +87,17 @@ test(
|
|
|
87
87
|
});
|
|
88
88
|
await sleep(1000);
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
await pl.isAlive();
|
|
91
91
|
processStop(pl.pid!);
|
|
92
92
|
await sleep(3000);
|
|
93
93
|
console.log(`Platforma after first stop: %o`, pl.debugInfo());
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
await pl.isAlive();
|
|
96
96
|
processStop(pl.pid!);
|
|
97
97
|
await sleep(3000);
|
|
98
98
|
console.log(`Platforma after second stop: %o`, pl.debugInfo());
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
await pl.isAlive();
|
|
101
101
|
expect(pl.debugInfo().nRuns).toEqual(3);
|
|
102
102
|
|
|
103
103
|
pl.stop();
|
package/src/local/pl.ts
CHANGED
|
@@ -12,9 +12,11 @@ import upath from "upath";
|
|
|
12
12
|
import fsp from "node:fs/promises";
|
|
13
13
|
import type { Required } from "utility-types";
|
|
14
14
|
import * as os from "node:os";
|
|
15
|
+
import { sleep } from "@milaboratories/ts-helpers";
|
|
15
16
|
import type { ProxySettings } from "@milaboratories/pl-http";
|
|
16
17
|
import { defaultHttpDispatcher } from "@milaboratories/pl-http";
|
|
17
18
|
import { parseHttpAuth } from "@milaboratories/pl-model-common";
|
|
19
|
+
import { plHealthCheck, ServingStatus } from "@milaboratories/pl-healthcheck";
|
|
18
20
|
|
|
19
21
|
export const LocalConfigYaml = "config-local.yaml";
|
|
20
22
|
|
|
@@ -35,6 +37,7 @@ export class LocalPl {
|
|
|
35
37
|
private readonly workingDir: string,
|
|
36
38
|
private readonly startOptions: ProcessOptions,
|
|
37
39
|
private readonly initialStartHistory: Trace,
|
|
40
|
+
private readonly grpcPort?: number,
|
|
38
41
|
private readonly onClose?: (pl: LocalPl) => Promise<void>,
|
|
39
42
|
private readonly onError?: (pl: LocalPl) => Promise<void>,
|
|
40
43
|
private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,
|
|
@@ -93,8 +96,86 @@ export class LocalPl {
|
|
|
93
96
|
return this.wasStopped;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Polls the OS until the backend process is observed alive.
|
|
101
|
+
* Throws on timeout. Does NOT touch gRPC — use {@link isReady} for that.
|
|
102
|
+
*/
|
|
103
|
+
async isAlive(opts?: { timeoutMs?: number; pollIntervalMs?: number }): Promise<void> {
|
|
104
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);
|
|
105
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
106
|
+
|
|
107
|
+
while (true) {
|
|
108
|
+
if (Date.now() >= deadline) {
|
|
109
|
+
throw new Error("Timeout waiting for backend process to be alive");
|
|
110
|
+
}
|
|
111
|
+
await sleep(pollMs);
|
|
112
|
+
if (!this.pid) continue;
|
|
113
|
+
if (await isProcessAlive(this.pid)) return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Polls `grpc.health.v1.Health/Check` until the backend reports SERVING.
|
|
119
|
+
* Throws on timeout, on missing `grpcPort`, or if the process dies mid-wait.
|
|
120
|
+
*
|
|
121
|
+
* Calling this method is an explicit statement that the caller wants a real
|
|
122
|
+
* readiness signal; if `grpcPort` is undefined we throw rather than silently
|
|
123
|
+
* degrade to a process-only check (use {@link isAlive} for that).
|
|
124
|
+
*/
|
|
125
|
+
async isReady(opts?: {
|
|
126
|
+
grpcPort?: number;
|
|
127
|
+
timeoutMs?: number;
|
|
128
|
+
pollIntervalMs?: number;
|
|
129
|
+
}): Promise<void> {
|
|
130
|
+
const grpcPort = opts?.grpcPort ?? this.grpcPort;
|
|
131
|
+
|
|
132
|
+
if (grpcPort === undefined) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"isReady requires grpcPort to be configured — caller asked for a real readiness check but no gRPC port is available",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const deadline = Date.now() + (opts?.timeoutMs ?? 120_000);
|
|
139
|
+
const pollMs = opts?.pollIntervalMs ?? 500;
|
|
140
|
+
const requestTimeoutMs = Math.max(500, Math.min(pollMs, 2_000));
|
|
141
|
+
|
|
142
|
+
let processAlive = false;
|
|
143
|
+
let wasAlive = false;
|
|
144
|
+
let isFirstCheck = true;
|
|
145
|
+
|
|
146
|
+
while (true) {
|
|
147
|
+
if (Date.now() >= deadline) {
|
|
148
|
+
if (!processAlive) {
|
|
149
|
+
throw new Error("Timeout waiting for backend process to be alive");
|
|
150
|
+
}
|
|
151
|
+
throw new Error("Timeout waiting for backend to be ready");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!isFirstCheck) await sleep(pollMs);
|
|
155
|
+
isFirstCheck = false;
|
|
156
|
+
|
|
157
|
+
if (!this.pid) continue;
|
|
158
|
+
|
|
159
|
+
processAlive = await isProcessAlive(this.pid);
|
|
160
|
+
wasAlive = wasAlive || processAlive;
|
|
161
|
+
if (!processAlive) {
|
|
162
|
+
if (wasAlive) {
|
|
163
|
+
// saw the process alive at least once, now it's gone — definitive failure
|
|
164
|
+
throw new Error("Backend process died while waiting for readiness");
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const status = await plHealthCheck({
|
|
171
|
+
address: `127.0.0.1:${grpcPort}`,
|
|
172
|
+
defaultRequestTimeout: requestTimeoutMs,
|
|
173
|
+
});
|
|
174
|
+
if (status === ServingStatus.SERVING) return;
|
|
175
|
+
} catch {
|
|
176
|
+
// not yet serving
|
|
177
|
+
}
|
|
178
|
+
}
|
|
98
179
|
}
|
|
99
180
|
|
|
100
181
|
debugInfo() {
|
|
@@ -132,6 +213,14 @@ export type LocalPlOptions = {
|
|
|
132
213
|
*/
|
|
133
214
|
readonly proxy?: ProxySettings;
|
|
134
215
|
|
|
216
|
+
/**
|
|
217
|
+
* gRPC port the backend listens on (always on 127.0.0.1 for local).
|
|
218
|
+
* Required by {@link LocalPl.isReady}, which polls
|
|
219
|
+
* `grpc.health.v1.Health/Check` against this port to distinguish a process
|
|
220
|
+
* that is alive but still initializing from one actually serving gRPC.
|
|
221
|
+
*/
|
|
222
|
+
readonly grpcPort?: number;
|
|
223
|
+
|
|
135
224
|
readonly onClose?: (pl: LocalPl) => Promise<void>;
|
|
136
225
|
readonly onError?: (pl: LocalPl) => Promise<void>;
|
|
137
226
|
readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;
|
|
@@ -204,6 +293,7 @@ Only Basic auth is supported by the backend.`);
|
|
|
204
293
|
ops.workingDir,
|
|
205
294
|
processOpts,
|
|
206
295
|
t,
|
|
296
|
+
ops.grpcPort,
|
|
207
297
|
ops.onClose,
|
|
208
298
|
ops.onError,
|
|
209
299
|
ops.onCloseAndError,
|