fix(tui): enable Ctrl+C/ESC to cancel running backup/restore operations
PROBLEM: Users could not interrupt backup or restore operations through the TUI interface. Pressing Ctrl+C or ESC did nothing during execution. ROOT CAUSE: - BackupExecutionModel ignored ALL key presses while running (only handled when done) - RestoreExecutionModel returned tea.Quit but didn't cancel the context - The operation goroutine kept running in the background with its own context FIX: - Added cancel context.CancelFunc to both execution models - Create child context with WithCancel in New*Execution constructors - Handle ctrl+c and esc during execution to call cancel() - Show 'Cancelling...' status while waiting for graceful shutdown - Show cancel hint in View: 'Press Ctrl+C or ESC to cancel' The fix works because: - exec.CommandContext(ctx) will SIGKILL the subprocess when ctx is cancelled - pg_dump, pg_restore, psql, mysql all get terminated properly - User sees immediate feedback that cancellation is in progress
This commit is contained in:
@@ -20,12 +20,14 @@ type BackupExecutionModel struct {
|
||||
logger logger.Logger
|
||||
parent tea.Model
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc // Cancel function to stop the operation
|
||||
backupType string
|
||||
databaseName string
|
||||
ratio int
|
||||
status string
|
||||
progress int
|
||||
done bool
|
||||
cancelling bool // True when user has requested cancellation
|
||||
err error
|
||||
result string
|
||||
startTime time.Time
|
||||
@@ -34,11 +36,14 @@ type BackupExecutionModel struct {
|
||||
}
|
||||
|
||||
func NewBackupExecution(cfg *config.Config, log logger.Logger, parent tea.Model, ctx context.Context, backupType, dbName string, ratio int) BackupExecutionModel {
|
||||
// Create a cancellable context derived from parent
|
||||
childCtx, cancel := context.WithCancel(ctx)
|
||||
return BackupExecutionModel{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
parent: parent,
|
||||
ctx: ctx,
|
||||
ctx: childCtx,
|
||||
cancel: cancel,
|
||||
backupType: backupType,
|
||||
databaseName: dbName,
|
||||
ratio: ratio,
|
||||
@@ -206,9 +211,21 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.done {
|
||||
switch msg.String() {
|
||||
case "enter", "esc", "q":
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
if !m.done && !m.cancelling {
|
||||
// User requested cancellation - cancel the context
|
||||
m.cancelling = true
|
||||
m.status = "⏹️ Cancelling backup... (please wait)"
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
return m, nil
|
||||
} else if m.done {
|
||||
return m.parent, nil
|
||||
}
|
||||
case "enter", "q":
|
||||
if m.done {
|
||||
return m.parent, nil
|
||||
}
|
||||
}
|
||||
@@ -240,7 +257,12 @@ func (m BackupExecutionModel) View() string {
|
||||
|
||||
// Status with spinner
|
||||
if !m.done {
|
||||
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
|
||||
if m.cancelling {
|
||||
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
|
||||
} else {
|
||||
s.WriteString(fmt.Sprintf(" %s %s\n", spinnerFrames[m.spinnerFrame], m.status))
|
||||
s.WriteString("\n ⌨️ Press Ctrl+C or ESC to cancel\n")
|
||||
}
|
||||
} else {
|
||||
s.WriteString(fmt.Sprintf(" %s\n\n", m.status))
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type RestoreExecutionModel struct {
|
||||
logger logger.Logger
|
||||
parent tea.Model
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc // Cancel function to stop the operation
|
||||
archive ArchiveInfo
|
||||
targetDB string
|
||||
cleanFirst bool
|
||||
@@ -44,19 +45,23 @@ type RestoreExecutionModel struct {
|
||||
spinnerFrames []string
|
||||
|
||||
// Results
|
||||
done bool
|
||||
err error
|
||||
result string
|
||||
elapsed time.Duration
|
||||
done bool
|
||||
cancelling bool // True when user has requested cancellation
|
||||
err error
|
||||
result string
|
||||
elapsed time.Duration
|
||||
}
|
||||
|
||||
// NewRestoreExecution creates a new restore execution model
|
||||
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, saveDebugLog bool, workDir string) RestoreExecutionModel {
|
||||
// Create a cancellable context derived from parent
|
||||
childCtx, cancel := context.WithCancel(ctx)
|
||||
return RestoreExecutionModel{
|
||||
config: cfg,
|
||||
logger: log,
|
||||
parent: parent,
|
||||
ctx: ctx,
|
||||
ctx: childCtx,
|
||||
cancel: cancel,
|
||||
archive: archive,
|
||||
targetDB: targetDB,
|
||||
cleanFirst: cleanFirst,
|
||||
@@ -274,11 +279,32 @@ func (m RestoreExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
// Always allow quitting
|
||||
return m.parent, tea.Quit
|
||||
|
||||
case "enter", " ", "esc":
|
||||
case "ctrl+c", "esc":
|
||||
if !m.done && !m.cancelling {
|
||||
// User requested cancellation - cancel the context
|
||||
m.cancelling = true
|
||||
m.status = "⏹️ Cancelling restore... (please wait)"
|
||||
m.phase = "Cancelling"
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
return m, nil
|
||||
} else if m.done {
|
||||
return m.parent, nil
|
||||
}
|
||||
case "q":
|
||||
if !m.done && !m.cancelling {
|
||||
m.cancelling = true
|
||||
m.status = "⏹️ Cancelling restore... (please wait)"
|
||||
m.phase = "Cancelling"
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
return m, nil
|
||||
} else if m.done {
|
||||
return m.parent, tea.Quit
|
||||
}
|
||||
case "enter", " ":
|
||||
if m.done {
|
||||
return m.parent, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user