Cómo iniciar procesos externos con Python y el módulo de subprocesos

Cómo iniciar procesos externos con Python y el módulo de subprocesos

En nuestros scripts de automatización a menudo necesitamos lanzar y monitorear programas externos para realizar nuestras tareas deseadas. Al trabajar con Python, podemos usar el módulo de subprocesos para realizar dichas operaciones. Este módulo es parte de la biblioteca estándar de lenguaje de programación. En este tutorial lo echaremos un vistazo rápido y aprenderemos los conceptos básicos de su uso.

En este tutorial aprenderás:

  • Cómo usar la función "Ejecutar" para generar un proceso externo
  • Cómo capturar un proceso de salida estándar y error estándar
  • Cómo verificar el estado existente de un proceso y aumentar una excepción si falla
  • Cómo ejecutar un proceso en un shell intermediario
  • Cómo establecer un tiempo de espera para un proceso
  • Cómo usar la clase Popen directamente para tuve dos procesos
Cómo iniciar procesos externos con Python y el módulo de subprocesos

Requisitos y convenciones de software utilizados

Requisitos de software y convenciones de línea de comandos de Linux
Categoría Requisitos, convenciones o versión de software utilizada
Sistema Distribución independiente
Software Python3
Otro Conocimiento de Python y Programación Orientada a Objetos
Convenciones # - requiere que los comandos de Linux dados se ejecuten con privilegios raíz directamente como un usuario raíz o mediante el uso de sudo dominio
$-Requiere que los comandos de Linux dados se ejecuten como un usuario regular no privilegiado

La función "Ejecutar"

El correr La función se ha agregado al subproceso módulo solo en versiones relativamente recientes de Python (3.5). El uso ahora es la forma recomendada de generar procesos y debe cubrir los casos de uso más comunes. Antes de todo lo demás, veamos su uso más simple. Supongamos que queremos ejecutar el ls -al dominio; En una cáscara de pitón correríamos:

>>> importar subprocesos >>> proceso = subprocesos.run (['ls', '-l', '-a']) 

La salida del comando externo se muestra en la pantalla:

Total 132 DRWX------. 22 EGDOC EGDOC 4096 30 de noviembre 12:18 . DRWXR-XR-X. 4 raíz raíz 4096 22 de noviembre 13: 11… -RW-------. 1 EGDOC EGDOC 10438 DEC 1 12:54 .bash_history -rw-r-r---. 1 EGDOC EGDOC 18 27 de julio 15:10 .Bash_logout […] 

Aquí acabamos de utilizar el primer argumento obligatorio aceptado por la función, que puede ser una secuencia que "describe" un comando y sus argumentos (como en el ejemplo) o una cadena, que debe usarse cuando se ejecuta con el shell = verdadero argumento (lo veremos más tarde).

Capturando el comando stdout y stderr

¿Qué pasa si no queremos que se muestre la salida del proceso en la pantalla, sino que se puede hacer referencia después de que salga del proceso?? En ese caso podemos establecer el captura_output argumento de la función a Verdadero:

>>> proceso = subproceso.run (['ls', '-l', '-a'], capture_output = true) 

¿Cómo podemos recuperar la salida (stdout y stderr) del proceso después? Si observa los ejemplos anteriores, puede ver que usamos el proceso variable para hacer referencia a lo que devuelve el correr Función: A Proceso completo objeto. Este objeto representa el proceso que fue lanzado por la función y tiene muchas propiedades útiles. Entre los demás, stdout y stderr se utilizan para "almacenar" los descriptores correspondientes del comando si, como dijimos, el captura_output el argumento se establece en Verdadero. En este caso, para obtener el stdout del proceso que ejecutaríamos:

>>> proceso.stdout 

Stdout y stderr se almacenan como secuencias bytes por defecto. Si queremos que se almacenen como cadenas, debemos establecer el texto argumento del correr funcionar a Verdadero.



Administrar una falla del proceso

El comando que ejecutamos en los ejemplos anteriores se ejecutó sin errores. Sin embargo, al escribir un programa, todos los casos deben tenerse en cuenta, entonces, ¿qué pasa si un proceso designado falla?? Por defecto, no sucedería nada "especial". Veamos un ejemplo; Corremos el LS comandar nuevamente, intentar enumerar el contenido del /raíz Directorio, que normalmente, en Linux no es legible por los usuarios normales:

>>> proceso = subproceso.run (['ls', '-l', '-a', '/root']) 

Una cosa que podemos hacer para verificar si un proceso lanzado falló, es verificar su estado existente, que se almacena en el código de retorno propiedad del Proceso completo objeto:

>>> proceso.returncode 2 

Ver? En este caso el código de retorno era 2, confirmando que el proceso encontró un problema de permiso y no se completó con éxito. Podríamos probar la salida de un proceso de esta manera, o más elegantemente podríamos hacer para que se plantee una excepción cuando ocurra un fracaso. Introducir el controlar argumento del correr Función: cuando está configurado en Verdadero y un proceso engendrado falla, el Llamado Processerror Se plantea la excepción:

>>> proceso = subproceso.run (['ls', '-l', '-a', '/root'], check = true) ls: no se puede abrir el directorio '/root': permiso rastreado negado (la llamada más reciente Última): archivo "" , línea 1, en el archivo "/usr/lib64/python3.9/subprocesos.Py ", línea 524, en Run Raise Llamada ProcessError (RetCode, Process.Args, subprocesos.LlamadoProcesserror: comando '[' ls ',' -l ',' -a ','/root ']' devuelto el estado de salida no cero 2. 

Manejo excepciones En Python es bastante fácil, por lo que para administrar una falla del proceso podríamos escribir algo como:

>>> intente: ... proceso = subprocesos.run (['ls', '-l', '-a', '/root'], check = true) ... excepto subprocesos.Llamado ProcessError AS E: ... # Solo un ejemplo, se debe hacer algo útil para administrar la falla!... imprimir (f "e.CMD falló!") ... ls: no se puede abrir el directorio '/root': permiso denegado ['ls', '-l', '-a', '/root'] fallido! >>> 

El Llamado Processerror La excepción, como dijimos, se plantea cuando un proceso sale con un no 0 estado. El objeto tiene propiedades como código de retorno, CMD, stdout, stderr; Lo que representan es bastante obvio. En el ejemplo anterior, por ejemplo, solo usamos el CMD propiedad, para informar la secuencia que se usó para describir el comando y sus argumentos en el mensaje que escribimos cuando ocurrió la excepción.

Ejecutar un proceso en un shell

Los procesos lanzados con el correr función, se ejecutan "directamente", esto significa que no se utiliza ningún shell para lanzarlos: por lo tanto, no hay variables de entorno disponibles para el proceso y no se realizan expansiones de shell. Veamos un ejemplo que implica el uso de $ Inicio variable:

>>> proceso = subproceso.run (['ls', '-al', '$ home']) ls: no puede acceder a '$ home': no ​​hay dicho archivo o directorio 

Como puedes ver el $ Inicio La variable no se expandió. Se recomienda ejecutar procesos de esta manera para evitar posibles riesgos de seguridad. Sin embargo, si en ciertos casos, necesitamos invocar un caparazón como un proceso intermedio, necesitamos establecer el caparazón Parámetro del correr funcionar a Verdadero. En tales casos, es preferible especificar el comando que se ejecutará y sus argumentos como un cadena:

>>> proceso = subproceso.Run ('ls -al $ home', shell = true) Total 136 DRWX------. 23 EGDOC EGDOC 4096 Dic 3 09:35 . DRWXR-XR-X. 4 raíz raíz 4096 22 de noviembre 13: 11… -RW-------. 1 EGDOC EGDOC 11885 Dic 3 09:35 .bash_history -rw-r-r---. 1 EGDOC EGDOC 18 27 de julio 15:10 .Bash_logout […] 

Todas las variables existentes en el entorno de usuario se pueden usar al invocar un caparazón como un proceso intermedio: si bien esto puede parecer útil, puede ser una fuente de problemas, especialmente cuando se trata de una entrada potencialmente peligrosa, lo que podría conducir a inyecciones de concha. Ejecutar un proceso con shell = verdadero Por lo tanto, se desaniman y debe usarse solo en casos seguros.



Especificando un tiempo de espera para un proceso

Por lo general, no queremos que los procesos de comportamiento se ejecuten para siempre en nuestro sistema una vez que sean lanzados. Si usamos el se acabó el tiempo Parámetro del correr función, podemos especificar una cantidad de tiempo en segundos que el proceso debe tomar para completar. Si no se completa en ese período de tiempo, el proceso será asesinado con un Sigkill señal, que, como sabemos, no puede ser atrapado por un proceso. Demostremos un poco de un proceso de ejecución y proporcionando un tiempo de espera en segundos:

>>> proceso = subproceso.ejecutar (['ping', 'Google.com '], Tiempo de espera = 5) Ping Google.com (216.58.206.46) 56 (84) bytes de datos. 64 bytes de MIL07S07-In-F14.1e100.neto (216.58.206.46): icmp_seq = 1 ttl = 113 time = 29.3 MS 64 bytes de LHR35S10-In-F14.1e100.neto (216.58.206.46): icmp_seq = 2 ttl = 113 tiempo = 28.3 MS 64 bytes de LHR35S10-In-F14.1e100.neto (216.58.206.46): icmp_seq = 3 ttl = 113 tiempo = 28.5 MS 64 bytes de LHR35S10-In-F14.1e100.neto (216.58.206.46): icmp_seq = 4 ttl = 113 tiempo = 28.5 MS 64 bytes de LHR35S10-In-F14.1e100.neto (216.58.206.46): icmp_seq = 5 ttl = 113 tiempo = 28.1 ms TraceBack (la llamada más reciente): archivo "", línea 1, en el archivo "/usr/lib64/python3.9/subprocesos.py ", línea 503, en run stdout, stderr = proceso.comunicar (entrada, tiempo de tiempo = tiempo de espera) "USR/lib64/python3.9/subprocesos.py ", línea 1130, en comunicar stdout, stderr = self._Communicate (Entrada, EndTime, TimeOut) File "/USR/Lib64/Python3.9/subprocesos.py ", línea 2003, en _communication yo.esperar (tiempo de espera = self._remaining_time (endtime)) archivo "/usr/lib64/python3.9/subprocesos.py ", línea 1185, en espera return yo._WAIT (TIMEOUT = TIMEOUT) FILE "/USR/LIB64/PYTHON3.9/subprocesos.py ", línea 1907, en _wait recaudó tiempo de espera (yo mismo.Args, tiempo de espera) subprocesos.Tiempo de espera: comando '[' ping ',' Google.com ']' cronometrado después de 4.999826977029443 segundos 

En el ejemplo anterior lanzamos el silbido comando sin especificar una cantidad fija de Solicitud de eco paquetes, por lo tanto, podría funcionar para siempre. También especificamos un tiempo de espera de 5 segundos a través del se acabó el tiempo parámetro. Como podemos observar el programa inicialmente, pero el Tiempo agotado Se planteó la excepción cuando se alcanzó la cantidad especificada de segundos, y el proceso fue asesinado.

Las funciones de llamada, check_output y check_call

Como dijimos antes, el correr La función es la forma recomendada de ejecutar un proceso externo y debe cubrir la mayoría de los casos. Antes de que se introdujera en Python 3.5, las tres funciones API de alto nivel principales utilizadas para lanzar un proceso fueron llamar, check_output y check_call; Veamos brevemente.

En primer lugar, el llamar Función: se utiliza para ejecutar el comando descrito por el argumentos parámetro; espera a que se complete el comando y devuelva su código de retorno. Corresponde aproximadamente al uso básico del correr función.

El check_call el comportamiento de la función es prácticamente el mismo de la de la correr función cuando el controlar El parámetro se establece en Verdadero: ejecuta el comando especificado y espera a que se complete. Si su estado existente no es 0, a Llamado Processerror Se plantea la excepción.

Finalmente, el check_output Función: funciona de manera similar a check_call, pero devoluciones la salida del programa: no se muestra cuando la función se ejecuta.

Trabajar en un nivel más bajo con la clase Popen

Hasta ahora exploramos las funciones API de alto nivel en el módulo de subprocesos, especialmente correr. Todas estas funciones, debajo del capó interactúan con el Estocada clase. Debido a esto, en la gran mayoría de los casos no tenemos que trabajar con él directamente. Sin embargo, cuando se necesita más flexibilidad, creando Estocada Los objetos se vuelven directamente necesarios.



Supongamos, por ejemplo, queremos conectar dos procesos, recreando el comportamiento de una carcasa "tubería". Como sabemos, cuando tuve dos comandos en la carcasa, la salida estándar de la del lado izquierdo de la tubería (|) se usa como la entrada estándar de la derecha (verifique este artículo sobre las redirecciones de shell si desea saber más sobre el tema). En el ejemplo a continuación, el resultado de la tubería, los dos comandos se almacenan en una variable:

$ output = "$ (dmesg | grep sda)" 

Para emular este comportamiento utilizando el módulo de subprocesos, sin tener que establecer el caparazón parámetro Verdadero Como vimos antes, debemos usar el Estocada clase directamente:

dmesg = subproceso.Popen (['dmesg'], stdout = subprocesos.Tubería) Grep = subproceso.Popen (['grep', 'sda'], stdin = dmesg.stdout) dmesg.stdout.cerrar () salida = grep.Comunicate () [0] 

Para comprender el ejemplo anterior, debemos recordar que un proceso comenzó utilizando el Estocada La clase directamente no bloquea la ejecución del script, ya que ahora se espera.

Lo primero que hicimos en el fragmento de código anterior fue crear el Estocada objeto que representa el dmesg proceso. Establecimos el stdout de este proceso a subproceso.TUBO: Este valor indica que se debe abrir una tubería a la corriente especificada.

Que creamos otra instancia del Estocada clase para el grep proceso. En el Estocada Constructor, especificamos el comando y sus argumentos, por supuesto, pero, aquí está la parte importante, establecemos la salida estándar del dmesg proceso para ser utilizado como entrada estándar (stdin = dmesg.stdout), así que recrear el caparazón
comportamiento de tubería.

Después de crear el Estocada objeto para el grep comando, cerramos el stdout transmisión de la dmesg proceso, utilizando el cerca() Método: Esto, como se indica en la documentación, es necesario para permitir que el primer proceso reciba una señal Sigpipe. Intentemos explicar por qué. Normalmente, cuando dos procesos están conectados por una tubería, si el que está a la derecha de la tubería (GREP en nuestro ejemplo) sale antes de la izquierda (DMESG), este último recibe un Siglo
señal (tubería rota) y, por defecto, se termina.

Sin embargo, al replicar el comportamiento de una tubería entre dos comandos en Python, hay un problema: el stdout del primer proceso se abre tanto en el script principal como en la entrada estándar del otro proceso. De esta manera, incluso si el grep El proceso termina, la tubería seguirá abierta en el proceso de la persona que llama (nuestro script), por lo tanto, el primer proceso nunca recibirá el Siglo señal. Por eso necesitamos cerrar el stdout flujo del primer proceso en nuestro
Script principal después de lanzar el segundo.

Lo último que hicimos fue llamar al comunicar() método en el grep objeto. Este método se puede usar para pasar opcionalmente la entrada a un proceso; espera que el proceso termine y devuelva una tupla donde el primer miembro es el proceso stdout (a lo que se hace referencia al producción variable) y el segundo el proceso stderr.

Conclusiones

En este tutorial vimos la forma recomendada de generar procesos externos con Python utilizando el subproceso módulo y el correr función. El uso de esta función debería ser suficiente para la mayoría de los casos; Sin embargo, cuando se necesita un mayor nivel de flexibilidad, uno debe usar el Estocada Clase directamente. Como siempre, sugerimos echar un vistazo a
Documentación de subprocesos para una descripción completa de la firma de funciones y clases disponibles en
el módulo.

Tutoriales de Linux relacionados:

  • Una introducción a la automatización, herramientas y técnicas de Linux
  • Mastering Bash Script Loops
  • Cosas para instalar en Ubuntu 20.04
  • Bucles anidados en guiones Bash
  • Cómo usar el comando tcpdump en linux
  • Mint 20: Mejor que Ubuntu y Microsoft Windows?
  • Manejo de la entrada del usuario en scripts bash
  • Tutorial de depuración de GDB para principiantes
  • Cosas que hacer después de instalar Ubuntu 20.04 fossa focal Linux
  • Cómo monitorear la actividad de la red en un sistema Linux