fix: route air's own log messages to stderr instead of stdout (#887)
* fix: route air's own log messages to stderr instead of stdout Air's internal loggers (main, build, runner, watcher) were writing to stdout, causing air's messages to intermix with the user's application output. This made it impossible to separate them with shell redirections like `air 2>/dev/null`. Change the log output destination from os.Stdout/color.Output to os.Stderr/color.Error so air's messages go to stderr while the user's app output remains on stdout. Also remove dead `c.Stdout` and `c.Stderr` assignments in startCmd() on all platforms — these had no effect after StdoutPipe()/StderrPipe() were already called. Fixes #744 Signed-off-by: majiayu000 <1835304752@qq.com> * test: add regression test for logger stderr routing Signed-off-by: majiayu000 <1835304752@qq.com> * fix: route warning messages in config.go to stderr Signed-off-by: majiayu000 <1835304752@qq.com> * fix: update smoke test to check stderr for air log messages Since air's log output now goes to stderr, the smoke test must check nohup.err instead of nohup.out for the "running" message. Signed-off-by: majiayu000 <1835304752@qq.com> * fix: print splash banner to stderr --------- Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: xiantang <zhujingdi1998@gmail.com>
This commit is contained in:
@@ -45,8 +45,8 @@ jobs:
|
||||
sleep 15
|
||||
echo "" >> main.go
|
||||
sleep 5
|
||||
cat nohup.out
|
||||
count=$(grep "running" nohup.out | wc -l)
|
||||
cat nohup.err
|
||||
count=$(grep "running" nohup.err | wc -l)
|
||||
if [ "$count" -eq "2" ]; then
|
||||
echo "value=PASS" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
Start-Sleep -Seconds 15
|
||||
Add-Content -Path "main.go" -Value "`n"
|
||||
Start-Sleep -Seconds 5
|
||||
if (Test-Path $log) { Get-Content $log }
|
||||
if (Test-Path $log) {
|
||||
$count = (Select-String -Path $log -Pattern "running" | Measure-Object).Count
|
||||
if (Test-Path $err) { Get-Content $err }
|
||||
if (Test-Path $err) {
|
||||
$count = (Select-String -Path $err -Pattern "running" | Measure-Object).Count
|
||||
} else {
|
||||
$count = 0
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func GetVersionInfo() versionInfo { //revive:disable:unexported-return
|
||||
|
||||
func printSplash() {
|
||||
versionInfo := GetVersionInfo()
|
||||
fmt.Printf(`
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
__ _ ___
|
||||
/ /\ | | | |_)
|
||||
/_/--\ |_| |_| \_ %s, built with Go %s
|
||||
|
||||
+2
-2
@@ -383,7 +383,7 @@ func (c *Config) preprocess(args map[string]TomlInfo) error {
|
||||
if !c.Build.IgnoreDangerousRootDir {
|
||||
return fmt.Errorf("refusing to run in %s - this would watch too many files. Please run air in a project directory", dirName)
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "[warning] ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory")
|
||||
fmt.Fprintln(os.Stderr, "[warning] ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory")
|
||||
}
|
||||
|
||||
if c.TmpDir == "" {
|
||||
@@ -539,5 +539,5 @@ func warnDeprecatedBin(cfg *Config) {
|
||||
if cfg.Build.Bin == "" || len(cfg.Build.Entrypoint) > 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(os.Stdout, "[warning] build.bin is deprecated; set build.entrypoint instead")
|
||||
fmt.Fprintln(os.Stderr, "[warning] build.bin is deprecated; set build.entrypoint instead")
|
||||
}
|
||||
|
||||
+12
-12
@@ -336,19 +336,19 @@ cmd = "go build -o ./tmp/main ."
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pipe: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
_, _ = InitConfig(cfgPath, nil)
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
@@ -381,19 +381,19 @@ ignore_dangerous_root_dir = true
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pipe: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
_, _ = InitConfig(cfgPath, nil)
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
@@ -418,19 +418,19 @@ ignore_dangerous_root_dir = false
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pipe: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
_, _ = InitConfig(cfgPath, nil)
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
@@ -455,19 +455,19 @@ cmd = "go build -o ./tmp/main ."
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create pipe: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
os.Stderr = w
|
||||
|
||||
_, _ = InitConfig(cfgPath, nil)
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("failed to close writer: %v", err)
|
||||
}
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
|
||||
+2
-2
@@ -68,9 +68,9 @@ func newLogFunc(colorname string, cfg cfgLog) logFunc {
|
||||
msg = fmt.Sprintf("[%s] %s", t, msg)
|
||||
}
|
||||
if colorname == rawColor {
|
||||
fmt.Fprintf(os.Stdout, msg, v...)
|
||||
fmt.Fprintf(os.Stderr, msg, v...)
|
||||
} else {
|
||||
color.New(getColor(colorname)).Fprintf(color.Output, msg, v...)
|
||||
color.New(getColor(colorname)).Fprintf(color.Error, msg, v...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogFuncWritesToStderr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Capture stderr
|
||||
oldStderr := os.Stderr
|
||||
rErr, wErr, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stderr pipe: %v", err)
|
||||
}
|
||||
os.Stderr = wErr
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
rOut, wOut, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stdout pipe: %v", err)
|
||||
}
|
||||
os.Stdout = wOut
|
||||
|
||||
logFn := newLogFunc(rawColor, cfgLog{})
|
||||
logFn("test message from air")
|
||||
|
||||
wErr.Close()
|
||||
wOut.Close()
|
||||
os.Stderr = oldStderr
|
||||
os.Stdout = oldStdout
|
||||
|
||||
stderrOut, err := io.ReadAll(rErr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read stderr: %v", err)
|
||||
}
|
||||
stdoutOut, err := io.ReadAll(rOut)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read stdout: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(stderrOut), "test message from air") {
|
||||
t.Errorf("expected log output on stderr, got: %q", stderrOut)
|
||||
}
|
||||
if strings.Contains(string(stdoutOut), "test message from air") {
|
||||
t.Errorf("log output should not appear on stdout, got: %q", stdoutOut)
|
||||
}
|
||||
}
|
||||
@@ -71,9 +71,6 @@ 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,7 +4,6 @@ package runner
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -70,9 +69,6 @@ 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,7 +4,6 @@ package runner
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -60,9 +59,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user