Recherche de site Web

Comment lancer des processus externes avec Python et le module de sous-processus


Dans nos scripts d’automatisation, nous avons souvent besoin de lancer et de surveiller des programmes externes pour accomplir les tâches souhaitées. Lorsque nous travaillons avec Python, nous pouvons utiliser le module subprocess pour effectuer ces opérations. Ce module fait partie de la bibliothèque standard du langage de programmation. Dans ce tutoriel, nous allons y jeter un coup d’œil rapide et nous apprendrons les bases de son utilisation.

Dans ce tutoriel, vous allez apprendre :

  • Comment utiliser la fonction « run » pour générer un processus externe
  • Comment capturer une sortie standard de processus et une erreur standard
  • Comment vérifier l’état d’existence d’un processus et lever une exception en cas d’échec
  • Comment exécuter un processus dans un shell intermédiaire
  • Comment définir un délai d’expiration pour un processus
  • Comment utiliser la classe Popen directement pour canaliser deux processus

Comment lancer des processus externes avec Python et le module de sous-processus

Configuration logicielle requise et conventions utilisées

La fonction « run »

La fonction run n’a été ajoutée au module subprocess que dans les versions relativement récentes de Python (3.5). Son utilisation est désormais la méthode recommandée pour générer des processus et devrait couvrir les cas d’utilisation les plus courants. Avant tout le reste, voyons son utilisation la plus simple. Supposons que nous voulions exécuter la commande ls -al ; dans un shell Python, nous exécuterions :

>>> import subprocess
>>> process = subprocess.run(['ls', '-l', '-a'])

Le résultat de la commande externe s’affiche à l’écran :

total 132
drwx------. 22 egdoc egdoc  4096 Nov 30 12:18 .
drwxr-xr-x.  4 root  root   4096 Nov 22 13:11 ..
-rw-------.  1 egdoc egdoc 10438 Dec  1 12:54 .bash_history
-rw-r--r--.  1 egdoc egdoc    18 Jul 27 15:10 .bash_logout
[...]

Ici, nous venons d’utiliser le premier argument obligatoire accepté par la fonction, qui peut être une séquence qui « décrit » une commande et ses arguments (comme dans l’exemple) ou une chaîne de caractères, qui devrait être utilisée lors de l’exécution avec l’argument shell=True (nous le verrons plus tard).

Capture de la commande stdout et stderr

Que se passe-t-il si nous ne voulons pas que le résultat du processus soit affiché à l’écran, mais plutôt capturé, afin qu’il puisse être référencé après la fin du processus ? Dans ce cas, nous pouvons définir l’argument capture_output de la fonction à True :

>>> process = subprocess.run(['ls', '-l', '-a'], capture_output=True)

Comment pouvons-nous récupérer la sortie (stdout et stderr) du processus par la suite ? Si vous observez les exemples ci-dessus, vous pouvez voir que nous avons utilisé la variable process pour référencer ce qui est renvoyé par la fonction run : un objet CompletedProcess. Cet objet représente le processus qui a été lancé par la fonction et possède de nombreuses propriétés utiles. Parmi les autres, stdout et stderr sont utilisés pour « stocker » les descripteurs correspondants de la commande si, comme nous l’avons dit, l’argument capture_output est défini à True. Dans ce cas, pour obtenir le stdout du processus, nous exécuterions :

>>> process.stdout

Par défaut, stdout et stderr sont stockés sous forme de séquences d’octets. Si nous voulons qu’ils soient stockés sous forme de chaînes, nous devons définir l’argument text de la fonction run à True.

Gérer une défaillance de processus

La commande que nous avons exécutée dans les exemples précédents a été exécutée sans erreur. Lors de l’écriture d’un programme, cependant, tous les cas doivent être pris en compte, alors que se passe-t-il si un processus généré échoue ? Par défaut, rien de « spécial » ne se produirait. Voyons un exemple ; nous exécutons à nouveau la commande ls, en essayant de lister le contenu du répertoire /root, qui normalement, sous Linux, n’est pas lisible par les utilisateurs normaux :

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

