@milaboratories/pl-deployments 2.17.18 → 2.18.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 CHANGED
@@ -10,6 +10,8 @@ let upath = require("upath");
10
10
  upath = require_runtime.__toESM(upath);
11
11
  let node_os = require("node:os");
12
12
  node_os = require_runtime.__toESM(node_os);
13
+ let node_net = require("node:net");
14
+ node_net = require_runtime.__toESM(node_net);
13
15
  let _milaboratories_pl_http = require("@milaboratories/pl-http");
14
16
  let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
15
17
  //#region src/local/pl.ts
@@ -25,11 +27,12 @@ var LocalPl = class {
25
27
  nRuns = 0;
26
28
  lastRunHistory = {};
27
29
  wasStopped = false;
28
- constructor(logger, workingDir, startOptions, initialStartHistory, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
30
+ constructor(logger, workingDir, startOptions, initialStartHistory, grpcPort, onClose, onError, onCloseAndError, onCloseAndErrorNoStop) {
29
31
  this.logger = logger;
30
32
  this.workingDir = workingDir;
31
33
  this.startOptions = startOptions;
32
34
  this.initialStartHistory = initialStartHistory;
35
+ this.grpcPort = grpcPort;
33
36
  this.onClose = onClose;
34
37
  this.onError = onError;
35
38
  this.onCloseAndError = onCloseAndError;
@@ -72,7 +75,9 @@ var LocalPl = class {
72
75
  return this.wasStopped;
73
76
  }
74
77
  async isAlive() {
75
- return await require_process.isProcessAlive((0, _milaboratories_ts_helpers.notEmpty)(this.pid));
78
+ if (!await require_process.isProcessAlive((0, _milaboratories_ts_helpers.notEmpty)(this.pid))) return false;
79
+ if (this.grpcPort === void 0) return true;
80
+ return await tcpProbe("127.0.0.1", this.grpcPort);
76
81
  }
77
82
  debugInfo() {
78
83
  return {
@@ -127,7 +132,7 @@ Only Basic auth is supported by the backend.`);
127
132
  args: processOpts.args,
128
133
  cwd: processOpts.opts.cwd
129
134
  });
130
- const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
135
+ const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.grpcPort, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
131
136
  await pl.start();
132
137
  return pl;
133
138
  });
@@ -177,6 +182,30 @@ function mergeDefaultOps(ops, numCpu) {
177
182
  ...withoutSpawnOps
178
183
  };
179
184
  }
185
+ /** Resolves true if a TCP connect to host:port succeeds within timeoutMs. Used by
186
+ * {@link LocalPl.isAlive} to verify the gRPC listener has actually bound — the backend
187
+ * process can be alive long before its server sockets are accepting connections. */
188
+ async function tcpProbe(host, port, timeoutMs = 500) {
189
+ return await new Promise((resolve) => {
190
+ const socket = node_net.default.connect({
191
+ host,
192
+ port
193
+ });
194
+ const finalize = (ok) => {
195
+ socket.destroy();
196
+ resolve(ok);
197
+ };
198
+ const timer = setTimeout(() => finalize(false), timeoutMs);
199
+ socket.once("connect", () => {
200
+ clearTimeout(timer);
201
+ finalize(true);
202
+ });
203
+ socket.once("error", () => {
204
+ clearTimeout(timer);
205
+ finalize(false);
206
+ });
207
+ });
208
+ }
180
209
  /** Gets default options for a platforma local binary
181
210
  * and overrides them with the provided options. */
182
211
  function plProcessOps(binaryPath, configPath, ops, workDir, defaultEnv) {
@@ -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","os","fsp","resolveLocalPlBinaryPath","readPid","processStop","newDefaultPlBinarySource","net"],"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 net from \"node:net\";\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 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 async isAlive(): Promise<boolean> {\n if (!(await isProcessAlive(notEmpty(this.pid)))) return false;\n if (this.grpcPort === undefined) return true;\n return await tcpProbe(\"127.0.0.1\", this.grpcPort);\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 * When set, {@link LocalPl.isAlive} additionally probes this TCP port so callers\n * can distinguish a process that is alive but still binding from a process that is\n * actually accepting gRPC connections.\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/** Resolves true if a TCP connect to host:port succeeds within timeoutMs. Used by\n * {@link LocalPl.isAlive} to verify the gRPC listener has actually bound — the backend\n * process can be alive long before its server sockets are accepting connections. */\nasync function tcpProbe(host: string, port: number, timeoutMs = 500): Promise<boolean> {\n return await new Promise<boolean>((resolve) => {\n const socket = net.connect({ host, port });\n const finalize = (ok: boolean) => {\n socket.destroy();\n resolve(ok);\n };\n const timer = setTimeout(() => finalize(false), timeoutMs);\n socket.once(\"connect\", () => {\n clearTimeout(timer);\n finalize(true);\n });\n socket.once(\"error\", () => {\n clearTimeout(timer);\n finalize(false);\n });\n });\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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"}
@@ -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,7 +28,7 @@ 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>;
@@ -60,6 +61,13 @@ type LocalPlOptions = {
60
61
  * Backend only supports Basic auth.
61
62
  */
62
63
  readonly proxy?: ProxySettings;
64
+ /**
65
+ * gRPC port the backend listens on (always on 127.0.0.1 for local).
66
+ * When set, {@link LocalPl.isAlive} additionally probes this TCP port so callers
67
+ * can distinguish a process that is alive but still binding from a process that is
68
+ * actually accepting gRPC connections.
69
+ */
70
+ readonly grpcPort?: number;
63
71
  readonly onClose?: (pl: LocalPl) => Promise<void>;
64
72
  readonly onError?: (pl: LocalPl) => Promise<void>;
65
73
  readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"pl.d.ts","names":[],"sources":["../../src/local/pl.ts"],"mappings":";;;;;;;;;cAkBa,eAAA;;AAAb;;;;cAOa,OAAA;EAAA,iBAQQ,MAAA;EAAA,iBACA,UAAA;EAAA,iBACA,YAAA;EAAA,iBACA,mBAAA;EAAA,iBACA,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,eAAA;EAAA,iBACA,qBAAA;EAAA,QAdX,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,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;EAIM,OAAA,CAAA,GAAW,OAAA;EAIjB,SAAA,CAAA;;;;;;;;;;KAaU,cAAA;EArFH,oDAuFE,UAAA,UArFD;EAAA,SAuFC,MAAA;WAEA,QAAA,GAAW,cAAA,EArFD;EAAA,SAuFV,YAAA,GAAe,YAAA;EArFS;;;;;EAAA,SA2FxB,QAAA;EAzFqC;;;;;EAAA,SA+FrC,KAAA,GAAQ,aAAA;EAAA,SAER,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;;;iBAoG1E,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"}
1
+ {"version":3,"file":"pl.d.ts","names":[],"sources":["../../src/local/pl.ts"],"mappings":";;;;;;;;;cAmBa,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;EAIM,OAAA,CAAA,GAAW,OAAA;EAMjB,SAAA,CAAA;;;;;;;;;;KAaU,cAAA;EAxFH,oDA0FE,UAAA,UAxFD;EAAA,SA0FC,MAAA;WAEA,QAAA,GAAW,cAAA,EAxFD;EAAA,SA0FV,YAAA,GAAe,YAAA;EAxFS;;;;;EAAA,SA8FxB,QAAA;EA3FqB;;;;;EAAA,SAiGrB,KAAA,GAAQ,aAAA;EAhGE;;;;;;EAAA,SAwGV,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;;;iBAqG1E,eAAA,CAAgB,GAAA,EAAK,cAAA,EAAgB,MAAA,WAAiB,kBAAA;;;iBAuDtD,YAAA,CACd,UAAA,OACA,UAAA,UACA,GAAA,EAAK,kBAAA,EACL,OAAA,UACA,UAAA,EAAY,MAAA,+BACX,cAAA"}
package/dist/local/pl.js CHANGED
@@ -6,6 +6,7 @@ import { notEmpty } 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
+ import net from "node:net";
9
10
  import { defaultHttpDispatcher } from "@milaboratories/pl-http";
10
11
  import { parseHttpAuth } from "@milaboratories/pl-model-common";
11
12
  //#region src/local/pl.ts
@@ -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;
@@ -68,7 +70,9 @@ var LocalPl = class {
68
70
  return this.wasStopped;
69
71
  }
70
72
  async isAlive() {
71
- return await isProcessAlive(notEmpty(this.pid));
73
+ if (!await isProcessAlive(notEmpty(this.pid))) return false;
74
+ if (this.grpcPort === void 0) return true;
75
+ return await tcpProbe("127.0.0.1", this.grpcPort);
72
76
  }
73
77
  debugInfo() {
74
78
  return {
@@ -123,7 +127,7 @@ Only Basic auth is supported by the backend.`);
123
127
  args: processOpts.args,
124
128
  cwd: processOpts.opts.cwd
125
129
  });
126
- const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
130
+ const pl = new LocalPl(logger, ops.workingDir, processOpts, t, ops.grpcPort, ops.onClose, ops.onError, ops.onCloseAndError, ops.onCloseAndErrorNoStop);
127
131
  await pl.start();
128
132
  return pl;
129
133
  });
