mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Fix stdout/stderr pipe hang from lingering Wine background processes
msbuild (and the toolrelay.exe-less fallback path for cl/link/etc.) inherited os.Stdout/os.Stderr directly into the wine subprocess. Wine's wineserver and its service processes (services.exe, winedevice.exe, explorer.exe, ...) inherit those same descriptors and keep running well after the actual build finishes, so a caller piping our output (`| tee`, `| tail`, CI log capture) would never see EOF and hang indefinitely - even though the real build completed in seconds. Both paths now pipe stdout/stderr through our own copy goroutines, wait for the tool's own process (not pipe EOF) to determine completion, and grant a bounded 500ms grace period to drain whatever's already buffered before moving on. Verified against a real hang (msbuild building freetype.vcxproj piped through `tail`) and confirmed instant return after the fix, both on success and on a build error.
This commit is contained in:
+75
-23
@@ -10,10 +10,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
"github.com/Cheviiot/msvc-go-wine/internal/wineenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// pipeDrainGrace bounds how long we wait for a tool's stdout/stderr copy
|
||||||
|
// goroutines to see EOF after the tool's own process has already exited.
|
||||||
|
// Wine keeps wineserver and its service processes (services.exe,
|
||||||
|
// winedevice.exe, explorer.exe, ...) running in the background for reuse
|
||||||
|
// across invocations, and they inherit our pipes' write ends - so EOF can
|
||||||
|
// otherwise never arrive, hanging any caller piping our output (`| tee`,
|
||||||
|
// `| tail`, CI log capture) long after the actual build finished.
|
||||||
|
const pipeDrainGrace = 500 * time.Millisecond
|
||||||
|
|
||||||
// toolRelayName is where `msvc-go-wine install` places the compiled
|
// toolRelayName is where `msvc-go-wine install` places the compiled
|
||||||
// toolrelay.exe helper, shared across all arch bin dirs.
|
// toolrelay.exe helper, shared across all arch bin dirs.
|
||||||
const toolRelayName = "toolrelay.exe"
|
const toolRelayName = "toolrelay.exe"
|
||||||
@@ -70,9 +80,10 @@ func Run(tool string, args []string) int {
|
|||||||
var exitCode int
|
var exitCode int
|
||||||
switch {
|
switch {
|
||||||
case s.rawStdout:
|
case s.rawStdout:
|
||||||
// MSBuild: skip all filtering/toolrelay, inherit stdio directly, and
|
// MSBuild: skip all filtering/toolrelay (its output is meant to be
|
||||||
// add the extra environment MSBuild's own toolset/SDK-detection
|
// read as-is), and add the extra environment MSBuild's own
|
||||||
// props need on top of the generic INCLUDE/LIB/WINEPATH.
|
// toolset/SDK-detection props need on top of the generic
|
||||||
|
// INCLUDE/LIB/WINEPATH.
|
||||||
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
cmd := exec.Command(wineBin, append([]string{toolExePath}, rewritten...)...)
|
||||||
env := buildEnv(paths)
|
env := buildEnv(paths)
|
||||||
for k, v := range msbuildEnv(cfg, paths) {
|
for k, v := range msbuildEnv(cfg, paths) {
|
||||||
@@ -80,9 +91,7 @@ func Run(tool string, args []string) int {
|
|||||||
}
|
}
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
exitCode = runRawStdout(cmd)
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
exitCode = runAndWait(cmd)
|
|
||||||
default:
|
default:
|
||||||
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
relay := filepath.Join(paths.BaseUnix, "bin", toolRelayName)
|
||||||
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
if fi, err := os.Stat(relay); err == nil && !fi.IsDir() {
|
||||||
@@ -173,6 +182,57 @@ func runViaToolRelay(wineBin, relayExe, exePath string, args []string, paths *wi
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runRawStdout runs cmd, copying its stdout/stderr through byte-for-byte
|
||||||
|
// (MSBuild's own console formatting is meant to reach the user as-is). It
|
||||||
|
// pipes rather than inheriting os.Stdout/os.Stderr directly so that only our
|
||||||
|
// own copy goroutines - not the caller's terminal or pipe - are exposed to
|
||||||
|
// Wine's background processes holding those descriptors open; see
|
||||||
|
// pipeDrainGrace.
|
||||||
|
func runRawStdout(cmd *exec.Cmd) int {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
doneOut := make(chan struct{})
|
||||||
|
doneErr := make(chan struct{})
|
||||||
|
go func() { io.Copy(os.Stdout, stdout); close(doneOut) }()
|
||||||
|
go func() { io.Copy(os.Stderr, stderr); close(doneErr) }()
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
drain(doneOut)
|
||||||
|
drain(doneErr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// drain waits for a pipe-copy goroutine to see EOF, but not past
|
||||||
|
// pipeDrainGrace - see its doc comment for why EOF can otherwise never come.
|
||||||
|
func drain(done <-chan struct{}) {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(pipeDrainGrace):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func buildEnv(p *wineenv.Paths) []string {
|
func buildEnv(p *wineenv.Paths) []string {
|
||||||
overrides := map[string]string{
|
overrides := map[string]string{
|
||||||
"INCLUDE": p.Include,
|
"INCLUDE": p.Include,
|
||||||
@@ -219,13 +279,16 @@ func runFiltered(cmd *exec.Cmd, stdoutF, stderrF lineFilter) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
doneOut := make(chan struct{})
|
||||||
wg.Add(2)
|
doneErr := make(chan struct{})
|
||||||
go func() { defer wg.Done(); pumpLines(stdout, os.Stdout, stdoutF) }()
|
go func() { pumpLines(stdout, os.Stdout, stdoutF); close(doneOut) }()
|
||||||
go func() { defer wg.Done(); pumpLines(stderr, os.Stderr, stderrF) }()
|
go func() { pumpLines(stderr, os.Stderr, stderrF); close(doneErr) }()
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
err = cmd.Wait()
|
||||||
|
drain(doneOut)
|
||||||
|
drain(doneErr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
return exitErr.ExitCode()
|
return exitErr.ExitCode()
|
||||||
}
|
}
|
||||||
@@ -248,14 +311,3 @@ func pumpLines(r io.Reader, w *os.File, filter lineFilter) {
|
|||||||
fmt.Fprintln(w, line)
|
fmt.Fprintln(w, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndWait(cmd *exec.Cmd) int {
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
||||||
return exitErr.ExitCode()
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, "msvc-go-wine:", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user