Invocación de procesos en segundo plano desde NodeJS

Es posible que te encuentres en una situación en la que tengas que integrar tu implementación con sistemas legados, o que se desarrollan en paralelo en otras tecnologías, o directamente con scripts. Para esos casos NodeJs nos ofrece de serie un potente módulo, llamado child_process, que permite invocar procesos como si estuviéramos poniendo comandos en nuestro terminal. En esta publicación te explicamos algunas claves sobre cómo invocar, sincronizar y matar procesos en segundo plano lanzados desde NodeJS.

Invocando procesos con spawn

Empezamos con un ejemplo simple pero completo, supongamos que queremos invocar un comando ls para listar el contenido del directorio /usr. Para ello, escribimos el siguiente código en un nuevo fichero:

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});
  1. Lo primero es importar la función spawn de child_process: const { spawn } = require(‘child_process’);

  2. A continuación llamamos a la función spawn con los parámetros necesarios para invocar nuestro comando de listado y lo asignamos a una variable const ls = spawn(‘ls’, [‘-lh’, ‘/usr’]);  A efectos, ls es nuestra referencia al proceso hijo que acabamos de invocar.

  3. Por defecto se establecen pipes para stdin, stdout, y stderr entre el proceso padre de NodeJS y el proceso hijo invocado. Estas tuberías tienen capacidades limitadas y específicas de la plataforma, si el proceso hijo genera una cantidad enorme de datos puede saturar el buffer de la tubería y quedarse bloqueado hasta que haya espacio en este. Por eso es importante usar la opción { stdio: ‘ignore’ } para deshabilitar las tuberías si no van a ser utilizadas.

  4. Detectamos el evento close del proceso invocado así como el código de estado que devuelve: ls.on(‘close’, (code) => {});

Este es el resultado que produce nuestro pequeño programa:

$ node server.js

stdout: total 0
drwxr-xr-x  972 root wheel    30K 5 jun 18:35 bin
drwxr-xr-x  304 root wheel   9,5K 22 feb 13:29 lib
drwxr-xr-x  249 root wheel   7,8K 5 jun 18:35 libexec
drwxr-xr-x    7 root wheel   224B 25 feb 10:31 local
drwxr-xr-x  239 root wheel   7,5K 22 feb 13:25 sbin
drwxr-xr-x   46 root wheel   1,4K 22 feb 13:25 share
drwxr-xr-x    5 root wheel   160B 5 feb 05:41 standalone

child process exited with code 0

Lanzando procesos independientes (detached)

Por defecto cuando se lanza un proceso hijo, el padre se queda esperando a que este termine antes de terminar él mismo. En algunas ocasiones, especialmente con procesos de larga duración, puede ser interesante que tanto el proceso NodeJS como el invocado se ejecuten de forma independiente. De esta manera, puede terminar el proceso padre y continuar ejecutándose el proceso invocado, para lograr necesitaremos la opción detached y la función unref(), veamos cómo:

const { spawn } = require('child_process');

const subprocess = spawn(process.argv[0], ['child_program.js'], {
  detached: true,
  stdio: 'ignore'
});

subprocess.unref();

Invocando la función unref() conseguimos que el loop de eventos del proceso padre no incluya al hijo, de manera que pueda terminar independientemente de que lo haga o no el hijo. El problema es que si queremos que el hijo se siga ejecutando una vez terminado el padre, necesitamos indicar una configuración de stdio que no dependa del padre, bien sea ignorándola por completo (como en el ejemplo anterior) o volcando esas salidas a un fichero externo, como en el siguiente ejemplo:

const fs = require('fs');
const { spawn } = require('child_process');
const out = fs.openSync('./out.log', 'a');
const err = fs.openSync('./out.log', 'a');

const subprocess = spawn('prg', [], {
  detached: true,
  stdio: [ 'ignore', out, err ]
});

subprocess.unref();

Matando los procesos hijos

