E2E-тестирование оркестровки с Gulp в Windows: невозможно уничтожить процесс (ы)

То, что я пытаюсь достичь

Этот вопрос связан с другим, недавно закрытым с ужасным хаком ™.

Я пытаюсь написать сценарий, который может быть использован в контексте конвейера CI/build.

Сценарий должен запускать сквозные тесты Protractor для нашего Углового одностраничного приложения (SPA).

Сценарий должен выполнять следующие действия (в порядке):

  1. запустить микросервис.NET Core под названием "Приложение",
  2. запустить микросервис.NET Core под названием "Интернет",
  3. запустить SPA
  4. выполните команду, выполняющую тесты Protractor
  5. после того, как шаги 4 завершены (успешно или с ошибкой), завершите процессы, созданные на этапах 1-3. Это абсолютно необходимо, иначе сборка никогда не завершится в CI и/или будут процессы z/b/z/

Проблема

Я не начал работать на шаге 4 ("тест e2e"), потому что я действительно хочу убедиться, что шаг 5 ("очистка") работает по назначению.

Как вы могли догадаться (справа), шаг очистки не работает. В частности, процессы "App" и "Web" по какой-то причине не убиваются и продолжают работать.

Кстати, я убедился, что мой скрипт gulp выполняется с повышенными привилегиями (admin).

Проблема - ОБНОВЛЕНИЕ 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"       
}                          

Аналогично, для 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"       
}                          

Как я это делаю

В принципе, рабочая лошадь здесь - это runCmdAndListen() которая отталкивает процессы, запуская cmd предоставленный в качестве аргумента. Когда функция запускает процесс, является средством Node.js exec(), он затем createdProcesses массив 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 = [];

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

Основные задачи 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');
      // }
    });
});

Задача оркестровки

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

Ответ 1

  • dotnet run не распространяет сигнал kill на детей в Windows (что поведение в Windows, которое отличается в ОС POSIX), вы делаете правильные вещи, а именно управлять дочерними процессами
  • однако SIGTERM не будет работать на windows: nodejs doc process_signal_events; что ваша проблема. Возможно, вы захотите попробовать SIGKILL.
  • для чистого js-решения, просто process.kill(process.pid, SIGKILL) хотя это, возможно, нужно протестировать: nodejs issue 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
    }
    

    Вы можете использовать stopproctree функцию из-за скрипта ps через передачу родительского PID в качестве аргумента функции stopproctree с помощью:

    param([Int32]$parentprocid)
    stopproctree $parentprocid
    

    (в скрипте, скажем, treecleaner.ps1), а затем powershell.exe -file treecleaner.ps1 -parentprocid XXX