Skip to content

Commit 80cfdca

Browse files
committed
feat: suppression stricte des logs parasites en mode non-verbose et animation BCASL
- Ajout d'une redirection sys.stdout/sys.stderr vers devnull pendant BCASL et la compilation. - Silence du logger 'bcasl' (niveau ERROR) en mode normal. - Propagation du mode silencieux aux processus sandbox via PYCOMPILER_QUIET. - Ajout du spinner animé Rich pour la commande 'ark run bcasl'. - Support de l'option --verbose pour toutes les commandes liées à BCASL.
1 parent 5bfb6bd commit 80cfdca

3 files changed

Lines changed: 135 additions & 30 deletions

File tree

Ui/Cli/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ def run_group():
306306
@click.option("--timeout", type=float)
307307
@click.option("--parallel", type=int)
308308
@click.option("--list-plugins", is_flag=True)
309-
def run_bcasl_cmd(timeout, parallel, list_plugins):
309+
@click.option("--verbose", "-v", is_flag=True)
310+
def run_bcasl_cmd(timeout, parallel, list_plugins, verbose):
310311
"""Run BCASL in headless mode for the current workspace."""
311312
args: list[str]
312313
if list_plugins:
@@ -316,7 +317,7 @@ def run_bcasl_cmd(timeout, parallel, list_plugins):
316317
if timeout is not None:
317318
args.extend(["--timeout", str(timeout)])
318319
del parallel
319-
raise click.exceptions.Exit(run_bcasl_headless(args))
320+
raise click.exceptions.Exit(run_bcasl_headless(args, verbose=verbose))
320321

321322
@cli.command("gui")
322323
@click.option("--legacy", is_flag=True)

Ui/Cli/helpers.py

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from __future__ import annotations
22

3+
import logging
34
import os
45
import signal
56
import subprocess
7+
import sys
68
import threading
79
import venv
10+
from contextlib import contextmanager
811
from dataclasses import dataclass
912
from pathlib import Path
1013
from typing import Any
@@ -13,6 +16,40 @@
1316
_CLI_CANCEL_EVENT = threading.Event()
1417

1518

