Skip to content

Commit c68f1fe

Browse files
committed
Amélioration de la gestion de l'UI pendant la compilation et renforcement de la logique métier (process killer et détection internet)
1 parent b89ab85 commit c68f1fe

5 files changed

Lines changed: 181 additions & 81 deletions

File tree

Core/Compiler/utils.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -471,23 +471,52 @@ def check_module_available(module_name: str, python_path: Optional[str] = None)
471471
except Exception:
472472
return False
473473

474-
def check_internet_connection(timeout: float = 3.0) -> bool:
474+
def check_internet_connection(timeout: float = 3.0, retries: int = 1) -> bool:
475475
"""
476-
Check if internet connection is available by trying to connect to a reliable host.
476+
Check if internet connection is available with high certainty.
477+
Uses a multi-tiered approach: DNS, socket connection, and optional HTTP.
477478
478479
Args:
479-
timeout: Timeout in seconds for the connection attempt.
480+
timeout: Timeout in seconds for each attempt.
481+
retries: Number of retries before giving up.
480482
481483
Returns:
482484
True if internet is available, False otherwise.
483485
"""
484486
import socket
485-
hosts = [("8.8.8.8", 53), ("1.1.1.1", 53), ("www.google.com", 80)]
486-
for host, port in hosts:
487-
try:
488-
socket.setdefaulttimeout(timeout)
489-
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
490-
return True
491-
except (socket.timeout, socket.error):
492-
continue
487+
import http.client
488+
489+
# Tier 1: Fast DNS check (Public DNS)
490+
# Using 8.8.8.8 (Google) and 1.1.1.1 (Cloudflare)
491+
dns_hosts = [("8.8.8.8", 53), ("1.1.1.1", 53), ("8.8.4.4", 53)]
492+
493+
for attempt in range(retries + 1):
494+
for host, port in dns_hosts:
495+
try:
496+
socket.setdefaulttimeout(timeout)
497+
# Try to establish a socket connection (TCP)
498+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
499+
s.connect((host, port))
500+
s.close()
501+
return True
502+
except (socket.timeout, socket.error):
503+
continue
504+
505+
# Tier 2: HTTP check (if Tier 1 fails, to handle captive portals or DNS blocks)
506+
http_hosts = ["www.google.com", "www.bing.com", "www.github.com"]
507+
for host in http_hosts:
508+
try:
509+
conn = http.client.HTTPSConnection(host, timeout=timeout)
510+
conn.request("HEAD", "/")
511+
res = conn.getresponse()
512+
conn.close()
513+
if res.status < 400:
514+
return True
515+
except Exception:
516+
continue
517+
518+
if attempt < retries:
519+
import time
520+
time.sleep(1.0) # Wait before retry
521+
493522
return False

Core/process_killer.py

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -99,40 +99,41 @@ def kill_process_tree(self, pid: int, include_parent: bool = True) -> bool:
9999
def _kill_with_psutil(self, pid: int, include_parent: bool) -> bool:
100100
try:
101101
parent = psutil.Process(pid)
102-
children = parent.children(recursive=True)
102+
# Use a set to avoid duplicates and handle circular dependencies if any
103+
all_procs = set(parent.children(recursive=True))
104+
if include_parent:
105+
all_procs.add(parent)
103106

104-
# Send SIGTERM/Terminate to everyone
105-
for child in children:
106-
try:
107-
child.terminate()
108-
except psutil.NoSuchProcess:
109-
pass
107+
# Sort by depth if possible, or just kill children first
108+
procs = sorted(list(all_procs), key=lambda p: len(p.parents()), reverse=True)
110109

111-
if include_parent:
110+
# Phase 1: SIGTERM / Terminate
111+
for p in procs:
112112
try:
113-
parent.terminate()
114-
except psutil.NoSuchProcess:
113+
if p.is_running():
114+
p.terminate()
115+
except (psutil.NoSuchProcess, psutil.AccessDenied):
115116
pass
116-
117-
# Wait for them to exit
118-
procs = children + ([parent] if include_parent else [])
117+
118+
# Wait for they to exit
119119
gone, alive = psutil.wait_procs(procs, timeout=self.timeout / 2)
120120

121-
# Force kill remaining
121+
# Phase 2: Force kill remaining (SIGKILL)
122122
for p in alive:
123123
try:
124-
_logger.warning("Forcing kill on PID %d", p.pid)
125-
p.kill()
126-
except psutil.NoSuchProcess:
124+
if p.is_running():
125+
_logger.warning("Forcing kill on PID %d (%s)", p.pid, p.name())
126+
p.kill()
127+
except (psutil.NoSuchProcess, psutil.AccessDenied):
127128
pass
128129

129-
# Final wait to avoid zombies
130+
# Final wait to reap zombies
130131
if alive:
131-
psutil.wait_procs(alive, timeout=1.0)
132+
psutil.wait_procs(alive, timeout=2.0)
132133

