@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/dist/ssh/pl.cjs +146 -6
- package/dist/ssh/pl.cjs.map +1 -1
- package/dist/ssh/pl.d.ts +15 -3
- package/dist/ssh/pl.d.ts.map +1 -1
- package/dist/ssh/pl.js +147 -7
- package/dist/ssh/pl.js.map +1 -1
- package/dist/ssh/pl_paths.cjs +4 -0
- package/dist/ssh/pl_paths.cjs.map +1 -1
- package/dist/ssh/pl_paths.d.ts +1 -0
- package/dist/ssh/pl_paths.d.ts.map +1 -1
- package/dist/ssh/pl_paths.js +4 -1
- package/dist/ssh/pl_paths.js.map +1 -1
- package/package.json +4 -4
- package/src/ssh/__tests__/pl-docker.test.ts +30 -1
- package/src/ssh/pl.ts +174 -0
- package/src/ssh/pl_paths.ts +4 -0
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'
|
package/src/ssh/pl_paths.ts
CHANGED
|
@@ -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
|
+
}
|