Files
dbbackup/internal/cleanup/command_windows.go
Alexander Renz 59812400a4
All checks were successful
CI/CD / Test (push) Successful in 2m59s
CI/CD / Lint (push) Successful in 1m10s
CI/CD / Integration Tests (push) Successful in 50s
CI/CD / Native Engine Tests (push) Successful in 50s
CI/CD / Build Binary (push) Successful in 43s
CI/CD / Test Release Build (push) Successful in 1m17s
CI/CD / Release Binaries (push) Successful in 10m7s
v5.4.3: Bulletproof SIGINT handling & eliminate external gzip
## SIGINT Cleanup - Zero Zombie Processes
- Add cleanup.SafeCommand() with process group setup (Setpgid=true)
- Replace all exec.CommandContext with cleanup.SafeCommand in backup/restore
- Replace cmd.Process.Kill() with cleanup.KillCommandGroup() for entire process tree
- Add cleanup.Handler for graceful shutdown with registered cleanup functions
- Add rich cluster progress view for TUI
- Add test script: scripts/test-sigint-cleanup.sh

## Eliminate External gzip Process
- Replace zgrep (spawns gzip -cdfq) with in-process pgzip decompression
- All decompression now uses parallel pgzip (2-4x faster, no subprocess)

Files modified:
- internal/cleanup/command.go, command_windows.go, handler.go (new)
- internal/backup/engine.go (7 SafeCommand + 6 KillCommandGroup)
- internal/restore/engine.go (19 SafeCommand + 2 KillCommandGroup)
- internal/restore/{fast_restore,safety,diagnose,preflight,large_db_guard,version_check,error_report}.go
- internal/tui/restore_exec.go, rich_cluster_progress.go (new)
2026-02-02 14:44:49 +01:00

100 lines
2.3 KiB
Go

//go:build windows
// +build windows
package cleanup
import (
"context"
"fmt"
"os/exec"
"time"
"dbbackup/internal/logger"
)
// SafeCommand creates an exec.Cmd with proper setup for clean termination on Windows.
func SafeCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, args...)
// Windows doesn't use process groups the same way as Unix
// exec.CommandContext will handle termination via the context
return cmd
}
// TrackedCommand creates a command that is tracked for cleanup on shutdown.
type TrackedCommand struct {
*exec.Cmd
log logger.Logger
name string
}
// NewTrackedCommand creates a tracked command
func NewTrackedCommand(ctx context.Context, log logger.Logger, name string, args ...string) *TrackedCommand {
tc := &TrackedCommand{
Cmd: SafeCommand(ctx, name, args...),
log: log,
name: name,
}
return tc
}
// StartWithCleanup starts the command and registers cleanup with the handler
func (tc *TrackedCommand) StartWithCleanup(h *Handler) error {
if err := tc.Cmd.Start(); err != nil {
return err
}
// Register cleanup function
pid := tc.Cmd.Process.Pid
h.RegisterCleanup(fmt.Sprintf("kill-%s-%d", tc.name, pid), func(ctx context.Context) error {
return tc.Kill()
})
return nil
}
// Kill terminates the command on Windows
func (tc *TrackedCommand) Kill() error {
if tc.Cmd.Process == nil {
return nil
}
tc.log.Debug("Terminating process", "name", tc.name, "pid", tc.Cmd.Process.Pid)
if err := tc.Cmd.Process.Kill(); err != nil {
tc.log.Debug("Kill failed", "error", err)
return err
}
tc.log.Debug("Process terminated", "name", tc.name, "pid", tc.Cmd.Process.Pid)
return nil
}
// WaitWithContext waits for the command to complete, handling context cancellation properly.
func WaitWithContext(ctx context.Context, cmd *exec.Cmd, log logger.Logger) error {
if cmd.Process == nil {
return fmt.Errorf("process not started")
}
cmdDone := make(chan error, 1)
go func() {
cmdDone <- cmd.Wait()
}()
select {
case err := <-cmdDone:
return err
case <-ctx.Done():
log.Debug("Context cancelled, terminating process", "pid", cmd.Process.Pid)
cmd.Process.Kill()
select {
case <-cmdDone:
case <-time.After(5 * time.Second):
// Already killed, just wait for it
}
return ctx.Err()
}
}