19+
@contextmanager
20+
def _redirect_output(enabled: bool):
21+
"""Redirect stdout and stderr to devnull if enabled is True."""
22+
if not enabled:
23+
yield
24+
return
25+
26+
# Set env var for child processes (e.g. BCASL sandbox workers)
27+
os.environ["PYCOMPILER_QUIET"] = "1"
28+
29+
# Silence 'bcasl' logger
30+
bcasl_logger = logging.getLogger("bcasl")
31+
old_level = bcasl_logger.level
32+
# If not verbose, only show CRITICAL/ERROR if they happen during compilation
33+
# but the user wants it really clean, so let's go with ERROR.
34+
bcasl_logger.setLevel(logging.ERROR)
35+
36+
# Some handlers might be bound to the original sys.stderr/stdout.
37+
# We don't remove them to avoid side effects, but setting the level should be enough.
38+
39+
with open(os.devnull, "w") as fnull:
40+
old_stdout = sys.stdout
41+
old_stderr = sys.stderr
42+
try:
43+
sys.stdout = fnull
44+
sys.stderr = fnull
45+
yield
46+
finally:
47+
sys.stdout = old_stdout
48+
sys.stderr = old_stderr
49+
bcasl_logger.setLevel(old_level)
50+
os.environ.pop("PYCOMPILER_QUIET", None)
51+
52+
1653
def _cli_sigint_handler(signum, frame):
1754
"""Handle SIGINT (Ctrl+C) by setting the cancellation event."""
1855
_CLI_CANCEL_EVENT.set()
@@ -42,12 +79,16 @@ def run_engine_compile(
4279
def _on_stdout(line: str):
4380
captured_stdout.append(line)
4481
if verbose:
82+
# We are inside the redirection if verbose is False,
83+
# but here verbose is False so we don't print.
84+
# If verbose is True, we are NOT redirected.
4585
plain(line)
4686
elif status:
4787
clean = line.strip()
4888
if clean:
4989
# Update status message if it's a high-level step or important progress
5090
if any(x in clean for x in ("Etape", "->", "Execution", "Commande")):
91+
# We must use console.print or status.update which bypasses sys.stdout if rich is used
5192
status.update(f"[cyan]{clean}[/cyan]")
5293

5394
def _on_stderr(line: str):
@@ -61,15 +102,17 @@ def _on_stderr(line: str):
61102
old_handler = signal.signal(signal.SIGINT, _cli_sigint_handler)
62103

63104
try:
64-
result = run_engine_compile_streaming(
65-
workspace=workspace,
66-
engine_id=engine_id,
67-
context=context,
68-
engine_config=engine_config,
69-
on_stdout=_on_stdout,
70-
on_stderr=_on_stderr,
71-
stop_signal=lambda: _CLI_CANCEL_EVENT.is_set(),
72-
)
105+
# Use redirection to catch any direct print() calls from engines or their dependencies
106+
with _redirect_output(not verbose):
107+
result = run_engine_compile_streaming(
108+
workspace=workspace,
109+
engine_id=engine_id,
110+
context=context,
111+
engine_config=engine_config,
112+
on_stdout=_on_stdout,
113+
on_stderr=_on_stderr,
114+
stop_signal=lambda: _CLI_CANCEL_EVENT.is_set(),
115+
)
73116
except Exception as exc:
74117
result = {
75118
"success": False,
@@ -466,7 +509,9 @@ def append(self, msg: str):
466509
old_handler = signal.signal(signal.SIGINT, _cli_sigint_handler)
467510

468511
try:
469-
report = run_pre_compile(host)
512+
# Catch direct prints from BCASL plugins or loader
513+
with _redirect_output(not verbose):
514+
report = run_pre_compile(host)
470515
except Exception as exc:
471516
if status:
472517
status.stop()
@@ -497,11 +542,11 @@ def append(self, msg: str):
497542
return True
498543

499544

500-
def run_bcasl_headless(args: list[str]) -> int:
545+
def run_bcasl_headless(args: list[str], verbose: bool = False) -> int:
501546
"""Run BCASL in headless mode for the current workspace."""
502547
from bcasl.Loader import run_pre_compile
503548

504-
from .output import error, success
549+
from .output import error, success, log, info, get_console
505550

506551
workspace = Path.cwd()
507552
if "run" in args:
@@ -519,21 +564,55 @@ def __init__(self, ws_dir: Path):
519564

520565
class Logger:
521566
def append(self, msg: str):
522-
print(msg, end="", flush=True)
567+
if verbose:
568+
log("BCASL", msg.rstrip())
523569

524570
self.log = Logger()
525571

526572
host = CliBcaslHost(workspace)
573+
574+
console = get_console()
575+
status = None
576+
if not verbose and console:
577+
status = console.status("[cyan]Verification BCASL (headless)...[/cyan]", spinner="dots")
578+
status.start()
579+
580+
if verbose:
581+
info(f"Running BCASL headless in {workspace}...")
582+
583+
# Register SIGINT handler
584+
_CLI_CANCEL_EVENT.clear()
585+
old_handler = signal.signal(signal.SIGINT, _cli_sigint_handler)
586+
527587
try:
528-
report = run_pre_compile(host)
588+
with _redirect_output(not verbose):
589+
report = run_pre_compile(host)
590+
591+
if _CLI_CANCEL_EVENT.is_set():
592+
if status:
593+
status.stop()
594+
error("BCASL annule par l'utilisateur (Ctrl+C).")
595+
return 1
596+
529597
if report and hasattr(report, "ok") and not getattr(report, "ok"):
598+
if status:
599+
status.stop()
530600
error("\nBCASL found issues.")
531601
return 1
602+
603+
if status:
604+
status.stop()
532605
success("\nBCASL completed successfully.")
533606
return 0
534607
except Exception as exc:
608+
if status:
609+
status.stop()
535610
error(f"BCASL failed: {exc}")
536611
return 1
612+
finally:
613+
signal.signal(signal.SIGINT, old_handler)
614+
if status:
615+
status.stop()
537616

538617

539618
def launch_gui(*, legacy: bool = False) -> int:

bcasl/executor.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -954,22 +954,47 @@ def _plugin_worker(
954954
955955
Renvoie un dict via la queue: {ok: bool, error: str, duration_ms: float}
956956
"""
957+
import logging as _logging
958+
import os as _os
959+
import sys as _sys
957960
import time as _time
958961
import traceback as _tb
959962
from pathlib import Path as _Path
960963

961-
_configure_worker_env(config)
962-
_maybe_init_qt_app(config)
963-
_enforce_sdk_progress()
964-
_apply_resource_limits(config)
964+
# Handle quiet mode for sandbox processes
965+
_quiet = _os.environ.get("PYCOMPILER_QUIET") == "1"
966+
_fnull = None
967+
_old_stdout = None
968+
_old_stderr = None
969+
970+
if _quiet:
971+
_fnull = open(_os.devnull, "w")
972+
_old_stdout = _sys.stdout
973+
_old_stderr = _sys.stderr
974+
_sys.stdout = _fnull
975+
_sys.stderr = _fnull
976+
# Also silence 'bcasl' logger in the child process
977+
_logging.getLogger("bcasl").setLevel(_logging.ERROR)
978+
965979
try:
966-
from bcasl import PreCompileContext as _PCC
967-
968-
plg = _load_plugin_instance(module_path, plugin_id, project_root, config)
969-
ctx = _PCC(_Path(project_root), config=dict(config or {}))
970-
t0 = _time.perf_counter()
971-
plg.on_pre_compile(ctx)
972-
dur = (_time.perf_counter() - t0) * 1000.0
973-
q.put({"ok": True, "error": "", "duration_ms": dur})
974-
except Exception:
975-
q.put({"ok": False, "error": _tb.format_exc(), "duration_ms": 0.0})
980+
_configure_worker_env(config)
981+
_maybe_init_qt_app(config)
982+
_enforce_sdk_progress()
983+
_apply_resource_limits(config)
984+
try:
985+
from bcasl import PreCompileContext as _PCC
986+
987+
plg = _load_plugin_instance(module_path, plugin_id, project_root, config)
988+
ctx = _PCC(_Path(project_root), config=dict(config or {}))
989+
t0 = _time.perf_counter()
990+
plg.on_pre_compile(ctx)
991+
dur = (_time.perf_counter() - t0) * 1000.0
992+
q.put({"ok": True, "error": "", "duration_ms": dur})
993+
except Exception:
994+
q.put({"ok": False, "error": _tb.format_exc(), "duration_ms": 0.0})
995+
finally:
996+
if _quiet:
997+
_sys.stdout = _old_stdout
998+
_sys.stderr = _old_stderr
999+
if _fnull:
1000+
_fnull.close()

0 commit comments

Comments
 (0)