在Windows上与Gulp进行E2E测试编排:无法杀死进程

Igo*_*nko 13 javascript windows process node.js gulp

我想要实现的目标

这个问题与我最近以一个可怕的hack™ 关闭的另一个问题有关.

我正在尝试编写一个脚本,可以在CI /构建管道的上下文中使用一个步骤.

该脚本应该为我们的Angular单页面应用程序(SPA)运行基于Protractor的端到端测试.

该脚本需要执行以下操作(按顺序):

  1. 运行名为"App"的.NET Core微服务
  2. 运行名为"Web"的.NET Core微服务
  3. 运行SPA
  4. 运行一个执行Protractor测试的命令
  5. 在步骤4完成后(成功或出错),终止在步骤1-3中创建的进程.这绝对必要的,否则构建将永远不会在CI中完成和/或将会出现僵尸Web/App/SPA进程,这将破坏未来的构建管道执行.

问题

我还没有开始处理第4步("e2e测试"),因为我真的想确保第5步("清理")按预期工作.

正如你猜的那样(右),清理步骤不起作用.具体来说,进程"App"和"Web"不会因某种原因而被杀死并继续运行.

顺便说一句,我确保我的gulp脚本以提升(管理员)权限执行.

问题 - 更新1

我刚刚发现问题的直接原因(我认为),我不知道究竟是什么原因.按照我的预期,有5个进程而不是1个进程.例如,对于App流程,在流程管理器中观察到以下流程:

{                          
  "id": 14840,             
  "binary": "cmd.exe",     
  "title": "Console"       
},                         
{                          
  "id": 12600,             
  "binary": "dotnet.exe",  
  "title": "Console"       
},                         
{                          
  "id": 12976,             
  "binary": "cmd.exe",     
  "title": "Console"       
},                         
{                          
  "id": 5492,              
  "binary": "cmd.exe",     
  "title": "Console"       
},                         
{                          
  "id": 2636,              
  "binary": "App.exe",
  "title": "Console"       
}                          
Run Code Online (Sandbox Code Playgroud)

同样,为Web服务创建了五个进程而不是一个进程:

{                          
  "id": 13264,             
  "binary": "cmd.exe",     
  "title": "Console"       
},                         
{                          
  "id": 1900,              
  "binary": "dotnet.exe",  
  "title": "Console"       
},                         
{                          
  "id": 4668,              
  "binary": "cmd.exe",     
  "title": "Console"       
},                         
{                          
  "id": 15520,             
  "binary": "Web.exe",
  "title": "Console"       
},                         
{                          
  "id": 7516,              
  "binary": "cmd.exe",     
  "title": "Console"       
}                          
Run Code Online (Sandbox Code Playgroud)

我是怎么做的

基本上,这里的工作马是runCmdAndListen()通过运行cmd提供的参数来旋转进程的函数.当函数启动一个进程是Node.js的手段时exec(),它会被推送到createdProcesses数组进行跟踪.

被调用的Gulp步骤CLEANUP = "cleanup"负责迭代createdProcesses和调用.kill('SIGTERM')它们中的每一个,这应该杀死之前创建的所有进程.

gulpfile.js (Gulp任务脚本)

进口和常数

const gulp = require('gulp');
const exec = require('child_process').exec;
const path = require('path');

const RUN_APP = `run-app`;
const RUN_WEB = `run-web`;
const RUN_SPA = `run-spa`;
const CLEANUP = `cleanup`;

const appDirectory = path.join(`..`, `App`);
const webDirectory = path.join(`..`, `Web`);
const spaDirectory = path.join(`.`);

const createdProcesses = [];
Run Code Online (Sandbox Code Playgroud)

runCmdAndListen()

/**
 * Runs a command and taps on `stdout` waiting for a `resolvePhrase` if provided.
 * @param {*} name Title of the process to use in console output.
 * @param {*} command Command to execute.
 * @param {*} cwd Command working directory.
 * @param {*} env Command environment parameters.
 * @param {*} resolvePhrase Phrase to wait for in `stdout` and resolve on.
 * @param {*} rejectOnError Flag showing whether to reject on a message in `stderr` or not.
 */
