Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ASF_SOURCE_HEADERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ Subtracting in a checkout is not the enumeration excusing itself. It decides whi

| Comment syntax | Extensions |
| --- | --- |
| `/* … */` | `.cjs`, `.css`, `.js`, `.mjs`, `.mts`, `.rs`, `.swift`, `.ts`, `.tsx` |
| `/* … */` | `.cjs`, `.cs`, `.css`, `.js`, `.mjs`, `.mts`, `.rs`, `.swift`, `.ts`, `.tsx` |
| `//` | `.jsonc` |
| `#` | `.ps1`, `.py`, `.sh`, `.toml`, `.yaml`, `.yml`, `Dockerfile`, `network-policy` |
| `<!-- … -->` | `.html`, `.md` |
| `<!-- … -->` | `.csproj`, `.html`, `.md` |

## Reviewed exclusions

Expand Down
5 changes: 5 additions & 0 deletions experiments/maka-cu-windows/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/bin/
**/obj/
out/
*.log
captures/
128 changes: 128 additions & 0 deletions experiments/maka-cu-windows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# maka-cu-windows feasibility spike

This private experiment supports apache/maka#4318. It is a supervised,
fixture-only feasibility spike for a long-lived C# helper on Windows. It does
not enable a Windows product backend, change the public protocol, or perform
keyboard, pointer, coordinate, `PostMessage`, `SendInput`, foreground, or
screen-rectangle fallback actions.

The local machine has only .NET SDK 8.0.421, so the spike targets
`net8.0-windows10.0.22621.0`. .NET 8 is temporary evidence only; a production
follow-up must evaluate .NET 10 LTS and rebuild self-contained artifacts for
runtime patching. No SDK was installed globally for this spike.

## Components

- `src/Program.cs`: line-delimited JSON-RPC 2.0, an MTA UIA lane, bounded
request/snapshot registries, strict process-start/window-generation identity,
opaque snapshot tokens, typed readback outcomes, and cancellation settlement.
- `src/WgcCapture.cs`: target-window `CreateForWindow(HWND)` capture and a
D3D11 staging-texture PNG encoder. Capture has no rectangle fallback and
reports `capture_unavailable` on failure.
- `fixture/HangWindowFixture`: purpose-built WinForms fixture. `freeze` blocks
its UI thread so UIA provider calls can hang; `recreate` replaces its HWND;
`cover` tests target capture under occlusion.
- `driver.mjs`: safe smoke flow. It accepts only the exact fixture HWND and
never scans or mutates user windows.
- `lifecycle-driver.mjs`: C4–C6, identity, and parent-death reproduction
scenarios. It owns and tears down all fixture/helper processes.
- `parent-probe.mjs`: short lived host used to prove helper parent-death
cleanup after an initialized, blocked observe.
- `protocol-regression.mjs`: malformed-method, unknown-cancel, and EOF plus
stdout-backpressure regressions without a GUI.
- `publish.ps1`: reproducible self-contained single-file publish plus manifest
and SHA-256 hash.

## Build and fixture run

