Зависание при программном запуске командного файла (с перенаправлением вывода), когда командный файл просто вызывает «start some.exe» - PullRequest
0 голосов
/ 09 ноября 2018

РЕДАКТИРОВАТЬ: Найдено несколько дубликатов без ответов:

  1. Проблема с перенаправлением вывода в пакете
  2. Как использовать команду "start" без наследования дескрипторов в дочернем процессе?

У меня есть некоторый код на C #, который пытается быть универсальным средством запуска процессов в более крупной долгосрочной программе. Этот код должен захватывать выходные данные процессов, которые он запускает, а также ждать их завершения. Обычно мы запускаем пакетные файлы с этим кодом, и все работает нормально, за исключением случаев, когда мы хотим запустить другой дочерний процесс из внутри пакетного файла, чтобы он пережил процесс пакетного файла. В качестве примера, скажем, я просто хочу выполнить « start notepad.exe » внутри командного файла.

Я столкнулся с теми же проблемами, что и в этом вопросе: Process.WaitForExit не возвращается, даже если Process.HasExited имеет значение true

По сути, даже если процесс пакетного файла, кажется, завершается очень быстро (как и ожидалось), моя программа зависает до тех пор, пока дочерний процесс (например, блокнот) также не завершится. Однако в моем сценарии блокнот не предназначен для выхода.

Я попытался ввести «cmd.exe / C» во все точки цепочки, но безуспешно. Я попытался явно завершить пакетный файл с помощью «exit» или «exit / B». Я пытался читать вывод синхронно и асинхронно - с рабочими потоками или без них. Я попробовал шаблоны здесь: https://github.com/alabax/CsharpRedirectStandardOutput/tree/master/RedirectStandardOutputLibrary (см. FixedSimplePattern.cs и AdvancedPattern.cs), снова безуспешно.

РЕДАКТИРОВАТЬ: я также пытался с некоторым кодом C # P / Invoke, который запускает процесс через Windows API (CreatePipe / CreateProcess и т. Д.), Поэтому я не думаю, что эта проблема специфична для C # Process API.

Единственный обходной путь, который я нашел, состоял в том, чтобы заменить команду запуска инструментом, вызывающим CreateProcess, с флагом DETACHED_PROCESS (CREATE_NO_WINDOW также работает).

Принятый ответ в вышеупомянутом вопросе SO (https://stackoverflow.com/a/26722542/5932003) - самая близкая вещь во всем Интернете, которая, кажется, работает, но оказывается, что это утечка потоков с каждым запускаемым вами пакетным файлом. Я бы оставил комментарий там, но у меня пока нет репутации, чтобы делать это:).

Модифицированный код, который демонстрирует утечку потока:

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace TestSO26713374WaitForExit
{
    class Program
    {
    static void Main(string[] args)
    {
        while(true)
        {
            string foobat =
@"@echo off
START ping -t localhost
REM START ping -t google.com
REM ECHO Batch file is done!
EXIT /B 123
";

            File.WriteAllText("foo.bat", foobat);

            Process p = new Process
            {
                StartInfo =
                new ProcessStartInfo("foo.bat")
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true
                }
            };

            p.Start();

            var _ = ConsumeReader(p.StandardOutput);
            _ = ConsumeReader(p.StandardError);

            //Console.WriteLine("Calling WaitForExit()...");
            p.WaitForExit();
            //Console.WriteLine("Process has exited. Exit code: {0}", p.ExitCode);
            //Console.WriteLine("WaitForExit returned.");


            ThreadPool.GetMaxThreads(out int max, out int max2);
            ThreadPool.GetAvailableThreads(out int available, out int available2);

            Console.WriteLine(
                $"Active thread count: {max - available} (thread pool), {System.Diagnostics.Process.GetCurrentProcess().Threads.Count} (all).");

            Thread.Sleep(8000);
        }
    }

    async static Task ConsumeReader(TextReader reader)
    {
        string text;

        while ((text = await reader.ReadLineAsync()) != null)
        {
            Console.WriteLine(text);
        }
    }
}
}

Выход из вышеперечисленного:

Active thread count: 2 (thread pool), 15 (all).
Active thread count: 4 (thread pool), 18 (all).
Active thread count: 6 (thread pool), 19 (all).
Active thread count: 8 (thread pool), 20 (all).
Active thread count: 9 (thread pool), 21 (all).
Active thread count: 11 (thread pool), 23 (all).
Active thread count: 13 (thread pool), 25 (all).
Active thread count: 15 (thread pool), 27 (all).
Active thread count: 17 (thread pool), 29 (all).
Active thread count: 19 (thread pool), 31 (all).
Active thread count: 21 (thread pool), 33 (all).
...

