Основы Rust: обсуждаем процессы

Игорь Грегорченко

В нашей последовательной серии материалов мы рассмотрим базовые основы новомодного языка Rust. А во второй части цикла на основе изученного попробуем написать самые простые смарт-контракты для таких блокчейн-проектов, как Solana. В этом туториале будет много примеров, мало теории и быстрый темп продвижения.

Этот пост — вольный перевод на русский вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Стив Донован. Начало можно найти вот здесь, а оглавление всей серии — вот тут.

Эта часть посвящена модулю std::process, который занимается запуском и обработкой процессов (преимущественно дочерних).

Процессы

Фундаментальная необходимость заключается в том, чтобы ваши программы на Rust корректно запускали другие программы (или, точнее, запускали процессы). Ваша программа может породить столько дочерних процессов, сколько захочет, и, как следует из названия, они имеют особые отношения со своим родителем.

Запустить программу очень просто с помощью структуры Command, которая формирует аргументы для передачи программе:

use std::process::Command;

fn main() {
let status = Command::new("rustc")
.arg("-V")
.status()
.expect("no rustc?");

println!("cool {} code {}", status.success(), status.code().unwrap());
}
// rustc 1.15.0-nightly (8f02c429a 2016-12-15)
// cool true code 0

Итак, что здесь происходит: new получает имя программы (оно будет искаться в PATH, если это не абсолютное имя файла), arg добавляет новый аргумент, а status вызывает ее запуск. Все это вместе возвращает Result, который является Ok, а в случае если программа действительно была запущена, также содержит ExitStatus.

В данном случае программа завершилась успешно и вернула код выхода 0.

Если мы заменим -V на -v (легкая ошибка), то rustc завершится неудачей:

error: no input filename
given cool false code 101

Итак, всего есть три варианта:

  • программа не существовала, была неудачной, или нам не разрешили ее запустить;
  • программа запустилась, но не была успешной — ненулевой код выхода;
  • программа запустилась с нулевым кодом выхода. Успех!

По умолчанию потоки стандартного вывода и стандартной ошибки программы идут в терминал.

Часто мы очень заинтересованы в захвате этого вывода, поэтому существует соответствующий метод вывода.

/ process2.rs
use std::process::Command;

fn main() {
let output = Command::new("rustc")
.arg("-V")
.output()
.expect("no rustc?");

if output.status.success() {
println!("ok!");
}
println!("len stdout {} stderr {}", output.stdout.len(), output.stderr.len());
}
// ok!
// len stdout 44 stderr 0

Как и в случае со статусом, наша программа блокируется до завершения дочернего процесса, и мы получаем обратно три вещи — статус (как и раньше), содержимое stdout и содержимое stderr.

Захваченный вывод — это просто Vec<u8> — просто байты. Напомним, что у нас нет гарантии, что данные, которые мы получаем от операционной системы, являются правильно закодированной строкой UTF-8. Фактически у нас нет гарантии, что это вообще строка —  программы могут возвращать произвольные двоичные данные.

Если мы уверены, что выходные данные являются UTF-8, то String::from_utf8 преобразует эти векторы или байты — что возвращает Result, потому что это преобразование может оказаться неудачным. Более небрежной функцией является String::from_utf8_lossy, которая сделает хорошую попытку преобразования и вставит недопустимый знак Unicode � там, где это не удалось.

Вот еще одна полезная функция, которая запускает программу с помощью оболочки. Она использует обычный механизм оболочки для соединения stderr с  stdout. В Windows имя оболочки отличается, но в остальном все работает, как и ожидалось.

fn shell(cmd: &str) -> (String,bool) {
let cmd = format!("{} 2>&1",cmd);
let shell = if cfg!(windows) {"cmd.exe"} else {"/bin/sh"};
let flag = if cfg!(windows) {"/c"} else {"-c"};
let output = Command::new(shell)
.arg(flag)
.arg(&cmd)
.output()
.expect("no shell?");
(
String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
output.status.success()
)

}fn shell_success(cmd: &str) -> Option<String> {
let (output,success) = shell(cmd);
if success {Some(output)} else {None}
}

