diff --git a/src/script_chainer/config/script_config.py b/src/script_chainer/config/script_config.py index fd10e4f..f426e4a 100644 --- a/src/script_chainer/config/script_config.py +++ b/src/script_chainer/config/script_config.py @@ -156,6 +156,7 @@ class ScriptConfig: attach_direction: str = AttachDirection.NONE no_log_timeout_seconds: int = 0 no_log_max_retries: int = 3 + block: bool = True # 不参与序列化的元数据 idx: int = field(default=0, repr=False, compare=False) diff --git a/src/script_chainer/gui/page/script_edit_interface.py b/src/script_chainer/gui/page/script_edit_interface.py index 7c50e4f..27ac1f6 100644 --- a/src/script_chainer/gui/page/script_edit_interface.py +++ b/src/script_chainer/gui/page/script_edit_interface.py @@ -128,6 +128,15 @@ def get_content_widget(self) -> QWidget: ) content_widget.add_widget(self.run_timeout_seconds_opt) + block_switch_widget, self.block_switch = self._create_switch_option('阻塞') + self.block_opt = MultiPushSettingCard( + icon=FluentIcon.PAUSE, + title='阻塞运行', + content='勾选后启动会等待该脚本运行结束(默认开启);不勾选则在后台非阻塞启动', + btn_list=[block_switch_widget], + ) + content_widget.add_widget(self.block_opt) + self.check_done_opt = ComboBoxSettingCard( icon=FluentIcon.COMPLETED, title='检查完成方式', @@ -257,6 +266,7 @@ def init_by_config(self, config: ScriptConfig): self.kill_script_after_done_switch.setChecked(config.kill_script_after_done) self.kill_game_after_done_switch.setChecked(config.kill_game_after_done) self.script_arguments_opt.setValue(config.script_arguments, emit_signal=False) + self.block_switch.setChecked(config.block) self.notify_start_switch.setChecked(config.notify_start) self.notify_done_switch.setChecked(config.notify_done) @@ -351,6 +361,7 @@ def get_config_value(self) -> ScriptConfig: config.kill_script_after_done = self.kill_script_after_done_switch.isChecked() config.kill_game_after_done = self.kill_game_after_done_switch.isChecked() config.script_arguments = self.script_arguments_opt.getValue() + config.block = self.block_switch.isChecked() config.notify_start = self.notify_start_switch.isChecked() config.notify_done = self.notify_done_switch.isChecked() diff --git a/src/script_chainer/gui/page/script_setting_cards.py b/src/script_chainer/gui/page/script_setting_cards.py index 548b2bd..99908c8 100644 --- a/src/script_chainer/gui/page/script_setting_cards.py +++ b/src/script_chainer/gui/page/script_setting_cards.py @@ -162,8 +162,10 @@ def on_debug_clicked(self) -> None: command=cmd, cwd=cwd, title=f'调试 {display}', + block=self.config.block, ) - show_success(self.window(), '调试运行', f'已在终端启动 {display}') + mode = '阻塞' if self.config.block else '后台非阻塞' + show_success(self.window(), '调试运行', f'已以{mode}模式启动 {display}') except Exception as e: show_error(self.window(), '启动失败', str(e)) diff --git a/src/script_chainer/gui/page/script_setting_interface.py b/src/script_chainer/gui/page/script_setting_interface.py index 1fa5ad1..99c112b 100644 --- a/src/script_chainer/gui/page/script_setting_interface.py +++ b/src/script_chainer/gui/page/script_setting_interface.py @@ -1,3 +1,4 @@ +from PySide6.QtCore import QThread, Signal from PySide6.QtWidgets import QFileDialog, QHBoxLayout, QVBoxLayout, QWidget from qfluentwidgets import ( Action, @@ -36,9 +37,35 @@ show_warning, ) from script_chainer.utils.process_utils import launch_in_terminal -from script_chainer.utils.runner_utils import ( - build_runner_command, -) +from script_chainer.utils.runner_utils import build_runner_command + + +class ChainRunner(QThread): + """后台逐条启动脚本链,每条按自身 block 决定阻塞/非阻塞,结束后经信号汇总结果。""" + + chain_finished = Signal(int, list) + + def __init__(self, chain_name: str, specs: list[tuple[int, bool, str]], parent=None): + super().__init__(parent) + self.chain_name = chain_name + self.specs = specs + + def run(self) -> None: + launched = 0 + failed: list[str] = [] + for script_index, block, display_name in self.specs: + try: + cmd, cwd = build_runner_command(self.chain_name, script_index) + proc = launch_in_terminal(command=cmd, cwd=cwd, block=block) + except Exception: + failed.append(display_name) + continue + # 阻塞脚本非零退出 → 计入失败;非阻塞 rc 为 wt 的 0,按已启动计 + if proc.returncode not in (0, None): + failed.append(display_name) + else: + launched += 1 + self.chain_finished.emit(launched, failed) class ScriptSettingRootInterface(VerticalScrollInterface): @@ -201,26 +228,32 @@ def on_import_python_script_clicked(self) -> None: self.update_chain_display() def on_run_chain_clicked(self) -> None: - """拉起独立 runner 运行当前脚本链。""" + """按配置顺序逐条启动当前脚本链,每条按自身 block 决定阻塞/非阻塞。""" if self.chosen_config is None or self._runner_launch_in_progress: return + specs = [ + (i, cfg.block, cfg.display_name) + for i, cfg in enumerate(self.chosen_config.script_list) + if cfg.enabled + ] + if not specs: + show_warning(self.window(), '无法运行', '没有已启用的脚本') + return + self._runner_launch_in_progress = True self.run_chain_btn.setEnabled(False) - chain_name = self.chosen_config.module_name - try: - cmd, cwd = build_runner_command(chain_name) - launch_in_terminal( - command=cmd, - cwd=cwd, - title=f'运行脚本链 {chain_name}', - ) - show_success(self.window(), '运行全部', f'已在终端启动脚本链 {chain_name}') - except Exception as e: - show_error(self.window(), '启动失败', str(e)) - finally: - self._runner_launch_in_progress = False - self.run_chain_btn.setEnabled(True) + self._chain_runner = ChainRunner(self.chosen_config.module_name, specs, parent=self) + self._chain_runner.chain_finished.connect(self._on_chain_run_finished) + self._chain_runner.start() + + def _on_chain_run_finished(self, launched: int, failed: list) -> None: + self._runner_launch_in_progress = False + self.run_chain_btn.setEnabled(True) + if failed: + show_error(self.window(), '运行异常', f'{len(failed)} 个脚本启动失败:{", ".join(failed)}') + else: + show_success(self.window(), '运行全部', f'已启动 {launched} 个脚本') def update_chain_display(self) -> None: """更新脚本链的显示""" diff --git a/src/script_chainer/utils/process_utils.py b/src/script_chainer/utils/process_utils.py index c3dbc7a..d62901a 100644 --- a/src/script_chainer/utils/process_utils.py +++ b/src/script_chainer/utils/process_utils.py @@ -41,6 +41,7 @@ def launch_in_terminal( command: list[str], cwd: str | None = None, title: str | None = None, + block: bool = False, ) -> subprocess.Popen: """在终端窗口中启动命令。 @@ -49,11 +50,25 @@ def launch_in_terminal( Args: command: 命令及参数列表。 cwd: 工作目录。 - title: 终端窗口标题。 + title: 终端窗口标题(仅非阻塞的 wt 分支使用)。 + block: 是否阻塞等待命令真正结束。 Returns: 启动的 Popen 对象。 + + 说明: + - ``block=False``(默认):用 wt 开新标签页/窗口后**立即返回**(非阻塞)。 + - ``block=True``:Windows Terminal 开完标签页即退出,无法等待真正的子进程, + 故绕过 wt、改用新建控制台窗口(``CREATE_NEW_CONSOLE``)并 ``.wait()``—— + 既有可见窗口,又能正确阻塞到脚本真正结束。 """ + if block: + # 阻塞:不能用 wt(开完标签页即退、等不到真进程),改用新控制台窗口并等待。 + flags = subprocess.CREATE_NEW_CONSOLE if sys.platform == 'win32' else 0 + proc = subprocess.Popen(command, cwd=cwd, creationflags=flags) + proc.wait() + return proc + wt_path = _find_windows_terminal() if wt_path is not None: wt_cmd = [wt_path]