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
master
韩天峰 3 weeks ago
parent 1ac462ae9f
commit ab01b2c5c9
  1. 3
      examples/ssh-tunnel-qt/README.md
  2. 29
      examples/ssh-tunnel-qt/app/TunnelApplication.php
  3. 20
      examples/ssh-tunnel-qt/cpp-src/qt_tunnel.cc

@ -11,6 +11,8 @@
- SOCKS5:通过 SSH 服务器建立本地 SOCKS5 代理(OpenSSH `-D`)。 - SOCKS5:通过 SSH 服务器建立本地 SOCKS5 代理(OpenSSH `-D`)。
- 新建、查看、编辑和删除规则。 - 新建、查看、编辑和删除规则。
- 单条隧道启动、停止、状态展示和 SSH 输出日志。 - 单条隧道启动、停止、状态展示和 SSH 输出日志。
- “全部启动”一键启动所有未运行(含错误)状态的隧道,已在运行或正在
切换状态的隧道会被跳过;单条失败会汇总提示,不影响其余隧道。
- 支持中文输入法组合事件(Linux IBus/Fcitx、Windows TSF/IMM)。 - 支持中文输入法组合事件(Linux IBus/Fcitx、Windows TSF/IMM)。
- 表格列宽可拖动调整,刷新规则时不会重置列宽。 - 表格列宽可拖动调整,刷新规则时不会重置列宽。
- 映射端点分别显示为“本机地址”和“远程地址”;远程转发会按实际方向交换显示。 - 映射端点分别显示为“本机地址”和“远程地址”;远程转发会按实际方向交换显示。
@ -104,4 +106,5 @@ Windows 10/11 可在“可选功能”中安装 OpenSSH Client。
```bash ```bash
php tests/domain_test.php php tests/domain_test.php
php tests/startup_test.php php tests/startup_test.php
php tests/start_all_test.php
``` ```

@ -71,6 +71,8 @@ class TunnelApplication
$this->refresh(); $this->refresh();
} elseif ($type === 'start') { } elseif ($type === 'start') {
$this->start($id); $this->start($id);
} elseif ($type === 'start_all') {
$this->startAll();
} elseif ($type === 'stop') { } elseif ($type === 'stop') {
$this->stop($id); $this->stop($id);
} elseif ($type === 'process_started') { } 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 private function stop(string $id): void
{ {
$state = (string) ($this->states[$id] ?? 'stopped'); $state = (string) ($this->states[$id] ?? 'stopped');

@ -300,14 +300,17 @@ class TunnelWindowBox final : public Box {
auto *remove = new QPushButton(tr("删除")); auto *remove = new QPushButton(tr("删除"));
startButton_ = new QPushButton(tr("启动")); startButton_ = new QPushButton(tr("启动"));
stopButton_ = new QPushButton(tr("停止")); stopButton_ = new QPushButton(tr("停止"));
startAllButton_ = new QPushButton(tr("全部启动"));
startButton_->setEnabled(false); startButton_->setEnabled(false);
stopButton_->setEnabled(false); stopButton_->setEnabled(false);
startAllButton_->setEnabled(false);
auto *buttons = new QHBoxLayout(); auto *buttons = new QHBoxLayout();
buttons->addWidget(add); buttons->addWidget(add);
buttons->addWidget(edit); buttons->addWidget(edit);
buttons->addWidget(remove); buttons->addWidget(remove);
buttons->addWidget(clearLogButton_); buttons->addWidget(clearLogButton_);
buttons->addStretch(); buttons->addStretch();
buttons->addWidget(startAllButton_);
buttons->addWidget(startButton_); buttons->addWidget(startButton_);
buttons->addWidget(stopButton_); buttons->addWidget(stopButton_);
layout->addLayout(buttons); layout->addLayout(buttons);
@ -327,6 +330,10 @@ class TunnelWindowBox final : public Box {
enqueue("delete", id); enqueue("delete", id);
} }
}); });
QObject::connect(startAllButton_, &QPushButton::clicked, window_, [this]() {
startAllButton_->setEnabled(false);
enqueue("start_all");
});
QObject::connect(startButton_, &QPushButton::clicked, window_, [this]() { QObject::connect(startButton_, &QPushButton::clicked, window_, [this]() {
const QString id = selectedId(); const QString id = selectedId();
if (id.isEmpty()) { if (id.isEmpty()) {
@ -602,6 +609,18 @@ class TunnelWindowBox final : public Box {
} }
void updateActionButtons() { 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(); const QString id = selectedId();
if (id.isEmpty()) { if (id.isEmpty()) {
startButton_->setEnabled(false); startButton_->setEnabled(false);
@ -690,6 +709,7 @@ class TunnelWindowBox final : public Box {
QPlainTextEdit *log_ = nullptr; QPlainTextEdit *log_ = nullptr;
QPushButton *startButton_ = nullptr; QPushButton *startButton_ = nullptr;
QPushButton *stopButton_ = nullptr; QPushButton *stopButton_ = nullptr;
QPushButton *startAllButton_ = nullptr;
QPushButton *clearLogButton_ = nullptr; QPushButton *clearLogButton_ = nullptr;
QHash<QString, RuleForm> rules_; QHash<QString, RuleForm> rules_;
QHash<QString, QString> statuses_; QHash<QString, QString> statuses_;

Loading…
Cancel
Save