Supongamos que tenemos un proceso hijo que falla pero no termina por sí mismo, o simplemente se queda bloqueado y queremos darlo por finalizado transcurrido un timeout. En estos casos necesitamos que el proceso padre se encargue de matar al hijo, si los procesos son independientes (detached) esto es posible.

Para lograrlo, el padre se encarga de mandar una señal al hijo indicándole que termine, invocando a la función kill() con el tipo de señal deseada. Si no se especifica ninguna, por defecto ser manda SIGTERM.

Veamos un ejemplo:

const { spawn } = require('child_process');
const grep = spawn('grep', ['ssh']);

grep.on('close', (code, signal) => {
  console.log(
    `child process terminated due to receipt of signal ${signal}`);
});

// Send SIGHUP to process
grep.kill('SIGHUP');

Invocando procesos en segundo plano 

Vamos a emplear los conceptos que hemos visto anteriormente en un ejemplo de invocación de procesos en segundo plano.

En nuestro ejemplo, tenemos un proceso padre, que es un servidor NodeJS, cuya capa de dominio necesita producir y consumir datos que vienen dados por la ejecución de una serie de programas ( por ejemplo escritos en C) que vamos a invocar como procesos hijos, estos son long-process-1 y long-process-2.

exports.invokeChildren = params => {
  const childProcess = spawn('long-process-1', [params], {detached: true}).childProcess;
  const anotherChildProcess = spawn('long-process-2', [params], {detached: true}).childProcess;

  childProcess.stdout.on('data', async data => {     });
  childProcess.stderr.on('data', async data => {     });

  return childProcess.pid;
}
exports.invokeAnotherChildren = params => {
  const anotherChildProcess = spawn('long-process-2', [params], {detached: true}).childProcess;

  anotherChildProcess.stdout.on('data', async data => {     });
  anotherChildProcess.stderr.on('data', async data => {     });

  return anotherChildProcess.pid;
}

Estas funciones desencadenan la ejecución de procesos hijos y corren de forma totalmente independiente, un ejemplo de caso real puede ser una llamada a un endpoint de una API que provoca la invocación de una o varias de estas funciones.

Para controlar que no se quede corriendo ningún proceso cuya salida no haya sido controlada o que simplemente esté tardando más de lo normal, es recomendable matar los procesos invocados transcurrido un timeout. Esto puede hacerse justo después de la llamada a spawn que invoca el proceso, veamos otra vez el ejemplo incluyendo esto:

exports.invokeChildren = params => {
  const childProcess = spawn('long-process-1', [params], {detached: true}).childProcess;
  const anotherChildProcess = spawn('long-process-2', [params], {detached: true}).childProcess;

  // Stop job after max time
  const timeout = setTimeout(async () => {
      const error = { pid: childProcess.pid, code: -1, message: 'Process killed on timeout'};  
      logger.error(error);
      process.kill(childProcess.pid);
  }, 20000);

  childProcess.stdout.on('data', async data => {     });
  childProcess.stderr.on('data', async data => {     });

  return childProcess.pid;
}

Transcurridos 20 segundos queremos matar el proceso, para dejar constancia de ello primero construimos un objeto error y se lo pasamos a un logger, hecho esto matamos el proceso mediante process.kill().

Conclusiones

A pesar de habernos dejado bastantes detalles y funciones, espero que este post te haya servido para ver la potencia que ofrece el módulo child_process. Y es que con unos conceptos bastante sencillos podemos integrar la ejecución de cualquier proceso externo en nuestra propia aplicación, las posibilidades son infinitas.

Deja un comentario

Responsable » Solidgear.
Finalidad » Gestionar los comentarios.
Legitimación » Tu consentimiento.
Destinatarios » Los datos que me facilitas estarán ubicados en los servidores SolidgearGroup dentro de la UE.
Derechos » Podrás ejercer tus derechos, entre otros, a acceder, rectificar, limitar y suprimir tus datos.

Envíando este formulario aceptas la política de privacidad.

¿Necesitas una estimación?

Calcula ahora