Основы 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 для завершения текущего процесса.

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

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

Что такое прокси-сервер: пояснение простыми словами, зачем нужны прокси

Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…

21.11.2024

Что такое PWA приложение? Зачем необходимо прогрессивное веб-приложение

Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…

19.11.2024

Как создать игру на телефоне: программирование с помощью конструктора

Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…

17.11.2024

Google Bard: эффективный аналог ChatGPT

В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…

14.11.2024

Скрипт и программирование: что это такое простыми словами

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

12.11.2024

Дедлайн в разработке: что это такое простыми словами

Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…

11.11.2024