-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbase_process_manager.py
More file actions
217 lines (190 loc) · 8.78 KB
/
Copy pathbase_process_manager.py
File metadata and controls
217 lines (190 loc) · 8.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
from __future__ import annotations
import queue
import subprocess # nosec B404 - 管理器僅持有外部子程序物件,呼叫端以引數清單啟動
from threading import Thread
from PySide6.QtCore import QTimer
from PySide6.QtGui import QTextCharFormat
from PySide6.QtWidgets import QTextEdit, QWidget
from je_editor.pyside_ui.code.running_process_manager import run_instance_manager
from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict
from je_editor.utils.logging.loggin_instance import jeditor_logger
class BaseProcessManager:
"""
程式/Shell 執行管理器的共用基底類別。
Base class for ExecManager and ShellManager with shared process logic.
"""
def __init__(
self,
main_window: QWidget | None = None,
encoding: str = "utf-8",
buffer_size: int = 1024,
) -> None:
self.read_program_error_output_from_thread: Thread | None = None
self.read_program_output_from_thread: Thread | None = None
self.main_window = main_window
self.code_result: QTextEdit | None = None
self.timer: QTimer | None = None
self._still_running: bool = True
self.process: subprocess.Popen | None = None
self.run_output_queue: queue.Queue = queue.Queue()
self.run_error_queue: queue.Queue = queue.Queue()
self.program_encoding: str = encoding
self.program_buffer: int = buffer_size
run_instance_manager.instance_list.append(self)
@property
def still_running(self) -> bool:
return self._still_running
@still_running.setter
def still_running(self, value: bool) -> None:
self._still_running = value
def _start_reader_threads(self) -> None:
"""啟動 stdout/stderr 讀取執行緒 / Start stdout/stderr reader threads"""
self.read_program_output_from_thread = Thread(
target=self._read_stdout, daemon=True
)
self.read_program_output_from_thread.start()
self.read_program_error_output_from_thread = Thread(
target=self._read_stderr, daemon=True
)
self.read_program_error_output_from_thread.start()
def _start_pull_timer(self, interval: int = 50) -> None:
"""啟動輸出拉取定時器 / Start output pull timer"""
self.timer = QTimer(self.main_window)
self.timer.setInterval(interval)
self.timer.timeout.connect(self.pull_text)
self.timer.start()
def pull_text(self) -> None:
"""批次從佇列中取出訊息並顯示 / Batch-drain queues and display"""
text_cursor = self.code_result.textCursor()
# 批次處理標準輸出 / Batch stdout
stdout_parts = []
try:
while not self.run_output_queue.empty():
msg = str(self.run_output_queue.get_nowait()).strip()
if msg:
stdout_parts.append(msg)
except queue.Empty:
pass
if stdout_parts:
text_format = QTextCharFormat()
text_format.setForeground(actually_color_dict.get("normal_output_color"))
text_cursor.insertText("\n".join(stdout_parts), text_format)
text_cursor.insertBlock()
# 批次處理錯誤輸出 / Batch stderr
stderr_parts = []
try:
while not self.run_error_queue.empty():
msg = str(self.run_error_queue.get_nowait()).strip()
if msg:
stderr_parts.append(msg)
except queue.Empty:
pass
if stderr_parts:
text_format = QTextCharFormat()
text_format.setForeground(actually_color_dict.get("error_output_color"))
text_cursor.insertText("\n".join(stderr_parts), text_format)
text_cursor.insertBlock()
# 檢查子程序是否結束 / Check if process finished
if self.process is not None:
if self.process.returncode is not None:
self._on_process_finished()
elif self.still_running:
self.process.poll()
def _on_process_finished(self) -> None:
"""子程序結束時呼叫 (子類別覆寫) / Called when process finishes (override in subclass)"""
pass
def exit_program(self) -> None:
"""結束程式:清理執行緒、佇列與子程序 / Exit: clean threads, queues, and process"""
jeditor_logger.info(f"{self.__class__.__name__} exit_program")
self.still_running = False
# 在背景清理執行緒與子程序,避免阻塞主執行緒
# Clean up threads and process in background to avoid blocking the main thread
threads_to_join = []
if self.read_program_output_from_thread is not None:
threads_to_join.append(self.read_program_output_from_thread)
self.read_program_output_from_thread = None
if self.read_program_error_output_from_thread is not None:
threads_to_join.append(self.read_program_error_output_from_thread)
self.read_program_error_output_from_thread = None
process_to_cleanup = self.process
self.process = None
# 清空佇列 / Clear queues
self.print_and_clear_queue()
# 顯示退出訊息 / Show exit message
if process_to_cleanup is not None and self.code_result is not None:
returncode = process_to_cleanup.returncode
text_cursor = self.code_result.textCursor()
text_format = QTextCharFormat()
text_format.setForeground(actually_color_dict.get("normal_output_color"))
text_cursor.insertText(
f"{self._exit_message_prefix()} exit with code {returncode}",
text_format
)
text_cursor.insertBlock()
# 在背景執行緒中做 join 和 terminate / Do join and terminate in background
if threads_to_join or process_to_cleanup is not None:
cleanup_thread = Thread(
target=self._cleanup_in_background,
args=(threads_to_join, process_to_cleanup),
daemon=True,
)
cleanup_thread.start()
@staticmethod
def _cleanup_in_background(threads: list, process: subprocess.Popen | None) -> None:
"""在背景執行清理工作 / Perform cleanup work in background"""
for t in threads:
try:
t.join(timeout=2)
except RuntimeError as join_err:
jeditor_logger.debug(f"thread join failed during cleanup: {join_err}")
if process is not None:
try:
process.terminate()
process.wait(timeout=2)
except subprocess.TimeoutExpired:
try:
process.kill()
process.wait(timeout=2)
except (OSError, subprocess.TimeoutExpired) as kill_err:
jeditor_logger.debug(f"process kill failed during cleanup: {kill_err}")
except OSError as term_err:
jeditor_logger.debug(f"process terminate failed during cleanup: {term_err}")
def _exit_message_prefix(self) -> str:
"""退出訊息前綴 (子類別覆寫) / Exit message prefix (override in subclass)"""
return "Process"
def print_and_clear_queue(self) -> None:
"""清空輸出與錯誤佇列 / Clear stdout and stderr queues"""
self.run_output_queue = queue.Queue()
self.run_error_queue = queue.Queue()
def _read_stdout(self) -> None:
"""從子程序 stdout 持續讀取 / Continuously read from process stdout"""
try:
while self.still_running:
if self.process is None:
break
data = self.process.stdout.readline(
self.program_buffer
).decode(self.program_encoding, "replace")
if not data and (self.process is None or self.process.poll() is not None):
break
if self.process:
self.process.stdout.flush()
self.run_output_queue.put_nowait(data)
except (OSError, ValueError):
pass
def _read_stderr(self) -> None:
"""從子程序 stderr 持續讀取 / Continuously read from process stderr"""
try:
while self.still_running:
if self.process is None:
break
data = self.process.stderr.readline(
self.program_buffer
).decode(self.program_encoding, "replace")
if not data and (self.process is None or self.process.poll() is not None):
break
if self.process:
self.process.stderr.flush()
self.run_error_queue.put_nowait(data)
except (OSError, ValueError):
pass