Arrêt de génération et reclassification optionnelle depuis l'interface
- POST /api/stop : SIGTERM au pipeline (groupe isolé via start_new_session), SIGKILL du groupe après 15 s ; forwardé au worker en mode deux machines - « Régénérer » n'implique plus la reclassification : le DTM en cache est réutilisé tant que la méthode ne change pas ; nouvelle case « Reclasser le sol » (--force-classification) et rappel de la classification existante - Tâche marquée « Interrompu » (returncode négatif ou nul) au lieu d'Échec
This commit is contained in:
@ -18,12 +18,19 @@ le téléchargement IGN). Endpoints :
|
||||
demandées, option regenerate=true pour inclure les
|
||||
tuiles complètes
|
||||
POST /api/generate → télécharge (géoplateforme IGN) puis traite des cellules
|
||||
(option regenerate=true ajoute --force --force-classification ;
|
||||
option ground_class choisit la méthode de classification du sol ;
|
||||
(option regenerate=true ajoute --force : les visualisations
|
||||
sont refaites mais la classification existante est
|
||||
conservée ; option reclassify=true ajoute en plus
|
||||
--force-classification pour relancer la classification
|
||||
du sol — sinon changer simplement ground_class
|
||||
reclassifie déjà les tuiles dont la méthode diffère ;
|
||||
option all_missing=true traite toutes les dalles présentes
|
||||
dans input/ qui manquent les visualisations demandées, sans
|
||||
téléchargement ; sans --force, le pipeline ne génère que
|
||||
les visualisations manquantes des tuiles existantes)
|
||||
POST /api/stop → arrête la génération en cours (SIGTERM au pipeline, qui
|
||||
nettoie ses workers et processus PDAL ; SIGKILL du groupe
|
||||
en repli après 15 s)
|
||||
POST /api/export → assemble des dalles adjacentes en une image (PNG/JPEG/
|
||||
WebP) ou un PDF multi-couches, consultable sur téléphone
|
||||
(module export.py — mosaïque sans couture, habillage
|
||||
@ -52,6 +59,7 @@ import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@ -444,7 +452,15 @@ class PreviewRequest(BaseModel):
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
tiles: list = Field(..., description="liste [col, row] (entiers km L93)")
|
||||
regenerate: bool = Field(False, description="Régénérer les tuiles déjà générées")
|
||||
regenerate: bool = Field(False, description="Régénérer les tuiles déjà générées "
|
||||
"(visualisations refaites, classification conservée)")
|
||||
reclassify: bool = Field(False,
|
||||
description="Relancer la classification du sol même si "
|
||||
"la méthode demandée est déjà en cache "
|
||||
"(--force-classification). Défaut : conserver "
|
||||
"la classification existante ; choisir une "
|
||||
"autre méthode reclassifie de toute façon "
|
||||
"les tuiles concernées")
|
||||
ground_class: str = Field("ign",
|
||||
description="Méthode de classification du sol : "
|
||||
"auto, ign, smrf, csf")
|
||||
@ -907,16 +923,20 @@ def preview(req: PreviewRequest):
|
||||
return {"count": len(todo), "capped": capped, "cells": todo}
|
||||
|
||||
|
||||
def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False, ign_classes="sol", viz=None):
|
||||
def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False, ign_classes="sol", viz=None, reclassify=False):
|
||||
"""Commande de génération : téléchargement IGN + traitement des fichiers.
|
||||
|
||||
La classification du sol est choisie via `ground_class` (défaut : "ign",
|
||||
pré-classification IGN ; le pipeline bascule sur SMRF si un fichier ne la
|
||||
contient pas). Avec ign_classes, on choisit les classes LAS extraites
|
||||
pour le MNT (mode pur, ex. "sol,unclassified" pour combler les trous
|
||||
sans retouche). Avec regenerate=True, force la reclassification et la
|
||||
régénération des visualisations des tuiles déjà présentes. Avec
|
||||
bare_earth=True, le DTM est ramené au retour le plus bas de chaque
|
||||
sans retouche). Avec regenerate=True, les visualisations des tuiles déjà
|
||||
présentes sont refaites (--force) mais leur classification est conservée :
|
||||
le pipeline réutilise le DTM en cache quand la méthode ne change pas.
|
||||
Avec reclassify=True, --force-classification relance la classification
|
||||
même à méthode inchangée (sinon, choisir une méthode différente via
|
||||
ground_class/ign_classes reclassifie déjà les tuiles concernées).
|
||||
Avec bare_earth=True, le DTM est ramené au retour le plus bas de chaque
|
||||
cellule (sol nu). Avec viz, on choisit les visualisations générées
|
||||
(noms d'étapes du pipeline, ex. ["aspect", "wavelet", "slope"] ;
|
||||
défaut : aspect).
|
||||
@ -933,7 +953,9 @@ def _build_command(tiles, regenerate=False, ground_class="ign", bare_earth=False
|
||||
if bare_earth:
|
||||
cmd += ["--bare-earth"]
|
||||
if regenerate:
|
||||
cmd += ["--force", "--force-classification"]
|
||||
cmd += ["--force"]
|
||||
if reclassify:
|
||||
cmd += ["--force-classification"]
|
||||
if os.environ.get("LIDAR_GPU", "") == "1":
|
||||
cmd += ["-g", "all", "-w", os.environ.get("LIDAR_WORKERS", "2")]
|
||||
# Carte régénérée après chaque tuile terminée : la webapp l'affiche en
|
||||
@ -989,7 +1011,7 @@ def generate(req: GenerateRequest):
|
||||
proc = _job["proc"]
|
||||
if proc is not None and proc.poll() is None:
|
||||
raise HTTPException(409, "une génération est déjà en cours")
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate, ground_class=req.ground_class, bare_earth=req.bare_earth, ign_classes=req.ign_classes, viz=viz)
|
||||
cmd = _build_command(tiles, regenerate=req.regenerate, ground_class=req.ground_class, bare_earth=req.bare_earth, ign_classes=req.ign_classes, viz=viz, reclassify=req.reclassify)
|
||||
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_fh = open(LOG_FILE, "w", encoding="utf-8")
|
||||
# Nouveau run : journal d'événements remis à zéro (les tuiles affichées
|
||||
@ -998,8 +1020,13 @@ def generate(req: GenerateRequest):
|
||||
reset_events(OUTPUT_DIR)
|
||||
_job.update({"proc": None, "started": time.time(), "returncode": None, "cmd": cmd, "finished": None})
|
||||
_save_job_state()
|
||||
# start_new_session : le pipeline et ses workers/PDAL forment leur
|
||||
# propre groupe de processus — /api/stop peut le tuer en bloc sans
|
||||
# toucher la webapp, et le gestionnaire SIGTERM du pipeline (killpg)
|
||||
# reste confiné à son groupe.
|
||||
p = subprocess.Popen(cmd, stdout=log_fh, stderr=subprocess.STDOUT,
|
||||
cwd="/app" if Path("/app").exists() else None)
|
||||
cwd="/app" if Path("/app").exists() else None,
|
||||
start_new_session=True)
|
||||
|
||||
def _watch():
|
||||
rc = p.wait()
|
||||
@ -1014,6 +1041,46 @@ def generate(req: GenerateRequest):
|
||||
return {"demarré": True, "tuiles": len(tiles), "commande": " ".join(cmd)}
|
||||
|
||||
|
||||
@app.post("/api/stop", dependencies=[Depends(_require_token),
|
||||
Depends(_require_lan_for_generation)])
|
||||
def stop_generation():
|
||||
"""Arrête la génération en cours.
|
||||
|
||||
SIGTERM au pipeline : son gestionnaire de signal nettoie ses workers et
|
||||
ses processus PDAL (killpg sur son propre groupe, cf. cli.py). Si le
|
||||
processus ne meurt pas dans les 15 s (worker bloqué), SIGKILL du groupe
|
||||
entier. L'état final (returncode négatif) est enregistré par le fil de
|
||||
surveillance du job.
|
||||
"""
|
||||
if GENERATION_URL:
|
||||
# Webapp légère : l'arrêt concerne le pipeline de la machine distante.
|
||||
return _proxy_api("POST", "/api/stop")
|
||||
with _job_lock:
|
||||
proc = _job["proc"]
|
||||
if proc is None or proc.poll() is not None:
|
||||
raise HTTPException(409, "aucune génération en cours")
|
||||
try:
|
||||
proc.terminate() # SIGTERM → nettoyage propre du pipeline
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _escalate():
|
||||
try:
|
||||
proc.wait(timeout=15)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except (OSError, ProcessLookupError):
|
||||
try:
|
||||
proc.kill()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_escalate, daemon=True).start()
|
||||
logger.info("Arrêt de la génération demandé (SIGTERM au pipeline)")
|
||||
return {"arrêt": "demandé"}
|
||||
|
||||
|
||||
# --- Export multi-dalles (mosaïque image/PDF pour téléphone) ---------------
|
||||
_export_lock = threading.Lock()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user