Procesos en Java
Una de las ventajas del uso de Java es que su SDK incluye una integración de la gestión de procesos.
Java Runtime
La clase Runtime es la que incluye métodos para lanzar procesos, llamar al recolector de basura, saber cuanta memoria hay disponible, liberar procesos, etc. Cada aplicación tiene entonces acceso a una sola instancia de java.lang.Runtime a través de:
Runtime.getRuntime()Este sistema se usa mucho para generar patrones Singleton, es decir que solo tienen una instancia de ejecución que se comparte.
Patrón Singleton
El Singleton garantiza una única instancia de una clase, haciendo que cada llamada al constructor cree un nuevo objeto, pero no una nueva instancia, proporcionando un punto de acceso global a la misma. La clase se implementa con un constructor privado por defecto, evitando que cualquier otro punto del programa pueda acceder y hacer un new. Se crea además un método estático que actúa como constructor.
Se controla que al instanciarse no exista ya una instancia:
public class Estudiante {
private static Estudiante instance;
// Constructor privado
private Estudiante(){
}
//Get que comprueba si existe una instancia de Estudiante.
public static Estudiante getInstancia(){
if (instance == null){
instance = new Estudiante();
}
return instance;
}
}
public class Singleton {
public static void main(String[] args) {
// Única forma de crear instancias de la clase estudiante
Estudiante est_unico = Estudiante.getInstancia();
}
}Process exec
Para la creación de procesos nos interesa el método public Process exec(String command). Con este método podemos lanzar programas del ordenador como por ejemplo el bloc de notas del sistema operativo:
Runtime.getRuntime().exec("notepad.exe");Si la aplicación tiene la variable path integrada en el SO no es necesario añadirla al mismo como sucede con el bloc de notas o la calculadora, pero al contrario con Java o un navegador instalado aparte. En tal caso hay que añadir en el command la ubicación del mismo:
Runtime.getRuntime().exec("c:"+separator+"windows"+separator+"notepad.exe");El separator nos sirve para que automáticamente Java use el separador correspondiente al SO en el que trabaja ya que mientras en Windows se usa \ en los que tienen base Unix (Linux y MacOs) se usa /. El siguiente código es el que se usa para lanzar la aplicación en el SO correspondiente en exec usando String.format:
// Primero obtenemos la carpeta del usuario
String homeDirectory = System.getProperty("user.home");
boolean isWindows = System.getProperty("os.name")
.toLowerCase().startsWith("windows");
if (isWindows) {
Runtime.getRuntime()
.exec(String.format("cmd.exe /c dir %s", homeDirectory));
} else {
Runtime.getRuntime()
.exec(String.format("sh -c ls %s", homeDirectory));
}¡Atención, algún examen tuvo este ejercicio!
Este otro código muestra como capturar un evento tipo mouseClick en Linux o Windows:
// Calling app example
public void mouseClicked(MouseEvent e) {
// Launch Page
try {
// Linux version
Runtime.getRuntime().exec("open http://localhost:8153/go");
// Windows version
Runtime.getRuntime().exec("explorer http://localhost:8153/go");
} catch (IOException e1) {
// Don't care
}
}Existe un método llamado .waitfor() que actúa como llamada bloqueante para comprobar si el programa lanzado está en proceso, en cuanto se cierra el programa lanzado devuelve el valor 0.
proceso2 = r.exec(comando2);
System.out.println("Esperando a que termine la ejecución de " + comando2);
int resultado = proceso2.waitFor();
if (resultado == 0) {
System.out.println(comando2 + " ha finalizado correctamente");
} else {
System.out.println("ERROR en la ejecución de "+ comando2);
}
System.out.println("Lanzando el comando3: " + comando3);
proceso3 = r.exec(comando3);En los comandos de un exec podemos añadir más detalles como por ejemplo con qué dirección abrir un navegador:
String comando3 = "C:\\Program Files\\Mozilla Firefox\\firefox.exe https://alojaweb.educastur.es/web/cifplalaboral/centro";Lanzamiento de comandos
Los comandos se lanzan igual que el resto de procesos que hemos visto hasta ahora, pero nos permiten añadir otros comandos a ejecutar como por ejemplo dir.
String comando = String.format("cmd.exe /c "+comando+" %s", homeDirectory);Veamos un ejemplo paso a paso, primero aprovechamos la clase Properties para sacar las propiedades del sistema del ordenador:
Properties p = System.getProperties();
p.list(System.out);Podemos a través de esta clase obtener la propiedad del nombre del sistema por si tiene Windows:
//Obtenemos la propiedad user.home para el directorio raiz del usuario
String homeDirectory = System.getProperty("user.home");
//Obtenemos la propiedad os.name para saber si es un SO windows
boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");De esta forma podemos preparar el programa para que funcione tanto en Windows como en Linux
String comando = (isWindows? "dir": "ls");
if(isWindows)
comando = String.format("cmd.exe /c "+comando+" %s", homeDirectory);
else
comando = String.format("sh -c "+comando+" %s", homeDirectory);
Process proceso;Con lo siguiente tomamos la salida del proceso y la imprimimos en la consola línea a línea
InputStream is;
InputStreamReader isr;
BufferedReader br;
if (resultado == 0) {
System.out.println("DIR se lanzó con éxito");
is = proceso.getInputStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);Luego recorremos el BufferedReader para imprimir en pantalla.
String linea;
while ((linea = br.readLine()) != null) {
System.out.println(linea);
}
br.close();
isr.close();
is.close();Este es uno de los ejercicios que puede estar en el examen.
Lanzamiento de Scripts de MS-DOS
Podemos lanzar con un comando scripts de MS-DOS como el siguiente:
String comando = "cmd /c .\\maniobra\\saludo "+ nombre;Lanzamiento de una Clase Java
Para lanzar una clase Java es necesario obtener los Bytecode, porque primero hay que compilar:
String comando = "javac .\\maniobra\\Saludo.java ";En este caso vemos como usamos javac (Java Compiler) para compilar primero el programa. Al llamarlo lo compila y también lo lanza inmediatamente.
Interactuar con un Script de MSDOS o clase Java
En este caso vamos a añadir a las ideas anteriores una interacción en el Java que afecta al otro programa o al script.
String comando = "cmd /c .\\maniobra\\pregunta";
// Pedimos el nombre
System.out.println("Dime tu nombre:\n");
Scanner in = new Scanner(System.in);
String nombre = in.nextLine();
// Tras connseguir el nombre lanzamos el script
System.out.println("Lanzando el Script MS-DOS");
proceso = r.exec(comando);
//En este caso recurrimos a un OutputStream que es un flujo binario
OutputStream os = proceso.getOutputStream();
//Escribimos sobre la salida el valor de nombre
os.write((nombre+"\n")
// Nos devuelve los bytes que tiene el objeto para poder aplicarlo en este caso
.getBytes());
// Vaciar la salida estándar, fuerza la escritura. Por cada write hay que poner un flush
os.flush();Esto es el bat usado en este caso:
@echo off
set bienvenida=Seas bienvenido !!!
set /p nombre=
echo ==================================
echo ==== Hola %nombre%
echo ==== Fecha de hoy: %date%
echo ==== %bienvenida%
echo ==================================Interacción con clase Java
Usamos de nuevo OutputStream
// En este caso tenemos un fichero ya compilado por eso no recurrimos a javac
String comando = "java .\\maniobra\\Pregunta.java";
Process proceso;
// Hacemos la entrada desde el Java original
Scanner teclado = new Scanner(System.in);
System.out.print("Dime tu nombre: ");
String nombre = teclado.nextLine();
// Lanzamos el java final e introducmios la entrada con OutputStream
proceso = r.exec(comando);
OutputStream os = proceso.getOutputStream();
os.write((nombre+"\n").getBytes());
os.flush();De esta forma enlazamos la entrada del primer Java al que vamos a abrir.
public static void main(String[] args) {
// Creamos el processBuilder
ProcessBuilder pb = new ProcessBuilder();
// Obtenemos las variables que van a un HashMap
HashMap variablesEntorno = (HashMap<String,String>) pb.environment();
System.out.println(variablesEntorno.get("Path"));
// Usamos SET
Set s = variablesEntorno.entrySet();
// La e es la pareja palabra=significado o clave=valor
Entry e;
// Creamos el iterator que devuelve true siempre que haya elementos en el proceso,
// en este caso el Set s.
Iterator i = s.iterator();
while (i.hasNext()) {
e = (Entry) i.next();
// El valor de la clave el valor del valor
System.out.println(e.getKey() + ": " + e.getValue());
}
}ProcessBuilder y Process
Además del Runtime existen dos clases para la gestión de procesos: ProcessBuilder que gestiona los atributos y Process que controla su ejecución. Antes de ejecutar el proceso configuramos los parámetros con ProcessBuilder.
// Primero preparamos ProcessBuilder que lo generamos así
ProcessBuilder pb = new ProcessBuilder("CMD", "/C", "DIR");
//o con un String completo
ProcessBuilder pb = new ProcessBuilder("cmd /c dir")
// Y luego llamamos a Process .start()
Process p = pb.start();Esto indica que hay dos constructores diferentes de ProcessBuilder:
ProcessBuilder(List<String> );
ProcessBuilder(String);De esta manera tenemos dos formas de preparar comandos muy completos.
String command3 = "c:/windows/system32/shutdown -s -t 0";
// Aquí convertimos el String anterior en una List con split()
ProcessBuilder pb = new ProcessBuilder(command3.split("\\s"));Modificar el comando durante el tiempo de ejecución
Con ProcessBuilder y Process podemos modificar o consultar a posteriori los comandos del ProcessBuilder con el método command. Si se pasa un command sin parámetros se obtiene una lista del comando pasado.
En este ejemplo añadimos al proceso un fichero temporal:
String command = "java -jar install.jar -install";
ProcessBuilder pb = new ProcessBuilder(command.split("\\s"));
// Aquí incluimos lo que falta al proceso, aprovechando el formato List
if(isWindows){
pbuilder.command().add(0,"cmd");
pbuilder.command().add(1,"/c");
pbuilder.add("c:/temp");
} else {
//Aquí lo que toque en linux
}
// Y ya lanzamos
pb.start();Variables de entorno
Las variables de entorno se añaden a través del método environtment que devuelve un Map de tipo clave=valor.
Map<String,String> environment = pb.environment();Si reemplazamos una de las variables de entorno, concatenara en una variable como path a otra dirección Al ser un map añadimos las variables a través del método put y las intercambiamos con replace.
En este ejemplo vemos la variable path del sistema
ProcessBuilder pb = new ProcessBuilder();
HashMap variablesEntorno = (HashMap<String,String>) pb.environment();
System.out.println(variablesEntorno.get("Path"));Y obtenemos el siguiente resultado:
C:\Program Files\Common Files\Oracle\Java\javapath;C:\Program Files (x86)\Common Files\Oracle\Java\java8path;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Git\cmd;C:\Users\2damv16\AppData\Local\Microsoft\WindowsApps;⚠️Cuidado, lanzar clases hijas puede producir problemas de falta de memoria o encontrarse con bloqueos de seguridad del propio Sistema Operativo⚠️
Gestión de la Entrada/Salida de un proceso
Los procesos no cuentan con una terminal o consola en la que mostrar su información. Así que hay varias formas de hacer seguimiento y comprobarlo.
Redirección de la E/S estándar
Por defecto la E/S se redirige al proceso padre que es el que puede usar streams para enviar o recoger información del proceso hijo.
En ningún momento, cuando estamos programando un proceso, debemos pensar si va a ser lanzado como padre o como hijo.
La E/S en Linux se trata como todo fichero ya que en Linux todo es tratado como un fichero.
En cada proceso al acceder a un fichero se asigna un identificador único siendo los siguientes:
- 0 para la entrada estándar
- 1 para la salida estándar
- 2 para la salida de error
Con esto podemos redirigir la salida de estos procesos a un archivo y ver mensajes detallados y facilita automatizar pruebas. En la relación padre-hijo que se crea entre procesos los descriptores también se redirigen desde el hijo hacia el padre, usando 3 tuberías o pipes, una por cada stream de E/S por defecto. Esas tuberías pueden usarse de forma similiar a cómo se hace en los sistemas Linux.
getInputStream()
Los objetos de la clase Process tienen los métodos getInputStream y getOutputStream.
Process p = pbuilder.start();
BufferedReader processOutput =
new BufferedReader(new InputStreamReader(p.getInputStream()));
String linea;
while ((linea = processOutput.readLine()) != null) {
System.out.println("> " + linea);
}
processOutput.close();El flujo de entrada por defecto nos da bytes

