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:
2025-11-18 18:24:49 +00:00
parent 2a3224e2fd
commit 694c8c802a
10 changed files with 189 additions and 24 deletions

View File

@@ -8,6 +8,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"dbbackup/internal/cleanup"
"dbbackup/internal/config"
"dbbackup/internal/logger"
)
@@ -119,9 +120,17 @@ func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
// Cancel all running operations
if m.cancel != nil {
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
return m, tea.Quit
@@ -252,13 +261,13 @@ func (m MenuModel) View() string {
// handleSingleBackup opens database selector for single backup
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()
}
// handleSampleBackup opens database selector for sample backup
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()
}
@@ -272,7 +281,7 @@ func (m MenuModel) handleClusterBackup() (tea.Model, tea.Cmd) {
"🗄️ Cluster Backup",
"This will backup ALL databases in the cluster. Continue?",
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 confirm, nil
@@ -305,7 +314,7 @@ func (m MenuModel) handleSettings() (tea.Model, tea.Cmd) {
// handleRestoreSingle opens archive browser for single restore
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()
}
@@ -315,13 +324,13 @@ func (m MenuModel) handleRestoreCluster() (tea.Model, tea.Cmd) {
m.message = errorStyle.Render("❌ Cluster restore is available only for PostgreSQL")
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()
}
// handleBackupManager opens backup management view
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()
}