I've written a C# program to act as a precommit hook for Mercurial, but the only way I could get it working reliably was kind of convoluted, so I must have missed something. I'm running TortoiseHg 6.4.5 on Windows 10 Pro 64-bit and the program is targeting .NET 6.0.
The objective is to obtain a list of the changed files and process them just before commit. The most obvious thing seems to work at first:
IEnumerable<string> GetHgStatus()
{
// never terminates when a large number of files have been changed, and if I
// kill the hung hg.exe process there's nothing in the stdout/stderr streams.
// run exe: hg status -n -m -a
var psi = new ProcessStartInfo
{
// check status:
// -n = don't output leading status indicator, eg. M | A | R
// -m = show only modified files
// -a = show only files that have been added
FileName = config.HgPath,
Arguments = $"status -n -m -a",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = config.RepoPath,
};
var exe = Process.Start(psi);
try
{
exe.WaitForExit(); // never returns
while (!exe.StandardOutput.EndOfStream)
yield return exe.StandardOutput.ReadLine();
}
finally
{
exe.Dispose();
}
}
However, if the list of changed files is very long, the wait never completes. I see an instance of hg.exe in the task list with around 57-60MB allocated and doing nothing. If I kill it, no results are output to stdout or stderr.
I never have an issue when running this command from the command line, so I tried running it within a cmd shell:
IEnumerable<string> GetHgStatus()
{
// never terminates when a large number of files have been changed, and if I
// kill the hung hg.exe process there's nothing in the stdout/stderr streams.
// run exe: hg status -n -m -a
var psi = new ProcessStartInfo
{
// check status:
// -n = don't output leading status indicator, eg. M | A | R
// -m = show only modified files
// -a = show only files that have been added
FileName = "cmd.exe",
Arguments = $"/c \"{config.HgPath}\" status -n -m -a",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = config.RepoPath,
};
var exe = Process.Start(psi);
try
{
exe.WaitForExit(); // never returns
while (!exe.StandardOutput.EndOfStream)
yield return exe.StandardOutput.ReadLine();
}
finally
{
exe.Dispose();
}
}
This has the exact same behaviour as the first version. If I toggle CreateNoWindow option to false, then I can see hg creating a command shell window, and then it just sits there.
The only way I could get this to work reliably was to abandon redirecting the stdout/stderr, and redirect the output to a temp file that I then read in:
IEnumerable<string> GetHgStatus()
{
// have to loop catching file access exceptions until the file is accessible
// run exe: hg status -n -m -a
var output = Path.GetTempFileName();
var psi = new ProcessStartInfo
{
// check status:
// -n = don't output leading status indicator, eg. M | A | R
// -m = show only modified files
// -a = show only files that have been added
FileName = "cmd.exe",
Arguments = $"/c \"{config.HgPath}\" status -n -m -a > {output}",
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = config.RepoPath,
};
var exe = Process.Start(psi);
try
{
exe.WaitForExit();
// have to call Close() then poll until file is accessible
exe.Close();
do
{
try
{
return File.ReadAllLines(output);
}
catch (IOException e) when (e.Message.StartsWith("The process cannot access the file"))
{
continue;
}
} while (true);
}
finally
{
exe.Dispose();
File.Delete(output);
}
}
So two questions:
Is hg.exe waiting for something to consume its output buffer before it writes more? Maybe there are some additional properties to set on the process object to tell it to always read sdout as soon as it is available.
There are some examples here... maybe you need to ReadToEnd
on the stream. https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.standardoutput?view=net-7.0#remarks