Skip to content

Stop-aware waits, per-thread context and UI Automation, config-sync housekeeping, weak GUI callbacks - #512

Merged
JE-Chen merged 20 commits into
mainfrom
feat/progress-sweep-4
Oct 9, 2026
Merged

JE-Chen merged 20 commits into
mainfrom
feat/progress-sweep-4

Conversation

@JE-Chen

@JE-Chen JE-Chen commented Oct 9, 2026

Copy link
Copy Markdown
Member

What

  • Executor and servers — a stop wakes the polling waits; MCP gains ac_stop_execution and ac_list_executions; ChatOps says a stopped command was stopped; a USB ACL file that fails its integrity check denies every device; the RBAC caller, the bound device, the heal stamp and the stop token stay on the thread that set them.
  • Config sync — an oversize blob is refused before upload with a fixed message; AC_config_sync_collect_blobs removes blobs nothing references; the blob quota is shared across processes; an acknowledged deletion is held for 30 days so an unlisted machine meets it as a conflict; the folder mirror notices an edit in the same clock tick; the tab has a section picker and an assets_server switch.
  • GUI — task callbacks are held weakly; dispose() on the six remote-desktop classes and 17 tabs; closed tabs' pages are parked for a restyle; the Windows UI Automation backend gives each calling thread its own automation object and the accessibility tabs run off the GUI thread; the list of modules failing against the real PySide6 types shrinks from 70 to 18.
  • CI — the three macOS probes a runner has now measured are asserted.

Compatibility

CHANGELOG.md, Unreleased › Changed. Most visible: releasing the Remote Desktop tab ends a running WebRTC host or viewer session; UsbAcl built with default_policy="allow" denies while its file fails the check; an acknowledged config-sync deletion stays in the bucket for 30 days.

Checks

  • Full headless suite locally (Windows, Python 3.14): 12,937 passed; the 11 failures were the quoted counts, updated in the last commit.
  • ruff, bandit, radon cc -nc, the typing contract (plain and --extras) and the Qt-free import check pass.

Not verified by any run

  • The UI Automation threading is tested against fake COM objects only: neither the development machine nor CI has comtypes with a desktop.
  • The signaling-server tests skip locally and run here for the first time.
  • No free-threaded interpreter locally; the free-threaded-scope job is the first real run of the per-thread context tests.

Progress.md lists what remains.

JE-Chen added 19 commits October 9, 2026 06:58
…s its tab alive

A task handle and the relay under it are descendants of the tab. A partial of a
bound method, a lambda or a nested function connected to one of them, and the
controller's own registry, held the tab's wrapper; the last reference to a
parentless tab was then released from inside its grandchild's destructor and
the process aborted without a traceback.
…thread

Unregistering sends zeroconf's goodbye packets and joins its thread, and
registering probes the name first; both ran in the Publish and Stop slots. The
stats pollers' stop stays where it is: it only schedules a cancel on the
bridge's loop, which is now written down and pinned by a test.
Releasing the Remote Desktop tab stopped timers only: a connect still out went
on, the pop-out windows stayed, and a WebRTC host kept sharing the screen with
no panel left to stop it. A released computer-use or DAG tab's run kept going.
dispose() now cancels a tab's bare workers as well as its controller tasks, and
the rule that requires the hook covers tabs that only hold background work.
…d its real window

A closed tab's page is kept as a hidden child of the tab widget, inside the
window's tree, so a theme switch still restyled it in the same slot. And while
a page is parked its window() is the parking widget: the WebRTC tray's Open
would have shown that empty widget as a window instead of raising the main one.
An ACL built with default_policy="allow" kept allowing next to integrity_ok False, and rules read from an earlier intact version stayed in force after a later read failed. decide() now answers deny until the file reads intact again or a change is saved.
…ts the deletion

A tombstone waited only for devices under peers. A machine holding the entry that had never synced added it straight back once the tombstone was collected, with no conflict and no report. An acknowledged tombstone is now kept for thirty days from the commit that first carried it (deleted_at), so such a machine's copy becomes a conflict instead. Age only ever keeps a tombstone longer; acknowledgement by the listed devices stays necessary.
The folder mirror called a file changed only when its mtime was later than the recorded one, so an edit landing in the same tick (seen in CI right after a received file was noted) was never pushed. It now compares the recorded time and size, and for a file recorded within two seconds of its own last write also the content hash, until that window has passed. The os.utime workarounds in the tests are gone.
…thing names

The server answers an oversized PUT from its declared length and closes; a client still sending could see the reset first, so the per-file reason read as a connection error. HttpAssetTransport now reads max_blob_bytes from the listing and refuses locally. Nothing ever deleted a blob: config_sync_collect_blobs removes the ones no entry names (AC_config_sync_collect_blobs, ac_config_sync_collect_blobs), going by the age the listing now reports. A blob write also holds a lock file, so two server processes sharing a folder share one quota.
The tab could only sync the default sections and had no way to keep large scripts on the sync server. It gains one checkbox per section, the assets_server switch, a locator file field and the blob housekeeping command, all remembered with the other form fields (never the secret). The example now imports everything from the facade, and the config sync document covers this round's changes.
…ty tabs off the GUI thread

The Windows UIA backend kept one COM object on the instance and relied on COM
having been initialised wherever comtypes was first imported. A script started
from the GUI now runs on a worker thread, where AC_a11y_* used an object the
GUI thread had created, on a thread COM was never initialised on. Each calling
thread now initialises COM and creates its own object; no raw element outlives
a call, so nothing crosses threads. With that in place the Accessibility tab's
three commands and the A11y Audit run on a worker like every other tab.

Verified only against fake COM objects that refuse use from another thread:
comtypes is not installed on the machine this was written on.
…rom 70 to 18

Three causes covered most of it: Qt enum members spelled without their enum
class, _tr() declared to return QWidget instead of the widget it was given,
and mixins passing themselves where Qt wants a QWidget. None of it changes
what runs; each enum pair was checked to be the same object before the rename.
QImage.fromData keeps its str format with a marked ignore: the stub asks for
bytes and the binding raises ValueError for bytes.
…say so

AC_wait_window, AC_wait_text, the smart waits, AC_expect_poll and the other polling waits slept on their own time.sleep, so a stopped run sat out the wait's timeout. They now sleep on run_control.pause, which is a plain sleep outside a stoppable run. A stopped ChatOps command answers "stopped" rather than "failed", and MCP gets ac_stop_execution and ac_list_executions.

A run's stop token is bound to the thread that entered the run (utils/thread_bound.py), so a thread started inside a run does not answer to its stop on a free-threaded build either.
…ad that set them

They live in context variables and were per-thread only because a new thread starts with an empty context. On a free-threaded build it starts with a copy of its creator's, so a scheduler or observer thread first started during an admin's request kept running as that admin. The values now count on the binding thread only, as the variable scope already does.
An action runs on the thread that executes its list, where the caller is already in scope: the request's own, or the owner scope a scheduler or trigger opened. An owner argument would only let a list's author name another user.
# Conflicts:
#	je_auto_control/gui/config_sync_tab.py
@codacy-production

codacy-production Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 801 complexity · 25 duplication

Metric Results
Complexity 801
Duplication 25

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

@JE-Chen
JE-Chen merged commit df4db96 into main Oct 9, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant