Почему вызов процесса Тессеракта вызывает случайное создание этой службы sh? - PullRequest
4 голосов
/ 28 февраля 2020

У меня есть служба. NET Core 2.1, которая работает на виртуальной машине Ubuntu 18.04 и вызывает Tesseract OCR 4.00 через экземпляр Process. Я хотел бы использовать API-оболочку, но я мог найти только одну доступную, и она только в бета-версии для последней версии Tesseract - стабильная оболочка использует версию 3 вместо 4. В прошлом этот сервис работал достаточно хорошо, но я изменил его так, чтобы данные документа / изображения записывались и читались с диска реже в попытке повысить скорость. Служба вызывала гораздо больше внешних процессов (таких как ImageMagick), которые были излишними из-за наличия API, поэтому я заменял их на вызовы API.

Недавно я тестировал это с помощью Пример файла взят из реальных данных. Это факсимильный документ PDF, который имеет 133 страницы, но только 5,8 МБ, несмотря на это из-за оттенков серого и разрешения. Служба берет документ, разбивает его на отдельные страницы, затем назначает несколько потоков (по одному потоку на страницу) для вызова Tesseract и обрабатывает их, используя Parallel.For. Ограничения потока настраиваются. Мне известно, что у Tesseract есть собственная переменная среды многопоточности (OMP_THREAD_LIMIT). В предыдущем тестировании я обнаружил, что установка его на «1» идеально подходит для нашей установки в данный момент, но в моем недавнем тестировании по этой проблеме я попытался оставить его неустановленным (значение Dynami c) без каких-либо улучшений.

Проблема заключается в том, что непредсказуемо, когда вызывается Tesseract, служба будет зависать около минуты, а затем cra sh, при этом единственная ошибка, отображаемая в journalctl:

dotnet[32328]: Error while reaping child. errno = 10
dotnet[32328]:    at System.Environment.FailFast(System.String, System.Exception)
dotnet[32328]:    at System.Environment.FailFast(System.String)
dotnet[32328]:    at System.Diagnostics.ProcessWaitState.TryReapChild()
dotnet[32328]:    at System.Diagnostics.ProcessWaitState.CheckChildren(Boolean)
dotnet[32328]:    at System.Diagnostics.Process.OnSigChild(Boolean)

Я не могу ничего найти в сети для этой конкретной ошибки. Мне кажется, основываясь на связанном исследовании, которое я провел с классом Process, это происходит, когда процесс завершается, а do tnet пытается очистить ресурсы, которые он использовал. Я действительно в растерянности относительно того, как даже подойти к этой проблеме, хотя я пробовал ряд «догадок», таких как изменение предельных значений потока. Там нет пересечения между потоками. Каждый поток имеет свой собственный раздел страниц (в зависимости от того, как Parallel.For разделяет коллекцию), и он начинает работать с этими страницами, по одному за раз.

Вот вызов процесса, вызываемый из нескольких потоков (8 - это предел, который мы обычно устанавливаем):

private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
        {
            var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
            string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
                    $"pg_{pageNumber.ToString().PadLeft(3, '0')}");
            page.Write(inputPageImagePath);

            var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
            bool success;

            _logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);

            var psi = new ProcessStartInfo("tesseract", cmdArgs)
            {
                RedirectStandardError = true,
                RedirectStandardOutput = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            // 0 is not the default value for this environment variable. It should remain unset if there 
            // is no config value, as it is determined dynamically by default within OpenMP.
            if (_processorConfig.TesseractThreadLimit > 0) 
                psi.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());

            using (var p = new Process() { StartInfo = psi })
            {
                string standardErr, standardOut;
                int exitCode;
                p.Start();
                standardOut = p.StandardOutput.ReadToEnd();
                standardErr = p.StandardError.ReadToEnd();        
                p.WaitForExit();
                exitCode = p.ExitCode;

                if (!string.IsNullOrEmpty(standardOut))
                    _logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
                if (!string.IsNullOrEmpty(standardErr))
                    _logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));
                success = p.ExitCode == 0;
            }

            return success;
        }

РЕДАКТИРОВАТЬ 4: После большого тестирования и обсуждения с Клинтом в чате, вот что мы узнали. Ошибка возникает из события Process «OnSigChild», что очевидно из трассировки стека, но нет способа подключиться к тому же событию, которое вызывает эту ошибку. Процесс никогда не прерывается при заданном тайм-ауте в 10 секунд (Tesseract обычно занимает всего несколько секунд для обработки данной страницы). Любопытно, что если тайм-аут процесса удален, и я жду закрытия стандартных потоков вывода и ошибок, он будет зависать в течение хороших 20-30 секунд, но процесс не появляется в ps auxf в течение этого времени зависания. Насколько я могу судить, Linux может определить, что процесс завершен, а NET - нет. В противном случае ошибка, по-видимому, возникает в тот самый момент, когда процесс завершается выполнением.

