`boost :: asio :: io_context` с` boost :: process :: async_pipe`: есть ли способ запустить его надежно? - PullRequest
1 голос
/ 11 июля 2019

С помощью Windows API мы можем использовать каналы для пересылки выходных данных процесса и потоков ошибок, поэтому мы можем читать выходные данные процесса без каких-либо временных файлов.Вместо этого:

std::system("my_command.exe > out.tmp");

мы можем работать намного быстрее и без риска генерировать много забытых временных файлов (например, при сбое системы).

Linux имеет аналогичную функциональность.Однако реализация кода для каждой ОС является трудоемкой и сложной задачей, поэтому представляется целесообразным использовать какое-либо портативное решение.

boost::process претендует на роль такого решения.Однако это принципиально ненадежно.Смотрите следующий пример программы:

#include <fstream>
#include <iostream>
#include <memory>
#include <vector>

#include <boost/asio.hpp>
#include <boost/process.hpp>

void ReadPipe(boost::process::async_pipe& pipe, char* output_buffer, size_t output_buffer_size, boost::asio::io_context::strand& executor, std::ofstream& output_saver)
{
    namespace io = boost::asio;
    using namespace std;
    io::async_read(
        pipe,
        io::buffer(
            output_buffer,
            output_buffer_size
        ),
        io::bind_executor(executor, [&pipe, output_buffer, output_buffer_size, &executor, &output_saver](const boost::system::error_code& error, std::size_t bytes_transferred) mutable
            {
                // Save transferred data
                if (bytes_transferred)
                    output_saver.write(output_buffer, bytes_transferred);
                // Handle error
                if (error)
                {
                    if (error.value() == boost::asio::error::basic_errors::broken_pipe)
                        cout << "Child standard output is broken, so the process is most probably exited." << endl;
                    else
                        cout << "Child standard output read error occurred. " << boost::system::system_error(error).what() << endl;
                }
                else
                {
                    //this_thread::sleep_for(chrono::milliseconds(50));
                    ReadPipe(pipe, output_buffer, output_buffer_size, executor, output_saver);
                }
            })
    );
}

int main(void)
{
    namespace io = boost::asio;
    namespace bp = boost::process;
    using namespace std;
    // Initialize
    io::io_context asio_context;
    io::io_context::strand executor(asio_context);
    bp::async_pipe process_out(asio_context);
    char buffer[65535];
    constexpr const size_t buffer_size = sizeof(buffer);
    ofstream output_saver(R"__(c:\screen.png)__", ios_base::out | ios_base::binary | ios_base::trunc);
    // Schedule to read standard output
    ReadPipe(process_out, buffer, buffer_size, executor, output_saver);
    // Run child
    bp::child process(
        bp::search_path("adb"),
        bp::args({ "exec-out", "screencap", "-p" }),
        bp::std_in.close(),
        bp::std_out > process_out,
        bp::std_err > process_out
    );
    asio_context.run();
    process.wait();
    output_saver.close();
    // Finish
    return 0;
}

Этот код работает хорошо;он запускает ADB, генерирует скриншот устройства Android и сохраняет его с асинхронным каналом, поэтому временные файлы не используются.Этот конкретный пример сохраняет снимок экрана в виде файла, но в реальном приложении вы можете сохранить данные в памяти, загрузить и проанализировать их.

Я использую ADB в моем примере, потому что этот инструмент дает хороший пример данных, сгенерированных сравнительномедленно и отправляется через USB или Wi-Fi (так же медленно), и размер данных является сравнительно большим (для устройства Full HD со сложным изображением файл PNG будет 1M +).

Когда я раскомментирую следующую строку:

this_thread::sleep_for(chrono::milliseconds(50));

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

Таким образом, даже такая короткая задержка в 50 миллисекунд заставляет ускорить реализацию асинхронного канала.

Это ненормальная ситуация.Что делать, если загрузка процессора составляет около 100% (т.е. мы находимся на сильно загруженном сервере)?Что, если поток запускает другие задания ASIO, которые могут выполняться в течение 50 миллисекунд или меньше?Таким образом, это просто легко воспроизводимая реализация фундаментальной ошибки ASIO boost: асинхронный канал не может допустить каких-либо задержек, когда вы начали его читать;Вы должны снова вызвать async_read сразу после получения данных, в противном случае вы рискуете потерять свои данные.

На практике, когда я использую один и тот же контекст ASIO для запуска нескольких заданий (не только одного async_read)который читает стандартный вывод процесса), async_pipe терпит неудачу в 50% попыток прочитать 1M данных или более.

Кто-нибудь знает обходной путь, как сделать async_pipe надежным и не разрывать соединение, если контекст ASIOзапускает async_read с очень небольшими задержками, необходимыми для запуска других заданий?

...