Мы обрезаем все пробелы справа, чтобы, если вы скажете shell("which rustc"), то получили бы путь без лишних строк.

Вы можете контролировать выполнение программы, запущенной Process, указывая каталог, в котором она будет выполняться, используя метод current_dir и переменные окружения, которые она видит, используя env.

До сих пор наша программа просто ждала завершения дочернего процесса. Если вы используете метод spawn, то мы возвращаемся немедленно и должны явно дождаться завершения — или уйти и заняться чем-то другим в это время. Этот пример ниже также показывает, как подавить стандартный выход и стандартную ошибку:

// process5.rs
use std::process::{Command,Stdio};

fn main() {
let mut child = Command::new("rustc")
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("no rustc?");

let res = child.wait();
println!("res {:?}", res);
}

По умолчанию дочерняя программа наследует стандартный ввод и вывод родительской программы. В данном случае мы перенаправляем направление дочернего вывода в «никуда». Это эквивалентно тому, чтобы сказать > /dev/null 2> /dev/null в оболочке Unix.

В Rust можно делать подобные вещи, используя оболочку (sh или cmd). И таким образом вы получаете полный программный контроль над созданием процессов.

Например, если у нас просто .stdout(Stdio::piped()), то стандартный вывод дочернего процесса перенаправляется в пайп. Тогда child.stdout  —  это то, что вы можете использовать для прямого чтения вывода (то есть это реализует Read). Аналогично вы можете использовать метод .stdout(Stdio::piped()), чтобы писать в child.stdin.

Но если мы использовали wait_with_output вместо wait, то он возвращает Result<Output>, а вывод дочернего объекта, как и раньше, записывается в поле stdout этого Output как Vec<u8>.

Структура Child также предоставляет вам явный метод kill.

Подводя итоги

Для порождения процесса можно использовать несколько методов Command, таких как spawn или output. В частности, output порождает дочерний процесс и ждет, пока он завершится, а spawn возвращает Child, представляющий сам порожденный дочерний процесс.

В завершение приведем развернутый пример обработки ввода-вывода (Process I/O).

Stdout, stdin и stderr дочернего процесса могут быть настроены путем передачи Stdio соответствующему методу в Command. После порождения к ним можно получить доступ из дочернего процесса. Например, передача вывода из одной команды в другую может быть выполнена следующим образом:

use std::process::{Command, Stdio};

// stdout must be configured with `Stdio::piped` in order to use
// `echo_child.stdout`
let echo_child = Command::new("echo")
.arg("Oh no, a tpyo!")
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start echo process");

// Note that `echo_child` is moved here, but we won't be needing
// `echo_child` anymore
let echo_out = echo_child.stdout.expect("Failed to open echo stdout");

let mut sed_child = Command::new("sed")
.arg("s/tpyo/typo/")
.stdin(Stdio::from(echo_out))
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start sed process");

let output = sed_child.wait_with_output().expect("Failed to wait on sed");
assert_eq!(b"Oh no, a typo!\n", output.stdout.as_slice());

Обратите внимание, что ChildStderr и ChildStdout реализуют чтение, а ChildStdin реализует запись:

use std::process::{Command, Stdio};
use std::io::Write;

let mut child = Command::new("/bin/cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("failed to execute child");

// If the child process fills its stdout buffer, it may end up
// waiting until the parent reads the stdout, and not be able to
// read stdin in the meantime, causing a deadlock.
// Writing from another thread ensures that stdout is being read
// at the same time, avoiding the problem.
let mut stdin = child.stdin.take().expect("failed to get stdin");
std::thread::spawn(move || {
stdin.write_all(b"test").expect("failed to write to stdin");
});

let output = child
.wait_with_output()
.expect("failed to wait on child");

assert_eq!(b"test", output.stdout.as_slice());

Итак, как видно из примера выше, модуль std::process в основном занимается порождением и взаимодействием с дочерними процессами, но он также предоставляет abort и exit для завершения текущего процесса.

Продолжение следует…

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023