From ab01b2c5c9c220b69dcf2418271c1d686b31ffd8 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 16 Sep 2026 19:57:51 +0800 Subject: [PATCH] feat(tunnel): add start all tunnels functionality - Added "Start All" button to UI with proper enable/disable logic - Implemented startAll method to start all non-running tunnels at once - Added logic to skip already running or transitioning tunnels - Implemented failure collection and reporting for individual tunnel errors - Updated README documentation with new feature description - Added start_all_test.php to --- examples/ssh-tunnel-qt/README.md | 3 ++ .../ssh-tunnel-qt/app/TunnelApplication.php | 29 +++++++++++++++++++ examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc | 20 +++++++++++++ 3 files changed, 52 insertions(+) diff --git a/examples/ssh-tunnel-qt/README.md b/examples/ssh-tunnel-qt/README.md index 79118abe..dff90a16 100644 --- a/examples/ssh-tunnel-qt/README.md +++ b/examples/ssh-tunnel-qt/README.md @@ -11,6 +11,8 @@ - SOCKS5:通过 SSH 服务器建立本地 SOCKS5 代理(OpenSSH `-D`)。 - 新建、查看、编辑和删除规则。 - 单条隧道启动、停止、状态展示和 SSH 输出日志。 +- “全部启动”一键启动所有未运行(含错误)状态的隧道,已在运行或正在 + 切换状态的隧道会被跳过;单条失败会汇总提示,不影响其余隧道。 - 支持中文输入法组合事件(Linux IBus/Fcitx、Windows TSF/IMM)。 - 表格列宽可拖动调整,刷新规则时不会重置列宽。 - 映射端点分别显示为“本机地址”和“远程地址”;远程转发会按实际方向交换显示。 @@ -104,4 +106,5 @@ Windows 10/11 可在“可选功能”中安装 OpenSSH Client。 ```bash php tests/domain_test.php php tests/startup_test.php +php tests/start_all_test.php ``` diff --git a/examples/ssh-tunnel-qt/app/TunnelApplication.php b/examples/ssh-tunnel-qt/app/TunnelApplication.php index 78d2fb6c..baa2fb58 100644 --- a/examples/ssh-tunnel-qt/app/TunnelApplication.php +++ b/examples/ssh-tunnel-qt/app/TunnelApplication.php @@ -71,6 +71,8 @@ class TunnelApplication $this->refresh(); } elseif ($type === 'start') { $this->start($id); + } elseif ($type === 'start_all') { + $this->startAll(); } elseif ($type === 'stop') { $this->stop($id); } elseif ($type === 'process_started') { @@ -127,6 +129,33 @@ class TunnelApplication } } + /** + * Start every rule that is not already running or transitioning. A + * failing rule must not prevent the remaining tunnels from being + * attempted, so failures are collected and reported together. + */ + private function startAll(): void + { + $failures = []; + foreach ($this->repository->all() as $rule) { + $state = (string) ($this->states[$rule->id] ?? 'stopped'); + if ($state === 'running' || $state === 'starting' || $state === 'stopping') { + continue; + } + try { + $this->start($rule->id); + } catch (Throwable $error) { + $failures[] = $rule->name . ':' . $error->getMessage(); + } + } + + // Always refresh so the button re-enables when nothing was startable. + $this->refresh(); + if ($failures !== []) { + throw new RuntimeException("以下隧道启动失败:\n" . implode("\n", $failures)); + } + } + private function stop(string $id): void { $state = (string) ($this->states[$id] ?? 'stopped'); diff --git a/examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc b/examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc index 32ea737d..43298e49 100644 --- a/examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc +++ b/examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc @@ -300,14 +300,17 @@ class TunnelWindowBox final : public Box { auto *remove = new QPushButton(tr("删除")); startButton_ = new QPushButton(tr("启动")); stopButton_ = new QPushButton(tr("停止")); + startAllButton_ = new QPushButton(tr("全部启动")); startButton_->setEnabled(false); stopButton_->setEnabled(false); + startAllButton_->setEnabled(false); auto *buttons = new QHBoxLayout(); buttons->addWidget(add); buttons->addWidget(edit); buttons->addWidget(remove); buttons->addWidget(clearLogButton_); buttons->addStretch(); + buttons->addWidget(startAllButton_); buttons->addWidget(startButton_); buttons->addWidget(stopButton_); layout->addLayout(buttons); @@ -327,6 +330,10 @@ class TunnelWindowBox final : public Box { enqueue("delete", id); } }); + QObject::connect(startAllButton_, &QPushButton::clicked, window_, [this]() { + startAllButton_->setEnabled(false); + enqueue("start_all"); + }); QObject::connect(startButton_, &QPushButton::clicked, window_, [this]() { const QString id = selectedId(); if (id.isEmpty()) { @@ -602,6 +609,18 @@ class TunnelWindowBox final : public Box { } void updateActionButtons() { + // "Start all" is independent of the table selection: it only needs at + // least one rule that is neither running nor transitioning. + bool anyStartable = false; + for (auto it = statuses_.constBegin(); it != statuses_.constEnd(); ++it) { + const QString &status = it.value(); + if (status != "running" && status != "starting" && status != "stopping") { + anyStartable = true; + break; + } + } + startAllButton_->setEnabled(anyStartable); + const QString id = selectedId(); if (id.isEmpty()) { startButton_->setEnabled(false); @@ -690,6 +709,7 @@ class TunnelWindowBox final : public Box { QPlainTextEdit *log_ = nullptr; QPushButton *startButton_ = nullptr; QPushButton *stopButton_ = nullptr; + QPushButton *startAllButton_ = nullptr; QPushButton *clearLogButton_ = nullptr; QHash rules_; QHash statuses_;