Add comprehensive process cleanup on TUI exit
- Created internal/cleanup package for orphaned process management - KillOrphanedProcesses(): Finds and kills pg_dump, pg_restore, gzip, pigz - killProcessGroup(): Kills entire process groups (handles pipelines) - Pass parent context through all TUI operations (backup/restore inherit cancellation) - Menu cancel now kills all child processes before exit - Fixed context chain: menu.ctx → backup/restore operations - No more zombie processes when user quits TUI mid-operation Context chain: - signal.NotifyContext in main.go → menu.ctx - menu.ctx → backup_exec.ctx, restore_exec.ctx - Child contexts inherit cancellation via context.WithTimeout(parentCtx) - All exec.CommandContext use proper parent context Prevents: Orphaned pg_dump/pg_restore eating CPU/disk after TUI quit
This commit is contained in:
138
internal/cleanup/processes.go
Normal file
138
internal/cleanup/processes.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
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
|
||||||
|
func KillOrphanedProcesses(log logger.Logger) error {
|
||||||
|
processNames := []string{"pg_dump", "pg_restore", "gzip", "pigz", "gunzip"}
|
||||||
|
|
||||||
|
myPID := os.Getpid()
|
||||||
|
var killed []string
|
||||||
|
var errors []error
|
||||||
|
|
||||||
|
for _, procName := range processNames {
|
||||||
|
pids, err := findProcessesByName(procName, myPID)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Failed to search for processes", "process", procName, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pid := range pids {
|
||||||
|
if err := killProcessGroup(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// findProcessesByName returns PIDs of processes matching the given name
|
||||||
|
func findProcessesByName(name string, excludePID int) ([]int, error) {
|
||||||
|
// Use pgrep for efficient process searching
|
||||||
|
cmd := exec.Command("pgrep", "-x", name)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
// Exit code 1 means no processes found (not an error)
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||||
|
return []int{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pids []int
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(line)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't kill our own process
|
||||||
|
if pid == excludePID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pids = append(pids, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// killProcessGroup kills a process and its entire process group
|
||||||
|
func killProcessGroup(pid int) error {
|
||||||
|
// First try to get the process group ID
|
||||||
|
pgid, err := syscall.Getpgid(pid)
|
||||||
|
if err != nil {
|
||||||
|
// Process might already be gone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the entire process group (negative PID kills the group)
|
||||||
|
// This catches pipelines like "pg_dump | gzip"
|
||||||
|
if err := syscall.Kill(-pgid, syscall.SIGTERM); err != nil {
|
||||||
|
// If SIGTERM fails, try SIGKILL
|
||||||
|
syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also kill the specific PID in case it's not in a group
|
||||||
|
syscall.Kill(pid, syscall.SIGTERM)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProcessGroup sets the current process to be a process group leader
|
||||||
|
// This should be called when starting external commands to ensure clean termination
|
||||||
|
func SetProcessGroup(cmd *exec.Cmd) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setpgid: true,
|
||||||
|
Pgid: 0, // Create new process group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KillCommandGroup kills a command and its entire process group
|
||||||
|
func KillCommandGroup(cmd *exec.Cmd) error {
|
||||||
|
if cmd.Process == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pid := cmd.Process.Pid
|
||||||
|
|
||||||
|
// Get the process group ID
|
||||||
|
pgid, err := syscall.Getpgid(pid)
|
||||||
|
if err != nil {
|
||||||
|
// Process might already be gone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the entire process group
|
||||||
|
if err := syscall.Kill(-pgid, syscall.SIGTERM); err != nil {
|
||||||
|
// If SIGTERM fails, use SIGKILL
|
||||||
|
syscall.Kill(-pgid, syscall.SIGKILL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -55,6 +56,7 @@ type ArchiveBrowserModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
archives []ArchiveInfo
|
archives []ArchiveInfo
|
||||||
cursor int
|
cursor int
|
||||||
loading bool
|
loading bool
|
||||||
@@ -65,11 +67,12 @@ type ArchiveBrowserModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewArchiveBrowser creates a new archive browser
|
// NewArchiveBrowser creates a new archive browser
|
||||||
func NewArchiveBrowser(cfg *config.Config, log logger.Logger, parent tea.Model, mode string) ArchiveBrowserModel {
|
func NewArchiveBrowser(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, mode string) ArchiveBrowserModel {
|
||||||
return ArchiveBrowserModel{
|
return ArchiveBrowserModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
loading: true,
|
loading: true,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
filterType: "all",
|
filterType: "all",
|
||||||
@@ -206,7 +209,7 @@ func (m ArchiveBrowserModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Open restore preview
|
// Open restore preview
|
||||||
preview := NewRestorePreview(m.config, m.logger, m.parent, selected, m.mode)
|
preview := NewRestorePreview(m.config, m.logger, m.parent, m.ctx, selected, m.mode)
|
||||||
return preview, preview.Init()
|
return preview, preview.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type BackupExecutionModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
backupType string
|
backupType string
|
||||||
databaseName string
|
databaseName string
|
||||||
ratio int
|
ratio int
|
||||||
@@ -32,11 +33,12 @@ type BackupExecutionModel struct {
|
|||||||
spinnerFrame int
|
spinnerFrame int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, backupType, dbName string, ratio int) BackupExecutionModel {
|
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
|
||||||
return BackupExecutionModel{
|
return BackupExecutionModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
backupType: backupType,
|
backupType: backupType,
|
||||||
databaseName: dbName,
|
databaseName: dbName,
|
||||||
ratio: ratio,
|
ratio: ratio,
|
||||||
@@ -50,7 +52,7 @@ func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model,
|
|||||||
func (m BackupExecutionModel) Init() tea.Cmd {
|
func (m BackupExecutionModel) Init() tea.Cmd {
|
||||||
// TUI handles all display through View() - no progress callbacks needed
|
// TUI handles all display through View() - no progress callbacks needed
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
executeBackupWithTUIProgress(m.config, m.logger, m.backupType, m.databaseName, m.ratio),
|
executeBackupWithTUIProgress(m.ctx, m.config, m.logger, m.backupType, m.databaseName, m.ratio),
|
||||||
backupTickCmd(),
|
backupTickCmd(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -74,11 +76,12 @@ type backupCompleteMsg struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeBackupWithTUIProgress(cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int) tea.Cmd {
|
func executeBackupWithTUIProgress(parentCtx context.Context, cfg *config.Config, log logger.Logger, backupType, dbName string, ratio int) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
// Use configurable cluster timeout (minutes) from config; default set in config.New()
|
// Use configurable cluster timeout (minutes) from config; default set in config.New()
|
||||||
|
// Use parent context to inherit cancellation from TUI
|
||||||
clusterTimeout := time.Duration(cfg.ClusterTimeoutMinutes) * time.Minute
|
clusterTimeout := time.Duration(cfg.ClusterTimeoutMinutes) * time.Minute
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), clusterTimeout)
|
ctx, cancel := context.WithTimeout(parentCtx, clusterTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,6 +18,7 @@ type BackupManagerModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
archives []ArchiveInfo
|
archives []ArchiveInfo
|
||||||
cursor int
|
cursor int
|
||||||
loading bool
|
loading bool
|
||||||
@@ -27,11 +29,12 @@ type BackupManagerModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewBackupManager creates a new backup manager
|
// NewBackupManager creates a new backup manager
|
||||||
func NewBackupManager(cfg *config.Config, log logger.Logger, parent tea.Model) BackupManagerModel {
|
func NewBackupManager(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context) BackupManagerModel {
|
||||||
return BackupManagerModel{
|
return BackupManagerModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +129,7 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if selected.Format.IsClusterBackup() {
|
if selected.Format.IsClusterBackup() {
|
||||||
mode = "restore-cluster"
|
mode = "restore-cluster"
|
||||||
}
|
}
|
||||||
preview := NewRestorePreview(m.config, m.logger, m.parent, selected, mode)
|
preview := NewRestorePreview(m.config, m.logger, m.parent, m.ctx, selected, mode)
|
||||||
return preview, preview.Init()
|
return preview, preview.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ type ConfirmationModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
title string
|
title string
|
||||||
message string
|
message string
|
||||||
cursor int
|
cursor int
|
||||||
@@ -75,7 +77,7 @@ func (m ConfirmationModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.onConfirm()
|
return m.onConfirm()
|
||||||
}
|
}
|
||||||
// Default: execute cluster backup for backward compatibility
|
// Default: execute cluster backup for backward compatibility
|
||||||
executor := NewBackupExecution(m.config, m.logger, m.parent, "cluster", "", 0)
|
executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, "cluster", "", 0)
|
||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
}
|
}
|
||||||
return m.parent, nil
|
return m.parent, nil
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type DatabaseSelectorModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
databases []string
|
databases []string
|
||||||
cursor int
|
cursor int
|
||||||
selected string
|
selected string
|
||||||
@@ -28,11 +29,12 @@ type DatabaseSelectorModel struct {
|
|||||||
backupType string // "single" or "sample"
|
backupType string // "single" or "sample"
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDatabaseSelector(cfg *config.Config, log logger.Logger, parent tea.Model, title string, backupType string) DatabaseSelectorModel {
|
func NewDatabaseSelector(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, title string, backupType string) DatabaseSelectorModel {
|
||||||
return DatabaseSelectorModel{
|
return DatabaseSelectorModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
databases: []string{"Loading databases..."},
|
databases: []string{"Loading databases..."},
|
||||||
title: title,
|
title: title,
|
||||||
loading: true,
|
loading: true,
|
||||||
@@ -115,7 +117,7 @@ func (m DatabaseSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For single backup, go directly to execution
|
// For single backup, go directly to execution
|
||||||
executor := NewBackupExecution(m.config, m.logger, m.parent, m.backupType, m.selected, 0)
|
executor := NewBackupExecution(m.config, m.logger, m.parent, m.ctx, m.backupType, m.selected, 0)
|
||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (m InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
// If this is from database selector, execute backup with ratio
|
// If this is from database selector, execute backup with ratio
|
||||||
if selector, ok := m.parent.(DatabaseSelectorModel); ok {
|
if selector, ok := m.parent.(DatabaseSelectorModel); ok {
|
||||||
ratio, _ := strconv.Atoi(m.value)
|
ratio, _ := strconv.Atoi(m.value)
|
||||||
executor := NewBackupExecution(selector.config, selector.logger, selector.parent,
|
executor := NewBackupExecution(selector.config, selector.logger, selector.parent, selector.ctx,
|
||||||
selector.backupType, selector.selected, ratio)
|
selector.backupType, selector.selected, ratio)
|
||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
"dbbackup/internal/cleanup"
|
||||||
"dbbackup/internal/config"
|
"dbbackup/internal/config"
|
||||||
"dbbackup/internal/logger"
|
"dbbackup/internal/logger"
|
||||||
)
|
)
|
||||||
@@ -119,9 +120,17 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
|
// Cancel all running operations
|
||||||
if m.cancel != nil {
|
if m.cancel != nil {
|
||||||
m.cancel()
|
m.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any orphaned processes before exit
|
||||||
|
m.logger.Info("Cleaning up processes before exit")
|
||||||
|
if err := cleanup.KillOrphanedProcesses(m.logger); err != nil {
|
||||||
|
m.logger.Warn("Failed to clean up all processes", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
m.quitting = true
|
m.quitting = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
@@ -252,13 +261,13 @@ func (m MenuModel) View() string {
|
|||||||
|
|
||||||
// handleSingleBackup opens database selector for single backup
|
// handleSingleBackup opens database selector for single backup
|
||||||
func (m MenuModel) handleSingleBackup() (tea.Model, tea.Cmd) {
|
func (m MenuModel) handleSingleBackup() (tea.Model, tea.Cmd) {
|
||||||
selector := NewDatabaseSelector(m.config, m.logger, m, "🗄️ Single Database Backup", "single")
|
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "🗄️ Single Database Backup", "single")
|
||||||
return selector, selector.Init()
|
return selector, selector.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSampleBackup opens database selector for sample backup
|
// handleSampleBackup opens database selector for sample backup
|
||||||
func (m MenuModel) handleSampleBackup() (tea.Model, tea.Cmd) {
|
func (m MenuModel) handleSampleBackup() (tea.Model, tea.Cmd) {
|
||||||
selector := NewDatabaseSelector(m.config, m.logger, m, "📊 Sample Database Backup", "sample")
|
selector := NewDatabaseSelector(m.config, m.logger, m, m.ctx, "📊 Sample Database Backup", "sample")
|
||||||
return selector, selector.Init()
|
return selector, selector.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +281,7 @@ func (m MenuModel) handleClusterBackup() (tea.Model, tea.Cmd) {
|
|||||||
"🗄️ Cluster Backup",
|
"🗄️ Cluster Backup",
|
||||||
"This will backup ALL databases in the cluster. Continue?",
|
"This will backup ALL databases in the cluster. Continue?",
|
||||||
func() (tea.Model, tea.Cmd) {
|
func() (tea.Model, tea.Cmd) {
|
||||||
executor := NewBackupExecution(m.config, m.logger, m, "cluster", "", 0)
|
executor := NewBackupExecution(m.config, m.logger, m, m.ctx, "cluster", "", 0)
|
||||||
return executor, executor.Init()
|
return executor, executor.Init()
|
||||||
})
|
})
|
||||||
return confirm, nil
|
return confirm, nil
|
||||||
@@ -305,7 +314,7 @@ func (m MenuModel) handleSettings() (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
// handleRestoreSingle opens archive browser for single restore
|
// handleRestoreSingle opens archive browser for single restore
|
||||||
func (m MenuModel) handleRestoreSingle() (tea.Model, tea.Cmd) {
|
func (m MenuModel) handleRestoreSingle() (tea.Model, tea.Cmd) {
|
||||||
browser := NewArchiveBrowser(m.config, m.logger, m, "restore-single")
|
browser := NewArchiveBrowser(m.config, m.logger, m, m.ctx, "restore-single")
|
||||||
return browser, browser.Init()
|
return browser, browser.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,13 +324,13 @@ func (m MenuModel) handleRestoreCluster() (tea.Model, tea.Cmd) {
|
|||||||
m.message = errorStyle.Render("❌ Cluster restore is available only for PostgreSQL")
|
m.message = errorStyle.Render("❌ Cluster restore is available only for PostgreSQL")
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
browser := NewArchiveBrowser(m.config, m.logger, m, "restore-cluster")
|
browser := NewArchiveBrowser(m.config, m.logger, m, m.ctx, "restore-cluster")
|
||||||
return browser, browser.Init()
|
return browser, browser.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleBackupManager opens backup management view
|
// handleBackupManager opens backup management view
|
||||||
func (m MenuModel) handleBackupManager() (tea.Model, tea.Cmd) {
|
func (m MenuModel) handleBackupManager() (tea.Model, tea.Cmd) {
|
||||||
manager := NewBackupManager(m.config, m.logger, m)
|
manager := NewBackupManager(m.config, m.logger, m, m.ctx)
|
||||||
return manager, manager.Init()
|
return manager, manager.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type RestoreExecutionModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
archive ArchiveInfo
|
archive ArchiveInfo
|
||||||
targetDB string
|
targetDB string
|
||||||
cleanFirst bool
|
cleanFirst bool
|
||||||
@@ -48,11 +49,12 @@ type RestoreExecutionModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRestoreExecution creates a new restore execution model
|
// NewRestoreExecution creates a new restore execution model
|
||||||
func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) RestoreExecutionModel {
|
func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) RestoreExecutionModel {
|
||||||
return RestoreExecutionModel{
|
return RestoreExecutionModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
archive: archive,
|
archive: archive,
|
||||||
targetDB: targetDB,
|
targetDB: targetDB,
|
||||||
cleanFirst: cleanFirst,
|
cleanFirst: cleanFirst,
|
||||||
@@ -71,7 +73,7 @@ func NewRestoreExecution(cfg *config.Config, log logger.Logger, parent tea.Model
|
|||||||
|
|
||||||
func (m RestoreExecutionModel) Init() tea.Cmd {
|
func (m RestoreExecutionModel) Init() tea.Cmd {
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
executeRestoreWithTUIProgress(m.config, m.logger, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.restoreType, m.cleanClusterFirst, m.existingDBs),
|
executeRestoreWithTUIProgress(m.ctx, m.config, m.logger, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.restoreType, m.cleanClusterFirst, m.existingDBs),
|
||||||
restoreTickCmd(),
|
restoreTickCmd(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -97,11 +99,12 @@ type restoreCompleteMsg struct {
|
|||||||
elapsed time.Duration
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeRestoreWithTUIProgress(cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) tea.Cmd {
|
func executeRestoreWithTUIProgress(parentCtx context.Context, cfg *config.Config, log logger.Logger, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
// Use configurable cluster timeout (minutes) from config; default set in config.New()
|
// Use configurable cluster timeout (minutes) from config; default set in config.New()
|
||||||
|
// Use parent context to inherit cancellation from TUI
|
||||||
restoreTimeout := time.Duration(cfg.ClusterTimeoutMinutes) * time.Minute
|
restoreTimeout := time.Duration(cfg.ClusterTimeoutMinutes) * time.Minute
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), restoreTimeout)
|
ctx, cancel := context.WithTimeout(parentCtx, restoreTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type RestorePreviewModel struct {
|
|||||||
config *config.Config
|
config *config.Config
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
|
ctx context.Context
|
||||||
archive ArchiveInfo
|
archive ArchiveInfo
|
||||||
mode string
|
mode string
|
||||||
targetDB string
|
targetDB string
|
||||||
@@ -61,7 +62,7 @@ type RestorePreviewModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRestorePreview creates a new restore preview
|
// NewRestorePreview creates a new restore preview
|
||||||
func NewRestorePreview(cfg *config.Config, log logger.Logger, parent tea.Model, archive ArchiveInfo, mode string) RestorePreviewModel {
|
func NewRestorePreview(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, archive ArchiveInfo, mode string) RestorePreviewModel {
|
||||||
// Default target database name from archive
|
// Default target database name from archive
|
||||||
targetDB := archive.DatabaseName
|
targetDB := archive.DatabaseName
|
||||||
if targetDB == "" {
|
if targetDB == "" {
|
||||||
@@ -72,6 +73,7 @@ func NewRestorePreview(cfg *config.Config, log logger.Logger, parent tea.Model,
|
|||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
|
ctx: ctx,
|
||||||
archive: archive,
|
archive: archive,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
targetDB: targetDB,
|
targetDB: targetDB,
|
||||||
@@ -249,7 +251,7 @@ func (m RestorePreviewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to restore execution
|
// Proceed to restore execution
|
||||||
exec := NewRestoreExecution(m.config, m.logger, m.parent, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode, m.cleanClusterFirst, m.existingDBs)
|
exec := NewRestoreExecution(m.config, m.logger, m.parent, m.ctx, m.archive, m.targetDB, m.cleanFirst, m.createIfMissing, m.mode, m.cleanClusterFirst, m.existingDBs)
|
||||||
return exec, exec.Init()
|
return exec, exec.Init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user