133-
return True
134+
return not any(p.is_running() for p in procs)
134135
except psutil.NoSuchProcess:
135-
return False
136+
return True # Already gone
136137
except Exception as e:
137138
_logger.error("Error killing process tree with psutil: %s", e)
138139
return False
@@ -157,32 +158,68 @@ def _kill_with_taskkill(self, pid: int, include_parent: bool) -> bool:
157158
def _kill_with_signals(self, pid: int, include_parent: bool) -> bool:
158159
"""Unix fallback using signals and process groups."""
159160
try:
160-
# If start_new_session=True was used, pid is the pgid
161+
# Try to get the process group ID
162+
pgid = -1
161163
try:
162164
pgid = os.getpgid(pid)
163-
if pgid == pid:
164-
# Kill the whole group
165+
except Exception:
166+
pass
167+
168+
if pgid > 1: # Avoid killing system-wide groups
169+
try:
170+
# Terminate the whole group
165171
os.killpg(pgid, signal.SIGTERM)
166-
time.sleep(self.timeout / 2)
172+
# Wait a bit
173+
for _ in range(int(self.timeout * 2)):
174+
if not self._is_alive(pid):
175+
break
176+
time.sleep(0.5)
177+
167178
if self._is_alive(pid):
179+
_logger.warning("Forcing kill on process group %d", pgid)
168180
os.killpg(pgid, signal.SIGKILL)
169181
return True
170-
except Exception:
171-
pass
182+
except Exception as e:
183+
_logger.debug("Group kill failed, falling back to PID: %s", e)
184+
185+
# Fallback to killing only the pid and trying to find children via /proc
186+
if not _IS_WINDOWS:
187+
self._kill_unix_children_fallback(pid)
172188

173-
# Fallback to killing only the pid if group killing failed
174189
if include_parent:
175-
os.kill(pid, signal.SIGTERM)
176-
time.sleep(self.timeout / 2)
177-
if self._is_alive(pid):
178-
os.kill(pid, signal.SIGKILL)
190+
try:
191+
os.kill(pid, signal.SIGTERM)
192+
time.sleep(self.timeout / 2)
193+
if self._is_alive(pid):
194+
os.kill(pid, signal.SIGKILL)
195+
except ProcessLookupError:
196+
pass
179197
return True
180-
except ProcessLookupError:
181-
return False
182198
except Exception as e:
183199
_logger.error("Error killing process with signals: %s", e)
184200
return False
185201