function runCmdAndListen(name, command, cwd, env, resolvePhrase, rejectOnError) {

  const options = { cwd };
  if (env) options.env = env;

  return new Promise((resolve, reject) => {
    const newProcess = exec(command, options);

    console.info(`Adding a running process with id ${newProcess.pid}`);
    createdProcesses.push({ childProcess: newProcess, isRunning: true });

    newProcess.on('exit', () => {
      createdProcesses
        .find(({ childProcess, _ }) => childProcess.pid === newProcess.pid)
        .isRunning = false;
    });

    newProcess.stdout
      .on(`data`, chunk => {
        if (resolvePhrase && chunk.toString().indexOf(resolvePhrase) >= 0) {
          console.info(`RESOLVED ${name}/${resolvePhrase}`);
          resolve();
        }
      });

    newProcess.stderr
      .on(`data`, chunk => {
        if (rejectOnError) reject(chunk);
      });

    if (!resolvePhrase) {
      console.info(`RESOLVED ${name}`);
      resolve();
    }
  });
}
Run Code Online (Sandbox Code Playgroud)

基本的Gulp任务

gulp.task(RUN_APP, () => runCmdAndListen(
  `[App]`,
  `dotnet run --no-build --no-dependencies`,
  appDirectory,
  { 'ASPNETCORE_ENVIRONMENT': `Development` },
  `Now listening on:`,
  true)
);

gulp.task(RUN_WEB, () => runCmdAndListen(
  `[Web]`,
  `dotnet run --no-build --no-dependencies`,
  webDirectory,
  { 'ASPNETCORE_ENVIRONMENT': `Development` },
  `Now listening on:`,
  true)
);

gulp.task(RUN_SPA, () => runCmdAndListen(
  `[SPA]`,
  `npm run start-prodish-for-e2e`,
  spaDirectory,
  null,
  `webpack: Compiled successfully
  `,
  false)
);

gulp.task(CLEANUP, () => {
  createdProcesses
    .forEach(({ childProcess, isRunning }) => {
      console.warn(`Killing child process ${childProcess.pid}`);

      // if (isRunning) {
      childProcess.kill('SIGTERM');
      // }
    });
});
Run Code Online (Sandbox Code Playgroud)

编排任务

gulp.task(
  'e2e',
  gulp.series(
    gulp.series(
      RUN_APP,
      RUN_WEB,
    ),
    RUN_SPA,
    CLEANUP,
  ),
  () => console.info(`All tasks complete`),
);

gulp.task('default', gulp.series('e2e'));
Run Code Online (Sandbox Code Playgroud)

Sol*_*eil 1

  • dotnet run不会将终止信号传播给 Windows 中的子进程(这是 Windows 中的行为,与 POSIX 操作系统不同),您正在做正确的事情,即管理子进程
  • 但是SIGTERM不适用于 Windows:nodejs doc process_signal_events;那是你的问题。你可能想尝试一下SIGKILL
  • 不过,对于纯 js 解决方案,process.kill(process.pid, SIGKILL)可能需要测试:nodejs issues 12378
  • 对于可靠的经 MSFT 测试的解决方案(但不是跨平台),请考虑使用 powershell 来管理树,这要归功于以下 powershell 功能:

    function startproc($mydotnetcommand)
    {
        $parentprocid = Start-Process $mydotnetcommand -passthru
    }
    
    function stopproctree($parentprocid)
    {
        $childpidlist= Get-WmiObject win32_process |`
            where {$_.ParentProcessId -eq $parentprocid}
        Get-Process -Id $childpidlist -ErrorAction SilentlyContinue |`
            Stop-Process -Force
    }
    
    Run Code Online (Sandbox Code Playgroud)

    您可以通过将父 PID 作为参数传递给函数,从 ps 脚本外部使用第二个函数stopproctree

    param([Int32]$parentprocid)
    stopproctree $parentprocid
    
    Run Code Online (Sandbox Code Playgroud)

    (在脚本中,例如treecleaner.ps1),然后powershell.exe -file treecleaner.ps1 -parentprocid XXX