fix: restore stdout/stderr inheritance for TTY detection (#895)

This commit is contained in:
silverwind
2026-04-08 05:44:07 +02:00
committed by GitHub
parent 24363a00a2
commit 3df4a176ee
4 changed files with 60 additions and 0 deletions
+3
View File
@@ -71,6 +71,9 @@ func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser,
return nil, nil, nil, err
}
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Start()
if err != nil {
return nil, nil, nil, err
+49
View File
@@ -3,6 +3,7 @@ package runner
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@@ -865,6 +866,54 @@ func Test_killCmd_SendInterrupt_SlowGracefulExit(t *testing.T) {
elapsed, 1.0-elapsed.Seconds())
}
// Regression test for https://github.com/air-verse/air/issues/894
// Child process must inherit stdout/stderr so ANSI color codes pass through
// and TTY detection (isatty) works correctly.
func TestStartCmdPreservesColorOutput(t *testing.T) {
chdir(t, "_testdata")
origStdout := os.Stdout
origStderr := os.Stderr
captureOutR, captureOutW, err := os.Pipe()
require.NoError(t, err)
captureErrR, captureErrW, err := os.Pipe()
require.NoError(t, err)
os.Stdout = captureOutW
os.Stderr = captureErrW
t.Cleanup(func() {
os.Stdout = origStdout
os.Stderr = origStderr
captureOutR.Close()
captureOutW.Close()
captureErrR.Close()
captureErrW.Close()
})
cfg := &Config{Build: cfgBuild{}}
e := Engine{config: cfg, logger: newLogger(cfg)}
var colorCmd string
if runtime.GOOS == "windows" {
colorCmd = `$e=[char]0x1b; [Console]::Out.Write("${e}[31mhello${e}[0m"); [Console]::Error.Write("${e}[32mworld${e}[0m")`
} else {
colorCmd = `printf '\033[31mhello\033[0m'; printf '\033[32mworld\033[0m' >&2`
}
cmd, _, _, err := e.startCmd(colorCmd)
require.NoError(t, err)
_ = cmd.Wait()
captureOutW.Close()
captureErrW.Close()
stdoutData, _ := io.ReadAll(captureOutR)
stderrData, _ := io.ReadAll(captureErrR)
assert.Contains(t, string(stdoutData), "\033[31m",
"child process stdout should preserve ANSI escape codes for color output")
assert.Contains(t, string(stderrData), "\033[32m",
"child process stderr should preserve ANSI escape codes for color output")
}
func TestIsDangerousRoot(t *testing.T) {
t.Parallel()
+4
View File
@@ -4,6 +4,7 @@ package runner
import (
"io"
"os"
"os/exec"
"syscall"
"time"
@@ -69,6 +70,9 @@ func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser,
return nil, nil, nil, err
}
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Start()
if err != nil {
return nil, nil, nil, err
+4
View File
@@ -4,6 +4,7 @@ package runner
import (
"io"
"os"
"os/exec"
"strconv"
"strings"
@@ -59,6 +60,9 @@ func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser,
return nil, nil, nil, err
}
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Start()
if err != nil {
return nil, nil, nil, err