What am I trying to solve
To improve my build pipeline, I'd like to add an end-to-end test step. I'm planning to achieve it by means of a CLI tool (.NET "Console App"). The tool will spin up and orchestrate a few npm
/node
commands (processes).
More specifically, there will be:
- a back-end process;
- a front-end process;
- and a test process.
When a test process (3
) completes, the CLI tool should terminate back-end (1
) and front-end (2
) processes gracefully, plus return 0
exit code if every orchestrated process has successfully terminated.
Trouble
In my Minimal, Complete, and Verifiable example below I'm trying to spin up a process serviceAlikeProcess
and a failing process (brokenWithErrorProcess
). When the latter one fails, I'm trying to forcibly terminate the former one via Kill(process)
method.
!!! As it is suggested here, the node
/npm
processes are being launched via cmd
process. I.e. I'm first spinning up a cmd
process, and then write node test.js
to its stdin
stream. The node
process gets launched just fine but when the cmd
process is terminated later, the node
process keeps running and producing the output.
I suppose this happens due to the fact cmd
and node
processes are not getting linked in a parent-child relationship (because if I manually terminate the cmd
process from a Task Manager, I observe same exact behavior).
Question
How do I reliably kill both processes?
Idea: I was thinking about capturing the node
process' pid
and then terminate both cmd
and node
processes myself, but I haven't found a way to capture that pid
...
Code
Program.cs
using System;
using System.Diagnostics;
using System.IO;
namespace RunE2E
{
public class Program
{
static string currentDirectory = Directory.GetCurrentDirectory();
public static int Main(string[] args)
{
var serviceAlikeProcess = StartProcessViaCmd("node", "test.js", "");
var brokenWithErrorProcess = StartProcessViaCmd("npm", "THIS IS NOT A REAL COMMAND, THEREFORE EXPECTED TO FAIL", "");
brokenWithErrorProcess.Exited += (_, __) => KillProcess(serviceAlikeProcess);
serviceAlikeProcess.WaitForExit();
return serviceAlikeProcess.ExitCode;
}
private static Process StartProcessViaCmd(string command, string arguments, string workingDirectory)
{
workingDirectory = NormalizeWorkingDirectory(workingDirectory);
var process = new Process
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo
{
FileName = "cmd",
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
}
};
process.ErrorDataReceived += (_, e) => handle(command, arguments, workingDirectory, "ERROR", e.Data);
process.OutputDataReceived += (_, e) => handle(command, arguments, workingDirectory, "OUTPUT", e.Data);
try
{
Console.WriteLine($"[{workingDirectory}] {command} {arguments}");
var _ = process.Start();
process.BeginOutputReadLine();
process.StandardInput.WriteLine($"{command} {arguments} & exit");
}
catch (Exception exc)
{
Console.WriteLine($"[{workingDirectory}] {command} {arguments} : {exc}");
throw;
}
return process;
}
static string NormalizeWorkingDirectory(string workingDirectory)
{
if (string.IsNullOrWhiteSpace(workingDirectory))
return currentDirectory;
else if (Path.IsPathRooted(workingDirectory))
return workingDirectory;
else
return Path.GetFullPath(Path.Combine(currentDirectory, workingDirectory));
}
static Action<string, string, string, string, string> handle =
(string command, string arguments, string workingDirectory, string level, string message) =>
Console.WriteLine($"[{workingDirectory}] {command} {arguments} {level}: {message}");
static void KillProcess(Process process)
{
if (process != null && !process.HasExited)
process.Kill();
}
}
}
test.js
setInterval(() => {
console.info(new Date());
}, 1000);
I really really don't like ending up answering my own question(s), especially when the answer is based on a hack-ish way of achieving the results.
However, I understand that this may save somebody else their time. So, here's my solution:
Code
Additionally, one may want to use the following methods when dealing with .NET Core processes.