Skip to content

Commit f09500a

Browse files
feat(miniapp): add CDP debug port support
- Enables Chrome DevTools Protocol debugging per-app via a configurable port, so developers can attach browser devtools without modifying the host process or using a global flag. - Port is validated before launch with a bind-test to fail fast instead of silently ignoring a conflict at runtime. - CDP URL is discovered via polling after WebView2 init and surfaced in both the toolbar button and the context menu, avoiding any need to hunt for the URL manually. - Environment variable injection is scoped tightly around the CreateCoreWebView2EnvironmentWithOptions call and restored immediately so concurrent launches on different ports cannot race.
1 parent aad3616 commit f09500a

11 files changed

Lines changed: 305 additions & 7 deletions

src/MiniAppDefinition.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct MiniAppDefinition
2020
QString icon; // Reserved for future custom icon path
2121
QString healthCheckUrl; // Health poll URL (defaults to url if empty)
2222
int healthTimeoutMs = 30000; // Timeout in ms (range 5000-300000)
23+
int debugPort = 0; // CDP debug port (0 = disabled, 1-65535 = enabled)
2324
bool autoKillOnClose = true; // Kill process on tab close
2425

2526
bool isValid() const { return !name.isEmpty() && !url.isEmpty(); }

src/MiniAppInstance.cpp

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include <QNetworkAccessManager>
1515
#include <QNetworkReply>
1616
#include <QNetworkRequest>
17+
#include <QTcpSocket>
18+
#include <QHostAddress>
1719
#include <QUrl>
1820

1921
MiniAppInstance::MiniAppInstance(const MiniAppDefinition &def, QObject *parent)
@@ -55,6 +57,11 @@ QString MiniAppInstance::debugInfo() const
5557
info += QStringLiteral("\n");
5658
info += m_webView->debugInfo();
5759
}
60+
if (!m_cdpHttpUrl.isEmpty()) {
61+
info += QStringLiteral("\n--- CDP ---\n");
62+
info += QStringLiteral("CDP HTTP URL: %1\n").arg(m_cdpHttpUrl);
63+
info += QStringLiteral("CDP WebSocket URL: %1\n").arg(m_cdpWsUrl);
64+
}
5865
return info;
5966
}
6067

@@ -65,6 +72,17 @@ void MiniAppInstance::start()
6572

6673
setState(Idle);
6774

75+
// Pre-launch bind-test for CDP debug port
76+
if (m_def.debugPort > 0) {
77+
QTcpSocket sock;
78+
if (!sock.bind(QHostAddress::LocalHost, static_cast<quint16>(m_def.debugPort))) {
79+
m_lastError = tr("Debug port %1 is in use").arg(m_def.debugPort);
80+
setState(Failed);
81+
return;
82+
}
83+
sock.close();
84+
}
85+
6886
if (!m_def.command.isEmpty()) {
6987
spawnProcess();
7088
} else {
@@ -87,6 +105,8 @@ void MiniAppInstance::retry()
87105
m_webView->deleteLater();
88106
m_webView = nullptr;
89107
}
108+
m_cdpHttpUrl.clear();
109+
m_cdpWsUrl.clear();
90110
start();
91111
}
92112

@@ -112,13 +132,24 @@ void MiniAppInstance::destroy()
112132
m_nam->deleteLater();
113133
m_nam = nullptr;
114134
}
135+
m_cdpHttpUrl.clear();
136+
m_cdpWsUrl.clear();
115137
emit finished();
116138
}
117139

118140
void MiniAppInstance::setState(State s)
119141
{
120142
if (m_state == s) return;
121143
m_state = s;
144+
145+
// Clear CDP URLs on terminal/idle states per spec
146+
if (s == Failed || s == Crashed || s == Idle) {
147+
m_cdpHttpUrl.clear();
148+
m_cdpWsUrl.clear();
149+
if (m_webView)
150+
m_webView->hideCdpUrl();
151+
}
152+
122153
emit stateChanged(s);
123154
emit titleChanged(buildTitle());
124155
}
@@ -261,7 +292,7 @@ void MiniAppInstance::onHealthPoll()
261292

