console: fix inconsequential goroutine leak #20667 (#1520)

This commit is contained in:
Daniel Liu 2025-09-21 18:56:56 +08:00 committed by GitHub
parent 2e545959dd
commit 395071e9f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -309,62 +309,62 @@ func (c *Console) Evaluate(statement string) {
// the configured user prompter. // the configured user prompter.
func (c *Console) Interactive() { func (c *Console) Interactive() {
var ( var (
prompt = c.prompt // Current prompt line (used for multi-line inputs) prompt = c.prompt // Current prompt line (used for multi-line inputs)
indents = 0 // Current number of input indents (used for multi-line inputs) indents = 0 // Current number of input indents (used for multi-line inputs)
input = "" // Current user input input = "" // Current user input
scheduler = make(chan string) // Channel to send the next prompt on and receive the input inputLine = make(chan string, 1) // receives user input
inputErr = make(chan error, 1) // receives liner errors
requestLine = make(chan string) // requests a line of input
interrupt = make(chan os.Signal, 1)
) )
// Start a goroutine to listen for promt requests and send back inputs
go func() { // Monitor Ctrl-C. While liner does turn on the relevant terminal mode bits to avoid
for { // the signal, a signal can still be received for unsupported terminals. Unfortunately
// Read the next user input // there is no way to cancel the line reader when this happens. The readLines
line, err := c.prompter.PromptInput(<-scheduler) // goroutine will be leaked in this case.
if err != nil { signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
// In case of an error, either clear the prompt or fail defer signal.Stop(interrupt)
if err == liner.ErrPromptAborted { // ctrl-C
prompt, indents, input = c.prompt, 0, "" // The line reader runs in a separate goroutine.
scheduler <- "" go c.readLines(inputLine, inputErr, requestLine)
continue defer close(requestLine)
}
close(scheduler)
return
}
// User input retrieved, send for interpretation and loop
scheduler <- line
}
}()
// Monitor Ctrl-C too in case the input is empty and we need to bail
abort := make(chan os.Signal, 1)
signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)
// Start sending prompts to the user and reading back inputs // Start sending prompts to the user and reading back inputs
for { for {
// Send the next prompt, triggering an input read and process the result // Send the next prompt, triggering an input read.
scheduler <- prompt requestLine <- prompt
select { select {
case <-abort: case <-interrupt:
// User forcefully quite the console
fmt.Fprintln(c.printer, "caught interrupt, exiting") fmt.Fprintln(c.printer, "caught interrupt, exiting")
return return
case line, ok := <-scheduler: case err := <-inputErr:
// User input was returned by the prompter, handle special cases if err == liner.ErrPromptAborted && indents > 0 {
if !ok || (indents <= 0 && exit.MatchString(line)) { // When prompting for multi-line input, the first Ctrl-C resets
// the multi-line state.
prompt, indents, input = c.prompt, 0, ""
continue
}
return
case line := <-inputLine:
// User input was returned by the prompter, handle special cases.
if indents <= 0 && exit.MatchString(line) {
return return
} }
if onlyWhitespace.MatchString(line) { if onlyWhitespace.MatchString(line) {
continue continue
} }
// Append the line to the input and check for multi-line interpretation // Append the line to the input and check for multi-line interpretation.
input += line + "\n" input += line + "\n"
indents = countIndents(input) indents = countIndents(input)
if indents <= 0 { if indents <= 0 {
prompt = c.prompt prompt = c.prompt
} else { } else {
prompt = strings.Repeat(".", indents*3) + " " prompt = strings.Repeat(".", indents*3) + " "
} }
// If all the needed lines are present, save the command and run // If all the needed lines are present, save the command and run it.
if indents <= 0 { if indents <= 0 {
if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
@ -381,6 +381,18 @@ func (c *Console) Interactive() {
} }
} }
// readLines runs in its own goroutine, prompting for input.
func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
for p := range prompt {
line, err := c.prompter.PromptInput(p)
if err != nil {
errc <- err
} else {
input <- line
}
}
}
// countIndents returns the number of identations for the given input. // countIndents returns the number of identations for the given input.
// In case of invalid input such as var a = } the result can be negative. // In case of invalid input such as var a = } the result can be negative.
func countIndents(input string) int { func countIndents(input string) int {