Мои вопросы:

  1. Почему команда запуска не полностью разорвет цепочку перенаправления вывода?
  2. Застрял ли я с вышеупомянутым инструментом, который вызывает CreateProcess (... DETACHED_PROCESS ...)?

Спасибо!

Ответы [ 2 ]

0 голосов
/ 15 ноября 2018

Вот замена для команды запуска:

start / b powershell.exe Start-Process -FilePath "notepad.exe"

«start / b» используется только для запуска powershell без отображения его окна (которое иначе мигало бы на секунду - раздражает). Powershell вступает во владение после этого и запускает наш процесс без каких-либо побочных эффектов.

Если PowerShell не вариант, оказывается, что эта супер простая программа C # может служить той же цели:

using System.Diagnostics;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            // You may want to expand this to set CreateNoWindow = true
            Process.Start(args[0]);
        }
    }
}

Нет необходимости в CreateProcess (... DETACHED_PROCESS ...).

0 голосов
/ 15 ноября 2018

У меня нет ответа на первый вопрос.Это более фундаментальная деталь проектирования или реализации Windows, и я не могу сказать, почему они решили сделать это таким образом.

Что касается второго вопроса ...

К сожалению, это кажетсябыть ограничением класса Process из-за способа его реализации.

Использование асинхронного чтения из потоков stdout и stderr решает проблему WaitForExit(), но ничего не решаетосновной способ, которым Windows связывает родительский процесс с дочерним процессом.Выходные потоки родителя (которые перенаправлены) не будут закрыты до тех пор, пока дочерний процесс не завершится, и поэтому в программе на C # все еще ожидают чтения родительских потоков вывода, ожидающих закрытия этих потоков.

В классе Process для перенаправленного вывода он оборачивает дескриптор потока ввода-вывода в объект FileStream, а при создании этого объекта не создает его с флагом async, установленным в true, который требуется, чтобы объект использовал IOCP для асинхронных операций.Он даже не создает базовый нативный объект канала с поддержкой IOCP.

Таким образом, когда код выполняет асинхронное чтение в потоке, это реализуется в .NET посредством «фальсифицированной» асинхронности.То есть он просто ставит в очередь синхронное чтение в обычном пуле потоков.

Таким образом, вы получаете новый поток активного пула потоков для каждого ожидающего чтения, по два на процесс.И последние чтения из выходных потоков не будут возвращаться, пока дочерний процесс не завершится, связав эти потоки пула потоков.

Я не вижу никакого отличного способа избежать этого.

Дляпроцесс, который вы знаете будет иметь очень маленькое количество вывода в потоках stdout и stderr (или вообще ни одного ... в данном примере, вообще ничего не записывается в stderr, например), вымог просто не читать потоки.Но буфер для каждого из этих потоков не очень велик, поэтому невозможность их чтения обычно приводит к тому, что процесс в конечном итоге блокируется.

В некоторых особых случаях вы можете расположить вещи так, чтобы не читать изпотоки, когда вы знаете, что вы в конце.Например, в приведенном выше коде вы можете раскомментировать «Пакетный файл готов!»и завершите цикл ConsumeReader(), когда увидите этот текст.Но это не решение, которое будет работать во многих случаях.Он будет полагаться на то, чтобы точно знать, что будет записано, по крайней мере для самой последней строки, потоков stdout и stderr.

Для чего это стоит, по крайней мере, я бы сказал, что вы не буквально утечки темы.Каждый поток на самом деле что-то делает, и каждый поток на самом деле, наконец, будет освобожден, как только связанный процесс завершится.И, честно говоря, в Windows поток стоит намного меньше, чем процесс, поэтому, если у вас достаточно дочерних процессов, запущенных одновременно, и вы беспокоитесь о количестве потоков в одном процессе, вам, вероятно, придется жарить больше рыбы (т.е. все эти дочерние процессы поглощают ресурсы).

После исследования того, что я мог, я думаю, что ваше текущее решение, инструмент, используемый вместо команды start для создания вашего процесса как отдельного потомка,наверное, самый элегантный.Единственной надежной альтернативой было бы эффективное повторное внедрение самого класса Process (или, по крайней мере, частей, которые вы здесь используете), за исключением поддержки IOCP, так что чтение в стандартных потоках фактически осуществляется асинхронно.

Полагаю, вы могли бы попытаться сообщить об этой проблеме в Microsoft, но, думаю, на данный момент они не заинтересованы в каких-либо изменениях в классе Process.

...