262293
void MiniAppInstance::createWebView()
263294
{
264-
m_webView = WebViewWidget::create(m_def.id, QUrl(m_def.url), nullptr);
295+
m_webView = WebViewWidget::create(m_def.id, QUrl(m_def.url), m_def.debugPort, nullptr);
265296
if (!m_webView) {
266297
// Platform doesn't support embedded webview (Linux)
267298
setState(Running);
@@ -272,6 +303,12 @@ void MiniAppInstance::createWebView()
272303
this, &MiniAppInstance::onWebViewNavigationCompleted);
273304
connect(m_webView, &WebViewWidget::processFailed,
274305
this, &MiniAppInstance::onWebViewProcessFailed);
306+
connect(m_webView, &WebViewWidget::cdpReady,
307+
this, [this](const QString &httpUrl, const QString &wsUrl) {
308+
m_cdpHttpUrl = httpUrl;
309+
m_cdpWsUrl = wsUrl;
310+
emit cdpUrlChanged(httpUrl);
311+
});
275312

276313
setState(Initializing);
277314
}

src/MiniAppInstance.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
class QNetworkAccessManager;
2222
class QNetworkReply;
23+
class QTcpSocket;
2324
class WebViewWidget;
2425

2526
class MiniAppInstance : public QObject
@@ -49,6 +50,9 @@ class MiniAppInstance : public QObject
4950
const MiniAppDefinition &definition() const { return m_def; }
5051
QString debugInfo() const;
5152

53+
QString cdpHttpUrl() const { return m_cdpHttpUrl; }
54+
QString cdpWsUrl() const { return m_cdpWsUrl; }
55+
5256
WebViewWidget *webViewWidget() const { return m_webView; }
5357
void setDockWidget(ads::CDockWidget *dw) { m_dockWidget = dw; }
5458
ads::CDockWidget *dockWidget() const { return m_dockWidget; }
@@ -60,6 +64,7 @@ class MiniAppInstance : public QObject
6064
signals:
6165
void stateChanged(MiniAppInstance::State newState);
6266
void titleChanged(const QString &title);
67+
void cdpUrlChanged(const QString &httpUrl);
6368
void finished();
6469

6570
private slots:
@@ -86,5 +91,7 @@ private slots:
8691
WebViewWidget *m_webView = nullptr;
8792
QPointer<ads::CDockWidget> m_dockWidget;
8893
QString m_lastError;
94+
QString m_cdpHttpUrl;
95+
QString m_cdpWsUrl;
8996
};
9097

src/MiniAppManager.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include "NotepadNextApplication.h"
1313
#include "WebViewWidget.h"
1414

15+
#include <QApplication>
16+
#include <QClipboard>
1517
#include <QDesktopServices>
1618
#include <QDialog>
1719
#include <QFont>
@@ -140,6 +142,12 @@ void MiniAppManager::launchApp(const MiniAppDefinition &def)
140142
layout->addWidget(text);
141143
dlg.exec();
142144
});
145+
if (instance->definition().debugPort > 0) {
146+
QAction *cdpAction = menu.addAction(tr("Copy CDP URL"), this, [instance]() {
147+
QApplication::clipboard()->setText(instance->cdpHttpUrl());
148+
});
149+
cdpAction->setEnabled(!instance->cdpHttpUrl().isEmpty());
150+
}
143151
menu.addSeparator();
144152
menu.addAction(tr("Retry"), instance, &MiniAppInstance::retry);
145153
menu.addAction(tr("Close"), dw, &ads::CDockWidget::closeDockWidget);