```powershell
dotnet build experiments/maka-cu-windows/src/MakaCuWindows.csproj -c Release
dotnet build experiments/maka-cu-windows/fixture/HangWindowFixture/HangWindowFixture.csproj -c Release
node experiments/maka-cu-windows/lifecycle-driver.mjs `
experiments/maka-cu-windows/src/bin/Release/net8.0-windows10.0.22621.0/maka-cu-windows.exe `
experiments/maka-cu-windows/fixture/HangWindowFixture/bin/Release/net8.0-windows10.0.22621.0/maka-cu-windows-fixture.exe
node experiments/maka-cu-windows/protocol-regression.mjs `
experiments/maka-cu-windows/out/publish/maka-cu-windows.exe
```

The lifecycle run starts only the named fixture and helper. The fixture window
is visible because UIA and WGC require an interactive desktop. Do not point
either driver at a user application.

## Published artifact

```powershell
powershell -ExecutionPolicy Bypass -File experiments/maka-cu-windows/publish.ps1
node experiments/maka-cu-windows/lifecycle-driver.mjs `
experiments/maka-cu-windows/out/publish/maka-cu-windows.exe `
experiments/maka-cu-windows/out/fixture/maka-cu-windows-fixture.exe
```

The intended layout is self-contained `win-x64`, single-file, trimming
disabled, unsigned, and `distributionReady: false`. The manifest records the
actual SDK, target framework, publish settings, sizes, and hashes. The
published run is the relevant packaging evidence; `dotnet run` is not.

## Current local evidence (Windows 11 Insider 10.0.26220, x64)

| Check | Result | Evidence or limit |
| --- | --- | --- |
| 1. handshake | pass | Protocol `maka.cu.windows/0`; 10 s handshake / 20 s request / 2 s cancel grace declared. |
| 2. MTA UIA observation | pass on fixture | Dedicated MTA lane, bounded shallow tree, exact PID/HWND/start time/generation. |
| 3. semantic action | pass on fixture | `ValuePattern.SetValue`, pre-dispatch snapshot spend, strict revalidation, readback; duplicate tokens refuse. |
| 4. WGC target capture | pass on fixture | `CreateForWindow(HWND)` plus D3D11 staging readback produced real PNG bytes (464x352, 9,366 bytes); decoded LimeGreen sentinel remained present under an occluding fixture cover and after uncover. No rectangle fallback. |
| 5. cancellation settlement | pass on fixture | In-flight post-dispatch cancellation settles the original request with `verified`; queued cancellation settles `refused/cancelled_before_dispatch` and readback proves no mutation. |
| 6. hung-provider recovery | pass on fixture/helper | Frozen fixture remains alive; supervisor kills helper after 2 s, confirms exit, restarts with a new generation, and old snapshots are unknown. |
| identity cases | partial | Whole-window recreation returns a new HWND and rejects the old snapshot; same-window control replacement was not exercised. New explicit selection is required. |
| parent death | pass on fixture/helper | Dedicated parent probe completed initialize and a blocked observe, exited host code 77, and the helper disappeared without the lifecycle driver killing that helper PID. EOF and fail-closed stdout backpressure also exited within the deadline. A production integration should still add independent OS-level supervision. |

These are development-machine results, not clean-machine or supported-release
certification. A clean machine with no .NET runtime/SDK and an interactive
desktop has not been run here, so the spike is not a production go decision.

## D1–D6 decision record

- **D1 UIA binding:** retain managed `System.Windows.Automation` for the spike;
the dedicated MTA lane and targeted `FindFirst` avoid unbounded Chromium
subtree enumeration. A blocked provider still needs helper restart.
- **D2 frame transport:** use bounded base64 PNG in the private RPC envelope
for the spike. The complete UTF-8 response is capped at 6 MiB; capture
dimensions/pixels and PNG bytes are bounded before/after allocations.
- **D3 publishing/runtime:** evaluate the checked-in .NET 8 single-file
settings with `publish.ps1`; move the baseline to .NET 10 LTS before
production. Native extraction and clean-machine behavior remain unverified.
- **D4 supervision:** keep cancellation control out of the UIA lane; settle
known outcomes, spend queued mutation snapshots, and force-restart a helper
after the 2 s grace. The spike's bounded writer fails closed under blocked
stdout; a Windows Job Object or independent parent watchdog remains a
production hardening item.
- **D5 code home:** keep the prototype under `experiments/maka-cu-windows/`
until all six checks and clean-machine evidence pass.
- **D6 Go boundary:** no Go forwarding helper is justified by current local
evidence; revisit only with measured packaging or integration constraints.

Recommendation: **hold production enablement; the feasibility gate is not
complete**. The local fixture and published artifact checks pass, but this is
an Insider development machine. Same-window control replacement, clean-machine
evidence, and packaging measurements remain open under #4318. A supported-release
run and stronger OS-level parent ownership should be addressed before rollout.
Signing is deferred production distribution work, not an additional spike gate.
99 changes: 99 additions & 0 deletions experiments/maka-cu-windows/RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Local spike results

Run date: 2026-08-31 (Asia/Shanghai). Environment: Windows 11 Pro Insider
Preview x64 build 10.0.26220, interactive session 3; Node.js 24.19; .NET
SDK 8.0.421; target `net8.0-windows10.0.22621.0`; Windows SDK reference
10.0.22621.56. This is development-machine evidence, not clean-machine or
supported-release certification.

## Build and package

Both projects built with `dotnet build ... -c Release --no-restore`: 0
warnings, 0 errors. `publish.ps1` then produced self-contained `win-x64`,
single-file, untrimmed artifacts (`distributionReady: false`). The manifest
records helper 188,261,839 bytes, SHA-256
`25A38D720641428BA1AB6E512E5E8A9773D9D79435D1BE99FDF59A095546D1A1`, and
fixture 188,220,927 bytes, SHA-256
`AEEF7ADC6C16205A0137B63DDA524E29ADAE135CEBD660AA870C11E2CC7BE71D`.

The publish used the installed .NET 8.0.421 SDK only. A clean-machine restore
and runtime test were not performed.

## Published lifecycle evidence

Command:

```powershell
node experiments/maka-cu-windows/lifecycle-driver.mjs `
experiments/maka-cu-windows/out/publish/maka-cu-windows.exe `
experiments/maka-cu-windows/out/fixture/maka-cu-windows-fixture.exe
```

Result: `failures=0`. The fixture reported outer `SIZE 480x360` and DWM
`CAPTURE_SIZE 464x352`; WGC returned a real decoded PNG at 464x352 (9,366
bytes). The stable LimeGreen sentinel decoded to 4,000 pixels in baseline,
covered, and uncovered frames. The run passed C4 capture and occlusion, C5a
in-flight settlement (`verified/value_set`), C5b queued cancellation with no
mutation, C5c control-plane responsiveness, C6 blocked request unsettled
through the 2-second grace and fixture survival, helper termination/restart,
and fresh readback.

The same run passed exact PID+HWND whole-window recreation checks: the old
snapshot refused with `stale_target_revalidate_failed`, the new HWND was
selected explicitly, and a fresh observe succeeded. Same-window control
replacement was not exercised. The parent probe printed
`HOST_STAGE initialized` and `HOST_STAGE observe_sent`, exited host code 77,
and the helper disappeared without the lifecycle driver killing that helper.
EOF stdin closure exited a new helper with code 0 in 10 ms. All owned fixture
and helper processes were absent after teardown.

## Protocol regressions

Command:

```powershell
node experiments/maka-cu-windows/protocol-regression.mjs `
experiments/maka-cu-windows/out/publish/maka-cu-windows.exe
```

