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
## 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)
100 lines
2.3 KiB
Go
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()
|
|
}
|
|
}
|