src/MiniAppRegistry.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ QList<MiniAppDefinition> MiniAppRegistry::workspaceApps(const QString &workspace
6161
def.icon = obj.value(QStringLiteral("icon")).toString();
6262
def.healthCheckUrl = obj.value(QStringLiteral("healthCheckUrl")).toString();
6363
def.healthTimeoutMs = obj.value(QStringLiteral("healthTimeoutMs")).toInt(30000);
64+
def.debugPort = obj.value(QStringLiteral("debugPort")).toInt(0);
6465
def.autoKillOnClose = obj.value(QStringLiteral("autoKillOnClose")).toBool(true);
6566
if (!def.name.isEmpty())
6667
result.append(def);
@@ -100,6 +101,7 @@ void MiniAppRegistry::setWorkspaceApps(const QString &workspacePath, const QList
100101
if (!def.icon.isEmpty()) obj.insert(QStringLiteral("icon"), def.icon);
101102
if (!def.healthCheckUrl.isEmpty()) obj.insert(QStringLiteral("healthCheckUrl"), def.healthCheckUrl);
102103
if (def.healthTimeoutMs != 30000) obj.insert(QStringLiteral("healthTimeoutMs"), def.healthTimeoutMs);
104+
if (def.debugPort > 0) obj.insert(QStringLiteral("debugPort"), def.debugPort);
103105
if (!def.autoKillOnClose) obj.insert(QStringLiteral("autoKillOnClose"), false);
104106
arr.append(obj);
105107
}
@@ -157,6 +159,7 @@ QList<MiniAppDefinition> MiniAppRegistry::parseJson(const QString &json)
157159
def.icon = obj.value(QStringLiteral("icon")).toString();
158160
def.healthCheckUrl = obj.value(QStringLiteral("healthCheckUrl")).toString();
159161
def.healthTimeoutMs = obj.value(QStringLiteral("healthTimeoutMs")).toInt(30000);
162+
def.debugPort = obj.value(QStringLiteral("debugPort")).toInt(0);
160163
def.autoKillOnClose = obj.value(QStringLiteral("autoKillOnClose")).toBool(true);
161164
if (!def.name.isEmpty())
162165
result.append(def);
@@ -179,6 +182,7 @@ QString MiniAppRegistry::toJson(const QList<MiniAppDefinition> &apps)
179182
if (!def.icon.isEmpty()) obj.insert(QStringLiteral("icon"), def.icon);
180183
if (!def.healthCheckUrl.isEmpty()) obj.insert(QStringLiteral("healthCheckUrl"), def.healthCheckUrl);
181184
if (def.healthTimeoutMs != 30000) obj.insert(QStringLiteral("healthTimeoutMs"), def.healthTimeoutMs);
185+
if (def.debugPort > 0) obj.insert(QStringLiteral("debugPort"), def.debugPort);
182186
if (!def.autoKillOnClose) obj.insert(QStringLiteral("autoKillOnClose"), false);
183187
arr.append(obj);
184188
}

