Add pause/resume support with SPACE key#625
Add pause/resume support with SPACE key#625sheikhshaheerimran wants to merge 1 commit intoOJ:devfrom
Conversation
- Add PauseController for worker synchronization - Add keyboard listener for SPACE key toggle - Show [PAUSED] in progress bar when paused
|
@firefart Requesting your review |
There was a problem hiding this comment.
Pull request overview
This pull request adds runtime pause/resume functionality to gobuster, allowing users to pause and resume scans by pressing the SPACE key during execution. This addresses the enhancement request in issue #18.
Changes:
- Implemented a thread-safe
PauseControllerwith channel-based synchronization for pausing/resuming workers - Added cross-platform keyboard listeners (Unix and Windows) with raw terminal mode for real-time key detection
- Integrated pause functionality into worker loops with context-aware blocking
- Enhanced progress bar to display pause status and user instructions
- Updated Windows terminal clear sequence to use proper ANSI escape codes
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| libgobuster/pause.go | Core pause controller implementation with mutex-protected state and channel-based wait/resume mechanism |
| libgobuster/pause_test.go | Comprehensive unit tests for pause controller functionality including concurrency scenarios |
| libgobuster/libgobuster.go | Integration of pause checks into worker loop for blocking on pause events |
| cli/keyboard.go | Unix/Linux keyboard listener with raw terminal mode and signal handling |
| cli/keyboard_windows.go | Windows keyboard listener with platform-specific stdin handling |
| cli/gobuster.go | UI integration showing pause status in progress bar and starting keyboard listener conditionally |
| cli/const_windows.go | Windows terminal clear line constant updated to proper ANSI escape sequence |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func TestPauseControllerConcurrency(t *testing.T) { | ||
| t.Parallel() | ||
| pc := NewPauseController() | ||
|
|
||
| var wg sync.WaitGroup | ||
| for i := 0; i < 100; i++ { | ||
| wg.Add(1) | ||
| go func() { | ||
| defer wg.Done() | ||
| for j := 0; j < 100; j++ { | ||
| pc.Pause() | ||
| _ = pc.IsPaused() | ||
| pc.Resume() | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| wg.Wait() | ||
| } |
There was a problem hiding this comment.
The concurrency test doesn't cover the critical race condition scenario where multiple goroutines are blocked in Wait() while Resume()/Toggle() is called. This test only exercises Pause/Resume/IsPaused in a loop without any goroutines waiting.
Add a test case where multiple goroutines call Wait() and are blocked, then call Toggle()/Resume() to ensure all waiting goroutines are properly unblocked.
| func StartKeyboardListener(ctx context.Context, g *libgobuster.Gobuster, cancel context.CancelFunc) func() { | ||
| fd := int(os.Stdin.Fd()) | ||
| oldState, err := term.MakeRaw(fd) | ||
| if err != nil { | ||
| return func() {} | ||
| } | ||
|
|
||
| var restoreOnce sync.Once | ||
| restoreTerminal := func() { | ||
| restoreOnce.Do(func() { | ||
| _ = term.Restore(fd, oldState) | ||
| }) | ||
| } | ||
|
|
||
| // restore terminal on signals | ||
| sigCh := make(chan os.Signal, 1) | ||
| signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) | ||
|
|
||
| go func() { | ||
| select { | ||
| case sig := <-sigCh: | ||
| restoreTerminal() | ||
| signal.Stop(sigCh) | ||
| // re-raise signal | ||
| p, _ := os.FindProcess(os.Getpid()) | ||
| _ = p.Signal(sig.(syscall.Signal)) | ||
| case <-ctx.Done(): | ||
| signal.Stop(sigCh) | ||
| } | ||
| }() | ||
|
|
||
| // read keys and send to channel | ||
| keyCh := make(chan byte, 1) | ||
| go func() { | ||
| buf := make([]byte, 1) | ||
| for { | ||
| n, err := os.Stdin.Read(buf) | ||
| if err != nil || n == 0 { | ||
| return | ||
| } | ||
| select { | ||
| case keyCh <- buf[0]: | ||
| case <-ctx.Done(): | ||
| return | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| // handle key events | ||
| go func() { | ||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| return | ||
| case key := <-keyCh: | ||
| if key == ' ' { | ||
| g.Pause.Toggle() | ||
| } | ||
| // Ctrl+C | ||
| if key == 3 { | ||
| restoreTerminal() | ||
| cancel() | ||
| return | ||
| } | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| return restoreTerminal | ||
| } |
There was a problem hiding this comment.
The keyboard listener functionality lacks test coverage. Consider adding tests to verify:
- Correct handling of SPACE key for pause/resume toggle
- Correct handling of Ctrl+C
- Terminal state restoration on various exit paths
- Goroutine cleanup on context cancellation
| func StartKeyboardListener(ctx context.Context, g *libgobuster.Gobuster, cancel context.CancelFunc) func() { | ||
| fd := int(syscall.Stdin) | ||
| oldState, err := term.MakeRaw(fd) | ||
| if err != nil { | ||
| return func() {} | ||
| } | ||
|
|
||
| var restoreOnce sync.Once | ||
| restoreTerminal := func() { | ||
| restoreOnce.Do(func() { | ||
| _ = term.Restore(fd, oldState) | ||
| }) | ||
| } | ||
|
|
||
| // restore terminal on signals | ||
| sigCh := make(chan os.Signal, 1) | ||
| signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) | ||
|
|
||
| go func() { | ||
| select { | ||
| case <-sigCh: | ||
| restoreTerminal() | ||
| signal.Stop(sigCh) | ||
| cancel() | ||
| case <-ctx.Done(): | ||
| signal.Stop(sigCh) | ||
| } | ||
| }() | ||
|
|
||
| // read keys and send to channel | ||
| keyCh := make(chan byte, 1) | ||
| go func() { | ||
| buf := make([]byte, 1) | ||
| for { | ||
| n, err := os.Stdin.Read(buf) | ||
| if err != nil || n == 0 { | ||
| return | ||
| } | ||
| select { | ||
| case keyCh <- buf[0]: | ||
| case <-ctx.Done(): | ||
| return | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| // handle key events | ||
| go func() { | ||
| for { | ||
| select { | ||
| case <-ctx.Done(): | ||
| return | ||
| case key := <-keyCh: | ||
| if key == ' ' { | ||
| g.Pause.Toggle() | ||
| } | ||
| // Ctrl+C | ||
| if key == 3 { | ||
| restoreTerminal() | ||
| cancel() | ||
| return | ||
| } | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| return restoreTerminal | ||
| } |
There was a problem hiding this comment.
The keyboard listener functionality lacks test coverage. Consider adding tests to verify:
- Correct handling of SPACE key for pause/resume toggle
- Correct handling of Ctrl+C
- Terminal state restoration on various exit paths
- Goroutine cleanup on context cancellation
| g.Pause.Toggle() | ||
| } | ||
| // Ctrl+C | ||
| if key == 3 { |
There was a problem hiding this comment.
The magic number 3 for Ctrl+C detection lacks clarity. Consider defining a named constant such as const ctrlC = 3 to make the code more maintainable and self-documenting.
| g.Pause.Toggle() | ||
| } | ||
| // Ctrl+C | ||
| if key == 3 { |
There was a problem hiding this comment.
The magic number 3 for Ctrl+C detection lacks clarity. Consider defining a named constant such as const ctrlC = 3 to make the code more maintainable and self-documenting.
Add runtime pause/resume functionality for scans. Press SPACE to pause, press again to resume.
[PAUSED]prefix when pausedCloses #18