@@ -173,6 +177,30 @@ function mergeDefaultOps(ops, numCpu) {
173
177
  ...withoutSpawnOps
174
178
  };
175
179
  }
180
+ /** Resolves true if a TCP connect to host:port succeeds within timeoutMs. Used by
181
+ * {@link LocalPl.isAlive} to verify the gRPC listener has actually bound — the backend
182
+ * process can be alive long before its server sockets are accepting connections. */
183
+ async function tcpProbe(host, port, timeoutMs = 500) {
184
+ return await new Promise((resolve) => {
185
+ const socket = net.connect({
186
+ host,
187
+ port
188
+ });
189
+ const finalize = (ok) => {
190
+ socket.destroy();
191
+ resolve(ok);
192
+ };
193
+ const timer = setTimeout(() => finalize(false), timeoutMs);
194
+ socket.once("connect", () => {
195
+ clearTimeout(timer);
196
+ finalize(true);
197
+ });
198
+ socket.once("error", () => {
199
+ clearTimeout(timer);
200
+ finalize(false);
201
+ });
202
+ });
203
+ }
176
204
  /** Gets default options for a platforma local binary
177
205
  * and overrides them with the provided options. */
178
206
  function plProcessOps(binaryPath, configPath, ops, workDir, defaultEnv) {
@@ -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":"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"}
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 net from \"node:net\";\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 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 async isAlive(): Promise<boolean> {\n if (!(await isProcessAlive(notEmpty(this.pid)))) return false;\n if (this.grpcPort === undefined) return true;\n return await tcpProbe(\"127.0.0.1\", this.grpcPort);\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 * When set, {@link LocalPl.isAlive} additionally probes this TCP port so callers\n * can distinguish a process that is alive but still binding from a process that is\n * actually accepting gRPC connections.\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/** Resolves true if a TCP connect to host:port succeeds within timeoutMs. Used by\n * {@link LocalPl.isAlive} to verify the gRPC listener has actually bound — the backend\n * process can be alive long before its server sockets are accepting connections. */\nasync function tcpProbe(host: string, port: number, timeoutMs = 500): Promise<boolean> {\n return await new Promise<boolean>((resolve) => {\n const socket = net.connect({ host, port });\n const finalize = (ok: boolean) => {\n socket.destroy();\n resolve(ok);\n };\n const timer = setTimeout(() => finalize(false), timeoutMs);\n socket.once(\"connect\", () => {\n clearTimeout(timer);\n finalize(true);\n });\n socket.once(\"error\", () => {\n clearTimeout(timer);\n finalize(false);\n });\n });\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":";;;;;;;;;;;;AAmBA,MAAa,kBAAkB;;;;;;AAO/B,IAAa,UAAb,MAAqB;CACnB;CACA;CACA,QAAwB;CACxB,iBAAgC,EAAE;CAClC,aAAqB;CAErB,YACE,QACA,YACA,cACA,qBACA,UACA,SACA,SACA,iBACA,uBACA;AATiB,OAAA,SAAA;AACA,OAAA,aAAA;AACA,OAAA,eAAA;AACA,OAAA,sBAAA;AACA,OAAA,WAAA;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,MAAI,CAAE,MAAM,eAAe,SAAS,KAAK,IAAI,CAAC,CAAG,QAAO;AACxD,MAAI,KAAK,aAAa,KAAA,EAAW,QAAO;AACxC,SAAO,MAAM,SAAS,aAAa,KAAK,SAAS;;CAGnD,YAAY;AACV,SAAO;GACL,gBAAgB,KAAK;GACrB,OAAO,KAAK;GACZ,KAAK,KAAK;GACV,YAAY,KAAK;GACjB,qBAAqB,KAAK;GAC1B,YAAY,KAAK;GAClB;;;;;;AA8CL,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,UACJ,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;;;;;AAM1C,eAAe,SAAS,MAAc,MAAc,YAAY,KAAuB;AACrF,QAAO,MAAM,IAAI,SAAkB,YAAY;EAC7C,MAAM,SAAS,IAAI,QAAQ;GAAE;GAAM;GAAM,CAAC;EAC1C,MAAM,YAAY,OAAgB;AAChC,UAAO,SAAS;AAChB,WAAQ,GAAG;;EAEb,MAAM,QAAQ,iBAAiB,SAAS,MAAM,EAAE,UAAU;AAC1D,SAAO,KAAK,iBAAiB;AAC3B,gBAAa,MAAM;AACnB,YAAS,KAAK;IACd;AACF,SAAO,KAAK,eAAe;AACzB,gBAAa,MAAM;AACnB,YAAS,MAAM;IACf;GACF;;;;AAKJ,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"}
package/dist/ssh/pl.cjs CHANGED
@@ -8,9 +8,9 @@ const require_connection_info = require("./connection_info.cjs");
8
8
  let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
