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);


