Runtime.exec: примеры (JAVA)
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.
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).
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-подходом через оболочку.
Runtime.getRuntime().exec(new String[]{"nohup","/path/to/app",">/dev/null","2>&1","&"});
// часто проще использовать systemd или screen для демонизации
Процесс запущен в фоне, PID доступен в механизмах ОС
4) Запуск процесса с таймаутом и принудительным завершением.
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).
Process p = Runtime.getRuntime().exec(new String[]{"sleep","60"});
long pid = p.pid();
System.out.println("PID: " + pid);
// далее можно использовать pid для мониторинга на уровне ОС
p.destroy();
PID: 12345
6) Передача больших потоков данных во входной поток процесса.
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) Использование оболочки для сложных конструкций с перенаправлениями и пайпами.
Runtime.getRuntime().exec(new String[]{"bash","-c","ps aux | grep java | wc -l"});
(количество процессов java)
Пояснения: при использовании пайпов и оболочки требуется запуск shell (bash, cmd), тогда возрастает риск инъекций, поэтому следует осторожно обрабатывать пользовательские данные.