diff --git a/app/src/main/java/com/winlator/core/ProcessHelper.java b/app/src/main/java/com/winlator/core/ProcessHelper.java index e8c16fac6a..4c98aec674 100644 --- a/app/src/main/java/com/winlator/core/ProcessHelper.java +++ b/app/src/main/java/com/winlator/core/ProcessHelper.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.Executors; public abstract class ProcessHelper { @@ -106,6 +107,71 @@ public static int exec(String command, String[] envp, File workingDir) { return exec(command, envp, workingDir, null); } + // Execute the command and capture its output. + // Use ProcessBuilder to merge stderr into stdout + // Deadlock-safe strategy: + // includeStderr=true - redirectErrorStream(true): stderr merged into stdout at the OS + // level, single pipe, zero deadlock risk. + // includeStderr=false - streams are kept separate; a daemon thread drains and discards + // stderr concurrently so the 64 KB pipe buffer never fills while + // the main thread reads stdout. This keeps stdout clean for callers + // such as SteamTokenLogin + public static String execWithOutput(String command, String[] envp, File workingDir, boolean includeStderr) { + StringBuilder output = new StringBuilder(); + Thread stderrDrainer = null; + try { + ProcessBuilder pb = new ProcessBuilder(splitCommand(command)); + Map env = pb.environment(); + env.clear(); + if (envp != null) { + for (String kv : envp) { + int eq = kv.indexOf('='); + if (eq > 0) env.put(kv.substring(0, eq), kv.substring(eq + 1)); + } + } + if (workingDir != null) pb.directory(workingDir); + pb.redirectErrorStream(includeStderr); // merge only when caller wants stderr + + java.lang.Process process = pb.start(); + + // When not merging, drain stderr on a daemon thread to prevent pipe-buffer deadlock. + // SteamTokenLogin uses this when calling includeStderr=false + if (!includeStderr) { + final InputStream stderrStream = process.getErrorStream(); + stderrDrainer = new Thread(() -> { + try { + byte[] buf = new byte[4096]; + while (stderrStream.read(buf) != -1) { + if (Thread.currentThread().isInterrupted()) + break; + } + } catch (IOException ignored) {} + }, "stderr-drainer"); + stderrDrainer.setDaemon(true); // won't block app shutdown if something goes wrong + stderrDrainer.start(); + } + + // Read stdout (or the merged stream) inline; EOF arrives after the process exits. + try (BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String l; + while ((l = r.readLine()) != null) output.append(l).append("\n"); + } + + // Process has already exited (we drained its stdout to EOF). + // waitFor() reaps the OS process-table entry. + process.waitFor(); + + if (stderrDrainer != null) { + stderrDrainer.join(5_000); // bounded wait; daemon thread is reaped on JVM exit anyway + } + } catch (Exception e) { + output.append("Error: ").append(e.getMessage()); + } + + // Format output: trim trailing whitespace/newlines + return output.toString().trim(); + } + public static class ProcessInfo { public final int pid; public final int ppid; diff --git a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java index 09d207eb77..b551dc3293 100644 --- a/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java +++ b/app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java @@ -709,48 +709,9 @@ public String execShellCommand(String command, boolean includeStderr) { FileUtils.chmod(box64File, 0755); } - // Execute the command and capture its output. - // - // IMPORTANT: stderr MUST be drained concurrently with stdout, even when - // includeStderr=false. Wine spits out a flood of fixme:/err: lines on - // stderr; if we don't read it, the kernel's pipe buffer (~64 KB) fills, - // wine's next write(stderr,...) blocks, and the whole subprocess hangs - // forever -- which then deadlocks our stdout read too. SteamTokenLogin - // calls this with includeStderr=false, so this used to hang on boot. - try { - Log.d("BionicProgramLauncherComponent", "Shell command is " + finalCommand); - java.lang.Process process = Runtime.getRuntime().exec(finalCommand, envVars.toStringArray(), workingDir != null ? workingDir : imageFs.getRootDir()); - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - - final StringBuilder stderrBuf = new StringBuilder(); - Thread stderrPump = new Thread(() -> { - try { - String l; - while ((l = errorReader.readLine()) != null) { - if (includeStderr) stderrBuf.append(l).append('\n'); - // else: discard, but we MUST still consume the stream - } - } catch (IOException ignored) { - // Subprocess closed stderr; fine. - } - }, "execShellCommand-stderr-pump"); - stderrPump.setDaemon(true); - stderrPump.start(); - - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - process.waitFor(); - stderrPump.join(); - if (includeStderr) output.append(stderrBuf); - } catch (Exception e) { - output.append("Error: ").append(e.getMessage()); - } - - // Format output: trim trailing whitespace/newlines - return output.toString().trim(); + Log.d("BionicProgramLauncherComponent", "Shell command is " + finalCommand); + return ProcessHelper.execWithOutput(finalCommand, envVars.toStringArray(), + workingDir != null ? workingDir : imageFs.getRootDir(), includeStderr); } public void restartWineServer() { diff --git a/app/src/main/java/com/winlator/xenvironment/components/GlibcProgramLauncherComponent.java b/app/src/main/java/com/winlator/xenvironment/components/GlibcProgramLauncherComponent.java index 9f3542cac1..2b19666e63 100644 --- a/app/src/main/java/com/winlator/xenvironment/components/GlibcProgramLauncherComponent.java +++ b/app/src/main/java/com/winlator/xenvironment/components/GlibcProgramLauncherComponent.java @@ -331,27 +331,8 @@ public String execShellCommand(String command, boolean includeStderr) { String finalCommand = box64Path + " " + command; // Execute the command and capture its output - try { - Log.d("GlibcProgramLauncherComponent", "Shell command is " + finalCommand); - java.lang.Process process = Runtime.getRuntime().exec(finalCommand, envVars.toStringArray(), workingDir != null ? workingDir : imageFs.getRootDir()); - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); - - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - if (includeStderr) { - while ((line = errorReader.readLine()) != null) { - output.append(line).append("\n"); - } - } - process.waitFor(); - } catch (Exception e) { - output.append("Error: ").append(e.getMessage()); - } - - // Format output: trim trailing whitespace/newlines - return output.toString().trim(); + Log.d("GlibcProgramLauncherComponent", "Shell command is " + finalCommand); + return ProcessHelper.execWithOutput(finalCommand, envVars.toStringArray(), + workingDir != null ? workingDir : imageFs.getRootDir(), includeStderr); } }