Une chose que nous pouvons faire pour vérifier si un processus lancé a échoué est de vérifier son état existant, qui est stocké dans la propriété returncode de l’objet CompletedProcess :

>>> process.returncode
2

Voir? Dans ce cas, le code de retour était 2, confirmant que le processus a rencontré un problème d’autorisation et qu’il n’a pas été mené à bien. Nous pourrions tester le résultat d’un processus de cette façon, ou plus élégamment, nous pourrions faire en sorte qu’une exception soit levée lorsqu’un échec se produit. Entrez l’argument check de la fonction run : lorsqu’il est défini sur True et qu’un processus généré échoue, l’exception CalledProcessError est levée :

>>> process = subprocess.run(['ls', '-l', '-a', '/root'], check=True)
ls: cannot open directory '/root': Permission denied
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.9/subprocess.py", line 524, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['ls', '-l', '-a', '/root']' returned non-zero exit status 2.

La gestion des exceptions en Python est assez facile, donc pour gérer un échec de processus, nous pourrions écrire quelque chose comme :

>>> try:
...   process = subprocess.run(['ls', '-l', '-a', '/root'], check=True)
... except subprocess.CalledProcessError as e:
...   # Just an example, something useful to manage the failure should be done!
...   print(f"{e.cmd} failed!")
...
ls: cannot open directory '/root': Permission denied
['ls', '-l', '-a', '/root'] failed!
>>>

L’exception CalledProcessError, comme nous l’avons dit, est levée lorsqu’un processus se termine avec un état différent de 0. L’objet a des propriétés telles que returncode, cmd, stdout, stderr ; Ce qu’ils représentent est assez évident. Dans l’exemple ci-dessus, par exemple, nous avons simplement utilisé la propriété cmd, pour signaler la séquence qui a été utilisée pour décrire la commande et ses arguments dans le message que nous avons écrit lorsque l’exception s’est produite.

Exécuter un processus dans un shell

Les processus lancés avec la fonction run, sont exécutés « directement », c’est-à-dire qu’aucun shell n’est utilisé pour les lancer : aucune variable d’environnement n’est donc disponible pour le processus et les extensions de shell ne sont pas effectuées. Voyons un exemple qui implique l’utilisation de la variable $HOME :

>>> process = subprocess.run(['ls', '-al', '$HOME'])
ls: cannot access '$HOME': No such file or directory

Comme vous pouvez le voir, la variable $HOME n’a pas été développée. Il est recommandé d’exécuter les processus de cette manière afin d’éviter les risques de sécurité potentiels. Si, dans certains cas, cependant, nous devons invoquer un shell en tant que processus intermédiaire, nous devons définir le paramètre shell de la fonction run à True. Dans ce cas, il est préférable de spécifier la commande à exécuter et ses arguments sous forme de chaîne de caractères :

>>> process = subprocess.run('ls -al $HOME', shell=True)
total 136
drwx------. 23 egdoc egdoc  4096 Dec  3 09:35 .
drwxr-xr-x.  4 root  root   4096 Nov 22 13:11 ..
-rw-------.  1 egdoc egdoc 11885 Dec  3 09:35 .bash_history
-rw-r--r--.  1 egdoc egdoc    18 Jul 27 15:10 .bash_logout
[...]

Toutes les variables existantes dans l’environnement utilisateur peuvent être utilisées lors de l’appel d’un shell en tant que processus intermédiaire : bien que cela puisse sembler pratique, cela peut être une source de problèmes, en particulier lorsqu’il s’agit d’une entrée potentiellement dangereuse, ce qui pourrait conduire à des injections de shell. L’exécution d’un processus avec shell=True est donc déconseillée et ne doit être utilisée que dans des cas sûrs.

Spécification d’un délai d’expiration pour un processus

Nous ne voulons généralement pas que les processus qui se comportent mal s’exécutent indéfiniment sur notre système une fois qu’ils sont lancés. Si nous utilisons le paramètre timeout de la fonction run, nous pouvons spécifier une durée en secondes que le processus doit prendre pour se terminer. S’il n’est pas terminé dans ce laps de temps, le processus sera tué par un signal SIGKILL, qui, comme nous le savons, ne peut pas être capté par un processus. Faisons-le en sorte qu’un processus de longue durée soit mis en place et qu’il fournisse un délai d’expiration en secondes :

>>> process = subprocess.run(['ping', 'google.com'], timeout=5)
PING google.com (216.58.206.46) 56(84) bytes of data.
64 bytes from mil07s07-in-f14.1e100.net (216.58.206.46): icmp_seq=1 ttl=113 time=29.3 ms
64 bytes from lhr35s10-in-f14.1e100.net (216.58.206.46): icmp_seq=2 ttl=113 time=28.3 ms
64 bytes from lhr35s10-in-f14.1e100.net (216.58.206.46): icmp_seq=3 ttl=113 time=28.5 ms
64 bytes from lhr35s10-in-f14.1e100.net (216.58.206.46): icmp_seq=4 ttl=113 time=28.5 ms
64 bytes from lhr35s10-in-f14.1e100.net (216.58.206.46): icmp_seq=5 ttl=113 time=28.1 ms
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.9/subprocess.py", line 503, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
  File "/usr/lib64/python3.9/subprocess.py", line 1130, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
  File "/usr/lib64/python3.9/subprocess.py", line 2003, in _communicate
    self.wait(timeout=self._remaining_time(endtime))
  File "/usr/lib64/python3.9/subprocess.py", line 1185, in wait
    return self._wait(timeout=timeout)
  File "/usr/lib64/python3.9/subprocess.py", line 1907, in _wait
    raise TimeoutExpired(self.args, timeout)
subprocess.TimeoutExpired: Command '['ping', 'google.com']' timed out after 4.999826977029443 seconds

Dans l’exemple ci-dessus, nous avons lancé la commande ping sans spécifier un nombre fixe de paquets ECHO REQUEST, elle pourrait donc potentiellement s’exécuter indéfiniment. Nous avons également spécifié un délai d’expiration de 5 secondes via le paramètre timeout. Comme nous pouvons l’observer, le programme s’est exécuté initialement, mais l’exception TimeoutExpired a été levée lorsque le nombre de secondes spécifié a été atteint et le processus a été arrêté.

Les fonctions d’appel, de check_output et de check_call

Comme nous l’avons dit précédemment, la fonction run est la méthode recommandée pour exécuter un processus externe et devrait couvrir la majorité des cas. Avant son introduction dans Python 3.5, les trois principales fonctions API de haut niveau utilisées pour lancer un processus étaient call, check_output et check_call ; Voyons-les brièvement.

Tout d’abord, la fonction call : elle sert à exécuter la commande décrite par le paramètre args ; Il attend que la commande soit terminée et renvoie son code de retour. Cela correspond à peu près à l’utilisation de base de la fonction run.

Le comportement de la fonction check_call est pratiquement le même que celui de la fonction run lorsque le paramètre check est défini sur True : elle exécute la commande spécifiée et attend qu’elle se termine. Si son état existe n’est pas 0, une exception CalledProcessError est levée.

Enfin, la fonction check_output : elle fonctionne de la même manière que check_call, mais renvoie la sortie du programme : elle ne s’affiche pas lorsque la fonction est exécutée.

Travailler à un niveau inférieur avec la classe Popen

Jusqu’à présent, nous avons exploré les fonctions API de haut niveau dans le module de sous-processus, en particulier run. Toutes ces fonctions, sous le capot interagissent avec la classe Popen. Pour cette raison, dans la grande majorité des cas, nous n’avons pas à travailler directement avec lui. Cependant, lorsque plus de flexibilité est nécessaire, la création directe d’objets Popen devient nécessaire.

