From c94f53889c6b68598ef901b39b91fe008dcdb416 Mon Sep 17 00:00:00 2001 From: Bikebrainz <133275931+Bikebrainz@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:07:34 -0600 Subject: [PATCH 1/3] Run native Repeater sends in the background and expose Stop --- .github/workflows/ci.yml | 2 + Src/App/app.qml | 28 ++++- Src/Core/Networking/repeater.hpp | 1 + scripts/native_repeater_controls_test.py | 134 +++++++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 scripts/native_repeater_controls_test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42176999..f0fb2ced 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -158,6 +158,8 @@ jobs: run: python scripts/outbound_scope_regression.py build/Src/App/Release/NullockApp.exe - name: control API and Repeater responsiveness run: python scripts/control_responsiveness_regression.py build/Src/App/Release/NullockApp.exe + - name: native Repeater Send and Stop controls + run: python scripts/native_repeater_controls_test.py - name: workspace crash recovery run: python scripts/workspace_recovery_regression.py build/Src/App/Release/NullockApp.exe - name: authenticated scanner regression diff --git a/Src/App/app.qml b/Src/App/app.qml index 871f89bc..af63cd50 100644 --- a/Src/App/app.qml +++ b/Src/App/app.qml @@ -675,6 +675,7 @@ ApplicationWindow { spacing: 8 HeaderCell { text: "Target" } TextField { + id: repeaterHostField Layout.preferredWidth: 280 placeholderText: "host" text: repeater.host @@ -686,8 +687,10 @@ ApplicationWindow { } HeaderCell { text: ":" } TextField { + id: repeaterPortField Layout.preferredWidth: 80 text: "" + repeater.port + validator: IntValidator { bottom: 1; top: 65535; locale: "C" } color: root.text font.family: "Consolas" font.pixelSize: 12 @@ -701,11 +704,32 @@ ApplicationWindow { Item { Layout.fillWidth: true } Cell { text: repeater.statusLine; color: root.accent; Layout.preferredWidth: 200 } AccentButton { - label: repeater.busy ? "..." : "Send" - onClicked: repeater.send() + label: repeater.cancelling ? "Stopping..." : "Stop" + visible: repeater.busy + enabled: !repeater.cancelling + opacity: enabled ? 1 : 0.5 + onClicked: repeater.cancel() + ToolTip.visible: stopHover.hovered + ToolTip.text: "Stops after the current response or transport timeout; skips further redirects." + HoverHandler { id: stopHover } + } + AccentButton { + label: repeater.busy ? "Sending..." : "Send" + enabled: !repeater.busy && repeaterHostField.text.trim().length > 0 + && repeaterPortField.acceptableInput && repeater.requestText.length > 0 + opacity: enabled ? 1 : 0.5 + onClicked: { + // Rectangle buttons do not necessarily take focus from a field. + // Commit the visible target before copying it into the worker. + repeater.host = repeaterHostField.text.trim() + repeater.port = Number(repeaterPortField.text) + repeater.sendAsync() + } } AccentButton { label: "Clear" + enabled: !repeater.busy + opacity: enabled ? 1 : 0.5 onClicked: repeater.clear() } } diff --git a/Src/Core/Networking/repeater.hpp b/Src/Core/Networking/repeater.hpp index 636d873f..2eb33e24 100644 --- a/Src/Core/Networking/repeater.hpp +++ b/Src/Core/Networking/repeater.hpp @@ -90,6 +90,7 @@ class Repeater : public QObject { Q_PROPERTY(qint64 elapsedMs READ elapsedMs NOTIFY responseChanged) Q_PROPERTY(int responseBytes READ responseBytes NOTIFY responseChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + Q_PROPERTY(bool cancelling READ cancelling NOTIFY busyChanged) Q_PROPERTY(int activeTab READ activeTab NOTIFY tabsChanged) Q_PROPERTY(int tabCount READ tabCount NOTIFY tabsChanged) // Recompute Content-Length from the actual body before each send (Burp's diff --git a/scripts/native_repeater_controls_test.py b/scripts/native_repeater_controls_test.py new file mode 100644 index 00000000..049ec2f7 --- /dev/null +++ b/scripts/native_repeater_controls_test.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Exercise the actual native Repeater controls in Qt's QML runtime.""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def block(source, start): + """Extract a QML component, ignoring braces inside strings and comments.""" + opening = source.index('{', start) + depth, quote, escaped, line_comment, block_comment = 0, '', False, False, False + i = opening + while i < len(source): + char, pair = source[i], source[i:i + 2] + if line_comment: + if char == '\n': line_comment = False + elif block_comment: + if pair == '*/': block_comment = False; i += 1 + elif quote: + if escaped: escaped = False + elif char == '\\': escaped = True + elif char == quote: quote = '' + elif pair == '//': line_comment = True; i += 1 + elif pair == '/*': block_comment = True; i += 1 + elif char in ('"', "'"): quote = char + elif char == '{': depth += 1 + elif char == '}': + depth -= 1 + if depth == 0: return source[start:i + 1] + i += 1 + raise AssertionError('unclosed QML block') + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('qml', nargs='?', default=shutil.which('qml')) + runner = parser.parse_args().qml + assert runner, 'Qt qml runtime is required' + source = (ROOT / 'Src/App/app.qml').read_text(encoding='utf-8') + components = '\n'.join(block(source, source.index('component ' + name + ' :')) + for name in ['Cell', 'HeaderCell', 'AccentButton']) + field = source.index('id: repeaterHostField') + controls = block(source, source.rfind('Rectangle {', 0, field)) + harness = '''import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +ApplicationWindow { + id: root + width: 1480; height: 180; visible: true + property color accent: "orange" + property color text: "white" + property color pane: "black" + property color line: "gray" + QtObject { + id: repeater + property string host: "old.fixture" + property int port: 80 + property bool useTls: false + property string requestText: "GET / HTTP/1.1\\r\\n\\r\\n" + property string statusLine: "" + property bool busy: false + property bool cancelling: false + property int sends: 0 + property int stops: 0 + property int clears: 0 + function send() { throw new Error("native control called blocking Send") } + function sendAsync() { sends++; busy = true } + function cancel() { stops++; cancelling = true } + function clear() { clears++; requestText = "" } + } + COMPONENTS + ColumnLayout { anchors.fill: parent; CONTROLS } + function check(ok, message) { if (!ok) throw new Error(message) } + function button(item, label) { + if (item.label === label) return item + const children = item.children || [] + for (let i = 0; i < children.length; ++i) { + const found = button(children[i], label) + if (found) return found + } + return null + } + Component.onCompleted: Qt.callLater(function() { + try { + const send = button(root.contentItem, "Send") + const stop = button(root.contentItem, "Stop") + const clear = button(root.contentItem, "Clear") + check(send && stop && clear, "missing native controls") + for (const port of ["", "0", "65536", "invalid"]) { + repeaterPortField.text = port + check(!send.enabled, "Send enabled for invalid port: " + port) + } + repeaterPortField.text = "8081" + repeaterHostField.text = " " + check(!send.enabled, "Send enabled for blank host") + repeaterHostField.text = " new.fixture " + check(send.enabled, "valid draft cannot send") + send.clicked() + check(repeater.sends === 1 && repeater.busy, "Send did not start background work") + check(repeater.host === "new.fixture" && repeater.port === 8081, "visible target was not committed") + check(!send.enabled && !clear.enabled && stop.visible, "busy controls are incorrect") + stop.clicked() + check(repeater.stops === 1 && repeater.cancelling, "Stop did not cancel") + check(!stop.enabled && stop.label === "Stopping...", "pending Stop state is incorrect") + repeater.busy = false + repeater.cancelling = false + check(!stop.visible && send.enabled && clear.enabled, "controls did not recover after completion") + clear.clicked() + check(repeater.clears === 1 && !send.enabled, "empty draft can send after Clear") + console.log("PASS: native Send uses the worker, commits visible targets, validates inputs and exposes Stop/busy states") + Qt.exit(0) + } catch (error) { console.error(error); Qt.exit(1) } + }) +} +'''.replace('COMPONENTS', components).replace('CONTROLS', controls) + with tempfile.TemporaryDirectory(prefix='nullock-native-controls-') as temporary: + file = Path(temporary) / 'controls.qml' + file.write_text(harness, encoding='utf-8') + result = subprocess.run([runner, str(file)], capture_output=True, text=True, timeout=20, + env={**os.environ, 'QT_QPA_PLATFORM': 'offscreen', 'QT_QUICK_BACKEND': 'software', + 'QT_QUICK_CONTROLS_STYLE': 'Basic', 'QT_FORCE_STDERR_LOGGING': '1'}, + creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0) + output = result.stdout + result.stderr + print(output, end='') + assert result.returncode == 0 and 'PASS: native Send' in output, f'native QML controls failed ({result.returncode})' + + +if __name__ == '__main__': + main() From 0b21ffd3478df581f6ad2080a19c0557f299bb22 Mon Sep 17 00:00:00 2001 From: Bikebrainz <133275931+Bikebrainz@users.noreply.github.com> Date: Tue, 29 Sep 2026 20:09:15 -0600 Subject: [PATCH 2/3] Document native Repeater background sends and remaining cancellation limits --- CHANGELOG.md | 8 ++++++++ docs/changelog.html | 5 +++-- docs/docs/index.html | 2 +- docs/reviews/2026-09-29-build-follow-up.md | 15 ++++++++++----- docs/roadmap/index.html | 4 ++-- docs/roadmap/parity.json | 14 +++++++------- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf019e48..496f98e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ developer-facing record. ## [Unreleased] +### Native Repeater responsiveness +- Run native-window sends on the existing background worker. Show Send/Stop + progress and disable Send/Clear while the request is active. +- Copy the visible target fields before sending, reject blank hosts and ports + outside 1–65535, and expose the pending cancellation state to QML. +- Stop prevents further redirects after the current response or transport + timeout. Immediate socket cancellation and simultaneous sends remain open work. + ### Reflected HTML context handling - Keep HTML template contents inert, including nested templates and apparent closing tags inside raw text, comments or attributes. diff --git a/docs/changelog.html b/docs/changelog.html index 93189ac0..0e952b08 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -52,12 +52,13 @@

What shipped

The development build includes corrected security checks, certificate-cache validation and Repeater CLI fixes; v3.7.0 remains the latest public release, and the v3.8.0 notes below are a draft.