Skip to content

Instantly share code, notes, and snippets.

@TehGM
Created August 3, 2020 09:07
Show Gist options
  • Save TehGM/c953b670ad8019b2b2be6af7b14807c2 to your computer and use it in GitHub Desktop.
Save TehGM/c953b670ad8019b2b2be6af7b14807c2 to your computer and use it in GitHub Desktop.
Test for process start leaking memory
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace ProcessStartLeakTest
{
class Program
{
static int _iteration = 0;
static async Task Main(string[] args)
{
TimeSpan waitTime = TimeSpan.Zero;
TerminalResult result;
// for Raspberry Pi: "xprintidle"
string prc = "ifconfig";
// for windows: "ipconfig"
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
prc = "ipconfig";
for (; ; )
{
_iteration++;
result = await ExecuteAndWaitAsync(prc, false).ConfigureAwait(false);
if (waitTime > TimeSpan.Zero)
await Task.Delay(waitTime).ConfigureAwait(false);
}
}
// note this code has some parts that aren't even needed - I was simply trying anything to solve this problem at this point
public static async Task<TerminalResult> ExecuteAndWaitAsync(string command, bool asRoot, CancellationToken cancellationToken = default)
{
using Process prc = CreateNewProcess(command, asRoot);
// we need to redirect stdstreams to read them
prc.StartInfo.RedirectStandardOutput = true;
prc.StartInfo.RedirectStandardError = true;
// start the process
Console.WriteLine($"{_iteration}: Starting the process");
using Task waitForExitTask = WaitForExitAsync(prc, cancellationToken);
prc.Start();
// read streams
string[] streamResults = await Task.WhenAll(prc.StandardOutput.ReadToEndAsync(), prc.StandardError.ReadToEndAsync()).ConfigureAwait(false);
// wait till it fully exits, but no longer than half a second
// this prevents hanging when process has already finished, but takes long time to fully close
await Task.WhenAny(waitForExitTask, Task.Delay(500, cancellationToken)).ConfigureAwait(false);
// if process still didn't exit, force kill it
if (!prc.HasExited)
prc.Kill(true); // doing it with a try-catch approach instead of HasExited check gives no difference
return new TerminalResult(streamResults[0], streamResults[1]);
}
public static Task<int> WaitForExitAsync(Process process, CancellationToken cancellationToken = default)
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
IDisposable tokenRegistration = null;
EventHandler callback = null;
tokenRegistration = cancellationToken.Register(() =>
{
Unregister();
tcs.TrySetCanceled(cancellationToken);
});
callback = (sender, args) =>
{
Unregister();
tcs.TrySetResult(process.ExitCode);
};
process.Exited += callback;
process.EnableRaisingEvents = true;
void Unregister()
{
lock (tcs)
{
if (tokenRegistration == null)
return;
process.EnableRaisingEvents = false;
process.Exited -= callback;
tokenRegistration?.Dispose();
tokenRegistration = null;
}
}
return tcs.Task;
}
private static Process CreateNewProcess(string command, bool asRoot)
{
Console.WriteLine($"{_iteration}: Creating process: {command}");
Process prc = new Process();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string escapedCommand = command.Replace("\"", "\\\"");
// if as root, just sudo it
if (asRoot)
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"sudo {escapedCommand}\"");
// if not as root, we need to open it as current user
// this may still run as root if the process is running as root
else
prc.StartInfo = new ProcessStartInfo("/bin/bash", $"-c \"{escapedCommand}\"");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
prc.StartInfo = new ProcessStartInfo("CMD.exe", $"/C {command}");
if (asRoot)
prc.StartInfo.Verb = "runas";
}
else
throw new PlatformNotSupportedException($"{nameof(ExecuteAndWaitAsync)} is only supported on Windows and Linux platforms.");
prc.StartInfo.UseShellExecute = false;
prc.StartInfo.CreateNoWindow = true;
return prc;
}
}
public class TerminalResult
{
public string Output { get; }
public string Errors { get; }
public bool HasErrors => !string.IsNullOrEmpty(this.Errors);
public TerminalResult(string output, string errors)
{
this.Output = output;
this.Errors = errors;
}
public TerminalResult(string output)
: this(output, null) { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment