@@ -79,6 +79,8 @@ def __init__(self, parent_widget):
7979 self ._auto_venv_cache : dict [str , str ] = {}
8080 # Cache for manager-provided venv per workspace (None if not found)
8181 self ._manager_venv_cache : dict [str , str | None ] = {}
82+ # User-driven cancellation flag for long-running async flows
83+ self ._cancel_requested = False
8284
8385 # ---------- Workspace pref (.ark/pref.json) ----------
8486 def _workspace_pref_path (self , workspace_dir : str ) -> str :
@@ -561,13 +563,17 @@ def _done(code, _status):
561563 def ensure_tools_installed (self , venv_root : str , tools : list [str ]) -> None :
562564 """Asynchronously check/install the provided tools list with progress dialog."""
563565 try :
566+ self ._reset_cancel_state ()
564567 self ._venv_check_pkgs = list (tools )
565568 self ._venv_check_index = 0
566569 self ._venv_check_pip_exe = self .pip_path (venv_root )
567570 self ._venv_check_path = venv_root
568571 self ._venv_check_use_python = False
569572 self .venv_check_progress = ProgressDialog (
570- "Vérification du venv" , self .parent
573+ "Vérification du venv" , self .parent , cancelable = True
574+ )
575+ self ._bind_cancel_for_dialog (
576+ self .venv_check_progress , "vérification des outils du venv"
571577 )
572578 self .venv_check_progress .set_message (f"Vérification de { tools [0 ]} ..." )
573579 self .venv_check_progress .set_progress (0 , len (tools ))
@@ -581,6 +587,7 @@ def ensure_tools_installed(self, venv_root: str, tools: list[str]) -> None:
581587 def ensure_tools_installed_system (self , tools : list [str ]) -> None :
582588 """Asynchronously check/install tools in system Python using pip."""
583589 try :
590+ self ._reset_cancel_state ()
584591 self ._venv_check_pkgs = list (tools )
585592 self ._venv_check_index = 0
586593 self ._venv_check_pip_exe = sys .executable
@@ -589,7 +596,10 @@ def ensure_tools_installed_system(self, tools: list[str]) -> None:
589596 )
590597 self ._venv_check_use_python = True
591598 self .venv_check_progress = ProgressDialog (
592- "Vérification du Python système" , self .parent
599+ "Vérification du Python système" , self .parent , cancelable = True
600+ )
601+ self ._bind_cancel_for_dialog (
602+ self .venv_check_progress , "vérification des outils système"
593603 )
594604 self .venv_check_progress .set_message (f"Vérification de { tools [0 ]} ..." )
595605 self .venv_check_progress .set_progress (0 , len (tools ))
@@ -698,6 +708,41 @@ def _safe_log(
698708 except Exception :
699709 pass
700710
711+ def _reset_cancel_state (self ) -> None :
712+ try :
713+ self ._cancel_requested = False
714+ except Exception :
715+ pass
716+
717+ def _is_cancel_requested (self ) -> bool :
718+ try :
719+ return bool (getattr (self , "_cancel_requested" , False ))
720+ except Exception :
721+ return False
722+
723+ def _request_cancel (self , action_label : str | None = None ) -> None :
724+ if self ._is_cancel_requested ():
725+ return
726+ self ._cancel_requested = True
727+ suffix = f" ({ action_label } )" if action_label else ""
728+ self ._safe_log (
729+ f"🛑 Annulation demandée par l'utilisateur{ suffix } ." ,
730+ f"🛑 Cancellation requested by user{ suffix } ." ,
731+ level = "warning" ,
732+ )
733+ self .terminate_tasks ()
734+
735+ def _bind_cancel_for_dialog (self , dialog , action_label : str ) -> None :
736+ if dialog is None :
737+ return
738+ btn = getattr (dialog , "btn_cancel" , None )
739+ if btn is None :
740+ return
741+ try :
742+ btn .clicked .connect (lambda : self ._request_cancel (action_label ))
743+ except Exception :
744+ pass
745+
701746 def _is_stdlib_module (self , module_name : str ) -> bool :
702747 """Check if a module is part of Python's standard library."""
703748 try :
@@ -1309,6 +1354,7 @@ def _t(_key: str, fr: str, en: str) -> str:
13091354 # ---------- Existing venv: check and install tools ----------
13101355 def check_tools_in_venv (self , venv_path : str ):
13111356 try :
1357+ self ._reset_cancel_state ()
13121358 ok , reason = self .validate_venv_strict (venv_path )
13131359 if not ok :
13141360 self ._safe_log (f"❌ Invalid venv: { reason } " )
@@ -1336,7 +1382,10 @@ def _after_binding(ok_bind: bool):
13361382 self ._venv_check_pip_exe = pip_exe
13371383 self ._venv_check_path = venv_path
13381384 self .venv_check_progress = ProgressDialog (
1339- "Vérification du venv" , self .parent
1385+ "Vérification du venv" , self .parent , cancelable = True
1386+ )
1387+ self ._bind_cancel_for_dialog (
1388+ self .venv_check_progress , "vérification des outils du venv"
13401389 )
13411390 self .venv_check_progress .set_message ("Vérification de PyInstaller..." )
13421391 self .venv_check_progress .set_progress (0 , len (self ._venv_check_pkgs ))
@@ -1348,6 +1397,13 @@ def _after_binding(ok_bind: bool):
13481397 self ._safe_log (f"❌ Erreur lors de la vérification du venv: { e } " )
13491398
13501399 def _check_next_venv_pkg (self ):
1400+ if self ._is_cancel_requested ():
1401+ try :
1402+ if self .venv_check_progress :
1403+ self .venv_check_progress .close ()
1404+ except Exception :
1405+ pass
1406+ return
13511407 if self ._venv_check_index >= len (self ._venv_check_pkgs ):
13521408 try :
13531409 self .venv_check_progress .set_message ("Vérification terminée." )
@@ -1386,6 +1442,8 @@ def _check_next_venv_pkg(self):
13861442 def _on_venv_pkg_checked (self , process , code , status , pkg ):
13871443 if getattr (self .parent , "_closing" , False ):
13881444 return
1445+ if self ._is_cancel_requested ():
1446+ return
13891447 if code == 0 :
13901448 if self ._venv_check_use_python :
13911449 self ._safe_log (
@@ -1448,6 +1506,8 @@ def _on_venv_pkg_checked(self, process, code, status, pkg):
14481506 def _on_venv_check_output (self , process , error = False ):
14491507 if getattr (self .parent , "_closing" , False ):
14501508 return
1509+ if self ._is_cancel_requested ():
1510+ return
14511511 data = (
14521512 process .readAllStandardError ().data ().decode ()
14531513 if error
@@ -1743,6 +1803,8 @@ def select_best_venv(self, workspace_dir: str) -> str | None:
17431803 def _on_venv_pkg_installed (self , process , code , status , pkg ):
17441804 if getattr (self .parent , "_closing" , False ):
17451805 return
1806+ if self ._is_cancel_requested ():
1807+ return
17461808 if code == 0 :
17471809 self ._safe_log (f"✅ { pkg } installé dans le venv." )
17481810 else :
@@ -1795,6 +1857,7 @@ def create_venv_if_needed(self, path: str, prefer_manager: bool = True):
17951857
17961858 self ._safe_log ("🔧 Aucun venv trouvé, création automatique..." )
17971859 try :
1860+ self ._reset_cancel_state ()
17981861 # Recherche d'un python embarqué à côté de l'exécutable
17991862 python_candidate = None
18001863 exe_dir = os .path .dirname (sys .executable )
@@ -1846,7 +1909,10 @@ def create_venv_if_needed(self, path: str, prefer_manager: bool = True):
18461909 self ._safe_log (f"➡️ Utilisation de sys.executable : { python_candidate } " )
18471910
18481911 self .venv_progress_dialog = ProgressDialog (
1849- "Création de l'environnement virtuel" , self .parent
1912+ "Création de l'environnement virtuel" , self .parent , cancelable = True
1913+ )
1914+ self ._bind_cancel_for_dialog (
1915+ self .venv_progress_dialog , "création de l'environnement virtuel"
18501916 )
18511917 self .venv_progress_dialog .set_message ("Création du venv..." )
18521918
@@ -1883,6 +1949,8 @@ def create_venv_if_needed(self, path: str, prefer_manager: bool = True):
18831949 def _on_venv_output (self , process , error = False ):
18841950 if getattr (self .parent , "_closing" , False ):
18851951 return
1952+ if self ._is_cancel_requested ():
1953+ return
18861954 data = (
18871955 process .readAllStandardError ().data ().decode ()
18881956 if error
@@ -1902,6 +1970,9 @@ def _on_venv_output(self, process, error=False):
19021970 def _on_venv_created (self , process , code , status , venv_path ):
19031971 if getattr (self .parent , "_closing" , False ):
19041972 return
1973+ if self ._is_cancel_requested ():
1974+ self ._safe_log ("ℹ️ Création du venv annulée." )
1975+ return
19051976 if code == 0 :
19061977 self ._safe_log ("✅ Environnement virtuel créé avec succès." )
19071978 try :
@@ -2370,6 +2441,7 @@ def _start_requirements_install(
23702441 req_path : str ,
23712442 use_system_python : bool = False ,
23722443 ):
2444+ self ._reset_cancel_state ()
23732445 py_exe = sys .executable if use_system_python else self .python_path (venv_root )
23742446 if not os .path .isfile (py_exe ):
23752447 self ._safe_log (
@@ -2417,7 +2489,10 @@ def _start_requirements_install(
24172489 self ._req_use_system_python = bool (use_system_python )
24182490 self ._pip_phase = "ensurepip"
24192491 self .progress_dialog = ProgressDialog (
2420- "Installation des dépendances" , self .parent
2492+ "Installation des dépendances" , self .parent , cancelable = True
2493+ )
2494+ self ._bind_cancel_for_dialog (
2495+ self .progress_dialog , "installation des dépendances"
24212496 )
24222497 self .progress_dialog .set_message ("Activation de pip (ensurepip)..." )
24232498 process = QProcess (self .parent )
@@ -2445,6 +2520,8 @@ def _start_requirements_install(
24452520 def _on_pip_output (self , process , error = False ):
24462521 if getattr (self .parent , "_closing" , False ):
24472522 return
2523+ if self ._is_cancel_requested ():
2524+ return
24482525 data = (
24492526 process .readAllStandardError ().data ().decode ()
24502527 if error
@@ -2466,6 +2543,9 @@ def _on_pip_output(self, process, error=False):
24662543 def _on_pip_finished (self , process , code , status ):
24672544 if getattr (self .parent , "_closing" , False ):
24682545 return
2546+ if self ._is_cancel_requested ():
2547+ self ._safe_log ("ℹ️ Installation des dépendances annulée." )
2548+ return
24692549 phase = self ._pip_phase
24702550 if phase == "ensurepip" :
24712551 # Proceed to upgrade pip/setuptools/wheel regardless of ensurepip result
@@ -2611,6 +2691,9 @@ def has_active_tasks(self) -> bool:
26112691 return False
26122692
26132693 def terminate_tasks (self ):
2694+ had_active = self .has_active_tasks ()
2695+ if had_active :
2696+ self ._cancel_requested = True
26142697 # Kill processes
26152698 for attr in [
26162699 "_venv_create_process" ,
@@ -2624,6 +2707,13 @@ def terminate_tasks(self):
26242707 proc .kill ()
26252708 except Exception :
26262709 pass
2710+
2711+ if had_active :
2712+ self ._safe_log (
2713+ "🛑 Opérations venv interrompues." ,
2714+ "🛑 Venv operations interrupted." ,
2715+ level = "warning" ,
2716+ )
26272717 setattr (self , attr , None )
26282718 # Close dialogs
26292719 for dlg_attr in [
@@ -2977,6 +3067,7 @@ def create_venv_with_manager(
29773067 ):
29783068 """Create venv using the detected environment manager."""
29793069 try :
3070+ self ._reset_cancel_state ()
29803071 manager = self ._detect_environment_manager (workspace_dir )
29813072
29823073 if not venv_path :
@@ -3019,7 +3110,12 @@ def create_venv_with_manager(
30193110
30203111 # Execute command
30213112 self .venv_progress_dialog = ProgressDialog (
3022- f"Création du venv avec { manager } " , self .parent
3113+ f"Création du venv avec { manager } " ,
3114+ self .parent ,
3115+ cancelable = True ,
3116+ )
3117+ self ._bind_cancel_for_dialog (
3118+ self .venv_progress_dialog , f"création venv via { manager } "
30233119 )
30243120 self .venv_progress_dialog .set_message (f"Création du venv avec { manager } ..." )
30253121
@@ -3053,6 +3149,7 @@ def install_dependencies_with_manager(
30533149 ):
30543150 """Install dependencies using the detected environment manager."""
30553151 try :
3152+ self ._reset_cancel_state ()
30563153 manager = self ._detect_environment_manager (workspace_dir )
30573154
30583155 if not venv_path :
@@ -3107,7 +3204,10 @@ def install_dependencies_with_manager(
31073204
31083205 # Execute command
31093206 self .progress_dialog = ProgressDialog (
3110- f"Installation avec { manager } " , self .parent
3207+ f"Installation avec { manager } " , self .parent , cancelable = True
3208+ )
3209+ self ._bind_cancel_for_dialog (
3210+ self .progress_dialog , f"installation via { manager } "
31113211 )
31123212 self .progress_dialog .set_message (
31133213 f"Installation des dépendances avec { manager } ..."
@@ -3142,6 +3242,9 @@ def _on_manager_install_finished(self, process, code, status, manager):
31423242 """Callback after manager-based installation."""
31433243 if getattr (self .parent , "_closing" , False ):
31443244 return
3245+ if self ._is_cancel_requested ():
3246+ self ._safe_log (f"ℹ️ Installation via { manager } annulée." )
3247+ return
31453248
31463249 if code == 0 :
31473250 self ._safe_log (f"✅ Installation avec { manager } réussie." )
0 commit comments