202+
def _kill_unix_children_fallback(self, ppid: int) -> None:
203+
"""Fallback to kill children on Unix without psutil or pgid."""
204+
try:
205+
# Try using pgrep to find children
206+
result = subprocess.run(
207+
["pgrep", "-P", str(ppid)],
208+
capture_output=True,
209+
text=True,
210+
check=False
211+
)
212+
if result.returncode == 0:
213+
for line in result.stdout.splitlines():
214+
child_pid = int(line.strip())
215+
self._kill_unix_children_fallback(child_pid)
216+
try:
217+
os.kill(child_pid, signal.SIGKILL)
218+
except Exception:
219+
pass
220+
except Exception:
221+
pass
222+
186223
def kill_by_name(self, name: str, ignore_case: bool = True) -> int:
187224
"""
188225
Kill all processes matching the given name.
@@ -217,22 +254,27 @@ def kill_by_name(self, name: str, ignore_case: bool = True) -> int:
217254
return killed_count
218255

219256
def _is_alive(self, pid: int) -> bool:
257+
if pid <= 0:
258+
return False
220259
if psutil:
221260
try:
222-
return psutil.Process(pid).is_running()
261+
p = psutil.Process(pid)
262+
return p.is_running() and p.status() != psutil.STATUS_ZOMBIE
223263
except psutil.NoSuchProcess:
224264
return False
225265

226266
try:
227267
if _IS_WINDOWS:
268+
# Tasklist can be slow, but it's reliable for existence
228269
res = subprocess.run(
229-
["tasklist", "/FI", f"PID eq {pid}"],
270+
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
230271
capture_output=True,
231272
text=True,
232273
check=False,
233274
)
234275
return str(pid) in res.stdout
235276
else:
277+
# On Unix, kill(pid, 0) checks if process exists
236278
os.kill(pid, 0)
237279
return True
238280
except (ProcessLookupError, PermissionError):

Ui/Gui/IdeLikeGui/connections.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,10 @@ def init_ide_like_ui(self) -> None:
656656
except Exception:
657657
pass
658658
_schedule_ide_like_async_init(self)
659+
try:
660+
self.set_controls_enabled(True)
661+
except Exception:
662+
pass
659663

660664

661665
def _setup_status_bar(self) -> None:

Ui/Gui/UiConnection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,10 @@ def init_ui(self) -> None:
603603
except Exception:
604604
pass
605605
_show_initial_help_message(self)
606+
try:
607+
self.set_controls_enabled(True)
608+
except Exception:
609+
pass
606610

607611

608612
# =========================================================================

Ui/Gui/UiFeatures.py

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -347,26 +347,40 @@ def update_command_preview(self):
347347

348348
def set_controls_enabled(self, enabled: bool) -> None:
349349
"""Enable or disable primary UI controls."""
350-
self.compile_btn.setEnabled(enabled)
351-
try:
352-
if self.compile_btn and hasattr(self.compile_btn, "style"):
353-
self.compile_btn.style().unpolish(self.compile_btn)
354-
self.compile_btn.style().polish(self.compile_btn)
355-
self.compile_btn.update()
356-
except Exception:
357-
pass
358-
self.cancel_btn.setEnabled(not enabled)
359-
self.btn_select_folder.setEnabled(enabled)
360-
self.btn_select_files.setEnabled(enabled)
361-
self.btn_remove_file.setEnabled(enabled)
350+
if hasattr(self, "compile_btn") and self.compile_btn:
351+
self.compile_btn.setEnabled(enabled)
352+
try:
353+
if hasattr(self.compile_btn, "style"):
354+
self.compile_btn.style().unpolish(self.compile_btn)
355+
self.compile_btn.style().polish(self.compile_btn)
356+
self.compile_btn.update()
357+
except Exception:
358+
pass
359+
360+
if hasattr(self, "cancel_btn") and self.cancel_btn:
361+
self.cancel_btn.setEnabled(not enabled)
362+
363+
if hasattr(self, "btn_select_folder") and self.btn_select_folder:
364+
self.btn_select_folder.setEnabled(enabled)
365+
if hasattr(self, "btn_select_files") and self.btn_select_files:
366+
self.btn_select_files.setEnabled(enabled)
367+
if hasattr(self, "btn_remove_file") and self.btn_remove_file:
368+
self.btn_remove_file.setEnabled(enabled)
362369

363370
for attr in (
364371
"btn_suggest_deps",
365372
"btn_bc_loader",
373+
"btn_acasl_loader",
366374
"select_lang",
367375
"select_theme",
368376
"btn_show_stats",
369377
"btn_clear_workspace",
378+
"btn_help",
379+
"btn_lock_manager",
380+
"activity_btn_deps",
381+
"advanced_cfg_btn",
382+
"toolButton_more",
383+
"compiler_tabs",
370384
):
371385
try:
372386
w = getattr(self, attr, None)
@@ -375,42 +389,49 @@ def set_controls_enabled(self, enabled: bool) -> None:
375389
except Exception:
376390
pass
377391

378-
self.venv_button.setEnabled(enabled)
392+
if hasattr(self, "venv_button") and self.venv_button:
393+
self.venv_button.setEnabled(enabled)
394+
379395
self._refresh_grey_targets()
380396

381397
def _refresh_grey_targets(self) -> None:
382398
"""Refresh visual state of controls."""
383399
try:
384-
grey_targets = [
385-
getattr(self, attr, None)
386-
for attr in (
387-
"compile_btn",
388-
"btn_select_folder",
389-
"btn_select_files",
390-
"btn_remove_file",
391-
"btn_bc_loader",
392-
"btn_suggest_deps",
393-
394-
"select_lang",
395-
"select_theme",
396-
"btn_show_stats",
397-
"btn_clear_workspace",
398-
"venv_button",
399-
)
400-
]
401-
for w in grey_targets:
400+
target_names = (
401+
"compile_btn",
402+
"btn_select_folder",
403+
"btn_select_files",
404+
"btn_remove_file",
405+
"btn_bc_loader",
406+
"btn_acasl_loader",
407+
"btn_suggest_deps",
408+
"select_lang",
409+
"select_theme",
410+
"btn_show_stats",
411+
"btn_clear_workspace",
412+
"venv_button",
413+
"btn_help",
414+
"btn_lock_manager",
415+
"activity_btn_deps",
416+
"advanced_cfg_btn",
417+
"toolButton_more",
418+
)
419+
for attr in target_names:
402420
try:
421+
w = getattr(self, attr, None)
403422
if w and hasattr(w, "style"):
404423
w.style().unpolish(w)
405424
w.style().polish(w)
406425
w.update()
407426
except Exception:
408427
pass
428+
409429
if hasattr(self, "cancel_btn") and self.cancel_btn:
410430
try:
411-
self.cancel_btn.style().unpolish(self.cancel_btn)
412-
self.cancel_btn.style().polish(self.cancel_btn)
413-
self.cancel_btn.update()
431+
if hasattr(self.cancel_btn, "style"):
432+
self.cancel_btn.style().unpolish(self.cancel_btn)
433+
self.cancel_btn.style().polish(self.cancel_btn)
434+
self.cancel_btn.update()
414435
except Exception:
415436
pass
416437
except Exception:

0 commit comments

Comments
 (0)