Самым удивительным для меня остается то, что часть кода, обрабатывающая процессы, действительно не сильно изменилась по сравнению с рабочей версией этого кода, которую мы имеем в производстве. Это говорит о том, что я где-то допустил ошибку, но просто не могу ее найти. Думаю, мне придется открыть проблему на трекере do tnet GitHub.

1 Ответ

2 голосов
/ 03 марта 2020

"Ошибка при пожинании потомка"

Процессы задерживают некоторые ресурсы в ядре, Вкл. Unix, когда умирает родительский процесс, это процесс инициализации, который отвечает за очистку ресурсов ядра как процесса Zombine, так и процесса Orphan (он же пожинает ребенка). . NET Ядро пожинает дочерние процессы, как только они завершаются.

"Я обнаружил, что удаление вызовов ReadToEnd потоков stdout и stderr приводит к завершению процессов сразу вместо зависания, с той же ошибкой "

Ошибка связана с тем, что вы преждевременно вызываете p.ExitCode даже до того, как процесс завершится, и с ReadToEnd вы * просто задерживает эту активность

Сводка обновленного кода

  • StartInfo.FileName должна указывать на имя файла, который вы хотите запустить
  • UseShellExecute в false, если процесс должен быть создан непосредственно из исполняемого файла, и true, если вы предполагаете, что оболочка должна использоваться при запуске процесса;
  • Добавлены асинхронные операции чтения в стандартные потоки вывода и ошибок
  • AutoResetEvents для оповещения о выходе и ошибки при завершении операций
  • Process.Close() для освобождения ресурсов
  • Проще установить и использовать * 104 0 * ArgumentList over Arguments свойство

Блог Redhat на NetProcess в Linux

Исправленный модуль

private bool ProcessOcrPage(IMagickImage page, int pageNumber, object instanceId)
{
    StringBuilder output = new StringBuilder();
    StringBuilder error = new StringBuilder();
    int exitCode;
    var inputPageImagePath = Path.Combine(_fileOps.GetThreadWorkingDirectory(instanceId), $"ocrIn_{pageNumber}.{page.Format.ToString().ToLower()}");
        string outputPageFilePathWithoutExt = Path.Combine(_fileOps.GetThreadOutputDirectory(instanceId),
                $"pg_{pageNumber.ToString().PadLeft(3, '0')}");
    page.Write(inputPageImagePath);

    var cmdArgs = $"-l eng \"{inputPageImagePath}\" \"{outputPageFilePathWithoutExt}\" pdf";
    bool success;

    _logger.LogStatement($"[Thread {instanceId}] Executing the following command:{Environment.NewLine}tesseract {cmdArgs}", LogLevel.Debug);


    using (var outputWaitHandle = new AutoResetEvent(false))
    using (var errorWaitHandle = new AutoResetEvent(false))
    {
        try
        {
            using (var process = new Process())
            {
                process.StartInfo = new ProcessStartInfo
                { 
                    WindowStyle = ProcessWindowStyle.Hidden,
                    FileName = "tesseract.exe", // Verify if this is indeed the process that you want to start ?
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    Arguments = cmdArgs,
                    WorkingDirectory = Path.GetDirectoryName(path)
                };



                if (_processorConfig.TesseractThreadLimit > 0) 
                    process.StartInfo.EnvironmentVariables.Add("OMP_THREAD_LIMIT", _processorConfig.TesseractThreadLimit.ToString());


                process.OutputDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        outputWaitHandle.Set();
                    }
                    else
                    {
                        output.AppendLine(e.Data);
                    }
                };
                process.ErrorDataReceived += (sender, e) =>
                {
                    if (e.Data == null)
                    {
                        errorWaitHandle.Set();
                    }
                    else
                    {
                        error.AppendLine(e.Data);
                    }
                };

                process.Start();

                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                  if (!outputWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !errorWaitHandle.WaitOne(ProcessTimeOutMiliseconds) && !process.WaitForExit(ProcessTimeOutMiliseconds))
                  {
                    //To cancel the read operation if the process is stil reading after the timeout this will prevent ObjectDisposeException
                    process.CancelOutputRead();
                    process.CancelErrorRead();

                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.WriteLine("Timed Out");

                    //To release allocated resource for the Process
                    process.Close();
                    //Timed out
                    return  false;
                  }

                  Console.ForegroundColor = ConsoleColor.Green;
                  Console.WriteLine("Completed On Time");

                 exitCode = process.ExitCode;

                  if (!string.IsNullOrEmpty(standardOut))
                    _logger.LogStatement($"Tesseract stdOut:\n{standardOut}", LogLevel.Debug, nameof(ProcessOcrPage));
                  if (!string.IsNullOrEmpty(standardErr))
                    _logger.LogStatement($"Tesseract stdErr:\n{standardErr}", LogLevel.Debug, nameof(ProcessOcrPage));

                 process.Close();

                 return exitCode == 0 ? true : false;
            }
        }
        Catch
        {
           //Handle Exception
        }
    }
}
...