Skip to content

Commit e12c986

Browse files
committed
Durcit les flux de compilation et détend la mise en page IDE-like
Sécurise la lecture temps réel stdout/stderr du processus de compilation pour éviter les crashs lorsque les flux ne sont pas disponibles. Ajoute une détection explicite des flux monitorables, émet un warning unique quand stdout/stderr sont indisponibles, puis interrompt proprement la lecture live au lieu de laisser select ou read échouer. Protège également la lecture résiduelle de fin de compilation en gérant le cas où stdout ou stderr valent None. Ajoute un test de non-régression couvrant ce cas limite afin de vérifier qu'un warning est émis, qu'aucune sortie parasite n'est publiée et que la boucle de lecture live se désactive proprement. Ajuste en parallèle la disposition de l'interface IDE-like pour réduire l'effet de compression visuelle : barre haute plus généreuse, largeur minimale accrue pour les boutons Build et Cancel, panneaux latéraux mieux bornés et proportions de splitters rééquilibrées afin de redonner de l'espace utile à la zone centrale et aux logs.
1 parent eb9911d commit e12c986

3 files changed

Lines changed: 148 additions & 13 deletions

File tree

Core/Compiler/compiler.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def __init__(
101101
self.cancel_requested = False
102102
self.process: Optional[subprocess.Popen] = None
103103
self.start_time: Optional[datetime] = None
104+
self._live_output_disabled = False
105+
self._stream_warning_emitted = False
104106

105107
def run(self) -> None:
106108
"""Exécute le processus de compilation."""
@@ -154,21 +156,15 @@ def _read_output(self) -> None:
154156
if self.process is None or self.process.poll() is not None:
155157
break
156158

157-
stdout_stream = self.process.stdout
158-
stderr_stream = self.process.stderr
159-
if stdout_stream is None and stderr_stream is None:
160-
self.error_ready.emit(
161-
"Warning: process streams are unavailable; stopping realtime read."
162-
)
163-
break
164-
165-
watched_streams = [s for s in (stdout_stream, stderr_stream) if s is not None]
166-
if not watched_streams:
159+
streams = self._available_live_streams()
160+
if not streams:
161+
self._warn_missing_live_streams()
162+
self._live_output_disabled = True
167163
break
168164

169165
# Utiliser select pour attendre des données
170166
try:
171-
ready, _, _ = select.select(watched_streams, [], [], 0.1)
167+
ready, _, _ = select.select(streams, [], [], 0.1)
172168

173169
for stream in ready:
174170
if stdout_stream is not None and stream == stdout_stream:
@@ -183,6 +179,8 @@ def _read_output(self) -> None:
183179
self._update_progress(line)
184180

185181
except Exception:
182+
self._warn_missing_live_streams()
183+
self._live_output_disabled = True
186184
break
187185

188186
time.sleep(0.01)
@@ -194,7 +192,10 @@ def _read_remaining(self) -> None:
194192

195193
# Lire stdout restant
196194
try:
197-
remaining_stdout = self.process.stdout.read() if self.process.stdout else ""
195+
if self.process.stdout is None:
196+
remaining_stdout = None
197+
else:
198+
remaining_stdout = self.process.stdout.read()
198199
if remaining_stdout:
199200
for line in remaining_stdout.strip().split("\n"):
200201
if line:
@@ -204,14 +205,43 @@ def _read_remaining(self) -> None:
204205

205206
# Lire stderr restant
206207
try:
207-
remaining_stderr = self.process.stderr.read() if self.process.stderr else ""
208+
if self.process.stderr is None:
209+
remaining_stderr = None
210+
else:
211+
remaining_stderr = self.process.stderr.read()
208212
if remaining_stderr:
209213
for line in remaining_stderr.strip().split("\n"):
210214
if line:
211215
self.error_ready.emit(line.rstrip())
212216
except Exception:
213217
pass
214218

219+
def _available_live_streams(self) -> list:
220+
"""Return stdout/stderr streams that can be safely monitored in real time."""
221+
if self.process is None:
222+
return []
223+
streams = []
224+
try:
225+
if getattr(self.process, "stdout", None) is not None:
226+
streams.append(self.process.stdout)
227+
except Exception:
228+
pass
229+
try:
230+
if getattr(self.process, "stderr", None) is not None:
231+
streams.append(self.process.stderr)
232+
except Exception:
233+
pass
234+
return streams
235+
236+
def _warn_missing_live_streams(self) -> None:
237+
"""Emit a single warning when live stdout/stderr monitoring is unavailable."""
238+
if self._stream_warning_emitted:
239+
return
240+
self._stream_warning_emitted = True
241+
self.error_ready.emit(
242+
"Warning: stdout/stderr unavailable; stopping real-time output reading."
243+
)
244+
215245
def _update_progress(self, line: str) -> None:
216246
"""Met à jour la progression basée sur la sortie."""
217247
# Détecter les patterns de progression courants

Core/IdeLikeGui/connections.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
QToolButton,
3030
QStatusBar,
3131
QWidget,
32+
QFrame,
33+
QSplitter,
3234
)
3335

