Runtime.exec: примеры (JAVA)

Разбор Runtime.exec в Java
Раздел: Работа с процессами и памятью (Runtime, Process)
Runtime.exec(String command): Process

Общее описание Runtime.exec

Метод Runtime.exec принадлежит классу java.lang.Runtime и служит для запуска внешних процессов из Java-приложения. Возвращаемое значение - объект java.lang.Process, предоставляющий доступ к стандартным потокам процесса, к его коду завершения и к идентификатору процесса (в современных версиях Java).

Существуют перегрузки метода:

  • exec(String command) - передача единой строковой команды. Строка обрабатывается операционной системой, поэтому поведение зависит от оболочки и правил разбиения аргументов.
  • exec(String[] cmdarray) - массив токенов команды: первый элемент - исполняемый файл, последующие - аргументы. Рекомендуется для корректной передачи аргументов с пробелами и специальных символов.
  • exec(String command, String[] envp) - команда плюс массив переменных окружения в формате "NAME=VALUE". Если передать null, используется окружение текущего процесса.
  • exec(String[] cmdarray, String[] envp) - массив токенов и окружение.
  • exec(String command, String[] envp, java.io.File dir) - команда, окружение и рабочая директория процесса.
  • exec(String[] cmdarray, String[] envp, java.io.File dir) - массив, окружение и рабочая директория.

Возвращаемые значения и исключения:

  • Возвращается объект Process; для получения кода завершения используется process.waitFor() и process.exitValue().
  • Возможные исключения: IOException при ошибке создания процесса или если команда не найдена; SecurityException при ограничениях SecurityManager; NullPointerException при недопустимых null-параметрах в некоторых перегрузках.

Особенности поведения:

  • При использовании версии с одной строкой (String command) платформа может запускать оболочку для разбора команды, что приводит к необходимости экранирования и уязвимостей, если строка формируется из пользовательских данных.
  • Потоки вывода и ошибок процесса должны читаться, иначе возможна блокировка процесса по заполнению буфера.
  • Рекомендуется рассматривать ProcessBuilder как более гибкую и предпочтительную альтернативу, позволяющую задавать перенаправления потоков, рабочую директорию и окружение.

Короткие примеры использования

1) Простая команда, вывод в консоль (Unix).

Runtime.getRuntime().exec("ls -la");

Результат (если не читать поток):

(процесс запущен, вывод остался в потоке и не отображается в приложении)

2) Чтение вывода команды (универсальный пример).

Process p = Runtime.getRuntime().exec(new String[]{"ls","-la"});
try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()))) {
    String line;
    while ((line = r.readLine()) != null) System.out.println(line);
}
int code = p.waitFor();
System.out.println("Exit: " + code);

Возможный результат:

drwxr-xr-x  5 user group  160 Apr 1 12:00 .
-rw-r--r--  1 user group 1024 Apr 1 11:59 file.txt
Exit: 0

3) Передача окружения и рабочей директории.

String[] env = {"MYVAR=123"};
Process p = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c","echo $MYVAR"}, env, new java.io.File("/tmp"));
// чтение вывода как в предыдущем примере

Возможный результат:

123
Exit: 0

4) Запуск на Windows через cmd для использования операторов оболочки.

Runtime.getRuntime().exec(new String[]{"cmd.exe","/c","dir C:\\"});

Результат аналогичен выводу команды dir в командной строке.

Похожие API на Java

ProcessBuilder - более гибкая замена Runtime.exec. Позволяет явно задавать список аргументов, окружение, рабочую директорию и перенаправления потоков. Предпочтительнее при сложных сценариях и при необходимости управления вводом/выводом.

java.lang.Process - не альтернатива, а возвращаемый объект, который предоставляет методы getInputStream(), getErrorStream(), getOutputStream(), waitFor(), exitValue(), destroy(), pid() (в новых версиях).

Когда использовать что:

  • Для простого однократного запуска достаточно Runtime.exec с корректным массивом аргументов.
  • Для управления потоками, перенаправления в файлы или сложной настройки окружения предпочтительнее ProcessBuilder.

Альтернативы в других языках

PHP

// shell_exec
$output = shell_exec('ls -la');
echo $output;
(список файлов)

Отличие: простота вызова, но сложное управление потоками требует дополнительных функций (proc_open).

JavaScript (Node.js)

const { exec, spawn } = require('child_process');
exec('ls -la', (err, stdout, stderr) => console.log(stdout));
(список файлов)

spawn предоставляет потоковую работу без запуска оболочки по умолчанию, полезно для передачи аргументов.

Python

import subprocess
res = subprocess.run(['ls','-la'], capture_output=True, text=True)
print(res.stdout)
(список файлов)

subprocess.run удобен и безопасен; subprocess.Popen нужен для асинхронной работы.

SQL

-- В стандартном SQL нет выполнения ОС, но в MS SQL есть xp_cmdshell
EXEC xp_cmdshell 'dir C:\\';
(вывод dir)

Небезопасно; требует прав администратора и обычно отключено.

C#

using System.Diagnostics;
var p = new ProcessStartInfo("cmd.exe","/c dir") { RedirectStandardOutput = true };
var proc = Process.Start(p);
Console.WriteLine(proc.StandardOutput.ReadToEnd());
(список файлов)

Lua

os.execute('ls -la')
(стандартный вывод консоли)

Go

package main
import(
  "os/exec"
  "fmt"
)
func main(){
  out, _ := exec.Command("ls","-la").CombinedOutput()
  fmt.Println(string(out))
}
(список файлов)

Kotlin

val p = Runtime.getRuntime().exec(arrayOf("ls","-la"))
val out = p.inputStream.bufferedReader().readText()
println(out)
(список файлов)

