From 6c01575f245335fdc9562c28085f86eb29ae3259 Mon Sep 17 00:00:00 2001 From: Nathan Baird Date: Fri, 22 May 2026 13:21:44 -0700 Subject: [PATCH] Eliminate conhost.exe from git.exe and hooks process launches GVFS launches git.exe processes with CreateNoWindow=true (or CREATE_NO_WINDOW in native code). Despite the name, this flag tells Windows to create a new hidden console for each child process, which allocates a conhost.exe instance. For frequent, small git operations (e.g., during prefetch), the per-process conhost creation/teardown overhead is disproportionately large. Changes: - GitProcess.cs: Set CreateNoWindow=false. With UseShellExecute=false and stdout/stderr redirected to pipes, the child inherits the parent's console state. Since GVFS.Mount runs as a service with no console, the child gets no console and no conhost. Also remove the unused redirectStandardError parameter (all callers pass true). - ProcessHelper.cs: Set CreateNoWindow=false unconditionally. When redirectOutput is true, I/O goes through pipes so no console is needed. When redirectOutput is false, the child inherits the parent's console handles, which is correct for terminal contexts and harmless in service contexts (output was already going to an invisible hidden console). - GitHooksLoader.cpp: Use DETACHED_PROCESS instead of CREATE_NO_WINDOW in the no-console branch. Explicitly detaches from any console without allocating a new one. The console branch (user terminal) is unchanged. Verified with edge-case tests across terminal, hidden-console, and fully-detached (DETACHED_PROCESS) parent contexts. All git status, git fetch (prefetch hook), and gvfs health scenarios pass. Assisted-by: Tyrie Vella Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Common/Git/GitProcess.cs | 16 ++++++++++++---- GVFS/GVFS.Common/ProcessHelper.cs | 11 ++++++++++- GVFS/GitHooksLoader/GitHooksLoader.cpp | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs index caca4df64..b818fd915 100644 --- a/GVFS/GVFS.Common/Git/GitProcess.cs +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -815,16 +815,24 @@ public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize) return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 -c repack.packKeptObjects=true multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize} --no-progress"); } - public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory, bool usePreCommandHook) + public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, string gitObjectsDirectory, bool usePreCommandHook) { ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath); processInfo.WorkingDirectory = workingDirectory; processInfo.UseShellExecute = false; processInfo.RedirectStandardInput = true; processInfo.RedirectStandardOutput = true; - processInfo.RedirectStandardError = redirectStandardError; + processInfo.RedirectStandardError = true; processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = true; + + // CreateNoWindow=false avoids allocating a hidden conhost.exe per child + // process. This is safe because both stdout and stderr are redirected via + // pipes, so the child never needs a console for I/O. If a future change + // stops redirecting either stream (to forward output to the parent console + // instead), CreateNoWindow must be set to true for that case — otherwise + // the non-redirected stream inherits the parent's console handle, which + // may be absent when running as a service, causing lost output. + processInfo.CreateNoWindow = false; processInfo.StandardOutputEncoding = UTF8NoBOM; processInfo.StandardErrorEncoding = UTF8NoBOM; @@ -903,7 +911,7 @@ protected virtual Result InvokeGitImpl( // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx // To avoid deadlocks, use asynchronous read operations on at least one of the streams. // Do not perform a synchronous read to the end of both redirected streams. - using (this.executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, redirectStandardError: true, gitObjectsDirectory: gitObjectsDirectory, usePreCommandHook: usePreCommandHook)) + using (this.executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, gitObjectsDirectory: gitObjectsDirectory, usePreCommandHook: usePreCommandHook)) { StringBuilder output = new StringBuilder(); StringBuilder errors = new StringBuilder(); diff --git a/GVFS/GVFS.Common/ProcessHelper.cs b/GVFS/GVFS.Common/ProcessHelper.cs index a9731d6d5..a67f4159e 100644 --- a/GVFS/GVFS.Common/ProcessHelper.cs +++ b/GVFS/GVFS.Common/ProcessHelper.cs @@ -18,7 +18,16 @@ public static ProcessResult Run(string programName, string args, bool redirectOu processInfo.RedirectStandardOutput = redirectOutput; processInfo.RedirectStandardError = redirectOutput; processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = redirectOutput; + + // CreateNoWindow=false avoids allocating a hidden conhost.exe per child + // process. When redirectOutput is true, I/O goes through pipes so no + // console is needed. When redirectOutput is false, the child inherits the + // parent's console handles — this works when the parent has a console + // (e.g., GVFS.Hooks invoked from a terminal), but output is silently lost + // when the parent has no console (e.g., service context). This is + // acceptable because CreateNoWindow=true would only send that output to + // an invisible hidden console instead. + processInfo.CreateNoWindow = false; processInfo.Arguments = args; return Run(processInfo); diff --git a/GVFS/GitHooksLoader/GitHooksLoader.cpp b/GVFS/GitHooksLoader/GitHooksLoader.cpp index dfb259b87..9f1619d0a 100644 --- a/GVFS/GitHooksLoader/GitHooksLoader.cpp +++ b/GVFS/GitHooksLoader/GitHooksLoader.cpp @@ -129,7 +129,7 @@ int ExecuteHook(const std::wstring &applicationName, wchar_t *hookName, int argc /* Git disallows stdin from hooks */ si.dwFlags = STARTF_USESTDHANDLES; - creationFlags |= CREATE_NO_WINDOW; + creationFlags |= DETACHED_PROCESS; } ZeroMemory(&pi, sizeof(pi));