Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Модуль process

Доступно только с флагом функции process.

Реализация асинхронного управления процессами для Tokio.

Этот модуль предоставляет структуру Command, которая имитирует интерфейс типа std::process::Command из стандартной библиотеки, но предоставляет асинхронные версии функций, создающих процессы. Эти функции (spawn, status, output и их варианты) возвращают типы, "осведомленные" о future, которые взаимодействуют с Tokio. Поддержка асинхронных процессов обеспечивается через обработку сигналов в Unix и системные API в Windows.

Примеры

Вот пример программы, которая запустит echo hello world и затем будет ждать его завершения.

use tokio::process::Command;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Использование аналогично типу `Command` стандартной библиотеки
    let mut child = Command::new("echo")
        .arg("hello")
        .arg("world")
        .spawn()
        .expect("не удалось запустить");

    // Ожидаем завершения команды
    let status = child.wait().await?;
    println!("команда завершилась с: {}", status);
    Ok(())
}

Теперь рассмотрим пример, где мы не только запускаем echo hello world, но и захватываем его вывод.

use tokio::process::Command;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Как и выше, но используем `output`, который возвращает future вместо
    // немедленного возврата `Child`.
    let output = Command::new("echo").arg("hello").arg("world")
                        .output();

    let output = output.await?;

    assert!(output.status.success());
    assert_eq!(output.stdout, b"hello world\n");
    Ok(())
}

Мы также можем читать ввод построчно.

use tokio::io::{BufReader, AsyncBufReadExt};
use tokio::process::Command;

use std::process::Stdio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::new("cat");

    // Указываем, что хотим получить стандартный вывод команды через канал.
    // По умолчанию стандартный ввод/вывод/ошибка наследуются от текущего процесса
    // (например, это означает, что стандартный ввод будет поступать с клавиатуры,
    // а стандартный вывод/ошибка будут направляться непосредственно в терминал,
    // если этот процесс вызван из командной строки).
    cmd.stdout(Stdio::piped());

    let mut child = cmd.spawn()
        .expect("не удалось запустить команду");

    let stdout = child.stdout.take()
        .expect("дочерний процесс не имел дескриптора stdout");

    let mut reader = BufReader::new(stdout).lines();

    // Убеждаемся, что дочерний процесс запущен в среде выполнения, чтобы он мог
    // самостоятельно прогрессировать, пока мы ожидаем какой-либо вывод.
    tokio::spawn(async move {
        let status = child.wait().await
            .expect("дочерний процесс столкнулся с ошибкой");

        println!("статус дочернего процесса: {}", status);
    });

    while let Some(line) = reader.next_line().await? {
        println!("Строка: {}", line);
    }

    Ok(())
}

Вот еще один пример использования sort с записью в стандартный ввод дочернего процесса и захватом вывода отсортированного текста.

use tokio::io::AsyncWriteExt;
use tokio::process::Command;

use std::process::Stdio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut cmd = Command::new("sort");

    // Указываем, что хотим канал как для вывода, так и для ввода.
    // Аналогично захвату вывода, настройка канала для stdin позволяет
    // использовать его как асинхронный писатель.
    cmd.stdout(Stdio::piped());
    cmd.stdin(Stdio::piped());

    let mut child = cmd.spawn().expect("не удалось запустить команду");

    // Животные, которые мы хотим отсортировать
    let animals: &[&str] = &["dog", "bird", "frog", "cat", "fish"];

    let mut stdin = child
        .stdin
        .take()
        .expect("дочерний процесс не имел дескриптора stdin");

    // Записываем наших животных в дочерний процесс
    // Обратите внимание, что поведение `sort` - буферизировать _весь ввод_ перед записью любого вывода.
    // В общем смысле рекомендуется писать в дочерний процесс в отдельной задаче,
    // одновременно ожидая его завершения (или вывода), чтобы избежать взаимоблокировок
    // (например, дочерний процесс пытается записать какой-то вывод, но зависает в ожидании,
    // пока родительский процесс прочитает его, в то время как родительский процесс
    // завис в ожидании полной записи своего ввода перед чтением вывода).
    stdin
        .write(animals.join("\n").as_bytes())
        .await
        .expect("не удалось записать в stdin");

    // Мы удаляем дескриптор здесь, что сигнализирует EOF дочернему процессу.
    // Это сообщает дочернему процессу, что в канале больше нет данных.
    drop(stdin);

    let op = child.wait_with_output().await?;

    // Результаты должны вернуться в отсортированном порядке
    assert_eq!(op.stdout, "bird\ncat\ndog\nfish\nfrog\n".as_bytes());

    Ok(())
}

С некоторой координацией мы также можем передавать вывод одной команды в другую.

use tokio::join;
use tokio::process::Command;
use std::process::Stdio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut echo = Command::new("echo")
        .arg("hello world!")
        .stdout(Stdio::piped())
        .spawn()
        .expect("не удалось запустить echo");

    let tr_stdin: Stdio = echo
        .stdout
        .take()
        .unwrap()
        .try_into()
        .expect("не удалось преобразовать в Stdio");

    let tr = Command::new("tr")
        .arg("a-z")
        .arg("A-Z")
        .stdin(tr_stdin)
        .stdout(Stdio::piped())
        .spawn()
        .expect("не удалось запустить tr");

    let (echo_result, tr_output) = join!(echo.wait(), tr.wait_with_output());

    assert!(echo_result.unwrap().success());

    let tr_output = tr_output.expect("не удалось дождаться tr");
    assert!(tr_output.status.success());

    assert_eq!(tr_output.stdout, b"HELLO WORLD!\n");

    Ok(())
}

Особенности

Удаление/Отмена

Аналогично поведению стандартной библиотеки и в отличие от парадигмы futures, где удаление подразумевает отмену, запущенный процесс по умолчанию продолжит выполняться даже после удаления дескриптора Child.

Метод Command::kill_on_drop может быть использован для изменения этого поведения и убийства дочернего процесса, если обертка Child удалена до его завершения.

Процессы Unix

В Unix-платформах процессы должны быть "убраны" (reaped) их родительским процессом после завершения, чтобы освободить все ресурсы ОС. Дочерний процесс, который завершился, но еще не был убран своим родителем, считается "зомби"-процессом. Такие процессы продолжают учитываться в ограничениях, накладываемых системой, и наличие слишком большого количества зомби-процессов может препятствовать запуску дополнительных процессов.

Среда выполнения Tokio будет пытаться убирать и очищать любые процессы, которые она запустила, на основе принципа "максимального усилия". Не дается дополнительных гарантий относительно того, насколько быстро или как часто эта процедура будет выполняться.

Рекомендуется избегать удаления дескриптора дочернего процесса до его полного ожидания, если требуются более строгие гарантии очистки.

Структуры

ИмяОписание
ChildПредставление дочернего процесса, запущенного в цикле событий.
ChildStderrПоток стандартной ошибки для запущенных дочерних процессов.
ChildStdinПоток стандартного ввода для запущенных дочерних процессов.
ChildStdoutПоток стандартного вывода для запущенных дочерних процессов.
CommandЭта структура имитирует API std::process::Command из стандартной библиотеки, но заменяет функции, создающие процесс, асинхронными вариантами. Основные предоставляемые асинхронные функции - spawn, status и output.