Ключевые отличия от Java: синтаксические особенности, в некоторых языках есть более высокоуровневые и удобные API для запуска процессов (Python subprocess, Node.js child_process).

Типичные ошибки и их проявления

1) Неправильная токенизация аргументов при использовании String command.

// Плохо: аргументы с пробелами
Runtime.getRuntime().exec("mycmd -o C:\\Program Files\\out.txt");
IOException: Cannot run program "mycmd -o C:\\Program": CreateProcess error=2, ...

Решение: использовать массив аргументов.

2) Блокировка по заполнению буфера вывода/ошибок.

Process p = Runtime.getRuntime().exec("someLongOutputCmd");
// не читать getInputStream() и getErrorStream()
int code = p.waitFor();
Программа зависает: процесс ждет освобождения буфера, waitFor не возвращается.

Рекомендация: организовать чтение потоков в отдельных потоках или использовать ProcessBuilder.redirectOutput.

3) Отсутствие команды или неверный путь.

Runtime.getRuntime().exec("nonexistentcmd");
IOException: Cannot run program "nonexistentcmd": error=2, No such file or directory

4) Проблемы с кодировкой вывода на Windows.

Process p = Runtime.getRuntime().exec("chcp 866 & dir", null, new File("C:\\"));
// неверная кодировка при чтении InputStreamReader без указания charset
Вместо русских символов появляются кракозябры

Решение: указывать правильную кодировку при чтении потоков.

5) Игнорирование SecurityManager. В средах с включенным SecurityManager Runtime.exec может выбросить SecurityException.

try { Runtime.getRuntime().exec("whoami"); } catch (SecurityException e){ e.printStackTrace(); }
java.security.AccessControlException: access denied ...

Изменения и рекомендации по версии Java

Сам API Runtime.exec долгое время оставался стабильным и не претерпел существенных изменений. Основные практические изменения и рекомендации:

  • ProcessBuilder, появившийся в JDK 5, считается более современным и удобным способом запуска процессов и управления перенаправлением потоков.
  • В новых версиях Java объект Process предоставляет дополнительные методы, например pid() для получения идентификатора процесса и методы для управления потоками в сочетании с более удобными API.
  • Механизмы безопасности изменялись в разных релизах JVM: SecurityManager долгое время использовался для ограничения запуска процессов; в более новых релизах его статус пересматривался, поэтому поведение в защищенных средах может отличаться.

В итоге, в современных проектах рекомендуется отдать предпочтение ProcessBuilder и новым возможностям класса Process для удобства и читаемости кода.

Расширенные и редкие сценарии использования

1) Чтение stdout и stderr параллельно с использованием ExecutorService.

Пример java
Process p = Runtime.getRuntime().exec(new String[]{"bash","-c","for i in {1..100}; do echo out$i; echo err$i 1>&2; done"});
java.util.concurrent.ExecutorService es = java.util.concurrent.Executors.newFixedThreadPool(2);
es.submit(() -> { try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()))) { String s; while ((s = r.readLine()) != null) System.out.println("OUT> " + s); } catch (Exception ignored) {} });
es.submit(() -> { try (java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(p.getErrorStream()))) { String s; while ((s = r.readLine()) != null) System.out.println("ERR> " + s); } catch (Exception ignored) {} });
int code = p.waitFor();
es.shutdown();
System.out.println("Exit: " + code);
OUT> out1
ERR> err1
OUT> out2
ERR> err2
...
Exit: 0

2) Перенаправление вывода в файл через ProcessBuilder (демонстрация предпочтения ProcessBuilder).

Пример java
ProcessBuilder pb = new ProcessBuilder("ls","-la");
pb.redirectOutput(new java.io.File("/tmp/ls.txt"));
Process p = pb.start();
int code = p.waitFor();
System.out.println("Exit: " + code);
System.out.println(new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get("/tmp/ls.txt"))));
Exit: 0
(содержимое /tmp/ls.txt с выводом ls)

3) Запуск фона (detached) на Unix с двойным fork-подходом через оболочку.

Пример java
Runtime.getRuntime().exec(new String[]{"nohup","/path/to/app",">/dev/null","2>&1","&"});
// часто проще использовать systemd или screen для демонизации
Процесс запущен в фоне, PID доступен в механизмах ОС

4) Запуск процесса с таймаутом и принудительным завершением.

Пример java
Process p = Runtime.getRuntime().exec(new String[]{"sleep","30"});
if (!p.waitFor(5, java.util.concurrent.TimeUnit.SECONDS)) {
    p.destroy(); // мягкое завершение
    if (!p.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) p.destroyForcibly();
}
System.out.println("Finished: " + p.isAlive());
Finished: false

5) Получение PID и работа с ним (современные JVM).

Пример java
Process p = Runtime.getRuntime().exec(new String[]{"sleep","60"});
long pid = p.pid();
System.out.println("PID: " + pid);
// далее можно использовать pid для мониторинга на уровне ОС
p.destroy();
PID: 12345

6) Передача больших потоков данных во входной поток процесса.

Пример java
Process p = Runtime.getRuntime().exec(new String[]{"gzip","-c"});
try (java.io.OutputStream os = p.getOutputStream()) {
    os.write(new byte[10_000_000]); // пример большого объема данных
}
// читать результат из p.getInputStream()
Пока данные пишутся, процесс сжимает и выдает результат в stdout

7) Использование оболочки для сложных конструкций с перенаправлениями и пайпами.

Пример java
Runtime.getRuntime().exec(new String[]{"bash","-c","ps aux | grep java | wc -l"});
(количество процессов java)

Пояснения: при использовании пайпов и оболочки требуется запуск shell (bash, cmd), тогда возрастает риск инъекций, поэтому следует осторожно обрабатывать пользовательские данные.

джава Runtime.exec function comments

En
Runtime.exec Executes the specified string command in a separate process