docs(mosaic): preserve negative-origin nested-grab probe evidence
This commit is contained in:
parent
4c1c395423
commit
88755fd92d
10 changed files with 1020 additions and 4 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### STEP20C experimental evidence
|
||||
|
||||
- Added a source-only negative-origin nested-grab probe. Two outputs at `x=-800/0` verified signed pointer conversion, real standard popup crossing, sampled private Mosaic clipping, hidden-owner return, standard-layout return and a positive horizontal regression. Production STEP20C support remains open.
|
||||
- Added a source-only stacked-output nested valid-grab probe. Both standard controls showed real popup overflow and pointer delivery below `y=600`; the private guarded Mosaic build clipped A/B at sampled lower points, preserved hidden-owner and standard-layout return, and reproduced the horizontal control. STEP20C production support remains open.
|
||||
- Added a source-only nested XDG grab fixture and a comparative headless probe. Two real button serials produced an interactive toplevel → popup A → popup B chain; sampled guarded Mosaic clipping, child-first dismissal, hidden-owner camera return and standard-layout return were feasible. STEP20C production support remains open.
|
||||
- Preserved the valid-serial XDG grab / hidden-owner probe's original C and Python sources, private patches and protocol XML with hashes and a disposable headless replay procedure. Its result remains **PARTIALLY_FEASIBLE**; no production source, package or UX policy changed.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,18 @@ popups, and `splith` restored the sampled ordinary lower-output behavior.
|
|||
This is vertical popup evidence only. The production compositor still has no
|
||||
qualified cross-output viewport clip or committed scene/transaction contract.
|
||||
|
||||
The [negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md)
|
||||
measured HEADLESS-1 at `(-800,0)` and HEADLESS-2 at `(0,0)` in both standard
|
||||
controls and private Mosaic. In the pinned wlroots path, the virtual-pointer
|
||||
absolute fraction maps through the signed output-layout box; the runner
|
||||
verified that `global_x+800` reaches the requested output and client surface.
|
||||
The disposable applied SceneFX clip and popup-owner guard excluded sampled
|
||||
right-output A/B pixels and hits, while standard layouts retained real
|
||||
crossing and popup buttons. A fully hidden applied owner and return to
|
||||
`splith` also passed at measured points. This remains experimental evidence:
|
||||
production viewport clipping, transaction consistency, continuous damage
|
||||
coverage and SceneFX ABI ownership are not settled.
|
||||
|
||||
## IPC
|
||||
|
||||
On a Mosaic workspace, `get_tree` reports `layout: "mosaic"` and adds a `mosaic` object with `active_row` and `rows`. Each row entry contains ordered child node IDs, `scroll_x`, and focused node ID or null. Existing Sway fields retain their shapes; normal workspace/window events and `get_workspaces` paths remain in use. This is an extension to the Sway layout vocabulary: strict IPC consumers with a closed layout enum must recognize `mosaic`. Additional rows in the model are not a promise of usable multi-row IPC behavior.
|
||||
|
|
|
|||
|
|
@ -47,7 +47,18 @@ coverage, other output geometries, migrations, transaction generations,
|
|||
effects, or SceneFX ABI/package maintenance. Production adjacent-output
|
||||
Mosaic remains **NOT YET SUPPORTED**.
|
||||
|
||||
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; the disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage too. Popups, effects, saved buffers, vertical Y-crossing behavior, pointer behavior in those additional arrangements and complete damage correctness remain unqualified. Multi-output Mosaic remains **NOT YET SUPPORTED**.
|
||||
The [negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md)
|
||||
is **FEASIBLE for its sampled matrix only**. With HEADLESS-1 actually at
|
||||
`x=-800`, signed global pointer requests reached both outputs, standard
|
||||
controls exposed real A/B popup pixels and hits beyond `x=0`, and the
|
||||
disposable guarded Mosaic build clipped those measured pixels and hits while
|
||||
preserving visible interactions. Hidden-owner and standard-layout return
|
||||
cases passed at measured points. This does not qualify other output
|
||||
transforms, migration/resize, complete damage/effects, transaction
|
||||
generations or SceneFX ABI/package maintenance. Production adjacent-output
|
||||
Mosaic remains **NOT YET SUPPORTED**.
|
||||
|
||||
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; that earlier disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage in that candidate. Popups, effects, saved buffers and complete damage correctness remain unqualified across the full matrix. The later probes above narrow individual cases without qualifying multi-output Mosaic, which remains **NOT YET SUPPORTED**.
|
||||
|
||||
The later [SceneFX subtree clipping spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) is also **PARTIALLY FEASIBLE**. Its private inherited tree clip stopped tested tiled pixels and pointer hits from crossing the output boundary, including a fully hidden cell, but a real GTK XDG popup rendered and received input on the neighboring output because its scene tree lives in the global popup layer. Dedicated subsurface, saved-buffer, nested-popup and complete effect/damage validation remain open. The spike changed no production library or compositor source; multi-output Mosaic remains **NOT YET SUPPORTED**.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
[Nested-grab source fixture](../tooling/probes/step20c-nested-grab/README.md) reconstructs the same disposable experiment.
|
||||
14. [STEP20C vertical-output nested-grab probe](design/STEP20C_VERTICAL_OUTPUT_NESTED_GRAB_PROBE.md): sampled vertical popup clipping and pointer evidence with two standard controls and private Mosaic; production gates remain open.
|
||||
[Vertical source fixture](../tooling/probes/step20c-vertical-grab/README.md) replays it in a disposable laboratory.
|
||||
15. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||
15. [STEP20C negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md): sampled signed-coordinate pointer, popup clipping, hidden-owner and standard-layout evidence with real `x=-800` headless origin; production gates remain open.
|
||||
[Negative-origin source fixture](../tooling/probes/step20c-negative-origin/README.md) replays it in a disposable laboratory.
|
||||
16. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
|
||||
|
||||
## Project
|
||||
|
||||
|
|
@ -61,6 +63,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
|
|||
| [STEP20C valid-grab and hidden-owner probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; valid-serial popup grab measured, hidden-owner leak found and closed only in a private guard; broader STEP20C gates remain open |
|
||||
| [STEP20C nested valid-grab probe](design/STEP20C_NESTED_VALID_GRAB_PROBE.md) | Experimental evidence | FEASIBLE for sampled two-level grabbed XDG popup cases on horizontal headless outputs; no production qualification |
|
||||
| [STEP20C vertical-output nested-grab probe](design/STEP20C_VERTICAL_OUTPUT_NESTED_GRAB_PROBE.md) | Experimental evidence | FEASIBLE for sampled two-level grabbed XDG popup cases across the horizontal output boundary; no production qualification |
|
||||
| [STEP20C negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md) | Experimental evidence | FEASIBLE for sampled two-level grabbed XDG popup cases with HEADLESS-1 at `x=-800`; no production qualification |
|
||||
|
||||
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,11 +42,21 @@ controls produced real Y-crossing A/B popup pixels and button targets, while
|
|||
the disposable guarded Mosaic build excluded sampled lower popup pixels/hits
|
||||
and preserved upper interaction, hidden-owner return and standard-layout
|
||||
return. Its [source fixture](../tooling/probes/step20c-vertical-grab/README.md)
|
||||
also reproduced the horizontal B-crossing control. The next separate geometry
|
||||
gate is a bounded negative-origin comparison. Scaling, rotation,
|
||||
also reproduced the horizontal B-crossing control. The separate negative-origin
|
||||
comparison is recorded below. Scaling, rotation,
|
||||
resize/migration, transaction generations, complete damage/effects and
|
||||
dependency maintenance remain open. STEP20C stays **NEXT**.
|
||||
|
||||
The [negative-origin nested-grab probe](design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md)
|
||||
is **FEASIBLE within its sampled signed-coordinate headless matrix**:
|
||||
HEADLESS-1 remained at `x=-800`, the virtual pointer reached both outputs,
|
||||
standard controls proved A/B crossing pixels and buttons, and the private
|
||||
guarded Mosaic build clipped sampled right-output pixels and hits. Hidden
|
||||
owner and `splith` return passed. Its [source fixture](../tooling/probes/step20c-negative-origin/README.md)
|
||||
preserves replay. Scaling, rotation, resize/migration, transaction
|
||||
generations, complete damage/effects and dependency maintenance remain open.
|
||||
STEP20C stays **NEXT**.
|
||||
|
||||
## STEP20D — multi-row Spatial Canvas · PLANNED
|
||||
|
||||
**Goal:** connect rows to runtime tree, transactions, scene, input, and IPC. **Planned deliverables:** active-row navigation, row-local scroll/focus, deterministic insertion/reflow, in-row reorder, and between-row movement. **Exit criteria:** real tiled containers preserve identity and order across row changes; focus and camera restore correctly; headless multi-row tests pass. **Docs first:** settle exact interactions and row invariants in UX and Architecture. Unset key combinations remain TBD.
|
||||
|
|
|
|||
338
docs/design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md
Normal file
338
docs/design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
# STEP20C negative-origin nested-grab probe
|
||||
|
||||
**Status: EXPERIMENTAL / FEASIBLE for the measured negative-origin matrix.** This is a
|
||||
disposable feasibility test, not a production clipping change or UX decision.
|
||||
The canonical [architecture](../ARCHITECTURE.md), [UX](../UX.md),
|
||||
[limitations](../KNOWN_LIMITATIONS.md) and [roadmap](../ROADMAP.md) retain
|
||||
authority.
|
||||
|
||||
## Problem and hypotheses (recorded before fixture adaptation)
|
||||
|
||||
An output at global `x=-800` makes global scene and pointer coordinates signed.
|
||||
The virtual-pointer protocol instead transmits unsigned absolute coordinates
|
||||
with unsigned extents. Passing a negative global X directly to that protocol
|
||||
would wrap it. **Hypothesis H1:** with two adjacent 800×600 outputs, wlroots
|
||||
maps normalized pointer X through the layout bounding box; therefore
|
||||
`injected_x=global_x-min_x=global_x+800` with extent 1600 reaches the
|
||||
requested global point. This must be confirmed from the pinned wlroots path
|
||||
and from client pointer events on both outputs before any clipping result is
|
||||
valid. A protocol-level injection acknowledgement alone is insufficient.
|
||||
|
||||
**Hypothesis H2:** the disposable SceneFX subtree clip, owner-derived popup
|
||||
clip and hidden-owner guard use signed applied scene positions correctly when
|
||||
the viewport begins at `-800`. The control layouts must first demonstrate
|
||||
real pixels and button delivery beyond `x=0`; configured rectangles alone do
|
||||
not prove a crossing. **Hypothesis H3:** changing Mosaic back to `splith`
|
||||
removes the private clip while both XDG popups remain mapped.
|
||||
|
||||
## Coordinate contract and requested geometry
|
||||
|
||||
| Quantity | HEADLESS-1 | HEADLESS-2 |
|
||||
| --- | --- | --- |
|
||||
| Requested global origin | `(-800,0)` | `(0,0)` |
|
||||
| Requested size, scale, transform | `800×600`, `1`, `normal` | `800×600`, `1`, `normal` |
|
||||
| Global X interval | `[-800,0)` | `[0,800)` |
|
||||
| Local X | `global_x+800` | `global_x` |
|
||||
|
||||
The requested layout bounds are `[-800,800) × [0,600)`, with border `x=0`.
|
||||
`get_outputs` is authoritative. If the compositor normalizes these origins,
|
||||
the negative-origin test is **INCONCLUSIVE** and the actual rectangles must
|
||||
still be retained. Workspace 1 and the tiled popup owner belong to
|
||||
HEADLESS-1; workspace 2 holds `wev` on HEADLESS-2.
|
||||
|
||||
For a requested global point `(-100,190)`, HEADLESS-1 local X is `700` and
|
||||
candidate virtual-pointer coordinates are `(700,190)` with extent `1600×600`.
|
||||
For `(100,190)`, HEADLESS-2 local X is `100` and the candidate transmitted X
|
||||
is `900`. The runner must log requested signed global point, layout minimum,
|
||||
transmitted normalized point, extents, reached output, client-local motion and
|
||||
button events. It must reject out-of-bounds and nonintegral injection values.
|
||||
|
||||
Mosaic row order and logical owner IPC rectangle remain distinct from camera
|
||||
and applied scene placement. For each sample, record logical `rect`,
|
||||
`window_rect`, `scroll_x`, applied owner/popup scene positions, SceneFX clip,
|
||||
XDG role configure rectangles and actual captures. The candidate applied
|
||||
content X is `owner.rect.x - scroll_x + owner.window_rect.x` in Mosaic;
|
||||
the compositor scene log and captured pixels must verify it. Popup A's XDG
|
||||
configured rectangle is relative to the toplevel surface; B's is relative
|
||||
to A's surface. Their resulting global rectangles are estimates until scene
|
||||
position and pixels corroborate them. XDG anchor rectangles must lie within
|
||||
their effective parent window geometry.
|
||||
|
||||
## Experimental method and acceptance
|
||||
|
||||
Reuse the historical nested client, the vertical fixture's parameterized
|
||||
pointer and the same three pinned private SceneFX/Mosaic patches, including
|
||||
the hidden-owner guard. Adapt only the new fixture directory. Run identical
|
||||
clients in (A) unmodified SwayFX `splith`, (B) private compositor `splith`,
|
||||
and (C) private compositor `mosaic`, all with verified negative output origins.
|
||||
Use isolated runtimes and sockets, `swaymsg -s`, two-output captures, IPC,
|
||||
`WAYLAND_DEBUG=1`, compositor logs and structured measurements. A separate
|
||||
positive-origin horizontal subset checks regression. No installed library,
|
||||
daily session or production source is touched.
|
||||
|
||||
1. Validate signed pointer targeting on both outputs, including round trip and
|
||||
left/right buttons. Failed targeting invalidates later graphics tests.
|
||||
2. Open two visible popups: TOPLEVEL button serial grabs A, then A button
|
||||
serial grabs B; verify distinct actual serials, parent XDG identities,
|
||||
configure/ack/map order and interaction with each surface.
|
||||
3. Test B crossing `x=0`, then A and B crossing. Both standard controls must
|
||||
show actual popup pixels and B/A button targets on HEADLESS-2. Mosaic must
|
||||
preserve visible left portions and suppress right popup pixels/hits, with
|
||||
ordinary-client events interpreted under normal XDG grab semantics.
|
||||
4. In separate runs, test outside-click dismissal, fully hidden applied owner
|
||||
with no click and with a click, and `layout splith` while popups remain open.
|
||||
5. Replay a horizontal positive-origin subset with the same adapted clients
|
||||
and injector. Derive samples from actual configured/scene geometry and
|
||||
capture data, never from assumed historic positions.
|
||||
|
||||
Each scenario retains output JSON, signed coordinate conversions, pointer
|
||||
events, owner IPC/Mosaic state, XDG configuration and identities, scene clips,
|
||||
two captures per stage, lifecycle events, process exit codes and a
|
||||
`measurements.json`. Rendering, hit testing, XDG grab, coordinate transform
|
||||
and lifecycle verdicts are separate. A screenshot alone or a protocol send
|
||||
alone cannot establish a pass. Keep failures and inconclusive runs visible.
|
||||
|
||||
The probe is **FEASIBLE** only if negative origins and control crossing are
|
||||
real, Mosaic excludes measured clipped pixels/hits while preserving visible
|
||||
interaction, and private standard layout has no blocking regression. It is
|
||||
**PARTIALLY_FEASIBLE** for demonstrated core behavior with matrix gaps,
|
||||
**BLOCKED** for reproducible rendering/input leak, XDG violation or blocking
|
||||
standard regression, and **INCONCLUSIVE** if geometry, crossing or pointer
|
||||
events cannot be established. These labels apply only to this probe.
|
||||
|
||||
## Initial observations and correction history
|
||||
|
||||
The first unmodified SwayFX `pointer` run recorded `get_outputs` with active
|
||||
HEADLESS-1 `(-800,0,800,600)` and HEADLESS-2 `(0,0,800,600)`, both scale `1`
|
||||
and transform `normal`. Thus SwayFX did apply the requested negative origin.
|
||||
The fixture transmitted `302` for global `-498` and `830` for global `30`.
|
||||
The owner received `wl_pointer.enter` with surface-local X `100`, then `105`
|
||||
on the return trip; `wev` received enters with local X `28`, then `33`
|
||||
(its surface starts inside the container). This establishes preliminary
|
||||
two-output targeting, subject to full button validation. Evidence is retained
|
||||
in `run-negative-control-pointer-initial/` under the new laboratory.
|
||||
|
||||
The first calibration checker falsely required `POINTER_MOTION` lines. An
|
||||
inter-output move produces `wl_pointer.enter` at the destination instead;
|
||||
both clients logged those enters. The checker now accepts enter or motion.
|
||||
That first run exited with a fixture `RuntimeError` after its captures; it is
|
||||
not classified as a compositor or clipping failure.
|
||||
|
||||
The actual wlroots 0.20.2 path confirms H1 at the source level:
|
||||
[`types/wlr_virtual_pointer_v1.c`](https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/0.20.2/types/wlr_virtual_pointer_v1.c)
|
||||
forms normalized `x / x_extent`, while
|
||||
[`types/wlr_cursor.c`](https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/0.20.2/types/wlr_cursor.c)
|
||||
converts that fraction to `mapping.width * x + mapping.x` using the output
|
||||
layout box when no device mapping overrides it. Sway's absolute-motion path
|
||||
uses the wlroots cursor. With box X `-800`, width `1600`, and transmitted X
|
||||
`global_x+800`, the nominal reconstructed global point is `global_x`.
|
||||
Client events remain the empirical check for device mapping and targeting.
|
||||
|
||||
The completed `pointer` runs in unmodified SwayFX, private `splith`, and
|
||||
private Mosaic each reached both outputs on two round trips. TOPLEVEL received
|
||||
right and left press/release (`button=273/272`); `wev` received left and
|
||||
right press/release. In the control, requested global `(-498,150)` reached
|
||||
TOPLEVEL local `(100,125)` and requested `(30,80)` reached `wev` local
|
||||
`(28,55)`; the approximately 2 px X and 25 px Y differences are the client
|
||||
surface inset, not an output-origin shift. Private `splith` repeated those
|
||||
coordinates, and private Mosaic used global `(-458,150)` for TOPLEVEL local
|
||||
`(100,125)` after its camera translation. All protocol X values were
|
||||
nonnegative and inside the `1600×600` extent.
|
||||
|
||||
All three negative-origin `visible` runs mapped A and B and kept them open
|
||||
through separate right clicks. A's role configure was `(50,80,250,170)` and
|
||||
B's `(20,60,170,130)` relative to its parent. Popup pixels were confined to
|
||||
HEADLESS-1 in this reference placement; HEADLESS-2 contained zero exact A
|
||||
yellow or B green pixels.
|
||||
|
||||
The negative-origin `b_cross` controls both configured B at
|
||||
`(-178,165,300,130)` after applied-owner translation and measured **15,860**
|
||||
exact B-green pixels on HEADLESS-2 at the open capture. Private Mosaic
|
||||
configured B at `(-138,165,300,130)` and measured **0** B-green pixels on
|
||||
HEADLESS-2 while retaining **17,940** on HEADLESS-1. These are preliminary
|
||||
render measurements; button traces and the remaining matrix are under review.
|
||||
|
||||
At the B crossing point, both standard controls delivered two B right-button
|
||||
events and retained **15,860** B-green pixels beyond `x=0`; private Mosaic
|
||||
delivered the left visible B button, then delivered the right-output button
|
||||
to `wev`, with no right-output B button. Its legitimate outside click then
|
||||
caused `popup_done` B before A. In the `both_cross` placement, both standard
|
||||
controls had **6,120** A-yellow and **27,560** B-green pixels on HEADLESS-2;
|
||||
private Mosaic had **0** of each there, while retaining **30,080** A-yellow
|
||||
and **6,240** B-green pixels on HEADLESS-1. A separate `a_cross` run is used
|
||||
for A's right-output button, since a first outside click can dismiss the
|
||||
grabbed hierarchy.
|
||||
|
||||
## Comparative results
|
||||
|
||||
All three compositors used HEADLESS-1 `(-800,0,800,600)` and HEADLESS-2
|
||||
`(0,0,800,600)` at scale 1, transform normal. The same nested client used
|
||||
TOPLEVEL XDG object 9, A object 12 and B object 15 in each run; A named
|
||||
TOPLEVEL as parent, B named A. In `b_cross`, both standard controls took real
|
||||
TOPLEVEL left-button serial **69** to grab A and A left-button serial **74**
|
||||
to grab B. Private Mosaic used **63** and **68**. Each `GRAB_REQUEST` used
|
||||
the exact target event's serial; configure, `xdg_surface` ack and MAP followed
|
||||
in order. No protocol error or immediate grab denial occurred. The anchored
|
||||
rectangles stay within their effective parent geometry: the horizontal
|
||||
`b_cross` A anchor X `200` is inside the standard top buffer width `420`,
|
||||
and B anchor X `220` is inside A's width `300`; `both_cross` A anchor X `350`
|
||||
is likewise inside `420`, B anchor X `160` inside A's width `350`.
|
||||
|
||||
| Scenario | Unmodified SwayFX `splith` | Private `splith` | Private Mosaic | Measured distinction |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Pointer reference | PASS | PASS | PASS | Signed global points reached both clients twice; left/right press and release on both outputs. |
|
||||
| A/B fully visible | PASS | PASS | PASS | Two grabs, mapped distinct surfaces, A/B right clicks, no unexpected dismissal. |
|
||||
| B crosses `x=0` | PASS | PASS | PASS | Standard B right pixels 15,860 and B button; Mosaic right pixels 0, B left button, `wev` right button and normal outside dismissal. |
|
||||
| A and B cross `x=0` | PASS | PASS | PASS | Standard right A/B pixels 6,120/27,560; Mosaic 0/0 with separate visible A/B clicks. |
|
||||
| A crossing hit in separate run | PASS | PASS | PASS | Standard right A button; Mosaic no right A button, `wev` button and normal dismissal. |
|
||||
| Outside click and lifecycle | PASS | PASS | PASS | `popup_done` B then A; client destroys B then A; ordinary client receives left button. |
|
||||
| Owner fully hidden, no click | N/A | N/A | PASS | Applied scene owner `(-1440,0,640,600)` does not intersect viewport `(-800,0,800,600)`; A/B pixels 0 then return with same XDG IDs. |
|
||||
| Owner hidden with click | N/A | N/A | PASS | No hidden A/B button; B then A dismissed; pixels stay absent after camera return. |
|
||||
| Mosaic → `splith` with A/B open | N/A | N/A | PASS | Right B pixels return to 15,860; B receives right button at `(20,195)` before any `popup_done`. |
|
||||
| Positive horizontal regression | PASS | PASS | PASS | Standard B right pixels 15,860; Mosaic 0 with the same button-routing distinction as the historical horizontal probe. |
|
||||
|
||||
The B-crossing point was global `(20,195)` on HEADLESS-2. In standard
|
||||
controls its open-capture RGB was B green `(32,208,160)` and its right click
|
||||
went to B. In private Mosaic that point was ordinary background
|
||||
`(238,238,238)`, and its right click went to `wev`; B received no button
|
||||
there. The corresponding Mosaic visible B point `(-98,195)` was green and
|
||||
delivered a B right button. For A, the separate standard crossing point
|
||||
`(87,125)` was A yellow `(224,192,32)` and delivered an A button; Mosaic's
|
||||
scene-adjusted point `(127,125)` was ordinary background and delivered no A
|
||||
button, while visible A/B points remained interactive. Ordinary-client
|
||||
delivery and popup dismissal are distinct observations; dismissal alone is
|
||||
never counted as a clipped-surface hit.
|
||||
|
||||
The private `PROBE_POPUP_CLIP` log recorded clip local `(-242,-25,800,600)`
|
||||
in the sampled Mosaic runs. Relative to viewport origin `(-800,0)`, this
|
||||
gives applied root popup-tree scene origin `(-558,25)`; combining the logged
|
||||
XDG configure offsets gives A `(-358,105,300,170)` and B
|
||||
`(-138,165,300,130)` in `b_cross`. These match the independently calculated
|
||||
owner/window/camera positions and the captured crossing colors. In
|
||||
`both_cross`, A is `(-208,105,350,170)` and B
|
||||
`(-48,165,300,130)`. A negative global origin is therefore present in both
|
||||
the applied clip and popup surface geometry, not only in an IPC label.
|
||||
|
||||
For hidden owner, the compositor logged `PROBE_POPUP_HIDDEN_OWNER` with
|
||||
applied `(-1440,0,640,600)` versus viewport `(-800,0,800,600)`, proving
|
||||
empty intersection from applied scene state. The no-click run's left A/B
|
||||
exact pixel counts were **42,200/17,940 → 0/0 → 42,200/17,940**. There
|
||||
was no `popup_done` before return and no new XDG CREATE or MAP; XDG IDs
|
||||
remained A 12 and B 15.
|
||||
The IPC owner stayed container ID 8 and `focused=true` in open, hidden,
|
||||
hidden-hover and returned snapshots; its logical rectangle stayed
|
||||
`(-160,25,640,575)` while the applied scene position changed with camera.
|
||||
In the separate hidden-click run, neither popup
|
||||
received the right button; B then A received `popup_done`, were destroyed
|
||||
child first and did not return. With both still open, switching to `splith`
|
||||
restored the standard right B-green count **15,860** and a B right-button
|
||||
press/release with no earlier dismissal.
|
||||
|
||||
The positive-origin regression used HEADLESS-1 `(0,0)` and HEADLESS-2
|
||||
`(800,0)` with the same compiled clients and pointer. Both standard controls
|
||||
again produced **15,860** B-green pixels on HEADLESS-2 and delivered the
|
||||
right-output button to B. Private Mosaic produced **0** and delivered the
|
||||
button to `wev`, followed by normal B/A dismissal. Counts happen to match
|
||||
the historical horizontal probe after checking the actual applied placement;
|
||||
the test did not assume their equality.
|
||||
|
||||
## Scope of conclusion
|
||||
|
||||
This negative-origin, two-output, nested-valid-grab experiment is
|
||||
**FEASIBLE for the measured placements and event points**. It establishes
|
||||
correct signed-to-absolute pointer conversion in the observed wlroots path,
|
||||
real control crossing, sampled private clipping/hit behavior, hidden-owner
|
||||
guard behavior and standard-layout restoration. It does not prove continuous
|
||||
pixel coverage between samples or define universal offscreen-popup dismissal
|
||||
UX. Scaling, rotation, output migration/resize, complete damage/effects,
|
||||
transaction generations and ABI/package maintenance remain **NOT TESTED**.
|
||||
STEP20C remains **NEXT** and production adjacent-output clipping remains
|
||||
**NOT YET SUPPORTED**.
|
||||
|
||||
## Independent freeze replay (2026-10-11)
|
||||
|
||||
A fresh laboratory, `/tmp/swayfx-mosaic-nested-grab-1DzZQbcH/`, was built by
|
||||
the preserved `prepare.sh` from full Git checkouts, rather than an archive.
|
||||
Its build log is `/tmp/mno-prepare-ltr6sSlW`. Fourteen new run directories
|
||||
cover pointer targeting on both outputs in all three configurations,
|
||||
negative-origin B crossing and A+B crossing in all three, private Mosaic
|
||||
hidden-owner return and `splith` return, and positive-origin B crossing in
|
||||
all three. `analyze.py` decoded the new PNGs. The fresh output JSON again
|
||||
reported `(-800,0,800,600)` and `(0,0,800,600)` for negative-origin runs.
|
||||
In each pointer run, the owner received destination `wl_pointer.enter` at
|
||||
local `(100,125)` and `(105,135)`, and `wev` received enters at `(28,55)`
|
||||
and `(33,60)`; both clients also logged press/release events. These client
|
||||
events, not injector acknowledgements, validate signed-global targeting.
|
||||
|
||||
New control and private-standard B-crossing captures each contain 15,860
|
||||
B-green pixels on HEADLESS-2 and two B button presses. Private Mosaic again
|
||||
contains zero B-green pixels there, preserves 17,940 on HEADLESS-1, delivers
|
||||
the visible B press, and sends the clipped-side press to `wev`, followed by
|
||||
normal B/A `popup_done`. For A+B crossing, both standard controls again
|
||||
contain 6,120 A-yellow and 27,560 B-green right-output pixels; private
|
||||
Mosaic contains zero of either there while retaining 30,080/6,240 on the
|
||||
left. The fresh logs show distinct A/B XDG objects and actual A/B grab
|
||||
serials (69/74 in the control B run; 63/68 in private Mosaic), with matching
|
||||
button serial, configure/ack/map order and no recorded protocol error.
|
||||
Serials varied in another control run, as expected; their numeric values are
|
||||
run evidence, not a protocol constant.
|
||||
|
||||
In the new no-click hidden-owner run, `scroll_x` moved 400 → 1280 → 400,
|
||||
both popup colors changed 42,200/17,940 → 0/0 → 42,200/17,940 on
|
||||
HEADLESS-1, the same XDG objects remained mapped, and no `popup_done`
|
||||
preceded return. In the new layout-return run, `splith` restored 15,860
|
||||
B-green pixels on HEADLESS-2 and a B press without prior dismissal. The
|
||||
positive-origin B-crossing subset reproduced the same control/private
|
||||
distinction. These are sampled stabilized observations: the replay did not
|
||||
capture every presented frame or establish complete damage coverage.
|
||||
|
||||
## Reproduction, provenance and validation
|
||||
|
||||
The retained laboratory is
|
||||
`/tmp/swayfx-mosaic-nested-grab-NWy4A23x/`; its preparation log is
|
||||
`/tmp/mno-prepare-4FC2ZHqF`. The source-only
|
||||
[fixture](../../tooling/probes/step20c-negative-origin/README.md) documents a
|
||||
fresh replay from a full Git checkout. The three unchanged private patches
|
||||
applied with `git apply --check` to Mosaic
|
||||
`cd550382b311efdbfe3ed892f4e1d281d852e754` and SceneFX
|
||||
`37ccd723bef49e6891156ffafce8f549f01446cc`. The historical pointer
|
||||
extents patch alone adapted a disposable copy of `pointer-daemon.c`; no new
|
||||
private patch was needed. The historical nested client was byte-identical.
|
||||
The environment used `/usr/bin/sway` 0.6, wlroots 0.20.2, SceneFX 0.5.0
|
||||
and Wayland client 1.26.0. `ldd` resolved the private Mosaic executable to
|
||||
the laboratory `libscenefx-0.5.so` and system `libwlroots-0.20.so`.
|
||||
|
||||
`artifacts-sha256.txt` lists **895** run files from the final and retained
|
||||
intermediate cases; its SHA-256 is
|
||||
`0f3d74cb3477aa24b5e0f6bcf84f736ffcd31280de04b08b4622a761046eb793`.
|
||||
`sha256sum -c` verified **895/895**. `negative-origin-source-sha256.txt`
|
||||
lists six source inputs; its SHA-256 is
|
||||
`8cbf6228c0bd317442262265692f4027e41fd82c2e7c7cd22bbb0e03ddaf729d`,
|
||||
and `sha256sum -c` verified **6/6**. Each run directory retains verified
|
||||
output JSON, signed injections, observed client-local pointer reference,
|
||||
XDG and `WAYLAND_DEBUG=1` traces, IPC trees, scene clips, two-output PNGs,
|
||||
RGB/count analysis, `popup_done` order and process exit status. The
|
||||
intermediate failed pointer-calibration check and its evidence remain under
|
||||
`run-negative-control-pointer-initial`; earlier `both_cross` and `toggle`
|
||||
attempts remain under `*-initial`. The final `both_cross` runs were replayed
|
||||
with the settled runner. The first `toggle` sampled B on the left after
|
||||
return; the final run explicitly sampled its right-output portion.
|
||||
|
||||
Private SceneFX and Mosaic builds passed; the Mosaic Meson model test passed
|
||||
**1/1**. C17 syntax checks passed for both client sources against generated
|
||||
protocol headers. Shell syntax, Python AST parsing without bytecode cache,
|
||||
document links, `git diff --check`, source hashes and artifact hashes passed.
|
||||
An independent structured matrix check passed pointer targeting, both grabs,
|
||||
crossing colors/buttons, outside dismissal, hidden-owner, layout return and
|
||||
positive regression. All completed case directories include actual exit
|
||||
codes; normal runner teardown produced compositor/client `0` and expected
|
||||
`SIGTERM` `-15` for the persistent pointer and `wev`. Their private runtime
|
||||
directories were removed after reaping; no probe process remained. These
|
||||
validation passes support only the measured scientific conclusion above.
|
||||
|
||||
No production Mosaic source, production patch, `PKGBUILD`, installed library,
|
||||
historical fixture, UX policy or active session was changed; at the time of
|
||||
the original experiment nothing had been committed or pushed. The smallest next bounded experiment is repeated
|
||||
open/close and camera movement at this same negative-origin geometry, with
|
||||
frame-by-frame stale-pixel and damage observations. It would test a currently
|
||||
open gate without changing output transform or popup protocol.
|
||||
94
tooling/probes/step20c-negative-origin/README.md
Normal file
94
tooling/probes/step20c-negative-origin/README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# STEP20C negative-origin nested-grab fixture (EXPERIMENTAL)
|
||||
|
||||
This source-only fixture rebuilds the pinned private compositor and SceneFX
|
||||
through the historical [nested-grab fixture](../step20c-nested-grab/README.md),
|
||||
then applies only the historical parameterized
|
||||
[pointer patch](../step20c-vertical-grab/pointer-extents.patch) to a disposable
|
||||
client copy. The original nested client supplies two real XDG grabs and
|
||||
horizontal placements. No production source, installed library or historical
|
||||
fixture is modified. The [probe report](../../../docs/design/STEP20C_NEGATIVE_ORIGIN_NESTED_GRAB_PROBE.md)
|
||||
contains the coordinate contract and evidence.
|
||||
|
||||
## Pinned inputs
|
||||
|
||||
| Input | Base | SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| SceneFX private patch | SceneFX `37ccd723bef49e6891156ffafce8f549f01446cc` | `c74c515b98da8f5f637e260e6a3755c7a75bd78d1c39e54a67850f1c383e30e7` |
|
||||
| Mosaic private patch | Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754` | `d416f6413ea097a48a44d231e50ffba363914f46c8687f0ea2e4dcacd5f31fe9` |
|
||||
| Hidden-owner guard | After Mosaic private patch | `35785a8f3e863b3c1fd87932d8ff0c2949b03cb04382cc34e1126170968f802b` |
|
||||
| `nested-client.c` | Original nested fixture | `2e40c98af72a0df8b316810589a0be8f4425a763db1de448b49a7813956b6f7d` |
|
||||
| `pointer-daemon.c` | Original valid-grab fixture, then patch below | `a00fcedc38ccb7ca4c25c982d9f38e8bde3168a3833f0c9cf2056e841d720938` |
|
||||
| `pointer-extents.patch` | Original vertical fixture | `4eeb9f808d0941edca82dc3c5d24a9c936cc3d9d8bb80af3db6aed6af0d47c50` |
|
||||
|
||||
The new `prepare.sh` (`ed9d0e8bf50338d39519e003463e637cdba3ecfb147e05a097e88ac6c8def694`),
|
||||
`run.py` (`7b9e2d311dcf942e4dc625b8fcc25c650b7c3ec1f604828397cf1de120b43541`),
|
||||
`analyze.py` (`2d33f4876ea1e982b32f9e73145db86836ba2fa7f5382f9433b4b9f076098a49`)
|
||||
and patched pointer source (`ede9a39adbe1744c771a784f6ad0c296259e36b942c78d75be620b86b9f2a66d`)
|
||||
can be rechecked against the source manifest in each laboratory. The
|
||||
three private patches, nested client and pointer patch remain byte-identical
|
||||
to their historical versions. No new SceneFX, Mosaic or client patch is
|
||||
needed for this geometry; all changes are in the new runner and wrapper.
|
||||
|
||||
## Fresh preparation and replay
|
||||
|
||||
Run from a full Git checkout containing the pinned commits. Dependencies are
|
||||
those listed in the linked historical fixture: Git, Meson, Ninja, C17
|
||||
compiler, `pkg-config`, `wayland-scanner`, `kitty`, `grim`, `wev`, `swaymsg`,
|
||||
Python and the documented development libraries. `prepare.sh` creates a new
|
||||
temporary laboratory, checks and applies private patches there, builds
|
||||
SceneFX/Mosaic privately, generates protocol bindings and compiles the two
|
||||
clients. It prints `LAB` and `BUILD_LOG`; retain both paths.
|
||||
`analyze.py` additionally needs Pillow to decode the retained PNG captures.
|
||||
|
||||
```sh
|
||||
prep=$(sh tooling/probes/step20c-negative-origin/prepare.sh)
|
||||
printf '%s\n' "$prep"
|
||||
LAB=$(printf '%s\n' "$prep" | sed -n 's/^LAB=//p')
|
||||
test -d "$LAB"
|
||||
for mode in control private_standard private_mosaic; do
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" pointer negative
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" visible negative
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" b_cross negative
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" both_cross negative
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" a_cross negative
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" outside negative
|
||||
done
|
||||
for scenario in hidden hidden_click toggle; do
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" private_mosaic "$scenario" negative
|
||||
done
|
||||
for mode in control private_standard private_mosaic; do
|
||||
python tooling/probes/step20c-negative-origin/run.py "$LAB" "$mode" b_cross positive
|
||||
done
|
||||
PYTHONDONTWRITEBYTECODE=1 python tooling/probes/step20c-negative-origin/analyze.py "$LAB"
|
||||
```
|
||||
|
||||
Each invocation creates a single-use `run-GEOMETRY-CASE-SCENARIO` directory
|
||||
under `LAB`. Retain a failed case by renaming its directory before retrying.
|
||||
The runner writes `outputs-verified.json`, `injections.json`,
|
||||
`pointer-validation.json`, `measurements.json` for popup scenarios, IPC tree
|
||||
and output snapshots, `WAYLAND_DEBUG=1` client trace, `wev`, pointer and
|
||||
compositor logs, captures of both outputs, and `exit-status.json`. The
|
||||
coordinate conversion is `sent_x=global_x-min_x` after IPC confirms the
|
||||
negative layout bounds; the absolute extent is `1600×600`. Inspect client
|
||||
events as well as pointer acknowledgements. A control popup crossing is
|
||||
conclusive only if actual pixels and buttons cross the border.
|
||||
|
||||
Each case uses a short, private mode-0700 `/tmp/mno-*` runtime and an explicit
|
||||
discovered `SWAYSOCK` for every `swaymsg -s` call. `WAYLAND_DISPLAY` is private.
|
||||
The runner removes its own runtime only after reaping its process groups and
|
||||
writing exit codes. Private SceneFX is loaded only for the private compositor.
|
||||
The current desktop and ordinary Sway IPC socket are never addressed.
|
||||
|
||||
## Checks and cleanup
|
||||
|
||||
```sh
|
||||
sh -n tooling/probes/step20c-negative-origin/prepare.sh
|
||||
PYTHONDONTWRITEBYTECODE=1 python -c 'import ast; ast.parse(open("tooling/probes/step20c-negative-origin/run.py").read())'
|
||||
(cd "$LAB" && sha256sum -c negative-origin-source-sha256.txt)
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Review measurements and hash evidence before removing the named `LAB` and
|
||||
`BUILD_LOG`. Generated protocol sources, binaries, captures, logs and sockets
|
||||
remain outside Git. Cleanup must target only paths printed by this probe;
|
||||
historical laboratories and the active session are outside its ownership.
|
||||
119
tooling/probes/step20c-negative-origin/analyze.py
Normal file
119
tooling/probes/step20c-negative-origin/analyze.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Summarize retained headless evidence without treating counts as a verdict.
|
||||
|
||||
Usage: python analyze.py LAB
|
||||
Requires Pillow for PNG decoding. Writes analysis.json inside each run directory.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from PIL import Image
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit(__doc__)
|
||||
lab = Path(sys.argv[1]).resolve()
|
||||
colors = {'A_yellow': (224, 192, 32), 'B_green': (32, 208, 160)}
|
||||
|
||||
|
||||
def open_image(path):
|
||||
return Image.open(path).convert('RGB')
|
||||
|
||||
|
||||
def local_points(lines, pattern):
|
||||
points = []
|
||||
for line in lines:
|
||||
match = re.search(pattern, line)
|
||||
if match:
|
||||
points.append(tuple(map(float, match.groups())))
|
||||
return points
|
||||
|
||||
|
||||
for run in sorted(lab.glob('run-*')):
|
||||
if not (run / 'exit-status.json').exists():
|
||||
continue
|
||||
client = (run / 'client.log').read_text()
|
||||
ordinary = (run / 'wev.log').read_text()
|
||||
compositor = (run / 'compositor.log').read_text()
|
||||
result = {
|
||||
'run': run.name,
|
||||
'exit_status': json.loads((run / 'exit-status.json').read_text()),
|
||||
'grab_requests': re.findall(
|
||||
r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\w+) serial=(\d+) used=(\d+)',
|
||||
client),
|
||||
'popup_parents': re.findall(
|
||||
r'(POPUP_[AB]) CREATE wl=(\d+) xdg=(\d+) role=(\d+) parent=(\w+) parent_xdg=(\d+)',
|
||||
client),
|
||||
'popup_done_order': re.findall(r'(POPUP_[AB]) POPUP_DONE', client),
|
||||
'popup_destroy_order': re.findall(r'(POPUP_[AB]) DESTROY', client),
|
||||
'popup_buttons': re.findall(
|
||||
r'POINTER_BUTTON target=(POPUP_[AB]) serial=(\d+) button=(\d+) state=(\d+)',
|
||||
client),
|
||||
'ordinary_buttons': re.findall(
|
||||
r'button: serial: (\d+); time: \d+; button: (\d+).*state: ([01])',
|
||||
ordinary),
|
||||
'scene_clip_logs': re.findall(
|
||||
r'PROBE_POPUP_CLIP owner=(\S+) local=(-?\d+),(-?\d+) (\d+)x(\d+)',
|
||||
compositor),
|
||||
'hidden_owner_logs': re.findall(r'PROBE_POPUP_HIDDEN_OWNER[^\n]*', compositor),
|
||||
'captures': {},
|
||||
'open_sample_rgb': {},
|
||||
}
|
||||
reference_path = run / 'pointer-validation.json'
|
||||
if reference_path.exists():
|
||||
reference = json.loads(reference_path.read_text())
|
||||
owner_points = local_points(reference['owner_motion_lines'],
|
||||
r'x=(-?[\d.]+) y=(-?[\d.]+)')
|
||||
ordinary_points = local_points(reference['ordinary_motion_lines'],
|
||||
r'x, y: (-?[\d.]+), (-?[\d.]+)')
|
||||
if len(owner_points) >= 2 and len(ordinary_points) >= 2:
|
||||
result['pointer_observed'] = [
|
||||
{'requested_global': list(reference['global_points'][i]),
|
||||
'observed_output': output, 'observed_surface_local': list(point)}
|
||||
for i, output, point in zip(range(4),
|
||||
('HEADLESS-1', 'HEADLESS-2', 'HEADLESS-1', 'HEADLESS-2'),
|
||||
(owner_points[-2], ordinary_points[-2],
|
||||
owner_points[-1], ordinary_points[-1]))]
|
||||
for path in sorted(run.glob('*.png')):
|
||||
if not path.name.startswith(('left-', 'right-')):
|
||||
continue
|
||||
image = open_image(path)
|
||||
pixels = image.get_flattened_data()
|
||||
result['captures'][path.name] = {
|
||||
'size': list(image.size),
|
||||
'exact_color_pixels': {name: pixels.count(rgb) for name, rgb in colors.items()},
|
||||
}
|
||||
measurements_path = run / 'measurements.json'
|
||||
if measurements_path.exists():
|
||||
result['measurements'] = json.loads(measurements_path.read_text())
|
||||
# CONTRACT: the private log's clip is local to the root popup tree.
|
||||
# Subtracting it from the output viewport origin recovers that applied
|
||||
# scene-tree origin; XDG role configure offsets locate A and B below it.
|
||||
if result['scene_clip_logs']:
|
||||
clip = result['scene_clip_logs'][-1]
|
||||
left = result['measurements']['verified_outputs']['HEADLESS-1']
|
||||
root_x, root_y = left[0] - int(clip[1]), left[1] - int(clip[2])
|
||||
a = result['measurements']['popup_a_configure']
|
||||
b = result['measurements']['popup_b_configure']
|
||||
result['applied_scene_from_clip'] = {
|
||||
'root_popup_tree': [root_x, root_y],
|
||||
'popup_a': [root_x + a[0], root_y + a[1], a[2], a[3]],
|
||||
'popup_b': [root_x + a[0] + b[0], root_y + a[1] + b[1], b[2], b[3]],
|
||||
}
|
||||
for sample in result['measurements']['sample_points']:
|
||||
x, y = sample['global']
|
||||
output = 'left' if x < result['measurements']['verified_outputs']['HEADLESS-2'][0] else 'right'
|
||||
origin = result['measurements']['verified_outputs'][
|
||||
'HEADLESS-1' if output == 'left' else 'HEADLESS-2']
|
||||
path = run / f'{output}-open.png'
|
||||
if path.exists():
|
||||
local = (int(x - origin[0]), int(y - origin[1]))
|
||||
image = open_image(path)
|
||||
result['open_sample_rgb'][sample['purpose']] = {
|
||||
'global': [x, y], 'output': output, 'local': list(local),
|
||||
'rgb': list(image.getpixel(local)) if (
|
||||
0 <= local[0] < image.width and 0 <= local[1] < image.height) else None,
|
||||
}
|
||||
(run / 'analysis.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(run.name, result['captures'].get('left-open.png', {}).get('exact_color_pixels'),
|
||||
result['captures'].get('right-open.png', {}).get('exact_color_pixels'))
|
||||
28
tooling/probes/step20c-negative-origin/prepare.sh
Normal file
28
tooling/probes/step20c-negative-origin/prepare.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
# EXPERIMENTAL: rebuild the historical private compositor in a fresh lab.
|
||||
set -eu
|
||||
repo=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
fixture="$repo/tooling/probes/step20c-negative-origin"
|
||||
prior="$repo/tooling/probes/step20c-nested-grab"
|
||||
pointer_patch="$repo/tooling/probes/step20c-vertical-grab/pointer-extents.patch"
|
||||
build_log=$(mktemp /tmp/mno-prepare-XXXXXXXX)
|
||||
if ! sh "$prior/prepare.sh" > "$build_log" 2>&1; then
|
||||
printf 'PRIVATE_BUILD_FAILED log=%s\n' "$build_log" >&2
|
||||
tail -40 "$build_log" >&2
|
||||
exit 1
|
||||
fi
|
||||
lab=$(sed -n 's/^LAB=//p' "$build_log")
|
||||
test -n "$lab" && test -d "$lab"
|
||||
trap 'rm -rf -- "$lab"' 0
|
||||
# CONTRACT: adapt only the copied client; original fixtures and private
|
||||
# compositor patches retain their historical hashes.
|
||||
cp "$repo/tooling/probes/step20c-valid-grab/pointer-daemon.c" "$lab/pointer-daemon.c"
|
||||
(cd "$lab" && git apply --check "$pointer_patch" && git apply "$pointer_patch")
|
||||
cc -std=c17 -Wall -Wextra -Wno-unused-parameter -I"$lab" \
|
||||
"$lab/pointer-daemon.c" "$lab/virtual-pointer-protocol.c" -o "$lab/pointer-daemon" \
|
||||
$(pkg-config --cflags --libs wayland-client)
|
||||
sha256sum "$prior/nested-client.c" "$pointer_patch" "$fixture/prepare.sh" \
|
||||
"$fixture/run.py" "$fixture/analyze.py" "$lab/pointer-daemon.c" \
|
||||
> "$lab/negative-origin-source-sha256.txt"
|
||||
trap - 0
|
||||
printf 'LAB=%s\nBUILD_LOG=%s\n' "$lab" "$build_log"
|
||||
400
tooling/probes/step20c-negative-origin/run.py
Normal file
400
tooling/probes/step20c-negative-origin/run.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
"""EXPERIMENTAL negative/positive-origin nested XDG grab runner.
|
||||
|
||||
Usage: python run.py LAB {control,private_standard,private_mosaic} SCENARIO GEOMETRY
|
||||
SCENARIO: pointer, visible, b_cross, both_cross, a_cross, outside, hidden,
|
||||
hidden_click, toggle
|
||||
GEOMETRY: negative, positive
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
if len(sys.argv) != 5:
|
||||
raise SystemExit(__doc__)
|
||||
LAB = pathlib.Path(sys.argv[1]).resolve()
|
||||
case, scenario, geometry = sys.argv[2:5]
|
||||
assert case in ('control', 'private_standard', 'private_mosaic')
|
||||
assert scenario in ('pointer', 'visible', 'b_cross', 'both_cross', 'a_cross', 'outside',
|
||||
'hidden', 'hidden_click', 'toggle')
|
||||
assert geometry in ('negative', 'positive')
|
||||
if scenario in ('hidden', 'hidden_click', 'toggle') and case != 'private_mosaic':
|
||||
raise SystemExit('this scenario requires private_mosaic')
|
||||
assert LAB.is_dir() and (LAB / 'nested-client').is_file()
|
||||
placement = ('visible' if scenario in ('pointer', 'visible', 'outside') else
|
||||
'both_cross' if scenario in ('both_cross', 'a_cross') else 'b_cross')
|
||||
run = LAB / f'run-{geometry}-{case}-{scenario}'
|
||||
run.mkdir(mode=0o700, exist_ok=False)
|
||||
runtime = pathlib.Path(tempfile.mkdtemp(prefix='mno-', dir='/tmp'))
|
||||
# WHY: keep both Unix socket names below sockaddr_un.sun_path even if LAB is
|
||||
# long; the compositor derives its IPC path from XDG_RUNTIME_DIR.
|
||||
if len(os.fsencode(str(runtime / 'sway-ipc.4294967295.2147483647.sock'))) >= 108:
|
||||
raise RuntimeError('private IPC socket path exceeds Linux sockaddr_un.sun_path')
|
||||
(run / 'runtime-path').write_text(str(runtime) + '\n')
|
||||
origin_x = -800 if geometry == 'negative' else 0
|
||||
output2_x = origin_x + 800
|
||||
output2 = f'output HEADLESS-2 resolution 800x600 position {output2_x} 0\n'
|
||||
(run / 'config').write_text(
|
||||
f'output HEADLESS-1 resolution 800x600 position {origin_x} 0\n' + output2 +
|
||||
'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n'
|
||||
'focus_follows_mouse no\n')
|
||||
env = os.environ.copy()
|
||||
env.update(XDG_RUNTIME_DIR=str(runtime), WLR_BACKENDS='headless',
|
||||
WLR_HEADLESS_OUTPUTS='2', WLR_RENDERER='pixman')
|
||||
env.pop('WAYLAND_DISPLAY', None)
|
||||
env.pop('SWAYSOCK', None)
|
||||
env.pop('I3SOCK', None)
|
||||
env.pop('LD_LIBRARY_PATH', None)
|
||||
compositor_env = env.copy()
|
||||
if case == 'control':
|
||||
binary = pathlib.Path('/usr/bin/sway')
|
||||
else:
|
||||
binary = LAB / 'mosaic-build/sway/swayfx-mosaic'
|
||||
compositor_env['LD_LIBRARY_PATH'] = str(LAB / 'scenefx-build')
|
||||
files = []
|
||||
children = []
|
||||
|
||||
|
||||
def spawn(argv, name, *, debug=False, stdin=None):
|
||||
log = (run / name).open('w')
|
||||
files.append(log)
|
||||
child_env = env.copy()
|
||||
if debug:
|
||||
child_env['WAYLAND_DEBUG'] = '1'
|
||||
proc = subprocess.Popen(argv, env=child_env, stdin=stdin, stdout=log,
|
||||
stderr=subprocess.STDOUT, text=True,
|
||||
start_new_session=True)
|
||||
children.append(proc)
|
||||
return proc
|
||||
|
||||
|
||||
def wait(fn, seconds=12):
|
||||
end = time.monotonic() + seconds
|
||||
while time.monotonic() < end:
|
||||
result = fn()
|
||||
if result:
|
||||
return result
|
||||
if compositor.poll() is not None:
|
||||
raise RuntimeError(f'compositor exited {compositor.returncode}')
|
||||
if 'pointer' in globals() and pointer.poll() is not None:
|
||||
raise RuntimeError(f'virtual pointer exited {pointer.returncode}')
|
||||
time.sleep(.05)
|
||||
raise RuntimeError('timeout waiting for probe event')
|
||||
|
||||
|
||||
def ipc(*args):
|
||||
return json.loads(subprocess.run(['swaymsg', '-s', str(sock), *args],
|
||||
env=env, text=True, capture_output=True, check=True,
|
||||
timeout=5).stdout)
|
||||
|
||||
|
||||
def find(node, predicate):
|
||||
if isinstance(node, dict):
|
||||
if predicate(node):
|
||||
return node
|
||||
for child in node.get('nodes', []) + node.get('floating_nodes', []):
|
||||
result = find(child, predicate)
|
||||
if result:
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
def tree():
|
||||
return ipc('-t', 'get_tree')
|
||||
|
||||
|
||||
def ws():
|
||||
return find(tree(), lambda n: n.get('type') == 'workspace' and n.get('name') == '1')
|
||||
|
||||
|
||||
def owner():
|
||||
return find(tree(), lambda n: n.get('app_id') == 'org.mosaic.NestedGrabProbe')
|
||||
|
||||
|
||||
def logged(s):
|
||||
path = run / 'client.log'
|
||||
return path.exists() and s in path.read_text()
|
||||
|
||||
|
||||
def configured(name):
|
||||
"""The server's last role configure, not the positioner request."""
|
||||
matches = re.findall(rf'{name} ROLE_CONFIGURE x=(-?\d+) y=(-?\d+) w=(\d+) h=(\d+)',
|
||||
(run / 'client.log').read_text())
|
||||
if not matches:
|
||||
raise RuntimeError(f'missing {name} ROLE_CONFIGURE')
|
||||
return tuple(map(int, matches[-1]))
|
||||
|
||||
|
||||
def verify_outputs():
|
||||
expected = {'HEADLESS-1': (origin_x, 0, 800, 600),
|
||||
'HEADLESS-2': (output2_x, 0, 800, 600)}
|
||||
actual = {}
|
||||
data = ipc('-t', 'get_outputs')
|
||||
(run / 'outputs-verified.json').write_text(json.dumps(data, indent=2))
|
||||
for output in data:
|
||||
if output.get('name') in expected and output.get('active'):
|
||||
rect = output['rect']
|
||||
if output.get('scale') != 1.0 or output.get('transform') != 'normal':
|
||||
raise RuntimeError(f'unexpected output scale/transform: {output}')
|
||||
actual[output['name']] = (rect['x'], rect['y'], rect['width'], rect['height'])
|
||||
if actual != expected:
|
||||
raise RuntimeError(f'output geometry mismatch: {actual} != {expected}')
|
||||
return expected
|
||||
|
||||
|
||||
def capture(tag):
|
||||
(run / f'tree-{tag}.json').write_text(json.dumps(tree(), indent=2))
|
||||
(run / f'outputs-{tag}.json').write_text(json.dumps(ipc('-t', 'get_outputs'), indent=2))
|
||||
for output, label in (('HEADLESS-1', 'left'), ('HEADLESS-2', 'right')):
|
||||
subprocess.run(['grim', '-o', output, str(run / f'{label}-{tag}.png')],
|
||||
env=env, check=True, timeout=8, stdout=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def inject(action, x, y):
|
||||
"""Convert a signed global point only after verifying layout bounds."""
|
||||
if not isinstance(x, int) or not isinstance(y, int):
|
||||
raise ValueError(f'nonintegral global pointer request: {x},{y}')
|
||||
if not (origin_x <= x < origin_x + extent[0] and 0 <= y < extent[1]):
|
||||
raise ValueError(f'global point outside verified layout: {x},{y}')
|
||||
normalized_x = x - origin_x
|
||||
output = 'HEADLESS-1' if x < output2_x else 'HEADLESS-2'
|
||||
local_x = x - (origin_x if output == 'HEADLESS-1' else output2_x)
|
||||
entry = {'action': action, 'global': [x, y],
|
||||
'layout_min': [origin_x, 0], 'sent': [normalized_x, y],
|
||||
'extent': list(extent), 'expected_output': output,
|
||||
'expected_output_local': [local_x, y]}
|
||||
injection_log.append(entry)
|
||||
(run / 'injections.json').write_text(json.dumps(injection_log, indent=2))
|
||||
marker = f'INJECT_DONE {action} {normalized_x},{y}'
|
||||
pointer.stdin.write(f'{action} {normalized_x} {y}\n')
|
||||
pointer.stdin.flush()
|
||||
wait(lambda: marker in (run / 'pointer.log').read_text())
|
||||
|
||||
|
||||
def command(value):
|
||||
client.stdin.write(value + '\n')
|
||||
client.stdin.flush()
|
||||
wait(lambda: f'COMMAND {value}' in (run / 'client.log').read_text())
|
||||
|
||||
|
||||
compositor_log = (run / 'compositor.log').open('w')
|
||||
files.append(compositor_log)
|
||||
compositor = None
|
||||
try:
|
||||
compositor = subprocess.Popen([str(binary), '-c', str(run / 'config'), '-d'],
|
||||
env=compositor_env, stdout=compositor_log,
|
||||
stderr=subprocess.STDOUT, start_new_session=True)
|
||||
def sockets():
|
||||
s = next(iter(runtime.glob('sway-ipc.*.sock')), None)
|
||||
w = next((p for p in runtime.glob('wayland-*') if p.suffix != '.lock'), None)
|
||||
return (s, w) if s and w else None
|
||||
|
||||
sock, way = wait(sockets)
|
||||
env.update(SWAYSOCK=str(sock), WAYLAND_DISPLAY=way.name)
|
||||
verified_outputs = verify_outputs()
|
||||
injection_log = []
|
||||
extent = (1600, 600)
|
||||
pointer = spawn([str(LAB / 'pointer-daemon'), *map(str, extent)], 'pointer.log',
|
||||
stdin=subprocess.PIPE)
|
||||
wait(lambda: 'POINTER_READY' in (run / 'pointer.log').read_text())
|
||||
ipc('workspace 1')
|
||||
ipc('layout mosaic' if case == 'private_mosaic' else 'layout splith')
|
||||
spawn(['kitty', '--class', 'A', '-o', 'background=#aa2222',
|
||||
'/usr/bin/sleep', '90'], 'A.log')
|
||||
wait(lambda: len(ws()['nodes']) >= 1)
|
||||
client = spawn([str(LAB / 'nested-client'), placement], 'client.log',
|
||||
debug=True, stdin=subprocess.PIPE)
|
||||
wait(lambda: owner() and logged('TOPLEVEL MAP'))
|
||||
for label in ('C', 'D'):
|
||||
spawn(['kitty', '--class', label, '/usr/bin/sleep', '90'], label + '.log')
|
||||
time.sleep(.35)
|
||||
ipc('workspace 2')
|
||||
spawn(['stdbuf', '-oL', 'wev'], 'wev.log')
|
||||
ordinary = wait(lambda: find(tree(), lambda n: n.get('app_id') == 'wev'))
|
||||
ordinary_rect = ordinary['rect']
|
||||
ipc('workspace 1')
|
||||
ipc('[app_id="org.mosaic.NestedGrabProbe"] focus')
|
||||
if case == 'private_mosaic':
|
||||
current = ws()['mosaic']['rows'][0]['scroll_x']
|
||||
ipc(f'layout mosaic scroll {400-current:g}')
|
||||
time.sleep(.4)
|
||||
o = owner()
|
||||
scroll = ws()['mosaic']['rows'][0]['scroll_x'] if case == 'private_mosaic' else 0
|
||||
# CONTRACT: logical IPC X and camera displacement are distinct. A scene
|
||||
# log and sampled pixels must corroborate this applied-content estimate.
|
||||
rendered_x = o['rect']['x'] - scroll + o['window_rect']['x']
|
||||
rendered_y = o['rect']['y'] + o['window_rect']['y']
|
||||
top_x = int(max(origin_x + 40, min(output2_x - 40, rendered_x + 100)))
|
||||
right_x = int(max(output2_x + 30, ordinary_rect['x'] + 30))
|
||||
right_y = int(max(80, ordinary_rect['y'] + 30))
|
||||
calibration = [(top_x, 150), (right_x, right_y),
|
||||
(top_x + 5, 160), (right_x + 5, right_y + 5)]
|
||||
for x, y in calibration:
|
||||
inject('move', x, y)
|
||||
time.sleep(.13)
|
||||
capture('pointer-reference')
|
||||
calibration_evidence = {'global_points': calibration,
|
||||
'owner_motion_lines': [line for line in (run / 'client.log').read_text().splitlines()
|
||||
if ('POINTER_MOTION target=TOPLEVEL' in line or
|
||||
'POINTER_ENTER target=TOPLEVEL' in line)],
|
||||
'ordinary_motion_lines': [line for line in (run / 'wev.log').read_text().splitlines()
|
||||
if 'motion' in line.lower() or 'enter' in line.lower()]}
|
||||
(run / 'pointer-validation.json').write_text(json.dumps(calibration_evidence, indent=2))
|
||||
if len(calibration_evidence['owner_motion_lines']) < 2 or not calibration_evidence['ordinary_motion_lines']:
|
||||
raise RuntimeError('pointer calibration did not show motion on both clients')
|
||||
if scenario == 'pointer':
|
||||
for action, x, y in [('right', top_x, 150), ('left', right_x, right_y),
|
||||
('right', right_x + 5, right_y + 5),
|
||||
('left', top_x + 5, 160)]:
|
||||
inject(action, x, y)
|
||||
time.sleep(.2)
|
||||
capture('pointer-buttons')
|
||||
print('POINTER_REFERENCE', calibration_evidence, flush=True)
|
||||
else:
|
||||
inject('left', top_x, 150)
|
||||
wait(lambda: logged('POPUP_A MAP'))
|
||||
a_cfg = configured('POPUP_A')
|
||||
a_x = int(max(origin_x + 40, min(output2_x - 40, rendered_x + a_cfg[0] + 40)))
|
||||
a_y = int(min(585, rendered_y + a_cfg[1] + 35))
|
||||
inject('left', a_x, a_y)
|
||||
wait(lambda: logged('POPUP_B MAP'))
|
||||
b_cfg = configured('POPUP_B')
|
||||
time.sleep(.3)
|
||||
capture('open')
|
||||
a_global = (rendered_x + a_cfg[0], rendered_y + a_cfg[1], a_cfg[2], a_cfg[3])
|
||||
b_global = (a_global[0] + b_cfg[0], a_global[1] + b_cfg[1], b_cfg[2], b_cfg[3])
|
||||
a_cross = a_global[0] < output2_x < a_global[0] + a_global[2]
|
||||
b_cross = b_global[0] < output2_x < b_global[0] + b_global[2]
|
||||
measurements = {'geometry': geometry, 'case': case, 'scenario': scenario,
|
||||
'verified_outputs': verified_outputs, 'layout_origin': [origin_x, 0],
|
||||
'layout_extent': extent, 'owner_id': o['id'], 'owner_rect': o['rect'],
|
||||
'owner_window_rect': o['window_rect'], 'rendered_content_estimate': [rendered_x, rendered_y],
|
||||
'scroll_x': scroll, 'ordinary_rect': ordinary_rect,
|
||||
'popup_a_configure': a_cfg, 'popup_b_configure': b_cfg,
|
||||
'popup_a_global_estimate': a_global, 'popup_b_global_estimate': b_global,
|
||||
'popup_a_crossing_estimate': a_cross, 'popup_b_crossing_estimate': b_cross,
|
||||
'sample_points': []}
|
||||
def save():
|
||||
(run / 'measurements.json').write_text(json.dumps(measurements, indent=2))
|
||||
save()
|
||||
print('CHAIN_OPEN', case, scenario, placement, 'owner_id', o['id'],
|
||||
'owner_rect', o['rect'], 'scroll', scroll, 'a_configure', a_cfg,
|
||||
'b_configure', b_cfg, 'a_global', a_global, 'b_global', b_global,
|
||||
'a_cross', a_cross, 'b_cross', b_cross, flush=True)
|
||||
if scenario == 'visible':
|
||||
for purpose, rect in [('visible_a', a_global), ('visible_b', b_global)]:
|
||||
x = int(max(origin_x + 20, min(output2_x - 20, rect[0] + 20)))
|
||||
y = int(rect[1] + 20)
|
||||
measurements['sample_points'].append({'purpose': purpose, 'global': [x, y]})
|
||||
inject('right', x, y)
|
||||
capture('visible-clicks')
|
||||
elif scenario in ('b_cross', 'both_cross', 'a_cross'):
|
||||
if not b_cross or (scenario in ('both_cross', 'a_cross') and not a_cross):
|
||||
print('INCONCLUSIVE configured popup crossing absent', flush=True)
|
||||
else:
|
||||
left_b = (int(min(output2_x - 20, b_global[0] + 40)), int(b_global[1] + 30))
|
||||
right_b = (int(max(output2_x + 20, ordinary_rect['x'] + 15,
|
||||
b_global[0] + 20)), int(max(b_global[1] + 30,
|
||||
ordinary_rect['y'] + 15)))
|
||||
if not (b_global[0] <= left_b[0] < output2_x and
|
||||
b_global[0] <= right_b[0] < b_global[0] + b_global[2] and
|
||||
b_global[1] <= right_b[1] < b_global[1] + b_global[3] and
|
||||
ordinary_rect['x'] <= right_b[0] < ordinary_rect['x'] + ordinary_rect['width']):
|
||||
print('INCONCLUSIVE no verified popup/wev overlap', left_b, right_b, flush=True)
|
||||
else:
|
||||
actions = []
|
||||
if scenario in ('both_cross', 'a_cross'):
|
||||
left_a = (int(a_global[0] + 20), int(a_global[1] + 20))
|
||||
right_a = (int(max(output2_x + 10, a_global[0] + a_global[2] - 15)),
|
||||
int(a_global[1] + 20))
|
||||
actions += [('right', *left_a, 'visible_a')]
|
||||
actions += [('right', *left_b, 'visible_b')]
|
||||
if scenario == 'a_cross':
|
||||
actions += [('right', *right_a, 'crossing_a')]
|
||||
else:
|
||||
actions += [('right', *right_b, 'crossing_b')]
|
||||
for action, x, y, purpose in actions:
|
||||
measurements['sample_points'].append(
|
||||
{'purpose': purpose, 'global': [x, y], 'action': action})
|
||||
save()
|
||||
inject(action, x, y)
|
||||
time.sleep(.2)
|
||||
capture(f'{purpose}-{action}')
|
||||
elif scenario == 'outside':
|
||||
outside = (int(max(output2_x + 30, ordinary_rect['x'] + 20)),
|
||||
int(min(500, max(300, ordinary_rect['y'] + 200))))
|
||||
measurements['sample_points'].append({'purpose': 'outside_click', 'global': outside})
|
||||
save()
|
||||
inject('left', *outside)
|
||||
time.sleep(.2)
|
||||
capture('outside')
|
||||
elif scenario in ('hidden', 'hidden_click'):
|
||||
ipc('layout mosaic scroll 880')
|
||||
time.sleep(.5)
|
||||
capture('hidden')
|
||||
hidden_owner = owner()
|
||||
hidden_scroll = ws()['mosaic']['rows'][0]['scroll_x']
|
||||
hidden_x = hidden_owner['rect']['x'] - hidden_scroll + hidden_owner['window_rect']['x']
|
||||
measurements['hidden_owner_applied_estimate'] = [hidden_x,
|
||||
hidden_owner['rect']['y'] + hidden_owner['window_rect']['y'],
|
||||
hidden_owner['window_rect']['width'], hidden_owner['window_rect']['height']]
|
||||
measurements['hidden_scroll_x'] = hidden_scroll
|
||||
save()
|
||||
inject('move', origin_x + 100, 190)
|
||||
time.sleep(.2)
|
||||
capture('hidden-hover')
|
||||
if scenario == 'hidden_click':
|
||||
inject('right', origin_x + 100, 190)
|
||||
time.sleep(.2)
|
||||
capture('hidden-click')
|
||||
ipc('layout mosaic scroll -880')
|
||||
time.sleep(.4)
|
||||
capture('returned')
|
||||
elif scenario == 'toggle':
|
||||
ipc('layout splith')
|
||||
time.sleep(.4)
|
||||
capture('standard')
|
||||
standard_owner = owner()
|
||||
standard_top_x = standard_owner['rect']['x'] + standard_owner['window_rect']['x']
|
||||
standard_top_y = standard_owner['rect']['y'] + standard_owner['window_rect']['y']
|
||||
toggle_x = int(max(output2_x + 20,
|
||||
standard_top_x + a_cfg[0] + b_cfg[0] + 20))
|
||||
toggle_y = int(standard_top_y + a_cfg[1] + b_cfg[1] + 30)
|
||||
measurements['standard_owner_rect'] = standard_owner['rect']
|
||||
measurements['sample_points'].append(
|
||||
{'purpose': 'standard_popup_b', 'global': [toggle_x, toggle_y]})
|
||||
save()
|
||||
inject('right', toggle_x, toggle_y)
|
||||
time.sleep(.2)
|
||||
capture('standard-click')
|
||||
print('FINAL', 'a_done', logged('POPUP_A POPUP_DONE'),
|
||||
'b_done', logged('POPUP_B POPUP_DONE'),
|
||||
'client_alive', client.poll() is None, flush=True)
|
||||
|
||||
finally:
|
||||
for child in children:
|
||||
if child.poll() is None:
|
||||
os.killpg(child.pid, signal.SIGTERM)
|
||||
if compositor is not None and compositor.poll() is None:
|
||||
os.killpg(compositor.pid, signal.SIGTERM)
|
||||
# CONTRACT: only process groups started by this runner are terminated.
|
||||
# Reap them before removing their private socket directory.
|
||||
for proc in [*children, *([compositor] if compositor is not None else [])]:
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait(timeout=5)
|
||||
(run / 'exit-status.json').write_text(json.dumps({
|
||||
'compositor': None if compositor is None else compositor.returncode,
|
||||
'children': [{'argv': proc.args, 'returncode': proc.returncode}
|
||||
for proc in children]}, indent=2))
|
||||
for file in files:
|
||||
file.close()
|
||||
shutil.rmtree(runtime)
|
||||
Loading…
Reference in a new issue