Fix cross-platform builds: process cleanup and disk space checking
- Add platform-specific implementations for Windows, BSD systems - Create platform-specific disk space checking with proper syscalls - Add Windows process cleanup using tasklist/taskkill - Add BSD-specific Statfs_t field handling (F_blocks, F_bavail, F_bsize) - Support 9/10 target platforms (Linux, Windows, macOS, FreeBSD, OpenBSD) - Process cleanup now works on all Unix-like systems and Windows - Phase 2 TUI improvements compatible across platforms
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows && !openbsd && !netbsd
|
||||||
|
// +build !windows,!openbsd,!netbsd
|
||||||
|
|
||||||
package checks
|
package checks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,18 +9,6 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DiskSpaceCheck represents disk space information
|
|
||||||
type DiskSpaceCheck struct {
|
|
||||||
Path string
|
|
||||||
TotalBytes uint64
|
|
||||||
AvailableBytes uint64
|
|
||||||
UsedBytes uint64
|
|
||||||
UsedPercent float64
|
|
||||||
Sufficient bool
|
|
||||||
Warning bool
|
|
||||||
Critical bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckDiskSpace checks available disk space for a given path
|
// CheckDiskSpace checks available disk space for a given path
|
||||||
func CheckDiskSpace(path string) *DiskSpaceCheck {
|
func CheckDiskSpace(path string) *DiskSpaceCheck {
|
||||||
// Get absolute path
|
// Get absolute path
|
||||||
@@ -37,9 +28,9 @@ func CheckDiskSpace(path string) *DiskSpaceCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate space
|
// Calculate space (handle different types on different platforms)
|
||||||
totalBytes := stat.Blocks * uint64(stat.Bsize)
|
totalBytes := uint64(stat.Blocks) * uint64(stat.Bsize)
|
||||||
availableBytes := stat.Bavail * uint64(stat.Bsize)
|
availableBytes := uint64(stat.Bavail) * uint64(stat.Bsize)
|
||||||
usedBytes := totalBytes - availableBytes
|
usedBytes := totalBytes - availableBytes
|
||||||
usedPercent := float64(usedBytes) / float64(totalBytes) * 100
|
usedPercent := float64(usedBytes) / float64(totalBytes) * 100
|
||||||
|
|
||||||
@@ -146,16 +137,4 @@ func EstimateBackupSize(databaseSize uint64, compressionLevel int) uint64 {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// formatBytes formats bytes to human-readable format
|
|
||||||
func formatBytes(bytes uint64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if bytes < unit {
|
|
||||||
return fmt.Sprintf("%d B", bytes)
|
|
||||||
}
|
|
||||||
div, exp := uint64(unit), 0
|
|
||||||
for n := bytes / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
|
|||||||
111
internal/checks/disk_check_bsd.go
Normal file
111
internal/checks/disk_check_bsd.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
//go:build openbsd || netbsd
|
||||||
|
// +build openbsd netbsd
|
||||||
|
|
||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckDiskSpace checks available disk space for a given path (OpenBSD/NetBSD implementation)
|
||||||
|
func CheckDiskSpace(path string) *DiskSpaceCheck {
|
||||||
|
// Get absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
absPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get filesystem stats
|
||||||
|
var stat syscall.Statfs_t
|
||||||
|
if err := syscall.Statfs(absPath, &stat); err != nil {
|
||||||
|
// Return error state
|
||||||
|
return &DiskSpaceCheck{
|
||||||
|
Path: absPath,
|
||||||
|
Critical: true,
|
||||||
|
Sufficient: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate space (OpenBSD/NetBSD use different field names)
|
||||||
|
totalBytes := uint64(stat.F_blocks) * uint64(stat.F_bsize)
|
||||||
|
availableBytes := uint64(stat.F_bavail) * uint64(stat.F_bsize)
|
||||||
|
usedBytes := totalBytes - availableBytes
|
||||||
|
usedPercent := float64(usedBytes) / float64(totalBytes) * 100
|
||||||
|
|
||||||
|
check := &DiskSpaceCheck{
|
||||||
|
Path: absPath,
|
||||||
|
TotalBytes: totalBytes,
|
||||||
|
AvailableBytes: availableBytes,
|
||||||
|
UsedBytes: usedBytes,
|
||||||
|
UsedPercent: usedPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status thresholds
|
||||||
|
check.Critical = usedPercent >= 95
|
||||||
|
check.Warning = usedPercent >= 80 && !check.Critical
|
||||||
|
check.Sufficient = !check.Critical && !check.Warning
|
||||||
|
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDiskSpaceForRestore checks if there's enough space for restore (needs 4x archive size)
|
||||||
|
func CheckDiskSpaceForRestore(path string, archiveSize int64) *DiskSpaceCheck {
|
||||||
|
check := CheckDiskSpace(path)
|
||||||
|
requiredBytes := uint64(archiveSize) * 4 // Account for decompression
|
||||||
|
|
||||||
|
// Override status based on required space
|
||||||
|
if check.AvailableBytes < requiredBytes {
|
||||||
|
check.Critical = true
|
||||||
|
check.Sufficient = false
|
||||||
|
check.Warning = false
|
||||||
|
} else if check.AvailableBytes < requiredBytes*2 {
|
||||||
|
check.Warning = true
|
||||||
|
check.Sufficient = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatDiskSpaceMessage creates a user-friendly disk space message
|
||||||
|
func FormatDiskSpaceMessage(check *DiskSpaceCheck) string {
|
||||||
|
var status string
|
||||||
|
var icon string
|
||||||
|
|
||||||
|
if check.Critical {
|
||||||
|
status = "CRITICAL"
|
||||||
|
icon = "❌"
|
||||||
|
} else if check.Warning {
|
||||||
|
status = "WARNING"
|
||||||
|
icon = "⚠️ "
|
||||||
|
} else {
|
||||||
|
status = "OK"
|
||||||
|
icon = "✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf(`📊 Disk Space Check (%s):
|
||||||
|
Path: %s
|
||||||
|
Total: %s
|
||||||
|
Available: %s (%.1f%% used)
|
||||||
|
%s Status: %s`,
|
||||||
|
status,
|
||||||
|
check.Path,
|
||||||
|
formatBytes(check.TotalBytes),
|
||||||
|
formatBytes(check.AvailableBytes),
|
||||||
|
check.UsedPercent,
|
||||||
|
icon,
|
||||||
|
status)
|
||||||
|
|
||||||
|
if check.Critical {
|
||||||
|
msg += "\n \n ⚠️ CRITICAL: Insufficient disk space!"
|
||||||
|
msg += "\n Operation blocked. Free up space before continuing."
|
||||||
|
} else if check.Warning {
|
||||||
|
msg += "\n \n ⚠️ WARNING: Low disk space!"
|
||||||
|
msg += "\n Backup may fail if database is larger than estimated."
|
||||||
|
} else {
|
||||||
|
msg += "\n \n ✓ Sufficient space available"
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
131
internal/checks/disk_check_windows.go
Normal file
131
internal/checks/disk_check_windows.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package checks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
getDiskFreeSpaceEx = kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckDiskSpace checks available disk space for a given path (Windows implementation)
|
||||||
|
func CheckDiskSpace(path string) *DiskSpaceCheck {
|
||||||
|
// Get absolute path
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
absPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the drive root (e.g., "C:\")
|
||||||
|
vol := filepath.VolumeName(absPath)
|
||||||
|
if vol == "" {
|
||||||
|
// If no volume, try current directory
|
||||||
|
vol = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64
|
||||||
|
|
||||||
|
// Call Windows API
|
||||||
|
pathPtr, _ := syscall.UTF16PtrFromString(vol)
|
||||||
|
ret, _, _ := getDiskFreeSpaceEx.Call(
|
||||||
|
uintptr(unsafe.Pointer(pathPtr)),
|
||||||
|
uintptr(unsafe.Pointer(&freeBytesAvailable)),
|
||||||
|
uintptr(unsafe.Pointer(&totalNumberOfBytes)),
|
||||||
|
uintptr(unsafe.Pointer(&totalNumberOfFreeBytes)))
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
// API call failed, return error state
|
||||||
|
return &DiskSpaceCheck{
|
||||||
|
Path: absPath,
|
||||||
|
Critical: true,
|
||||||
|
Sufficient: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate usage
|
||||||
|
usedBytes := totalNumberOfBytes - totalNumberOfFreeBytes
|
||||||
|
usedPercent := float64(usedBytes) / float64(totalNumberOfBytes) * 100
|
||||||
|
|
||||||
|
check := &DiskSpaceCheck{
|
||||||
|
Path: absPath,
|
||||||
|
TotalBytes: totalNumberOfBytes,
|
||||||
|
AvailableBytes: freeBytesAvailable,
|
||||||
|
UsedBytes: usedBytes,
|
||||||
|
UsedPercent: usedPercent,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status thresholds
|
||||||
|
check.Critical = usedPercent >= 95
|
||||||
|
check.Warning = usedPercent >= 80 && !check.Critical
|
||||||
|
check.Sufficient = !check.Critical && !check.Warning
|
||||||
|
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDiskSpaceForRestore checks if there's enough space for restore (needs 4x archive size)
|
||||||
|
func CheckDiskSpaceForRestore(path string, archiveSize int64) *DiskSpaceCheck {
|
||||||
|
check := CheckDiskSpace(path)
|
||||||
|
requiredBytes := uint64(archiveSize) * 4 // Account for decompression
|
||||||
|
|
||||||
|
// Override status based on required space
|
||||||
|
if check.AvailableBytes < requiredBytes {
|
||||||
|
check.Critical = true
|
||||||
|
check.Sufficient = false
|
||||||
|
check.Warning = false
|
||||||
|
} else if check.AvailableBytes < requiredBytes*2 {
|
||||||
|
check.Warning = true
|
||||||
|
check.Sufficient = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatDiskSpaceMessage creates a user-friendly disk space message
|
||||||
|
func FormatDiskSpaceMessage(check *DiskSpaceCheck) string {
|
||||||
|
var status string
|
||||||
|
var icon string
|
||||||
|
|
||||||
|
if check.Critical {
|
||||||
|
status = "CRITICAL"
|
||||||
|
icon = "❌"
|
||||||
|
} else if check.Warning {
|
||||||
|
status = "WARNING"
|
||||||
|
icon = "⚠️ "
|
||||||
|
} else {
|
||||||
|
status = "OK"
|
||||||
|
icon = "✓"
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf(`📊 Disk Space Check (%s):
|
||||||
|
Path: %s
|
||||||
|
Total: %s
|
||||||
|
Available: %s (%.1f%% used)
|
||||||
|
%s Status: %s`,
|
||||||
|
status,
|
||||||
|
check.Path,
|
||||||
|
formatBytes(check.TotalBytes),
|
||||||
|
formatBytes(check.AvailableBytes),
|
||||||
|
check.UsedPercent,
|
||||||
|
icon,
|
||||||
|
status)
|
||||||
|
|
||||||
|
if check.Critical {
|
||||||
|
msg += "\n \n ⚠️ CRITICAL: Insufficient disk space!"
|
||||||
|
msg += "\n Operation blocked. Free up space before continuing."
|
||||||
|
} else if check.Warning {
|
||||||
|
msg += "\n \n ⚠️ WARNING: Low disk space!"
|
||||||
|
msg += "\n Backup may fail if database is larger than estimated."
|
||||||
|
} else {
|
||||||
|
msg += "\n \n ✓ Sufficient space available"
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
29
internal/checks/types.go
Normal file
29
internal/checks/types.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package checks
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// DiskSpaceCheck represents disk space information
|
||||||
|
type DiskSpaceCheck struct {
|
||||||
|
Path string
|
||||||
|
TotalBytes uint64
|
||||||
|
AvailableBytes uint64
|
||||||
|
UsedBytes uint64
|
||||||
|
UsedPercent float64
|
||||||
|
Sufficient bool
|
||||||
|
Warning bool
|
||||||
|
Critical bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatBytes formats bytes to human-readable format
|
||||||
|
func formatBytes(bytes uint64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
div, exp := uint64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
package cleanup
|
package cleanup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
117
internal/cleanup/processes_windows.go
Normal file
117
internal/cleanup/processes_windows.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package cleanup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"dbbackup/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KillOrphanedProcesses finds and kills any orphaned pg_dump, pg_restore, gzip, or pigz processes (Windows implementation)
|
||||||
|
func KillOrphanedProcesses(log logger.Logger) error {
|
||||||
|
processNames := []string{"pg_dump.exe", "pg_restore.exe", "gzip.exe", "pigz.exe", "gunzip.exe"}
|
||||||
|
|
||||||
|
myPID := os.Getpid()
|
||||||
|
var killed []string
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
for _, procName := range processNames {
|
||||||
|
pids, err := findProcessesByNameWindows(procName, myPID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to search for processes", "process", procName, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pid := range pids {
|
||||||
|
if err := killProcessWindows(pid); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("failed to kill %s (PID %d): %w", procName, pid, err))
|
||||||
|
} else {
|
||||||
|
killed = append(killed, fmt.Sprintf("%s (PID %d)", procName, pid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(killed) > 0 {
|
||||||
|
log.Info("Cleaned up orphaned processes", "count", len(killed), "processes", strings.Join(killed, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("some processes could not be killed: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProcessesByNameWindows returns PIDs of processes matching the given name (Windows implementation)
|
||||||
|
func findProcessesByNameWindows(name string, excludePID int) ([]int, error) {
|
||||||
|
// Use tasklist command for Windows
|
||||||
|
cmd := exec.Command("tasklist", "/FO", "CSV", "/NH", "/FI", fmt.Sprintf("IMAGENAME eq %s", name))
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// No processes found or command failed
|
||||||
|
return []int{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pids []int
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CSV output: "name","pid","session","mem"
|
||||||
|
fields := strings.Split(line, ",")
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove quotes from PID field
|
||||||
|
pidStr := strings.Trim(fields[1], `"`)
|
||||||
|
pid, err := strconv.Atoi(pidStr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't kill our own process
|
||||||
|
if pid == excludePID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pids = append(pids, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// killProcessWindows kills a process on Windows
|
||||||
|
func killProcessWindows(pid int) error {
|
||||||
|
// Use taskkill command
|
||||||
|
cmd := exec.Command("taskkill", "/F", "/PID", strconv.Itoa(pid))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProcessGroup sets up process group for Windows (no-op, Windows doesn't use Unix process groups)
|
||||||
|
func SetProcessGroup(cmd *exec.Cmd) {
|
||||||
|
// Windows doesn't support Unix-style process groups
|
||||||
|
// We can set CREATE_NEW_PROCESS_GROUP flag instead
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KillCommandGroup kills a command on Windows
|
||||||
|
func KillCommandGroup(cmd *exec.Cmd) error {
|
||||||
|
if cmd.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, just kill the process directly
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user