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...)
Ne jamais faire ça :
# 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
Code illustratif — non implĂ©mentĂ© dans le projet

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 :

  1. Vérifier returncode avant de toucher à stdout
  2. Loguer stderr en cas d'erreur (c'est lĂ  que pyCycloneFlow met ses messages)
  3. Parser stdout avec json.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Ă©.

Chapitre précédent Phase 5 Terminée : Retour au sommaire