3436

@@ -222,6 +224,78 @@ def _find(cls, name: str):
222224
_setup_status_bar(self)
223225

224226

227+
def _tune_ide_like_layout(self) -> None:
228+
"""Relax tight layout constraints in the IDE-like UI for better label fit."""
229+
header = self.ui.findChild(QFrame, "header")
230+
if header is not None:
231+
try:
232+
header.setMinimumHeight(48)
233+
header.setMaximumHeight(56)
234+
except Exception:
235+
pass
236+
237+
for btn_name in ("compile_btn", "cancel_btn"):
238+
btn = getattr(self, btn_name, None)
239+
if btn is None:
240+
continue
241+
try:
242+
btn.setMinimumSize(124, 34)
243+
except Exception:
244+
pass
245+
246+
workspace_panel = self.ui.findChild(QFrame, "workspace_panel")
247+
if workspace_panel is not None:
248+
try:
249+
workspace_panel.setMinimumWidth(280)
250+
workspace_panel.setMaximumWidth(400)
251+
except Exception:
252+
pass
253+
254+
tools_panel = self.ui.findChild(QFrame, "tools_panel")
255+
if tools_panel is not None:
256+
try:
257+
tools_panel.setMinimumWidth(200)
258+
tools_panel.setMaximumWidth(280)
259+
except Exception:
260+
pass
261+
262+
logs_panel = self.ui.findChild(QFrame, "logs_panel")
263+
if logs_panel is not None:
264+
try:
265+
logs_panel.setMinimumHeight(190)
266+
logs_panel.setMaximumHeight(320)
267+
except Exception:
268+
pass
269+
270+
main_splitter = self.ui.findChild(QSplitter, "mainSplitter")
271+
if main_splitter is not None:
272+
try:
273+
main_splitter.setStretchFactor(0, 0)
274+
main_splitter.setStretchFactor(1, 0)
275+
main_splitter.setStretchFactor(2, 1)
276+
main_splitter.setSizes([52, 300, 1013])
277+
except Exception:
278+
pass
279+
280+
top_splitter = self.ui.findChild(QSplitter, "topSplitter")
281+
if top_splitter is not None:
282+
try:
283+
top_splitter.setStretchFactor(0, 1)
284+
top_splitter.setStretchFactor(1, 0)
285+
top_splitter.setSizes([820, 245])
286+
except Exception:
287+
pass
288+
289+
right_splitter = self.ui.findChild(QSplitter, "rightSplitter")
290+
if right_splitter is not None:
291+
try:
292+
right_splitter.setStretchFactor(0, 1)
293+
right_splitter.setStretchFactor(1, 0)
294+
right_splitter.setSizes([470, 230])
295+
except Exception:
296+
pass
297+
298+
225299
def _setup_ide_like_compiler_tabs(self) -> None:
226300
"""Bind engine tabs to compiler_tabs using existing EngineLoader registry."""
227301
tabs = getattr(self, "compiler_tabs", None)
@@ -572,6 +646,7 @@ def init_ide_like_ui(self) -> None:
572646
"""Initialize the ide-like UI and wire it to existing Core methods."""
573647
_load_ide_like_ui(self)
574648
_map_ide_like_widgets(self)
649+
_tune_ide_like_layout(self)
575650
_apply_classic_policies(self)
576651
_setup_more_tools_menu(self)
577652
try:

tests/test_compiler_smoke.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717

1818
from __future__ import annotations
1919

20+
import pytest
21+
22+
pytest.importorskip("PySide6")
23+
2024
from Core.ArkConfigManager import set_entrypoint
2125
import Core.Compiler as compiler_module
26+
from Core.Compiler.compiler import CompilationThread
2227

2328

2429
class DummyMainProcess:
@@ -101,3 +106,28 @@ def fake_start(_self, _engine, files):
101106
compiler_module.compile_all(gui)
102107

103108
assert captured.get("files") == [str(entry)]
109+
110+
111+
def test_compilation_thread_warns_when_live_streams_are_unavailable() -> None:
112+
thread = CompilationThread(program="python", args=["-V"])
113+
warnings: list[str] = []
114+
outputs: list[str] = []
115+
116+
thread.error_ready.connect(warnings.append)
117+
thread.output_ready.connect(outputs.append)
118+
119+
class DummyProcess:
120+
stdout = None
121+
stderr = None
122+
123+
def poll(self):
124+
return None
125+
126+
thread.process = DummyProcess()
127+
128+
thread._read_output()
129+
thread._read_remaining()
130+
131+
assert thread._live_output_disabled is True
132+
assert any("stdout/stderr unavailable" in msg for msg in warnings)
133+
assert outputs == []

0 commit comments

Comments
 (0)