11from __future__ import annotations
22
3+ import logging
34import os
45import signal
56import subprocess
7+ import sys
68import threading
79import venv
10+ from contextlib import contextmanager
811from dataclasses import dataclass
912from pathlib import Path
1013from typing import Any
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+
1653def _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 ("\n BCASL found issues." )
531601 return 1
602+
603+ if status :
604+ status .stop ()
532605 success ("\n BCASL 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
539618def launch_gui (* , legacy : bool = False ) -> int :
0 commit comments