9
9
  let upath = require("upath");
10
10
  upath = require_runtime.__toESM(upath);
11
- let _milaboratories_pl_http = require("@milaboratories/pl-http");
12
11
  let node_net = require("node:net");
13
12
  node_net = require_runtime.__toESM(node_net);
13
+ let _milaboratories_pl_http = require("@milaboratories/pl-http");
14
14
  let _milaboratories_pl_config = require("@milaboratories/pl-config");
15
15
  //#region src/ssh/pl.ts
16
16
  const minRequiredGlibcVersion = 2.28;
package/dist/ssh/pl.js CHANGED
@@ -6,8 +6,8 @@ import { generateSupervisordConfig, generateSupervisordConfigWithMinio, isAllAli
6
6
  import { newConnectionInfo, parseConnectionInfo, stringifyConnectionInfo } from "./connection_info.js";
7
7
  import { notEmpty, sleep } from "@milaboratories/ts-helpers";
8
8
  import upath from "upath";
9
- import { defaultHttpDispatcher } from "@milaboratories/pl-http";
10
9
  import net from "node:net";
10
+ import { defaultHttpDispatcher } from "@milaboratories/pl-http";
11
11
  import { generateSshPlConfigs, getFreePort } from "@milaboratories/pl-config";
12
12
  //#region src/ssh/pl.ts
13
13
  const minRequiredGlibcVersion = 2.28;
package/dist/ssh/ssh.cjs CHANGED
@@ -6,10 +6,10 @@ node_fs = require_runtime.__toESM(node_fs);
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  let upath = require("upath");
8
8
  upath = require_runtime.__toESM(upath);
9
- let ssh2 = require("ssh2");
10
- ssh2 = require_runtime.__toESM(ssh2);
11
9
  let node_net = require("node:net");
12
10
  node_net = require_runtime.__toESM(node_net);
11
+ let ssh2 = require("ssh2");
12
+ ssh2 = require_runtime.__toESM(ssh2);
13
13
  let node_dns = require("node:dns");
14
14
  node_dns = require_runtime.__toESM(node_dns);
15
15
  let node_crypto = require("node:crypto");
package/dist/ssh/ssh.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MiLogger } from "@milaboratories/ts-helpers";
2
- import ssh, { Client, ConnectConfig, SFTPWrapper } from "ssh2";
3
2
  import net from "node:net";