Result: `protocol failures=0`. A wrong method type returned JSON-RPC `-32600`
and exited cleanly; unknown cancellation id 999 emitted no notification
response and left both unrelated `debug_sleep` requests complete; 1,000
initialize messages with stdout intentionally undrained exited fail-closed
with code 2 rather than hanging indefinitely.

## Limits and decision

The experiment remains fixture-only and does not enable a Windows production
backend or use global input, screen-rectangle fallback, arbitrary app launch,
or user-window selection. Process start time and window generation are
required; unavailable identity data refuses actions. GUID tokens are unique per
snapshot, but same-value HWND reuse cannot be proven as impossible by this
fingerprint and remains a documented residual risk. A production integration
should add OS-level parent ownership (for example a Job Object), signing,
supported-release testing, and clean-machine evidence.

Recommendation: **hold production enablement; the feasibility gate is not
complete** despite the local fixture and packaged spike passing. Same-window
control replacement, clean-machine execution, and packaging measurements still
need evidence under #4318. This is not a demonstrated technical no-go for .NET.
Signing remains deferred production distribution work, not an extra spike gate.
92 changes: 92 additions & 0 deletions experiments/maka-cu-windows/capture-probe.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// Focused WGC probe. Starts only the purpose-built fixture and helper, asks
// for one target-window capture, and always tears both down.
import { spawn } from 'node:child_process';
import { createInterface } from 'node:readline';

const helper = process.argv[2];
const fixture = process.argv[3];
if (!helper || !fixture) throw new Error('usage: node capture-probe.mjs <helper> <fixture>');

const fx = spawn(fixture, [], { stdio: ['pipe', 'pipe', 'inherit'] });
const fr = createInterface({ input: fx.stdout });
let target;
const ready = new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('fixture READY timeout')), 8000);
fr.on('line', (line) => {
const m = line.match(/^READY (\d+) (\d+)$/);
if (m) {
clearTimeout(timer);
target = { pid: Number(m[1]), hwnd: Number(m[2]) };
resolve();
}
});
});

const h = spawn(helper, [], { stdio: ['pipe', 'pipe', 'pipe'] });
h.stderr.on('data', (chunk) => process.stderr.write(chunk));
const hr = createInterface({ input: h.stdout });
let nextId = 1;
const pending = new Map();
hr.on('line', (line) => {
const msg = JSON.parse(line);
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
p(msg);
}
});
const call = (method, params = {}, timeout = 10000) =>
new Promise((resolve, reject) => {
const id = nextId++;
pending.set(id, resolve);
h.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
setTimeout(() => {
if (pending.delete(id)) reject(new Error(`${method} timeout`));
}, timeout);
});

try {
await ready;
const hello = await call('initialize');
const capture = await call(
'capture',
{
hwnd: target.hwnd,
windowGeneration: (await call('observe', { hwnd: target.hwnd })).result.target
.windowGeneration,
},
10000,
);
console.log(JSON.stringify({ target, hello: hello.result, capture }, null, 2));
} finally {
try {
h.stdin.end();
} catch {}
try {
fx.stdin.write('shutdown\n');
fx.stdin.end();
} catch {}
setTimeout(() => {
if (!h.killed) h.kill();
if (!fx.killed) fx.kill();
}, 300);
}
Loading