@@ -58,38 +58,44 @@ def flush(self):
5858# 1/2 du processus -- sans capture au niveau fd, ce texte n'apparaîtrait
5959# jamais dans la console.
6060# On duplique donc les fds réels vers des tubes (pipes), lus en continu par
61- # des threads dédiés qui relaient chaque fragment via notify_output. Une
62- # fois cette capture active, sys.stdout/sys.stderr Python restent les
63- # objets standard (liés au fd 1/2, désormais redirigés vers nos tubes) --
64- # on se garde bien de les remplacer par un objet Python personnalisé
65- # séparé (cf. init_native ci-dessous) : TOUT (print() Python ET C/C++)
66- # passe alors par le MÊME tube, lu par le MÊME thread, dans l'ordre
67- # chronologique réel des écritures. Avoir deux chemins distincts (un appel
68- # Python synchrone d'un côté, un tube lu de façon asynchrone par un thread
69- # dédié de l'autre) faisait que les sorties C++ et Python pouvaient
70- # apparaître dans un ordre différent de celui où elles avaient réellement
71- # eu lieu.
72- # NOTE : une fois les fds redirigés, TOUTE écriture sur stdout/stderr faite
73- # par le PROCESSUS entier (pas seulement par le code exécuté dans la
74- # console) passe par ce mécanisme -- c'est le prix à payer pour intercepter
75- # des écritures C/C++ de bas niveau, qui n'ont pas d'autre point d'accroche .
61+ # des threads dédiés qui relaient chaque fragment via notify_output. Cette
62+ # capture est activée UNIQUEMENT pendant la durée d'une exécution
63+ # déclenchée par la console (start()/exec_already_executed()) et désactivée
64+ # (fds restaurés) dès qu'elle se termine -- ainsi, seules les sorties
65+ # produites par le code exécuté PAR LA CONSOLE sont récupérées, pas celles
66+ # d'une autre partie de l'application pendant le reste du temps.
67+ # Pendant la capture, sys.stdout/sys.stderr Python restent les objets
68+ # standard (liés au fd 1/2, temporairement redirigés vers nos tubes) -- on
69+ # se garde bien de les remplacer par un objet Python personnalisé séparé
70+ # (cf. init_native ci-dessous) : TOUT (print() Python ET C/C++) passe alors
71+ # par le MÊME tube, lu par le MÊME thread, dans l'ordre chronologique réel
72+ # des écritures. Avoir deux chemins distincts (un appel Python synchrone
73+ # d'un côté, un tube lu de façon asynchrone par un thread dédié de l'autre)
74+ # faisait que les sorties C++ et Python pouvaient apparaître dans un ordre
75+ # différent de celui où elles avaient réellement eu lieu .
7676# Limite connue : le stdout/stderr C++ est généralement bufferisé en mode
7777# "pleine mémoire tampon" (non ligne-par-ligne) dès lors qu'il ne pointe
7878# plus sur un terminal ; le texte peut donc n'apparaître qu'après un flush
7979# explicite (std::endl, std::flush, ou la fin du programme) côté code
8080# wrappé -- ce n'est pas quelque chose que l'on puisse forcer depuis ici.
81- _fd_capture_started = False
82-
83-
84- def _start_fd_capture ():
85- """Retourne True si la capture fd a bien démarré, False sinon
86- (environnement sans vrais descripteurs de fichier standards, rare)."""
87- global _fd_capture_started
88- if _fd_capture_started :
81+ _saved_stdout_fd = None
82+ _saved_stderr_fd = None
83+ _capture_depth = 0 # compteur de réentrance (imbrication par précaution)
84+
85+
86+ def _enable_fd_capture ():
87+ """Redirige fd 1/2 vers des tubes pour la durée d'une exécution.
88+ Retourne True si la capture est active (déjà activée par un appel
89+ englobant, ou tout juste démarrée), False si les vrais descripteurs de
90+ fichier standards ne sont pas disponibles (environnement rare)."""
91+ global _saved_stdout_fd , _saved_stderr_fd , _capture_depth
92+ if _capture_depth > 0 :
93+ _capture_depth += 1
8994 return True
90- _fd_capture_started = True
9195
9296 try :
97+ _saved_stdout_fd = os .dup (1 )
98+ _saved_stderr_fd = os .dup (2 )
9399 stdout_r , stdout_w = os .pipe ()
94100 stderr_r , stderr_w = os .pipe ()
95101 os .dup2 (stdout_w , 1 )
@@ -112,9 +118,36 @@ def reader(fd, kind):
112118
113119 threading .Thread (target = reader , args = (stdout_r , "stdout" ), daemon = True ).start ()
114120 threading .Thread (target = reader , args = (stderr_r , "stderr" ), daemon = True ).start ()
121+ _capture_depth = 1
115122 return True
116123
117124
125+ def _disable_fd_capture ():
126+ """Restaure fd 1/2 à leur état d'avant _enable_fd_capture() (annule le
127+ dernier niveau d'imbrication uniquement)."""
128+ global _saved_stdout_fd , _saved_stderr_fd , _capture_depth
129+ if _capture_depth <= 0 :
130+ return
131+ _capture_depth -= 1
132+ if _capture_depth > 0 :
133+ return
134+
135+ try :
136+ sys .stdout .flush ()
137+ sys .stderr .flush ()
138+ except Exception :
139+ pass
140+ try :
141+ os .dup2 (_saved_stdout_fd , 1 )
142+ os .dup2 (_saved_stderr_fd , 2 )
143+ os .close (_saved_stdout_fd )
144+ os .close (_saved_stderr_fd )
145+ except OSError :
146+ pass
147+ _saved_stdout_fd = None
148+ _saved_stderr_fd = None
149+
150+
118151# ---------------------------------------------------------------------------
119152# Validation des lignes pouvant recevoir un point d'arrêt : uniquement les
120153# lignes qui correspondent réellement à une instruction (bytecode), jamais
@@ -303,25 +336,17 @@ def init_native(native_module):
303336 global _native
304337 _native = native_module
305338
306- if _start_fd_capture ():
307- # Tout (Python ET C/C++) transite désormais par le même tube :
308- # sys.stdout/sys.stderr restent les objets standard, mais on les
309- # passe en ligne-bufferisé pour que le texte apparaisse rapidement
310- # dans la console (par défaut, un flux non lié à un terminal est
311- # bufferisé en pleine mémoire tampon, ce qui retarderait
312- # l'affichage jusqu'à ce que le tampon se remplisse).
313- try :
314- sys .stdout .reconfigure (line_buffering = True )
315- sys .stderr .reconfigure (line_buffering = True )
316- except (AttributeError , ValueError ):
317- pass # interpréteur trop ancien / flux non reconfigurable : tant pis
318- else :
319- # Environnement sans vrais descripteurs de fichier standards
320- # (rare) : repli sur la redirection Python simple -- elle ne
321- # capture que print()/write() Python (pas le C/C++ de bas niveau),
322- # mais reste préférable à une absence totale de sortie.
323- sys .stdout = _StreamRedirector ("stdout" )
324- sys .stderr = _StreamRedirector ("stderr" )
339+ # Reconfigure sys.stdout/sys.stderr en ligne-bufferisé une bonne fois
340+ # pour toutes (réglage indépendant de ce que fd 1/2 pointe réellement à
341+ # un instant donné -- il s'applique aussi bien avant qu'après une
342+ # redirection temporaire via _enable_fd_capture()) : par défaut, un
343+ # flux non lié à un terminal est bufferisé en pleine mémoire tampon, ce
344+ # qui retarderait l'affichage jusqu'à ce que le tampon se remplisse.
345+ try :
346+ sys .stdout .reconfigure (line_buffering = True )
347+ sys .stderr .reconfigure (line_buffering = True )
348+ except (AttributeError , ValueError ):
349+ pass # interpréteur trop ancien / flux non reconfigurable : tant pis
325350
326351
327352def set_mode (mode ):
@@ -402,6 +427,19 @@ def start(debug, step_first=False):
402427 _debugger ._auto_steps_remaining = 1 if (debug and step_first ) else 0
403428 code = _pending_code
404429
430+ # Capture des flux sortants scopée à la durée de CETTE exécution (voir
431+ # commentaire plus haut) : activée ici, désactivée dans le `finally`
432+ # de target() ci-dessous, une fois la session terminée (elle couvre
433+ # donc aussi bien toutes les pauses intermédiaires que la fin réelle).
434+ fd_capture_active = _enable_fd_capture ()
435+ if not fd_capture_active :
436+ # Environnement sans vrais descripteurs de fichier standards
437+ # (rare) : repli sur la redirection Python simple -- elle ne
438+ # capture que print()/write() Python (pas le C/C++ de bas niveau),
439+ # mais reste préférable à une absence totale de sortie.
440+ sys .stdout = _StreamRedirector ("stdout" )
441+ sys .stderr = _StreamRedirector ("stderr" )
442+
405443 def target ():
406444 # Distingue, pour le C++, un arrêt VOLONTAIRE (bouton "Arrêter",
407445 # via request_stop()) d'une fin normale (tout le code envoyé a
@@ -457,6 +495,11 @@ def target():
457495 _native .notify_exception (lineno , text )
458496 finally :
459497 _finish (stopped_early )
498+ if fd_capture_active :
499+ _disable_fd_capture ()
500+ else :
501+ sys .stdout = sys .__stdout__
502+ sys .stderr = sys .__stderr__
460503
461504 _run_thread = threading .Thread (target = target , daemon = True )
462505 _run_thread .start ()
@@ -498,10 +541,20 @@ def exec_already_executed(source):
498541 appelé que lorsque la console n'est pas en cours d'exécution.
499542 Retourne None en cas de succès, ou le texte de l'exception sinon.
500543 """
544+ fd_capture_active = _enable_fd_capture ()
545+ if not fd_capture_active :
546+ sys .stdout = _StreamRedirector ("stdout" )
547+ sys .stderr = _StreamRedirector ("stderr" )
501548 try :
502549 code = compile (source , CONSOLE_FILENAME , "exec" )
503550 exec (code , _console_globals , _console_globals )
504551 return None
505552 except BaseException :
506553 etype , evalue , _etb = sys .exc_info ()
507554 return "" .join (traceback .format_exception_only (etype , evalue )).strip ()
555+ finally :
556+ if fd_capture_active :
557+ _disable_fd_capture ()
558+ else :
559+ sys .stdout = sys .__stdout__
560+ sys .stderr = sys .__stderr__
0 commit comments