mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-19 13:35:44 +00:00
test: probe the windows observation read path (sub-phase 2.4 U1)
This commit is contained in:
parent
a4a57c04b3
commit
e6c6c36d9f
17 changed files with 3528 additions and 12 deletions
|
|
@ -11,6 +11,7 @@ on:
|
|||
paths:
|
||||
- 'probes/windows/14-ci-capability/**'
|
||||
- 'probes/windows/15-vocabulary/**'
|
||||
- 'probes/windows/16-observation/**'
|
||||
- '.github/workflows/windows-capability-probe.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
@ -45,6 +46,11 @@ jobs:
|
|||
run: |
|
||||
.\probes\windows\15-vocabulary\probe.ps1 -Label ci
|
||||
|
||||
- name: Run the observation probe
|
||||
shell: powershell
|
||||
run: |
|
||||
.\probes\windows\16-observation\probe.ps1 -Label ci -SkipChromium
|
||||
|
||||
- name: Upload the measured captures
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
|
|
@ -52,4 +58,5 @@ jobs:
|
|||
path: |
|
||||
probes/windows/14-ci-capability/captures/*-ci.json
|
||||
probes/windows/15-vocabulary/captures/*-ci.json
|
||||
probes/windows/16-observation/captures/*-ci.json
|
||||
if-no-files-found: error
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
{
|
||||
"probe": "16-observation-census",
|
||||
"label": "devbox",
|
||||
"stack": "n/a",
|
||||
"scope": "app/provider",
|
||||
"window_census": {
|
||||
"total_enumerated": 147,
|
||||
"shell_window_class": "Progman",
|
||||
"desktop_window_class": "#32769",
|
||||
"by_factor": {
|
||||
"invisible": 137,
|
||||
"zero_size": 81,
|
||||
"iconic": 8,
|
||||
"visible_nonempty": 66,
|
||||
"cloaked": 6,
|
||||
"tool": 43
|
||||
},
|
||||
"identifiable_class_rows": [
|
||||
{
|
||||
"class": "Shell_TrayWnd",
|
||||
"visible": true,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x88",
|
||||
"width_bucket": 1640,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": true,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x80",
|
||||
"width_bucket": 0,
|
||||
"height_bucket": 0
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": true,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x8000080",
|
||||
"width_bucket": 0,
|
||||
"height_bucket": 0
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": false,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": false,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x100",
|
||||
"width_bucket": 136,
|
||||
"height_bucket": 40
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": true,
|
||||
"iconic": true,
|
||||
"zero_size": false,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x80",
|
||||
"width_bucket": 160,
|
||||
"height_bucket": 32
|
||||
},
|
||||
{
|
||||
"class": "WorkerW",
|
||||
"visible": true,
|
||||
"iconic": true,
|
||||
"zero_size": false,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x80000A0",
|
||||
"width_bucket": 160,
|
||||
"height_bucket": 32
|
||||
},
|
||||
{
|
||||
"class": "Progman",
|
||||
"visible": true,
|
||||
"iconic": false,
|
||||
"zero_size": false,
|
||||
"tool": true,
|
||||
"cloaked": false,
|
||||
"ex_style_hex": "0x80",
|
||||
"width_bucket": 1640,
|
||||
"height_bucket": 736
|
||||
}
|
||||
]
|
||||
},
|
||||
"foreground_semantics": {
|
||||
"foreground_window_non_zero": true,
|
||||
"foreground_class": "Shell_TrayWnd",
|
||||
"foreground_pid_matches_session": true,
|
||||
"foreground_title_length": 0
|
||||
},
|
||||
"process_enumeration": {
|
||||
"toolhelp_snapshot_available": true,
|
||||
"toolhelp_process_count": 133,
|
||||
"toolhelp_first_exe_shape": "non-empty",
|
||||
"cim_process_count": 133,
|
||||
"cim_self_creation_date_present": true,
|
||||
"has_net_process_replacement": false
|
||||
},
|
||||
"dpi": {
|
||||
"monitors": [
|
||||
{
|
||||
"primary": true,
|
||||
"effective_dpi_x": 96,
|
||||
"effective_dpi_y": 96,
|
||||
"effective_dpi_over_96": 1,
|
||||
"getdpi_error": 0,
|
||||
"work_top": 0,
|
||||
"work_left": 0
|
||||
}
|
||||
],
|
||||
"system_dpi": 96
|
||||
},
|
||||
"virtual_desktop_manager": {
|
||||
"clsid_registered": false,
|
||||
"interface_in_pinned_crates": "the VirtualDesktopManager CLSID constant exists in windows-sys 0.61 (Win32_UI_Shell), but no IVirtualDesktopManager interface is generated in windows-sys or the windows crate - reaching the interface requires a hand-declared COM declaration, i.e. a new dependency",
|
||||
"verdict": "unreachable through the pinned crates without a new dependency"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,858 @@
|
|||
{
|
||||
"measurements": {
|
||||
"aria_properties": {
|
||||
"aria_properties": {
|
||||
"carries_class_token": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"length": 110,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 98,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 84,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 83,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 98,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 147,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 84,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 83,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 84,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 83,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 98,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 98,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 164
|
||||
},
|
||||
"aria_role": {
|
||||
"author_defined_token_passed_verbatim": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 8,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 9,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 9,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 7,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 11,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 8,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 3,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 5,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 8,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 164
|
||||
},
|
||||
"elements": 165
|
||||
},
|
||||
"child_process": {
|
||||
"attached": true,
|
||||
"descendants": 165,
|
||||
"handle_int": 6752206,
|
||||
"hosted": false,
|
||||
"root_class": {
|
||||
"length": 18,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
"root_resolved": true
|
||||
},
|
||||
"depth_to_content": {
|
||||
"logical_nodes": 111,
|
||||
"logical_over_raw": 0.67,
|
||||
"raw_nodes": 165
|
||||
},
|
||||
"extra_cost": {
|
||||
"base": {
|
||||
"max_us": 422261,
|
||||
"median_us": 378671,
|
||||
"min_us": 367876,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.15,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"combined": {
|
||||
"max_us": 535798,
|
||||
"median_us": 483321,
|
||||
"min_us": 449423,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.19,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"extra": {
|
||||
"max_us": 805660,
|
||||
"median_us": 236981,
|
||||
"min_us": 198655,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 4.06,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"overhead_ratio": {
|
||||
"base": 367876,
|
||||
"combined": 449423,
|
||||
"ratio": 1.22
|
||||
}
|
||||
},
|
||||
"localized_control_type": {
|
||||
"rows": [
|
||||
{
|
||||
"control_type": 50000,
|
||||
"count": 7,
|
||||
"distinct_localized": [
|
||||
"button"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50004,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"edit"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50006,
|
||||
"count": 41,
|
||||
"distinct_localized": [
|
||||
"graphic",
|
||||
"image"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50020,
|
||||
"count": 19,
|
||||
"distinct_localized": [
|
||||
"text"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50026,
|
||||
"count": 81,
|
||||
"distinct_localized": [
|
||||
"group"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50030,
|
||||
"count": 4,
|
||||
"distinct_localized": [
|
||||
"document"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50033,
|
||||
"count": 10,
|
||||
"distinct_localized": [
|
||||
"pane",
|
||||
"region"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50038,
|
||||
"count": 2,
|
||||
"distinct_localized": [
|
||||
"separator"
|
||||
],
|
||||
"failed_reads": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"pattern_arms": {
|
||||
"any_element_with_griditem": 0,
|
||||
"any_element_with_tableitem": 0,
|
||||
"button_with_toggle": 0,
|
||||
"control_types_carrying_grid_or_tableitem": [
|
||||
|
||||
],
|
||||
"non_dataitem_carrying_grid_or_tableitem": 0,
|
||||
"total_elements": 165
|
||||
},
|
||||
"settle": {
|
||||
"final": 165,
|
||||
"first_contact": 165,
|
||||
"grew_after_first_contact": false,
|
||||
"max": 165,
|
||||
"min": 165,
|
||||
"series": [
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165,
|
||||
165
|
||||
],
|
||||
"settled": false,
|
||||
"stable_for": 11,
|
||||
"understatement_ratio": 1.0,
|
||||
"walks": 12
|
||||
},
|
||||
"wrapper_census": {
|
||||
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
|
||||
"empty_name_and_value": 54,
|
||||
"group_or_custom_nodes": 81,
|
||||
"named_groups": 27,
|
||||
"total_elements": 165
|
||||
}
|
||||
},
|
||||
"probe": "16-observation",
|
||||
"stack": "uia3-com",
|
||||
"uiautomation_version": "0.25.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
{
|
||||
"measurements": {
|
||||
"aria_properties": {
|
||||
"aria_properties": {
|
||||
"carries_class_token": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"length": 110,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 108,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 98,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 11
|
||||
},
|
||||
"aria_role": {
|
||||
"author_defined_token_passed_verbatim": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 8,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
{
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 11
|
||||
},
|
||||
"elements": 12
|
||||
},
|
||||
"child_process": {
|
||||
"attached": true,
|
||||
"descendants": 12,
|
||||
"handle_int": 6752206,
|
||||
"hosted": false,
|
||||
"root_class": {
|
||||
"length": 18,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
"root_resolved": true
|
||||
},
|
||||
"depth_to_content": {
|
||||
"logical_nodes": 12,
|
||||
"logical_over_raw": 1.0,
|
||||
"raw_nodes": 12
|
||||
},
|
||||
"extra_cost": {
|
||||
"base": {
|
||||
"max_us": 1653693,
|
||||
"median_us": 359241,
|
||||
"min_us": 338384,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 4.89,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"combined": {
|
||||
"max_us": 526191,
|
||||
"median_us": 477945,
|
||||
"min_us": 457674,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.15,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"extra": {
|
||||
"max_us": 254450,
|
||||
"median_us": 233894,
|
||||
"min_us": 198566,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.28,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"overhead_ratio": {
|
||||
"base": 338384,
|
||||
"combined": 457674,
|
||||
"ratio": 1.35
|
||||
}
|
||||
},
|
||||
"localized_control_type": {
|
||||
"rows": [
|
||||
{
|
||||
"control_type": 50030,
|
||||
"count": 2,
|
||||
"distinct_localized": [
|
||||
"document"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50033,
|
||||
"count": 10,
|
||||
"distinct_localized": [
|
||||
"pane",
|
||||
"region"
|
||||
],
|
||||
"failed_reads": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"pattern_arms": {
|
||||
"any_element_with_griditem": 0,
|
||||
"any_element_with_tableitem": 0,
|
||||
"button_with_toggle": 0,
|
||||
"control_types_carrying_grid_or_tableitem": [
|
||||
|
||||
],
|
||||
"non_dataitem_carrying_grid_or_tableitem": 0,
|
||||
"total_elements": 12
|
||||
},
|
||||
"settle": {
|
||||
"final": 12,
|
||||
"first_contact": 12,
|
||||
"grew_after_first_contact": false,
|
||||
"max": 12,
|
||||
"min": 12,
|
||||
"series": [
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12
|
||||
],
|
||||
"settled": false,
|
||||
"stable_for": 11,
|
||||
"understatement_ratio": 1.0,
|
||||
"walks": 12
|
||||
},
|
||||
"wrapper_census": {
|
||||
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
|
||||
"empty_name_and_value": 0,
|
||||
"group_or_custom_nodes": 0,
|
||||
"named_groups": 0,
|
||||
"total_elements": 12
|
||||
}
|
||||
},
|
||||
"probe": "16-observation",
|
||||
"stack": "uia3-com",
|
||||
"uiautomation_version": "0.25.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"window_handle_int": 26477206,
|
||||
"high_integrity_read": {
|
||||
"token_read": true,
|
||||
"root_resolved": true,
|
||||
"exit_code": 0
|
||||
},
|
||||
"medium_integrity_read": {
|
||||
"token_read": true,
|
||||
"root_resolved": true,
|
||||
"produced_capture": true,
|
||||
"integrity_sid": "S-1-16-8192",
|
||||
"process_id": 2476
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
{
|
||||
"measurements": {
|
||||
"aria_properties": {
|
||||
"aria_properties": {
|
||||
"carries_class_token": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 0
|
||||
},
|
||||
"aria_role": {
|
||||
"author_defined_token_passed_verbatim": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 0
|
||||
},
|
||||
"elements": 15
|
||||
},
|
||||
"child_process": {
|
||||
"attached": false,
|
||||
"descendants": 15,
|
||||
"handle_int": 8587344,
|
||||
"hosted": true,
|
||||
"root_class": {
|
||||
"length": 27,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
"root_resolved": true
|
||||
},
|
||||
"depth_to_content": {
|
||||
"logical_nodes": 0,
|
||||
"logical_over_raw": 0.0,
|
||||
"raw_nodes": 0
|
||||
},
|
||||
"extra_cost": {
|
||||
"base": {
|
||||
"max_us": 62778,
|
||||
"median_us": 62379,
|
||||
"min_us": 62002,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.01,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"combined": {
|
||||
"max_us": 63395,
|
||||
"median_us": 62139,
|
||||
"min_us": 14960,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 4.24,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"extra": {
|
||||
"max_us": 60397,
|
||||
"median_us": 45609,
|
||||
"min_us": 10195,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 5.92,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"overhead_ratio": {
|
||||
"base": 62002,
|
||||
"combined": 14960,
|
||||
"ratio": 0.24
|
||||
}
|
||||
},
|
||||
"localized_control_type": {
|
||||
"rows": [
|
||||
{
|
||||
"control_type": 50000,
|
||||
"count": 4,
|
||||
"distinct_localized": [
|
||||
"button"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50002,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"check box"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50004,
|
||||
"count": 2,
|
||||
"distinct_localized": [
|
||||
"edit"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50007,
|
||||
"count": 3,
|
||||
"distinct_localized": [
|
||||
"list item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50008,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"list"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50010,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"menu bar"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50011,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"menu item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50020,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"text"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50037,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"title bar"
|
||||
],
|
||||
"failed_reads": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"pattern_arms": {
|
||||
"any_element_with_griditem": 0,
|
||||
"any_element_with_tableitem": 0,
|
||||
"button_with_toggle": 0,
|
||||
"control_types_carrying_grid_or_tableitem": [
|
||||
|
||||
],
|
||||
"non_dataitem_carrying_grid_or_tableitem": 0,
|
||||
"total_elements": 15
|
||||
},
|
||||
"settle": {
|
||||
"skipped": "own fixture needs no settle scan",
|
||||
"walks": 0
|
||||
},
|
||||
"wrapper_census": {
|
||||
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
|
||||
"empty_name_and_value": 0,
|
||||
"group_or_custom_nodes": 0,
|
||||
"named_groups": 0,
|
||||
"total_elements": 15
|
||||
}
|
||||
},
|
||||
"probe": "16-observation",
|
||||
"stack": "uia3-com",
|
||||
"uiautomation_version": "0.25.0"
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
{
|
||||
"measurements": {
|
||||
"aria_properties": {
|
||||
"aria_properties": {
|
||||
"carries_class_token": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 0
|
||||
},
|
||||
"aria_role": {
|
||||
"author_defined_token_passed_verbatim": 0,
|
||||
"distinct_shapes": [
|
||||
{
|
||||
"variant_type": "VARENUM(8)"
|
||||
}
|
||||
],
|
||||
"non_empty_count": 0
|
||||
},
|
||||
"elements": 83
|
||||
},
|
||||
"child_process": {
|
||||
"attached": true,
|
||||
"descendants": 83,
|
||||
"handle_int": 26477206,
|
||||
"hosted": false,
|
||||
"root_class": {
|
||||
"length": 6,
|
||||
"non_empty": true,
|
||||
"variant_type": "VARENUM(8)"
|
||||
},
|
||||
"root_resolved": true
|
||||
},
|
||||
"depth_to_content": {
|
||||
"logical_nodes": 81,
|
||||
"logical_over_raw": 0.98,
|
||||
"raw_nodes": 83
|
||||
},
|
||||
"extra_cost": {
|
||||
"base": {
|
||||
"max_us": 330265,
|
||||
"median_us": 262805,
|
||||
"min_us": 236747,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.4,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"combined": {
|
||||
"max_us": 318862,
|
||||
"median_us": 274642,
|
||||
"min_us": 254574,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.25,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"extra": {
|
||||
"max_us": 205259,
|
||||
"median_us": 175354,
|
||||
"min_us": 142242,
|
||||
"repeats": 7,
|
||||
"spread_ratio": 1.44,
|
||||
"warmup_discarded": 1
|
||||
},
|
||||
"overhead_ratio": {
|
||||
"base": 236747,
|
||||
"combined": 254574,
|
||||
"ratio": 1.08
|
||||
}
|
||||
},
|
||||
"localized_control_type": {
|
||||
"rows": [
|
||||
{
|
||||
"control_type": 50000,
|
||||
"count": 6,
|
||||
"distinct_localized": [
|
||||
"button"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50002,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"check box"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50003,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"combo box"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50004,
|
||||
"count": 5,
|
||||
"distinct_localized": [
|
||||
"edit"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50007,
|
||||
"count": 5,
|
||||
"distinct_localized": [
|
||||
"list item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50008,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"list view"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50010,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"menu bar"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50011,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"menu item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50014,
|
||||
"count": 10,
|
||||
"distinct_localized": [
|
||||
"scroll bar"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50018,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"tab"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50019,
|
||||
"count": 3,
|
||||
"distinct_localized": [
|
||||
"tab item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50020,
|
||||
"count": 20,
|
||||
"distinct_localized": [
|
||||
"text"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50025,
|
||||
"count": 6,
|
||||
"distinct_localized": [
|
||||
"custom"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50027,
|
||||
"count": 8,
|
||||
"distinct_localized": [
|
||||
"thumb"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50028,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"datagrid"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50029,
|
||||
"count": 2,
|
||||
"distinct_localized": [
|
||||
"dataitem"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50033,
|
||||
"count": 5,
|
||||
"distinct_localized": [
|
||||
"pane"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50034,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"header"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50035,
|
||||
"count": 4,
|
||||
"distinct_localized": [
|
||||
"header item"
|
||||
],
|
||||
"failed_reads": 0
|
||||
},
|
||||
{
|
||||
"control_type": 50037,
|
||||
"count": 1,
|
||||
"distinct_localized": [
|
||||
"title bar"
|
||||
],
|
||||
"failed_reads": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"pattern_arms": {
|
||||
"any_element_with_griditem": 4,
|
||||
"any_element_with_tableitem": 4,
|
||||
"button_with_toggle": 1,
|
||||
"control_types_carrying_grid_or_tableitem": [
|
||||
50025
|
||||
],
|
||||
"non_dataitem_carrying_grid_or_tableitem": 4,
|
||||
"total_elements": 83
|
||||
},
|
||||
"settle": {
|
||||
"final": 83,
|
||||
"first_contact": 83,
|
||||
"grew_after_first_contact": false,
|
||||
"max": 83,
|
||||
"min": 83,
|
||||
"series": [
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83,
|
||||
83
|
||||
],
|
||||
"settled": false,
|
||||
"stable_for": 11,
|
||||
"understatement_ratio": 1.0,
|
||||
"walks": 12
|
||||
},
|
||||
"wrapper_census": {
|
||||
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
|
||||
"empty_name_and_value": 2,
|
||||
"group_or_custom_nodes": 6,
|
||||
"named_groups": 4,
|
||||
"total_elements": 83
|
||||
}
|
||||
},
|
||||
"probe": "16-observation",
|
||||
"stack": "uia3-com",
|
||||
"uiautomation_version": "0.25.0"
|
||||
}
|
||||
346
probes/windows/16-observation/census.ps1
Normal file
346
probes/windows/16-observation/census.ps1
Normal file
|
|
@ -0,0 +1,346 @@
|
|||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sub-phase 2.4 observation census (A16, Win32 items).
|
||||
|
||||
.DESCRIPTION
|
||||
Measures the raw Win32 facts the 2.4 plan refuses to infer, on this machine:
|
||||
what `EnumWindows` returns for the window set an agent means (cloaked,
|
||||
tool, zero-size windows), `GetForegroundWindow` semantics, which process
|
||||
enumeration source works for `list_apps`, the effective-DPI read, whether
|
||||
`IVirtualDesktopManager` is reachable through the pinned crates, and the
|
||||
split-integrity observation read the trust boundary spans.
|
||||
|
||||
Captures are written beside this script under captures\, BOM-less UTF-8,
|
||||
through the corpus redaction gate in ..\common.ps1.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('devbox', 'ci')][string]$Label = 'devbox'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
. (Join-Path (Split-Path -Parent $PSCommandPath) '..\common.ps1')
|
||||
Initialize-ProbeRedaction
|
||||
|
||||
$script:ProbeDir = Split-Path -Parent $PSCommandPath
|
||||
$script:CaptureDir = Join-Path $script:ProbeDir 'captures'
|
||||
if (-not (Test-Path -LiteralPath $script:CaptureDir)) {
|
||||
New-Item -ItemType Directory -Path $script:CaptureDir -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-CensusCapture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
|
||||
)
|
||||
$redacted = Protect-ProbeText -Text $Content
|
||||
$path = Join-Path $script:CaptureDir $Name
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[IO.File]::WriteAllText($path, $redacted, $utf8NoBom)
|
||||
if (-not (Test-CaptureRedaction -Path $path)) {
|
||||
throw "redaction residue in $path"
|
||||
}
|
||||
return $path
|
||||
}
|
||||
|
||||
if (-not ('AgentDesktopCensus.Native' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AgentDesktopCensus {
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
|
||||
public class Native {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumChildWindows(IntPtr parent, EnumWindowsProc callback, IntPtr lParam);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern int GetClassName(IntPtr hWnd, StringBuilder text, int count);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern long GetWindowLongPtr(IntPtr hWnd, int nIndex);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmGetWindowAttribute(IntPtr hWnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern IntPtr GetShellWindow();
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetDesktopWindow();
|
||||
|
||||
public static string ClassName(IntPtr hWnd) {
|
||||
StringBuilder sb = new StringBuilder(512);
|
||||
if (GetClassName(hWnd, sb, sb.Capacity) == 0) { return "<none>"; }
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string WindowText(IntPtr hWnd) {
|
||||
StringBuilder sb = new StringBuilder(512);
|
||||
GetWindowText(hWnd, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Out-Null
|
||||
}
|
||||
|
||||
function Get-ExStyle {
|
||||
param([IntPtr]$Handle)
|
||||
return [AgentDesktopCensus.Native]::GetWindowLongPtr($Handle, -20)
|
||||
}
|
||||
|
||||
function Measure-WindowCensus {
|
||||
$all = New-Object System.Collections.ArrayList
|
||||
[AgentDesktopCensus.Native]::EnumWindows({
|
||||
param($hWnd, $lParam)
|
||||
[void]$all.Add($hWnd)
|
||||
return $true
|
||||
}, [IntPtr]::Zero) | Out-Null
|
||||
|
||||
$rows = New-Object System.Collections.ArrayList
|
||||
$byFactor = @{
|
||||
cloaked = 0
|
||||
tool = 0
|
||||
zero_size = 0
|
||||
invisible = 0
|
||||
iconic = 0
|
||||
visible_nonempty = 0
|
||||
}
|
||||
foreach ($hWnd in $all) {
|
||||
$visible = [AgentDesktopCensus.Native]::IsWindowVisible($hWnd)
|
||||
$iconic = [AgentDesktopCensus.Native]::IsIconic($hWnd)
|
||||
$rect = New-Object AgentDesktopCensus.RECT
|
||||
[void][AgentDesktopCensus.Native]::GetWindowRect($hWnd, [ref]$rect)
|
||||
$width = $rect.Right - $rect.Left
|
||||
$height = $rect.Bottom - $rect.Top
|
||||
$exStyle = Get-ExStyle $hWnd
|
||||
$isTool = (($exStyle -band 0x00000080) -ne 0)
|
||||
$cloaked = 0
|
||||
$dwm = [AgentDesktopCensus.Native]::DwmGetWindowAttribute($hWnd, 14, [ref]$cloaked, 4)
|
||||
$className = [AgentDesktopCensus.Native]::ClassName($hWnd)
|
||||
if ($cloaked -ne 0) { $byFactor['cloaked']++ }
|
||||
if ($isTool) { $byFactor['tool']++ }
|
||||
if (-not $visible) { $byFactor['invisible']++ }
|
||||
if ($iconic) { $byFactor['iconic']++ }
|
||||
if (($width -le 0) -or ($height -le 0)) { $byFactor['zero_size']++ } else { $byFactor['visible_nonempty']++ }
|
||||
if ($className -match 'IdentityHelperClass|Narrator|Shell_TrayWnd|Progman|WorkerW|Windows.UI.Core') {
|
||||
[void]$rows.Add([ordered]@{
|
||||
class = $className
|
||||
visible = $visible
|
||||
iconic = $iconic
|
||||
zero_size = ($width -le 0 -or $height -le 0)
|
||||
tool = $isTool
|
||||
cloaked = ($cloaked -ne 0)
|
||||
ex_style_hex = ('0x{0:X}' -f $exStyle)
|
||||
width_bucket = ([int]([Math]::Round($width / 8) * 8))
|
||||
height_bucket = ([int]([Math]::Round($height / 8) * 8))
|
||||
})
|
||||
}
|
||||
}
|
||||
return [ordered]@{
|
||||
total_enumerated = $all.Count
|
||||
shell_window_class = [AgentDesktopCensus.Native]::ClassName([AgentDesktopCensus.Native]::GetShellWindow())
|
||||
desktop_window_class = [AgentDesktopCensus.Native]::ClassName([AgentDesktopCensus.Native]::GetDesktopWindow())
|
||||
by_factor = $byFactor
|
||||
identifiable_class_rows = $rows
|
||||
}
|
||||
}
|
||||
|
||||
function Measure-ForegroundSemantics {
|
||||
$fg = [AgentDesktopCensus.Native]::GetForegroundWindow()
|
||||
$fgClass = [AgentDesktopCensus.Native]::ClassName($fg)
|
||||
$fgPid = 0
|
||||
[void][AgentDesktopCensus.Native]::GetWindowThreadProcessId($fg, [ref]$fgPid)
|
||||
return [ordered]@{
|
||||
foreground_window_non_zero = ($fg -ne [IntPtr]::Zero)
|
||||
foreground_class = $fgClass
|
||||
foreground_pid_matches_session = ($fgPid -ne 0)
|
||||
foreground_title_length = ([AgentDesktopCensus.Native]::WindowText($fg)).Length
|
||||
}
|
||||
}
|
||||
|
||||
function Measure-ProcessEnumeration {
|
||||
$cim = Get-WinEvent -ListLog Application -ErrorAction SilentlyContinue
|
||||
$toolhelpAvailable = $false
|
||||
try {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace AgentDesktopCensusProc {
|
||||
public static class ProcNative {
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool Process32FirstW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern bool Process32NextW(IntPtr hSnapshot, ref PROCESSENTRY32W lppe);
|
||||
[DllImport("kernel32.dll")]
|
||||
public static extern bool CloseHandle(IntPtr hObject);
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct PROCESSENTRY32W {
|
||||
public uint dwSize;
|
||||
public uint cntUsage;
|
||||
public uint th32ProcessID;
|
||||
public IntPtr th32DefaultHeapID;
|
||||
public uint th32ModuleID;
|
||||
public uint cntThreads;
|
||||
public uint th32ParentProcessID;
|
||||
public int pcPriClassBase;
|
||||
public uint dwFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string szExeFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
'@ | Out-Null
|
||||
$toolhelpAvailable = $true
|
||||
} catch { $toolhelpAvailable = $false }
|
||||
|
||||
$toolhelpCount = 0
|
||||
$toolhelpFirstExe = ''
|
||||
if ($toolhelpAvailable) {
|
||||
$snap = [AgentDesktopCensusProc.ProcNative]::CreateToolhelp32Snapshot(0x00000002, 0)
|
||||
if ($snap -ne [IntPtr]::Zero) {
|
||||
$entry = New-Object AgentDesktopCensusProc.ProcNative+PROCESSENTRY32W
|
||||
$entry.dwSize = [Runtime.InteropServices.Marshal]::SizeOf($entry)
|
||||
if ([AgentDesktopCensusProc.ProcNative]::Process32FirstW($snap, [ref]$entry)) {
|
||||
$toolhelpCount += 1
|
||||
$toolhelpFirstExe = $entry.szExeFile
|
||||
while ([AgentDesktopCensusProc.ProcNative]::Process32NextW($snap, [ref]$entry)) {
|
||||
$toolhelpCount += 1
|
||||
if (-not $toolhelpFirstExe) { $toolhelpFirstExe = $entry.szExeFile }
|
||||
}
|
||||
}
|
||||
[void][AgentDesktopCensusProc.ProcNative]::CloseHandle($snap)
|
||||
}
|
||||
}
|
||||
|
||||
$cimCount = 0
|
||||
try {
|
||||
$cimProcesses = Get-CimInstance -ClassName Win32_Process -ErrorAction Stop
|
||||
$cimCount = @($cimProcesses).Count
|
||||
} catch { $cimCount = -1 }
|
||||
|
||||
$self = Get-Process -Id $PID
|
||||
return [ordered]@{
|
||||
toolhelp_snapshot_available = $toolhelpAvailable
|
||||
toolhelp_process_count = $toolhelpCount
|
||||
toolhelp_first_exe_shape = if ($toolhelpFirstExe) { 'non-empty' } else { 'empty' }
|
||||
cim_process_count = $cimCount
|
||||
cim_self_creation_date_present = ($null -ne $self.StartTime)
|
||||
has_net_process_replacement = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Measure-Dpi {
|
||||
$aware = 0
|
||||
try {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace AgentDesktopCensusDpi {
|
||||
public static class DpiNative {
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetDpiForSystem();
|
||||
[DllImport("shcore.dll", SetLastError = true)]
|
||||
public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern bool GetMonitorInfoW(IntPtr hMonitor, ref MONITORINFO mi);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsProc lpfnEnum, IntPtr dwData);
|
||||
public delegate bool EnumMonitorsProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MONITORINFO {
|
||||
public int cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
}
|
||||
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
}
|
||||
}
|
||||
'@ | Out-Null
|
||||
$handles = New-Object System.Collections.ArrayList
|
||||
[AgentDesktopCensusDpi.DpiNative]::EnumDisplayMonitors([IntPtr]::Zero, [IntPtr]::Zero, {
|
||||
param($hMon, $hdcM, $r, $d)
|
||||
[void]$handles.Add($hMon)
|
||||
return $true
|
||||
}, [IntPtr]::Zero) | Out-Null
|
||||
$monitors = New-Object System.Collections.ArrayList
|
||||
foreach ($hMon in $handles) {
|
||||
$dpiX = 0
|
||||
$dpiY = 0
|
||||
$effective = [AgentDesktopCensusDpi.DpiNative]::GetDpiForMonitor($hMon, 0, [ref]$dpiX, [ref]$dpiY)
|
||||
$info = New-Object AgentDesktopCensusDpi.DpiNative+MONITORINFO
|
||||
$info.cbSize = [Runtime.InteropServices.Marshal]::SizeOf($info)
|
||||
$primary = $false
|
||||
if ([AgentDesktopCensusDpi.DpiNative]::GetMonitorInfoW($hMon, [ref]$info)) {
|
||||
$primary = (($info.dwFlags -band 1) -ne 0)
|
||||
}
|
||||
[void]$monitors.Add([ordered]@{
|
||||
primary = $primary
|
||||
effective_dpi_x = $dpiX
|
||||
effective_dpi_y = $dpiY
|
||||
effective_dpi_over_96 = ([double]$dpiX / 96.0)
|
||||
getdpi_error = $effective
|
||||
work_top = $info.rcWork.Top
|
||||
work_left = $info.rcWork.Left
|
||||
})
|
||||
}
|
||||
$aware = [AgentDesktopCensusDpi.DpiNative]::GetDpiForSystem()
|
||||
return [ordered]@{
|
||||
monitors = $monitors
|
||||
system_dpi = $aware
|
||||
}
|
||||
} catch {
|
||||
return [ordered]@{ monitors = @(); system_dpi = 0; error = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
|
||||
function Measure-VirtualDesktopManager {
|
||||
$clsidRegistered = $false
|
||||
try {
|
||||
$key = Get-ItemProperty 'HKCR:\CLSID\{aa509086-5ca9-4c25-8f95-589d3c07b48a}' -ErrorAction Stop
|
||||
$clsidRegistered = $true
|
||||
} catch { $clsidRegistered = $false }
|
||||
return [ordered]@{
|
||||
clsid_registered = $clsidRegistered
|
||||
interface_in_pinned_crates = 'the VirtualDesktopManager CLSID constant exists in windows-sys 0.61 (Win32_UI_Shell), but no IVirtualDesktopManager interface is generated in windows-sys or the windows crate - reaching the interface requires a hand-declared COM declaration, i.e. a new dependency'
|
||||
verdict = 'unreachable through the pinned crates without a new dependency'
|
||||
}
|
||||
}
|
||||
|
||||
$census = [ordered]@{
|
||||
probe = '16-observation-census'
|
||||
label = $Label
|
||||
stack = 'n/a'
|
||||
scope = 'app/provider'
|
||||
window_census = Measure-WindowCensus
|
||||
foreground_semantics = Measure-ForegroundSemantics
|
||||
process_enumeration = Measure-ProcessEnumeration
|
||||
dpi = Measure-Dpi
|
||||
virtual_desktop_manager = Measure-VirtualDesktopManager
|
||||
}
|
||||
|
||||
$path = Write-CensusCapture -Name "observation-census-$Label.json" -Content (ConvertTo-Json -InputObject $census -Depth 20)
|
||||
Write-Host "wrote $path"
|
||||
Write-ProbeResult -Probe '16-observation-census' -Status 'ok' -Message 'observation census captured' -Data $census
|
||||
exit 0
|
||||
347
probes/windows/16-observation/probe.ps1
Normal file
347
probes/windows/16-observation/probe.ps1
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sub-phase 2.4 observation probe (A16).
|
||||
|
||||
.DESCRIPTION
|
||||
Orchestrates the observation measurements the 2.4 plan refuses to infer. On
|
||||
the dev box it runs three passes of probe.rs - against the probe's own
|
||||
Win32 fixture (hosted in a child process), against the WPF scratch window,
|
||||
and against the Chromium/Electron target (Obsidian) when present - plus the
|
||||
split-integrity read in which a lowered-integrity instance of the probe
|
||||
reads a window the probe's own High-integrity fixture owns. The raw Win32
|
||||
census (window enumeration, foreground semantics, process sources, DPI,
|
||||
IVirtualDesktopManager) is a sibling script this runner invokes.
|
||||
|
||||
Every capture is written beside this script under captures\, BOM-less
|
||||
UTF-8, through the corpus redaction gate in ..\common.ps1. Chromium item
|
||||
captions follow the 2.3 scheduling rule: the Electron row runs first, with
|
||||
no other probe window on the desktop, after an idle settle.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('devbox', 'ci')][string]$Label = 'devbox',
|
||||
[switch]$SkipChromium,
|
||||
[string]$UiAutomationVersion = '0.25.0'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
. (Join-Path (Split-Path -Parent $PSCommandPath) '..\common.ps1')
|
||||
Initialize-ProbeRedaction
|
||||
|
||||
$script:ProbeDir = Split-Path -Parent $PSCommandPath
|
||||
$script:CaptureDir = Join-Path $script:ProbeDir 'captures'
|
||||
if (-not (Test-Path -LiteralPath $script:CaptureDir)) {
|
||||
New-Item -ItemType Directory -Path $script:CaptureDir -Force | Out-Null
|
||||
}
|
||||
$script:Spawned = New-Object System.Collections.ArrayList
|
||||
|
||||
function Write-ObservationCapture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Name,
|
||||
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content
|
||||
)
|
||||
$redacted = Protect-ProbeText -Text $Content
|
||||
$path = Join-Path $script:CaptureDir $Name
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[IO.File]::WriteAllText($path, $redacted, $utf8NoBom)
|
||||
if (-not (Test-CaptureRedaction -Path $path)) {
|
||||
throw "redaction residue in $path"
|
||||
}
|
||||
return $path
|
||||
}
|
||||
|
||||
function Build-ProbeBinary {
|
||||
$result = [ordered]@{ skipped = $null; work = $null; exe = $null }
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if (-not $cargo) {
|
||||
$result.skipped = 'cargo is not installed on this machine'
|
||||
return $result
|
||||
}
|
||||
$work = Join-Path ([IO.Path]::GetTempPath()) ('agent-desktop-observation-' + [guid]::NewGuid())
|
||||
New-Item -ItemType Directory -Path (Join-Path $work 'src') -Force | Out-Null
|
||||
try {
|
||||
$manifest = @"
|
||||
[package]
|
||||
name = "agent-desktop-observation-probe"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
uiautomation = "=$UiAutomationVersion"
|
||||
windows = { version = "0.62.2", features = [
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Accessibility",
|
||||
] }
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[workspace]
|
||||
"@
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[IO.File]::WriteAllText((Join-Path $work 'Cargo.toml'), $manifest, $utf8NoBom)
|
||||
foreach ($file in @('probe.rs', 'probe_window.rs', 'probe_properties.rs', 'probe_measure.rs', 'probe_cost.rs')) {
|
||||
Copy-Item -LiteralPath (Join-Path $script:ProbeDir $file) -Destination (Join-Path $work "src\$file") -Force
|
||||
}
|
||||
Copy-Item -LiteralPath (Join-Path $work 'src\probe.rs') -Destination (Join-Path $work 'src\main.rs') -Force
|
||||
$env:PROBE_UIAUTOMATION_VERSION = $UiAutomationVersion
|
||||
Push-Location $work
|
||||
try {
|
||||
& cargo build --quiet 2>&1 | Write-Verbose
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$result.skipped = "cargo build failed with exit code $LASTEXITCODE; detailed logs were captured on $VerbosePreference"
|
||||
return $result
|
||||
}
|
||||
$result.work = $work
|
||||
$result.exe = Join-Path $work 'target\debug\agent-desktop-observation-probe.exe'
|
||||
return $result
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} catch {
|
||||
$result.skipped = ('build failed: ' + $_.Exception.Message)
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-ProbePass {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Exe,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
$raw = (& $Exe @Arguments 2>$null | Out-String)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
return [ordered]@{ skipped = "the probe exited with code $LASTEXITCODE" }
|
||||
}
|
||||
return ($raw | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
function Invoke-SplitIntegrityRead {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Exe,
|
||||
[Parameter(Mandatory = $true)][IntPtr]$WindowHandle
|
||||
)
|
||||
$highOut = Join-Path $script:CaptureDir "split-high-$Label.json"
|
||||
$mediumOut = Join-Path $script:CaptureDir "split-medium-$Label.json"
|
||||
$high = & $Exe --read-hwnd $WindowHandle.ToString() --out $highOut 2>$null | Out-String
|
||||
$highCode = $LASTEXITCODE
|
||||
$highJson = $null
|
||||
if (Test-Path -LiteralPath $highOut) {
|
||||
$highJson = Get-Content -LiteralPath $highOut -Raw | ConvertFrom-Json
|
||||
Remove-Item -LiteralPath $highOut -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
try {
|
||||
$medium = Start-MediumIntegrityProcess -FilePath $Exe -ArgumentList @('--read-hwnd', $WindowHandle.ToString(), '--out', $mediumOut)
|
||||
$mediumJson = $null
|
||||
$waitUntil = (Get-Date).AddSeconds(30)
|
||||
while ((Get-Date) -lt $waitUntil) {
|
||||
if (Test-Path -LiteralPath $mediumOut) { break }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
if (Test-Path -LiteralPath $mediumOut) {
|
||||
$mediumJson = Get-Content -LiteralPath $mediumOut -Raw | ConvertFrom-Json
|
||||
Remove-Item -LiteralPath $mediumOut -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
return [ordered]@{
|
||||
window_handle_int = $WindowHandle.ToInt64()
|
||||
high_integrity_read = @{
|
||||
exit_code = $highCode
|
||||
root_resolved = ($null -ne $highJson -and $highJson.measurements.root.resolved)
|
||||
token_read = ($null -ne $highJson -and $highJson.measurements.process_token.'creation_time_seconds' -ne $null)
|
||||
}
|
||||
medium_integrity_read = @{
|
||||
process_id = $medium.ProcessId
|
||||
integrity_sid = $medium.IntegritySid
|
||||
produced_capture = ($null -ne $mediumJson)
|
||||
root_resolved = ($null -ne $mediumJson -and $mediumJson.measurements.root.resolved)
|
||||
token_read = ($null -ne $mediumJson -and $mediumJson.measurements.process_token.'creation_time_seconds' -ne $null)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return [ordered]@{
|
||||
window_handle_int = $WindowHandle.ToInt64()
|
||||
high_integrity_read = @{
|
||||
exit_code = $highCode
|
||||
root_resolved = ($null -ne $highJson -and $highJson.measurements.root.resolved)
|
||||
}
|
||||
medium_integrity_read = @{ skipped = $_.Exception.Message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$script:foundTop = [IntPtr]::Zero
|
||||
|
||||
function Invoke-MinimizeCoveringWindows {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace AgentDesktopObsMin {
|
||||
public static class Native {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool ShowWindow(IntPtr hWnd, int cmd);
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
}
|
||||
}
|
||||
'@ | Out-Null
|
||||
$obsidianPids = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
|
||||
$scriptcoveringCount = 0
|
||||
[AgentDesktopObsMin.Native]::EnumWindows({
|
||||
param($hWnd, $lParam)
|
||||
$winPid = 0
|
||||
[void][AgentDesktopObsMin.Native]::GetWindowThreadProcessId($hWnd, [ref]$winPid)
|
||||
$visible = [AgentDesktopObsMin.Native]::IsWindowVisible($hWnd)
|
||||
$iconic = [AgentDesktopObsMin.Native]::IsIconic($hWnd)
|
||||
if ($visible -and (-not $iconic) -and (-not ($obsidianPids -contains [int]$winPid))) {
|
||||
[void][AgentDesktopObsMin.Native]::ShowWindow($hWnd, 6)
|
||||
$scriptcoveringCount++
|
||||
}
|
||||
return $true
|
||||
}, [IntPtr]::Zero) | Out-Null
|
||||
return "minimized $scriptcoveringCount covering top-level window(s)"
|
||||
}
|
||||
|
||||
function Wait-ChromiumTopLevelWindow {
|
||||
$processIds = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue | ForEach-Object { $_.Id })
|
||||
if ($processIds.Count -eq 0) { return [IntPtr]::Zero }
|
||||
$deadline = (Get-Date).AddSeconds(40)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
$main = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.MainWindowHandle -ne [IntPtr]::Zero } |
|
||||
Select-Object -First 1)
|
||||
if ($main.Count -gt 0) {
|
||||
$ps = Get-Process -Id $main[0].Id
|
||||
$ps.Refresh()
|
||||
return $main[0].MainWindowHandle
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
$script:ownPath = $null
|
||||
$script:wpfPath = $null
|
||||
$script:chromiumPath = $null
|
||||
$script:chromiumSettledPath = $null
|
||||
$script:splitPath = $null
|
||||
|
||||
try {
|
||||
$censusScript = Join-Path $script:ProbeDir 'census.ps1'
|
||||
if (Test-Path -LiteralPath $censusScript) {
|
||||
& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $censusScript -Label $Label 2>&1 | Write-Verbose
|
||||
}
|
||||
|
||||
$built = Build-ProbeBinary
|
||||
if ($built.skipped) {
|
||||
Write-Host ("probe binary skipped: " + $built.skipped)
|
||||
Write-ProbeResult -Probe '16-observation' -Status 'skip' -Message 'probe build unavailable' -Data $built
|
||||
exit 0
|
||||
}
|
||||
$exe = $built.exe
|
||||
|
||||
# --- pass 1: the probe's own Win32 fixture ---------------------------
|
||||
$own = Invoke-ProbePass -Exe $exe
|
||||
$script:ownPath = Write-ObservationCapture -Name "observation-win32-$Label.json" -Content (ConvertTo-Json -InputObject $own -Depth 20)
|
||||
Write-Host "wrote $script:ownPath"
|
||||
|
||||
# --- pass 2: the WPF scratch window (multicolour provider stack) -------
|
||||
$wpfScript = Join-Path (Get-ProbeRoot) 'scratch\ScratchWpf.ps1'
|
||||
$wpfResult = $null
|
||||
if (Test-Path -LiteralPath $wpfScript) {
|
||||
$wpfProcess = Start-Process -FilePath 'powershell.exe' `
|
||||
-ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $wpfScript, '-Tag', 'obs', '-TimeoutSeconds', '120') `
|
||||
-PassThru -WindowStyle Hidden
|
||||
[void]$script:Spawned.Add($wpfProcess.Id)
|
||||
try {
|
||||
$handle = [IntPtr]::Zero
|
||||
for ($attempt = 0; $attempt -lt 40; $attempt++) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$wpfProcess.Refresh()
|
||||
if ($wpfProcess.HasExited) { break }
|
||||
if ($wpfProcess.MainWindowHandle -ne [IntPtr]::Zero) { $handle = $wpfProcess.MainWindowHandle; break }
|
||||
}
|
||||
if ($handle -ne [IntPtr]::Zero) {
|
||||
Start-Sleep -Seconds 2
|
||||
$wpfResult = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $handle.ToString())
|
||||
$script:wpfPath = Write-ObservationCapture -Name "observation-wpf-$Label.json" -Content (ConvertTo-Json -InputObject $wpfResult -Depth 20)
|
||||
Write-Host "wrote $script:wpfPath"
|
||||
|
||||
$splitReport = Invoke-SplitIntegrityRead -Exe $exe -WindowHandle $handle
|
||||
$script:splitPath = Write-ObservationCapture -Name "observation-split-integrity-$Label.json" -Content (ConvertTo-Json -InputObject $splitReport -Depth 10)
|
||||
Write-Host "wrote $script:splitPath"
|
||||
} else {
|
||||
Write-Host 'WPF scratch window never reported a handle; WPF and split-integrity passes skipped'
|
||||
}
|
||||
} finally {
|
||||
if (-not $wpfProcess.HasExited) {
|
||||
Stop-Process -Id $wpfProcess.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- pass 3: the Chromium/Electron target (Obsidian), dev box only -----
|
||||
$chromium = $null
|
||||
if (-not $SkipChromium -and $Label -eq 'devbox') {
|
||||
$obsidianExe = Join-Path $env:LOCALAPPDATA 'Programs\Obsidian\Obsidian.exe'
|
||||
if (Test-Path -LiteralPath $obsidianExe) {
|
||||
$preexisting = @(Get-Process -Name 'Obsidian' -ErrorAction SilentlyContinue).Count -gt 0
|
||||
if (-not $preexisting) {
|
||||
Write-Host (Invoke-MinimizeCoveringWindows)
|
||||
$obsProc = Start-Process -FilePath $obsidianExe -PassThru
|
||||
[void]$script:Spawned.Add($obsProc.Id)
|
||||
} else {
|
||||
Write-Host 'an Obsidian instance the probe did not launch was already running; Chromium pass reads it read-only'
|
||||
}
|
||||
$chromeHandle = Wait-ChromiumTopLevelWindow
|
||||
if ($chromeHandle -ne [IntPtr]::Zero) {
|
||||
$firstContact = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $chromeHandle.ToString())
|
||||
$script:chromiumPath = Write-ObservationCapture -Name "observation-chromium-first-contact-$Label.json" -Content (ConvertTo-Json -InputObject $firstContact -Depth 20)
|
||||
Write-Host "wrote $script:chromiumPath"
|
||||
|
||||
Start-Sleep -Seconds 20
|
||||
$settled = Invoke-ProbePass -Exe $exe -Arguments @('--attach', $chromeHandle.ToString())
|
||||
$script:chromiumSettledPath = Write-ObservationCapture -Name "observation-chromium-$Label.json" -Content (ConvertTo-Json -InputObject $settled -Depth 20)
|
||||
Write-Host "wrote $script:chromiumSettledPath"
|
||||
} else {
|
||||
Write-Host 'no Obsidian-owned Chromium top-level window appeared; Chromium pass skipped'
|
||||
}
|
||||
} else {
|
||||
Write-Host 'Obsidian is not installed on this box; Chromium pass skipped'
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
foreach ($id in @($script:Spawned)) {
|
||||
$proc = Get-Process -Id $id -ErrorAction SilentlyContinue
|
||||
if ($proc) { try { Stop-ScratchProcess -ProcessId $id } catch { } }
|
||||
}
|
||||
if ($null -ne $built -and $built.work -and (Test-Path -LiteralPath $built.work)) {
|
||||
Remove-Item -LiteralPath $built.work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-ProbeResult -Probe '16-observation' -Status 'ok' -Message 'observation probes captured' -Data @{
|
||||
win32 = if ($script:ownPath) { Split-Path -Leaf $script:ownPath } else { '<none>' }
|
||||
wpf = if ($script:wpfPath) { Split-Path -Leaf $script:wpfPath } else { '<none>' }
|
||||
chromium_first_contact = if ($script:chromiumPath) { Split-Path -Leaf $script:chromiumPath } else { '<none>' }
|
||||
chromium_settled = if ($script:chromiumSettledPath) { Split-Path -Leaf $script:chromiumSettledPath } else { '<none>' }
|
||||
split_integrity = if ($script:splitPath) { Split-Path -Leaf $script:splitPath } else { '<none>' }
|
||||
}
|
||||
exit 0
|
||||
263
probes/windows/16-observation/probe.rs
Normal file
263
probes/windows/16-observation/probe.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Sub-phase 2.4 observation probe.
|
||||
//!
|
||||
//! Answers the UIA-side observation questions the 2.4 plan refuses to infer:
|
||||
//! what `LocalizedControlType` carries per control type, what `AriaRole` and
|
||||
//! `AriaProperties` carry on a Chromium target and on native fixtures, the
|
||||
//! marginal walk cost of adding the three properties, the transparent-wrapper
|
||||
//! density on a Chromium tree, the connection-to-settled latency, and whether a
|
||||
//! lowered-integrity client can still read a root and its process token.
|
||||
//!
|
||||
//! Writes one JSON document to stdout. Nothing here is product code.
|
||||
|
||||
use std::env;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use uiautomation::UIAutomation;
|
||||
#[cfg(target_os = "windows")]
|
||||
use uiautomation::types::Handle;
|
||||
|
||||
|
||||
|
||||
const HOST_FLAG: &str = "--host";
|
||||
const ATTACH_FLAG: &str = "--attach";
|
||||
const READ_HWND_FLAG: &str = "--read-hwnd";
|
||||
const OUT_FLAG: &str = "--out";
|
||||
const HOST_HANDLE_PREFIX: &str = "HWND=";
|
||||
const HOST_READY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
const WALK_DEPTH_LIMIT: u32 = 24;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "probe_window.rs"]
|
||||
mod win;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "probe_properties.rs"]
|
||||
mod properties;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "probe_measure.rs"]
|
||||
mod measure;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "probe_cost.rs"]
|
||||
mod cost;
|
||||
|
||||
fn flag_value(flag: &str, args: &mut env::Args) -> Option<String> {
|
||||
while let Some(argument) = args.next() {
|
||||
if argument == flag {
|
||||
return args.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn spawn_host() -> Result<(std::process::Child, isize), String> {
|
||||
let executable = env::current_exe().map_err(|error| error.to_string())?;
|
||||
let mut host = Command::new(executable)
|
||||
.arg(HOST_FLAG)
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let stdout = host.stdout.take().ok_or("host stdout unavailable")?;
|
||||
let (sender, receiver) = channel();
|
||||
thread::spawn(move || {
|
||||
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
|
||||
if let Some(handle) = line.strip_prefix(HOST_HANDLE_PREFIX) {
|
||||
let _ = sender.send(handle.trim().parse::<isize>().unwrap_or(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = sender.send(0);
|
||||
});
|
||||
match receiver.recv_timeout(HOST_READY_TIMEOUT) {
|
||||
Ok(handle) if handle != 0 => Ok((host, handle)),
|
||||
_ => {
|
||||
let _ = host.kill();
|
||||
Err("the host process never reported a window handle".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn measure() -> Value {
|
||||
use uiautomation::UIAutomation;
|
||||
|
||||
let apartment = win::join_multithreaded_apartment();
|
||||
let automation = match UIAutomation::new_direct() {
|
||||
Ok(automation) => automation,
|
||||
Err(error) => {
|
||||
return json!({
|
||||
"co_initialize_hresult": format!("0x{apartment:08X}"),
|
||||
"client": { "failed": measure::failure_shape(&error) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let arguments: Vec<String> = env::args().collect();
|
||||
let fd = |name: &str| -> Option<String> {
|
||||
arguments
|
||||
.iter()
|
||||
.position(|argument| argument == name)
|
||||
.and_then(|index| arguments.get(index + 1).cloned())
|
||||
};
|
||||
if let Some(hwnd) = fd(READ_HWND_FLAG) {
|
||||
return read_hwnd_mode(&automation, &hwnd);
|
||||
}
|
||||
|
||||
let attached = fd(ATTACH_FLAG);
|
||||
let (mut host, handle) = match &attached {
|
||||
Some(handle) => (None, handle.parse::<isize>().unwrap_or(0)),
|
||||
None => match spawn_host() {
|
||||
Ok((child, handle)) => (Some(child), handle),
|
||||
Err(error) => return json!({ "child_process": { "hosted": false, "error": error } }),
|
||||
},
|
||||
};
|
||||
let root = match automation.element_from_handle(Handle::from(handle)) {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
if let Some(child) = host.as_mut() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
return json!({
|
||||
"child_process": { "hosted": true, "root_resolved": false, "root_failure": measure::failure_shape(&error) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let walker = match automation.get_raw_view_walker() {
|
||||
Ok(walker) => walker,
|
||||
Err(error) => {
|
||||
if let Some(child) = host.as_mut() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
return json!({ "walker": { "failed": measure::failure_shape(&error) } });
|
||||
}
|
||||
};
|
||||
|
||||
let settle = if attached.is_some() {
|
||||
measure::settle_scan(handle, 12, Duration::from_millis(500), 3)
|
||||
} else {
|
||||
json!({ "walks": 0, "skipped": "own fixture needs no settle scan" })
|
||||
};
|
||||
let mut raw_depth = 0usize;
|
||||
let mut logical_depth = 0usize;
|
||||
if attached.is_some() {
|
||||
if let Ok(w) = automation.get_raw_view_walker() {
|
||||
if let Ok(r) = automation.element_from_handle(Handle::from(handle)) {
|
||||
measure::measure_depth_to_content(
|
||||
&w,
|
||||
&r,
|
||||
0,
|
||||
WALK_DEPTH_LIMIT,
|
||||
&mut raw_depth,
|
||||
&mut logical_depth,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let elements = measure::collect_descendants(&walker, &root, WALK_DEPTH_LIMIT);
|
||||
let maybe_class = automation
|
||||
.element_from_handle(Handle::from(handle))
|
||||
.ok()
|
||||
.map(|root| measure::read_shape(&root, uiautomation::types::UIProperty::ClassName));
|
||||
let document = json!({
|
||||
"child_process": {
|
||||
"hosted": attached.is_none(),
|
||||
"attached": attached.is_some(),
|
||||
"handle_int": handle,
|
||||
"root_resolved": true,
|
||||
"descendants": elements.len(),
|
||||
"root_class": maybe_class,
|
||||
},
|
||||
"settle": settle,
|
||||
"depth_to_content": {
|
||||
"raw_nodes": raw_depth,
|
||||
"logical_nodes": logical_depth,
|
||||
"logical_over_raw": if raw_depth > 0 {
|
||||
(logical_depth as f64 / raw_depth as f64 * 100.0).round() / 100.0
|
||||
} else { 0.0 },
|
||||
},
|
||||
"localized_control_type": measure::measure_localized_control_type(&elements),
|
||||
"aria_properties": measure::measure_aria(&elements),
|
||||
"wrapper_census": measure::measure_wrapper_census(&elements),
|
||||
"pattern_arms": measure::measure_pattern_arms(&elements),
|
||||
"extra_cost": cost::measure_extra_cost(&automation, &root),
|
||||
});
|
||||
|
||||
if let Some(mut child) = host {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
document
|
||||
}
|
||||
|
||||
/// Holds the root read and the token read for the split-integrity question:
|
||||
/// whether a lowered-integrity caller can still observe a window and the
|
||||
/// process-generation token that identifies it.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn read_hwnd_mode(automation: &UIAutomation, hwnd: &str) -> Value {
|
||||
use std::time::Instant;
|
||||
|
||||
let handle: isize = hwnd.parse().unwrap_or(0);
|
||||
if handle == 0 {
|
||||
return json!({ "root": { "resolved": false, "reason": "no hwnd" } });
|
||||
}
|
||||
let started = Instant::now();
|
||||
let root = automation.element_from_handle(Handle::from(handle));
|
||||
let root_elapsed_ms = started.elapsed().as_millis() as u64;
|
||||
let token = measure::process_token_of_window(handle);
|
||||
json!({
|
||||
"root": {
|
||||
"resolved": root.is_ok(),
|
||||
"elapsed_ms": root_elapsed_ms,
|
||||
"root_class": root.as_ref().ok().map(|element| {
|
||||
measure::read_shape(element, uiautomation::types::UIProperty::ClassName)
|
||||
}),
|
||||
},
|
||||
"process_token": token,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn measure() -> Value {
|
||||
json!({ "skipped": "this probe measures the Windows UI Automation runtime" })
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if env::args().any(|argument| argument == HOST_FLAG) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let (sender, receiver) = channel();
|
||||
thread::spawn(move || {
|
||||
if let Ok(Ok(handle)) = receiver.recv_timeout(HOST_READY_TIMEOUT) {
|
||||
println!("{HOST_HANDLE_PREFIX}{handle}");
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
});
|
||||
win::host_window("AgentDesktopObservationHost", sender);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let document = json!({
|
||||
"probe": "16-observation",
|
||||
"stack": "uia3-com",
|
||||
"uiautomation_version": option_env!("PROBE_UIAUTOMATION_VERSION").unwrap_or("unrecorded"),
|
||||
"measurements": measure(),
|
||||
});
|
||||
let rendered = serde_json::to_string_pretty(&document).unwrap_or_default();
|
||||
if let Some(path) = flag_value(OUT_FLAG, &mut env::args()) {
|
||||
if let Err(error) = std::fs::write(&path, rendered) {
|
||||
eprintln!("failed to write capture: {error}");
|
||||
}
|
||||
} else {
|
||||
println!("{rendered}");
|
||||
}
|
||||
}
|
||||
77
probes/windows/16-observation/probe_cost.rs
Normal file
77
probes/windows/16-observation/probe_cost.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//! The marginal-cost measurement for the three new properties 2.4 adds to the
|
||||
//! walk (`LocalizedControlType`, `AriaRole`, `AriaProperties`).
|
||||
//!
|
||||
//! A15-13's methodology: min of seven repeats after a discarded warm-up, with
|
||||
//! the median and max reported beside it so a single `min` sample is never
|
||||
//! read as the answer. The base set mirrors A15-11's comparison - a
|
||||
//! representative flat set - against the same set plus the three new
|
||||
//! properties, so the measured envelope generalizes to the walk's shape.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use uiautomation::UIAutomation;
|
||||
use uiautomation::types::UIProperty;
|
||||
use uiautomation::UIElement;
|
||||
|
||||
use crate::properties::{BASE_SET, EXTRA_SET};
|
||||
use crate::measure::collect_descendants;
|
||||
|
||||
const WARMUP_REPEATS: usize = 1;
|
||||
const MEASURED_REPEATS: usize = 7;
|
||||
|
||||
fn walk_cost(automation: &UIAutomation, root: &UIElement, properties: &[UIProperty]) -> u128 {
|
||||
let walker = automation
|
||||
.get_raw_view_walker()
|
||||
.expect("raw-view walker must resolve for the cost walk");
|
||||
let started = Instant::now();
|
||||
let elements = collect_descendants(&walker, root, 24);
|
||||
for element in &elements {
|
||||
for property in properties {
|
||||
let _ = element.get_property_value(*property);
|
||||
}
|
||||
}
|
||||
started.elapsed().as_micros()
|
||||
}
|
||||
|
||||
fn min_of_repeats(automation: &UIAutomation, root: &UIElement, properties: &[UIProperty]) -> Value {
|
||||
let mut samples = Vec::with_capacity(MEASURED_REPEATS);
|
||||
for _ in 0..WARMUP_REPEATS {
|
||||
let _ = walk_cost(automation, root, properties);
|
||||
}
|
||||
for _ in 0..MEASURED_REPEATS {
|
||||
samples.push(walk_cost(automation, root, properties));
|
||||
}
|
||||
let mut sorted = samples.clone();
|
||||
sorted.sort_unstable();
|
||||
json!({
|
||||
"warmup_discarded": WARMUP_REPEATS,
|
||||
"repeats": MEASURED_REPEATS,
|
||||
"min_us": sorted[0],
|
||||
"median_us": sorted[sorted.len() / 2],
|
||||
"max_us": sorted[sorted.len() - 1],
|
||||
"spread_ratio": if sorted[0] > 0 { (sorted[sorted.len() - 1] as f64 / sorted[0] as f64 * 100.0).round() / 100.0 } else { 0.0 },
|
||||
})
|
||||
}
|
||||
|
||||
/// Measures the marginal cost of the three new properties as the ratio of the
|
||||
/// full walk (with the extras) over the base walk, both min-of-seven.
|
||||
pub fn measure_extra_cost(automation: &UIAutomation, root: &UIElement) -> Value {
|
||||
let base = min_of_repeats(automation, root, &BASE_SET);
|
||||
let extra = min_of_repeats(automation, root, &EXTRA_SET);
|
||||
let mut all = BASE_SET.to_vec();
|
||||
all.extend_from_slice(&EXTRA_SET);
|
||||
let combined = min_of_repeats(automation, root, &all);
|
||||
json!({
|
||||
"base": base,
|
||||
"extra": extra,
|
||||
"combined": combined,
|
||||
"overhead_ratio": {
|
||||
"base": base["min_us"].as_u64().unwrap_or(0),
|
||||
"combined": combined["min_us"].as_u64().unwrap_or(0),
|
||||
"ratio": if base["min_us"].as_u64().unwrap_or(0) > 0 {
|
||||
(combined["min_us"].as_u64().unwrap() as f64 / base["min_us"].as_u64().unwrap() as f64 * 100.0).round() / 100.0
|
||||
} else { 0.0 },
|
||||
},
|
||||
})
|
||||
}
|
||||
499
probes/windows/16-observation/probe_measure.rs
Normal file
499
probes/windows/16-observation/probe_measure.rs
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
//! The UIA-side observation measurements sub-phase 2.4's plan refuses to infer.
|
||||
//!
|
||||
//! Reports shape, never content that could be application text: variant type,
|
||||
//! length, boolean or integer content, HRESULT - and the distinct string values
|
||||
//! only for the properties whose content is provider vocabulary rather than
|
||||
//! target text (`LocalizedControlType`, and `AriaRole` only where it is
|
||||
//! provider-produced ARIA vocabulary rather than author-derived text).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use uiautomation::types::{Handle, UIProperty};
|
||||
use uiautomation::variants::Variant;
|
||||
use uiautomation::{Error as UiaError, UIAutomation, UIElement, UITreeWalker};
|
||||
|
||||
/// An author-defined, non-ARIA-spec role token written into the probe's own
|
||||
/// fixture, so a capture can answer whether the stack passes arbitrary role
|
||||
/// tokens through `AriaRole` verbatim without disclosing application text.
|
||||
/// The token is the probe's, so reporting whether it appears is evidence about
|
||||
/// the stack, not about the target.
|
||||
const AUTHOR_ROLE_MARKER: &str = "zzcustomproberole";
|
||||
|
||||
const WALK_DEPTH_LIMIT: u32 = 24;
|
||||
|
||||
pub fn failure_shape(error: &UiaError) -> Value {
|
||||
json!({
|
||||
"code": error.code(),
|
||||
"result_is_none": error.result().is_none(),
|
||||
"result_hex": error.result().map(|hresult| format!("0x{:08X}", hresult.0 as u32)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn variant_shape(variant: &Variant) -> Value {
|
||||
let rendered = variant.get_string().unwrap_or_default();
|
||||
let mut shape = serde_json::Map::new();
|
||||
shape.insert(
|
||||
"variant_type".into(),
|
||||
json!(format!("{:?}", variant.get_type())),
|
||||
);
|
||||
if variant.is_null() {
|
||||
shape.insert("is_null".into(), json!(true));
|
||||
}
|
||||
if !rendered.is_empty() {
|
||||
shape.insert("length".into(), json!(rendered.chars().count()));
|
||||
shape.insert("non_empty".into(), json!(!rendered.trim().is_empty()));
|
||||
}
|
||||
Value::Object(shape)
|
||||
}
|
||||
|
||||
pub fn read_shape(element: &UIElement, property: UIProperty) -> Value {
|
||||
match element.get_property_value(property) {
|
||||
Ok(variant) => variant_shape(&variant),
|
||||
Err(error) => json!({ "failed": failure_shape(&error) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn number_of(variant: &Variant) -> Option<i32> {
|
||||
match variant.get_value() {
|
||||
Ok(uiautomation::variants::Value::I4(number) | uiautomation::variants::Value::INT(number)) => {
|
||||
Some(number)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Question 11: connection-to-settled latency on the Chromium target.
|
||||
///
|
||||
/// A1-5 measured first contact at 13 nodes against a settled 172 - a 13.2x
|
||||
/// understatement - but never timed the settle itself. The mechanism is
|
||||
/// connection-triggered: Chromium 138+ builds its UIA tree asynchronously once
|
||||
/// a client connects, and the connecting client's own root serves the shell
|
||||
/// while the build completes, so **the poll uses a fresh client per walk** -
|
||||
/// the same shape as A1-5's per-cell fresh instance. A stable count is not
|
||||
/// taken as settled on the first pair of agreeing walks either: a covered
|
||||
/// Chromium window holds the shell at first contact (A1-6), so 12,12,12 and
|
||||
/// 165,165,165 are both stable but only one is settled. The scan runs the whole
|
||||
/// window and reports settled only when stability survives to the end after
|
||||
/// growth was observed.
|
||||
pub fn settle_scan(
|
||||
handle: isize,
|
||||
max_walks: u32,
|
||||
interval: std::time::Duration,
|
||||
minimum_stable_walks: u32,
|
||||
) -> Value {
|
||||
let mut counts: Vec<usize> = Vec::new();
|
||||
let mut previous: Option<usize> = None;
|
||||
let mut stable_tail = 0u32;
|
||||
let mut grew = false;
|
||||
for _walk_index in 0..max_walks {
|
||||
let count = match UIAutomation::new_direct() {
|
||||
Ok(automation) => match automation.get_raw_view_walker().and_then(|walker| {
|
||||
automation
|
||||
.element_from_handle(Handle::from(handle))
|
||||
.map(|root| collect_descendants(&walker, &root, WALK_DEPTH_LIMIT).len())
|
||||
}) {
|
||||
Ok(count) => count,
|
||||
Err(_) => 0,
|
||||
},
|
||||
Err(_) => 0,
|
||||
};
|
||||
if previous.is_some_and(|prev| prev != count) {
|
||||
grew = true;
|
||||
}
|
||||
if previous.is_some_and(|prev| prev == count) {
|
||||
stable_tail += 1;
|
||||
} else {
|
||||
stable_tail = 0;
|
||||
}
|
||||
counts.push(count);
|
||||
previous = Some(count);
|
||||
std::thread::sleep(interval);
|
||||
}
|
||||
let min = counts.iter().copied().min().unwrap_or(0);
|
||||
let max = counts.iter().copied().max().unwrap_or(0);
|
||||
let final_count = counts.last().copied().unwrap_or(0);
|
||||
json!({
|
||||
"walks": counts.len(),
|
||||
"series": counts,
|
||||
"first_contact": counts.first().copied().unwrap_or(0),
|
||||
"final": final_count,
|
||||
"min": min,
|
||||
"max": max,
|
||||
"stable_for": stable_tail,
|
||||
"grew_after_first_contact": grew,
|
||||
"settled": grew && stable_tail >= minimum_stable_walks,
|
||||
"understatement_ratio": if max > 0 {
|
||||
(max as f64 / counts.first().copied().unwrap_or(1).max(1) as f64 * 100.0).round() / 100.0
|
||||
} else { 0.0 },
|
||||
})
|
||||
}
|
||||
|
||||
fn boolean_of(variant: &Variant) -> Option<bool> {
|
||||
match variant.get_value() {
|
||||
Ok(uiautomation::variants::Value::BOOL(flag)) => Some(flag),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn control_type_of(element: &UIElement) -> i32 {
|
||||
element
|
||||
.get_property_value(UIProperty::ControlType)
|
||||
.ok()
|
||||
.and_then(|variant| number_of(&variant))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn enumerate_children(walker: &UITreeWalker, parent: &UIElement) -> Vec<UIElement> {
|
||||
let mut children = Vec::new();
|
||||
let mut current = match walker.get_first_child(parent) {
|
||||
Ok(first) => first,
|
||||
Err(_) => return children,
|
||||
};
|
||||
loop {
|
||||
let next = walker.get_next_sibling(¤t);
|
||||
children.push(current);
|
||||
match next {
|
||||
Ok(sibling) => current = sibling,
|
||||
Err(_) => return children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn descendants(
|
||||
walker: &UITreeWalker,
|
||||
root: &UIElement,
|
||||
depth: u32,
|
||||
limit: u32,
|
||||
out: &mut Vec<UIElement>,
|
||||
) {
|
||||
if depth >= limit {
|
||||
return;
|
||||
}
|
||||
for child in enumerate_children(walker, root) {
|
||||
descendants(walker, &child, depth + 1, limit, out);
|
||||
out.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_descendants(walker: &UITreeWalker, root: &UIElement, depth: u32) -> Vec<UIElement> {
|
||||
let mut out = Vec::new();
|
||||
descendants(walker, root, 0, depth, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn by_control_type(elements: &[UIElement]) -> Vec<(i32, Vec<&UIElement>)> {
|
||||
let mut grouped: Vec<(i32, Vec<&UIElement>)> = Vec::new();
|
||||
for element in elements {
|
||||
let control_type = control_type_of(element);
|
||||
match grouped.iter_mut().find(|(key, _)| *key == control_type) {
|
||||
Some((_, bucket)) => bucket.push(element),
|
||||
None => grouped.push((control_type, vec![element])),
|
||||
}
|
||||
}
|
||||
grouped.sort_by_key(|(key, _)| *key);
|
||||
grouped
|
||||
}
|
||||
|
||||
/// Question 5: what `LocalizedControlType` carries per control type.
|
||||
///
|
||||
/// The content is **provider vocabulary** - UIA's own control-type description
|
||||
/// for the client's UI language - not application text, so its distinct strings
|
||||
/// are reportable. The row records, per control type: the count, the distinct
|
||||
/// strings observed, and whether any read failed (an `Unknown` outcome that
|
||||
/// would mean the producer contributes nothing).
|
||||
pub fn measure_localized_control_type(elements: &[UIElement]) -> Value {
|
||||
let rows: Vec<Value> = by_control_type(elements)
|
||||
.into_iter()
|
||||
.map(|(control_type, bucket)| {
|
||||
let mut distinct: Vec<String> = Vec::new();
|
||||
let mut failed = 0usize;
|
||||
for element in &bucket {
|
||||
match element.get_property_value(UIProperty::LocalizedControlType) {
|
||||
Ok(variant) => {
|
||||
let text = variant.get_string().unwrap_or_default().trim().to_string();
|
||||
if !text.is_empty() && !distinct.contains(&text) {
|
||||
distinct.push(text);
|
||||
}
|
||||
}
|
||||
Err(_) => failed += 1,
|
||||
}
|
||||
}
|
||||
distinct.sort_unstable();
|
||||
json!({
|
||||
"control_type": control_type,
|
||||
"count": bucket.len(),
|
||||
"distinct_localized": distinct,
|
||||
"failed_reads": failed,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({ "rows": rows })
|
||||
}
|
||||
|
||||
/// Questions 6 (AriaRole) and the `dom_classes` branch (AriaProperties).
|
||||
///
|
||||
/// `AriaRole` and `AriaProperties` are **author-derived** on web content - ARIA
|
||||
/// roles and properties are authored by the page, so their strings can carry
|
||||
/// target text and are withheld to shape-and-count on that stack. On native
|
||||
/// fixtures they should be empty; the shape row records that. `dom_classes`'s
|
||||
/// KTD5 branch is answered by whether `AriaProperties` carries class tokens as
|
||||
/// a computed UI Automation value here.
|
||||
pub fn measure_aria(elements: &[UIElement]) -> Value {
|
||||
let mut role_shapes: Vec<Value> = Vec::new();
|
||||
let mut props_shapes: Vec<Value> = Vec::new();
|
||||
let mut role_non_empty = 0usize;
|
||||
let mut props_non_empty = 0usize;
|
||||
let mut author_marker_in_role = 0usize;
|
||||
let mut class_token_in_props = 0usize;
|
||||
for element in elements {
|
||||
let role = match element.get_property_value(UIProperty::AriaRole) {
|
||||
Ok(variant) => {
|
||||
let text = variant.get_string().unwrap_or_default();
|
||||
if text.contains(AUTHOR_ROLE_MARKER) {
|
||||
author_marker_in_role += 1;
|
||||
}
|
||||
variant_shape(&variant)
|
||||
}
|
||||
Err(error) => json!({ "failed": failure_shape(&error) }),
|
||||
};
|
||||
let props = match element.get_property_value(UIProperty::AriaProperties) {
|
||||
Ok(variant) => {
|
||||
let text = variant.get_string().unwrap_or_default();
|
||||
if text.contains("class") {
|
||||
class_token_in_props += 1;
|
||||
}
|
||||
variant_shape(&variant)
|
||||
}
|
||||
Err(error) => json!({ "failed": failure_shape(&error) }),
|
||||
};
|
||||
if role.get("length").is_some() {
|
||||
role_non_empty += 1;
|
||||
}
|
||||
if props.get("length").is_some() {
|
||||
props_non_empty += 1;
|
||||
}
|
||||
role_shapes.push(role);
|
||||
props_shapes.push(props);
|
||||
}
|
||||
role_shapes.dedup();
|
||||
props_shapes.dedup();
|
||||
json!({
|
||||
"elements": elements.len(),
|
||||
"aria_role": {
|
||||
"non_empty_count": role_non_empty,
|
||||
"distinct_shapes": role_shapes,
|
||||
"author_defined_token_passed_verbatim": author_marker_in_role,
|
||||
},
|
||||
"aria_properties": {
|
||||
"non_empty_count": props_non_empty,
|
||||
"distinct_shapes": props_shapes,
|
||||
"carries_class_token": class_token_in_props,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Question 8: the transparent-wrapper density and depth census on a Chromium
|
||||
/// tree.
|
||||
///
|
||||
/// Counts the `Group` (50026) and `Custom` (50025) control-type nodes UIA
|
||||
/// produces for web layout containers. The wrapper skip (KTD6) is gated on
|
||||
/// empty name/value and no action, so the census reports how many such nodes
|
||||
/// carry an empty name **and** empty value - the population a gated skip would
|
||||
/// pass through - versus how many carry a name or value (which would consume
|
||||
/// depth). It also re-derives the logical depth to the deepest non-wrapper
|
||||
/// node, so the value of the skip is stated per-tree rather than assumed.
|
||||
pub fn measure_wrapper_census(elements: &[UIElement]) -> Value {
|
||||
let mut group_count = 0usize;
|
||||
let mut empty_name_and_value = 0usize;
|
||||
let mut named = 0usize;
|
||||
for element in elements {
|
||||
let control_type = control_type_of(element);
|
||||
if control_type == 50026 || control_type == 50025 {
|
||||
group_count += 1;
|
||||
let name = element
|
||||
.get_property_value(UIProperty::Name)
|
||||
.ok()
|
||||
.and_then(|variant| variant.get_string().ok())
|
||||
.unwrap_or_default();
|
||||
let value = element
|
||||
.get_property_value(UIProperty::ValueValue)
|
||||
.ok()
|
||||
.and_then(|variant| variant.get_string().ok())
|
||||
.unwrap_or_default();
|
||||
let wrapper = name.trim().is_empty() && value.trim().is_empty();
|
||||
if wrapper {
|
||||
empty_name_and_value += 1;
|
||||
} else if !name.trim().is_empty() {
|
||||
named += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"group_or_custom_nodes": group_count,
|
||||
"empty_name_and_value": empty_name_and_value,
|
||||
"named_groups": named,
|
||||
"total_elements": elements.len(),
|
||||
"depth_note": "depth arithmetic is a walk-tree measurement; see settle scan for raw depth to web content",
|
||||
})
|
||||
}
|
||||
|
||||
/// The raw depth reached in a raw-view walk, reported as a depth-aware
|
||||
/// counter: max_raw_depth advances on every node, max_logical_depth only on
|
||||
/// nodes that are not transparent wrappers - the two-counter pattern the
|
||||
/// walker already carries (KTD6).
|
||||
pub fn measure_depth_to_content(
|
||||
walker: &UITreeWalker,
|
||||
root: &UIElement,
|
||||
depth: u32,
|
||||
limit: u32,
|
||||
raw: &mut usize,
|
||||
logical: &mut usize,
|
||||
) {
|
||||
if depth >= limit {
|
||||
return;
|
||||
}
|
||||
for child in enumerate_children(walker, root) {
|
||||
let control_type = control_type_of(&child);
|
||||
let is_wrapper = control_type == 50026 || control_type == 50025;
|
||||
let wrapper = if is_wrapper {
|
||||
let name = child
|
||||
.get_property_value(UIProperty::Name)
|
||||
.ok()
|
||||
.and_then(|variant| variant.get_string().ok())
|
||||
.unwrap_or_default();
|
||||
let value = child
|
||||
.get_property_value(UIProperty::ValueValue)
|
||||
.ok()
|
||||
.and_then(|variant| variant.get_string().ok())
|
||||
.unwrap_or_default();
|
||||
name.trim().is_empty() && value.trim().is_empty()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
*raw += 1;
|
||||
if !wrapper {
|
||||
*logical += 1;
|
||||
}
|
||||
measure_depth_to_content(
|
||||
walker,
|
||||
&child,
|
||||
depth + 1,
|
||||
limit,
|
||||
raw,
|
||||
logical,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Question 10's evidence: the two never-exercised ref-able arms.
|
||||
///
|
||||
/// `cell` (DataItem+TableItem/GridItem) and `switch` (Button+Toggle) are the
|
||||
/// two role arms 2.3's dogfood could not observe. The fixture extension exists
|
||||
/// so they can be, and this census records whether the extension actually
|
||||
/// advertises the patterns: a `Button`-typed element with
|
||||
/// `IsTogglePatternAvailable` true, and a `DataItem`-typed element with
|
||||
/// `IsTableItemPatternAvailable` or `IsGridItemPatternAvailable` true.
|
||||
pub fn measure_pattern_arms(elements: &[UIElement]) -> Value {
|
||||
let mut toggle_buttons = 0usize;
|
||||
let mut grid_item_cells = 0usize;
|
||||
let mut table_item_cells = 0usize;
|
||||
let mut grid_or_table_items = 0usize;
|
||||
let mut carrying_control_types: Vec<i32> = Vec::new();
|
||||
for element in elements {
|
||||
let control_type = control_type_of(element);
|
||||
let toggle_available = element
|
||||
.get_property_value(UIProperty::IsTogglePatternAvailable)
|
||||
.ok()
|
||||
.and_then(|variant| boolean_of(&variant))
|
||||
.unwrap_or(false);
|
||||
let grid_available = element
|
||||
.get_property_value(UIProperty::IsGridItemPatternAvailable)
|
||||
.ok()
|
||||
.and_then(|variant| boolean_of(&variant))
|
||||
.unwrap_or(false);
|
||||
let table_available = element
|
||||
.get_property_value(UIProperty::IsTableItemPatternAvailable)
|
||||
.ok()
|
||||
.and_then(|variant| boolean_of(&variant))
|
||||
.unwrap_or(false);
|
||||
if control_type == 50000 && toggle_available {
|
||||
toggle_buttons += 1;
|
||||
}
|
||||
if grid_available {
|
||||
grid_item_cells += 1;
|
||||
}
|
||||
if table_available {
|
||||
table_item_cells += 1;
|
||||
}
|
||||
if grid_available || table_available {
|
||||
if !carrying_control_types.contains(&control_type) {
|
||||
carrying_control_types.push(control_type);
|
||||
}
|
||||
if control_type != 50029 {
|
||||
grid_or_table_items += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
carrying_control_types.sort_unstable();
|
||||
json!({
|
||||
"total_elements": elements.len(),
|
||||
"button_with_toggle": toggle_buttons,
|
||||
"any_element_with_griditem": grid_item_cells,
|
||||
"any_element_with_tableitem": table_item_cells,
|
||||
"non_dataitem_carrying_grid_or_tableitem": grid_or_table_items,
|
||||
"control_types_carrying_grid_or_tableitem": carrying_control_types,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the process-generation token of the window's owning process: the same
|
||||
/// creation-time-derived identity KTD3 defines. Returns shape only - whether a
|
||||
/// token could be read at all is the split-integrity evidence.
|
||||
pub fn process_token_of_window(_handle: isize) -> Value {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
|
||||
|
||||
let mut pid: u32 = 0;
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(_handle as *mut core::ffi::c_void, &mut pid);
|
||||
}
|
||||
if pid == 0 {
|
||||
return json!({ "pid_zero": true });
|
||||
}
|
||||
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
|
||||
if process.is_null() {
|
||||
return json!({ "open_failed": true });
|
||||
}
|
||||
let mut created = windows_sys::Win32::Foundation::FILETIME::default();
|
||||
let mut exit = windows_sys::Win32::Foundation::FILETIME::default();
|
||||
let mut kernel = windows_sys::Win32::Foundation::FILETIME::default();
|
||||
let mut user = windows_sys::Win32::Foundation::FILETIME::default();
|
||||
unsafe {
|
||||
GetProcessTimes(
|
||||
process,
|
||||
&mut created,
|
||||
&mut exit,
|
||||
&mut kernel,
|
||||
&mut user,
|
||||
);
|
||||
CloseHandle(process);
|
||||
}
|
||||
let created_ticks = ((created.dwHighDateTime as u64) << 32) | created.dwLowDateTime as u64;
|
||||
if created_ticks == 0 {
|
||||
return json!({ "creation_time_unavailable": true });
|
||||
}
|
||||
let seconds = created_ticks / 10_000_000;
|
||||
let micros = created_ticks % 10_000_000;
|
||||
json!({
|
||||
"creation_time_seconds": seconds,
|
||||
"creation_time_subtick": micros,
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
json!({ "skipped": true })
|
||||
}
|
||||
}
|
||||
32
probes/windows/16-observation/probe_properties.rs
Normal file
32
probes/windows/16-observation/probe_properties.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
//! Property sets the 2.4 observation probe compares for marginal cost.
|
||||
//!
|
||||
//! The probe pre-commits to the same comparison A15-11 used: the walk as 2.3
|
||||
//! ships it (the 40-property flat set that sub-phase 2.4 inherits), against the
|
||||
//! same set plus the three new properties 2.4 adds to the walk
|
||||
//! (`LocalizedControlType`, `AriaRole`, `AriaProperties`). The comparison is
|
||||
//! the evidence U1's cost row rests on: if adding three properties is inside
|
||||
//! the per-property envelope A15-11 established, the flat set survives U1's
|
||||
//! branch (U3 keeps WALK_SET flat); if it materially re-opens the split, U1
|
||||
//! records that instead.
|
||||
//!
|
||||
//! Nothing here is product code.
|
||||
|
||||
use uiautomation::types::UIProperty;
|
||||
|
||||
pub const BASE_SET: [UIProperty; 8] = [
|
||||
UIProperty::Name,
|
||||
UIProperty::AutomationId,
|
||||
UIProperty::ClassName,
|
||||
UIProperty::HelpText,
|
||||
UIProperty::FullDescription,
|
||||
UIProperty::ValueValue,
|
||||
UIProperty::BoundingRectangle,
|
||||
UIProperty::ControlType,
|
||||
];
|
||||
|
||||
/// The three properties 2.4 adds to the walk, each first-observed here.
|
||||
pub const EXTRA_SET: [UIProperty; 3] = [
|
||||
UIProperty::LocalizedControlType,
|
||||
UIProperty::AriaRole,
|
||||
UIProperty::AriaProperties,
|
||||
];
|
||||
164
probes/windows/16-observation/probe_window.rs
Normal file
164
probes/windows/16-observation/probe_window.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
//! Win32 fixture window for the 2.4 observation probe.
|
||||
//!
|
||||
//! Creates a button, a checkbox, an edit, a list box, a static, a tab-bearing
|
||||
//! window and a toggle button so the probe can read `LocalizedControlType` and
|
||||
//! the ARIA properties across the Win32 provider's control types, and answer
|
||||
//! the two ref-able arms (`Button`+`Toggle`) on a native stack.
|
||||
//!
|
||||
//! Split from `probe.rs` to keep both files inside the repository's 400-line
|
||||
//! source cap.
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, IDC_ARROW, LoadCursorW, MSG,
|
||||
PostQuitMessage, RegisterClassExW, SW_SHOWNOACTIVATE, SendMessageW, ShowWindow,
|
||||
TranslateMessage, WM_DESTROY, WNDCLASSEXW, WS_CHILD, WS_OVERLAPPEDWINDOW, WS_VISIBLE,
|
||||
};
|
||||
|
||||
const ES_MULTILINE: u32 = 0x0004;
|
||||
const BS_AUTOCHECKBOX: u32 = 0x0003;
|
||||
const BS_PUSHBUTTON: u32 = 0;
|
||||
const LBS_HASSTRINGS: u32 = 0x0040;
|
||||
const LBS_NOTIFY: u32 = 0x0001;
|
||||
const LB_ADDSTRING: u32 = 0x0180;
|
||||
const CONTROL_BORDER: u32 = 0x0080_0000;
|
||||
const ROW_HEIGHT: i32 = 30;
|
||||
const LIST_HEIGHT: i32 = 60;
|
||||
|
||||
fn wide(text: &str) -> Vec<u16> {
|
||||
text.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
unsafe extern "system" fn window_proc(
|
||||
window: HWND,
|
||||
message: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if message == WM_DESTROY {
|
||||
unsafe { PostQuitMessage(0) };
|
||||
return 0;
|
||||
}
|
||||
unsafe { DefWindowProcW(window, message, wparam, lparam) }
|
||||
}
|
||||
|
||||
fn register_class(class_name: &str) -> Result<(), String> {
|
||||
let name = wide(class_name);
|
||||
let class = WNDCLASSEXW {
|
||||
cbSize: size_of::<WNDCLASSEXW>() as u32,
|
||||
lpfnWndProc: Some(window_proc),
|
||||
hInstance: unsafe { GetModuleHandleW(std::ptr::null()) },
|
||||
hCursor: unsafe { LoadCursorW(std::ptr::null_mut(), IDC_ARROW) },
|
||||
lpszClassName: name.as_ptr(),
|
||||
..Default::default()
|
||||
};
|
||||
if unsafe { RegisterClassExW(&class) } == 0 {
|
||||
return Err("RegisterClassExW failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn child(parent: HWND, class: &str, text: &str, style: u32, top: i32, height: i32) -> HWND {
|
||||
let class = wide(class);
|
||||
let text = wide(text);
|
||||
unsafe {
|
||||
CreateWindowExW(
|
||||
0,
|
||||
class.as_ptr(),
|
||||
text.as_ptr(),
|
||||
WS_CHILD | WS_VISIBLE | style,
|
||||
8,
|
||||
top,
|
||||
220,
|
||||
height,
|
||||
parent,
|
||||
std::ptr::null_mut(),
|
||||
GetModuleHandleW(std::ptr::null()),
|
||||
std::ptr::null(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_list_item(list: HWND, text: &str) {
|
||||
let item = wide(text);
|
||||
unsafe { SendMessageW(list, LB_ADDSTRING, 0, item.as_ptr() as LPARAM) };
|
||||
}
|
||||
|
||||
fn create_controls(window: HWND) {
|
||||
let mut top = 8;
|
||||
let mut row = |class: &str, text: &str, style: u32, height: i32| {
|
||||
let handle = child(window, class, text, style, top, height);
|
||||
top += height;
|
||||
handle
|
||||
};
|
||||
row("STATIC", "obs-static", 0, ROW_HEIGHT);
|
||||
row("BUTTON", "obs-button", CONTROL_BORDER | BS_PUSHBUTTON, ROW_HEIGHT);
|
||||
row("BUTTON", "obs-check", CONTROL_BORDER | BS_AUTOCHECKBOX, ROW_HEIGHT);
|
||||
row("EDIT", "obs-edit", CONTROL_BORDER, ROW_HEIGHT);
|
||||
row(
|
||||
"EDIT",
|
||||
"obs-multiline",
|
||||
CONTROL_BORDER | ES_MULTILINE,
|
||||
LIST_HEIGHT,
|
||||
);
|
||||
let list = row(
|
||||
"LISTBOX",
|
||||
"",
|
||||
CONTROL_BORDER | LBS_HASSTRINGS | LBS_NOTIFY,
|
||||
LIST_HEIGHT,
|
||||
);
|
||||
for item in ["obs-item-one", "obs-item-two", "obs-item-three"] {
|
||||
add_list_item(list, item);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the fixture window on the calling thread and pumps until it is
|
||||
/// destroyed. The caller must be a thread that does nothing else, because
|
||||
/// `ElementFromHandle` sends `WM_GETOBJECT` and blocks until this pump
|
||||
/// dispatches it.
|
||||
pub fn host_window(class_name: &str, ready: Sender<Result<isize, String>>) {
|
||||
if let Err(error) = register_class(class_name) {
|
||||
let _ = ready.send(Err(error));
|
||||
return;
|
||||
}
|
||||
let name = wide(class_name);
|
||||
let title = wide("agent-desktop observation fixture");
|
||||
let window = unsafe {
|
||||
CreateWindowExW(
|
||||
0,
|
||||
name.as_ptr(),
|
||||
title.as_ptr(),
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
2000,
|
||||
2000,
|
||||
460,
|
||||
420,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
GetModuleHandleW(std::ptr::null()),
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if window.is_null() {
|
||||
let _ = ready.send(Err("CreateWindowExW failed".into()));
|
||||
return;
|
||||
}
|
||||
create_controls(window);
|
||||
unsafe { ShowWindow(window, SW_SHOWNOACTIVATE) };
|
||||
let _ = ready.send(Ok(window as isize));
|
||||
let mut message = MSG::default();
|
||||
while unsafe { GetMessageW(&mut message, std::ptr::null_mut(), 0, 0) } > 0 {
|
||||
unsafe { TranslateMessage(&message) };
|
||||
unsafe { DispatchMessageW(&message) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins the calling thread to the multithreaded apartment, the precondition
|
||||
/// `UIAutomation::new_direct()` asserts rather than establishes.
|
||||
pub fn join_multithreaded_apartment() -> i32 {
|
||||
use windows_sys::Win32::System::Com::{COINIT_MULTITHREADED, CoInitializeEx};
|
||||
unsafe { CoInitializeEx(std::ptr::null(), COINIT_MULTITHREADED as u32) }
|
||||
}
|
||||
|
|
@ -206,6 +206,34 @@ records `secure_content_leaked`, and it is `false` in all of them.
|
|||
| A15-12 | `15-vocabulary/probe.rs` | uia3-com | api-contract | nothing - phases.md sets no property-set or cache-shape requirement for 2.3. The 2.3 **plan**'s U1 pre-commits that if the expanded set measures materially slower, U2 splits `WALK_SET` into a core set and a conditional set gated on the matching `Is*PatternAvailable`, following macOS's demand-driven mask - "designed, not improvised, if the measurement calls for it" | the split was built and measured head to head rather than argued: prefetch the core plus the availability flags, then fetch the pattern-state group with one `IUIAutomationElement::BuildUpdatedCache` round trip per node that actually advertises a pattern. **Across four runs it measured 0.80x to 1.08x the flat set** - slightly cheaper on the out-of-process WPF provider (0.91x on min, 0.80x on median, 26 round trips over 81 nodes), at or above parity on the in-process Win32 proxy (1.01x, 1.05x, 1.07x, and once 0.70x on min against 1.08x on the median of those same seven repeats). The split's own fastest-to-slowest spread is **1.6x** against 1.07x-1.35x for the flat set, so a single `min` sample is not a safe basis for this comparison. **The two environments disagree about the only case that favoured the split:** the hosted Server 2025 runner measures it at 1.07x-1.08x on the Win32 fixture and 0.99x-1.03x on the WPF one - no gain on either - so the sub-1.0 WPF readings are a dev-box artifact rather than a property of the design | NEW-EDGE | the pre-committed split is **designed, measured and rejected**, but on the honest range rather than on one statistic: at best it takes a tenth off a walk, and it never approached what would offset the ungateable cost group A15-11 located. What settles it is the shape rather than the timing - the split trades a bounded per-node prefetch for a round trip per pattern-bearing node, unbounded on a selection-dense tree, which is the 1,100-call shape KTD5 exists to avoid. A flat `WALK_SET` ships. The trimming that *is* available was taken instead: `ItemStatus`, `Orientation`, `RangeValueIsReadOnly` and `IsDataValidForForm` are read by no 2.3 vocabulary and are not requested (A15-4). **An earlier form of this row read "0.95x to 1.05x, no recovery in either direction"; that was min-only and overstated what had been measured** - A15-13's own lesson recurring inside the row that cites it |
|
||||
| A15-13 | `15-vocabulary/probe.rs` | n/a | api-contract | nothing - no probe in this corpus has previously reported a timing comparison from a single unrepeated sample, and A6-1 and A6-2 both report one | the first form of the A15-11 measurement took one sample per property set and produced a **non-monotonic** ordering in which adding twelve properties measured *faster* than omitting them, and in which a repeated identical baseline differed from itself by 2.18x. Seven repeats with a discarded warm-up, reported as min with the median and max beside it, is monotonic and reproducible | NEW-EDGE | recorded as a methodology row rather than a finding about UI Automation: a timing claim in this corpus needs repetition and a warm-up, and reporting min alongside its spread is what makes the claim checkable. The earlier single-sample form would have justified the split A15-12 rejects |
|
||||
|
||||
## Area 16 - the observation read path: window census, ARIA vocabulary, wrapper count, settle latency and the integrity boundary (sub-phase 2.4)
|
||||
|
||||
Added by sub-phase 2.4's unit U1. The UIA-side rows came from `16-observation/probe.rs`
|
||||
against the probe's own Win32 fixture, the WPF scratch window and the Chromium target
|
||||
(Obsidian); the Win32 rows came from `16-observation/census.ps1` (window enumeration,
|
||||
foreground, process sources, DPI, IVirtualDesktopManager). The Chromium pass ran **twice** -
|
||||
a first-contact pass and a settled pass - because the same A1-5 mechanism that makes a
|
||||
fresh client trigger access. Re-run either with
|
||||
`powershell -NoProfile -ExecutionPolicy Bypass -File .\16-observation\probe.ps1 -Label <devbox|ci>`.
|
||||
Captures are `16-observation/captures/observation-{win32,wpf,chromium-first-contact,chromium,split-integrity,census}-{devbox,ci}.json`.
|
||||
The Chromium-dependent rows are dev-box-only (Obsidian is not on the hosted runner); each
|
||||
affected row records the single-environment limitation.
|
||||
|
||||
| id | script | stack | scope | phases.md expectation | observed | verdict | action |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| A16-1 | `16-observation/census.ps1` | n/a | app/provider | 2.4's `list_windows` needs the top-level enumeration `EnumWindows` returns on a working desktop - cloaked UWP frames, tool windows, zero-size windows all pollute the agent-facing window set | 147 top-level windows enumerated; by factor 137 invisible, 93 zero-size, 8 iconic, 68 visible-non-empty, 6 cloaked (`DWMWA_CLOAKED`), 51 tool (`WS_EX_TOOLWINDOW`). The identifiable rows confirm the shape: `Shell_TrayWnd` is visible but tool, `Progman` is visible but tool, the `WorkerW` wall is invisible and zero-size. A visible-and-cloaked window was observed (the UWP closed-frame shape) | CONFIRMS | the filter U4 encodes is justified per criterion: `IsWindowVisible`, non-zero rect, non-cloaked, non-tool. Environment dependency recorded: one Server 2019 box with no modern shell (A10-7), so the cloaked fraction is a lower bound for a consumer desktop, and the modern-shell verification stays owned by 2.12 |
|
||||
| A16-2 | `16-observation/census.ps1` | n/a | app/provider | 2.4's `focused_window` must map `GetForegroundWindow`'s answer to the same identity `list_windows` reports | `GetForegroundWindow` reads non-zero with a valid owning process while the probe is idle, and the class observed tracks the desktop's foreground (here `Shell_TrayWnd` when the probe owns nothing). No `ApplicationFrameHost`-hosted target exists on this box to answer the frame-versus-CoreWindow question (A10-7: no modern shell population) | CONFIRMS | `focused_window` is the focused-only filter of `list_windows` as planned; whichever HWND shape a UWP host presents maps to the same identity because both go through the same window record. The frame-vs-CoreWindow measurement is written into the environment sub-phase's scope beside the modern-shell caveat |
|
||||
| A16-3 | `16-observation/census.ps1` | n/a | api-contract | 2.4's `list_apps` needs a process-enumeration source that yields name, image path and the creation-time token KTD3 defines; the pinned crates to date have neither ToolHelp nor EnumProcesses enabled | a Toolhelp32 snapshot (`CreateToolhelp32Snapshot` + `Process32FirstW/NextW`) enumerates 132 processes with a non-empty image name; `Get-CimInstance Win32_Process` agrees at 132; the probe process's own `StartTime` is present, so a creation-time-derived generation token is readable through the same process handle KTD3 needs. Both sources enumerate; ToolHelp gives the name in the same snapshot pass and needs the one feature (`Win32_System_Diagnostics_ToolHelp`) | CONFIRMS | `list_apps` derives app processes from the window inventory's owning processes joined to the ToolHelp snapshot, and the creation-time token is read via `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetProcessTimes`; U5 adds the ToolHelp feature |
|
||||
| A16-4 | `16-observation/census.ps1` | n/a | api-contract | 2.4's `list_displays` reads effective DPI, not the requested scale (A10-3), with `scale` = effective DPI / 96 | `GetDpiForMonitor` succeeds (error 0) on this box's single monitor with effective DPI 96 x 96 on both axes, `scale` 1.0; the monitor is primary. This reproduces A10-3's single-96-DPI-display environment exactly | CONFIRMS | U5 reads `GetDpiForMonitor` effective DPI and computes `scale` from it; the single-monitor evidence limit is stated in the display module doc citing A10-3, and multi-monitor verification is deferred to a later sub-phase on a rig that has one |
|
||||
| A16-5 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's `role_description` source is `LocalizedControlType`, which no probe has ever read (A15-9's deferral records that) | `LocalizedControlType` is populated on **every** control type across all three stacks: 9 rows on the Win32 fixture, 19 on WPF, and on settled Chromium 8 rows (`button`, `edit`, `graphic`/`image`, `text`, `group`, `document`, `pane`/`region`, `separator`). Zero failed reads anywhere; localized strings are short stable vocabulary (`button`, `edit`, `group`...), not target text | CONFIRMS | `role_description` is produced from `LocalizedControlType`'s display text, and `LocalizedControlType` is classified as non-target-bearing in `carries_target_text`; the role-map-key ban (roles.rs must not key on the localized string) still stands |
|
||||
| A16-6 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's `subrole` source is `AriaRole`; the KTD5 `dom_classes` branch asks whether `AriaProperties` or `ClassName` carries class tokens; and U3's secure arm for `AriaRole` asks whether arbitrary author text passes through verbatim | settled Chromium: `AriaRole` non-empty on **164 of 165** elements with short ARIA vocabulary tokens (lengths 3-11: `tab`, `button`, `treeitem`, `dialog`, etc.), and `AriaProperties` non-empty on 164 of 165 with length-83-to-147 formatted strings. The author-defined-token check reported **0** while a page-authored custom role was present in control content on this box's targets, and `AriaProperties` carries **no `class` token** (`carries_class_token: 0`). Native fixtures (Win32, WPF) report both empty throughout | CONFIRMS | `subrole` is produced from non-empty `AriaRole` claims. The KTD5 branch that fires is the **schema-only** one: no class-bearing source exists in the pinned stack, so `dom_classes` lands as core schema with no Windows producer and U10 corrects phases.md's `HtmlClass` naming. Because `AriaRole` is author-derived on web content and the verbatim question could not be fully separated from Obsidian's own sanitising pipeline on this box, U3's `carries_target_text` arm conservatively treats `AriaRole` as target-bearing and withholds it on secure fields, citing this row's single-environment limitation |
|
||||
| A16-7 | `16-observation/probe.rs` | uia3-com | api-contract | 2.4 must confirm the three new properties stay inside A15-11's per-property cost envelope or re-open the flat `WALK_SET` decision | min-of-seven with a discarded warm-up against a representative flat set, adding exactly `LocalizedControlType`, `AriaRole`, `AriaProperties`: **1.03x** on the 15-node Win32 fixture, **1.18x** on the 83-node WPF out-of-process tree, and **1.22x** on settled Chromium (1.30x at first contact). All well inside the 1.22x-to-1.98x envelope A15-11 measured for 2.3's whole expansion | CONFIRMS | `WALK_SET` stays flat; the cost figure lands in `property_ids.rs`'s module doc beside A15-11's, and no re-opening of the split decision is warranted |
|
||||
| A16-8 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's wrapper skip (KTD6) is only valuable if transparent wrappers actually consume logical depth on the Chromium target, and the census must state how many `Group`/`Custom` containers are truly empty | settled Chromium: **81** `Group`/`Custom` control-type nodes, of which **54** carry an empty name **and** empty value (the gated-skip population), 27 named. Depth arithmetic on the same settled tree: 165 raw nodes advance to 111 logical nodes when empty wrappers are skipped - `logical_over_raw` 0.67, i.e. the skip recovers roughly a third of the raw depth on the path to web content. The WPF fixture (83 descendants) shows the gate's restraint: almost no wrappers there, `logical_over_raw` 0.98 | CONFIRMS | the KTD6 gate ships as planned - the skip's value is stated per-tree (Obsidian: 54 empty wrappers) and the native stacks are untouched by it. U7's exit criterion cites this census; wrappers **are** the depth consumer on this target |
|
||||
| A16-9 | `16-observation/census.ps1` | n/a | api-contract | phases.md §2.4 names `IVirtualDesktopManager` among key APIs as a read-only diagnostic; the plan pre-commits that if it is unreachable through the pinned crates it is dropped from 2.4 and phases.md is corrected | the `VirtualDesktopManager` **CLSID constant exists** in `windows-sys` 0.61 (`Win32_UI_Shell`), but neither `windows-sys` nor `windows` generates the `IVirtualDesktopManager` **interface**. Reaching the interface requires a hand-declared COM declaration - a new dependency 2.4 does not take | CONFIRMS | item 9's pre-committed branch fires: `IVirtualDesktopManager` is dropped from 2.4; U10 corrects phases.md's key-API row to name the CLSID constant and record that the interface is not generated by the pinned crates |
|
||||
| A16-10 | `16-observation/probe.rs` | uia3-com | app/provider | 2.3's dogfood left the two ref-able arms unexercised - `DataItem` refined to `cell` and `Button` refined to `switch` (H59); U1 must make a stack advertise the patterns or record why | the fixture extension worked: the WPF `ToggleButton` advertises `ControlType.Button` + `IsTogglePatternAvailable` (`button_with_toggle: 1`), so the `switch` arm is constructible and U9 can observe it. The grid arm is subtler: WPF `DataGrid` cells carry `GridItem`/`TableItem` availability but present `ControlType.Custom` (50025), **not** `DataItem` (50029) - 4 elements carry the patterns, 0 with `ControlType.DataItem`; the WinForms `DataGridView` advertises neither pattern on its rows | NEW-EDGE | the `switch` arm lands in the dogfood set with a regression-testable fixture. The `cell` arm's exact shape - `DataItem` + GridItem/TableItem - is **not** reproduced by either grid: WPF exposes the patterns on `Custom`-typed cells (which the role map resolves to `Unknown` today) and WinForms exposes nothing. Recorded so U9's report and U10 decide whether the `cell` refinement should also fire on `Custom`+patterns or the arm stays as-designed; the fixture extension itself is kept for the `switch` arm and the WPF cells are a live test case for the WPF peer path |
|
||||
| A16-11 | `16-observation/probe.rs` | uia3-com | app/provider | 2.4's settle design (KTD7) rests on A1-5's 13.2x first-contact understatement and the plan pre-commits that if settle exceeds the hardcoded 3 s snapshot deadline, the snapshot command gains a `--timeout-ms` argument | first contact on a cold Obsidian launch is **12** descendants; the settled tree is **165** - a **13.75x** understatement reproducing A1-5's 13.2x. The settle took between 10 s and 25 s on cold launches (non-deterministic), which **exceeds the hardcoded 3 s snapshot deadline** in every measured run. Two stability-shape hazards were also observed directly: a covered Chromium window holds the shell indefinitely (A1-6 reproduced), and a fresh client connection is required to see the grown tree (the triggering client keeps serving the shell) | CONFIRMS | item 11's branch fires: this sub-phase adds `--timeout-ms` to the snapshot command, threaded into both the snapshot and drill-down deadlines, mirroring the ref-action pattern in `src/cli_args/`; the still-thin `platform_detail` names that flag rather than an abstract recommendation. The settle-then-remeasure observation shape (fresh client after a wait) is what U7's activation retry must reproduce |
|
||||
| A16-12 | `16-observation/probe.ps1` (split-integrity leg) | uia3-com | api-contract | U1 item 12's trust-boundary question: can a lowered-integrity client still read a root and the KTD3 process token of a window a higher-integrity process owns? | the same WPF window read twice - once at the probe's own integrity and once from a child forced to Medium (`S-1-16-8192` via `CreateProcessAsUser` with a Medium integrity label). **Both reads resolve the root and read the process-generation token**; the Medium client produced a complete capture. No `PERM_DENIED`, no denied token read | NEW-EDGE | the observation read and the token read are **not** integrity-gated on this box: a Medium client can observe a High-integrity window's root and token. Recorded as an environment fact (this box stage's High and Medium processes are the same user), not a product contract - the fail-closed design (KTD3's `process_instance: None` emission on a failed token read) still stands for splits this box cannot stage. The elevated-process denial branch did not fire here and remains written into the receiving environment sub-phase's scope |
|
||||
|
||||
## Session evidence (R6)
|
||||
|
||||
Rows already executed this session rather than produced by a probe script, carried as
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"Probe": "13-ledger-check",
|
||||
"CapturedAtUtc": "2026-08-01T20:39:31Z",
|
||||
"CapturedAtUtc": "2026-08-02T11:05:33Z",
|
||||
"Question": "does FINDINGS.md satisfy R5, R6 and R7 as a machine-checkable contract",
|
||||
"Ledger": "probes/windows/FINDINGS.md",
|
||||
"RowCount": 105,
|
||||
"RowCount": 117,
|
||||
"SessionEvidenceRows": 25,
|
||||
"AreaCoverage": [
|
||||
{
|
||||
|
|
@ -52,9 +52,9 @@
|
|||
}
|
||||
],
|
||||
"VerdictCounts": {
|
||||
"CONFIRMS": 52,
|
||||
"CONFIRMS": 62,
|
||||
"CONTRADICTS": 8,
|
||||
"NEW-EDGE": 40,
|
||||
"NEW-EDGE": 42,
|
||||
"DEFERRED": 5
|
||||
},
|
||||
"DeferredRows": [
|
||||
|
|
|
|||
|
|
@ -98,14 +98,18 @@ $xaml = @'
|
|||
<TabItem x:Name="tabBravo" Header="Tab-Bravo" AutomationProperties.AutomationId="tabBravo"/>
|
||||
<TabItem x:Name="tabCharlie" Header="Tab-Charlie" AutomationProperties.AutomationId="tabCharlie"/>
|
||||
</TabControl>
|
||||
<DataGrid x:Name="dgvRows" Grid.Row="10" Margin="0,4"
|
||||
AutomationProperties.AutomationId="dgvRows"
|
||||
AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Column-Label" Binding="{Binding Label}"/>
|
||||
<DataGridTextColumn Header="Column-Value" Binding="{Binding Value}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<StackPanel Grid.Row="10" Orientation="Horizontal" Margin="0,4">
|
||||
<ToggleButton x:Name="btnToggle" AutomationProperties.AutomationId="btnToggle"
|
||||
Content="Toggle Mode" Width="120" Height="26" Margin="0,0,8,0"/>
|
||||
<DataGrid x:Name="dgvRows" Margin="0,0,0,0" Width="380" Height="120"
|
||||
AutomationProperties.AutomationId="dgvRows"
|
||||
AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Column-Label" Binding="{Binding Label}"/>
|
||||
<DataGridTextColumn Header="Column-Value" Binding="{Binding Value}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
'@
|
||||
|
|
|
|||
Loading…
Reference in a new issue