为了改进我的构建管道,我想添加一个端到端的测试步骤。我计划通过CLI工具(.NET" Console App")来实现它。该工具将启动并编排一些npm
/ node
命令(进程)。
更具体地说,将会:
当测试过程(3
)完成时,CLI工具应该优雅地终止后端(1
)和前端(2
)进程,并返回{{1如果每个协调过程已成功终止,则退出代码。
在我的Minimal, Complete, and Verifiable example下方,我试图启动流程0
和失败的流程(serviceAlikeProcess
)。当后者失败时,我试图通过brokenWithErrorProcess
方法强行终止前一个。
!!! 由于suggested here,Kill(process)
/ node
进程正在通过npm
进程启动。即我首先启动cmd
进程,然后将cmd
写入其node test.js
流。 stdin
进程启动得很好但是当node
进程稍后终止时,cmd
进程会继续运行并生成输出。
我认为这是因为事实node
和cmd
进程未在父子关系中链接(因为如果我手动终止{{1}来自任务管理器的进程,我观察到相同的行为)。
如何可靠地杀死这两个进程?
想法:我在考虑抓住node
流程' cmd
,然后我自己终止了node
和pid
个进程,但我还没有办法捕获cmd
...
node
pid
答案 0 :(得分:0)
我真的真的不喜欢最后回答我自己的问题,特别是当答案基于一种实现结果的黑客方式时。
但是,据我所知,这可能会节省别人的时间。所以,这是我的解决方案:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace RunE2E
{
public class Program
{
static string currentDirectory = Directory.GetCurrentDirectory();
public static int Main(string[] args)
{
var serviceAlikeProcessResult = StartProcessViaCmd("node", "test.js", "");
var serviceAlikeProcess = serviceAlikeProcessResult.MainProcess;
var brokenWithErrorResult = StartProcessViaCmd("npm", "THIS IS NOT A REAL COMMAND, THEREFORE EXPECTED TO FAIL", "");
var brokenWithErrorProcess = brokenWithErrorResult.MainProcess;
brokenWithErrorProcess.Exited += (_, __) =>
{
KillProcesses("Front-End", serviceAlikeProcessResult.MainProcess, serviceAlikeProcessResult.CreatedProcesses);
KillProcesses("E2E-Test", brokenWithErrorResult.MainProcess, brokenWithErrorResult.CreatedProcesses);
};
serviceAlikeProcess.WaitForExit();
return serviceAlikeProcess.ExitCode;
}
private static CommandStartResult 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,
},
};
var createdProcesses = new List<Process>();
process.ErrorDataReceived += (_, e) => handle(command, arguments, workingDirectory, "ERROR", e.Data);
process.OutputDataReceived += (_, e) => handle(command, arguments, workingDirectory, "", e.Data);
var commandId = $"[{workingDirectory}] {command} {arguments}";
try
{
WriteLine(commandId);
createdProcesses = StartProcessAndCapture(commandId, process);
process.BeginOutputReadLine();
process.StandardInput.WriteLine($"{command} {arguments} & exit");
}
catch (Exception exc)
{
WriteLine($"{commandId}: {exc}");
throw;
}
return new CommandStartResult
{
MainProcess = process,
CreatedProcesses = createdProcesses,
};
}
static List<Process> StartProcessAndCapture(string commandId, Process processToStart)
{
var before = Process.GetProcesses().ToList();
var beforePidSet = new HashSet<int>(before.Select(process => process.Id));
var _ = processToStart.Start();
Thread.Sleep(3000);
var after = Process.GetProcesses().ToList();
var newlyCreatedProcessIdList = new HashSet<int>(after.Select(process => process.Id));
newlyCreatedProcessIdList.ExceptWith(beforePidSet);
var createdProcesses = after.Where(process => newlyCreatedProcessIdList.Contains(process.Id)).ToList();
foreach (var process in createdProcesses)
WriteLine($"{commandId} ||| [{process.Id}] {process.ProcessName}", ConsoleColor.Blue);
return createdProcesses;
}
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) =>
{
var defaultColor = Console.ForegroundColor;
Write($"[{workingDirectory}] ");
Write($"{command} ", ConsoleColor.DarkGreen);
Write($"{arguments}", ConsoleColor.Green);
Write($"{level} ", level == "" ? defaultColor : ConsoleColor.Red);
WriteLine($": {message}");
};
static void KillProcesses(string prefix, Process baseProcess, List<Process> processList)
{
processList = baseProcess == null ?
processList :
processList.Where(process => process.Id != baseProcess.Id).Append(baseProcess).ToList();
foreach (var process in processList)
KillProcess(prefix, process);
}
static void KillProcess(string prefix, Process process)
{
if (process != null && !process.HasExited)
try
{
WriteLine(prefix + " | Kill (" + process.ProcessName + ") [" + process.Id + "]");
process.Kill();
}
catch (Win32Exception win32exc)
{
WriteLine(prefix + " | Kill (" + process.ProcessName + ") [" + process.Id + "]: " + win32exc.Message);
}
}
static void WaitForExit(Process process)
{
while (process.HasExited == false) { }
}
static object console = new object();
static void Write(string text, ConsoleColor? color = null)
{
lock (console)
{
var original = Console.ForegroundColor;
Console.ForegroundColor = color.HasValue ? color.Value : original;
Console.Write(text);
Console.ForegroundColor = original;
}
}
static void WriteLine(string text = null, ConsoleColor? color = null)
{
lock (console)
{
var original = Console.ForegroundColor;
Console.ForegroundColor = color.HasValue ? color.Value : original;
Console.WriteLine(text);
Console.ForegroundColor = original;
}
}
}
class CommandStartResult
{
public Process MainProcess { get; set; }
public List<Process> CreatedProcesses { get; set; }
}
}
此外,在处理.NET Core进程时,可能需要使用以下方法。
private static CommandStartResult StartDotnetProcess(string arguments, string workingDirectory)
{
var command = "dotnet";
workingDirectory = NormalizeWorkingDirectory(workingDirectory);
var process = PrepareProcess(command, arguments, workingDirectory);
var createdProcesses = new List<Process>();
var commandId = $"[{workingDirectory}] {command} {arguments}";
try
{
WriteLine(commandId);
createdProcesses = StartProcessAndCapture(commandId, process);
process.BeginOutputReadLine();
}
catch (Exception exc)
{
WriteLine($"{commandId} : {exc}");
throw;
}
return new CommandStartResult
{
MainProcess = process,
CreatedProcesses = createdProcesses,
};
}
private static Process PrepareProcess(
string command,
string arguments,
string workingDirectory
)
{
var process = new Process
{
EnableRaisingEvents = true,
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
},
};
process.ErrorDataReceived += (_, e) => handle(command, arguments, workingDirectory, "ERROR", e.Data);
process.OutputDataReceived += (_, e) => handle(command, arguments, workingDirectory, "", e.Data);
process.StartInfo.Environment.Add("ASPNETCORE_ENVIRONMENT", "Development");
return process;
}