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
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc // Cancel function to stop the operation
|
||||||
backupType string
|
backupType string
|
||||||
databaseName string
|
databaseName string
|
||||||
ratio int
|
ratio int
|
||||||
status string
|
status string
|
||||||
progress int
|
progress int
|
||||||
done bool
|
done bool
|
||||||
|
cancelling bool // True when user has requested cancellation
|
||||||
err error
|
err error
|
||||||
result string
|
result string
|
||||||
startTime time.Time
|
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 {
|
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{
|
return BackupExecutionModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
ctx: ctx,
|
ctx: childCtx,
|
||||||
|
cancel: cancel,
|
||||||
backupType: backupType,
|
backupType: backupType,
|
||||||
databaseName: dbName,
|
databaseName: dbName,
|
||||||
ratio: ratio,
|
ratio: ratio,
|
||||||
@@ -206,9 +211,21 @@ func (m BackupExecutionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if m.done {
|
switch msg.String() {
|
||||||
switch msg.String() {
|
case "ctrl+c", "esc":
|
||||||
case "enter", "esc", "q":
|
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
|
return m.parent, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,7 +257,12 @@ func (m BackupExecutionModel) View() string {
|
|||||||
|
|
||||||
// Status with spinner
|
// Status with spinner
|
||||||
if !m.done {
|
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 {
|
} else {
|
||||||
s.WriteString(fmt.Sprintf(" %s\n\n", m.status))
|
s.WriteString(fmt.Sprintf(" %s\n\n", m.status))
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type RestoreExecutionModel struct {
|
|||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc // Cancel function to stop the operation
|
||||||
archive ArchiveInfo
|
archive ArchiveInfo
|
||||||
targetDB string
|
targetDB string
|
||||||
cleanFirst bool
|
cleanFirst bool
|
||||||
@@ -44,19 +45,23 @@ type RestoreExecutionModel struct {
|
|||||||
spinnerFrames []string
|
spinnerFrames []string
|
||||||
|
|
||||||
// Results
|
// Results
|
||||||
done bool
|
done bool
|
||||||
err error
|
cancelling bool // True when user has requested cancellation
|
||||||
result string
|
err error
|
||||||
elapsed time.Duration
|
result string
|
||||||
|
elapsed time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, ctx context.Context, archive ArchiveInfo, targetDB string, cleanFirst, createIfMissing bool, restoreType string, cleanClusterFirst bool, existingDBs []string, saveDebugLog bool, workDir 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, saveDebugLog bool, workDir string) RestoreExecutionModel {
|
||||||
|
// Create a cancellable context derived from parent
|
||||||
|
childCtx, cancel := context.WithCancel(ctx)
|
||||||
return RestoreExecutionModel{
|
return RestoreExecutionModel{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: log,
|
logger: log,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
ctx: ctx,
|
ctx: childCtx,
|
||||||
|
cancel: cancel,
|
||||||
archive: archive,
|
archive: archive,
|
||||||
targetDB: targetDB,
|
targetDB: targetDB,
|
||||||
cleanFirst: cleanFirst,
|
cleanFirst: cleanFirst,
|
||||||
@@ -274,11 +279,32 @@ func (m RestoreExecutionModel) 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", "esc":
|
||||||
// Always allow quitting
|
if !m.done && !m.cancelling {
|
||||||
return m.parent, tea.Quit
|
// User requested cancellation - cancel the context
|
||||||
|
m.cancelling = true
|
||||||
case "enter", " ", "esc":
|
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 {
|
if m.done {
|
||||||
return m.parent, nil
|
return m.parent, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user