src/dialogs/EditMiniAppsDialog.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
#include <QLabel>
1717
#include <QLineEdit>
1818
#include <QListWidget>
19+
#include <QMessageBox>
1920
#include <QPlainTextEdit>
2021
#include <QPushButton>
22+
#include <QSet>
2123
#include <QSpinBox>
2224
#include <QSplitter>
25+
#include <QTcpSocket>
26+
#include <QHostAddress>
2327
#include <QTimer>
2428
#include <QUuid>
2529
#include <QVBoxLayout>
@@ -134,6 +138,28 @@ EditMiniAppsDialog::EditMiniAppsDialog(MiniAppRegistry *registry,
134138
advLayout->addWidget(m_timeoutSpin);
135139
formLayout->addWidget(m_advancedGroup);
136140

141+
// Debug section (collapsible)
142+
m_debugGroup = new QGroupBox(tr("Debug"), rightWidget);
143+
m_debugGroup->setCheckable(true);
144+
m_debugGroup->setChecked(false);
145+
auto *debugLayout = new QVBoxLayout(m_debugGroup);
146+
debugLayout->addWidget(new QLabel(tr("CDP Debug Port:"), m_debugGroup));
147+
auto *portLayout = new QHBoxLayout;
148+
m_debugPortSpin = new QSpinBox(m_debugGroup);
149+
m_debugPortSpin->setRange(0, 65535);
150+
m_debugPortSpin->setSpecialValueText(tr("Disabled"));
151+
m_debugPortSpin->setToolTip(tr("0 = disabled. Set a port to enable Chrome DevTools Protocol debugging."));
152+
portLayout->addWidget(m_debugPortSpin, 1);
153+
m_randomPortBtn = new QPushButton(tr("Random"), m_debugGroup);
154+
m_randomPortBtn->setToolTip(tr("Find an available port in range 9222-9322"));
155+
portLayout->addWidget(m_randomPortBtn);
156+
debugLayout->addLayout(portLayout);
157+
m_portWarningLabel = new QLabel(m_debugGroup);
158+
m_portWarningLabel->setStyleSheet(QStringLiteral("color: orange; font-size: 10px;"));
159+
m_portWarningLabel->hide();
160+
debugLayout->addWidget(m_portWarningLabel);
161+
formLayout->addWidget(m_debugGroup);
162+
137163
formLayout->addStretch();
138164

139165
splitter->addWidget(leftWidget);
@@ -158,10 +184,12 @@ EditMiniAppsDialog::EditMiniAppsDialog(MiniAppRegistry *registry,
158184
connect(m_upBtn, &QPushButton::clicked, this, &EditMiniAppsDialog::onMoveUpClicked);
159185
connect(m_downBtn, &QPushButton::clicked, this, &EditMiniAppsDialog::onMoveDownClicked);
160186
connect(m_browseCwdBtn, &QPushButton::clicked, this, &EditMiniAppsDialog::onBrowseCwdClicked);
187+
connect(m_randomPortBtn, &QPushButton::clicked, this, &EditMiniAppsDialog::onRandomPortClicked);
161188
connect(m_scopeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
162189
this, &EditMiniAppsDialog::onScopeChanged);
163190
connect(m_urlEdit, &QLineEdit::textChanged, this, [this]() { m_validateTimer->start(); });
164191
connect(m_envEdit, &QPlainTextEdit::textChanged, this, [this]() { m_validateTimer->start(); });
192+
connect(m_debugPortSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, [this]() { m_validateTimer->start(); });
165193

166194
connect(m_buttonBox, &QDialogButtonBox::accepted, this, [this]() {
167195
commitCurrentApp();
@@ -248,6 +276,7 @@ void EditMiniAppsDialog::commitCurrentApp()
248276
def.env = m_envEdit->toPlainText();
249277
def.healthCheckUrl = m_healthUrlEdit->text().trimmed();
250278
def.healthTimeoutMs = m_timeoutSpin->value() * 1000;
279+
def.debugPort = m_debugGroup->isChecked() ? m_debugPortSpin->value() : 0;
251280

252281
// Ensure ID
253282
if (def.id.isEmpty())
@@ -271,6 +300,8 @@ void EditMiniAppsDialog::loadApp(int row)
271300
m_envEdit->setEnabled(valid);
272301
m_healthUrlEdit->setEnabled(valid);
273302
m_timeoutSpin->setEnabled(valid);
303+
m_debugPortSpin->setEnabled(valid);
304+
m_randomPortBtn->setEnabled(valid);
274305

275306
if (!valid) {
276307
m_nameEdit->clear();
@@ -280,8 +311,10 @@ void EditMiniAppsDialog::loadApp(int row)
280311
m_envEdit->clear();
281312
m_healthUrlEdit->clear();
282313
m_timeoutSpin->setValue(30);
314+
m_debugPortSpin->setValue(0);
283315
m_urlWarningLabel->hide();
284316
m_envWarningLabel->hide();
317+
m_portWarningLabel->hide();
285318
return;
286319
}
287320

@@ -293,7 +326,10 @@ void EditMiniAppsDialog::loadApp(int row)
293326
m_envEdit->setPlainText(def.env);
294327
m_healthUrlEdit->setText(def.healthCheckUrl);
295328
m_timeoutSpin->setValue(def.healthTimeoutMs / 1000);
329+
m_debugPortSpin->setValue(def.debugPort);
330+
m_debugGroup->setChecked(def.debugPort > 0);
296331
m_urlWarningLabel->hide();
332+
m_portWarningLabel->hide();
297333
validateFields();
298334
}
299335

@@ -372,6 +408,26 @@ void EditMiniAppsDialog::validateFields()
372408
m_urlWarningLabel->hide();
373409
}
374410

411+
// Debug port conflict validation
412+
const int port = m_debugPortSpin->value();
413+
if (port > 0 && m_currentRow >= 0) {
414+
bool conflict = false;
415+
for (int i = 0; i < m_apps.size(); ++i) {
416+
if (i == m_currentRow) continue;
417+
if (m_apps[i].debugPort == port) {
418+
m_portWarningLabel->setText(tr("Port %1 is already used by \"%2\"")
419+
.arg(port).arg(m_apps[i].name));
420+
m_portWarningLabel->show();
421+
conflict = true;
422+
break;
423+
}
424+
}
425+
if (!conflict)
426+
m_portWarningLabel->hide();
427+
} else {
428+
m_portWarningLabel->hide();
429+
}
430+
375431
// Env validation (same as EditTasksDialog)
376432
const QString envText = m_envEdit->toPlainText();
377433
if (envText.trimmed().isEmpty()) {
@@ -405,3 +461,32 @@ void EditMiniAppsDialog::updateButtonStates()
405461
m_upBtn->setEnabled(row > 0);
406462
m_downBtn->setEnabled(row >= 0 && row < count - 1);
407463
}
464+
465+
void EditMiniAppsDialog::onRandomPortClicked()
466+
{
467+
// Collect ports already assigned to other apps in the list
468+
QSet<int> usedPorts;
469+
for (int i = 0; i < m_apps.size(); ++i) {
470+
if (i == m_currentRow) continue;
471+
if (m_apps[i].debugPort > 0)
472+
usedPorts.insert(m_apps[i].debugPort);
473+
}
474+
475+
// Scan 9222-9322 for an available port
476+
for (int port = 9222; port <= 9322; ++port) {
477+
if (usedPorts.contains(port))
478+
continue;
479+
480+
// Bind-test: check if port is actually free on the system
481+
QTcpSocket sock;
482+
if (sock.bind(QHostAddress::LocalHost, static_cast<quint16>(port))) {
483+
sock.close();
484+
m_debugPortSpin->setValue(port);
485+
return;
486+
}
487+
}
488+
489+
// All ports exhausted
490+
QMessageBox::warning(this, tr("No Available Port"),
491+
tr("No available port in range 9222-9322. Please enter a port manually."));
492+
}

src/dialogs/EditMiniAppsDialog.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class QPlainTextEdit;
2323
class QPushButton;
2424
class QSpinBox;
2525
class QTimer;
26+
class QToolButton;
2627

2728
class EditMiniAppsDialog : public QDialog
2829
{
@@ -41,6 +42,7 @@ private slots:
4142
void onMoveDownClicked();
4243
void onBrowseCwdClicked();
4344
void onScopeChanged(int index);
45+
void onRandomPortClicked();
4446
void validateFields();
4547

4648
private:
@@ -77,6 +79,12 @@ private slots:
7779
QLineEdit *m_healthUrlEdit = nullptr;
7880
QSpinBox *m_timeoutSpin = nullptr;
7981

82+
// Debug section
83+
QGroupBox *m_debugGroup = nullptr;
84+
QSpinBox *m_debugPortSpin = nullptr;
85+
QPushButton *m_randomPortBtn = nullptr;
86+
QLabel *m_portWarningLabel = nullptr;
87+
8088
QLabel *m_urlWarningLabel = nullptr;
8189
QDialogButtonBox *m_buttonBox = nullptr;
8290
};

0 commit comments

Comments
 (0)