206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
QGIS 常驻 Daemon 进程。
|
||
|
||
由主进程在 FastAPI lifespan 中启动,通过 stdin/stdout JSON 行协议通信。
|
||
|
||
启动时预加载所有模板到内存缓存,后续请求秒级响应。
|
||
|
||
协议(每行一个完整 JSON):
|
||
输入(stdin): {"config": {...}, "models": [...], "result_file": "path/to/result.json"}
|
||
输出(result_file): {"results": [...]}
|
||
控制: {"action": "shutdown"}
|
||
|
||
生命周期:
|
||
1. FastAPI 启动 → 启动 daemon 子进程
|
||
2. daemon 初始化 QGIS,加载磁盘缓存
|
||
3. 收到请求 → 处理模板 → 结果写入 result_file → stdout 输出完成标记
|
||
4. FastAPI 关闭 → 发送 shutdown → daemon 退出
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
# ============================================================
|
||
# 环境初始化(必须在 QGIS import 之前)
|
||
# ============================================================
|
||
|
||
QGIS_ROOT = os.environ.get("QGIS_ROOT", "D:/QGIS")
|
||
|
||
|
||
def _detect_qgis_app_dir():
|
||
for name in ("qgis-ltr", "qgis"):
|
||
d = os.path.join(QGIS_ROOT, "apps", name)
|
||
if os.path.isdir(d):
|
||
return d
|
||
raise RuntimeError(f"未找到 QGIS 应用目录: {QGIS_ROOT}\\apps\\qgis-ltr 或 qgis")
|
||
|
||
|
||
def _setup_environment():
|
||
os.environ["PYTHONUTF8"] = "1"
|
||
os.environ["GDAL_FILENAME_IS_UTF8"] = "YES"
|
||
os.environ["VSI_CACHE"] = "TRUE"
|
||
os.environ["VSI_CACHE_SIZE"] = "1000000"
|
||
|
||
if sys.platform == "win32":
|
||
import ctypes
|
||
qgis_app_dir = _detect_qgis_app_dir()
|
||
for dll_dir in [
|
||
os.path.join(qgis_app_dir, "bin"),
|
||
os.path.join(QGIS_ROOT, "apps", "Qt5", "bin"),
|
||
os.path.join(QGIS_ROOT, "apps", "gdal", "lib"),
|
||
]:
|
||
if os.path.isdir(dll_dir):
|
||
os.add_dll_directory(dll_dir)
|
||
|
||
_preload_dlls = [
|
||
"qgis_core.dll", "qgispython.dll",
|
||
"Qt5Core.dll", "Qt5Gui.dll", "Qt5Widgets.dll",
|
||
"Qt5Network.dll", "Qt5Svg.dll", "Qt5Xml.dll",
|
||
"Qt5Concurrent.dll", "Qt5PrintSupport.dll",
|
||
]
|
||
for dll_dir in [
|
||
os.path.join(qgis_app_dir, "bin"),
|
||
os.path.join(QGIS_ROOT, "apps", "Qt5", "bin"),
|
||
os.path.join(QGIS_ROOT, "apps", "gdal", "lib"),
|
||
]:
|
||
for dll_name in _preload_dlls:
|
||
dll_path = os.path.join(dll_dir, dll_name)
|
||
if os.path.isfile(dll_path):
|
||
try:
|
||
ctypes.WinDLL(dll_path)
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def _setup_python_path():
|
||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
if project_root not in sys.path:
|
||
sys.path.insert(0, project_root)
|
||
qgis_app_dir = _detect_qgis_app_dir()
|
||
qgis_python = os.path.join(qgis_app_dir, "python")
|
||
if os.path.isdir(qgis_python) and qgis_python not in sys.path:
|
||
sys.path.insert(0, qgis_python)
|
||
|
||
|
||
def _scan_templates() -> list[str]:
|
||
"""扫描所有模板文件,返回绝对路径列表"""
|
||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
template_base = os.path.join(project_root, "app", "data", "template")
|
||
templates = []
|
||
for event_type in ["rainfall", "earthquake"]:
|
||
tdir = os.path.join(template_base, event_type)
|
||
if not os.path.isdir(tdir):
|
||
continue
|
||
for tf in sorted(os.listdir(tdir)):
|
||
if tf.endswith(".qgz") and not tf.startswith("tmp"):
|
||
templates.append(os.path.join(tdir, tf).replace("\\", "/"))
|
||
return templates
|
||
|
||
|
||
# ============================================================
|
||
# 主逻辑
|
||
# ============================================================
|
||
|
||
def main():
|
||
_setup_environment()
|
||
_setup_python_path()
|
||
|
||
from qgis.core import QgsApplication, QgsProject
|
||
|
||
# 初始化 QGIS(只做一次)
|
||
qgis_app_dir = _detect_qgis_app_dir()
|
||
QgsApplication.setPrefixPath(qgis_app_dir, True)
|
||
qgs_app = QgsApplication([], False)
|
||
qgs_app.initQgis()
|
||
|
||
from app.services.qgis.map_service import MapService, template_cache
|
||
from app.services.qgis.template_modifier import TemplateModifier
|
||
from app.config.qgis_mappings import build_static_layers_config, get_gpkg_dir
|
||
|
||
# 从磁盘恢复已缓存的模板
|
||
cached_count = template_cache.load_persistent_cache()
|
||
print(f"[qgis_daemon] QGIS 已启动, 磁盘缓存: {cached_count} 个模板",
|
||
file=sys.stderr, flush=True)
|
||
|
||
total_cached = len(template_cache._cache)
|
||
print(f"[qgis_daemon] 就绪: 缓存共{total_cached}个模板",
|
||
file=sys.stderr, flush=True)
|
||
|
||
# ── 请求处理循环 ──
|
||
print("[qgis_daemon] 等待请求...", file=sys.stderr, flush=True)
|
||
|
||
for line in sys.stdin:
|
||
line = line.strip()
|
||
if not line:
|
||
continue
|
||
|
||
try:
|
||
request = json.loads(line)
|
||
except json.JSONDecodeError as e:
|
||
resp = json.dumps({"status": "error", "message": f"JSON 解析失败: {e}"})
|
||
sys.stdout.write(resp + "\n")
|
||
sys.stdout.flush()
|
||
continue
|
||
|
||
action = request.get("action", "map")
|
||
|
||
if action == "shutdown":
|
||
print("[qgis_daemon] 收到 shutdown,退出", file=sys.stderr, flush=True)
|
||
break
|
||
|
||
if action == "map":
|
||
config = request["config"]
|
||
models = request["models"]
|
||
result_file = request.get("result_file", "")
|
||
results = []
|
||
|
||
t_batch = time.time()
|
||
if "static_layers" not in config or not config["static_layers"].get("gpkg_dir"):
|
||
config["static_layers"] = build_static_layers_config(get_gpkg_dir())
|
||
|
||
service = MapService(config)
|
||
|
||
for i, model in enumerate(models):
|
||
t_model = time.time()
|
||
try:
|
||
name = service.generate(model)
|
||
results.append({"name": name, "output": model["outFile"]})
|
||
elapsed = time.time() - t_model
|
||
print(f"[qgis_daemon] [{i+1}/{len(models)}] {name} ({elapsed:.1f}s)",
|
||
file=sys.stderr, flush=True)
|
||
except Exception as e:
|
||
elapsed = time.time() - t_model
|
||
results.append({"name": model.get("name", ""), "output": "", "error": str(e)})
|
||
print(f"[qgis_daemon] [{i+1}/{len(models)}] 失败: {e}",
|
||
file=sys.stderr, flush=True)
|
||
|
||
total = time.time() - t_batch
|
||
ok = sum(1 for r in results if "error" not in r)
|
||
print(f"[qgis_daemon] 完成: {ok}/{len(models)}, {total:.1f}s",
|
||
file=sys.stderr, flush=True)
|
||
|
||
resp = {"results": results}
|
||
|
||
# 写入结果文件(可靠),然后 stdout 发完成信号
|
||
if result_file:
|
||
try:
|
||
with open(result_file, "w", encoding="utf-8") as f:
|
||
json.dump(resp, f, ensure_ascii=False)
|
||
except Exception as e:
|
||
print(f"[qgis_daemon] 写结果文件失败: {e}", file=sys.stderr, flush=True)
|
||
|
||
sys.stdout.write("OK\n")
|
||
sys.stdout.flush()
|
||
|
||
# 清理
|
||
project = QgsProject.instance()
|
||
project.clear()
|
||
template_cache.cleanup()
|
||
print("[qgis_daemon] 已退出", file=sys.stderr, flush=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|