fix: fenêtres fantômes PyInstaller — désactiver ProcessPoolExecutor en mode frozen
ProcessPoolExecutor relançait l'exe pour chaque sous-processus de rastérisation sous PyInstaller --onefile, créant une fenêtre GUI par page. En mode frozen, la rastérisation est maintenant séquentielle. Aussi: remplacement du mutex Windows par un file lock (msvcrt.locking) plus fiable pour la protection anti-multi-instance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3583,13 +3583,18 @@ def redact_pdf_raster(original_pdf: Path, audit: List[PiiHit], out_pdf: Path, dp
|
|||||||
image_rects_by_page[pno] = img_rects
|
image_rects_by_page[pno] = img_rects
|
||||||
doc.close() # fermer AVANT le fork
|
doc.close() # fermer AVANT le fork
|
||||||
|
|
||||||
n_workers = min(n_pages, os.cpu_count() or 4)
|
|
||||||
tasks = [
|
tasks = [
|
||||||
(str(original_pdf), pno, rects_as_tuples.get(pno, []), dpi, ogc_label, jpeg_quality,
|
(str(original_pdf), pno, rects_as_tuples.get(pno, []), dpi, ogc_label, jpeg_quality,
|
||||||
image_rects_by_page.get(pno, []))
|
image_rects_by_page.get(pno, []))
|
||||||
for pno in range(n_pages)
|
for pno in range(n_pages)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Mode frozen (PyInstaller --onefile) : ProcessPoolExecutor relance l'exe
|
||||||
|
# et ouvre des fenêtres GUI fantômes → séquentiel obligatoire
|
||||||
|
if getattr(sys, 'frozen', False) or n_pages <= 2:
|
||||||
|
results = sorted([_rasterize_page(t) for t in tasks], key=lambda x: x[0])
|
||||||
|
else:
|
||||||
|
n_workers = min(n_pages, os.cpu_count() or 4)
|
||||||
with ProcessPoolExecutor(max_workers=n_workers) as pool:
|
with ProcessPoolExecutor(max_workers=n_workers) as pool:
|
||||||
results = sorted(pool.map(_rasterize_page, tasks), key=lambda x: x[0])
|
results = sorted(pool.map(_rasterize_page, tasks), key=lambda x: x[0])
|
||||||
|
|
||||||
|
|||||||
222
launcher.py
Normal file
222
launcher.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Launcher Windows — single-instance + download models on first run + launch GUI."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox
|
||||||
|
from pathlib import Path
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Single-instance guard (lock file in user's temp directory)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_lock_file = None
|
||||||
|
_lock_fd = None
|
||||||
|
|
||||||
|
def _ensure_single_instance():
|
||||||
|
"""Prevent multiple instances using a lock file.
|
||||||
|
Works reliably on Windows and Linux, including PyInstaller --onefile."""
|
||||||
|
global _lock_file, _lock_fd
|
||||||
|
import tempfile
|
||||||
|
_lock_file = Path(tempfile.gettempdir()) / "anonymisation_chcb.lock"
|
||||||
|
try:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import msvcrt
|
||||||
|
_lock_fd = open(_lock_file, "w")
|
||||||
|
msvcrt.locking(_lock_fd.fileno(), msvcrt.LK_NBLCK, 1)
|
||||||
|
else:
|
||||||
|
import fcntl
|
||||||
|
_lock_fd = open(_lock_file, "w")
|
||||||
|
fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
return True
|
||||||
|
except (OSError, IOError):
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Path resolution for PyInstaller frozen exe
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
APP_DIR = Path(sys._MEIPASS)
|
||||||
|
EXE_DIR = Path(sys.executable).parent
|
||||||
|
else:
|
||||||
|
APP_DIR = Path(__file__).resolve().parent
|
||||||
|
EXE_DIR = APP_DIR
|
||||||
|
|
||||||
|
# Log file next to the exe
|
||||||
|
LOG_FILE = EXE_DIR / "anonymisation.log"
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=str(LOG_FILE),
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)s %(message)s",
|
||||||
|
)
|
||||||
|
log = logging.getLogger("launcher")
|
||||||
|
|
||||||
|
# Make embedded modules importable
|
||||||
|
sys.path.insert(0, str(APP_DIR))
|
||||||
|
os.chdir(str(APP_DIR))
|
||||||
|
|
||||||
|
log.info(f"APP_DIR={APP_DIR}")
|
||||||
|
log.info(f"EXE_DIR={EXE_DIR}")
|
||||||
|
log.info(f"frozen={getattr(sys, 'frozen', False)}")
|
||||||
|
|
||||||
|
MODELS_DIR = APP_DIR / "models"
|
||||||
|
|
||||||
|
|
||||||
|
def check_models_ready():
|
||||||
|
"""Check that the CamemBERT-bio ONNX model is present."""
|
||||||
|
onnx_path = MODELS_DIR / "camembert-bio-deid" / "onnx" / "model.onnx"
|
||||||
|
ok = onnx_path.exists()
|
||||||
|
log.info(f"CamemBERT ONNX present: {ok} ({onnx_path})")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def launch_gui():
|
||||||
|
"""Launch the main GUI."""
|
||||||
|
log.info("Launching GUI...")
|
||||||
|
try:
|
||||||
|
import anonymizer_core_refactored_onnx # noqa — pre-import the core
|
||||||
|
log.info("Core imported OK")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Core import error: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import Pseudonymisation_Gui_V5 # noqa
|
||||||
|
log.info("GUI imported OK, starting mainloop...")
|
||||||
|
root = tk.Tk()
|
||||||
|
Pseudonymisation_Gui_V5.App(root)
|
||||||
|
root.mainloop()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"GUI error: {e}\n{traceback.format_exc()}")
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Erreur", f"Erreur au lancement de l'interface:\n\n{e}\n\nVoir {LOG_FILE}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SetupWindow:
|
||||||
|
"""Setup window for first launch (model download)."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.root = tk.Tk()
|
||||||
|
self.root.title("Anonymisation — Premier lancement")
|
||||||
|
self.root.geometry("520x320")
|
||||||
|
self.root.resizable(False, False)
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.root, padding=20)
|
||||||
|
frame.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
ttk.Label(frame, text="Configuration initiale", font=("", 14, "bold")).pack(pady=(0, 10))
|
||||||
|
ttk.Label(
|
||||||
|
frame,
|
||||||
|
text="Les modeles NER vont etre telecharges depuis HuggingFace.\nCela peut prendre quelques minutes selon la connexion.",
|
||||||
|
justify="center",
|
||||||
|
).pack(pady=(0, 15))
|
||||||
|
|
||||||
|
self.progress = ttk.Progressbar(frame, mode="indeterminate", length=420)
|
||||||
|
self.progress.pack(pady=10)
|
||||||
|
|
||||||
|
self.status_var = tk.StringVar(value="En attente...")
|
||||||
|
ttk.Label(frame, textvariable=self.status_var).pack(pady=5)
|
||||||
|
|
||||||
|
self.btn = ttk.Button(frame, text="Demarrer le telechargement", command=self.start_download)
|
||||||
|
self.btn.pack(pady=15)
|
||||||
|
|
||||||
|
def start_download(self):
|
||||||
|
self.btn.configure(state="disabled")
|
||||||
|
self.progress.start(10)
|
||||||
|
threading.Thread(target=self._download_thread, daemon=True).start()
|
||||||
|
|
||||||
|
def _download_thread(self):
|
||||||
|
try:
|
||||||
|
# 1. EDS-Pseudo
|
||||||
|
self._update("Chargement EDS-Pseudo (telechargement HuggingFace)...")
|
||||||
|
log.info("Downloading EDS-Pseudo...")
|
||||||
|
try:
|
||||||
|
from eds_pseudo_manager import EdsPseudoManager
|
||||||
|
mgr = EdsPseudoManager()
|
||||||
|
mgr.load()
|
||||||
|
self._update("EDS-Pseudo OK")
|
||||||
|
log.info("EDS-Pseudo OK")
|
||||||
|
except Exception as e:
|
||||||
|
self._update(f"EDS-Pseudo: {e}")
|
||||||
|
log.warning(f"EDS-Pseudo failed: {e}")
|
||||||
|
|
||||||
|
# 2. GLiNER
|
||||||
|
self._update("Chargement GLiNER (telechargement HuggingFace)...")
|
||||||
|
log.info("Downloading GLiNER...")
|
||||||
|
try:
|
||||||
|
from gliner_manager import GlinerManager
|
||||||
|
mgr = GlinerManager()
|
||||||
|
mgr.load()
|
||||||
|
self._update("GLiNER OK")
|
||||||
|
log.info("GLiNER OK")
|
||||||
|
except Exception as e:
|
||||||
|
self._update(f"GLiNER: {e}")
|
||||||
|
log.warning(f"GLiNER failed: {e}")
|
||||||
|
|
||||||
|
# 3. CamemBERT-bio ONNX
|
||||||
|
self._update("Verification CamemBERT-bio ONNX...")
|
||||||
|
if check_models_ready():
|
||||||
|
self._update("CamemBERT-bio ONNX OK")
|
||||||
|
else:
|
||||||
|
self._update("CamemBERT-bio ONNX manquant")
|
||||||
|
log.error("CamemBERT-bio ONNX not found")
|
||||||
|
|
||||||
|
self._update("Configuration terminee ! Lancement de l'interface...")
|
||||||
|
log.info("Setup complete, launching GUI in 2s")
|
||||||
|
self.root.after(2000, self._finish)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Setup error: {e}\n{traceback.format_exc()}")
|
||||||
|
self._update(f"Erreur: {e}")
|
||||||
|
self.root.after(0, lambda: self.btn.configure(state="normal"))
|
||||||
|
|
||||||
|
def _update(self, msg):
|
||||||
|
self.root.after(0, lambda: self.status_var.set(msg))
|
||||||
|
|
||||||
|
def _finish(self):
|
||||||
|
self.progress.stop()
|
||||||
|
self.root.destroy()
|
||||||
|
launch_gui()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log.info("=== Demarrage Anonymisation ===")
|
||||||
|
|
||||||
|
# Single-instance check
|
||||||
|
if not _ensure_single_instance():
|
||||||
|
log.warning("Another instance is already running. Exiting.")
|
||||||
|
try:
|
||||||
|
messagebox.showwarning(
|
||||||
|
"Anonymisation",
|
||||||
|
"L'application est deja en cours d'execution.\n\n"
|
||||||
|
"Regardez dans la barre des taches.",
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if check_models_ready():
|
||||||
|
launch_gui()
|
||||||
|
else:
|
||||||
|
setup = SetupWindow()
|
||||||
|
setup.run()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Fatal error: {e}\n{traceback.format_exc()}")
|
||||||
|
try:
|
||||||
|
messagebox.showerror("Erreur fatale", f"{e}\n\nVoir {LOG_FILE}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user