3
+ import ssh, { Client, ConnectConfig, SFTPWrapper } from "ssh2";
4
4
 
5
5
  //#region src/ssh/ssh.d.ts
6
6
  type SshAuthMethods = "publickey" | "password";
package/dist/ssh/ssh.js CHANGED
@@ -3,8 +3,8 @@ import { Retry3TimesWithDelay, RetryablePromise, retry } from "@milaboratories/t
3
3
  import fs from "node:fs";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import upath from "upath";
6
- import ssh, { Client } from "ssh2";
7
6
  import net from "node:net";
7
+ import ssh, { Client } from "ssh2";
8
8
  import dns from "node:dns";
9
9
  import { randomBytes } from "node:crypto";
10
10
  //#region src/ssh/ssh.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-deployments",
3
- "version": "2.17.18",
3
+ "version": "2.18.0",
4
4
  "description": "MiLaboratories Platforma Backend code service run wrapper",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -33,8 +33,8 @@
33
33
  "yaml": "^2.8.0",
34
34
  "zod": "~3.25.76",
35
35
  "@milaboratories/pl-http": "1.2.4",
36
- "@milaboratories/pl-model-common": "1.42.0",
37
36
  "@milaboratories/ts-helpers": "1.8.2",
37
+ "@milaboratories/pl-model-common": "1.42.0",
38
38
  "@milaboratories/pl-config": "1.8.1"