La clase Reader permite apoyarse en el InputStream para transformarlo en ficheros de caracteres

Aquí lo transformarlo para hacerlo más optimizado

Charsets y encodings
Para las aplicaciones que trabajan con la consola hay que tener en cuenta que en Windows se mejora la compatibilidad con la codificación CP850.
//Con esto especificamos qué codificación usar
new InputStreamReader(p.getInputStream(), "CP850");getErrorStream()
Si la salida de error ha sido previamente redirigida usando el método ProcessBuilder.redirectErrorStream(true) entonces la salida de error y la salida estándar llegan juntas con getInputStream() y no es necesario hacer un tratamiento adicional.
Process p = pbuilder.start();
BufferedReader processError =
new BufferedReader(new InputStreamReader(p.getErrorStream()));Patrón Decorator o Wrapper
En ambos tipos de streams de entrada (input y error) estamos recogiendo la información de un objeto de tipo BufferedReader. Podríamos usar directamente el InputStream que nos devuelven los métodos de Process, pero tendríamos que encargarnos nosotros de convertir los bytes a caracteres, de leer el stream caracter a caracter y de controlar el flujo al no disponer de un buffer.
import java.io.*;
public class Ejercicio2 {
public static void main(String[] args) {
String comando = "notepad";
ProcessBuilder pbuilder = new ProcessBuilder (comando);
Process p = null;
try {
p = pbuilder.start();
// 1- Procedemos a leer lo que devuelve el proceso hijo
InputStream is = p.getInputStream();
// 2- Lo convertimos en un InputStreamReader
// De esta forma podemos leer caracteres en vez de bytes
// El InputStreamReader nos permite gestionar diferentes codificaciones
InputStreamReader isr = new InputStreamReader(is);
// 2- Para mejorar el rendimiento hacemos un wrapper sobre un BufferedReader
// De esta forma podemos leer enteros, cadenas o incluso líneas.
BufferedReader br = new BufferedReader(isr);
// A Continuación leemos todo como una cadena, línea a línea
String linea;
while ((linea = br.readLine()) != null)
System.out.println(linea);
} catch (Exception e) {
System.out.println("Error en: "+comando);
e.printStackTrace();
} finally {
// Para finalizar, cerramos los recursos abiertos
br.close();
isr.close();
is.close();
}
}
}getOutputStream()
Así enviamos información del proceso padre al proceso hijo.
En este caso, el wrapper de mayor nivel nivel para usar un OutputStream es la clase PrintWriter que nos ofrece métodos similares a los de System.out.printxxxxx para gestionar el flujo de comunicación con el proceso hijo.
//Envoltorio de BW
PrintWriter toProcess = new PrintWriter(
//Envoltorio de OSW
new BufferedWriter(
new OutputStreamWriter(
p.getOutputStream(), "UTF-8")), true);
toProcess.println("sent to child");Heredar la E/S del proceso padre
Todo esto se puede hacer con una sola llamada si se va a heredad desde el padre con el método inheritIO().
Todo esto debe hacerse antes del start
Redirección de E/S estándar
En un sistema real, probablemente necesitemos guardar los resultados de un proceso en un archivo de log o de errores para su posterior análisis. Afortunadamente lo podemos hacer sin modificar el código de nuestras aplicaciones usando los métodos que proporciona el API de ProcessBuilder para hacer exactamente eso.
Por defecto, tal y como ya hemos visto, los procesos hijos reciben la entrada a través de una tubería a la que podemos acceder usando el OutputStream que nos devuelve Process.getOutputStream().
Sin embargo, tal y como veremos a continuación, esa entrada estándar se puede cambiar y redirigirse a otros destinos como un fichero usando el método redirectOutput(File) . Si modificamos la salida estándar, el método getOutputStream() devolverá ProcessBuilder.NullOutputStream.
Para redirigir la E/S salida antes de que el proceso sea ejecutado se usan métodos de la clase ProcessBuilder
Aquí redirigimos la salida antes de la llamada
ProcessBuilder processBuilder = new ProcessBuilder("java", "-version");
// La salida de error se enviará al mismo sitio que la estándar
processBuilder.redirectErrorStream(true);
File log = folder.newFile("java-version.log");
processBuilder.redirectOutput(log);
Process process = processBuilder.start();Opción más sencilla:
File log = tempFolder.newFile("java-version-append.log");
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(Redirect.appendTo(log));Otra vez más, es importante hacer notar la llamada a redirectErrorStream(true) . En el caso de que se produzca algún error, se mezclarán con los mensajes de salida en el fichero.
Información de procesos en Java
Con los métodos de la clase java.lang.ProcessHandle.Info podemos obtener información acerca del proceso en ejecución..current es un proceso estático de ProcessHandle y facilita la información del proceso en ejecución.
ProcessHandle processHandle = ProcessHandle.current();
ProcessHandle.Info processInfo = processHandle.info();
// Una vez conseguido el Objeto anterior podemos mostrar los detalles del proceso
System.out.println("PID: " + processHandle.pid());
System.out.println("Arguments: " + processInfo.arguments());
System.out.println("Command: " + processInfo.command());
System.out.println("Instant: " + processInfo.startInstant());
System.out.println("Total CPU duration: " + processInfo.totalCpuDuration());
System.out.println("User: " + processInfo.user());Desde el proceso padre no podemos mostrar info de la del hijo, pero se puede usar la instancia de java.lang.Process para llamar al método y obtener su instancia.
Process process = processBuilder.inheritIO().start();
ProcessHandle childProcessHandle = process.toHandle();
ProcessHandle.Info child processInfo = processHandle.info();OutputStream os = proceso.getOutputStream();