Integrate SBAS workflows and redesign task center
This commit is contained in:
@@ -273,6 +273,21 @@ class Settings(BaseSettings):
|
||||
"site-packages/isce/applications/stripmapApp.py"
|
||||
)
|
||||
ISCE2_PIPELINE_SCRIPT: str = ""
|
||||
LANDSAR_ENABLED: bool = True
|
||||
LANDSAR_HOME: str = ""
|
||||
LANDSAR_CONSOLE_EXE: str = ""
|
||||
LANDSAR_WORK_ROOT: str = ""
|
||||
LANDSAR_LICENSE_MODE: str = "netVersion"
|
||||
LANDSAR_LICENSE_HOST: str = "127.0.0.1"
|
||||
LANDSAR_LICENSE_PORT: int = 6666
|
||||
LANDSAR_CONFIG_ROW: str = ""
|
||||
LANDSAR_CONFIG_AUTO_WRITE: bool = True
|
||||
LANDSAR_AUTH_SERVER_EXE: str = ""
|
||||
LANDSAR_AUTH_SERVER_AUTO_START: bool = True
|
||||
LANDSAR_AUTH_SERVER_HOST: str = "127.0.0.1"
|
||||
LANDSAR_AUTH_SERVER_PORT: int = 6666
|
||||
LANDSAR_DEM_PATH: str = ""
|
||||
LANDSAR_DINSAR_TIMEOUT_SECONDS: int = 43200
|
||||
PYINT_ENABLED: bool = False
|
||||
PYINT_WSL_DISTRO: str = ""
|
||||
PYINT_WSL_PYTHON: str = ""
|
||||
@@ -328,13 +343,24 @@ class Settings(BaseSettings):
|
||||
GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT: str = ""
|
||||
GAMMA_SBAS_SOURCE_ROOTS: str = ""
|
||||
GAMMA_SBAS_ORBIT_ROOTS: str = ""
|
||||
GAMMA_SBAS_DEM_PATH: str = ""
|
||||
GAMMA_SBAS_DEFAULT_RLKS: int = 8
|
||||
GAMMA_SBAS_DEFAULT_AZLKS: int = 8
|
||||
GAMMA_SBAS_DEFAULT_MB_MODE: int = 0
|
||||
GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW: int = 16
|
||||
GAMMA_SBAS_AUTO_APPROVE_ITAB: bool = True
|
||||
GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO: float = 0.30
|
||||
GAMMA_SBAS_STEP_TIMEOUT_SECONDS: int = 43200
|
||||
GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS: int = 172800
|
||||
LANDSAR_SBAS_ENABLED: bool = True
|
||||
LANDSAR_SBAS_WORK_ROOT: str = ""
|
||||
LANDSAR_SBAS_PRODUCT_ROOT: str = ""
|
||||
LANDSAR_SBAS_DEM_PATH: str = ""
|
||||
LANDSAR_SBAS_SOURCE_ROOTS: str = ""
|
||||
LANDSAR_SBAS_TIMEOUT_SECONDS: int = 172800
|
||||
LANDSAR_SBAS_MIN_SCENES: int = 3
|
||||
LANDSAR_SBAS_PROID: str = "280039"
|
||||
LANDSAR_SBAS_PROCESS_NAME: str = "SBAS Stream"
|
||||
|
||||
JOB_WORKER_HEALTH_TIMEOUT: int = 60
|
||||
JOB_WORKER_JOB_HEARTBEAT_INTERVAL: float = 5.0
|
||||
@@ -491,6 +517,42 @@ class Settings(BaseSettings):
|
||||
"ISCE2_PIPELINE_SCRIPT",
|
||||
_windows_path_to_wsl_mount(local_pipeline),
|
||||
)
|
||||
if not self.LANDSAR_HOME:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"LANDSAR_HOME",
|
||||
os.path.join(project_root, "third_party", "LandSAR", "dist", "LandSAR_Portable"),
|
||||
)
|
||||
if not self.LANDSAR_CONSOLE_EXE:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"LANDSAR_CONSOLE_EXE",
|
||||
os.path.join(self.LANDSAR_HOME, "InSAR_Console.exe"),
|
||||
)
|
||||
if not self.LANDSAR_WORK_ROOT:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"LANDSAR_WORK_ROOT",
|
||||
os.path.join(self.RESULT_PUBLISH_ROOT, "landsar_work"),
|
||||
)
|
||||
if not self.LANDSAR_AUTH_SERVER_EXE:
|
||||
auth_server = os.path.join(
|
||||
project_root,
|
||||
"third_party",
|
||||
"LandSAR",
|
||||
"tools",
|
||||
"_portable_release",
|
||||
"LandSAR_auth_tools_win64",
|
||||
"landsar_net_auth_server.exe",
|
||||
)
|
||||
object.__setattr__(self, "LANDSAR_AUTH_SERVER_EXE", auth_server)
|
||||
if not self.LANDSAR_DEM_PATH:
|
||||
landsar_dem = (
|
||||
_clean_path_text(self.PYINT_PREPARED_DEM_PATH)
|
||||
or _clean_path_text(self.ISCE2_DEM_PATH)
|
||||
or _clean_path_text(self.IDL_DINSAR_DEM_BASE_FILE)
|
||||
)
|
||||
object.__setattr__(self, "LANDSAR_DEM_PATH", landsar_dem)
|
||||
if not self.WSL_DISTRO:
|
||||
fallback_distro = str(
|
||||
self.ISCE2_WSL_DISTRO
|
||||
@@ -667,6 +729,16 @@ class Settings(BaseSettings):
|
||||
)
|
||||
if not self.GAMMA_SBAS_ENV_SCRIPT:
|
||||
object.__setattr__(self, "GAMMA_SBAS_ENV_SCRIPT", self.PYINT_GAMMA_ENV_SCRIPT)
|
||||
if not self.GAMMA_SBAS_DEM_PATH:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"GAMMA_SBAS_DEM_PATH",
|
||||
self.LANDSAR_SBAS_DEM_PATH
|
||||
or self.LANDSAR_DEM_PATH
|
||||
or self.IDL_DINSAR_DEM_BASE_FILE
|
||||
or self.ISCE2_DEM_PATH
|
||||
or self.PYINT_PREPARED_DEM_PATH,
|
||||
)
|
||||
if not self.GAMMA_SBAS_WORK_ROOT:
|
||||
object.__setattr__(
|
||||
self,
|
||||
@@ -714,6 +786,11 @@ class Settings(BaseSettings):
|
||||
"GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW",
|
||||
max(1, int(self.GAMMA_SBAS_DEFAULT_REFERENCE_WINDOW or 16)),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO",
|
||||
min(1.0, max(0.0, float(self.GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO or 0.30))),
|
||||
)
|
||||
object.__setattr__(
|
||||
self,
|
||||
"GAMMA_SBAS_STEP_TIMEOUT_SECONDS",
|
||||
@@ -724,6 +801,26 @@ class Settings(BaseSettings):
|
||||
"GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS",
|
||||
max(self.GAMMA_SBAS_STEP_TIMEOUT_SECONDS, int(self.GAMMA_SBAS_WORKFLOW_TIMEOUT_SECONDS or 172800)),
|
||||
)
|
||||
if not self.LANDSAR_SBAS_WORK_ROOT:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"LANDSAR_SBAS_WORK_ROOT",
|
||||
os.path.join(self.LANDSAR_WORK_ROOT or os.path.join(self.RESULT_PUBLISH_ROOT, "landsar_work"), "sbas"),
|
||||
)
|
||||
if not self.LANDSAR_SBAS_PRODUCT_ROOT:
|
||||
object.__setattr__(
|
||||
self,
|
||||
"LANDSAR_SBAS_PRODUCT_ROOT",
|
||||
os.path.join(self.TIMESERIES_PRODUCT_DIR, "sbas_landsar"),
|
||||
)
|
||||
if not self.LANDSAR_SBAS_DEM_PATH:
|
||||
object.__setattr__(self, "LANDSAR_SBAS_DEM_PATH", self.LANDSAR_DEM_PATH)
|
||||
if not self.LANDSAR_SBAS_SOURCE_ROOTS:
|
||||
object.__setattr__(self, "LANDSAR_SBAS_SOURCE_ROOTS", self.LANDSAR_WORK_ROOT or r"D:\LandSAR_Work")
|
||||
object.__setattr__(self, "LANDSAR_SBAS_TIMEOUT_SECONDS", max(60, int(self.LANDSAR_SBAS_TIMEOUT_SECONDS or 172800)))
|
||||
object.__setattr__(self, "LANDSAR_SBAS_MIN_SCENES", max(3, int(self.LANDSAR_SBAS_MIN_SCENES or 3)))
|
||||
object.__setattr__(self, "LANDSAR_SBAS_PROID", _clean_path_text(self.LANDSAR_SBAS_PROID) or "280039")
|
||||
object.__setattr__(self, "LANDSAR_SBAS_PROCESS_NAME", str(self.LANDSAR_SBAS_PROCESS_NAME or "").strip() or "SBAS Stream")
|
||||
if self.TIMESERIES_ENABLED:
|
||||
if not self.TIMESERIES_WSL_DISTRO:
|
||||
object.__setattr__(self, "TIMESERIES_WSL_DISTRO", self.WSL_DISTRO or self.ISCE2_WSL_DISTRO)
|
||||
@@ -856,10 +953,15 @@ class Settings(BaseSettings):
|
||||
os.makedirs(settings.PYINT_WORK_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.PYINT_OUTPUT_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.PYINT_DEM_ROOT, exist_ok=True)
|
||||
if settings.LANDSAR_WORK_ROOT:
|
||||
os.makedirs(settings.LANDSAR_WORK_ROOT, exist_ok=True)
|
||||
if settings.GAMMA_SBAS_ENABLED:
|
||||
os.makedirs(settings.GAMMA_SBAS_WORK_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.GAMMA_SBAS_PRODUCT_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.GAMMA_SBAS_SCRIPT_TEMPLATE_ROOT, exist_ok=True)
|
||||
if settings.LANDSAR_SBAS_ENABLED:
|
||||
os.makedirs(settings.LANDSAR_SBAS_WORK_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.LANDSAR_SBAS_PRODUCT_ROOT, exist_ok=True)
|
||||
if settings.TIMESERIES_ENABLED and settings.TIMESERIES_WORK_ROOT:
|
||||
os.makedirs(settings.TIMESERIES_WORK_ROOT, exist_ok=True)
|
||||
|
||||
@@ -1026,6 +1128,7 @@ def validate_runtime_config() -> dict[str, Any]:
|
||||
_check_path(label="ORBIT_POOL_ISCE2", value=settings.ORBIT_POOL_ISCE2, errors=errors, warnings=warnings, expect_file=False)
|
||||
_check_path(label="RESULT_PUBLISH_ROOT", value=settings.RESULT_PUBLISH_ROOT, errors=errors, warnings=warnings, expect_file=False)
|
||||
_check_path(label="DINSAR_PRODUCT_DIR", value=settings.DINSAR_PRODUCT_DIR, errors=errors, warnings=warnings, expect_file=False)
|
||||
_check_path(label="LANDSAR_WORK_ROOT", value=settings.LANDSAR_WORK_ROOT, errors=errors, warnings=warnings, expect_file=False)
|
||||
_check_path(
|
||||
label="TIMESERIES_PRODUCT_DIR",
|
||||
value=settings.TIMESERIES_PRODUCT_DIR,
|
||||
@@ -1183,6 +1286,13 @@ def validate_runtime_config() -> dict[str, Any]:
|
||||
warnings=warnings,
|
||||
expect_file=False,
|
||||
)
|
||||
_check_path(
|
||||
label="GAMMA_SBAS_DEM_PATH",
|
||||
value=settings.GAMMA_SBAS_DEM_PATH,
|
||||
errors=errors,
|
||||
warnings=warnings,
|
||||
expect_file=True,
|
||||
)
|
||||
|
||||
if settings.ISCE2_ENABLED or settings.PYINT_ENABLED:
|
||||
info.append(
|
||||
|
||||
@@ -710,7 +710,7 @@ class Isce2Engine(DinsarEngine):
|
||||
available = True
|
||||
else:
|
||||
critical_failed = [check for check in report.checks if not check.ok and not check.skipped]
|
||||
status = "degraded" if critical_failed else "unavailable"
|
||||
status = "unavailable" if critical_failed else "degraded"
|
||||
available = False
|
||||
|
||||
return EngineAvailability(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -687,7 +687,7 @@ class PyintEngine(DinsarEngine):
|
||||
available = True
|
||||
else:
|
||||
critical_failed = [check for check in report.checks if not check.ok and not check.skipped]
|
||||
status = "degraded" if critical_failed else "unavailable"
|
||||
status = "unavailable" if critical_failed else "degraded"
|
||||
available = False
|
||||
return EngineAvailability(
|
||||
engine_code=self.engine_code,
|
||||
|
||||
+2
-2
@@ -192,7 +192,7 @@ async def lifespan(app: FastAPI):
|
||||
if ps_catalog_bootstrap.get("error"):
|
||||
print(f">>> [Timeseries Catalog] Startup bootstrap failed: {ps_catalog_bootstrap['error']}")
|
||||
print(
|
||||
">>> [Gamma SBAS Catalog] root={0} runs={1} db={2} rebuild={3} rebuilt={4}".format(
|
||||
">>> [SBAS Catalog] root={0} runs={1} db={2} rebuild={3} rebuilt={4}".format(
|
||||
sbas_catalog_bootstrap.get("storage_root") or "?",
|
||||
sbas_catalog_bootstrap.get("manifest_count", 0),
|
||||
sbas_catalog_bootstrap.get("db_count", 0),
|
||||
@@ -201,7 +201,7 @@ async def lifespan(app: FastAPI):
|
||||
)
|
||||
)
|
||||
if sbas_catalog_bootstrap.get("error"):
|
||||
print(f">>> [Gamma SBAS Catalog] Startup bootstrap failed: {sbas_catalog_bootstrap['error']}")
|
||||
print(f">>> [SBAS Catalog] Startup bootstrap failed: {sbas_catalog_bootstrap['error']}")
|
||||
print(
|
||||
">>> [Pairing] status={0} scenes={1} pairs={2} dirty={3} metric={4} rebuild={5}".format(
|
||||
pairing_bootstrap.get("status") or "?",
|
||||
|
||||
@@ -40,6 +40,12 @@ PYINT_PRODUCTION_JOB_MAX_ATTEMPTS = read_int_env(
|
||||
minimum=1,
|
||||
maximum=10,
|
||||
)
|
||||
LANDSAR_PRODUCTION_JOB_MAX_ATTEMPTS = read_int_env(
|
||||
"LANDSAR_PRODUCTION_JOB_MAX_ATTEMPTS",
|
||||
1,
|
||||
minimum=1,
|
||||
maximum=10,
|
||||
)
|
||||
|
||||
|
||||
class RunJobRequest(BaseModel):
|
||||
@@ -243,7 +249,12 @@ async def submit_run(
|
||||
"extra": dict(req.extra or {}),
|
||||
}
|
||||
|
||||
from ..services.job_handlers import JOB_TYPE_IDL_RUN_DINSAR, JOB_TYPE_ISCE2_RUN, JOB_TYPE_PYINT_RUN
|
||||
from ..services.job_handlers import (
|
||||
JOB_TYPE_IDL_RUN_DINSAR,
|
||||
JOB_TYPE_ISCE2_RUN,
|
||||
JOB_TYPE_LANDSAR_RUN,
|
||||
JOB_TYPE_PYINT_RUN,
|
||||
)
|
||||
create_managed_run = False
|
||||
normalized_extra = dict(payload["extra"])
|
||||
|
||||
@@ -252,7 +263,7 @@ async def submit_run(
|
||||
job_type = JOB_TYPE_IDL_RUN_DINSAR
|
||||
max_attempts = DINSAR_PRODUCTION_JOB_MAX_ATTEMPTS
|
||||
create_managed_run = True
|
||||
elif req.engine_code in {"isce2", "pyint"}:
|
||||
elif req.engine_code in {"isce2", "pyint", "landsar"}:
|
||||
if hasattr(engine, "normalize_extra"):
|
||||
try:
|
||||
payload["extra"] = engine.normalize_extra(payload["extra"])
|
||||
@@ -263,10 +274,14 @@ async def submit_run(
|
||||
job_type = JOB_TYPE_ISCE2_RUN
|
||||
max_attempts = ISCE2_PRODUCTION_JOB_MAX_ATTEMPTS
|
||||
create_managed_run = True
|
||||
else:
|
||||
elif req.engine_code == "pyint":
|
||||
job_type = JOB_TYPE_PYINT_RUN
|
||||
max_attempts = PYINT_PRODUCTION_JOB_MAX_ATTEMPTS
|
||||
create_managed_run = True
|
||||
else:
|
||||
job_type = JOB_TYPE_LANDSAR_RUN
|
||||
max_attempts = LANDSAR_PRODUCTION_JOB_MAX_ATTEMPTS
|
||||
create_managed_run = True
|
||||
if validation_summary is not None:
|
||||
validated_task_count = validation_summary.get("task_count", 0)
|
||||
payload["extra"].update(
|
||||
|
||||
@@ -4,18 +4,204 @@ import asyncio
|
||||
import mimetypes
|
||||
import subprocess
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
from sqlalchemy import String, cast, or_, select
|
||||
|
||||
from .. import database
|
||||
from ..config import settings
|
||||
from ..models import AuthUserORM, SystemJobORM, SystemTaskORM, TaskLogORM
|
||||
from ..services.job_queue_service import job_queue_service
|
||||
from ..services.landsar_sbas_service import landsar_sbas_service
|
||||
from ..services.sbas_insar_production_service import sbas_insar_production_service
|
||||
from ..services.task_service import task_service
|
||||
from .dependencies import _require_admin
|
||||
|
||||
|
||||
router = APIRouter(prefix="/sbas-insar-production", tags=["sbas-insar-production"])
|
||||
|
||||
|
||||
def _new_session():
|
||||
if database.AsyncSessionLocal is None:
|
||||
database.init_db()
|
||||
if database.AsyncSessionLocal is None:
|
||||
raise RuntimeError("Database session factory is not initialized.")
|
||||
return database.AsyncSessionLocal()
|
||||
|
||||
|
||||
def _dt(value):
|
||||
return value.isoformat() if value else None
|
||||
|
||||
|
||||
def _task_payload_matches(run_id: str):
|
||||
return or_(
|
||||
cast(SystemTaskORM.params, String).ilike(f"%{run_id}%"),
|
||||
SystemTaskORM.task_name.ilike(f"%{run_id}%"),
|
||||
)
|
||||
|
||||
|
||||
def _job_payload_matches(run_id: str, task_ids: list[str]):
|
||||
conditions = [
|
||||
cast(SystemJobORM.payload, String).ilike(f"%{run_id}%"),
|
||||
SystemJobORM.workflow_run_id == run_id,
|
||||
]
|
||||
if task_ids:
|
||||
conditions.append(SystemJobORM.task_id.in_(task_ids))
|
||||
return or_(*conditions)
|
||||
|
||||
|
||||
async def _load_run_background_activity(run_id: str) -> dict:
|
||||
try:
|
||||
async with _new_session() as db:
|
||||
task_result = await db.execute(
|
||||
select(SystemTaskORM)
|
||||
.where(_task_payload_matches(run_id))
|
||||
.order_by(SystemTaskORM.created_at.desc(), SystemTaskORM.id.desc())
|
||||
.limit(10)
|
||||
)
|
||||
tasks = list(task_result.scalars().all())
|
||||
task_ids = [
|
||||
str(task.task_id or "").strip()
|
||||
for task in tasks
|
||||
if str(task.task_id or "").strip()
|
||||
]
|
||||
job_result = await db.execute(
|
||||
select(SystemJobORM)
|
||||
.where(_job_payload_matches(run_id, task_ids))
|
||||
.order_by(SystemJobORM.created_at.desc(), SystemJobORM.id.desc())
|
||||
.limit(10)
|
||||
)
|
||||
jobs = list(job_result.scalars().all())
|
||||
if not task_ids:
|
||||
task_ids = sorted({
|
||||
str(job.task_id or "").strip()
|
||||
for job in jobs
|
||||
if str(job.task_id or "").strip()
|
||||
})
|
||||
logs = []
|
||||
if task_ids:
|
||||
log_result = await db.execute(
|
||||
select(TaskLogORM)
|
||||
.where(TaskLogORM.task_id.in_(task_ids))
|
||||
.order_by(TaskLogORM.timestamp.desc(), TaskLogORM.id.desc())
|
||||
.limit(20)
|
||||
)
|
||||
logs = list(log_result.scalars().all())
|
||||
except Exception as exc:
|
||||
return {
|
||||
"schema": "insar.sbas-background-activity/v1",
|
||||
"error": str(exc),
|
||||
"tasks": [],
|
||||
"jobs": [],
|
||||
"task_logs": [],
|
||||
"active": False,
|
||||
}
|
||||
|
||||
active_task_statuses = {"PENDING", "RUNNING"}
|
||||
active_job_statuses = {"READY", "PENDING", "RUNNING", "RETRY"}
|
||||
return {
|
||||
"schema": "insar.sbas-background-activity/v1",
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": task.task_id,
|
||||
"task_type": task.task_type,
|
||||
"task_name": task.task_name,
|
||||
"status": task.status,
|
||||
"progress": task.progress,
|
||||
"message": task.message,
|
||||
"created_at": _dt(task.created_at),
|
||||
"updated_at": _dt(task.updated_at),
|
||||
"started_at": _dt(task.started_at),
|
||||
"ended_at": _dt(task.ended_at),
|
||||
}
|
||||
for task in tasks
|
||||
],
|
||||
"jobs": [
|
||||
{
|
||||
"job_id": job.job_id,
|
||||
"job_type": job.job_type,
|
||||
"status": job.status,
|
||||
"attempts": job.attempts,
|
||||
"max_attempts": job.max_attempts,
|
||||
"locked_by": job.locked_by,
|
||||
"locked_at": _dt(job.locked_at),
|
||||
"heartbeat_at": _dt(job.heartbeat_at),
|
||||
"created_at": _dt(job.created_at),
|
||||
"updated_at": _dt(job.updated_at),
|
||||
"started_at": _dt(job.started_at),
|
||||
"finished_at": _dt(job.finished_at),
|
||||
"last_error": job.last_error,
|
||||
"task_id": job.task_id,
|
||||
}
|
||||
for job in jobs
|
||||
],
|
||||
"task_logs": [
|
||||
{
|
||||
"task_id": log.task_id,
|
||||
"level": log.log_level,
|
||||
"message": log.message,
|
||||
"timestamp": _dt(log.timestamp),
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
"active": any(str(task.status or "").upper() in active_task_statuses for task in tasks)
|
||||
or any(str(job.status or "").upper() in active_job_statuses for job in jobs),
|
||||
}
|
||||
|
||||
|
||||
def _load_wsl_process_summary(run_id: str) -> dict:
|
||||
distro = str(settings.GAMMA_SBAS_WSL_DISTRO or settings.WSL_DISTRO or "").strip()
|
||||
command = [
|
||||
"wsl.exe",
|
||||
*([] if not distro else ["-d", distro]),
|
||||
"--",
|
||||
"bash",
|
||||
"-lc",
|
||||
(
|
||||
"ps -eo pid,etime,stat,args --no-headers | "
|
||||
"grep -E 'gamma|SLC_interp|base_calc|mk_diff|mk_unw|ts_rate| mb |python|bash' | "
|
||||
"grep -v grep | head -20"
|
||||
),
|
||||
]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return {"available": False, "error": "wsl.exe not found", "processes": []}
|
||||
except Exception as exc:
|
||||
return {"available": False, "error": str(exc), "processes": []}
|
||||
|
||||
processes = []
|
||||
for line in (completed.stdout or "").splitlines():
|
||||
text = line.strip()
|
||||
if not text:
|
||||
continue
|
||||
parts = text.split(None, 3)
|
||||
processes.append(
|
||||
{
|
||||
"pid": parts[0] if len(parts) > 0 else "",
|
||||
"etime": parts[1] if len(parts) > 1 else "",
|
||||
"stat": parts[2] if len(parts) > 2 else "",
|
||||
"command": parts[3] if len(parts) > 3 else text,
|
||||
"matches_run": run_id in text,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"available": completed.returncode in {0, 1},
|
||||
"returncode": completed.returncode,
|
||||
"distro": distro or None,
|
||||
"processes": processes,
|
||||
"stderr_tail": (completed.stderr or "")[-1200:],
|
||||
}
|
||||
|
||||
|
||||
class SbasAoiBbox(BaseModel):
|
||||
min_lon: float = Field(ge=-180, le=180)
|
||||
min_lat: float = Field(ge=-90, le=90)
|
||||
@@ -30,6 +216,7 @@ class SbasAoiBbox(BaseModel):
|
||||
|
||||
|
||||
class SbasStackDiscoverRequest(BaseModel):
|
||||
sensor_family: str = Field(default="LT1", pattern="^(LT1|S1)$")
|
||||
source_roots: list[str] | None = None
|
||||
orbit_roots: list[str] | None = None
|
||||
min_scenes: int = Field(default=3, ge=2, le=100)
|
||||
@@ -43,7 +230,11 @@ class SbasStackDiscoverRequest(BaseModel):
|
||||
discovery_mode: str = Field(default="strict", pattern="^(strict|aoi)$")
|
||||
aoi_bbox: SbasAoiBbox | None = None
|
||||
min_aoi_coverage_ratio: float = Field(default=0.01, ge=0, le=1)
|
||||
min_common_overlap_ratio: float = Field(default=0.0, ge=0, le=1)
|
||||
min_common_overlap_ratio: float = Field(
|
||||
default_factory=lambda: float(settings.GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO or 0.30),
|
||||
ge=0,
|
||||
le=1,
|
||||
)
|
||||
|
||||
@field_validator("source_roots", "orbit_roots", mode="before")
|
||||
@classmethod
|
||||
@@ -57,6 +248,14 @@ class SbasStackDiscoverRequest(BaseModel):
|
||||
cleaned = [str(item or "").strip() for item in items if str(item or "").strip()]
|
||||
return cleaned or None
|
||||
|
||||
@field_validator("sensor_family", mode="before")
|
||||
@classmethod
|
||||
def _normalize_sensor_family(cls, value):
|
||||
text = str(value or "LT1").strip().upper()
|
||||
if text in {"SENTINEL1", "SENTINEL-1"}:
|
||||
return "S1"
|
||||
return text or "LT1"
|
||||
|
||||
@field_validator("platform", "relative_orbit", "orbit_direction", "admin_region", mode="before")
|
||||
@classmethod
|
||||
def _normalize_optional_text(cls, value):
|
||||
@@ -165,9 +364,122 @@ class SbasWorkflowJobRequest(SbasWorkflowPrepareRequest):
|
||||
timeout_seconds: int = Field(default=172800, ge=60, le=604800)
|
||||
|
||||
|
||||
class LandsarSbasAutoWorkflowRequest(SbasStackDiscoverRequest):
|
||||
sensor_family: str = Field(default="LT1", pattern="^LT1$")
|
||||
require_orbits: bool = False
|
||||
run_label: str | None = Field(default=None, max_length=160)
|
||||
dem_path: str | None = Field(default=None, max_length=1024)
|
||||
timeout_seconds: int | None = Field(default=None, ge=60, le=604800)
|
||||
import_timeout_seconds: int | None = Field(default=None, ge=60, le=604800)
|
||||
workflow_timeout_seconds: int | None = Field(default=None, ge=60, le=604800)
|
||||
params: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@router.get("/capabilities")
|
||||
async def get_sbas_insar_capabilities():
|
||||
return sbas_insar_production_service.get_capabilities()
|
||||
capabilities = sbas_insar_production_service.get_capabilities()
|
||||
capabilities["processors"] = [
|
||||
{
|
||||
"processor_code": capabilities.get("processor_code"),
|
||||
"profile_code": "lt1_gamma_sbas",
|
||||
"engine_code": capabilities.get("engine_code"),
|
||||
"label": "Gamma / IPTA SBAS",
|
||||
"enabled": bool((capabilities.get("runtime") or {}).get("enabled", True)),
|
||||
},
|
||||
{
|
||||
**landsar_sbas_service.get_capabilities(),
|
||||
"label": "LandSAR SBAS",
|
||||
},
|
||||
]
|
||||
return capabilities
|
||||
|
||||
|
||||
@router.get("/landsar/capabilities")
|
||||
async def get_landsar_sbas_capabilities():
|
||||
return landsar_sbas_service.get_capabilities()
|
||||
|
||||
|
||||
@router.post("/landsar/workflows/auto", status_code=202)
|
||||
async def submit_landsar_sbas_auto_workflow(request: LandsarSbasAutoWorkflowRequest):
|
||||
try:
|
||||
from ..services.job_handlers import JOB_TYPE_SBAS_LANDSAR_WORKFLOW
|
||||
|
||||
selection_request = {
|
||||
"run_label": request.run_label,
|
||||
"source_roots": request.source_roots,
|
||||
"orbit_roots": request.orbit_roots,
|
||||
"min_scenes": request.min_scenes,
|
||||
"discovery_mode": request.discovery_mode,
|
||||
"admin_region": request.admin_region,
|
||||
"aoi_bbox": request.aoi_bbox.model_dump() if request.aoi_bbox else None,
|
||||
"min_aoi_coverage_ratio": request.min_aoi_coverage_ratio,
|
||||
"min_common_overlap_ratio": request.min_common_overlap_ratio,
|
||||
"limit": request.limit,
|
||||
"dem_path": request.dem_path,
|
||||
"timeout_seconds": request.timeout_seconds,
|
||||
"import_timeout_seconds": request.import_timeout_seconds,
|
||||
"params": dict(request.params or {}),
|
||||
}
|
||||
payload = {
|
||||
"auto_select": True,
|
||||
"selection_request": selection_request,
|
||||
"timeout_seconds": request.workflow_timeout_seconds or request.timeout_seconds,
|
||||
}
|
||||
task_id = await task_service.create_task(
|
||||
task_type=JOB_TYPE_SBAS_LANDSAR_WORKFLOW,
|
||||
task_name="LandSAR SBAS Auto Workflow",
|
||||
params=payload,
|
||||
)
|
||||
job_id = await job_queue_service.create_job(
|
||||
job_type=JOB_TYPE_SBAS_LANDSAR_WORKFLOW,
|
||||
payload=payload,
|
||||
task_id=task_id,
|
||||
max_attempts=1,
|
||||
)
|
||||
return {
|
||||
"message": "LandSAR SBAS auto workflow job queued. Stack selection and input import will run in the background.",
|
||||
"run_id": None,
|
||||
"selection_pending": True,
|
||||
"task_id": task_id,
|
||||
"job_id": job_id,
|
||||
"job_type": JOB_TYPE_SBAS_LANDSAR_WORKFLOW,
|
||||
"status": "QUEUED",
|
||||
}
|
||||
except ValueError as exc:
|
||||
message = str(exc)
|
||||
status_code = 409 if "already running" in message.lower() or "conflict" in message.lower() or "任务冲突" in message else 400
|
||||
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||
|
||||
|
||||
@router.get("/landsar/runs")
|
||||
async def list_landsar_sbas_runs():
|
||||
return await asyncio.to_thread(landsar_sbas_service.list_runs)
|
||||
|
||||
|
||||
@router.get("/landsar/runs/{run_id}")
|
||||
async def get_landsar_sbas_run(run_id: str):
|
||||
try:
|
||||
return await asyncio.to_thread(landsar_sbas_service.get_run_detail, run_id)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/landsar/runs/{run_id}/artifacts/{relative_path:path}")
|
||||
async def get_landsar_sbas_run_artifact(run_id: str, relative_path: str):
|
||||
try:
|
||||
artifact_path = await asyncio.to_thread(landsar_sbas_service.resolve_artifact, run_id, relative_path)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
media_type = mimetypes.guess_type(str(artifact_path))[0] or "application/octet-stream"
|
||||
return FileResponse(
|
||||
artifact_path,
|
||||
media_type=media_type,
|
||||
filename=artifact_path.name,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/stacks/discover")
|
||||
@@ -175,6 +487,7 @@ async def discover_sbas_insar_stacks(request: SbasStackDiscoverRequest):
|
||||
try:
|
||||
return await asyncio.to_thread(
|
||||
sbas_insar_production_service.discover_stacks,
|
||||
sensor_family=request.sensor_family,
|
||||
source_roots=request.source_roots,
|
||||
orbit_roots=request.orbit_roots,
|
||||
min_scenes=request.min_scenes,
|
||||
@@ -200,6 +513,7 @@ async def audit_sbas_insar_stack(stack_id: str, request: SbasStackDiscoverReques
|
||||
return await asyncio.to_thread(
|
||||
sbas_insar_production_service.audit_stack,
|
||||
stack_id,
|
||||
sensor_family=request.sensor_family,
|
||||
source_roots=request.source_roots,
|
||||
orbit_roots=request.orbit_roots,
|
||||
min_scenes=request.min_scenes,
|
||||
@@ -222,6 +536,7 @@ async def submit_sbas_insar_run(stack_id: str, request: SbasRunSubmitRequest):
|
||||
return await asyncio.to_thread(
|
||||
sbas_insar_production_service.create_run,
|
||||
stack_id,
|
||||
sensor_family=request.sensor_family,
|
||||
run_label=request.run_label,
|
||||
source_roots=request.source_roots,
|
||||
orbit_roots=request.orbit_roots,
|
||||
@@ -253,13 +568,57 @@ async def list_sbas_insar_runs():
|
||||
@router.get("/runs/{run_id}")
|
||||
async def get_sbas_insar_run(run_id: str):
|
||||
try:
|
||||
return await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||
detail = await asyncio.to_thread(sbas_insar_production_service.get_run_detail, run_id)
|
||||
background_activity, wsl_processes = await asyncio.gather(
|
||||
_load_run_background_activity(run_id),
|
||||
asyncio.to_thread(_load_wsl_process_summary, run_id),
|
||||
)
|
||||
runtime_status = dict(detail.get("runtime_status") or {})
|
||||
runtime_status["background_activity"] = background_activity
|
||||
runtime_status["wsl_processes"] = wsl_processes
|
||||
runtime_status["active"] = bool(
|
||||
runtime_status.get("active")
|
||||
or background_activity.get("active")
|
||||
or any(item.get("matches_run") for item in wsl_processes.get("processes") or [])
|
||||
)
|
||||
detail["runtime_status"] = runtime_status
|
||||
return detail
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.delete("/runs/{run_id}")
|
||||
async def delete_sbas_insar_run(
|
||||
run_id: str,
|
||||
current_user: AuthUserORM = Depends(_require_admin),
|
||||
):
|
||||
_ = current_user
|
||||
try:
|
||||
async with _new_session() as db:
|
||||
result = await sbas_insar_production_service.delete_run_record(run_id, db=db)
|
||||
try:
|
||||
from ..services.sbas_insar_catalog_service import sbas_insar_catalog_service
|
||||
|
||||
catalog_result = await sbas_insar_catalog_service.rebuild_catalog(db, full_rebuild=True)
|
||||
except Exception as catalog_exc:
|
||||
catalog_result = {
|
||||
"status": "WARN",
|
||||
"message": f"SBAS catalog rebuild failed after run deletion: {catalog_exc}",
|
||||
}
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
message = str(exc)
|
||||
status_code = 409 if "active task/job" in message or "running" in message.lower() else 400
|
||||
raise HTTPException(status_code=status_code, detail=message) from exc
|
||||
return {
|
||||
**result,
|
||||
"catalog": catalog_result,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/runs/{run_id}/workflow", status_code=202)
|
||||
async def prepare_sbas_insar_workflow(run_id: str, request: SbasWorkflowPrepareRequest):
|
||||
try:
|
||||
|
||||
@@ -5,10 +5,11 @@ import os
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import AuthUserORM
|
||||
from ..models import AuthUserORM, SystemTaskORM
|
||||
from ..services.job_queue_service import job_queue_service
|
||||
from ..services.sbas_insar_catalog_service import (
|
||||
JOB_TYPE_REBUILD_SBAS_INSAR_CATALOG,
|
||||
@@ -26,6 +27,24 @@ class SbasInsarCatalogRebuildRequest(BaseModel):
|
||||
full_rebuild: bool = True
|
||||
|
||||
|
||||
class SbasInsarPointTimeseriesRequest(BaseModel):
|
||||
lon: float
|
||||
lat: float
|
||||
|
||||
|
||||
async def _get_active_sbas_catalog_rebuild_task(db: AsyncSession) -> SystemTaskORM | None:
|
||||
result = await db.execute(
|
||||
select(SystemTaskORM)
|
||||
.where(
|
||||
SystemTaskORM.task_type == TASK_TYPE_REBUILD_SBAS_INSAR_CATALOG,
|
||||
SystemTaskORM.status.in_(["PENDING", "RUNNING"]),
|
||||
)
|
||||
.order_by(SystemTaskORM.created_at.desc(), SystemTaskORM.id.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
@router.get("/sbas-insar-products/catalog-status")
|
||||
async def get_sbas_insar_catalog_status(
|
||||
current_user: AuthUserORM = Depends(_get_current_user),
|
||||
@@ -43,12 +62,40 @@ async def queue_sbas_insar_catalog_rebuild(
|
||||
admin_user: AuthUserORM = Depends(_require_admin),
|
||||
):
|
||||
_ = admin_user
|
||||
task_id = await task_service.create_task(
|
||||
TASK_TYPE_REBUILD_SBAS_INSAR_CATALOG,
|
||||
"SBAS-InSAR result catalog rebuild",
|
||||
params={"full_rebuild": request.full_rebuild},
|
||||
db=db,
|
||||
)
|
||||
existing_task = await _get_active_sbas_catalog_rebuild_task(db)
|
||||
if existing_task is not None:
|
||||
await _add_operation_audit_log(
|
||||
db,
|
||||
request=http_request,
|
||||
action="sbas_insar_catalog_rebuild_already_running",
|
||||
resource="sbas-insar-products/rebuild",
|
||||
detail={"task_id": existing_task.task_id, "full_rebuild": request.full_rebuild},
|
||||
)
|
||||
await db.commit()
|
||||
return {
|
||||
"message": "SBAS-InSAR result catalog rebuild is already queued or running.",
|
||||
"task_id": existing_task.task_id,
|
||||
"already_running": True,
|
||||
}
|
||||
|
||||
try:
|
||||
task_id = await task_service.create_task(
|
||||
TASK_TYPE_REBUILD_SBAS_INSAR_CATALOG,
|
||||
"SBAS-InSAR result catalog rebuild",
|
||||
params={"full_rebuild": request.full_rebuild},
|
||||
db=db,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await db.rollback()
|
||||
existing_task = await _get_active_sbas_catalog_rebuild_task(db)
|
||||
if existing_task is not None:
|
||||
return {
|
||||
"message": "SBAS-InSAR result catalog rebuild is already queued or running.",
|
||||
"task_id": existing_task.task_id,
|
||||
"already_running": True,
|
||||
}
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
await job_queue_service.create_job(
|
||||
JOB_TYPE_REBUILD_SBAS_INSAR_CATALOG,
|
||||
payload={"full_rebuild": request.full_rebuild},
|
||||
@@ -103,6 +150,32 @@ async def get_sbas_insar_product_detail(
|
||||
return detail
|
||||
|
||||
|
||||
@router.post("/sbas-insar-products/{product_db_id}/point-timeseries")
|
||||
async def query_sbas_insar_point_timeseries(
|
||||
product_db_id: int,
|
||||
request: SbasInsarPointTimeseriesRequest,
|
||||
current_user: AuthUserORM = Depends(_get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ = current_user
|
||||
try:
|
||||
detail = await sbas_insar_catalog_service.query_point_timeseries(
|
||||
db,
|
||||
product_db_id=product_db_id,
|
||||
lon=request.lon,
|
||||
lat=request.lat,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
if detail is None:
|
||||
raise HTTPException(status_code=404, detail="SBAS-InSAR product not found")
|
||||
return detail
|
||||
|
||||
|
||||
@router.get("/sbas-insar-products/{product_db_id}/preview")
|
||||
async def get_sbas_insar_product_preview(
|
||||
product_db_id: int,
|
||||
|
||||
@@ -25,6 +25,8 @@ from ..models import (
|
||||
WorkflowRunORM,
|
||||
WorkflowStepORM,
|
||||
)
|
||||
from ..utils import normalize_satellite_family
|
||||
from .dinsar_naming import build_fallback_pair_key
|
||||
from .envi_service import RUNTIME_DIR, _collect_task_folders, _resolve_dinsar_pair_identity, _to_local_path
|
||||
from .task_service import task_service
|
||||
from .workflow_service import workflow_service
|
||||
@@ -33,6 +35,7 @@ from .workflow_service import workflow_service
|
||||
TASK_TYPE_DINSAR_PRODUCTION = "IDL_RUN_DINSAR"
|
||||
TASK_TYPE_ISCE2_DINSAR_PRODUCTION = "ISCE2_RUN"
|
||||
TASK_TYPE_PYINT_DINSAR_PRODUCTION = "PYINT_RUN"
|
||||
TASK_TYPE_LANDSAR_DINSAR_PRODUCTION = "LANDSAR_RUN"
|
||||
RUN_STATUS_PENDING = "PENDING"
|
||||
RUN_STATUS_RUNNING = "RUNNING"
|
||||
RUN_STATUS_COMPLETED = "COMPLETED"
|
||||
@@ -83,6 +86,8 @@ def _task_type_for_engine(engine_code: str) -> str:
|
||||
return TASK_TYPE_ISCE2_DINSAR_PRODUCTION
|
||||
if normalized in {"pyint", "gamma"}:
|
||||
return TASK_TYPE_PYINT_DINSAR_PRODUCTION
|
||||
if normalized == "landsar":
|
||||
return TASK_TYPE_LANDSAR_DINSAR_PRODUCTION
|
||||
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
|
||||
|
||||
|
||||
@@ -94,6 +99,8 @@ def _workflow_name_for_engine(engine_code: str) -> str:
|
||||
return "dinsar_isce2_production"
|
||||
if normalized in {"pyint", "gamma"}:
|
||||
return "dinsar_pyint_gamma_production"
|
||||
if normalized == "landsar":
|
||||
return "dinsar_landsar_production"
|
||||
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
|
||||
|
||||
|
||||
@@ -105,6 +112,8 @@ def _workflow_step_name_for_engine(engine_code: str) -> str:
|
||||
return "Execute ISCE2 D-InSAR items"
|
||||
if normalized in {"pyint", "gamma"}:
|
||||
return "Execute PyINT/Gamma D-InSAR items"
|
||||
if normalized == "landsar":
|
||||
return "Execute LandSAR D-InSAR items"
|
||||
raise ValueError(f"Unsupported engine for D-InSAR production run: {engine_code}")
|
||||
|
||||
|
||||
@@ -150,6 +159,33 @@ def _looks_like_task_dir(path: str) -> bool:
|
||||
return os.path.isdir(os.path.join(path, "master")) and os.path.isdir(os.path.join(path, "slave"))
|
||||
|
||||
|
||||
def _looks_like_landsar_task_dir(path: str) -> bool:
|
||||
return os.path.isdir(os.path.join(path, "Input_Data"))
|
||||
|
||||
|
||||
def _looks_like_landsar_raw_task_dir(path: str) -> bool:
|
||||
normalized = os.path.normpath(os.path.abspath(_to_local_path(path)))
|
||||
master_dir = os.path.join(normalized, "master")
|
||||
slave_dir = os.path.join(normalized, "slave")
|
||||
if not os.path.isdir(master_dir) or not os.path.isdir(slave_dir):
|
||||
return False
|
||||
|
||||
def _has_lt1_file(directory: str) -> bool:
|
||||
try:
|
||||
with os.scandir(directory) as entries:
|
||||
for entry in entries:
|
||||
if not entry.is_file():
|
||||
continue
|
||||
name = entry.name.lower()
|
||||
if name.startswith("lt1") and name.endswith((".xml", ".tif", ".tiff")):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
return False
|
||||
|
||||
return _has_lt1_file(master_dir) and _has_lt1_file(slave_dir)
|
||||
|
||||
|
||||
def _normalize_rerun_mode(value: Optional[str]) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized in VALID_RERUN_MODES:
|
||||
@@ -182,6 +218,64 @@ def _discover_run_items(root_dir: str) -> List[Dict[str, Any]]:
|
||||
return items
|
||||
|
||||
|
||||
def _discover_landsar_run_items(root_dir: str) -> List[Dict[str, Any]]:
|
||||
if _looks_like_landsar_task_dir(root_dir) or _looks_like_landsar_raw_task_dir(root_dir):
|
||||
task_folders = [root_dir]
|
||||
else:
|
||||
task_folders = [
|
||||
folder
|
||||
for folder in _collect_task_folders(root_dir)
|
||||
if _looks_like_landsar_task_dir(folder) or _looks_like_landsar_raw_task_dir(folder)
|
||||
]
|
||||
|
||||
items: List[Dict[str, Any]] = []
|
||||
try:
|
||||
from ..dinsar_engines.landsar_engine import parse_lt1_slc_pair
|
||||
except Exception:
|
||||
parse_lt1_slc_pair = None
|
||||
|
||||
for order_index, folder in enumerate(task_folders, start=1):
|
||||
task_name = os.path.basename(folder)
|
||||
task_alias, pair_key, pair_meta = _resolve_dinsar_pair_identity(folder, task_name)
|
||||
has_input_data = _looks_like_landsar_task_dir(folder)
|
||||
has_raw_input = _looks_like_landsar_raw_task_dir(folder)
|
||||
pair = (
|
||||
parse_lt1_slc_pair(os.path.join(folder, "Input_Data"))
|
||||
if parse_lt1_slc_pair is not None and has_input_data
|
||||
else None
|
||||
)
|
||||
if parse_lt1_slc_pair is not None and has_input_data and not pair and not has_raw_input:
|
||||
continue
|
||||
if pair and not pair_meta.get("pair_key"):
|
||||
pair_key = build_fallback_pair_key(
|
||||
task_alias,
|
||||
"||".join([pair["master_xml"], pair["slave_xml"]]),
|
||||
satellite_family=normalize_satellite_family("lt1"),
|
||||
)
|
||||
elif not pair_meta.get("pair_key") and has_raw_input:
|
||||
pair_key = build_fallback_pair_key(
|
||||
task_alias,
|
||||
folder,
|
||||
satellite_family=normalize_satellite_family("lt1"),
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"order_index": order_index,
|
||||
"task_name": task_name,
|
||||
"task_alias": task_alias,
|
||||
"pair_key": pair_key,
|
||||
"pair_uid": pair_meta.get("pair_uid") or pair_meta.get("scene_pair_uid"),
|
||||
"network_run_id": pair_meta.get("network_run_id"),
|
||||
"network_edge_id": pair_meta.get("network_edge_id"),
|
||||
"policy_version": pair_meta.get("policy_version"),
|
||||
"selection_strategy": pair_meta.get("selection_strategy"),
|
||||
"source_task_dir": folder,
|
||||
"results_root_dir": os.path.join(settings.DINSAR_PRODUCT_DIR, pair_key),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def _current_pointer_path_for_root(
|
||||
results_root_dir: str,
|
||||
*,
|
||||
@@ -256,7 +350,12 @@ def _select_run_items(
|
||||
num_to_process: int,
|
||||
rerun_mode: Optional[str],
|
||||
) -> Dict[str, Any]:
|
||||
discovered_items = _discover_run_items(root_dir)
|
||||
normalized_engine = str(engine_code or "").strip().lower()
|
||||
discovered_items = (
|
||||
_discover_landsar_run_items(root_dir)
|
||||
if normalized_engine == "landsar"
|
||||
else _discover_run_items(root_dir)
|
||||
)
|
||||
normalized_mode = _normalize_rerun_mode(rerun_mode)
|
||||
|
||||
skipped_completed_count = 0
|
||||
|
||||
@@ -25,9 +25,15 @@ from ..models import (
|
||||
from ..idl_service import get_idl_status
|
||||
from .product_package_schema import CANONICAL_PACKAGE_SCHEMA
|
||||
from .pairing_state_service import pairing_state_service
|
||||
from .sbas_insar_catalog_service import sbas_insar_catalog_service
|
||||
from .wsl_runtime_registry import wsl_runtime_registry
|
||||
|
||||
|
||||
ALLOWED_PRODUCT_PACKAGE_SCHEMAS = {
|
||||
CANONICAL_PACKAGE_SCHEMA,
|
||||
"insar.gamma-ipta-sbas-run/v1",
|
||||
"insar.gamma-sbas-run/v1",
|
||||
}
|
||||
DEFAULT_WORKER_TIMEOUT_SECONDS = 60
|
||||
DEFAULT_SCHEMA_CACHE_SECONDS = 120
|
||||
_SCHEMA_CACHE_LOCK = asyncio.Lock()
|
||||
@@ -202,13 +208,22 @@ def _build_catalog_status(
|
||||
catalog_name: str,
|
||||
storage_root: str,
|
||||
enabled: bool = True,
|
||||
storage_roots: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
normalized_roots = [str(item or "").strip() for item in (storage_roots or [storage_root]) if str(item or "").strip()]
|
||||
if not normalized_roots and storage_root:
|
||||
normalized_roots = [storage_root]
|
||||
return {
|
||||
"ok": False,
|
||||
"catalog_name": catalog_name,
|
||||
"enabled": enabled,
|
||||
"storage_root": storage_root,
|
||||
"storage_root_exists": os.path.isdir(storage_root),
|
||||
"storage_roots": normalized_roots,
|
||||
"storage_root_exists": bool(storage_root) and os.path.isdir(storage_root),
|
||||
"storage_roots_status": [
|
||||
{"path": root, "exists": os.path.isdir(root)}
|
||||
for root in normalized_roots
|
||||
],
|
||||
"state_present": False,
|
||||
"catalog_status": None,
|
||||
"needs_rebuild": None,
|
||||
@@ -228,11 +243,13 @@ async def _check_catalog(
|
||||
catalog_name: str,
|
||||
storage_root: str,
|
||||
enabled: bool = True,
|
||||
storage_roots: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
status = _build_catalog_status(
|
||||
catalog_name=catalog_name,
|
||||
storage_root=storage_root,
|
||||
enabled=enabled,
|
||||
storage_roots=storage_roots,
|
||||
)
|
||||
if not enabled:
|
||||
status["ok"] = True
|
||||
@@ -262,6 +279,16 @@ async def _check_catalog(
|
||||
status["last_message"] = state.last_message
|
||||
if state.storage_root:
|
||||
status["storage_root"] = state.storage_root
|
||||
root_status_by_path = {
|
||||
item["path"]: item
|
||||
for item in status.get("storage_roots_status", [])
|
||||
}
|
||||
if state.storage_root not in root_status_by_path:
|
||||
status.setdefault("storage_roots", []).insert(0, state.storage_root)
|
||||
status.setdefault("storage_roots_status", []).insert(
|
||||
0,
|
||||
{"path": state.storage_root, "exists": os.path.isdir(state.storage_root)},
|
||||
)
|
||||
status["storage_root_exists"] = os.path.isdir(state.storage_root)
|
||||
|
||||
count_result = await db.execute(
|
||||
@@ -273,7 +300,9 @@ async def _check_catalog(
|
||||
|
||||
manifest_count = int(status["manifest_count"] or 0)
|
||||
needs_rebuild = bool(status["needs_rebuild"])
|
||||
status["ok"] = bool(status["storage_root_exists"]) and not (
|
||||
roots_status = status.get("storage_roots_status") or []
|
||||
roots_exist = all(bool(item.get("exists")) for item in roots_status) if roots_status else bool(status["storage_root_exists"])
|
||||
status["ok"] = roots_exist and not (
|
||||
manifest_count > 0 and needs_rebuild
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -299,10 +328,13 @@ async def _check_timeseries_result_catalog() -> Dict[str, Any]:
|
||||
|
||||
|
||||
async def _check_sbas_insar_result_catalog() -> Dict[str, Any]:
|
||||
run_roots = sbas_insar_catalog_service.get_run_roots()
|
||||
primary_root = run_roots[0] if run_roots else os.path.join(settings.GAMMA_SBAS_WORK_ROOT, "runs")
|
||||
return await _check_catalog(
|
||||
catalog_name="sbas_insar",
|
||||
storage_root=os.path.join(settings.GAMMA_SBAS_WORK_ROOT, "runs"),
|
||||
enabled=bool(settings.GAMMA_SBAS_ENABLED),
|
||||
storage_root=primary_root,
|
||||
storage_roots=run_roots,
|
||||
enabled=bool(settings.GAMMA_SBAS_ENABLED or settings.LANDSAR_SBAS_ENABLED),
|
||||
)
|
||||
|
||||
|
||||
@@ -389,6 +421,8 @@ def _sanitize_product_package_status(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"ok": bool(payload.get("ok")),
|
||||
"total_count": int(payload.get("total_count") or 0),
|
||||
"canonical_count": int(payload.get("canonical_count") or 0),
|
||||
"valid_schema_count": int(payload.get("valid_schema_count") or 0),
|
||||
"invalid_schema_count": int(payload.get("invalid_schema_count") or 0),
|
||||
"missing_manifest_count": int(payload.get("missing_manifest_count") or 0),
|
||||
"missing_publish_dir_count": int(payload.get("missing_publish_dir_count") or 0),
|
||||
"missing_processor_count": int(payload.get("missing_processor_count") or 0),
|
||||
@@ -1033,8 +1067,11 @@ async def _check_product_packages() -> Dict[str, Any]:
|
||||
status = {
|
||||
"ok": False,
|
||||
"canonical_schema": CANONICAL_PACKAGE_SCHEMA,
|
||||
"allowed_schemas": sorted(ALLOWED_PRODUCT_PACKAGE_SCHEMAS),
|
||||
"total_count": 0,
|
||||
"canonical_count": 0,
|
||||
"valid_schema_count": 0,
|
||||
"invalid_schema_count": 0,
|
||||
"missing_manifest_count": 0,
|
||||
"missing_publish_dir_count": 0,
|
||||
"missing_processor_count": 0,
|
||||
@@ -1077,8 +1114,13 @@ async def _check_product_packages() -> Dict[str, Any]:
|
||||
status["by_family"][family_key] = int(status["by_family"].get(family_key, 0)) + 1
|
||||
status["by_engine"][engine_key] = int(status["by_engine"].get(engine_key, 0)) + 1
|
||||
|
||||
if str(package_schema or "").strip() == CANONICAL_PACKAGE_SCHEMA:
|
||||
schema_key = str(package_schema or "").strip()
|
||||
if schema_key == CANONICAL_PACKAGE_SCHEMA:
|
||||
status["canonical_count"] += 1
|
||||
if schema_key in ALLOWED_PRODUCT_PACKAGE_SCHEMAS:
|
||||
status["valid_schema_count"] += 1
|
||||
else:
|
||||
status["invalid_schema_count"] += 1
|
||||
if not str(manifest_path or "").strip() or not os.path.isfile(str(manifest_path)):
|
||||
status["missing_manifest_count"] += 1
|
||||
if not str(publish_dir or "").strip() or not os.path.isdir(str(publish_dir)):
|
||||
@@ -1097,7 +1139,7 @@ async def _check_product_packages() -> Dict[str, Any]:
|
||||
status["missing_processor_count"] == 0,
|
||||
status["missing_runtime_count"] == 0,
|
||||
status["missing_native_output_count"] == 0,
|
||||
status["canonical_count"] == status["total_count"],
|
||||
status["invalid_schema_count"] == 0,
|
||||
]
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -1432,7 +1474,8 @@ async def get_health_status(
|
||||
wsl_runtime_status.get("ok"),
|
||||
pairing_system_status.get("ok"),
|
||||
(not settings.TIMESERIES_ENABLED) or timeseries_result_catalog_status.get("ok"),
|
||||
(not settings.GAMMA_SBAS_ENABLED) or sbas_insar_result_catalog_status.get("ok"),
|
||||
(not (settings.GAMMA_SBAS_ENABLED or settings.LANDSAR_SBAS_ENABLED))
|
||||
or sbas_insar_result_catalog_status.get("ok"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ JOB_TYPE_GF3_SARSCAPE_SYNC = "GF3_SARSCAPE_SYNC"
|
||||
JOB_TYPE_GF3_SARSCAPE_CLEAN = "GF3_SARSCAPE_CLEAN"
|
||||
JOB_TYPE_ISCE2_RUN = "ISCE2_RUN"
|
||||
JOB_TYPE_PYINT_RUN = "PYINT_RUN"
|
||||
JOB_TYPE_LANDSAR_RUN = "LANDSAR_RUN"
|
||||
JOB_TYPE_PUBLISH_DINSAR_PRODUCTS = "PUBLISH_DINSAR_PRODUCTS"
|
||||
JOB_TYPE_REBUILD_DINSAR_CATALOG = "REBUILD_DINSAR_CATALOG"
|
||||
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
|
||||
@@ -101,6 +102,7 @@ JOB_TYPE_SBAS_RDC_DEM = "SBAS_RDC_DEM"
|
||||
JOB_TYPE_SBAS_INTERFEROGRAMS = "SBAS_INTERFEROGRAMS"
|
||||
JOB_TYPE_SBAS_IPTA_TIMESERIES = "SBAS_IPTA_TIMESERIES"
|
||||
JOB_TYPE_SBAS_GAMMA_WORKFLOW = "SBAS_GAMMA_WORKFLOW"
|
||||
JOB_TYPE_SBAS_LANDSAR_WORKFLOW = "SBAS_LANDSAR_WORKFLOW"
|
||||
|
||||
COPY_ALLOWED_STATUSES = {"PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"}
|
||||
|
||||
@@ -2602,6 +2604,12 @@ async def _run_wsl_dinsar_production_controller(
|
||||
|
||||
managed_run_dir = os.path.normpath(execution.output_dir)
|
||||
managed_native_output_dir = os.path.join(managed_run_dir, "native")
|
||||
if engine_code == "landsar":
|
||||
landsar_work_root = str(getattr(settings, "LANDSAR_WORK_ROOT", "") or "").strip()
|
||||
if landsar_work_root:
|
||||
managed_native_output_dir = os.path.normpath(
|
||||
os.path.join(landsar_work_root, run_key, "native")
|
||||
)
|
||||
managed_work_dir = os.path.join(managed_native_output_dir, "workflow")
|
||||
managed_export_dir = os.path.join(managed_native_output_dir, "export")
|
||||
managed_orbit_output_dir = os.path.join(managed_work_dir, "orbits")
|
||||
@@ -2740,10 +2748,12 @@ async def _run_wsl_dinsar_production_controller(
|
||||
task_result = ((detail.get("task_results") or [{}])[0]) if result else {}
|
||||
|
||||
try:
|
||||
if not result or not result.success or not bool(task_result.get("success", result.success if result else False)):
|
||||
result_error = str(result.error or "").strip() if result else ""
|
||||
result_success = bool(result.success) if result else False
|
||||
if not result or not result_success or not bool(task_result.get("success", result_success)):
|
||||
error_message = (
|
||||
str(task_result.get("error") or "").strip()
|
||||
or str(result.error or "").strip()
|
||||
or result_error
|
||||
or run_exception_text
|
||||
or str(task_result.get("stderr_tail") or "").strip()
|
||||
or f"{engine_title} run failed."
|
||||
@@ -2853,19 +2863,19 @@ async def _run_wsl_dinsar_production_controller(
|
||||
await task_service.add_log(
|
||||
job.task_id,
|
||||
"INFO",
|
||||
f"WSL command [{item_label}]: {task_result.get('command')}",
|
||||
f"{engine_title} command [{item_label}]: {task_result.get('command')}",
|
||||
)
|
||||
if task_result.get("stdout_tail"):
|
||||
await task_service.add_log(
|
||||
job.task_id,
|
||||
"INFO",
|
||||
f"WSL stdout tail [{item_label}]:\n{task_result.get('stdout_tail')}",
|
||||
f"{engine_title} stdout tail [{item_label}]:\n{task_result.get('stdout_tail')}",
|
||||
)
|
||||
if task_result.get("stderr_tail"):
|
||||
await task_service.add_log(
|
||||
job.task_id,
|
||||
"WARNING",
|
||||
f"WSL stderr tail [{item_label}]:\n{task_result.get('stderr_tail')}",
|
||||
f"{engine_title} stderr tail [{item_label}]:\n{task_result.get('stderr_tail')}",
|
||||
)
|
||||
|
||||
publish_result = None
|
||||
@@ -3093,6 +3103,60 @@ async def _handle_pyint_run(job: SystemJobORM) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _handle_landsar_run(job: SystemJobORM) -> None:
|
||||
production_run_id = str((job.payload or {}).get("production_run_id") or "").strip()
|
||||
if production_run_id:
|
||||
try:
|
||||
await _run_wsl_dinsar_production_controller(
|
||||
job,
|
||||
engine_code="landsar",
|
||||
engine_title="LandSAR",
|
||||
fallback_timeout_seconds=int(getattr(settings, "LANDSAR_DINSAR_TIMEOUT_SECONDS", 0) or 43200),
|
||||
)
|
||||
except Exception as exc:
|
||||
latest_message = f"LandSAR D-InSAR production controller failed: {exc}"
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
run = await dinsar_production_service.get_run(production_run_id, db)
|
||||
if run is not None and str(run.status or "").strip().upper() not in {"COMPLETED", "FAILED", "CANCELLED"}:
|
||||
summary_payload = dict(run.summary_json or {})
|
||||
summary_payload["controller_error"] = str(exc)
|
||||
await dinsar_production_service.finalize_run(
|
||||
run,
|
||||
db=db,
|
||||
status="FAILED",
|
||||
summary_payload=summary_payload,
|
||||
latest_message=latest_message,
|
||||
)
|
||||
dinsar_production_service.append_run_log(
|
||||
run.run_id,
|
||||
f"[controller-failed] {exc}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
current_task = await task_service.get_task(job.task_id)
|
||||
if current_task and current_task.status not in {"COMPLETED", "FAILED", "CANCELLED"}:
|
||||
await task_service.add_log(job.task_id, "ERROR", latest_message)
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
status="FAILED",
|
||||
progress=100,
|
||||
message=latest_message,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
return
|
||||
|
||||
await _handle_queued_engine_run(
|
||||
job,
|
||||
engine_title="LandSAR",
|
||||
fallback_timeout_seconds=int(getattr(settings, "LANDSAR_DINSAR_TIMEOUT_SECONDS", 0) or 43200),
|
||||
)
|
||||
|
||||
|
||||
async def _handle_water_geocode(job: SystemJobORM) -> None:
|
||||
"""单景 SAR 地理编码 job handler(多视 + 地理编码 + 辐射定标)。"""
|
||||
from .water_service import run_geocoding_workflow, WATER_RESULTS_DIR
|
||||
@@ -4963,6 +5027,187 @@ async def _handle_sbas_gamma_workflow(job: SystemJobORM) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _handle_sbas_landsar_workflow(job: SystemJobORM) -> None:
|
||||
from .landsar_sbas_service import landsar_sbas_service
|
||||
|
||||
payload = job.payload or {}
|
||||
run_id = str(payload.get("run_id") or "").strip()
|
||||
auto_select = bool(payload.get("auto_select"))
|
||||
if not run_id and not auto_select:
|
||||
raise ValueError("SBAS_LANDSAR_WORKFLOW requires run_id or auto_select")
|
||||
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or int(
|
||||
getattr(settings, "LANDSAR_SBAS_TIMEOUT_SECONDS", 0) or 172800
|
||||
)
|
||||
|
||||
await task_service.start_task(
|
||||
job.task_id,
|
||||
message=(
|
||||
"LandSAR SBAS auto workflow started: selecting LT-1 stack"
|
||||
if auto_select
|
||||
else f"LandSAR SBAS workflow started: {run_id}"
|
||||
),
|
||||
)
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
progress=5,
|
||||
message=(
|
||||
"Using Gamma SBAS production-area stack discovery for LandSAR input selection..."
|
||||
if auto_select
|
||||
else f"Preparing LandSAR SBAS workflow: {run_id}"
|
||||
),
|
||||
)
|
||||
|
||||
last_log_at = 0.0
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
def _progress(event: dict[str, Any]) -> None:
|
||||
nonlocal last_log_at
|
||||
level = str(event.get("level") or "INFO").upper()
|
||||
message = str(event.get("message") or "").strip()
|
||||
if not message:
|
||||
return
|
||||
now = time.monotonic()
|
||||
if level == "INFO" and now - last_log_at < 0.2:
|
||||
return
|
||||
last_log_at = now
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
task_service.add_log(job.task_id, level, message),
|
||||
loop,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if auto_select:
|
||||
selection_request = dict(payload.get("selection_request") or {})
|
||||
selection_limit = selection_request.get("limit")
|
||||
if selection_limit is None:
|
||||
selection_limit = 30
|
||||
await task_service.add_log(
|
||||
job.task_id,
|
||||
"INFO",
|
||||
(
|
||||
"LandSAR auto workflow is reusing Gamma stack discovery: "
|
||||
f"admin_region={selection_request.get('admin_region') or '-'}, "
|
||||
f"min_scenes={selection_request.get('min_scenes') or '-'}"
|
||||
),
|
||||
)
|
||||
try:
|
||||
detail = await asyncio.to_thread(
|
||||
landsar_sbas_service.create_run_from_best_stack,
|
||||
run_label=selection_request.get("run_label"),
|
||||
source_roots=selection_request.get("source_roots"),
|
||||
orbit_roots=selection_request.get("orbit_roots"),
|
||||
min_scenes=selection_request.get("min_scenes"),
|
||||
discovery_mode=selection_request.get("discovery_mode") or "strict",
|
||||
admin_region=selection_request.get("admin_region"),
|
||||
aoi_bbox=selection_request.get("aoi_bbox"),
|
||||
min_aoi_coverage_ratio=selection_request.get("min_aoi_coverage_ratio", 0.01),
|
||||
min_common_overlap_ratio=selection_request.get(
|
||||
"min_common_overlap_ratio",
|
||||
settings.GAMMA_SBAS_MIN_COMMON_OVERLAP_RATIO,
|
||||
),
|
||||
limit=selection_limit,
|
||||
dem_path=selection_request.get("dem_path"),
|
||||
timeout_seconds=selection_request.get("timeout_seconds"),
|
||||
import_timeout_seconds=selection_request.get("import_timeout_seconds"),
|
||||
params=dict(selection_request.get("params") or {}),
|
||||
)
|
||||
except Exception as exc:
|
||||
await task_service.add_log(job.task_id, "ERROR", f"LandSAR auto stack selection failed: {exc}")
|
||||
raise
|
||||
|
||||
run_id = (
|
||||
(detail.get("run") or {}).get("run_id")
|
||||
or (detail.get("manifest") or {}).get("run_id")
|
||||
or ""
|
||||
)
|
||||
if not run_id:
|
||||
raise ValueError("LandSAR auto workflow did not create a run.")
|
||||
selection = detail.get("selection") or {}
|
||||
await task_service.add_log(
|
||||
job.task_id,
|
||||
"INFO",
|
||||
(
|
||||
"LandSAR auto stack selected: "
|
||||
f"stack_id={selection.get('selected_stack_id') or '-'}, run_id={run_id}"
|
||||
),
|
||||
)
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
progress=15,
|
||||
message=f"LandSAR SBAS Run created from Gamma-selected stack: {run_id}",
|
||||
)
|
||||
|
||||
runner_task = asyncio.create_task(
|
||||
asyncio.to_thread(
|
||||
landsar_sbas_service.execute_run,
|
||||
run_id,
|
||||
timeout_seconds=timeout_seconds,
|
||||
progress_callback=_progress,
|
||||
)
|
||||
)
|
||||
|
||||
async def _keepalive() -> None:
|
||||
while not runner_task.done():
|
||||
await asyncio.sleep(30)
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
progress=50,
|
||||
message=f"LandSAR SBAS workflow is still running: {run_id}",
|
||||
)
|
||||
|
||||
keepalive_task = asyncio.create_task(_keepalive())
|
||||
try:
|
||||
result = await runner_task
|
||||
finally:
|
||||
keepalive_task.cancel()
|
||||
try:
|
||||
await keepalive_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
manifest = result.get("manifest") or {}
|
||||
workflow = manifest.get("workflow") or {}
|
||||
summary = workflow.get("summary") or {}
|
||||
status = str(manifest.get("status") or "").strip().upper()
|
||||
if status not in {"LANDSAR_SBAS_COMPLETED", "LANDSAR_SBAS_PARTIAL"}:
|
||||
failed_count = summary.get("failed_count") or manifest.get("failed_task_count") or 0
|
||||
if status == "LANDSAR_SBAS_RUNTIME_UNSUPPORTED":
|
||||
unsupported_count = summary.get("unsupported_proid_count") or 0
|
||||
configured_proid = manifest.get("proid") or "unknown"
|
||||
message = (
|
||||
f"LandSAR SBAS runtime unsupported: configured proID {configured_proid} is not recognized by "
|
||||
f"this LandSAR installation. failure_kind=unsupported_proid, "
|
||||
f"next_stage={manifest.get('next_stage') or 'configure_landsar_sbas_runtime'}, "
|
||||
f"unsupported_tasks={unsupported_count}"
|
||||
)
|
||||
await task_service.add_log(job.task_id, "ERROR", message)
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
status="FAILED",
|
||||
progress=100,
|
||||
message=message,
|
||||
)
|
||||
raise RuntimeError(message)
|
||||
raise RuntimeError(
|
||||
"LandSAR SBAS workflow failed: "
|
||||
f"status={status or 'UNKNOWN'}, failed={failed_count}"
|
||||
)
|
||||
|
||||
await task_service.update_task(
|
||||
job.task_id,
|
||||
status="COMPLETED",
|
||||
progress=100,
|
||||
message=(
|
||||
f"LandSAR SBAS workflow {status.lower()}: "
|
||||
f"completed={summary.get('completed_count', 0)}, "
|
||||
f"failed={summary.get('failed_count', 0)}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
_HANDLERS = {
|
||||
JOB_TYPE_SCAN_DATA: _handle_scan_data,
|
||||
JOB_TYPE_SCAN_ASSET_INVENTORY: _handle_scan_asset_inventory,
|
||||
@@ -4993,6 +5238,7 @@ _HANDLERS = {
|
||||
JOB_TYPE_IDL_RUN_DINSAR: _handle_idl_run_dinsar,
|
||||
JOB_TYPE_ISCE2_RUN: _handle_isce2_run,
|
||||
JOB_TYPE_PYINT_RUN: _handle_pyint_run,
|
||||
JOB_TYPE_LANDSAR_RUN: _handle_landsar_run,
|
||||
JOB_TYPE_WATER_GEOCODE: _handle_water_geocode,
|
||||
JOB_TYPE_SAR_SCENE_PREPROCESS: _handle_sar_scene_preprocess,
|
||||
JOB_TYPE_WATER_FLOOD: _handle_water_flood,
|
||||
@@ -5009,6 +5255,7 @@ _HANDLERS = {
|
||||
JOB_TYPE_SBAS_INTERFEROGRAMS: _handle_sbas_interferograms,
|
||||
JOB_TYPE_SBAS_IPTA_TIMESERIES: _handle_sbas_ipta_timeseries,
|
||||
JOB_TYPE_SBAS_GAMMA_WORKFLOW: _handle_sbas_gamma_workflow,
|
||||
JOB_TYPE_SBAS_LANDSAR_WORKFLOW: _handle_sbas_landsar_workflow,
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ def _clean_dict(payload: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
|
||||
def _kind_from_engine(engine_code: str) -> Optional[str]:
|
||||
normalized = str(engine_code or "").strip().lower()
|
||||
if normalized in {"envi", "sarscape"}:
|
||||
if normalized in {"envi", "sarscape", "landsar"}:
|
||||
return "windows"
|
||||
if normalized in {"isce2", "pyint", "gamma"}:
|
||||
return "wsl"
|
||||
|
||||
@@ -340,6 +340,8 @@ def _iter_flat_result_candidates(root_dir: str) -> Iterable[Dict[str, Any]]:
|
||||
|
||||
if is_standard_isce2_disp_file(normalized_root, entry.path):
|
||||
source_dir = os.path.dirname(os.path.dirname(os.path.dirname(entry.path)))
|
||||
run_meta = find_json_sidecar(source_dir, RUN_META_FILENAME, max_levels=0) or {}
|
||||
engine_code = _first_text(run_meta.get("engine_code")) or "isce2"
|
||||
source_files = [entry.path]
|
||||
coh_candidates = (
|
||||
os.path.join(source_dir, "assets", "coh", "coh.tif"),
|
||||
@@ -350,7 +352,7 @@ def _iter_flat_result_candidates(root_dir: str) -> Iterable[Dict[str, Any]]:
|
||||
source_files.append(coh_path)
|
||||
break
|
||||
yield {
|
||||
"engine_code": "isce2",
|
||||
"engine_code": engine_code,
|
||||
"name": os.path.splitext(entry.name)[0],
|
||||
"task_name": "",
|
||||
"source_dir": source_dir,
|
||||
|
||||
@@ -359,6 +359,16 @@ def _build_root_specs_from_settings() -> List[RootSpec]:
|
||||
owner_engine="dinsar",
|
||||
)
|
||||
)
|
||||
specs.extend(
|
||||
_iter_single_root_specs(
|
||||
env_var="LANDSAR_WORK_ROOT",
|
||||
path=settings.LANDSAR_WORK_ROOT,
|
||||
root_role="work_root_landsar",
|
||||
display_name="LandSAR Work Root",
|
||||
scan_mode="workspace",
|
||||
owner_engine="landsar",
|
||||
)
|
||||
)
|
||||
specs.extend(
|
||||
_iter_single_root_specs(
|
||||
env_var="TIMESERIES_PRODUCT_DIR",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@ from .pairing_state_service import pairing_state_service
|
||||
|
||||
PAIRING_POLICY_VERSION = "2026.05.raw-source.v2"
|
||||
PAIRING_WARNING_CANDIDATE_THRESHOLD = 3000
|
||||
PAIRING_ALL_STRATEGY_HARD_LIMIT = 20000
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -144,6 +145,12 @@ class SpatialService:
|
||||
require_orbit_data=require_orbit_data,
|
||||
)
|
||||
|
||||
if effective_params.strategy == "all" and len(candidate_pool) > PAIRING_ALL_STRATEGY_HARD_LIMIT:
|
||||
raise RuntimeError(
|
||||
f"全部配对命中 {len(candidate_pool)} 条候选边,超过系统一次性返回上限 "
|
||||
f"{PAIRING_ALL_STRATEGY_HARD_LIMIT}。请改用 SBAS/Sequential 策略,或收紧 AOI、日期范围、重叠率。"
|
||||
)
|
||||
|
||||
if len(candidate_pool) > PAIRING_WARNING_CANDIDATE_THRESHOLD:
|
||||
warnings.append(
|
||||
f"候选配对数超过 {PAIRING_WARNING_CANDIDATE_THRESHOLD}(当前: {len(candidate_pool)}),建议收紧参数或缩小 AOI。"
|
||||
|
||||
Reference in New Issue
Block a user