@milaboratories/pl-deployments 2.12.5 → 2.12.7

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/src/ssh/pl.ts CHANGED
@@ -7,6 +7,9 @@ import { downloadBinaryNoExtract } from '../common/pl_binary_download';
7
7
  import upath from 'upath';
8
8
  import * as plpath from './pl_paths';
9
9
  import { getDefaultPlVersion } from '../common/pl_version';
10
+ import type { ProxySettings } from '@milaboratories/pl-http';
11
+ import { defaultHttpDispatcher } from '@milaboratories/pl-http';
12
+ import type { Dispatcher } from 'undici';
10
13
 
11
14
  import net from 'node:net';
12
15
  import type { PlConfig, PlLicenseMode, SshPlConfigGenerationResult } from '@milaboratories/pl-config';
@@ -148,6 +151,7 @@ export class SshPl {
148
151
  return state.existedSettings!;
149
152
  }
150
153
  await this.doStepStopExistedPlatforma(state, onProgress);
154
+ await this.doStepCheckDbLock(state, onProgress);
151
155
 
152
156
  await onProgress?.('Installation platforma...');
153
157
 
@@ -213,6 +217,65 @@ export class SshPl {
213
217
  await onProgress?.('Connection information saved.');
214
218
  }
215
219
 
220
+ private async doStepCheckDbLock(
221
+ state: PlatformaInitState,
222
+ onProgress?: (...args: any[]) => Promise<any>,
223
+ ) {
224
+ const removeLockFile = async (lockFilePath: string) => {
225
+ try {
226
+ await this.sshClient.exec(`rm -f ${lockFilePath}`);
227
+ this.logger.info(`Removed stale lock file ${lockFilePath}`);
228
+ } catch (e: unknown) {
229
+ const msg = `Failed to remove stale lock file ${lockFilePath}: ${e}`;
230
+ this.logger.error(msg);
231
+ throw new Error(msg);
232
+ }
233
+ };
234
+
235
+ state.step = 'checkDbLock';
236
+ await onProgress?.('Checking for DB lock...');
237
+
238
+ const lockFilePath = plpath.platformaDbLock(state.remoteHome!);
239
+ const lockFileExists = await this.sshClient.checkFileExists(lockFilePath);
240
+
241
+ if (!lockFileExists) {
242
+ await onProgress?.('No DB lock found. Proceeding...');
243
+ return;
244
+ }
245
+
246
+ this.logger.info(`DB lock file found at ${lockFilePath}. Checking which process holds it...`);
247
+ const lockProcessInfo = await this.findLockHolder(lockFilePath);
248
+
249
+ if (!lockProcessInfo) {
250
+ this.logger.warn('Lock file exists but no process is holding it. Removing stale lock file...');
251
+ await removeLockFile(lockFilePath);
252
+ return;
253
+ }
254
+
255
+ this.logger.info(
256
+ `Found process ${lockProcessInfo.pid} (user: ${lockProcessInfo.user}) holding DB lock`,
257
+ );
258
+
259
+ if (lockProcessInfo.user !== this.username) {
260
+ const msg
261
+ = `DB lock is held by process ${lockProcessInfo.pid} `
262
+ + `owned by user '${lockProcessInfo.user}', but current user is '${this.username}'. `
263
+ + 'Cannot kill process owned by different user.';
264
+ this.logger.error(msg);
265
+ throw new Error(msg);
266
+ }
267
+
268
+ this.logger.info(`Process ${lockProcessInfo.pid} belongs to current user ${this.username}. Killing it...`);
269
+ await this.killRemoteProcess(lockProcessInfo.pid);
270
+ this.logger.info('Process holding DB lock has been terminated.');
271
+
272
+ // Verify lock file is gone or can be removed
273
+ const lockStillExists = await this.sshClient.checkFileExists(lockFilePath);
274
+ if (lockStillExists) {
275
+ await removeLockFile(lockFilePath);
276
+ }
277
+ }
278
+
216
279
  private async doStepConfigureSupervisord(state: PlatformaInitState, onProgress: ((...args: any) => Promise<any>) | undefined) {
217
280
  await onProgress?.('Writing supervisord configuration...');
218
281
  state.step = 'configureSupervisord';
@@ -317,6 +380,7 @@ export class SshPl {
317
380
 
318
381
  const downloadRes = await this.downloadBinariesAndUploadToTheServer(
319
382
  ops.localWorkdir, ops.plBinary!, state.remoteHome!, state.arch!, state.shouldUseMinio ?? false,
383
+ ops.proxy,
320
384
  );
321
385
  await onProgress?.('All required binaries have been downloaded and uploaded.');
322
386
 
@@ -387,18 +451,22 @@ export class SshPl {
387
451
  remoteHome: string,
388
452
  arch: Arch,
389
453
  shouldUseMinio: boolean,
454
+ proxy?: ProxySettings,
390
455
  ) {
391
456
  const state: DownloadAndUntarState[] = [];
457
+ const dispatcher = defaultHttpDispatcher(proxy);
392
458
  try {
393
459
  const pl = await this.downloadAndUntar(
394
460
  localWorkdir, remoteHome, arch,
395
461
  'pl', `pl-${plBinary.version}`,
462
+ dispatcher,
396
463
  );
397
464
  state.push(pl);
398
465
 
399
466
  const supervisor = await this.downloadAndUntar(
400
467
  localWorkdir, remoteHome, arch,
401
468
  'supervisord', plpath.supervisordDirName,
469
+ dispatcher,
402
470
  );
403
471
  state.push(supervisor);
404
472
 
@@ -407,6 +475,7 @@ export class SshPl {
407
475
  const minio = await this.downloadAndUntar(
408
476
  localWorkdir, remoteHome, arch,
409
477
  'minio', plpath.minioDirName,
478
+ dispatcher,
410
479
  );
411
480
  state.push(minio);
412
481
  await this.sshClient.chmod(minioPath, 0o750);
@@ -421,6 +490,105 @@ export class SshPl {
421
490
  const msg = `SshPl.downloadBinariesAndUploadToServer: ${e}, state: ${JSON.stringify(state)}`;
422
491
  this.logger.error(msg);
423
492
  throw e;
493
+ } finally {
494
+ await dispatcher.close();
495
+ }
496
+ }
497
+
498
+ private async findLockHolderWithLsof(lockFilePath: string): Promise<LockProcessInfo | null> {
499
+ try {
500
+ const { stdout } = await this.sshClient.exec(`lsof ${lockFilePath} 2>/dev/null || true`);
501
+ const output = stdout.trim();
502
+ if (!output) {
503
+ return null;
504
+ }
505
+
506
+ // Example:
507
+ // COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
508
+ // platforma 11628 rfiskov 10u REG 1,16 0 66670038 ./LOCK
509
+ const lines = output.split('\n');
510
+ if (lines.length <= 1) {
511
+ return null;
512
+ }
513
+
514
+ const parts = lines[1].trim().split(/\s+/);
515
+ if (parts.length < 3) {
516
+ return null;
517
+ }
518
+
519
+ const pid = Number.parseInt(parts[1], 10);
520
+ const user = parts[2];
521
+
522
+ return Number.isNaN(pid) || !user ? null : { pid, user };
523
+ } catch (e: unknown) {
524
+ this.logger.warn(`Failed to use lsof to check lock: ${e}`);
525
+ return null;
526
+ }
527
+ }
528
+
529
+ private async findLockHolderWithFuser(lockFilePath: string): Promise<LockProcessInfo | null> {
530
+ try {
531
+ const { stdout } = await this.sshClient.exec(`fuser ${lockFilePath} 2>/dev/null || true`);
532
+ const output = stdout.trim();
533
+ if (!output) {
534
+ return null;
535
+ }
536
+
537
+ // Example: ./LOCK: 11628
538
+ const match = output.match(/: (\d+)/);
539
+ if (!match) {
540
+ return null;
541
+ }
542
+
543
+ const pid = Number.parseInt(match[1], 10);
544
+ if (Number.isNaN(pid)) {
545
+ return null;
546
+ }
547
+
548
+ try {
549
+ const psResult = await this.sshClient.exec(`ps -o user= -p ${pid} 2>/dev/null || true`);
550
+ const user = psResult.stdout.trim();
551
+ return user ? { pid, user } : null;
552
+ } catch (e: unknown) {
553
+ this.logger.warn(`Failed to get user for PID ${pid}: ${e}`);
554
+ return null;
555
+ }
556
+ } catch (e: unknown) {
557
+ this.logger.warn(`Failed to use fuser to check lock: ${e}`);
558
+ return null;
559
+ }
560
+ }
561
+
562
+ private async findLockHolder(lockFilePath: string): Promise<LockProcessInfo | null> {
563
+ const viaLsof = await this.findLockHolderWithLsof(lockFilePath);
564
+ if (viaLsof) {
565
+ return viaLsof;
566
+ }
567
+ return this.findLockHolderWithFuser(lockFilePath);
568
+ }
569
+
570
+ private async killRemoteProcess(pid: number): Promise<void> {
571
+ this.logger.info(`Killing process ${pid}...`);
572
+
573
+ try {
574
+ // Try graceful termination first
575
+ await this.sshClient.exec(`kill ${pid} 2>/dev/null || true`);
576
+ await sleep(1000);
577
+
578
+ // Check if process still exists
579
+ try {
580
+ await this.sshClient.exec(`kill -0 ${pid} 2>/dev/null`);
581
+ // Process still exists, force kill
582
+ this.logger.warn(`Process ${pid} still alive after SIGTERM, forcing kill...`);
583
+ await this.sshClient.exec(`kill -9 ${pid} 2>/dev/null || true`);
584
+ await sleep(500);
585
+ } catch {
586
+ // Process is dead, nothing to do
587
+ }
588
+ } catch (e: unknown) {
589
+ const msg = `Failed to kill process ${pid}: ${e}`;
590
+ this.logger.error(msg);
591
+ throw new Error(msg);
424
592
  }
425
593
  }
426
594
 
@@ -436,6 +604,7 @@ export class SshPl {
436
604
  arch: Arch,
437
605
  softwareName: string,
438
606
  tgzName: string,
607
+ dispatcher?: Dispatcher,
439
608
  ): Promise<DownloadAndUntarState> {
440
609
  const state: DownloadAndUntarState = {};
441
610
  state.binBasePath = plpath.binariesDir(remoteHome);
@@ -453,6 +622,7 @@ export class SshPl {
453
622
  tgzName,
454
623
  arch: arch.arch,
455
624
  platform: arch.platform,
625
+ dispatcher,
456
626
  });
457
627
  break;
458
628
  } catch (e: unknown) {
@@ -611,11 +781,14 @@ export type SshPlConfig = {
611
781
  license: PlLicenseMode;
612
782
  useGlobalAccess?: boolean;
613
783
  plBinary?: PlBinarySourceDownload;
784
+ proxy?: ProxySettings;
614
785
 
615
786
  onProgress?: (...args: any) => Promise<any>;
616
787
  plConfigPostprocessing?: (config: PlConfig) => PlConfig;
617
788
  };
618
789
 
790
+ export type LockProcessInfo = { pid: number; user: string };
791
+
619
792
  const defaultSshPlConfig: Pick<
620
793
  SshPlConfig,
621
794
  | 'useGlobalAccess'
@@ -653,6 +826,7 @@ type PlatformaInitStep =
653
826
  | 'detectHome'
654
827
  | 'checkAlive'
655
828
  | 'stopExistedPlatforma'
829
+ | 'checkDbLock'
656
830
  | 'downloadBinaries'
657
831
  | 'fetchPorts'
658
832
  | 'generateNewConfig'
@@ -63,3 +63,7 @@ export function connectionInfo(remoteHome: string) {
63
63
  export function platformaCliLogs(remoteHome: string) {
64
64
  return upath.join(workDir(remoteHome), 'platforma_cli_logs.log');
65
65
  }
66
+
67
+ export function platformaDbLock(remoteHome: string) {
68
+ return upath.join(workDir(remoteHome), 'db', 'LOCK');
69
+ }