spindb 0.9.3 → 0.10.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/README.md +19 -10
- package/cli/commands/create.ts +72 -42
- package/cli/commands/engines.ts +61 -0
- package/cli/commands/logs.ts +3 -29
- package/cli/commands/menu/container-handlers.ts +32 -3
- package/cli/commands/menu/sql-handlers.ts +4 -26
- package/cli/helpers.ts +6 -6
- package/cli/index.ts +3 -3
- package/cli/utils/file-follower.ts +95 -0
- package/config/defaults.ts +3 -0
- package/config/os-dependencies.ts +79 -1
- package/core/binary-manager.ts +181 -66
- package/core/config-manager.ts +5 -65
- package/core/dependency-manager.ts +39 -1
- package/core/platform-service.ts +149 -11
- package/core/process-manager.ts +152 -33
- package/engines/base-engine.ts +27 -0
- package/engines/mysql/backup.ts +12 -5
- package/engines/mysql/index.ts +328 -110
- package/engines/mysql/restore.ts +22 -6
- package/engines/postgresql/backup.ts +7 -3
- package/engines/postgresql/binary-manager.ts +47 -31
- package/engines/postgresql/edb-binary-urls.ts +123 -0
- package/engines/postgresql/index.ts +109 -22
- package/engines/postgresql/version-maps.ts +63 -0
- package/engines/sqlite/index.ts +9 -19
- package/package.json +4 -2
package/core/platform-service.ts
CHANGED
|
@@ -121,6 +121,28 @@ export abstract class BasePlatformService {
|
|
|
121
121
|
*/
|
|
122
122
|
abstract getZonkyPlatform(): string | null
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Get the null device path for this platform ('/dev/null' on Unix, 'NUL' on Windows)
|
|
126
|
+
*/
|
|
127
|
+
abstract getNullDevice(): string
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the executable file extension for this platform ('' on Unix, '.exe' on Windows)
|
|
131
|
+
*/
|
|
132
|
+
abstract getExecutableExtension(): string
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Terminate a process by PID
|
|
136
|
+
* @param pid - Process ID to terminate
|
|
137
|
+
* @param force - If true, force kill (SIGKILL on Unix, /F on Windows)
|
|
138
|
+
*/
|
|
139
|
+
abstract terminateProcess(pid: number, force: boolean): Promise<void>
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check if a process is running by PID
|
|
143
|
+
*/
|
|
144
|
+
abstract isProcessRunning(pid: number): boolean
|
|
145
|
+
|
|
124
146
|
/**
|
|
125
147
|
* Copy text to clipboard
|
|
126
148
|
*/
|
|
@@ -153,13 +175,17 @@ export abstract class BasePlatformService {
|
|
|
153
175
|
async findToolPath(toolName: string): Promise<string | null> {
|
|
154
176
|
const whichConfig = this.getWhichCommand()
|
|
155
177
|
|
|
156
|
-
// First try the which/where command
|
|
178
|
+
// First try the which/where command (with timeout to prevent hanging)
|
|
157
179
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
const cmd = [whichConfig.command, ...whichConfig.args, toolName]
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.join(' ')
|
|
183
|
+
const { stdout } = await execAsync(cmd, { timeout: 5000 })
|
|
184
|
+
const path = stdout
|
|
185
|
+
.split(/\r?\n/)
|
|
186
|
+
.map((line) => line.trim())
|
|
187
|
+
.find((line) => line.length > 0)
|
|
188
|
+
if (path && existsSync(path)) return path
|
|
163
189
|
} catch {
|
|
164
190
|
// Not found via which, continue to search paths
|
|
165
191
|
}
|
|
@@ -186,7 +212,9 @@ export abstract class BasePlatformService {
|
|
|
186
212
|
*/
|
|
187
213
|
async getToolVersion(toolPath: string): Promise<string | null> {
|
|
188
214
|
try {
|
|
189
|
-
const { stdout } = await execAsync(`"${toolPath}" --version
|
|
215
|
+
const { stdout } = await execAsync(`"${toolPath}" --version`, {
|
|
216
|
+
timeout: 5000,
|
|
217
|
+
})
|
|
190
218
|
const match = stdout.match(/(\d+\.\d+(\.\d+)?)/)
|
|
191
219
|
return match ? match[1] : null
|
|
192
220
|
} catch {
|
|
@@ -332,6 +360,28 @@ class DarwinPlatformService extends BasePlatformService {
|
|
|
332
360
|
return null
|
|
333
361
|
}
|
|
334
362
|
|
|
363
|
+
getNullDevice(): string {
|
|
364
|
+
return '/dev/null'
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getExecutableExtension(): string {
|
|
368
|
+
return ''
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
372
|
+
const signal = force ? 'SIGKILL' : 'SIGTERM'
|
|
373
|
+
process.kill(pid, signal)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
isProcessRunning(pid: number): boolean {
|
|
377
|
+
try {
|
|
378
|
+
process.kill(pid, 0)
|
|
379
|
+
return true
|
|
380
|
+
} catch {
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
335
385
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
336
386
|
return `${dir}/${toolName}`
|
|
337
387
|
}
|
|
@@ -517,6 +567,28 @@ class LinuxPlatformService extends BasePlatformService {
|
|
|
517
567
|
return null
|
|
518
568
|
}
|
|
519
569
|
|
|
570
|
+
getNullDevice(): string {
|
|
571
|
+
return '/dev/null'
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
getExecutableExtension(): string {
|
|
575
|
+
return ''
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
579
|
+
const signal = force ? 'SIGKILL' : 'SIGTERM'
|
|
580
|
+
process.kill(pid, signal)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
isProcessRunning(pid: number): boolean {
|
|
584
|
+
try {
|
|
585
|
+
process.kill(pid, 0)
|
|
586
|
+
return true
|
|
587
|
+
} catch {
|
|
588
|
+
return false
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
520
592
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
521
593
|
return `${dir}/${toolName}`
|
|
522
594
|
}
|
|
@@ -591,9 +663,12 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
591
663
|
}
|
|
592
664
|
|
|
593
665
|
async detectPackageManager(): Promise<PackageManagerInfo | null> {
|
|
666
|
+
// Timeout for package manager detection (5 seconds)
|
|
667
|
+
const timeout = 5000
|
|
668
|
+
|
|
594
669
|
// Try chocolatey
|
|
595
670
|
try {
|
|
596
|
-
await execAsync('choco --version')
|
|
671
|
+
await execAsync('choco --version', { timeout })
|
|
597
672
|
return {
|
|
598
673
|
id: 'choco',
|
|
599
674
|
name: 'Chocolatey',
|
|
@@ -602,12 +677,12 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
602
677
|
updateCommand: 'choco upgrade all',
|
|
603
678
|
}
|
|
604
679
|
} catch {
|
|
605
|
-
// Not chocolatey
|
|
680
|
+
// Not chocolatey or timed out
|
|
606
681
|
}
|
|
607
682
|
|
|
608
683
|
// Try winget
|
|
609
684
|
try {
|
|
610
|
-
await execAsync('winget --version')
|
|
685
|
+
await execAsync('winget --version', { timeout })
|
|
611
686
|
return {
|
|
612
687
|
id: 'winget',
|
|
613
688
|
name: 'Windows Package Manager',
|
|
@@ -616,7 +691,21 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
616
691
|
updateCommand: 'winget upgrade --all',
|
|
617
692
|
}
|
|
618
693
|
} catch {
|
|
619
|
-
// Not winget
|
|
694
|
+
// Not winget or timed out
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Try scoop
|
|
698
|
+
try {
|
|
699
|
+
await execAsync('scoop --version', { timeout })
|
|
700
|
+
return {
|
|
701
|
+
id: 'scoop',
|
|
702
|
+
name: 'Scoop',
|
|
703
|
+
checkCommand: 'scoop --version',
|
|
704
|
+
installTemplate: 'scoop install {package}',
|
|
705
|
+
updateCommand: 'scoop update *',
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
// Not scoop or timed out
|
|
620
709
|
}
|
|
621
710
|
|
|
622
711
|
return null
|
|
@@ -627,6 +716,40 @@ class Win32PlatformService extends BasePlatformService {
|
|
|
627
716
|
return null
|
|
628
717
|
}
|
|
629
718
|
|
|
719
|
+
getNullDevice(): string {
|
|
720
|
+
return 'NUL'
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
getExecutableExtension(): string {
|
|
724
|
+
return '.exe'
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async terminateProcess(pid: number, force: boolean): Promise<void> {
|
|
728
|
+
// On Windows, use taskkill command
|
|
729
|
+
// /T = terminate child processes, /F = force termination
|
|
730
|
+
const args = force ? `/F /PID ${pid} /T` : `/PID ${pid}`
|
|
731
|
+
try {
|
|
732
|
+
await execAsync(`taskkill ${args}`)
|
|
733
|
+
} catch (error) {
|
|
734
|
+
// taskkill exits with error if process doesn't exist, which is fine
|
|
735
|
+
const e = error as { code?: number }
|
|
736
|
+
// Error code 128 means "process not found" which is acceptable
|
|
737
|
+
if (e.code !== 128) {
|
|
738
|
+
throw error
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
isProcessRunning(pid: number): boolean {
|
|
744
|
+
try {
|
|
745
|
+
// process.kill with signal 0 works on Windows for checking process existence
|
|
746
|
+
process.kill(pid, 0)
|
|
747
|
+
return true
|
|
748
|
+
} catch {
|
|
749
|
+
return false
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
630
753
|
protected buildToolPath(dir: string, toolName: string): string {
|
|
631
754
|
return `${dir}\\${toolName}.exe`
|
|
632
755
|
}
|
|
@@ -652,3 +775,18 @@ export function createPlatformService(): BasePlatformService {
|
|
|
652
775
|
|
|
653
776
|
// Export singleton instance for convenience
|
|
654
777
|
export const platformService = createPlatformService()
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Check if running on Windows
|
|
781
|
+
*/
|
|
782
|
+
export function isWindows(): boolean {
|
|
783
|
+
return process.platform === 'win32'
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Get spawn options for Windows shell requirements.
|
|
788
|
+
* Windows needs shell:true for proper command execution with quoted paths.
|
|
789
|
+
*/
|
|
790
|
+
export function getWindowsSpawnOptions(): { shell: true } | Record<string, never> {
|
|
791
|
+
return isWindows() ? { shell: true } : {}
|
|
792
|
+
}
|
package/core/process-manager.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { exec, spawn } from 'child_process'
|
|
1
|
+
import { exec, spawn, type SpawnOptions } from 'child_process'
|
|
2
2
|
import { promisify } from 'util'
|
|
3
3
|
import { existsSync } from 'fs'
|
|
4
4
|
import { readFile, rm } from 'fs/promises'
|
|
5
5
|
import { paths } from '../config/paths'
|
|
6
6
|
import { logDebug } from './error-handler'
|
|
7
|
+
import {
|
|
8
|
+
platformService,
|
|
9
|
+
isWindows,
|
|
10
|
+
getWindowsSpawnOptions,
|
|
11
|
+
} from './platform-service'
|
|
7
12
|
import type { ProcessResult, StatusResult } from '../types'
|
|
8
13
|
|
|
9
14
|
const execAsync = promisify(exec)
|
|
@@ -41,23 +46,9 @@ export class ProcessManager {
|
|
|
41
46
|
options: InitdbOptions = {},
|
|
42
47
|
): Promise<ProcessResult> {
|
|
43
48
|
const { superuser = 'postgres' } = options
|
|
44
|
-
|
|
45
|
-
// Track if directory existed before initdb (to know if we should clean up)
|
|
46
49
|
const dirExistedBefore = existsSync(dataDir)
|
|
47
50
|
|
|
48
|
-
const args = [
|
|
49
|
-
'-D',
|
|
50
|
-
dataDir,
|
|
51
|
-
'-U',
|
|
52
|
-
superuser,
|
|
53
|
-
'--auth=trust',
|
|
54
|
-
'--encoding=UTF8',
|
|
55
|
-
'--no-locale',
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
// Helper to clean up data directory on failure
|
|
59
51
|
const cleanupOnFailure = async () => {
|
|
60
|
-
// Only clean up if initdb created the directory (it didn't exist before)
|
|
61
52
|
if (!dirExistedBefore && existsSync(dataDir)) {
|
|
62
53
|
try {
|
|
63
54
|
await rm(dataDir, { recursive: true, force: true })
|
|
@@ -70,6 +61,38 @@ export class ProcessManager {
|
|
|
70
61
|
}
|
|
71
62
|
}
|
|
72
63
|
|
|
64
|
+
if (isWindows()) {
|
|
65
|
+
// On Windows, build the entire command as a single string
|
|
66
|
+
const cmd = `"${initdbPath}" -D "${dataDir}" -U ${superuser} --auth=trust --encoding=UTF8 --no-locale`
|
|
67
|
+
|
|
68
|
+
logDebug('initdb command (Windows)', { cmd })
|
|
69
|
+
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
exec(cmd, { timeout: 120000 }, async (error, stdout, stderr) => {
|
|
72
|
+
logDebug('initdb completed', { error: error?.message, stdout, stderr })
|
|
73
|
+
if (error) {
|
|
74
|
+
await cleanupOnFailure()
|
|
75
|
+
reject(new Error(`initdb failed with code ${error.code}: ${stderr || stdout || error.message}`))
|
|
76
|
+
} else {
|
|
77
|
+
resolve({ stdout, stderr })
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Unix path - use spawn without shell
|
|
84
|
+
const args = [
|
|
85
|
+
'-D',
|
|
86
|
+
dataDir,
|
|
87
|
+
'-U',
|
|
88
|
+
superuser,
|
|
89
|
+
'--auth=trust',
|
|
90
|
+
'--encoding=UTF8',
|
|
91
|
+
'--no-locale',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
logDebug('initdb command', { initdbPath, args })
|
|
95
|
+
|
|
73
96
|
return new Promise((resolve, reject) => {
|
|
74
97
|
const proc = spawn(initdbPath, args, {
|
|
75
98
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -78,14 +101,15 @@ export class ProcessManager {
|
|
|
78
101
|
let stdout = ''
|
|
79
102
|
let stderr = ''
|
|
80
103
|
|
|
81
|
-
proc.stdout
|
|
104
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
82
105
|
stdout += data.toString()
|
|
83
106
|
})
|
|
84
|
-
proc.stderr
|
|
107
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
85
108
|
stderr += data.toString()
|
|
86
109
|
})
|
|
87
110
|
|
|
88
111
|
proc.on('close', async (code) => {
|
|
112
|
+
logDebug('initdb completed', { code, stdout, stderr })
|
|
89
113
|
if (code === 0) {
|
|
90
114
|
resolve({ stdout, stderr })
|
|
91
115
|
} else {
|
|
@@ -95,6 +119,7 @@ export class ProcessManager {
|
|
|
95
119
|
})
|
|
96
120
|
|
|
97
121
|
proc.on('error', async (err) => {
|
|
122
|
+
logDebug('initdb error', { error: err.message })
|
|
98
123
|
await cleanupOnFailure()
|
|
99
124
|
reject(err)
|
|
100
125
|
})
|
|
@@ -110,7 +135,58 @@ export class ProcessManager {
|
|
|
110
135
|
options: StartOptions = {},
|
|
111
136
|
): Promise<ProcessResult> {
|
|
112
137
|
const { port, logFile } = options
|
|
138
|
+
const logDest = logFile || platformService.getNullDevice()
|
|
139
|
+
|
|
140
|
+
if (isWindows()) {
|
|
141
|
+
// On Windows, start without -w (wait) flag and poll for readiness
|
|
142
|
+
// The -w flag can hang indefinitely on Windows
|
|
143
|
+
let cmd = `"${pgCtlPath}" start -D "${dataDir}" -l "${logDest}"`
|
|
144
|
+
if (port) {
|
|
145
|
+
cmd += ` -o "-p ${port}"`
|
|
146
|
+
}
|
|
113
147
|
|
|
148
|
+
logDebug('pg_ctl start command (Windows)', { cmd })
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
exec(cmd, { timeout: 30000 }, async (error, stdout, stderr) => {
|
|
152
|
+
logDebug('pg_ctl start initiated', { error: error?.message, stdout, stderr })
|
|
153
|
+
|
|
154
|
+
if (error) {
|
|
155
|
+
reject(
|
|
156
|
+
new Error(
|
|
157
|
+
`pg_ctl start failed with code ${error.code}: ${stderr || stdout || error.message}`,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Poll for PostgreSQL to be ready using pg_isready or status check
|
|
164
|
+
const statusCmd = `"${pgCtlPath}" status -D "${dataDir}"`
|
|
165
|
+
let attempts = 0
|
|
166
|
+
const maxAttempts = 30
|
|
167
|
+
const pollInterval = 1000
|
|
168
|
+
|
|
169
|
+
const checkReady = () => {
|
|
170
|
+
attempts++
|
|
171
|
+
exec(statusCmd, (statusError, statusStdout) => {
|
|
172
|
+
if (!statusError && statusStdout.includes('server is running')) {
|
|
173
|
+
logDebug('pg_ctl start completed (Windows)', { attempts })
|
|
174
|
+
resolve({ stdout, stderr })
|
|
175
|
+
} else if (attempts >= maxAttempts) {
|
|
176
|
+
reject(new Error(`PostgreSQL failed to start within ${maxAttempts} seconds`))
|
|
177
|
+
} else {
|
|
178
|
+
setTimeout(checkReady, pollInterval)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Give it a moment before starting to poll
|
|
184
|
+
setTimeout(checkReady, 500)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Unix path - use spawn without shell
|
|
114
190
|
const pgOptions: string[] = []
|
|
115
191
|
if (port) {
|
|
116
192
|
pgOptions.push(`-p ${port}`)
|
|
@@ -121,12 +197,18 @@ export class ProcessManager {
|
|
|
121
197
|
'-D',
|
|
122
198
|
dataDir,
|
|
123
199
|
'-l',
|
|
124
|
-
|
|
200
|
+
logDest,
|
|
125
201
|
'-w', // Wait for startup to complete
|
|
126
|
-
'-
|
|
127
|
-
|
|
202
|
+
'-t',
|
|
203
|
+
'30', // Timeout after 30 seconds
|
|
128
204
|
]
|
|
129
205
|
|
|
206
|
+
if (pgOptions.length > 0) {
|
|
207
|
+
args.push('-o', pgOptions.join(' '))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
logDebug('pg_ctl start command', { pgCtlPath, args })
|
|
211
|
+
|
|
130
212
|
return new Promise((resolve, reject) => {
|
|
131
213
|
const proc = spawn(pgCtlPath, args, {
|
|
132
214
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -135,14 +217,15 @@ export class ProcessManager {
|
|
|
135
217
|
let stdout = ''
|
|
136
218
|
let stderr = ''
|
|
137
219
|
|
|
138
|
-
proc.stdout
|
|
220
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
139
221
|
stdout += data.toString()
|
|
140
222
|
})
|
|
141
|
-
proc.stderr
|
|
223
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
142
224
|
stderr += data.toString()
|
|
143
225
|
})
|
|
144
226
|
|
|
145
227
|
proc.on('close', (code) => {
|
|
228
|
+
logDebug('pg_ctl start completed', { code, stdout, stderr })
|
|
146
229
|
if (code === 0) {
|
|
147
230
|
resolve({ stdout, stderr })
|
|
148
231
|
} else {
|
|
@@ -154,7 +237,10 @@ export class ProcessManager {
|
|
|
154
237
|
}
|
|
155
238
|
})
|
|
156
239
|
|
|
157
|
-
proc.on('error',
|
|
240
|
+
proc.on('error', (err) => {
|
|
241
|
+
logDebug('pg_ctl start error', { error: err.message })
|
|
242
|
+
reject(err)
|
|
243
|
+
})
|
|
158
244
|
})
|
|
159
245
|
}
|
|
160
246
|
|
|
@@ -162,6 +248,29 @@ export class ProcessManager {
|
|
|
162
248
|
* Stop PostgreSQL server using pg_ctl
|
|
163
249
|
*/
|
|
164
250
|
async stop(pgCtlPath: string, dataDir: string): Promise<ProcessResult> {
|
|
251
|
+
if (isWindows()) {
|
|
252
|
+
// On Windows, build the entire command as a single string
|
|
253
|
+
const cmd = `"${pgCtlPath}" stop -D "${dataDir}" -m fast -w -t 30`
|
|
254
|
+
|
|
255
|
+
logDebug('pg_ctl stop command (Windows)', { cmd })
|
|
256
|
+
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
exec(cmd, { timeout: 60000 }, (error, stdout, stderr) => {
|
|
259
|
+
logDebug('pg_ctl stop completed', { error: error?.message, stdout, stderr })
|
|
260
|
+
if (error) {
|
|
261
|
+
reject(
|
|
262
|
+
new Error(
|
|
263
|
+
`pg_ctl stop failed with code ${error.code}: ${stderr || stdout || error.message}`,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
} else {
|
|
267
|
+
resolve({ stdout, stderr })
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Unix path - use spawn without shell
|
|
165
274
|
const args = [
|
|
166
275
|
'stop',
|
|
167
276
|
'-D',
|
|
@@ -169,8 +278,12 @@ export class ProcessManager {
|
|
|
169
278
|
'-m',
|
|
170
279
|
'fast',
|
|
171
280
|
'-w', // Wait for shutdown to complete
|
|
281
|
+
'-t',
|
|
282
|
+
'30', // Timeout after 30 seconds
|
|
172
283
|
]
|
|
173
284
|
|
|
285
|
+
logDebug('pg_ctl stop command', { pgCtlPath, args })
|
|
286
|
+
|
|
174
287
|
return new Promise((resolve, reject) => {
|
|
175
288
|
const proc = spawn(pgCtlPath, args, {
|
|
176
289
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -179,14 +292,15 @@ export class ProcessManager {
|
|
|
179
292
|
let stdout = ''
|
|
180
293
|
let stderr = ''
|
|
181
294
|
|
|
182
|
-
proc.stdout
|
|
295
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
183
296
|
stdout += data.toString()
|
|
184
297
|
})
|
|
185
|
-
proc.stderr
|
|
298
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
186
299
|
stderr += data.toString()
|
|
187
300
|
})
|
|
188
301
|
|
|
189
302
|
proc.on('close', (code) => {
|
|
303
|
+
logDebug('pg_ctl stop completed', { code, stdout, stderr })
|
|
190
304
|
if (code === 0) {
|
|
191
305
|
resolve({ stdout, stderr })
|
|
192
306
|
} else {
|
|
@@ -198,7 +312,10 @@ export class ProcessManager {
|
|
|
198
312
|
}
|
|
199
313
|
})
|
|
200
314
|
|
|
201
|
-
proc.on('error',
|
|
315
|
+
proc.on('error', (err) => {
|
|
316
|
+
logDebug('pg_ctl stop error', { error: err.message })
|
|
317
|
+
reject(err)
|
|
318
|
+
})
|
|
202
319
|
})
|
|
203
320
|
}
|
|
204
321
|
|
|
@@ -240,8 +357,6 @@ export class ProcessManager {
|
|
|
240
357
|
try {
|
|
241
358
|
const content = await readFile(pidFile, 'utf8')
|
|
242
359
|
const pid = parseInt(content.split('\n')[0], 10)
|
|
243
|
-
|
|
244
|
-
// Check if process is still running
|
|
245
360
|
process.kill(pid, 0)
|
|
246
361
|
return true
|
|
247
362
|
} catch (error) {
|
|
@@ -306,6 +421,7 @@ export class ProcessManager {
|
|
|
306
421
|
return new Promise((resolve, reject) => {
|
|
307
422
|
const proc = spawn(psqlPath, args, {
|
|
308
423
|
stdio: command ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
424
|
+
...getWindowsSpawnOptions(),
|
|
309
425
|
})
|
|
310
426
|
|
|
311
427
|
if (command) {
|
|
@@ -365,18 +481,21 @@ export class ProcessManager {
|
|
|
365
481
|
|
|
366
482
|
args.push(backupFile)
|
|
367
483
|
|
|
484
|
+
const spawnOptions: SpawnOptions = {
|
|
485
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
486
|
+
...getWindowsSpawnOptions(),
|
|
487
|
+
}
|
|
488
|
+
|
|
368
489
|
return new Promise((resolve, reject) => {
|
|
369
|
-
const proc = spawn(pgRestorePath, args,
|
|
370
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
371
|
-
})
|
|
490
|
+
const proc = spawn(pgRestorePath, args, spawnOptions)
|
|
372
491
|
|
|
373
492
|
let stdout = ''
|
|
374
493
|
let stderr = ''
|
|
375
494
|
|
|
376
|
-
proc.stdout
|
|
495
|
+
proc.stdout?.on('data', (data: Buffer) => {
|
|
377
496
|
stdout += data.toString()
|
|
378
497
|
})
|
|
379
|
-
proc.stderr
|
|
498
|
+
proc.stderr?.on('data', (data: Buffer) => {
|
|
380
499
|
stderr += data.toString()
|
|
381
500
|
})
|
|
382
501
|
|
package/engines/base-engine.ts
CHANGED
|
@@ -78,6 +78,33 @@ export abstract class BaseEngine {
|
|
|
78
78
|
database?: string,
|
|
79
79
|
): string
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Get the path to the psql client if available
|
|
83
|
+
* Default implementation throws; engines that can provide a bundled or
|
|
84
|
+
* configured psql should override this method.
|
|
85
|
+
*/
|
|
86
|
+
async getPsqlPath(): Promise<string> {
|
|
87
|
+
throw new Error('psql not found')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the path to the mysql client if available
|
|
92
|
+
* Default implementation throws; engines that can provide a bundled or
|
|
93
|
+
* configured mysql should override this method.
|
|
94
|
+
*/
|
|
95
|
+
async getMysqlClientPath(): Promise<string> {
|
|
96
|
+
throw new Error('mysql client not found')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the path to the mysqladmin client if available
|
|
101
|
+
* Default implementation throws; engines that can provide a bundled or
|
|
102
|
+
* configured mysqladmin should override this method.
|
|
103
|
+
*/
|
|
104
|
+
async getMysqladminPath(): Promise<string> {
|
|
105
|
+
throw new Error('mysqladmin not found')
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
/**
|
|
82
109
|
* Open an interactive shell/CLI connection
|
|
83
110
|
*/
|
package/engines/mysql/backup.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* Creates database backups in SQL or compressed (.dump = gzipped SQL) format using mysqldump.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { spawn } from 'child_process'
|
|
7
|
+
import { spawn, type SpawnOptions } from 'child_process'
|
|
8
8
|
import { createWriteStream } from 'fs'
|
|
9
9
|
import { stat } from 'fs/promises'
|
|
10
10
|
import { createGzip } from 'zlib'
|
|
11
11
|
import { pipeline } from 'stream/promises'
|
|
12
12
|
import { getMysqldumpPath } from './binary-detection'
|
|
13
|
+
import { getWindowsSpawnOptions } from '../../core/platform-service'
|
|
13
14
|
import { getEngineDefaults } from '../../config/defaults'
|
|
14
15
|
import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
|
|
15
16
|
|
|
@@ -82,9 +83,12 @@ async function createSqlBackup(
|
|
|
82
83
|
database,
|
|
83
84
|
]
|
|
84
85
|
|
|
85
|
-
const
|
|
86
|
+
const spawnOptions: SpawnOptions = {
|
|
86
87
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
87
|
-
|
|
88
|
+
...getWindowsSpawnOptions(),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const proc = spawn(mysqldump, args, spawnOptions)
|
|
88
92
|
|
|
89
93
|
let stderr = ''
|
|
90
94
|
|
|
@@ -140,9 +144,12 @@ async function createCompressedBackup(
|
|
|
140
144
|
database,
|
|
141
145
|
]
|
|
142
146
|
|
|
143
|
-
const
|
|
147
|
+
const spawnOptions: SpawnOptions = {
|
|
144
148
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
145
|
-
|
|
149
|
+
...getWindowsSpawnOptions(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const proc = spawn(mysqldump, args, spawnOptions)
|
|
146
153
|
|
|
147
154
|
const gzip = createGzip()
|
|
148
155
|
const output = createWriteStream(outputPath)
|