spindb 0.36.1 → 0.36.2
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/core/library-env.ts +120 -0
- package/engines/mariadb/index.ts +45 -14
- package/engines/redis/index.ts +39 -0
- package/engines/valkey/index.ts +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library environment utilities for dynamically-linked engine binaries.
|
|
3
|
+
*
|
|
4
|
+
* MariaDB, Redis, and Valkey hostdb binaries are linked against Homebrew's
|
|
5
|
+
* OpenSSL at absolute paths (e.g. /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib).
|
|
6
|
+
* On systems without that library, they fail with cryptic dyld errors.
|
|
7
|
+
*
|
|
8
|
+
* This module provides:
|
|
9
|
+
* - getLibraryEnv(): sets DYLD_FALLBACK_LIBRARY_PATH / LD_LIBRARY_PATH so
|
|
10
|
+
* the dynamic linker checks {binPath}/lib first (preparing for when hostdb
|
|
11
|
+
* bundles dylibs alongside binaries).
|
|
12
|
+
* - detectLibraryError(): scans process output for library-loading patterns
|
|
13
|
+
* and returns an actionable error message.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { platform as osPlatform } from 'os'
|
|
17
|
+
import { join } from 'path'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns env vars that point the dynamic linker at {binPath}/lib.
|
|
21
|
+
* On macOS: DYLD_FALLBACK_LIBRARY_PATH
|
|
22
|
+
* On Linux: LD_LIBRARY_PATH
|
|
23
|
+
* On Windows: returns undefined (not applicable).
|
|
24
|
+
*
|
|
25
|
+
* Usage: spread into spawn env: `{ ...process.env, ...getLibraryEnv(binPath) }`
|
|
26
|
+
*/
|
|
27
|
+
export function getLibraryEnv(
|
|
28
|
+
binPath: string,
|
|
29
|
+
): Record<string, string> | undefined {
|
|
30
|
+
const plat = osPlatform()
|
|
31
|
+
const libDir = join(binPath, 'lib')
|
|
32
|
+
|
|
33
|
+
if (plat === 'darwin') {
|
|
34
|
+
return { DYLD_FALLBACK_LIBRARY_PATH: libDir }
|
|
35
|
+
}
|
|
36
|
+
if (plat === 'linux') {
|
|
37
|
+
return { LD_LIBRARY_PATH: libDir }
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scans stderr/log output for dynamic library loading errors and returns
|
|
44
|
+
* an actionable message, or null if no library error was detected.
|
|
45
|
+
*/
|
|
46
|
+
export function detectLibraryError(
|
|
47
|
+
output: string,
|
|
48
|
+
engineName: string,
|
|
49
|
+
): string | null {
|
|
50
|
+
if (!output) return null
|
|
51
|
+
|
|
52
|
+
const plat = osPlatform()
|
|
53
|
+
const lower = output.toLowerCase()
|
|
54
|
+
|
|
55
|
+
// macOS dyld errors
|
|
56
|
+
if (
|
|
57
|
+
lower.includes('library not loaded') ||
|
|
58
|
+
lower.includes('dyld:') ||
|
|
59
|
+
lower.includes('dyld[')
|
|
60
|
+
) {
|
|
61
|
+
const needsOpenssl =
|
|
62
|
+
lower.includes('libssl') || lower.includes('libcrypto')
|
|
63
|
+
|
|
64
|
+
if (needsOpenssl && plat === 'darwin') {
|
|
65
|
+
return (
|
|
66
|
+
`${engineName} failed to start: missing OpenSSL libraries.\n` +
|
|
67
|
+
`The downloaded binary requires OpenSSL 3 which is not installed.\n` +
|
|
68
|
+
`Fix: brew install openssl@3\n` +
|
|
69
|
+
`Alternatively, re-download binaries after hostdb ships relocatable builds.`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
`${engineName} failed to start: a required dynamic library could not be loaded.\n` +
|
|
75
|
+
`This typically means the hostdb binary was built against libraries not present on this system.\n` +
|
|
76
|
+
(plat === 'darwin'
|
|
77
|
+
? `Try: brew install openssl@3\n`
|
|
78
|
+
: `Try: sudo apt-get install libssl-dev (or the equivalent for your distro)\n`) +
|
|
79
|
+
`See: https://github.com/robertjbass/hostdb/issues`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Linux GLIBC version errors
|
|
84
|
+
if (lower.includes('glibc') || lower.includes('libc.so')) {
|
|
85
|
+
return (
|
|
86
|
+
`${engineName} failed to start: incompatible system C library (GLIBC).\n` +
|
|
87
|
+
`The downloaded binary requires a newer GLIBC version than is installed.\n` +
|
|
88
|
+
`Options:\n` +
|
|
89
|
+
` - Upgrade your OS to a newer version\n` +
|
|
90
|
+
` - Use Docker: spindb can run inside containers with newer GLIBC\n` +
|
|
91
|
+
`See: https://github.com/robertjbass/hostdb/issues`
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Generic shared library errors on Linux
|
|
96
|
+
if (
|
|
97
|
+
lower.includes('error while loading shared libraries') ||
|
|
98
|
+
lower.includes('cannot open shared object file')
|
|
99
|
+
) {
|
|
100
|
+
const needsOpenssl =
|
|
101
|
+
lower.includes('libssl') || lower.includes('libcrypto')
|
|
102
|
+
|
|
103
|
+
if (needsOpenssl) {
|
|
104
|
+
return (
|
|
105
|
+
`${engineName} failed to start: missing OpenSSL libraries.\n` +
|
|
106
|
+
`Fix: sudo apt-get install libssl-dev (Debian/Ubuntu)\n` +
|
|
107
|
+
` sudo dnf install openssl-devel (Fedora/RHEL)\n` +
|
|
108
|
+
`See: https://github.com/robertjbass/hostdb/issues`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
`${engineName} failed to start: a required shared library is missing.\n` +
|
|
114
|
+
`Check the error output above for the specific library name and install it.\n` +
|
|
115
|
+
`See: https://github.com/robertjbass/hostdb/issues`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null
|
|
120
|
+
}
|
package/engines/mariadb/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
type UserCredentials,
|
|
53
53
|
} from '../../types'
|
|
54
54
|
import { parseTSVToQueryResult } from '../../core/query-parser'
|
|
55
|
+
import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
|
|
55
56
|
|
|
56
57
|
const execAsync = promisify(exec)
|
|
57
58
|
|
|
@@ -256,18 +257,27 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
256
257
|
const cmd = `"${installDb}" --datadir="${dataDir}"`
|
|
257
258
|
|
|
258
259
|
return new Promise((resolve, reject) => {
|
|
259
|
-
exec(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
260
|
+
exec(
|
|
261
|
+
cmd,
|
|
262
|
+
{ timeout: 120000, env: { ...process.env, ...getLibraryEnv(binPath) } },
|
|
263
|
+
async (error, stdout, stderr) => {
|
|
264
|
+
if (error) {
|
|
265
|
+
await cleanupOnFailure()
|
|
266
|
+
const libError = detectLibraryError(
|
|
267
|
+
stderr || stdout || error.message,
|
|
268
|
+
'MariaDB',
|
|
269
|
+
)
|
|
270
|
+
reject(
|
|
271
|
+
new Error(
|
|
272
|
+
libError ||
|
|
273
|
+
`MariaDB initialization failed with code ${error.code}: ${stderr || stdout || error.message}`,
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
} else {
|
|
277
|
+
resolve(dataDir)
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
)
|
|
271
281
|
})
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -293,6 +303,7 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
293
303
|
return new Promise((resolve, reject) => {
|
|
294
304
|
const proc = spawn(installDb, args, {
|
|
295
305
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
|
+
env: { ...process.env, ...getLibraryEnv(binPath) },
|
|
296
307
|
})
|
|
297
308
|
|
|
298
309
|
let stdout = ''
|
|
@@ -310,9 +321,11 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
310
321
|
resolve(dataDir)
|
|
311
322
|
} else {
|
|
312
323
|
await cleanupOnFailure()
|
|
324
|
+
const libError = detectLibraryError(stderr || stdout, 'MariaDB')
|
|
313
325
|
reject(
|
|
314
326
|
new Error(
|
|
315
|
-
|
|
327
|
+
libError ||
|
|
328
|
+
`MariaDB initialization failed with code ${code}: ${stderr || stdout}`,
|
|
316
329
|
),
|
|
317
330
|
)
|
|
318
331
|
}
|
|
@@ -384,11 +397,14 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
384
397
|
|
|
385
398
|
let proc: ReturnType<typeof spawn> | null = null
|
|
386
399
|
|
|
400
|
+
const libraryEnv = getLibraryEnv(binPath)
|
|
401
|
+
|
|
387
402
|
if (isWindows()) {
|
|
388
403
|
proc = spawn(mysqld, args, {
|
|
389
404
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
390
405
|
detached: true,
|
|
391
406
|
windowsHide: true,
|
|
407
|
+
env: { ...process.env, ...libraryEnv },
|
|
392
408
|
})
|
|
393
409
|
|
|
394
410
|
proc.stdout?.on('data', (data: Buffer) => {
|
|
@@ -403,6 +419,7 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
403
419
|
proc = spawn(mysqld, args, {
|
|
404
420
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
405
421
|
detached: true,
|
|
422
|
+
env: { ...process.env, ...libraryEnv },
|
|
406
423
|
})
|
|
407
424
|
proc.unref()
|
|
408
425
|
}
|
|
@@ -465,7 +482,21 @@ export class MariaDBEngine extends BaseEngine {
|
|
|
465
482
|
if (proc) {
|
|
466
483
|
proc.removeListener('error', errorHandler)
|
|
467
484
|
}
|
|
468
|
-
|
|
485
|
+
|
|
486
|
+
// Check log file for library errors
|
|
487
|
+
let libError: string | null = null
|
|
488
|
+
try {
|
|
489
|
+
const logContent = await readFile(logFile, 'utf-8')
|
|
490
|
+
libError = detectLibraryError(logContent, 'MariaDB')
|
|
491
|
+
} catch {
|
|
492
|
+
// Log file might not exist
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
reject(
|
|
496
|
+
new Error(
|
|
497
|
+
libError || 'MariaDB failed to start within timeout',
|
|
498
|
+
),
|
|
499
|
+
)
|
|
469
500
|
}
|
|
470
501
|
}
|
|
471
502
|
}
|
package/engines/redis/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type UserCredentials,
|
|
46
46
|
} from '../../types'
|
|
47
47
|
import { parseRedisResult } from '../../core/query-parser'
|
|
48
|
+
import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
|
|
48
49
|
|
|
49
50
|
const execAsync = promisify(exec)
|
|
50
51
|
|
|
@@ -477,6 +478,12 @@ export class RedisEngine extends BaseEngine {
|
|
|
477
478
|
|
|
478
479
|
logDebug(`Using redis-server for version ${version}: ${redisServer}`)
|
|
479
480
|
|
|
481
|
+
// Compute library fallback paths from the binary directory
|
|
482
|
+
// redisServer is e.g. /path/to/redis-8.4.0-darwin-arm64/bin/redis-server
|
|
483
|
+
// We need the parent directory (without /bin/redis-server)
|
|
484
|
+
const binBaseDir = binaryPath || this.getBinaryPath(version)
|
|
485
|
+
const libraryEnv = getLibraryEnv(binBaseDir)
|
|
486
|
+
|
|
480
487
|
const containerDir = paths.getContainerPath(name, { engine: ENGINE })
|
|
481
488
|
const configPath = join(containerDir, 'redis.conf')
|
|
482
489
|
const dataDir = paths.getContainerDataPath(name, { engine: ENGINE })
|
|
@@ -533,6 +540,7 @@ export class RedisEngine extends BaseEngine {
|
|
|
533
540
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
534
541
|
detached: true,
|
|
535
542
|
windowsHide: true,
|
|
543
|
+
env: { ...process.env, ...libraryEnv },
|
|
536
544
|
}
|
|
537
545
|
|
|
538
546
|
// Convert Windows path to Cygwin format for MSYS2/Cygwin-built binaries
|
|
@@ -603,6 +611,16 @@ export class RedisEngine extends BaseEngine {
|
|
|
603
611
|
logContent = '(log file not found or empty)'
|
|
604
612
|
}
|
|
605
613
|
|
|
614
|
+
// Check for library loading errors first
|
|
615
|
+
const libError = detectLibraryError(
|
|
616
|
+
stderrOutput + logContent,
|
|
617
|
+
'Redis',
|
|
618
|
+
)
|
|
619
|
+
if (libError) {
|
|
620
|
+
reject(new Error(libError))
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
|
|
606
624
|
const errorDetails = [
|
|
607
625
|
portError || 'Redis failed to start within timeout.',
|
|
608
626
|
`Binary: ${redisServer}`,
|
|
@@ -625,6 +643,7 @@ export class RedisEngine extends BaseEngine {
|
|
|
625
643
|
return new Promise((resolve, reject) => {
|
|
626
644
|
const proc = spawn(redisServer, [configPath], {
|
|
627
645
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
646
|
+
env: { ...process.env, ...libraryEnv },
|
|
628
647
|
})
|
|
629
648
|
|
|
630
649
|
let stdout = ''
|
|
@@ -678,6 +697,16 @@ export class RedisEngine extends BaseEngine {
|
|
|
678
697
|
logContent = '(log file not found or empty)'
|
|
679
698
|
}
|
|
680
699
|
|
|
700
|
+
// Check for library loading errors
|
|
701
|
+
const libError = detectLibraryError(
|
|
702
|
+
stderr + logContent,
|
|
703
|
+
'Redis',
|
|
704
|
+
)
|
|
705
|
+
if (libError) {
|
|
706
|
+
reject(new Error(libError))
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
|
|
681
710
|
const errorDetails = [
|
|
682
711
|
'Redis failed to start within timeout.',
|
|
683
712
|
`Binary: ${redisServer}`,
|
|
@@ -700,6 +729,16 @@ export class RedisEngine extends BaseEngine {
|
|
|
700
729
|
logContent = ''
|
|
701
730
|
}
|
|
702
731
|
|
|
732
|
+
// Check for library loading errors on non-zero exit
|
|
733
|
+
const libError = detectLibraryError(
|
|
734
|
+
stderr + stdout + logContent,
|
|
735
|
+
'Redis',
|
|
736
|
+
)
|
|
737
|
+
if (libError) {
|
|
738
|
+
reject(new Error(libError))
|
|
739
|
+
return
|
|
740
|
+
}
|
|
741
|
+
|
|
703
742
|
const errorDetails = [
|
|
704
743
|
stderr || stdout || `redis-server exited with code ${code}`,
|
|
705
744
|
logContent ? `Log content:\n${logContent}` : '',
|
package/engines/valkey/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type UserCredentials,
|
|
46
46
|
} from '../../types'
|
|
47
47
|
import { parseRedisResult } from '../../core/query-parser'
|
|
48
|
+
import { getLibraryEnv, detectLibraryError } from '../../core/library-env'
|
|
48
49
|
|
|
49
50
|
const execAsync = promisify(exec)
|
|
50
51
|
|
|
@@ -486,6 +487,10 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
486
487
|
|
|
487
488
|
logDebug(`Using valkey-server for version ${version}: ${valkeyServer}`)
|
|
488
489
|
|
|
490
|
+
// Compute library fallback paths from the binary directory
|
|
491
|
+
const binBaseDir = binaryPath || this.getBinaryPath(version)
|
|
492
|
+
const libraryEnv = getLibraryEnv(binBaseDir)
|
|
493
|
+
|
|
489
494
|
const containerDir = paths.getContainerPath(name, { engine: ENGINE })
|
|
490
495
|
const configPath = join(containerDir, 'valkey.conf')
|
|
491
496
|
const dataDir = paths.getContainerDataPath(name, { engine: ENGINE })
|
|
@@ -542,6 +547,7 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
542
547
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
543
548
|
detached: true,
|
|
544
549
|
windowsHide: true,
|
|
550
|
+
env: { ...process.env, ...libraryEnv },
|
|
545
551
|
}
|
|
546
552
|
|
|
547
553
|
// Convert Windows path to Cygwin format for Cygwin-built binaries
|
|
@@ -612,6 +618,16 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
612
618
|
logContent = '(log file not found or empty)'
|
|
613
619
|
}
|
|
614
620
|
|
|
621
|
+
// Check for library loading errors first
|
|
622
|
+
const libError = detectLibraryError(
|
|
623
|
+
stderrOutput + logContent,
|
|
624
|
+
'Valkey',
|
|
625
|
+
)
|
|
626
|
+
if (libError) {
|
|
627
|
+
reject(new Error(libError))
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
|
|
615
631
|
const errorDetails = [
|
|
616
632
|
portError || 'Valkey failed to start within timeout.',
|
|
617
633
|
`Binary: ${valkeyServer}`,
|
|
@@ -634,6 +650,7 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
634
650
|
return new Promise((resolve, reject) => {
|
|
635
651
|
const proc = spawn(valkeyServer, [configPath], {
|
|
636
652
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
653
|
+
env: { ...process.env, ...libraryEnv },
|
|
637
654
|
})
|
|
638
655
|
|
|
639
656
|
let stdout = ''
|
|
@@ -678,6 +695,23 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
678
695
|
reject(new Error(portError))
|
|
679
696
|
return
|
|
680
697
|
}
|
|
698
|
+
|
|
699
|
+
// Check for library loading errors
|
|
700
|
+
let logContent = ''
|
|
701
|
+
try {
|
|
702
|
+
logContent = await readFile(logFile, 'utf-8')
|
|
703
|
+
} catch {
|
|
704
|
+
logContent = ''
|
|
705
|
+
}
|
|
706
|
+
const libError = detectLibraryError(
|
|
707
|
+
stderr + logContent,
|
|
708
|
+
'Valkey',
|
|
709
|
+
)
|
|
710
|
+
if (libError) {
|
|
711
|
+
reject(new Error(libError))
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
|
|
681
715
|
reject(
|
|
682
716
|
new Error(
|
|
683
717
|
`Valkey failed to start within timeout. Check logs at: ${logFile}`,
|
|
@@ -685,6 +719,12 @@ export class ValkeyEngine extends BaseEngine {
|
|
|
685
719
|
)
|
|
686
720
|
}
|
|
687
721
|
} else {
|
|
722
|
+
// Check for library loading errors on non-zero exit
|
|
723
|
+
const libError = detectLibraryError(stderr || stdout, 'Valkey')
|
|
724
|
+
if (libError) {
|
|
725
|
+
reject(new Error(libError))
|
|
726
|
+
return
|
|
727
|
+
}
|
|
688
728
|
reject(
|
|
689
729
|
new Error(
|
|
690
730
|
stderr || stdout || `valkey-server exited with code ${code}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"author": "Bob Bass <bob@bbass.co>",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
|