2020y compris le démarrage, la surveillance, la gestion des erreurs et l'annulation
2121des compilations. Il gère également l'interface utilisateur dynamique pour les
2222moteurs externes et l'installation automatique des modules manquants.
23+
24+ Inspiré de OnlyMod/EngineOnlyMod/gui.py avec les améliorations suivantes:
25+ - Utilisation du registry pour conserver l'état du moteur (configuration UI)
26+ - Mesure précise du temps de compilation avec horodatage
27+ - Gestion de l'annulation via flag atomic (compatible avec select())
28+ - Hooks on_success différés utilisant l'instance engine du registry
29+ - Support complet du pont UI JSON pour les moteurs externes
30+
31+ Auteur: Ague Samuel Amen
32+ Version: 2.0.0
2333"""
2434
2535# ============================================================================
3747import platform # Détection du système d'exploitation (Windows/Linux)
3848import re # Expressions régulières (détection de modules manquants)
3949import subprocess # Exécution de processus externes (pip install)
40- import time # Minuteries et délais (pour pip install retry)
50+ import time # Minuteries et délais (pour pip install retry et mesure de temps)
51+ from datetime import datetime # Horodatage précis pour la mesure de performance
4152from typing import Any , Optional # Type hints pour les fonctions
4253
4354# --- Imports du framework PySide6 (Qt pour Python) ---
4455# QProcess : Gestion des processus externes avec signaux/slots Qt
4556# QTimer : Minuteries pour les timeouts et délais de grâce
46- from PySide6 .QtCore import QProcess , QTimer
57+ from PySide6 .QtCore import QProcess , QTimer , QAtomicInt , QThread , Signal # QAtomicInt pour flag d'annulation thread-safe
58+
59+ # --- Imports pour le threading avancé (inspiré de gui.py) ---
60+ import threading
61+ import select
62+ import time
4763
4864# --- Imports des widgets Qt utilisés pour l'interface dynamique ---
4965# QCheckBox : Cases à cocher pour les options dynamiques
7692
7793# Note : Support ACASL supprimé (obsolète)
7894
95+ # ============================================================================
96+ # SECTION 0 : COMPILATION THREAD (INSPIRED FROM gui.py)
97+ # ============================================================================
98+ # Thread avancé pour l'exécution de la compilation sans bloquer l'interface Qt
99+ # Inspiré de OnlyMod/EngineOnlyMod/gui.py pour une meilleure robustesse
100+ # ============================================================================
101+
102+ class CompilationThread (QThread ):
103+ """Thread pour exécuter la compilation sans bloquer l'UI."""
104+
105+ output_ready = Signal (str )
106+ error_ready = Signal (str )
107+ finished = Signal (int )
108+
109+ def __init__ (self , program , args , env , working_dir = None , cancel_file = None ):
110+ super ().__init__ ()
111+ self .program = program
112+ self .args = args
113+ self .env = env
114+ self .working_dir = working_dir
115+ self .cancel_file = cancel_file
116+ self .cancel_requested = False
117+ self .process = None
118+ self ._collected_stderr = [] # Pour collecter stderr pour try_install_missing_modules
119+
120+ def run (self ):
121+ """Exécute le processus de compilation."""
122+ try :
123+ proc = subprocess .Popen (
124+ [self .program ] + self .args ,
125+ stdout = subprocess .PIPE ,
126+ stderr = subprocess .PIPE ,
127+ text = True ,
128+ env = self .env ,
129+ cwd = self .working_dir ,
130+ bufsize = 1 ,
131+ )
132+
133+ import select
134+ import time
135+
136+ # Utiliser select pour lire stdout et stderr en temps réel
137+ while True :
138+ # Vérifier si l'annulation a été demandée
139+ if self .cancel_requested :
140+ proc .terminate ()
141+ try :
142+ proc .wait (timeout = 5 ) # Attendre que le processus se termine
143+ except subprocess .TimeoutExpired :
144+ proc .kill () # Forcer la terminaison si nécessaire
145+ if self .finished :
146+ self .finished .emit (- 1 ) # Code spécial pour annulation
147+ return
148+
149+ # Vérifier si le fichier sentinelle d'annulation existe
150+ if self .cancel_file and os .path .isfile (self .cancel_file ):
151+ proc .terminate ()
152+ try :
153+ proc .wait (timeout = 5 )
154+ except subprocess .TimeoutExpired :
155+ proc .kill ()
156+ if self .finished :
157+ self .finished .emit (- 1 )
158+ return
159+
160+ # Vérifier si le processus est terminé
161+ if proc .poll () is not None :
162+ break
163+
164+ # Utiliser select pour attendre des données sur stdout ou stderr
165+ ready , _ , _ = select .select ([proc .stdout , proc .stderr ], [], [], 0.1 )
166+
167+ for stream in ready :
168+ if stream == proc .stdout and self .output_ready :
169+ line = proc .stdout .readline ()
170+ if line :
171+ self .output_ready .emit (line .rstrip ())
172+ elif stream == proc .stderr and self .error_ready :
173+ line = proc .stderr .readline ()
174+ if line :
175+ line_str = line .rstrip ()
176+ self ._collected_stderr .append (line_str ) # Collecter pour try_install_missing_modules
177+ self .error_ready .emit (line_str )
178+
179+ time .sleep (0.01 ) # Petit délai pour éviter la surcharge CPU
180+
181+ # Lire tout ce qui reste dans les buffers après la fin du processus
182+ remaining_stdout = proc .stdout .read ()
183+ if remaining_stdout and self .output_ready :
184+ for line in remaining_stdout .strip ().split ("\n " ):
185+ if line :
186+ self .output_ready .emit (line .rstrip ())
187+
188+ remaining_stderr = proc .stderr .read ()
189+ if remaining_stderr and self .error_ready :
190+ for line in remaining_stderr .strip ().split ("\n " ):
191+ if line :
192+ self .error_ready .emit (line .rstrip ())
193+
194+ # Signaler la fin
195+ return_code = proc .returncode
196+ if self .finished :
197+ self .finished .emit (return_code )
198+
199+ except Exception as e :
200+ if self .error_ready :
201+ self .error_ready .emit (f"Error: { str (e )} " )
202+ if self .finished :
203+ self .finished .emit (1 )
204+
205+ def cancel (self ):
206+ """Demande l'annulation de la compilation."""
207+ self .cancel_requested = True
208+
209+
79210# ============================================================================
80211# SECTION 0 : VENV/PIP HELPERS (MOVED FROM engine_sdk/utils.py)
81212# ============================================================================
@@ -415,37 +546,42 @@ def start_compilation_process(self, file):
415546 cancel_file = None
416547
417548 # ========================================================================
418- # Étape 10: Création et configuration du processus QProcess
549+ # Étape 10: Création et configuration du thread de compilation
419550 # ========================================================================
420- process = QProcess (self )
421- if penv is not None :
422- try :
423- process .setProcessEnvironment (penv )
424- except Exception :
425- pass
426- process .setProgram (program )
427- process .setArguments (args )
428- process .setWorkingDirectory (self .workspace_dir )
429- process .file_path = file
430- process .file_basename = file_basename
431- process ._start_time = time .time ()
432- process ._engine_id = engine_id
433- process ._cancel_file = cancel_file
551+ # Utiliser CompilationThread au lieu de QProcess pour une meilleure isolation
552+ env_dict = dict (penv .systemEnvironment ()) if penv else os .environ .copy ()
553+ if self .workspace_dir :
554+ env_dict ["ARK_WORKSPACE" ] = self .workspace_dir
555+
556+ compilation_thread = CompilationThread (
557+ program = program ,
558+ args = args ,
559+ env = env_dict ,
560+ working_dir = self .workspace_dir ,
561+ cancel_file = cancel_file
562+ )
563+
564+ # Stocker les métadonnées dans le thread (similaire à QProcess)
565+ compilation_thread .file_path = file
566+ compilation_thread .file_basename = file_basename
567+ compilation_thread ._start_time = time .time ()
568+ compilation_thread ._engine_id = engine_id
569+ compilation_thread ._cancel_file = cancel_file
434570
435571 # ========================================================================
436- # Étape 11: Connexion des signaux du processus
572+ # Étape 11: Connexion des signaux du thread
437573 # ========================================================================
438- # readyReadStandardOutput : données stdout reçues
439- # readyReadStandardError : données stderr reçues
440- # finished: processus terminé
441- process . readyReadStandardOutput .connect (lambda p = process : self .handle_stdout (p ))
442- process . readyReadStandardError .connect (lambda p = process : self .handle_stderr (p ))
443- process .finished .connect (lambda ec , es , p = process : self .handle_finished (p , ec , es ))
574+ # output_ready : données stdout reçues
575+ # error_ready : données stderr reçues
576+ # finished: thread terminé
577+ compilation_thread . output_ready .connect (lambda line : self .handle_stdout (compilation_thread , line ))
578+ compilation_thread . error_ready .connect (lambda line : self .handle_stderr (compilation_thread , line ))
579+ compilation_thread .finished .connect (lambda ec : self .handle_finished (compilation_thread , ec , 0 ))
444580
445581 # ========================================================================
446- # Étape 12: Ajout aux processus actifs
582+ # Étape 12: Ajout aux processus actifs (threads)
447583 # ========================================================================
448- self .processes .append (process )
584+ self .processes .append (compilation_thread )
449585 self .current_compiling .add (file )
450586
451587 # ========================================================================
@@ -481,79 +617,67 @@ def start_compilation_process(self, file):
481617 t .setSingleShot (True )
482618
483619 # Fonction appelée lors du timeout
484- def _on_timeout (proc = process , seconds = timeout_sec ):
620+ def _on_timeout (thread = compilation_thread , seconds = timeout_sec ):
485621 try :
486622 self .log .append (
487- f"⏱️ Timeout ({ seconds } s) pour { getattr (proc , 'file_basename' , '?' )} . Arrêt en cours…"
623+ f"⏱️ Timeout ({ seconds } s) pour { getattr (thread , 'file_basename' , '?' )} . Arrêt en cours…"
488624 )
489625 except Exception :
490626 pass
491- # Tentative d'arrêt propre (SIGTERM)
492- try :
493- if proc .state () != QProcess .NotRunning :
494- proc .terminate ()
495- except Exception :
496- pass
497- # Si l'arrêt propre échoue, on force le kill
627+ # Demander l'annulation propre du thread
498628 try :
499- proc . kill ()
629+ thread . cancel ()
500630 except Exception :
501631 pass
502632
503633 # ========================================================================
504634 # Délai de grâce avant kill forcé
505635 # ========================================================================
506- # On attend 10 secondes pour que le processus se termine proprement
636+ # On attend 10 secondes pour que le thread se termine proprement
507637 grace = QTimer (self )
508638 grace .setSingleShot (True )
509639
510- def _force_kill (p = proc ):
511- if p . state () != QProcess . NotRunning :
640+ def _force_kill (thr = thread ):
641+ if thr . isRunning () :
512642 try :
513643 self .log .append (
514- f"🛑 Arrêt forcé du processus { getattr (p , 'file_basename' , '?' )} après délai de grâce."
644+ f"🛑 Arrêt forcé du thread { getattr (thr , 'file_basename' , '?' )} après délai de grâce."
515645 )
516646 except Exception :
517647 pass
518- # Kill full process tree if possible
648+ # Terminer le thread de force
519649 try :
520- pid2 = int (p .processId ())
521- except Exception :
522- pid2 = None
523- if pid2 :
524- _kill_process_tree (pid2 , timeout = 3.0 , log = self .log .append )
525- try :
526- p .kill ()
650+ thr .terminate ()
527651 except Exception :
528652 pass
529653
530654 grace .timeout .connect (_force_kill )
531655 grace .start (10000 ) # 10s grâce
532- proc ._grace_kill_timer = grace
656+ thread ._grace_kill_timer = grace
533657
534658 # Connexion de la minuterie et démarrage
535659 t .timeout .connect (_on_timeout )
536660 t .start (int (timeout_sec * 1000 ))
537- process ._timeout_timer = t
661+ compilation_thread ._timeout_timer = t
538662
539- # Fonction pour annuler la minuterie quand le processus finit normalement
540- def _cancel_timer (_ec , _es , timer = t ):
663+ # Fonction pour annuler la minuterie quand le thread finit normalement
664+ def _cancel_timer (ec , timer = t ):
541665 try :
542666 timer .stop ()
543667 except Exception :
544668 pass
545669
546- process .finished .connect (_cancel_timer )
670+ compilation_thread .finished .connect (_cancel_timer )
547671 except Exception :
548672 pass
549673
550674 # ========================================================================
551- # Étape 15: Lancement du processus de compilation
675+ # Étape 15: Lancement du thread de compilation
552676 # ========================================================================
553- process .start ()
677+ compilation_thread .start ()
554678
555679
556- def handle_stdout (self , process ):
680+ def handle_stdout (self , process , line = None ):
557681 """
558682 Gestionnaire pour la sortie standard du processus de compilation.
559683
@@ -563,15 +687,21 @@ def handle_stdout(self, process):
563687
564688 Args:
565689 self: Instance de la classe principale (GUI)
566- process: Objet QProcess du processus en cours
690+ process: Objet CompilationThread ou QProcess du processus en cours
691+ line: Ligne de sortie (pour CompilationThread) ou None (pour QProcess)
567692
568693 Returns:
569694 None
570695 """
571696 # ========================================================================
572697 # Lecture des données stdout du processus
573698 # ========================================================================
574- data = process .readAllStandardOutput ().data ().decode ()
699+ if line is not None :
700+ # Mode CompilationThread : ligne déjà fournie
701+ data = line
702+ else :
703+ # Mode QProcess : lecture depuis le processus
704+ data = process .readAllStandardOutput ().data ().decode ()
575705
576706 # ========================================================================
577707 # Traitement des événements JSON Lines
@@ -1216,7 +1346,7 @@ def handle_stderr(self, process):
12161346 self .log .append (f"<span style='color:red;'>{ data } </span>" )
12171347
12181348
1219- def handle_finished (self , process , exit_code , exit_status ):
1349+ def handle_finished (self , process , exit_code , exit_status = 0 ):
12201350 # Suppression de la réactivation ici (gérée à la toute fin dans try_start_processes)
12211351 import time
12221352
@@ -1354,7 +1484,18 @@ def handle_finished(self, process, exit_code, exit_status):
13541484
13551485
13561486def try_install_missing_modules (self , process ):
1357- output = process .readAllStandardError ().data ().decode ()
1487+ # Handle both QProcess and CompilationThread
1488+ try :
1489+ if hasattr (process , 'readAllStandardError' ):
1490+ # QProcess mode
1491+ output = process .readAllStandardError ().data ().decode ()
1492+ else :
1493+ # CompilationThread mode - stderr is emitted line by line
1494+ # We need to collect stderr from the thread or use a different approach
1495+ output = getattr (process , '_collected_stderr' , '' )
1496+ except Exception :
1497+ output = ''
1498+
13581499 missing_modules = re .findall (r"No module named '([\w\d_]+)'" , output )
13591500 if not hasattr (self , "_already_tried_modules" ):
13601501 self ._already_tried_modules = set ()
0 commit comments