mirror of
https://github.com/go-task/task.git
synced 2025-12-16 19:57:43 +01:00
wip: add synchronized output to prevent prompt interleaving
- Add SyncWriter to synchronize stdout/stderr writes with prompts - Add promptMutex to serialize interactive prompts - Keep rawStdout/rawStderr for BubbleTea to avoid deadlock - Update test Taskfile with nested deps scenario
This commit is contained in:
@@ -54,9 +54,11 @@ type (
|
|||||||
Failfast bool
|
Failfast bool
|
||||||
|
|
||||||
// I/O
|
// I/O
|
||||||
Stdin io.Reader
|
Stdin io.Reader
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
|
rawStdout io.Writer // unwrapped stdout for prompts
|
||||||
|
rawStderr io.Writer // unwrapped stderr for prompts
|
||||||
|
|
||||||
// Internal
|
// Internal
|
||||||
Taskfile *ast.Taskfile
|
Taskfile *ast.Taskfile
|
||||||
@@ -71,6 +73,7 @@ type (
|
|||||||
fuzzyModel *fuzzy.Model
|
fuzzyModel *fuzzy.Model
|
||||||
fuzzyModelOnce sync.Once
|
fuzzyModelOnce sync.Once
|
||||||
|
|
||||||
|
promptMutex sync.Mutex
|
||||||
concurrencySemaphore chan struct{}
|
concurrencySemaphore chan struct{}
|
||||||
taskCallCount map[string]*int32
|
taskCallCount map[string]*int32
|
||||||
mkdirMutexMap map[string]*sync.Mutex
|
mkdirMutexMap map[string]*sync.Mutex
|
||||||
|
|||||||
28
internal/output/sync_writer.go
Normal file
28
internal/output/sync_writer.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncWriter wraps an io.Writer with a mutex to synchronize writes.
|
||||||
|
// This is used to prevent output from interleaving with interactive prompts.
|
||||||
|
type SyncWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
mu *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSyncWriter creates a new SyncWriter that uses the provided mutex.
|
||||||
|
func NewSyncWriter(w io.Writer, mu *sync.Mutex) *SyncWriter {
|
||||||
|
return &SyncWriter{
|
||||||
|
w: w,
|
||||||
|
mu: mu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer with synchronized access.
|
||||||
|
func (sw *SyncWriter) Write(p []byte) (n int, err error) {
|
||||||
|
sw.mu.Lock()
|
||||||
|
defer sw.mu.Unlock()
|
||||||
|
return sw.w.Write(p)
|
||||||
|
}
|
||||||
11
requires.go
11
requires.go
@@ -27,7 +27,16 @@ func (e *Executor) promptForInteractiveVars(t *ast.Task, call *Call) (bool, erro
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
prompter := prompt.New()
|
// Lock to prevent multiple parallel prompts from interleaving
|
||||||
|
e.promptMutex.Lock()
|
||||||
|
defer e.promptMutex.Unlock()
|
||||||
|
|
||||||
|
// Use raw stderr for prompts to avoid deadlock with SyncWriter
|
||||||
|
prompter := &prompt.Prompter{
|
||||||
|
Stdin: e.Stdin,
|
||||||
|
Stdout: e.rawStdout,
|
||||||
|
Stderr: e.rawStderr,
|
||||||
|
}
|
||||||
var prompted bool
|
var prompted bool
|
||||||
|
|
||||||
for _, requiredVar := range t.Requires.Vars {
|
for _, requiredVar := range t.Requires.Vars {
|
||||||
|
|||||||
11
setup.go
11
setup.go
@@ -179,6 +179,17 @@ func (e *Executor) setupStdFiles() {
|
|||||||
if e.Stderr == nil {
|
if e.Stderr == nil {
|
||||||
e.Stderr = os.Stderr
|
e.Stderr = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep raw references for interactive prompts
|
||||||
|
e.rawStdout = e.Stdout
|
||||||
|
e.rawStderr = e.Stderr
|
||||||
|
|
||||||
|
// Wrap with synchronized writers when interactive mode is enabled
|
||||||
|
// to prevent output from interleaving with prompts
|
||||||
|
if e.Interactive {
|
||||||
|
e.Stdout = output.NewSyncWriter(e.Stdout, &e.promptMutex)
|
||||||
|
e.Stderr = output.NewSyncWriter(e.Stderr, &e.promptMutex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) setupLogger() {
|
func (e *Executor) setupLogger() {
|
||||||
|
|||||||
1
testdata/interactive_vars/.taskrc.yml
vendored
Normal file
1
testdata/interactive_vars/.taskrc.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
interactive: true
|
||||||
71
testdata/interactive_vars/Taskfile.yml
vendored
71
testdata/interactive_vars/Taskfile.yml
vendored
@@ -1,25 +1,66 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
simple:
|
main:
|
||||||
requires:
|
desc: Main task with nested deps
|
||||||
vars:
|
deps:
|
||||||
- MY_VAR
|
- dep-a
|
||||||
|
- dep-b
|
||||||
cmds:
|
cmds:
|
||||||
- echo "{{.MY_VAR}}"
|
- echo "Main task done!"
|
||||||
|
|
||||||
with-enum:
|
dep-a:
|
||||||
requires:
|
desc: Dependency A
|
||||||
vars:
|
deps:
|
||||||
- name: ENV
|
- leaf-a1
|
||||||
enum: [dev, staging, prod]
|
- leaf-a2
|
||||||
cmds:
|
cmds:
|
||||||
- echo "{{.ENV}}"
|
- echo "Dep A done"
|
||||||
|
|
||||||
multiple:
|
dep-b:
|
||||||
|
desc: Dependency B
|
||||||
|
deps:
|
||||||
|
- leaf-b1
|
||||||
|
- leaf-b2
|
||||||
|
cmds:
|
||||||
|
- echo "Dep B done"
|
||||||
|
|
||||||
|
leaf-a1:
|
||||||
|
desc: Leaf A1 with enum
|
||||||
requires:
|
requires:
|
||||||
vars:
|
vars:
|
||||||
- VAR1
|
- name: VAR_A1
|
||||||
- VAR2
|
enum:
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- gamma
|
||||||
cmds:
|
cmds:
|
||||||
- echo "{{.VAR1}} {{.VAR2}}"
|
- echo "Leaf A1 {{.VAR_A1}}"
|
||||||
|
|
||||||
|
leaf-a2:
|
||||||
|
desc: Leaf A2 with text
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- VAR_A2
|
||||||
|
cmds:
|
||||||
|
- echo "Leaf A2 {{.VAR_A2}}"
|
||||||
|
|
||||||
|
leaf-b1:
|
||||||
|
desc: Leaf B1 with enum
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: VAR_B1
|
||||||
|
enum:
|
||||||
|
- one
|
||||||
|
- two
|
||||||
|
- three
|
||||||
|
cmds:
|
||||||
|
- echo "Leaf B1 {{.VAR_B1}}"
|
||||||
|
|
||||||
|
leaf-b2:
|
||||||
|
desc: Leaf B2 with text
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- VAR_B2
|
||||||
|
cmds:
|
||||||
|
- echo "Leaf B2 {{.VAR_B2}}"
|
||||||
|
|||||||
Reference in New Issue
Block a user