39
39
  },
40
40
  "devDependencies": {
@@ -46,9 +46,9 @@
46
46
  "typescript": "~5.9.3",
47
47
  "utility-types": "^3.11.0",
48
48
  "vitest": "^4.1.3",
49
- "@milaboratories/ts-builder": "1.4.0",
49
+ "@milaboratories/build-configs": "2.0.0",
50
50
  "@milaboratories/ts-configs": "1.2.3",
51
- "@milaboratories/build-configs": "2.0.0"
51
+ "@milaboratories/ts-builder": "1.5.0"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22.19.0"
package/src/local/pl.ts CHANGED
@@ -10,6 +10,7 @@ import type { Trace } from "./trace";
10
10
  import { withTrace } from "./trace";
11
11
  import upath from "upath";
12
12
  import fsp from "node:fs/promises";
13
+ import net from "node:net";
13
14
  import type { Required } from "utility-types";
14
15
  import * as os from "node:os";
15
16
  import type { ProxySettings } from "@milaboratories/pl-http";
@@ -35,6 +36,7 @@ export class LocalPl {
35
36
  private readonly workingDir: string,
36
37
  private readonly startOptions: ProcessOptions,
37
38
  private readonly initialStartHistory: Trace,
39
+ private readonly grpcPort?: number,
38
40
  private readonly onClose?: (pl: LocalPl) => Promise<void>,
39
41
  private readonly onError?: (pl: LocalPl) => Promise<void>,
40
42
  private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,
@@ -94,7 +96,9 @@ export class LocalPl {
94
96
  }
95
97
 
96
98
  async isAlive(): Promise<boolean> {
97
- return await isProcessAlive(notEmpty(this.pid));
99
+ if (!(await isProcessAlive(notEmpty(this.pid)))) return false;
100
+ if (this.grpcPort === undefined) return true;
101
+ return await tcpProbe("127.0.0.1", this.grpcPort);
98
102
  }
99
103
 
100
104
  debugInfo() {
@@ -132,6 +136,14 @@ export type LocalPlOptions = {
132
136
  */
133
137
  readonly proxy?: ProxySettings;
134
138
 
139
+ /**
140
+ * gRPC port the backend listens on (always on 127.0.0.1 for local).
141
+ * When set, {@link LocalPl.isAlive} additionally probes this TCP port so callers
142
+ * can distinguish a process that is alive but still binding from a process that is
143
+ * actually accepting gRPC connections.
144
+ */
145
+ readonly grpcPort?: number;
146
+
135
147
  readonly onClose?: (pl: LocalPl) => Promise<void>;
136
148
  readonly onError?: (pl: LocalPl) => Promise<void>;
137
149
  readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;
@@ -204,6 +216,7 @@ Only Basic auth is supported by the backend.`);
204
216
  ops.workingDir,
205
217
  processOpts,
206
218
  t,
219
+ ops.grpcPort,
207
220
  ops.onClose,
208
221
  ops.onError,
209
222
  ops.onCloseAndError,
@@ -274,6 +287,28 @@ export function mergeDefaultOps(ops: LocalPlOptions, numCpu: number): LocalPlOpt
274
287
  return { ...result, ...withoutSpawnOps };
275
288
  }
276
289
 
290
+ /** Resolves true if a TCP connect to host:port succeeds within timeoutMs. Used by
291
+ * {@link LocalPl.isAlive} to verify the gRPC listener has actually bound — the backend
292
+ * process can be alive long before its server sockets are accepting connections. */
293
+ async function tcpProbe(host: string, port: number, timeoutMs = 500): Promise<boolean> {
294
+ return await new Promise<boolean>((resolve) => {
295
+ const socket = net.connect({ host, port });
296
+ const finalize = (ok: boolean) => {
297
+ socket.destroy();
298
+ resolve(ok);
299
+ };
300
+ const timer = setTimeout(() => finalize(false), timeoutMs);
301
+ socket.once("connect", () => {
302
+ clearTimeout(timer);
303
+ finalize(true);
304
+ });
305
+ socket.once("error", () => {
306
+ clearTimeout(timer);
307
+ finalize(false);
308
+ });
309
+ });
310
+ }
311
+
277
312
  /** Gets default options for a platforma local binary
278
313
  * and overrides them with the provided options. */
279
314
  export function plProcessOps(