5.2 subprocess â appeler des processus externes
En PHP on a exec(), shell_exec(), proc_open(). Python a le module subprocess â plus puissant, plus sĂ»r, et qui donne un contrĂŽle prĂ©cis sur stdin/stdout/stderr et le code de retour. C'est le module qu'on utilise quand Python doit orchestrer d'autres outils.
1. subprocess.run() â l'essentiel
subprocess.run() est le point d'entrée principal depuis Python 3.5. Il lance un processus, attend qu'il se termine, et retourne un objet avec tout ce qu'on veut savoir.
import subprocess
# Lancer une commande simple
result = subprocess.run(["ls", "-l"])
# result.returncode â 0 (succĂšs) ou non-zero (erreur)
# Capturer la sortie â sans capture_output, stdout va dans le terminal
result = subprocess.run(
["py", "main.py", "--pip", "scanner.txt", "-o", "-"],
capture_output=True, # capture stdout ET stderr
text=True # décode les bytes en str automatiquement
)
print(result.returncode) # 0
print(result.stdout) # le JSON SBOM
print(result.stderr) # les logs (timer, warnings...)
capture_output=TrueĂ©quivaut Ăstdout=PIPE, stderr=PIPEâ sans ça, la sortie va directement dans le terminaltext=TrueĂ©quivaut Ăencoding="utf-8"â sans ça,stdoutest desbytes- Le processus est attendu (bloquant) â
run()ne rend la main qu'Ă la fin
# shell=True ouvre la porte aux injections de commande
subprocess.run(f"py main.py --pip {user_input}", shell=True) # DANGEREUX
# Toujours passer une liste â les arguments sont isolĂ©s
subprocess.run(["py", "main.py", "--pip", user_input]) # sûr
Avec shell=True, si user_input contient ; rm -rf /, tu as un problĂšme. Avec une liste, chaque Ă©lĂ©ment est un argument littĂ©ral â pas d'interprĂ©tation shell.
2. Lire du JSON depuis un subprocess
pyCycloneFlow Ă©crit du JSON sur stdout. De l'autre cĂŽtĂ©, Python lit result.stdout â une string â et la parse avec json.loads().
import subprocess, json
def appeler_pycycloneflow(pip_path: str) -> dict:
result = subprocess.run(
["py", "main.py", "--pip", pip_path, "-o", "-"],
capture_output=True,
text=True,
cwd="D:/flo26/pyFlow/pyCycloneFlow" # répertoire de travail
)
if result.returncode != 0:
raise RuntimeError(f"pyCycloneFlow a échoué : {result.stderr}")
return json.loads(result.stdout) # str â dict Python
# Utilisation
sbom = appeler_pycycloneflow("scanner.txt")
print(sbom["specVersion"]) # "1.7"
print(len(sbom["components"])) # 3
Cette fonction appeler_pycycloneflow() montre comment Python appellerait pyCycloneFlow depuis Python. Dans notre projet, ce rĂŽle appartient Ă flowscope (Go) â voir section 3. On ne l'ajoute pas dans pyCycloneFlow/ car ce serait du code mort ; en revanche les tests d'intĂ©gration de 5.1 font exactement la mĂȘme chose pour valider le contrat.
Le pattern est toujours le mĂȘme :
- Vérifier
returncodeavant de toucher Ăstdout - Loguer
stderren cas d'erreur (c'est lĂ que pyCycloneFlow met ses messages) - Parser
stdoutavecjson.loads()
check=True â lever une exception automatiquement :
try:
result = subprocess.run(
["py", "main.py", "--pip", pip_path, "-o", "-"],
capture_output=True, text=True,
check=True # lĂšve CalledProcessError si returncode != 0
)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Erreur (exit {e.returncode}) : {e.stderr}")
raise
check=True Ă©vite d'oublier de tester le returncode manuellement â c'est la version "dĂ©fensive".
3. Comment flowscope (Go) nous appelle
Dans flowscope, c'est Go qui joue le rĂŽle de l'appelant â pas Python. Go a os/exec qui fonctionne exactement selon le mĂȘme contrat. Voici ce que tu Ă©criras dans flowscope :
// flowscope â handler Go (pour comprendre l'interop)
import (
"encoding/json"
"os/exec"
)
type SBOMComponent struct {
Name string `json:"name"`
Version string `json:"version"`
Purl string `json:"purl"`
}
type SBOM struct {
SpecVersion string `json:"specVersion"`
Components []SBOMComponent `json:"components"`
}
func scanHandler(w http.ResponseWriter, r *http.Request) error {
pipPath := r.FormValue("pip_path")
// Appel de pyCycloneFlow â mĂȘme logique qu'en Python
cmd := exec.Command("py", "main.py", "--pip", pipPath, "-o", "-")
cmd.Dir = "/srv/pyCycloneFlow" // répertoire de travail
out, err := cmd.Output() // capture stdout, attend la fin
if err != nil {
return fmt.Errorf("pyCycloneFlow a échoué : %w", err)
}
// Parser le JSON reçu sur stdout
var sbom SBOM
if err := json.Unmarshal(out, &sbom); err != nil {
return fmt.Errorf("JSON invalide : %w", err)
}
return app.Render(w, "results.html", map[string]any{
"SBOM": sbom,
"Count": len(sbom.Components),
})
}
cmd.Output() en Go capture uniquement stdout â stderr va dans le terminal du serveur (les logs de pyCycloneFlow). C'est exactement la sĂ©paration qu'on a mise en place cĂŽtĂ© Python.
4. L'architecture complĂšte
Vue d'ensemble de ce qui se passe quand flowscope scanne un projet :
Utilisateur
â
⌠POST /scan (pip_path=/srv/monprojet/requirements.txt)
flowscope (Go)
â
ââ filepath.Walk("/srv/monprojet") â trouve requirements.txt, go.mod...
â
⌠exec.Command("py", "main.py", "--pip", path, "-o", "-")
pyCycloneFlow (Python)
â
ââ load_file() â lit requirements.txt
ââ parse_pip() â gĂ©nĂšre Component[]
ââ SBOM.to_cyclonedx_json() â dict
ââ json.dump(data, sys.stdout) â JSON sur stdout
ââ sys.exit(0)
â
⌠stdout : JSON CycloneDX v1.7
flowscope (Go)
â
ââ cmd.Output() â []byte
ââ json.Unmarshal() â struct SBOM
ââ app.Render("results.html", sbom)
â
⌠Page HTML avec le tableau des composants
Utilisateur
pyCycloneFlow ne sait pas qui l'appelle â Go, Python, bash, peu importe. Il respecte le contrat (stdout = JSON, stderr = logs, exit 0/1) et ça suffit. C'est ça l'interopĂ©rabilitĂ©.