Supposons, par exemple, que nous voulions connecter deux processus, en recréant le comportement d’un shell « pipe ». Comme nous le savons, lorsque nous canalisons deux commandes dans le shell, la sortie standard de celle qui se trouve à gauche du tube (|) est utilisée comme entrée standard de celle qui se trouve à droite de celui-ci (consultez cet article sur les redirections de shell si vous voulez en savoir plus sur le sujet). Dans l’exemple ci-dessous, le résultat de la tuyauterie des deux commandes est stocké dans une variable :

output="$(dmesg | grep sda)"

Pour émuler ce comportement à l’aide du module subprocess, sans avoir à définir le paramètre shell à True comme nous l’avons vu précédemment, nous devons utiliser directement la classe Popen :

dmesg = subprocess.Popen(['dmesg'], stdout=subprocess.PIPE)
grep = subprocess.Popen(['grep', 'sda'], stdin=dmesg.stdout)
dmesg.stdout.close()
output = grep.comunicate()[0]

Pour comprendre l’exemple ci-dessus, il faut se rappeler qu’un processus commencé en utilisant directement la classe Popen ne bloque pas l’exécution du script, puisqu’il est maintenant attendu.

La première chose que nous avons faite dans l’extrait de code ci-dessus, a été de créer l’objet Popen représentant le processus dmesg. Nous définissons le stdout de ce processus sur subprocess. PIPE : cette valeur indique qu’un tuyau vers le flux spécifié doit être ouvert.

Nous avons ensuite créé une autre instance de la classe Popen pour le processus grep. Dans le constructeur Popen, nous avons spécifié la commande et ses arguments, bien sûr, mais, voici la partie importante, nous définissons la sortie standard du processus dmesg à utiliser comme entrée standard (stdin=dmesg.stdout), afin de recréer le comportement shell
pipe.

Après avoir créé l’objet Popen pour la commande grep, nous avons fermé le flux stdout du processus dmesg, en utilisant la méthode close() : celle-ci, comme indiqué dans la documentation, est nécessaire pour permettre au premier processus de recevoir un signal SIGPIPE. Essayons d’expliquer pourquoi. Normalement, lorsque deux processus sont reliés par un tube, si celui qui est à droite du tube (grep dans notre exemple) se termine avant celui de gauche (dmesg), ce dernier reçoit un signal SIGPIPE
(broken pipe) et par défaut, se termine lui-même.

Cependant, lors de la réplication du comportement d’un tube entre deux commandes en Python, il y a un problème : le stdout du premier processus est ouvert à la fois dans le script parent et dans l’entrée standard de l’autre processus. De cette façon, même si le processus grep se termine, le tube restera toujours ouvert dans le processus de l’appelant (notre script), donc le premier processus ne recevra jamais le signal SIGPIPE. C’est pourquoi nous devons fermer le flux stdout du premier processus dans notre script principal
après avoir lancé le second.

La dernière chose que nous avons faite a été d’appeler la méthode communicate() sur l’objet grep. Cette méthode peut être utilisée pour transmettre éventuellement des données d’entrée à un processus ; Il attend la fin du processus et renvoie un tuple où le premier membre est le processus stdout (qui est référencé par la variable de sortie) et le second le processus stderr.

Conclusions

Dans ce tutoriel, nous avons vu la manière recommandée de générer des processus externes avec Python en utilisant le module subprocess et la fonction run. L’utilisation de cette fonction devrait suffire dans la majorité des cas ; Cependant, lorsqu’un niveau de flexibilité plus élevé est nécessaire, il faut utiliser directement la classe Popen. Comme toujours, nous vous suggérons de jeter un coup d’œil à la documentation du sous-processus
pour un aperçu complet de la signature des fonctions et des classes disponibles dans
le module.

Articles connexes: