dsh-codex-style-computer-use 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -10
- package/README.zh.md +26 -9
- package/lib/audit.js +7 -0
- package/lib/batch.js +14 -19
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/compact-feedback.js +175 -0
- package/lib/continuation.js +1 -1
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +372 -136
- package/lib/core/native-runtime.js +17 -0
- package/lib/core/worker.cs +1779 -218
- package/lib/feedback-routes.json +11 -0
- package/lib/feedback.js +49 -16
- package/lib/image-output.js +59 -11
- package/lib/index.js +25 -7
- package/lib/key-command.js +37 -0
- package/lib/media-contract.js +43 -0
- package/lib/observations.js +267 -13
- package/lib/policy.js +7 -7
- package/lib/tool-contract.js +56 -0
- package/lib/tools.js +134 -49
- package/lib/verification-region.js +17 -0
- package/mcp/package.json +2 -1
- package/mcp/src/execution.js +8 -5
- package/mcp/src/index.js +16 -267
- package/mcp/src/tools.js +271 -0
- package/package.json +2 -1
- package/scripts/check-behavior.mjs +83 -0
- package/scripts/measure-tool-tokens.mjs +21 -0
- package/skills/computer-use/SKILL.md +20 -8
- package/skills/computer-use/references/lifecycle.md +11 -7
- package/skills/computer-use/references/maintenance.md +9 -5
- package/skills/computer-use/references/precision.md +39 -14
- package/skills/computer-use/references/progressive-control.md +10 -4
package/lib/core/worker.cs
CHANGED
|
@@ -20,6 +20,7 @@ using System.Collections.Generic;
|
|
|
20
20
|
using System.Collections.Concurrent;
|
|
21
21
|
using System.Diagnostics;
|
|
22
22
|
using System.Drawing;
|
|
23
|
+
using System.Drawing.Drawing2D;
|
|
23
24
|
using System.Drawing.Imaging;
|
|
24
25
|
using System.IO;
|
|
25
26
|
using System.Linq;
|
|
@@ -36,19 +37,34 @@ using Windows.Graphics.Imaging;
|
|
|
36
37
|
|
|
37
38
|
namespace DshComputerUse.Worker
|
|
38
39
|
{
|
|
39
|
-
//
|
|
40
|
+
// On-demand cadence: one monotonic timestamp, no timer, history list or disk writes.
|
|
41
|
+
// Only a successfully displayed cue advances the sliding three-minute window.
|
|
42
|
+
public sealed class PreparationCadence
|
|
43
|
+
{
|
|
44
|
+
public const int FullMs = 1000, RepeatMs = 100, RepeatWindowMs = 180000;
|
|
45
|
+
long lastShown = -1;
|
|
46
|
+
public int Duration(long now)
|
|
47
|
+
{ long previous = Interlocked.Read(ref lastShown); return previous >= 0 && now >= previous && now - previous < RepeatWindowMs ? RepeatMs : FullMs; }
|
|
48
|
+
public void Presented(long now) { Interlocked.Exchange(ref lastShown, now); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A live cycle observes physical holds; actuation or an explicit scene hold can start a handoff.
|
|
40
52
|
// Once yielded, three quiet seconds with no held input returns control to the agent.
|
|
41
53
|
public sealed class HumanInputState
|
|
42
54
|
{
|
|
43
55
|
public const int SustainedMs = 2000, IdleMs = 3000;
|
|
44
56
|
readonly object sync = new object();
|
|
45
57
|
readonly HashSet<int> held = new HashSet<int>();
|
|
58
|
+
readonly Dictionary<int, int> releasedSamples = new Dictionary<int, int>();
|
|
59
|
+
readonly HashSet<int> injectedTouched = new HashSet<int>();
|
|
46
60
|
bool enabled, acting, seen;
|
|
47
61
|
string phase = "idle", lastKind = "none";
|
|
48
|
-
long first, last, revision, burst;
|
|
62
|
+
long first, last, revision, burst, stateSeq;
|
|
63
|
+
int reconciledReleases;
|
|
49
64
|
public string Phase { get { lock (sync) { return phase; } } }
|
|
50
65
|
public long Revision { get { lock (sync) { return revision; } } }
|
|
51
66
|
public bool Active { get { lock (sync) { return phase != "idle"; } } }
|
|
67
|
+
public bool HasHeld { get { lock (sync) { return held.Count != 0; } } }
|
|
52
68
|
public string LastKind { get { lock (sync) { return lastKind; } } }
|
|
53
69
|
public bool Held(int vk)
|
|
54
70
|
{
|
|
@@ -69,7 +85,7 @@ namespace DshComputerUse.Worker
|
|
|
69
85
|
lock (sync)
|
|
70
86
|
{
|
|
71
87
|
if (enabled == on) return;
|
|
72
|
-
enabled = on;
|
|
88
|
+
enabled = on; stateSeq++;
|
|
73
89
|
if (!on) { phase = "idle"; acting = false; }
|
|
74
90
|
}
|
|
75
91
|
}
|
|
@@ -77,30 +93,30 @@ namespace DshComputerUse.Worker
|
|
|
77
93
|
{
|
|
78
94
|
lock (sync)
|
|
79
95
|
{
|
|
80
|
-
acting = on;
|
|
96
|
+
if (acting != on) { acting = on; stateSeq++; }
|
|
81
97
|
// Do not replay earlier ordinary activity. A key/button still physically
|
|
82
98
|
// held does need ownership before we can inject a conflicting chord.
|
|
83
99
|
if (on && enabled && phase == "idle" && held.Count > 0)
|
|
84
|
-
{ last = now; revision++; Start(now); }
|
|
100
|
+
{ last = now; revision++; stateSeq++; Start(now); }
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
|
|
88
104
|
public void Record(int key, bool down, bool up, long now) { Record(key, down, up, now, false); }
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
/// 2026-11-04: while a cycle is live, the human reaching for the mouse is a takeover no
|
|
92
|
-
/// matter whether an injection happens to be in flight at that instant — the old gate
|
|
93
|
-
/// (`idle && !acting`) only lit the yield during the few dozen milliseconds of each
|
|
94
|
-
/// click, so an interruption in the gaps showed no light at all.
|
|
105
|
+
/// Pointer jitter is filtered before this call. The protection flag includes both
|
|
106
|
+
/// actual injection and a retained scene hold; free observation never takes ownership.
|
|
95
107
|
public void Record(int key, bool down, bool up, long now, bool deliberate)
|
|
96
108
|
{
|
|
97
109
|
lock (sync)
|
|
98
110
|
{
|
|
99
111
|
bool continued = held.Count > 0 || (seen && now - last < IdleMs);
|
|
112
|
+
stateSeq++;
|
|
100
113
|
lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
|
|
101
|
-
if (key != 0) {
|
|
114
|
+
if (key != 0) {
|
|
115
|
+
if (down) held.Add(key); if (up) held.Remove(key);
|
|
116
|
+
releasedSamples.Remove(key); injectedTouched.Remove(key);
|
|
117
|
+
}
|
|
102
118
|
seen = true; last = now;
|
|
103
|
-
if (!enabled || (phase == "idle" && !acting
|
|
119
|
+
if (!enabled || (phase == "idle" && !acting)) return;
|
|
104
120
|
revision++;
|
|
105
121
|
if (phase == "idle" || !continued) Start(now);
|
|
106
122
|
if (now - first >= SustainedMs) phase = "waiting";
|
|
@@ -111,19 +127,77 @@ namespace DshComputerUse.Worker
|
|
|
111
127
|
lock (sync)
|
|
112
128
|
{
|
|
113
129
|
if (!enabled || phase == "idle") return;
|
|
114
|
-
if (held.Count == 0 && now - last >= IdleMs) { phase = "idle"; return; }
|
|
115
|
-
if (held.Count > 0 && now - first >= SustainedMs) phase = "waiting";
|
|
130
|
+
if (held.Count == 0 && now - last >= IdleMs) { phase = "idle"; stateSeq++; return; }
|
|
131
|
+
if (held.Count > 0 && now - first >= SustainedMs && phase != "waiting") { phase = "waiting"; stateSeq++; }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
public void NoteInjected(int key)
|
|
135
|
+
{
|
|
136
|
+
// An injected UP can make async state look released while a human
|
|
137
|
+
// still physically holds the same key. Never reconcile that case.
|
|
138
|
+
lock (sync) {
|
|
139
|
+
foreach (int tracked in held)
|
|
140
|
+
if (tracked == key || ModifierFamily(tracked) != 0 && ModifierFamily(tracked) == ModifierFamily(key) ||
|
|
141
|
+
(tracked == 1 || tracked == 2) && (key == 1 || key == 2))
|
|
142
|
+
{ injectedTouched.Add(tracked); releasedSamples.Remove(tracked); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
static int ModifierFamily(int key)
|
|
146
|
+
{ return key == 0x10 || key == 0xA0 || key == 0xA1 ? 0x10 : key == 0x11 || key == 0xA2 || key == 0xA3 ? 0x11 : key == 0x12 || key == 0xA4 || key == 0xA5 ? 0x12 : 0; }
|
|
147
|
+
public static bool AsyncButtonMayBeDown(int key, bool swapped, Func<int, bool> isDown)
|
|
148
|
+
{
|
|
149
|
+
// Async state names PHYSICAL buttons. With swapped primary/secondary
|
|
150
|
+
// semantics, require BOTH physical buttons up before repairing either
|
|
151
|
+
// history entry; never infer the low-level hook's mapping from a UI label.
|
|
152
|
+
return swapped && (key == 1 || key == 2) ? isDown(1) || isDown(2) : isDown(key);
|
|
153
|
+
}
|
|
154
|
+
public int ReconcileReleased(Func<int, bool> isDown, bool trusted, long now)
|
|
155
|
+
{
|
|
156
|
+
lock (sync) {
|
|
157
|
+
if (!trusted) { releasedSamples.Clear(); return 0; }
|
|
158
|
+
int removed = 0;
|
|
159
|
+
foreach (int key in held.ToArray()) {
|
|
160
|
+
if (injectedTouched.Contains(key) || isDown(key)) { releasedSamples.Remove(key); continue; }
|
|
161
|
+
int count; releasedSamples.TryGetValue(key, out count);
|
|
162
|
+
if (++count < 3) { releasedSamples[key] = count; continue; }
|
|
163
|
+
held.Remove(key); releasedSamples.Remove(key); removed++;
|
|
164
|
+
}
|
|
165
|
+
if (removed > 0) {
|
|
166
|
+
// This is fresh release evidence, not permission from elapsed
|
|
167
|
+
// time: still require the full quiet interval after this point.
|
|
168
|
+
last = now; seen = true; lastKind = "reconciled-release";
|
|
169
|
+
reconciledReleases += removed; revision++; stateSeq++;
|
|
170
|
+
}
|
|
171
|
+
return removed;
|
|
116
172
|
}
|
|
117
173
|
}
|
|
118
174
|
public object Snapshot(long now)
|
|
119
175
|
{
|
|
120
176
|
lock (sync) return Program.Dict("phase", phase, "active", phase != "idle",
|
|
121
|
-
"revision", revision, "burst", burst, "heldCount", held.Count, "lastKind", lastKind,
|
|
177
|
+
"revision", revision, "stateSeq", stateSeq, "burst", burst, "heldCount", held.Count, "lastKind", lastKind,
|
|
178
|
+
"reconciledReleaseCount", reconciledReleases,
|
|
122
179
|
"quietRemainingMs", phase == "idle" ? 0 : held.Count > 0 ? IdleMs : Math.Max(0, IdleMs - (now - last)),
|
|
123
180
|
"sustainedMs", SustainedMs, "idleMs", IdleMs);
|
|
124
181
|
}
|
|
125
182
|
}
|
|
126
183
|
|
|
184
|
+
// Domain ownership survives renderer failures and visibility changes. Glow
|
|
185
|
+
// only reads this state; cycle/operation lifecycle explicitly changes it.
|
|
186
|
+
public static class SceneControl
|
|
187
|
+
{
|
|
188
|
+
static volatile bool acting, holding;
|
|
189
|
+
public static bool Acting { get { return acting; } }
|
|
190
|
+
public static bool Holding { get { return holding; } }
|
|
191
|
+
public static void SetHolding(bool on)
|
|
192
|
+
{
|
|
193
|
+
holding = on && Panic.CycleLit && !Panic.Exited && !Panic.Stopped && !Panic.Engaged;
|
|
194
|
+
Panic.UpdateInputProtection();
|
|
195
|
+
}
|
|
196
|
+
public static void BeginActuation() { acting = true; SetHolding(true); }
|
|
197
|
+
public static void EndActuation() { acting = false; Panic.UpdateInputProtection(); }
|
|
198
|
+
public static void Reset() { acting = holding = false; Panic.UpdateInputProtection(); }
|
|
199
|
+
}
|
|
200
|
+
|
|
127
201
|
// The UAC secure desktop (and the lock screen) is the ONE surface automation must never
|
|
128
202
|
// reach — Windows enforces that itself. The honest addition (human request 2026-11-04)
|
|
129
203
|
// is to NOTICE the switch, refuse to dispatch while it stands, tell the human exactly
|
|
@@ -447,6 +521,10 @@ namespace DshComputerUse.Worker
|
|
|
447
521
|
public static extern bool OpenProcessToken(IntPtr proc, uint access, out IntPtr token);
|
|
448
522
|
[DllImport("advapi32.dll", SetLastError = true)]
|
|
449
523
|
public static extern bool GetTokenInformation(IntPtr token, int cls, out uint info, uint len, out uint ret);
|
|
524
|
+
[DllImport("advapi32.dll", EntryPoint = "GetTokenInformation", SetLastError = true)]
|
|
525
|
+
public static extern bool GetTokenInformationBuffer(IntPtr token, int cls, IntPtr info, uint len, out uint ret);
|
|
526
|
+
[DllImport("advapi32.dll")] public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
|
|
527
|
+
[DllImport("advapi32.dll")] public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index);
|
|
450
528
|
public const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000, TOKEN_QUERY = 0x0008;
|
|
451
529
|
public const int TokenElevation = 20;
|
|
452
530
|
[DllImport("user32.dll")]
|
|
@@ -473,6 +551,8 @@ namespace DshComputerUse.Worker
|
|
|
473
551
|
public static extern IntPtr CreateDIBSection(IntPtr dc, ref BITMAPINFO bmi, uint usage, out IntPtr bits, IntPtr hSection, uint offset);
|
|
474
552
|
[DllImport("user32.dll")]
|
|
475
553
|
public static extern bool GetCursorInfo(ref CURSORINFO pci);
|
|
554
|
+
[DllImport("user32.dll", EntryPoint = "LoadCursorW")]
|
|
555
|
+
public static extern IntPtr LoadCursor(IntPtr instance, IntPtr resource);
|
|
476
556
|
[DllImport("user32.dll")]
|
|
477
557
|
public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyHeight, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, uint diFlags);
|
|
478
558
|
[DllImport("user32.dll")]
|
|
@@ -484,6 +564,10 @@ namespace DshComputerUse.Worker
|
|
|
484
564
|
public static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
|
|
485
565
|
[DllImport("user32.dll")]
|
|
486
566
|
public static extern bool CloseDesktop(IntPtr h);
|
|
567
|
+
[DllImport("user32.dll")]
|
|
568
|
+
public static extern IntPtr GetThreadDesktop(uint threadId);
|
|
569
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
570
|
+
public static extern bool GetUserObjectInformation(IntPtr obj, int index, StringBuilder info, int length, out int needed);
|
|
487
571
|
|
|
488
572
|
// ---- low-level keyboard hook: the human's emergency brake ----
|
|
489
573
|
public delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
|
|
@@ -572,7 +656,6 @@ namespace DshComputerUse.Worker
|
|
|
572
656
|
[DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
|
|
573
657
|
|
|
574
658
|
static Thread _thread;
|
|
575
|
-
static volatile bool _acting;
|
|
576
659
|
static volatile bool _preparing;
|
|
577
660
|
static volatile int _preparationStart;
|
|
578
661
|
static int _preparationSerial, _presentedPreparation, _completedPreparation;
|
|
@@ -595,9 +678,12 @@ namespace DshComputerUse.Worker
|
|
|
595
678
|
"lastSuccessAgeMs", Volatile.Read(ref _presentSuccesses) == 0 ? (object)null : unchecked((uint)(now - _lastPresentTick)),
|
|
596
679
|
"lastFailedSerial", Volatile.Read(ref _lastFailedSerial), "lastFailedWant", Volatile.Read(ref _lastFailedWant),
|
|
597
680
|
"lastWin32Error", Volatile.Read(ref _lastFailedSerial) < 0 ? (object)null : _lastFailedError,
|
|
598
|
-
"rebuiltSerial", Volatile.Read(ref _rebuiltSerial));
|
|
681
|
+
"rebuiltSerial", Volatile.Read(ref _rebuiltSerial), "pointerFault", AgentPointer.Fault);
|
|
599
682
|
}
|
|
600
|
-
|
|
683
|
+
static readonly Stopwatch PreparationClock = Stopwatch.StartNew();
|
|
684
|
+
static readonly PreparationCadence PreparationFrequency = new PreparationCadence();
|
|
685
|
+
static volatile int _preparationMs = PreparationCadence.FullMs;
|
|
686
|
+
public static int PreparationMs { get { return _preparationMs; } }
|
|
601
687
|
static volatile bool _enabled = true;
|
|
602
688
|
static volatile string _label = "AI 正在操控此电脑 · 监听中";
|
|
603
689
|
|
|
@@ -605,22 +691,36 @@ namespace DshComputerUse.Worker
|
|
|
605
691
|
public static void Touch(int holdMs)
|
|
606
692
|
{
|
|
607
693
|
if (!_enabled) return;
|
|
694
|
+
_sceneVisible = true;
|
|
608
695
|
_thinking = true;
|
|
609
696
|
EnsureStarted();
|
|
610
697
|
}
|
|
611
|
-
|
|
612
|
-
|
|
698
|
+
// Renderer visibility is deliberately separate from scene ownership.
|
|
699
|
+
// Hiding the indicator must neither release the scene nor resurrect it
|
|
700
|
+
// on the next frame solely because the authoritative hold is still true.
|
|
701
|
+
static volatile bool _sceneVisible = true;
|
|
702
|
+
public static void Off() { _sceneVisible = false; _preparing = false; _thinking = false; }
|
|
703
|
+
public static bool Acting { get { return SceneControl.Acting; } }
|
|
704
|
+
public static bool Holding { get { return SceneControl.Holding; } }
|
|
613
705
|
public static bool Preparing { get { return _preparing; } }
|
|
614
706
|
public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
|
|
615
707
|
public static bool PreparationComplete { get { return _preparing && Volatile.Read(ref _completedPreparation) == Volatile.Read(ref _preparationSerial); } }
|
|
616
|
-
public static void ActuationBegin() {
|
|
617
|
-
public static void ActuationEnd() {
|
|
708
|
+
public static void ActuationBegin() { _sceneVisible = true; EnsureStarted(); }
|
|
709
|
+
public static void ActuationEnd() { }
|
|
618
710
|
public static void Prepare(bool on)
|
|
619
711
|
{
|
|
620
712
|
if (!on) { _preparing = false; return; }
|
|
621
713
|
_preparationStart = 0;
|
|
714
|
+
_preparationMs = PreparationFrequency.Duration(PreparationClock.ElapsedMilliseconds);
|
|
622
715
|
Interlocked.Increment(ref _preparationSerial);
|
|
623
|
-
_preparing = true; _enabled = true; EnsureStarted();
|
|
716
|
+
_preparing = true; _enabled = true; _sceneVisible = true; EnsureStarted();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
public static void PreparationPresented(int serial)
|
|
720
|
+
{
|
|
721
|
+
_preparationStart = Environment.TickCount;
|
|
722
|
+
PreparationFrequency.Presented(PreparationClock.ElapsedMilliseconds);
|
|
723
|
+
Volatile.Write(ref _presentedPreparation, serial);
|
|
624
724
|
}
|
|
625
725
|
|
|
626
726
|
// Nested injection sequences stay observable across drag/key/typing gaps.
|
|
@@ -629,7 +729,7 @@ namespace DshComputerUse.Worker
|
|
|
629
729
|
public static void InputBegin() { Interlocked.Increment(ref _injecting); }
|
|
630
730
|
public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
|
|
631
731
|
public static bool Injecting { get { return _injecting > 0; } }
|
|
632
|
-
public static void KeepActing() { /*
|
|
732
|
+
public static void KeepActing() { /* Solid blue follows actual input; the separate scene hold survives it. */ }
|
|
633
733
|
|
|
634
734
|
// Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
|
|
635
735
|
// during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
|
|
@@ -637,7 +737,7 @@ namespace DshComputerUse.Worker
|
|
|
637
737
|
static volatile bool _killNow = false;
|
|
638
738
|
public static void Kill()
|
|
639
739
|
{
|
|
640
|
-
|
|
740
|
+
_sceneVisible = false; _preparing = false; _thinking = false; _flashStart = 0;
|
|
641
741
|
_killNow = true;
|
|
642
742
|
}
|
|
643
743
|
|
|
@@ -653,6 +753,7 @@ namespace DshComputerUse.Worker
|
|
|
653
753
|
public static void Thinking(int holdMs)
|
|
654
754
|
{
|
|
655
755
|
if (!_enabled) return;
|
|
756
|
+
_sceneVisible = true;
|
|
656
757
|
_thinking = true;
|
|
657
758
|
EnsureStarted();
|
|
658
759
|
}
|
|
@@ -689,7 +790,8 @@ namespace DshComputerUse.Worker
|
|
|
689
790
|
public static int DeriveWant(bool flashOn, int committed)
|
|
690
791
|
{
|
|
691
792
|
if (Panic.Exited) return 0;
|
|
692
|
-
|
|
793
|
+
bool sceneVisible = _enabled && _sceneVisible;
|
|
794
|
+
return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting && sceneVisible ? 1 : (Holding && sceneVisible ? 8 : (flashOn ? 2 : committed))))));
|
|
693
795
|
}
|
|
694
796
|
/// The title states the same fact as the light, derived the same way (an ended session must
|
|
695
797
|
/// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
|
|
@@ -697,7 +799,7 @@ namespace DshComputerUse.Worker
|
|
|
697
799
|
{
|
|
698
800
|
if (Panic.Exited) return "AI 已停止操控 · 未监听";
|
|
699
801
|
return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
|
|
700
|
-
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 ·
|
|
802
|
+
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · " + (PreparationMs / 1000.0).ToString("0.0") + " 秒准备" : Acting ? "AI 正在操作 · 请暂勿移动键鼠" : Holding ? "AI 正在检查 · 请保持页面与鼠标位置" : ThinkingActive ? "可自由使用键鼠 · AI 未占用现场" : label);
|
|
701
803
|
}
|
|
702
804
|
static volatile string _shownTitle = null;
|
|
703
805
|
// What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
|
|
@@ -763,7 +865,7 @@ namespace DshComputerUse.Worker
|
|
|
763
865
|
|
|
764
866
|
/// Force the overlay into existence. Used when a stop is ADOPTED from another process:
|
|
765
867
|
/// the red border must be visible even though this process never actuated anything.
|
|
766
|
-
public static void Reveal() { _enabled = true; EnsureStarted(); }
|
|
868
|
+
public static void Reveal() { _enabled = true; _sceneVisible = true; EnsureStarted(); }
|
|
767
869
|
|
|
768
870
|
static void EnsureStarted()
|
|
769
871
|
{
|
|
@@ -853,6 +955,7 @@ namespace DshComputerUse.Worker
|
|
|
853
955
|
}
|
|
854
956
|
finally
|
|
855
957
|
{
|
|
958
|
+
AgentPointer.Dispose();
|
|
856
959
|
_hwnd = IntPtr.Zero; _shownTitle = null;
|
|
857
960
|
if (oldBmp != IntPtr.Zero && oldBmp != new IntPtr(-1)) SelectObject(memDc, oldBmp);
|
|
858
961
|
if (dib != IntPtr.Zero) DeleteObject(dib);
|
|
@@ -880,9 +983,9 @@ namespace DshComputerUse.Worker
|
|
|
880
983
|
_lastLoopTick = Environment.TickCount;
|
|
881
984
|
_rendererStage = "rendering";
|
|
882
985
|
Interlocked.Increment(ref _frames);
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
bool acting = _enabled && Acting;
|
|
986
|
+
// Solid blue is actual input; breathing blue retains the scene between
|
|
987
|
+
// steps. Cyan is reserved for an explicitly free desktop.
|
|
988
|
+
bool acting = _enabled && _sceneVisible && Acting;
|
|
886
989
|
bool thinking = ThinkingActive;
|
|
887
990
|
bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
|
|
888
991
|
|
|
@@ -893,6 +996,7 @@ namespace DshComputerUse.Worker
|
|
|
893
996
|
int preparationSerial = Volatile.Read(ref _preparationSerial);
|
|
894
997
|
int want = DeriveWant(flashOn, committed);
|
|
895
998
|
_want = want; // publish it: the state the light is showing, as a fact
|
|
999
|
+
AgentPointer.Update(want == 1 || want == 8, want == 8);
|
|
896
1000
|
|
|
897
1001
|
// Keep the overlay's WINDOW TITLE in sync with the state, from the render loop.
|
|
898
1002
|
// When the title was only written by Stopped(), a process that INHERITED a stop
|
|
@@ -912,6 +1016,7 @@ namespace DshComputerUse.Worker
|
|
|
912
1016
|
if (_killNow)
|
|
913
1017
|
{
|
|
914
1018
|
_killNow = false;
|
|
1019
|
+
AgentPointer.Hide();
|
|
915
1020
|
alpha = 0;
|
|
916
1021
|
if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
|
|
917
1022
|
curBitmap = 0; committed = 0; paintedFlash = 0;
|
|
@@ -921,7 +1026,7 @@ namespace DshComputerUse.Worker
|
|
|
921
1026
|
}
|
|
922
1027
|
// want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
|
|
923
1028
|
// 4 emergency-stopped (RED, steady — the session is dead until re-armed)
|
|
924
|
-
int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
1029
|
+
int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : ((want == 1 || want == 8) ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
925
1030
|
// A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
|
|
926
1031
|
//
|
|
927
1032
|
// The present below used to be gated on `diff != 0` alone, while the flash snaps
|
|
@@ -961,6 +1066,7 @@ namespace DshComputerUse.Worker
|
|
|
961
1066
|
flashFrame = true;
|
|
962
1067
|
}
|
|
963
1068
|
else if (want == 1) { target = FULL; breathFrame = true; }
|
|
1069
|
+
else if (want == 8) { target = HoldingAlpha(Environment.TickCount); breathFrame = true; }
|
|
964
1070
|
else if (want == 7) { target = 255; breathFrame = true; }
|
|
965
1071
|
else if (want == 4) target = FULL; // steady red, no breathing
|
|
966
1072
|
else if (want == 6)
|
|
@@ -1051,8 +1157,7 @@ namespace DshComputerUse.Worker
|
|
|
1051
1157
|
}
|
|
1052
1158
|
if (presented && want == 7 && !PreparationVisible)
|
|
1053
1159
|
{
|
|
1054
|
-
|
|
1055
|
-
Volatile.Write(ref _presentedPreparation, preparationSerial);
|
|
1160
|
+
PreparationPresented(preparationSerial);
|
|
1056
1161
|
}
|
|
1057
1162
|
if (presented && want == 7 && preparationElapsed >= PreparationMs)
|
|
1058
1163
|
Volatile.Write(ref _completedPreparation, preparationSerial);
|
|
@@ -1154,6 +1259,9 @@ namespace DshComputerUse.Worker
|
|
|
1154
1259
|
catch { /* display preference only; never changes control or capture behavior */ }
|
|
1155
1260
|
}
|
|
1156
1261
|
|
|
1262
|
+
public static int HoldingAlpha(int tick)
|
|
1263
|
+
{ return 120 + (int)Math.Round(42 * (1 - Math.Cos((unchecked((uint)tick) % 2500) * 2 * Math.PI / 2500.0))); }
|
|
1264
|
+
|
|
1157
1265
|
static void PaintBlue(IntPtr bits, int w, int h, bool intense)
|
|
1158
1266
|
{
|
|
1159
1267
|
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
@@ -1449,6 +1557,7 @@ namespace DshComputerUse.Worker
|
|
|
1449
1557
|
CycleLit = false; // R2: the cycle is dead and must not be revived by anyone
|
|
1450
1558
|
_controlsReleased = true;
|
|
1451
1559
|
Disarm(); // ... and its monitors die WITH it, not at the next turn
|
|
1560
|
+
SceneControl.Reset();
|
|
1452
1561
|
try { Glow.Kill(); } catch { }
|
|
1453
1562
|
try { Glow.Ask(false); } catch { }
|
|
1454
1563
|
try { Glow.Stopped(false); } catch { }
|
|
@@ -1479,7 +1588,8 @@ namespace DshComputerUse.Worker
|
|
|
1479
1588
|
// is what made the first signed op hide a brake the human had already set.
|
|
1480
1589
|
if (_engaged || _stopped)
|
|
1481
1590
|
{
|
|
1482
|
-
|
|
1591
|
+
SceneControl.Reset();
|
|
1592
|
+
try { Glow.Kill(); } catch { }
|
|
1483
1593
|
try { Glow.Stopped(true); } catch { }
|
|
1484
1594
|
// START the overlay too: `Thinking()` was the only thing that ever did, so a
|
|
1485
1595
|
// cycle whose FIRST call arrives with a brake already standing painted nothing at
|
|
@@ -1902,7 +2012,8 @@ namespace DshComputerUse.Worker
|
|
|
1902
2012
|
bool showRed = !_exited && CycleLit;
|
|
1903
2013
|
if (showRed)
|
|
1904
2014
|
{
|
|
1905
|
-
|
|
2015
|
+
SceneControl.Reset();
|
|
2016
|
+
try { Glow.Kill(); } catch { }
|
|
1906
2017
|
try { Glow.Stopped(true); } catch { }
|
|
1907
2018
|
try { Glow.Reveal(); } catch { }
|
|
1908
2019
|
}
|
|
@@ -2053,6 +2164,12 @@ namespace DshComputerUse.Worker
|
|
|
2053
2164
|
if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
|
|
2054
2165
|
bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
|
|
2055
2166
|
int message = wParam.ToInt32();
|
|
2167
|
+
if (!physical) {
|
|
2168
|
+
Interlocked.Exchange(ref _lastInjectedInputAt, HumanClock.ElapsedMilliseconds);
|
|
2169
|
+
int injectedKey = message == 0x201 || message == 0x202 ? 1 : message == 0x204 || message == 0x205 ? 2 :
|
|
2170
|
+
message == 0x207 || message == 0x208 ? 4 : message == 0x20B || message == 0x20C ? (((m.mouseData >> 16) & 0xFFFF) == 1 ? 5 : 6) : 0;
|
|
2171
|
+
if (injectedKey != 0) Handoff.NoteInjected(injectedKey);
|
|
2172
|
+
}
|
|
2056
2173
|
// Significant idle motion invalidates observations, but does not yield.
|
|
2057
2174
|
bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
|
|
2058
2175
|
true, Handoff.Active, HumanClock.ElapsedMilliseconds);
|
|
@@ -2094,6 +2211,10 @@ namespace DshComputerUse.Worker
|
|
|
2094
2211
|
bool down = m == Native.WM_KEYDOWN_ || m == Native.WM_SYSKEYDOWN_;
|
|
2095
2212
|
bool up = m == Native.WM_KEYUP_ || m == Native.WM_SYSKEYUP_;
|
|
2096
2213
|
bool human = ClassifyAndTrackKey(k.vkCode, down, up, k.dwExtraInfo, k.flags);
|
|
2214
|
+
if (!human && (down || up)) {
|
|
2215
|
+
Interlocked.Exchange(ref _lastInjectedInputAt, HumanClock.ElapsedMilliseconds);
|
|
2216
|
+
Handoff.NoteInjected((int)k.vkCode);
|
|
2217
|
+
}
|
|
2097
2218
|
if (human && (down || up))
|
|
2098
2219
|
{
|
|
2099
2220
|
RecordHumanInput((int)k.vkCode, down, up);
|
|
@@ -2283,6 +2404,7 @@ namespace DshComputerUse.Worker
|
|
|
2283
2404
|
public static bool Armed { get { return _armed; } }
|
|
2284
2405
|
public static bool Exited { get { return _exited; } }
|
|
2285
2406
|
static readonly Stopwatch HumanClock = Stopwatch.StartNew();
|
|
2407
|
+
static long _lastInjectedInputAt;
|
|
2286
2408
|
static readonly HumanInputState Handoff = new HumanInputState();
|
|
2287
2409
|
static readonly object HandoffPublishLock = new object();
|
|
2288
2410
|
static string _publishedHandoff = "idle";
|
|
@@ -2296,6 +2418,7 @@ namespace DshComputerUse.Worker
|
|
|
2296
2418
|
{
|
|
2297
2419
|
var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
|
|
2298
2420
|
state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
|
|
2421
|
+
state["workerGeneration"] = Program.WorkerGeneration;
|
|
2299
2422
|
return state;
|
|
2300
2423
|
}
|
|
2301
2424
|
static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
|
|
@@ -2308,6 +2431,18 @@ namespace DshComputerUse.Worker
|
|
|
2308
2431
|
if (desktop != IntPtr.Zero) { Native.CloseDesktop(desktop); return true; }
|
|
2309
2432
|
return false;
|
|
2310
2433
|
}
|
|
2434
|
+
static bool OnNormalInputDesktop()
|
|
2435
|
+
{
|
|
2436
|
+
// Async state may fail when the caller lacks desktop hook access.
|
|
2437
|
+
IntPtr desktop = Native.OpenInputDesktop(0, false, 0x0001 | 0x0008);
|
|
2438
|
+
if (desktop == IntPtr.Zero) return false;
|
|
2439
|
+
try {
|
|
2440
|
+
var current = new StringBuilder(256); var input = new StringBuilder(256); int needed;
|
|
2441
|
+
return Native.GetUserObjectInformation(Native.GetThreadDesktop(Native.GetCurrentThreadId()), 2, current, 512, out needed) &&
|
|
2442
|
+
Native.GetUserObjectInformation(desktop, 2, input, 512, out needed) &&
|
|
2443
|
+
String.Equals(current.ToString(), input.ToString(), StringComparison.OrdinalIgnoreCase);
|
|
2444
|
+
} finally { Native.CloseDesktop(desktop); }
|
|
2445
|
+
}
|
|
2311
2446
|
/// Polled from the monitor thread. Audits and notifies TRANSITIONS only.
|
|
2312
2447
|
static void PollSecureDesktop()
|
|
2313
2448
|
{
|
|
@@ -2330,19 +2465,17 @@ namespace DshComputerUse.Worker
|
|
|
2330
2465
|
throw error;
|
|
2331
2466
|
}
|
|
2332
2467
|
public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
|
|
2333
|
-
public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
|
|
2468
|
+
public static void Disarm() { _armed = false; SceneControl.Reset(); Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
|
|
2334
2469
|
public static void AgentActed() { _agentActAt = Environment.TickCount; }
|
|
2335
2470
|
public static void BeginActuation()
|
|
2336
2471
|
{
|
|
2337
2472
|
POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
|
|
2338
|
-
Glow.ActuationBegin();
|
|
2473
|
+
SceneControl.BeginActuation(); Glow.ActuationBegin();
|
|
2339
2474
|
Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
|
|
2340
2475
|
}
|
|
2341
|
-
public static void EndActuation()
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
Glow.ActuationEnd();
|
|
2345
|
-
}
|
|
2476
|
+
public static void EndActuation() { SceneControl.EndActuation(); Glow.ActuationEnd(); }
|
|
2477
|
+
public static void UpdateInputProtection()
|
|
2478
|
+
{ Handoff.SetActing(SceneControl.Acting || SceneControl.Holding, HumanClock.ElapsedMilliseconds); }
|
|
2346
2479
|
static bool IsBareModifier(uint vk)
|
|
2347
2480
|
{
|
|
2348
2481
|
return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
|
|
@@ -2375,7 +2508,12 @@ namespace DshComputerUse.Worker
|
|
|
2375
2508
|
_publishedInputEpoch = epoch;
|
|
2376
2509
|
if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
|
|
2377
2510
|
if (phase != "idle") Glow.HumanYield();
|
|
2378
|
-
if (changed)
|
|
2511
|
+
if (changed) {
|
|
2512
|
+
var state = (Dictionary<string, object>)CooperationState();
|
|
2513
|
+
Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + state["revision"] +
|
|
2514
|
+
"; kind=" + state["lastKind"] + "; heldCount=" + state["heldCount"] +
|
|
2515
|
+
"; quietRemainingMs=" + state["quietRemainingMs"] + "; inputEpoch=" + state["inputEpoch"]);
|
|
2516
|
+
}
|
|
2379
2517
|
Notify("handoff", phase);
|
|
2380
2518
|
}
|
|
2381
2519
|
}
|
|
@@ -2386,14 +2524,37 @@ namespace DshComputerUse.Worker
|
|
|
2386
2524
|
PollSecureDesktop();
|
|
2387
2525
|
long now = HumanClock.ElapsedMilliseconds;
|
|
2388
2526
|
Handoff.Enable(_armed && CycleLit && !_exited, now);
|
|
2527
|
+
IntPtr sampleForeground = IntPtr.Zero;
|
|
2528
|
+
bool trusted = Handoff.HasHeld && CanReconcileHeld(_hooksReady, _thread != null && _thread.IsAlive, SecureDesktopActive,
|
|
2529
|
+
SceneControl.Acting, Glow.Injecting, now - Interlocked.Read(ref _lastInjectedInputAt), OnNormalInputDesktop()) &&
|
|
2530
|
+
Program.CanObserveAsyncKeys(out sampleForeground);
|
|
2531
|
+
bool swapped = Native.GetSystemMetrics(23) != 0; // SM_SWAPBUTTON
|
|
2532
|
+
int reconciled = Handoff.ReconcileReleased(delegate(int key) {
|
|
2533
|
+
bool down = HumanInputState.AsyncButtonMayBeDown(key, swapped,
|
|
2534
|
+
delegate(int physical) { return (Native.GetAsyncKeyState(physical) & 0x8000) != 0; });
|
|
2535
|
+
// A foreground/desktop switch during the sample makes zero unknown,
|
|
2536
|
+
// never evidence of release. The next tick checks the new token.
|
|
2537
|
+
return down || Native.GetForegroundWindow() != sampleForeground || !OnNormalInputDesktop();
|
|
2538
|
+
}, trusted, now);
|
|
2539
|
+
if (reconciled > 0) Program.AuditRecord("HUMAN-RELEASE-RECONCILED", "count=" + reconciled + "; source=three-normal-desktop-async-up-samples; quietMs=" + HumanInputState.IdleMs);
|
|
2389
2540
|
Handoff.Tick(now);
|
|
2390
2541
|
if (Handoff.Active) Program.ReleaseOwnedInputs();
|
|
2391
2542
|
PublishHandoff();
|
|
2392
2543
|
}
|
|
2544
|
+
static bool CanReconcileHeld(bool hooksReady, bool hookAlive, bool secure, bool acting, bool injecting, long sinceInjected, bool inputDesktop)
|
|
2545
|
+
{ return hooksReady && hookAlive && !secure && !acting && !injecting && sinceInjected >= 300 && inputDesktop; }
|
|
2393
2546
|
|
|
2394
2547
|
static RecoveryPermit _temporary;
|
|
2395
2548
|
static long _externalInput;
|
|
2396
2549
|
public static string TemporaryPauseId { get { lock (SyncLock) { return _temporary == null ? "" : _temporary.Id; } } }
|
|
2550
|
+
public static string EventPauseId(string kind)
|
|
2551
|
+
{
|
|
2552
|
+
// Handoff events are serialized under HandoffPublishLock. Lifecycle
|
|
2553
|
+
// transitions take SyncLock then publish, so their notification must
|
|
2554
|
+
// not take SyncLock in the reverse order. These two observation events
|
|
2555
|
+
// neither own nor consume temporary-pause credentials.
|
|
2556
|
+
return kind == "handoff" || kind == "secure" ? "" : TemporaryPauseId;
|
|
2557
|
+
}
|
|
2397
2558
|
public static void ObserveTemporary(string id)
|
|
2398
2559
|
{
|
|
2399
2560
|
lock (SyncLock) { if (_temporary != null && _temporary.Id == id) _temporary.Observed = true; }
|
|
@@ -2469,6 +2630,7 @@ namespace DshComputerUse.Worker
|
|
|
2469
2630
|
InputVersion = Interlocked.Read(ref _externalInput), InputTick = Native.LastInputTick() };
|
|
2470
2631
|
WriteStopFile(why + "\nsource=agent-temporary\npauseId=" + _temporary.Id);
|
|
2471
2632
|
}
|
|
2633
|
+
SceneControl.Reset();
|
|
2472
2634
|
try { Glow.Kill(); } catch { }
|
|
2473
2635
|
try { Glow.Stopped(true); } catch { } // red border + red badge: unmistakable
|
|
2474
2636
|
// THE BRAKE IS A STATE CHANGE, SO IT GETS A LINE (2026-09-14). This is the record the
|
|
@@ -2531,6 +2693,7 @@ namespace DshComputerUse.Worker
|
|
|
2531
2693
|
Program.AuditRecord("ENGAGE-ASK", "source=agent" +
|
|
2532
2694
|
"; askPid=" + Process.GetCurrentProcess().Id +
|
|
2533
2695
|
"; cycleLit=" + (CycleLit ? 1 : 0) + "; why=" + why);
|
|
2696
|
+
SceneControl.Reset();
|
|
2534
2697
|
try { Glow.Kill(); } catch { }
|
|
2535
2698
|
try { Glow.Ask(true); } catch { } // fast-breathing RED, not the steady stop
|
|
2536
2699
|
announce = true;
|
|
@@ -2963,6 +3126,51 @@ namespace DshComputerUse.Worker
|
|
|
2963
3126
|
}
|
|
2964
3127
|
}
|
|
2965
3128
|
|
|
3129
|
+
// Only complete window-management chords may change their own foreground receiver.
|
|
3130
|
+
// Every down still passes the ordinary receiver guard. After the final accepted down,
|
|
3131
|
+
// only this chord's owned releases may bypass that guard; human/pause/cancel still win.
|
|
3132
|
+
public static class WindowManagementChord
|
|
3133
|
+
{
|
|
3134
|
+
public static bool IsExact(string canonical)
|
|
3135
|
+
{
|
|
3136
|
+
switch (canonical)
|
|
3137
|
+
{
|
|
3138
|
+
case "alt+tab": case "alt+shift+tab": case "alt+esc": case "alt+shift+esc":
|
|
3139
|
+
case "win+d": case "win+tab": case "win+left": case "win+right": case "win+up": case "win+down":
|
|
3140
|
+
case "rwin+d": case "rwin+tab": case "rwin+left": case "rwin+right": case "rwin+up": case "rwin+down":
|
|
3141
|
+
return true;
|
|
3142
|
+
default: return false;
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
public static void Run(IList<ushort> keys, Action<ushort, bool> send,
|
|
3147
|
+
Action<ushort> release, Func<ushort, bool> isDown, Action hold,
|
|
3148
|
+
Action checkContinuation, Action invalidateTarget)
|
|
3149
|
+
{
|
|
3150
|
+
int acceptedDowns = 0;
|
|
3151
|
+
try
|
|
3152
|
+
{
|
|
3153
|
+
KeyboardChord.Run(keys, delegate(ushort key, bool up) {
|
|
3154
|
+
if (up && acceptedDowns == keys.Count)
|
|
3155
|
+
{
|
|
3156
|
+
checkContinuation();
|
|
3157
|
+
release(key); // ledger-backed: never releases someone else's key
|
|
3158
|
+
}
|
|
3159
|
+
else
|
|
3160
|
+
{
|
|
3161
|
+
send(key, up);
|
|
3162
|
+
if (!up) acceptedDowns++;
|
|
3163
|
+
}
|
|
3164
|
+
}, release, isDown, hold);
|
|
3165
|
+
}
|
|
3166
|
+
finally
|
|
3167
|
+
{
|
|
3168
|
+
// Even a partial chord can open system UI. No stale sticky/refocus continuation.
|
|
3169
|
+
if (acceptedDowns > 0) invalidateTarget();
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
|
|
2966
3174
|
public sealed class ClipboardBytes
|
|
2967
3175
|
{
|
|
2968
3176
|
public uint Format;
|
|
@@ -3163,6 +3371,16 @@ namespace DshComputerUse.Worker
|
|
|
3163
3371
|
|
|
3164
3372
|
static class Program
|
|
3165
3373
|
{
|
|
3374
|
+
public const int ProtocolVersion = 2;
|
|
3375
|
+
public static readonly string WorkerGeneration = Guid.NewGuid().ToString("N");
|
|
3376
|
+
static Dictionary<string, object> RuntimeIdentity()
|
|
3377
|
+
{
|
|
3378
|
+
return Dict("packageVersion", Environment.GetEnvironmentVariable("DSH_CU_PACKAGE_VERSION") ?? "unreported",
|
|
3379
|
+
"buildId", Environment.GetEnvironmentVariable("DSH_CU_BUILD_ID") ?? "unreported",
|
|
3380
|
+
"sourceHash", Environment.GetEnvironmentVariable("DSH_CU_SOURCE_HASH") ?? "unreported",
|
|
3381
|
+
"binaryHash", Environment.GetEnvironmentVariable("DSH_CU_BINARY_HASH") ?? "unreported",
|
|
3382
|
+
"protocolVersion", ProtocolVersion, "workerGeneration", WorkerGeneration);
|
|
3383
|
+
}
|
|
3166
3384
|
static readonly JavaScriptSerializer Json = new JavaScriptSerializer();
|
|
3167
3385
|
static int _reqId = 0;
|
|
3168
3386
|
static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
|
|
@@ -3225,6 +3443,33 @@ namespace DshComputerUse.Worker
|
|
|
3225
3443
|
return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
|
|
3226
3444
|
};
|
|
3227
3445
|
internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
|
|
3446
|
+
internal static Func<IntPtr> InputForeground = delegate { return TopLevel(Native.GetForegroundWindow()); };
|
|
3447
|
+
internal static Func<IntPtr> InputPointReceiver = delegate {
|
|
3448
|
+
POINT p; return Native.GetCursorPos(out p) ? TopLevel(Native.WindowFromPoint(p)) : IntPtr.Zero;
|
|
3449
|
+
};
|
|
3450
|
+
internal static Func<IntPtr, bool> InputWindowExists = delegate(IntPtr h) { return Native.IsWindow(h); };
|
|
3451
|
+
internal static Func<IntPtr, uint> InputWindowPid = delegate(IntPtr h) { return PidOf(h); };
|
|
3452
|
+
[ThreadStatic] static Action _receiverCheck;
|
|
3453
|
+
sealed class ReceiverScope : IDisposable
|
|
3454
|
+
{
|
|
3455
|
+
readonly Action previous;
|
|
3456
|
+
public ReceiverScope(Action check) { previous = _receiverCheck; check(); _receiverCheck = check; }
|
|
3457
|
+
public void Dispose() { _receiverCheck = previous; }
|
|
3458
|
+
}
|
|
3459
|
+
static string InputReceiverProblem(long expected, uint expectedPid, bool exists, uint livePid, long actual)
|
|
3460
|
+
{
|
|
3461
|
+
if (expected == 0 || !exists || livePid != expectedPid) return "observed target disappeared or changed identity";
|
|
3462
|
+
if (actual != expected) return "input receiver changed from hwnd=" + expected + " to hwnd=" + actual;
|
|
3463
|
+
return null;
|
|
3464
|
+
}
|
|
3465
|
+
static IDisposable GuardReceiver(IntPtr expected, uint pid, bool keyboard)
|
|
3466
|
+
{
|
|
3467
|
+
return new ReceiverScope(delegate {
|
|
3468
|
+
IntPtr actual = keyboard ? InputForeground() : InputPointReceiver();
|
|
3469
|
+
string problem = InputReceiverProblem(expected.ToInt64(), pid, InputWindowExists(expected), InputWindowPid(expected), actual.ToInt64());
|
|
3470
|
+
if (problem != null) throw InputInterrupted("INPUT_TARGET_CHANGED", problem + ". Remaining input was refused; inspect any partial effect before retrying.");
|
|
3471
|
+
});
|
|
3472
|
+
}
|
|
3228
3473
|
|
|
3229
3474
|
static Exception InputInterrupted(string code, string message)
|
|
3230
3475
|
{
|
|
@@ -3321,11 +3566,31 @@ namespace DshComputerUse.Worker
|
|
|
3321
3566
|
}
|
|
3322
3567
|
public static bool CancelInput(int requestId)
|
|
3323
3568
|
{
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3569
|
+
return GetB(CancelInputStatus(requestId), "cancelled", false);
|
|
3570
|
+
}
|
|
3571
|
+
static Dictionary<string, object> CancelInputStatus(int requestId)
|
|
3572
|
+
{
|
|
3573
|
+
bool valid = requestId > 0;
|
|
3574
|
+
bool completed = valid && requestId <= _lastCompletedRequestId;
|
|
3575
|
+
if (valid && !completed) CancelledRequests[requestId] = true;
|
|
3576
|
+
bool active = valid && _activeRequestId == requestId;
|
|
3577
|
+
if (active) Interlocked.Increment(ref _cancelGeneration);
|
|
3578
|
+
bool cleanup = false; int owned = -1; string state = valid ? "input-lock-busy" : "invalid-request";
|
|
3579
|
+
// The reader must remain responsive even if SendInput/provider work
|
|
3580
|
+
// owns InputLock. Failure to obtain it is uncertainty, not clean-up.
|
|
3581
|
+
if (valid && Monitor.TryEnter(InputLock, 200)) {
|
|
3582
|
+
try {
|
|
3583
|
+
// Never release a newer request's accepted input on behalf
|
|
3584
|
+
// of an older cancellation.
|
|
3585
|
+
if (_activeRequestId == requestId) ReleaseOwnedInputs();
|
|
3586
|
+
owned = OwnedUps.Count;
|
|
3587
|
+
cleanup = owned == 0;
|
|
3588
|
+
state = cleanup ? "no-owned-input" : "owned-input-remains";
|
|
3589
|
+
} finally { Monitor.Exit(InputLock); }
|
|
3590
|
+
}
|
|
3591
|
+
return Dict("requestId", requestId, "cancelled", valid && !completed,
|
|
3592
|
+
"cleanupConfirmed", cleanup, "ownedInputCount", owned, "cleanupState", state,
|
|
3593
|
+
"workerGeneration", WorkerGeneration);
|
|
3329
3594
|
}
|
|
3330
3595
|
static string InputKey(INPUT input, out bool up, out INPUT release, out int physical)
|
|
3331
3596
|
{
|
|
@@ -3366,6 +3631,8 @@ namespace DshComputerUse.Worker
|
|
|
3366
3631
|
string key = InputKey(inputs[0], out isUp, out release, out physical);
|
|
3367
3632
|
if (isUp && key != null && !OwnedUps.ContainsKey(key)) return 1;
|
|
3368
3633
|
}
|
|
3634
|
+
if (_receiverCheck != null) _receiverCheck();
|
|
3635
|
+
CheckHumanInput();
|
|
3369
3636
|
uint sent = InputSender(inputs);
|
|
3370
3637
|
if (sent > 0) _operationDispatched = true;
|
|
3371
3638
|
for (int i = 0; i < Math.Min(sent, inputs.Length); i++)
|
|
@@ -3509,6 +3776,16 @@ namespace DshComputerUse.Worker
|
|
|
3509
3776
|
[STAThread]
|
|
3510
3777
|
static int Main(string[] args)
|
|
3511
3778
|
{
|
|
3779
|
+
// The cursor guardian owns only the temporary visual-hide lease. It
|
|
3780
|
+
// must never install input hooks, open a control cycle or emit worker
|
|
3781
|
+
// protocol messages into the parent's inherited output stream.
|
|
3782
|
+
if (args.Length > 0 && args[0] == "--pointer-guardian")
|
|
3783
|
+
{
|
|
3784
|
+
int ownerPid; long ownerStartTicks;
|
|
3785
|
+
if (args.Length != 4 || !Int32.TryParse(args[2], out ownerPid) ||
|
|
3786
|
+
!Int64.TryParse(args[3], out ownerStartTicks)) return 2;
|
|
3787
|
+
return PointerVisibilityLease.GuardianMain(args[1], ownerPid, ownerStartTicks);
|
|
3788
|
+
}
|
|
3512
3789
|
// DPI awareness first: it decides the coordinate space of everything below
|
|
3513
3790
|
// (GetSystemMetrics, GetCursorPos, capture pixels, UIA rects AND the
|
|
3514
3791
|
// interpretation of SendInput's absolute coordinates). Record which call won.
|
|
@@ -3537,8 +3814,9 @@ namespace DshComputerUse.Worker
|
|
|
3537
3814
|
// lives; the consumer can read the fields and draw no conclusion the worker did
|
|
3538
3815
|
// not record itself.
|
|
3539
3816
|
string hint;
|
|
3540
|
-
|
|
3541
|
-
|
|
3817
|
+
string pauseId = Panic.EventPauseId(kind);
|
|
3818
|
+
if (kind == "panic" && !string.IsNullOrEmpty(pauseId))
|
|
3819
|
+
hint = "Agent temporary diagnostic pause. Inspect the current target, then use checked recover only if the blocker is resolved. Credential: " + pauseId;
|
|
3542
3820
|
else if (kind == "panic")
|
|
3543
3821
|
hint = "STOPPED (brake engaged). The machine is stopped: nothing that drives it runs " +
|
|
3544
3822
|
"until a human resumes with Ctrl+Alt+R. WRAP UP NOW: one short summary of what " +
|
|
@@ -3563,7 +3841,7 @@ namespace DshComputerUse.Worker
|
|
|
3563
3841
|
WriteLine(Json.Serialize(Dict(
|
|
3564
3842
|
"event", kind,
|
|
3565
3843
|
"why", why,
|
|
3566
|
-
"pauseId",
|
|
3844
|
+
"pauseId", pauseId,
|
|
3567
3845
|
"cooperation", Panic.CooperationState(),
|
|
3568
3846
|
"ts", NowMs(),
|
|
3569
3847
|
"hint", hint)));
|
|
@@ -3600,6 +3878,8 @@ namespace DshComputerUse.Worker
|
|
|
3600
3878
|
Dictionary<string, object> hello = Dict(
|
|
3601
3879
|
"event", "ready",
|
|
3602
3880
|
"pid", Process.GetCurrentProcess().Id,
|
|
3881
|
+
"protocolVersion", ProtocolVersion, "workerGeneration", WorkerGeneration,
|
|
3882
|
+
"runtime", RuntimeIdentity(),
|
|
3603
3883
|
// COLD-START STATE, REPORTED AS A FACT (cold-start bug, 2026-09-13): this process has
|
|
3604
3884
|
// already adopted any STOP/EXITED record that was on disk before it existed, so the
|
|
3605
3885
|
// core can start from the machine's real state instead of "not stopped" — the two
|
|
@@ -3624,9 +3904,9 @@ namespace DshComputerUse.Worker
|
|
|
3624
3904
|
{
|
|
3625
3905
|
object argsValue;
|
|
3626
3906
|
var cancelArgs = request.TryGetValue("args", out argsValue) ? argsValue as Dictionary<string, object> : null;
|
|
3627
|
-
|
|
3907
|
+
var cancellation = CancelInputStatus(cancelArgs == null ? 0 : GetI(cancelArgs, "requestId", 0));
|
|
3628
3908
|
WriteLine(parser.Serialize(Dict("id", GetI(request, "id", 0), "ok", true,
|
|
3629
|
-
"data",
|
|
3909
|
+
"data", cancellation)));
|
|
3630
3910
|
continue;
|
|
3631
3911
|
}
|
|
3632
3912
|
} catch { /* Main loop reports malformed requests in the normal format. */ }
|
|
@@ -3893,7 +4173,7 @@ namespace DshComputerUse.Worker
|
|
|
3893
4173
|
// Is a real injection sequence in flight RIGHT NOW? The ACTING light is driven
|
|
3894
4174
|
// by the actual action scope, so both are reported as observable facts.
|
|
3895
4175
|
"injecting", Glow.Injecting,
|
|
3896
|
-
"acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
|
|
4176
|
+
"acting", Glow.Acting, "holding", Glow.Holding, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
|
|
3897
4177
|
"title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
|
|
3898
4178
|
// EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
|
|
3899
4179
|
// nothing held is the observable form of "no ghost right-click / Alt menu".
|
|
@@ -4090,6 +4370,7 @@ namespace DshComputerUse.Worker
|
|
|
4090
4370
|
IntPtr old = Native.SelectObject(mem, dib);
|
|
4091
4371
|
// SRCCOPY | CAPTUREBLT so layered windows are included
|
|
4092
4372
|
Native.BitBlt(mem, 0, 0, w, h, screen, x, y, 0x00CC0020 | 0x40000000);
|
|
4373
|
+
long capturedAt = NowMs();
|
|
4093
4374
|
bool cursorDrawn = false;
|
|
4094
4375
|
int cursorFlags = 0, cursorAtX = 0, cursorAtY = 0;
|
|
4095
4376
|
if (withCursor)
|
|
@@ -4152,13 +4433,16 @@ namespace DshComputerUse.Worker
|
|
|
4152
4433
|
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, q);
|
|
4153
4434
|
outBmp.Save(ms, codec, ep);
|
|
4154
4435
|
}
|
|
4436
|
+
int imageWidth = outBmp.Width, imageHeight = outBmp.Height;
|
|
4155
4437
|
outBmp.Dispose();
|
|
4156
4438
|
POINT cp2; Native.GetCursorPos(out cp2);
|
|
4157
4439
|
Dictionary<string, object> res = Dict(
|
|
4158
4440
|
"image", Convert.ToBase64String(ms.ToArray()),
|
|
4159
4441
|
"mime", mime,
|
|
4160
4442
|
"region", Dict("x", x, "y", y, "width", w, "height", h),
|
|
4161
|
-
"scaledTo",
|
|
4443
|
+
"scaledTo", imageWidth,
|
|
4444
|
+
"imageSize", new int[] { imageWidth, imageHeight },
|
|
4445
|
+
"capturedAt", capturedAt,
|
|
4162
4446
|
"cursor", PtDict(cp2.X, cp2.Y),
|
|
4163
4447
|
"ts", NowMs());
|
|
4164
4448
|
if (withCursor)
|
|
@@ -4170,7 +4454,8 @@ namespace DshComputerUse.Worker
|
|
|
4170
4454
|
res["cursorDrawn"] = cursorDrawn;
|
|
4171
4455
|
res["cursorFlags"] = cursorFlags;
|
|
4172
4456
|
res["cursorScreenPos"] = PtDict(cursorAtX, cursorAtY);
|
|
4173
|
-
res["cursorInImage"] =
|
|
4457
|
+
res["cursorInImage"] = Dict("x", (cursorAtX - x) * (double)imageWidth / w,
|
|
4458
|
+
"y", (cursorAtY - y) * (double)imageHeight / h);
|
|
4174
4459
|
res["cursorNote"] = cursorDrawn
|
|
4175
4460
|
? "pointer burned into the image at cursorInImage"
|
|
4176
4461
|
: ((cursorFlags & 1) == 0
|
|
@@ -4316,6 +4601,46 @@ namespace DshComputerUse.Worker
|
|
|
4316
4601
|
SendInputChecked(new INPUT[] { inp });
|
|
4317
4602
|
}
|
|
4318
4603
|
|
|
4604
|
+
internal static string StandardCursorShape(IntPtr cursor, Func<int, IntPtr> load)
|
|
4605
|
+
{
|
|
4606
|
+
if (cursor == IntPtr.Zero) return "unknown";
|
|
4607
|
+
int[] ids = { 32644, 32645, 32643, 32642, 32646, 32512, 32513, 32649, 32514, 32515, 32648, 32650, 32651 };
|
|
4608
|
+
string[] names = { "resize-ew", "resize-ns", "resize-nesw", "resize-nwse", "move", "arrow", "ibeam", "hand", "wait", "cross", "not-allowed", "app-starting", "help" };
|
|
4609
|
+
for (int i = 0; i < ids.Length; i++) if (load(ids[i]) == cursor) return names[i];
|
|
4610
|
+
return "unknown"; // Custom/browser-drawn cursors do not reveal a CSS value.
|
|
4611
|
+
}
|
|
4612
|
+
|
|
4613
|
+
static bool CursorMatches(string expected, string actual, bool visible)
|
|
4614
|
+
{
|
|
4615
|
+
if (String.IsNullOrEmpty(expected)) return true;
|
|
4616
|
+
if (!visible || actual == "unknown") return false;
|
|
4617
|
+
return expected == "resize" ? actual.StartsWith("resize-", StringComparison.Ordinal) : expected == actual;
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
static Dictionary<string, object> CursorEvidence(bool available, bool visible, IntPtr cursor, Func<int, IntPtr> load)
|
|
4621
|
+
{
|
|
4622
|
+
visible = available && visible;
|
|
4623
|
+
string shape = visible ? StandardCursorShape(cursor, load) : "unknown";
|
|
4624
|
+
string classification = !available ? "unavailable" : !visible ? "not-visible" : shape == "unknown" ? "unrecognized" : "standard";
|
|
4625
|
+
var result = Dict("available", available, "visible", visible, "shape", shape,
|
|
4626
|
+
"classification", classification, "source", "win32-standard-cursor");
|
|
4627
|
+
if (classification == "unrecognized")
|
|
4628
|
+
result["note"] = "Visible cursor handle does not match a supported Windows standard cursor. Browser col-resize/row-resize and custom cursors may be unrecognized; this does not prove a hit or a miss on a draggable handle.";
|
|
4629
|
+
return result;
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
static Dictionary<string, object> CursorSnapshot()
|
|
4633
|
+
{
|
|
4634
|
+
CURSORINFO ci = new CURSORINFO();
|
|
4635
|
+
ci.cbSize = (uint)Marshal.SizeOf(typeof(CURSORINFO));
|
|
4636
|
+
bool available = Native.GetCursorInfo(ref ci);
|
|
4637
|
+
bool visible = available && (ci.flags & Native.CURSOR_SHOWING) != 0;
|
|
4638
|
+
var result = CursorEvidence(available, visible, ci.hCursor,
|
|
4639
|
+
delegate(int id) { return Native.LoadCursor(IntPtr.Zero, new IntPtr(id)); });
|
|
4640
|
+
result["screenPos"] = available ? (object)PtDict(ci.ptScreenPos.X, ci.ptScreenPos.Y) : null;
|
|
4641
|
+
return result;
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4319
4644
|
static Dictionary<string, object> Move(Dictionary<string, object> a)
|
|
4320
4645
|
{
|
|
4321
4646
|
int x = GetI(a, "x", int.MinValue), y = GetI(a, "y", int.MinValue);
|
|
@@ -4337,11 +4662,75 @@ namespace DshComputerUse.Worker
|
|
|
4337
4662
|
Native.GetCursorPos(out p);
|
|
4338
4663
|
}
|
|
4339
4664
|
CheckHumanInput();
|
|
4340
|
-
return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections);
|
|
4665
|
+
return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections, "cursor", CursorSnapshot());
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
static Rectangle ClickSampleRegion(int x, int y, Rectangle desktop)
|
|
4669
|
+
{
|
|
4670
|
+
int width = Math.Min(340, desktop.Width), height = Math.Min(340, desktop.Height);
|
|
4671
|
+
if (width <= 0 || height <= 0) return Rectangle.Empty;
|
|
4672
|
+
int left = (int)Math.Min(Math.Max((long)x - width / 2, desktop.Left), (long)desktop.Right - width);
|
|
4673
|
+
int top = (int)Math.Min(Math.Max((long)y - height / 2, desktop.Top), (long)desktop.Bottom - height);
|
|
4674
|
+
return new Rectangle(left, top, width, height);
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
static int VerificationNumber(Dictionary<string, object> rectangle, string key)
|
|
4678
|
+
{
|
|
4679
|
+
object raw;
|
|
4680
|
+
if (!rectangle.TryGetValue(key, out raw) || !(raw is int || raw is long || raw is double || raw is float || raw is decimal))
|
|
4681
|
+
throw new ArgumentException("missing integer " + key);
|
|
4682
|
+
double n = Convert.ToDouble(raw);
|
|
4683
|
+
if (Double.IsNaN(n) || Double.IsInfinity(n) || n != Math.Truncate(n) || n <= Int32.MinValue || n > Int32.MaxValue)
|
|
4684
|
+
throw new ArgumentException("invalid integer " + key);
|
|
4685
|
+
return (int)n;
|
|
4686
|
+
}
|
|
4687
|
+
static Rectangle VerificationSampleRegion(Dictionary<string, object> a, int x, int y, Rectangle desktop)
|
|
4688
|
+
{
|
|
4689
|
+
if (!a.ContainsKey("verificationRegion")) return ClickSampleRegion(x, y, desktop);
|
|
4690
|
+
try {
|
|
4691
|
+
var box = a["verificationRegion"] as Dictionary<string, object>;
|
|
4692
|
+
if (box == null) throw new ArgumentException("expected rectangle object");
|
|
4693
|
+
int rx = VerificationNumber(box, "x"), ry = VerificationNumber(box, "y");
|
|
4694
|
+
int rw = VerificationNumber(box, "width"), rh = VerificationNumber(box, "height");
|
|
4695
|
+
if (rw <= 0 || rh <= 0 || (long)rw * rh > 16777216 ||
|
|
4696
|
+
(long)rx + rw > Int32.MaxValue || (long)ry + rh > Int32.MaxValue)
|
|
4697
|
+
throw new ArgumentException("positive size, 16M pixel maximum, non-overflowing edges required");
|
|
4698
|
+
Rectangle requested = new Rectangle(rx, ry, rw, rh);
|
|
4699
|
+
Rectangle visible = Rectangle.Intersect(requested, desktop);
|
|
4700
|
+
if (visible.Width <= 0 || visible.Height <= 0) throw new ArgumentException("region is outside the virtual desktop");
|
|
4701
|
+
return visible;
|
|
4702
|
+
} catch (Exception error) {
|
|
4703
|
+
var refusal = new ArgumentException("INVALID_VERIFICATION_REGION: " + error.Message + "; no input was dispatched.");
|
|
4704
|
+
refusal.Data["code"] = "INVALID_VERIFICATION_REGION"; refusal.Data["outcome"] = "not-dispatched";
|
|
4705
|
+
throw refusal;
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
static void AddPixelVerification(Dictionary<string, object> result, Dictionary<string, object> args,
|
|
4709
|
+
Rectangle region, byte[] before, byte[] after)
|
|
4710
|
+
{
|
|
4711
|
+
result["effectVerified"] = false;
|
|
4712
|
+
result["verificationAvailable"] = before != null && after != null && before.Length > 0 && before.Length == after.Length;
|
|
4713
|
+
result["verificationSource"] = args.ContainsKey("verificationRegion") ? "requested-region" : "click-neighborhood";
|
|
4714
|
+
result["verificationRegion"] = Dict("x", region.X, "y", region.Y, "width", region.Width, "height", region.Height);
|
|
4715
|
+
if (args.ContainsKey("verificationRegion")) result["requestedVerificationRegion"] = args["verificationRegion"];
|
|
4716
|
+
if (!(bool)result["verificationAvailable"]) {
|
|
4717
|
+
result["verificationNote"] = "Pixel samples unavailable; input completion does not establish the intended effect.";
|
|
4718
|
+
return;
|
|
4719
|
+
}
|
|
4720
|
+
double changed = Math.Round(SigDiffPct(before, after, 8), 2);
|
|
4721
|
+
result["changedPct"] = changed;
|
|
4722
|
+
result["verificationNote"] = "Sampled pixel difference only, not semantic verification. Inspect the expected result before replaying input.";
|
|
4723
|
+
if (changed < 0.15 && !args.ContainsKey("verify"))
|
|
4724
|
+
result["warning"] = "No sampled change in the " + (args.ContainsKey("verificationRegion") ? "requested witness region" : "click neighborhood") +
|
|
4725
|
+
". This is expected for an already-focused or already-satisfied control; an effect may also be delayed or elsewhere. " +
|
|
4726
|
+
"Check the intended state and focus/receiver before retrying.";
|
|
4341
4727
|
}
|
|
4342
4728
|
|
|
4343
4729
|
static Dictionary<string, object> Click(Dictionary<string, object> a)
|
|
4344
4730
|
{
|
|
4731
|
+
Rectangle sampleRegion = VerificationSampleRegion(a, GetI(a, "x", 0), GetI(a, "y", 0), new Rectangle(
|
|
4732
|
+
Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
|
|
4733
|
+
Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
|
|
4345
4734
|
string btn = GetS(a, "button", "left");
|
|
4346
4735
|
int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
|
|
4347
4736
|
int gap = GetI(a, "intervalMs", 60);
|
|
@@ -4471,41 +4860,33 @@ namespace DshComputerUse.Worker
|
|
|
4471
4860
|
// The window under the click point becomes the sticky target for later typing.
|
|
4472
4861
|
try { SetSticky(under); } catch { }
|
|
4473
4862
|
}
|
|
4474
|
-
//
|
|
4475
|
-
//
|
|
4476
|
-
// missed pixel, wrong element) — and "no change" is exactly the silent failure that
|
|
4477
|
-
// used to be indistinguishable from success. Region sampling is ~1700 pixels, cheaper
|
|
4478
|
-
// than a screenshot by three orders of magnitude.
|
|
4479
|
-
int vsW = Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN);
|
|
4480
|
-
int vsH = Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN);
|
|
4863
|
+
// A cheap pixel witness, not a business-success condition. The caller may
|
|
4864
|
+
// identify an effect elsewhere (e.g. a player bar), independently of the click.
|
|
4481
4865
|
bool verify = (tx != int.MinValue) && GetB(a, "verify", true);
|
|
4482
|
-
int
|
|
4483
|
-
int bx = Math.Min(Math.Max(tx - half, margin), Math.Max(margin, vsW - margin - 2 * half));
|
|
4484
|
-
int by = Math.Min(Math.Max(ty - half, margin), Math.Max(margin, vsH - margin - 2 * half));
|
|
4485
|
-
int bw = Math.Min(2 * half, Math.Max(16, vsW - 2 * margin));
|
|
4486
|
-
int bh = Math.Min(2 * half, Math.Max(16, vsH - 2 * margin));
|
|
4866
|
+
int bx = sampleRegion.X, by = sampleRegion.Y, bw = sampleRegion.Width, bh = sampleRegion.Height;
|
|
4487
4867
|
byte[] before = verify ? FrameSignatureRect(bx, by, bw, bh, 8) : null;
|
|
4488
4868
|
|
|
4489
4869
|
// Re-ask at the moment of injection: the pointer has travelled since the precondition
|
|
4490
4870
|
// above, and the only promise that matters is about the pixel the button is really
|
|
4491
4871
|
// pressed on. Cheap (one WindowFromPoint) and it closes the window between check and use.
|
|
4492
4872
|
if (tx != int.MinValue) HostGuardPoint(tx, ty, "click");
|
|
4873
|
+
if (under == IntPtr.Zero) under = InputPointReceiver();
|
|
4493
4874
|
|
|
4494
4875
|
uint down = Native.MOUSEEVENTF_LEFTDOWN, up = Native.MOUSEEVENTF_LEFTUP;
|
|
4495
4876
|
if (btn == "right") { down = Native.MOUSEEVENTF_RIGHTDOWN; up = Native.MOUSEEVENTF_RIGHTUP; }
|
|
4496
4877
|
else if (btn == "middle") { down = Native.MOUSEEVENTF_MIDDLEDOWN; up = Native.MOUSEEVENTF_MIDDLEUP; }
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4878
|
+
using (GuardReceiver(under, InputWindowPid(under), false)) {
|
|
4879
|
+
for (int i = 0; i < clicks; i++) {
|
|
4880
|
+
SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
|
|
4881
|
+
if (i < clicks - 1) Thread.Sleep(gap);
|
|
4882
|
+
}
|
|
4501
4883
|
}
|
|
4502
4884
|
EndActuationForReadback();
|
|
4503
|
-
|
|
4885
|
+
byte[] after = null;
|
|
4504
4886
|
if (verify && before != null)
|
|
4505
4887
|
{
|
|
4506
4888
|
Thread.Sleep(GetI(a, "verifyDelayMs", 240));
|
|
4507
|
-
|
|
4508
|
-
if (after != null) changedPct = Math.Round(SigDiffPct(before, after, 8), 2);
|
|
4889
|
+
after = FrameSignatureRect(bx, by, bw, bh, 8);
|
|
4509
4890
|
}
|
|
4510
4891
|
POINT fp; Native.GetCursorPos(out fp);
|
|
4511
4892
|
Dictionary<string, object> clickFocus = FocusReport(a, false);
|
|
@@ -4515,18 +4896,58 @@ namespace DshComputerUse.Worker
|
|
|
4515
4896
|
if (coveredBy != null) clickOut["coveredBy"] = coveredBy;
|
|
4516
4897
|
if (under != IntPtr.Zero)
|
|
4517
4898
|
clickOut["receiver"] = Short(WinTitleOf(under), 40) + " (pid " + PidOf(under) + ")";
|
|
4518
|
-
if (
|
|
4519
|
-
{
|
|
4520
|
-
clickOut["changedPct"] = changedPct;
|
|
4521
|
-
if (changedPct < 0.15 && !a.ContainsKey("verify"))
|
|
4522
|
-
clickOut["warning"] = "nothing changed near the click point within ~240ms. This is EXPECTED when the " +
|
|
4523
|
-
"click lands on an already-focused editor or a control already in the desired state (a caret blink " +
|
|
4524
|
-
"alone sits below the threshold — test report 2026-11-04); it can also mean a dead/disabled control " +
|
|
4525
|
-
"or a wrong pixel. Read focus/receiver above and only retry if they disagree with the intent.";
|
|
4526
|
-
}
|
|
4899
|
+
if (verify) AddPixelVerification(clickOut, a, sampleRegion, before, after);
|
|
4527
4900
|
return clickOut;
|
|
4528
4901
|
}
|
|
4529
4902
|
|
|
4903
|
+
static Exception DragPrecondition(string code, string detail)
|
|
4904
|
+
{
|
|
4905
|
+
var error = InputInterrupted(code, detail + " No drag button was pressed; earlier focus or pointer movement may have occurred.");
|
|
4906
|
+
error.Data["dragStarted"] = false;
|
|
4907
|
+
return error;
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
static Dictionary<string, object> CheckDragCursor(string expected, Dictionary<string, object> actual)
|
|
4911
|
+
{
|
|
4912
|
+
expected = (expected ?? "").Trim().ToLowerInvariant();
|
|
4913
|
+
string shape = (string)actual["shape"];
|
|
4914
|
+
string classification = (string)actual["classification"];
|
|
4915
|
+
string status = expected.Length == 0 ? "not-requested" : CursorMatches(expected, shape, (bool)actual["visible"])
|
|
4916
|
+
? "matched" : classification == "standard" ? "mismatched" : "unverifiable";
|
|
4917
|
+
var check = Dict("status", status, "expected", expected.Length == 0 ? null : (object)expected, "observed", shape);
|
|
4918
|
+
if (status == "mismatched" || status == "unverifiable") {
|
|
4919
|
+
string detail = status == "mismatched"
|
|
4920
|
+
? "Expected " + expected + ", observed standard cursor " + shape + ". Re-observe the intended handle before retrying."
|
|
4921
|
+
: "Cannot verify expected cursor " + expected + ": classification=" + classification + ". An unknown/unavailable cursor is not proof of an incorrect target. Browser col-resize/row-resize may use nonstandard cursors. If fresh independent geometry and hit-testing confirm the handle, the optional expectCursor may be omitted; verify the application result afterward.";
|
|
4922
|
+
// Keep the existing error code for callers; cursorCheck distinguishes a known
|
|
4923
|
+
// shape disagreement from missing evidence. Neither is converted to a match.
|
|
4924
|
+
var error = DragPrecondition("CURSOR_MISMATCH", detail);
|
|
4925
|
+
error.Data["expectedCursor"] = expected;
|
|
4926
|
+
error.Data["actualCursor"] = actual;
|
|
4927
|
+
error.Data["cursorCheck"] = check;
|
|
4928
|
+
throw error;
|
|
4929
|
+
}
|
|
4930
|
+
return check;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
static IntPtr CheckDragStart(Dictionary<string, object> a, int x, int y)
|
|
4934
|
+
{
|
|
4935
|
+
HostGuardPoint(x, y, "drag");
|
|
4936
|
+
POINT point; point.X = x; point.Y = y;
|
|
4937
|
+
IntPtr under = TopLevel(Native.WindowFromPoint(point));
|
|
4938
|
+
IntPtr want = a.ContainsKey("expectHwnd") ? new IntPtr(GetI(a, "expectHwnd", 0)) : _stickyHwnd;
|
|
4939
|
+
if (under == IntPtr.Zero) throw DragPrecondition("DRAG_START_UNREACHABLE", "No window owns the drag start pixel.");
|
|
4940
|
+
if (want != IntPtr.Zero) {
|
|
4941
|
+
if (!Native.IsWindow(want) || Native.IsIconic(want) || IsOffScreen(want))
|
|
4942
|
+
throw DragPrecondition("DRAG_TARGET_UNAVAILABLE", "The expected drag window is gone, minimized or off-screen.");
|
|
4943
|
+
if (under != want && PidOf(under) != PidOf(want))
|
|
4944
|
+
throw DragPrecondition("DRAG_START_COVERED", "The start pixel belongs to hwnd=" + under.ToInt64() + ", not expected hwnd=" + want.ToInt64() + ". Re-observe the target.");
|
|
4945
|
+
}
|
|
4946
|
+
if (ProcessElevated(PidOf(under)) && !SelfElevated())
|
|
4947
|
+
throw DragPrecondition("DRAG_START_ELEVATED", "The drag receiver is elevated and this worker cannot inject into it.");
|
|
4948
|
+
return under;
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4530
4951
|
static Dictionary<string, object> Drag(Dictionary<string, object> a)
|
|
4531
4952
|
{
|
|
4532
4953
|
int fx = GetI(a, "fromX", int.MinValue), fy = GetI(a, "fromY", int.MinValue);
|
|
@@ -4541,9 +4962,22 @@ namespace DshComputerUse.Worker
|
|
|
4541
4962
|
int durMs = Math.Max(50, GetI(a, "durationMs", 350));
|
|
4542
4963
|
AgentActing(a, "drag");
|
|
4543
4964
|
CheckHumanInput();
|
|
4965
|
+
if (a.ContainsKey("expectHwnd")) {
|
|
4966
|
+
HostGuardNamed(a, "drag");
|
|
4967
|
+
FocusReport(a, true);
|
|
4968
|
+
}
|
|
4969
|
+
CheckDragStart(a, fx, fy);
|
|
4544
4970
|
AnimatedMove(fx, fy, GetI(a, "moveDurationMs", 250));
|
|
4545
4971
|
Thread.Sleep(80);
|
|
4546
|
-
|
|
4972
|
+
CheckHumanInput();
|
|
4973
|
+
POINT startPoint;
|
|
4974
|
+
if (!Native.GetCursorPos(out startPoint) || Dist(startPoint, fx, fy) > 2)
|
|
4975
|
+
throw DragPrecondition("DRAG_START_MISSED", "The pointer did not reach the measured drag start within two screen pixels.");
|
|
4976
|
+
IntPtr startWindow = CheckDragStart(a, startPoint.X, startPoint.Y);
|
|
4977
|
+
Dictionary<string, object> startCursor = CursorSnapshot();
|
|
4978
|
+
Dictionary<string, object> cursorCheck = CheckDragCursor(GetS(a, "expectCursor", ""), startCursor);
|
|
4979
|
+
using (GuardReceiver(startWindow, InputWindowPid(startWindow), false))
|
|
4980
|
+
SendMouse(Native.MOUSEEVENTF_LEFTDOWN, 0, 0, 0);
|
|
4547
4981
|
Thread.Sleep(80);
|
|
4548
4982
|
POINT probe0; Native.GetCursorPos(out probe0);
|
|
4549
4983
|
if (Dist(probe0, fx, fy) > 24)
|
|
@@ -4572,7 +5006,9 @@ namespace DshComputerUse.Worker
|
|
|
4572
5006
|
SendMouse(Native.MOUSEEVENTF_LEFTUP, 0, 0, 0);
|
|
4573
5007
|
POINT p; Native.GetCursorPos(out p);
|
|
4574
5008
|
CheckHumanInput();
|
|
4575
|
-
return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections
|
|
5009
|
+
return Dict("x", p.X, "y", p.Y, "positionCorrections", _positionCorrections,
|
|
5010
|
+
"dragStarted", true, "startCursor", startCursor, "cursorCheck", cursorCheck, "startHwnd", startWindow.ToInt64(),
|
|
5011
|
+
"effectVerified", false, "note", "Input sequence completed. Cursor/window checks do not prove that a handle moved; verify the intended UI change.");
|
|
4576
5012
|
}
|
|
4577
5013
|
|
|
4578
5014
|
static Dictionary<string, object> Scroll(Dictionary<string, object> a)
|
|
@@ -4587,10 +5023,14 @@ namespace DshComputerUse.Worker
|
|
|
4587
5023
|
else HostGuardCursor("scroll");
|
|
4588
5024
|
AgentActing(a, "scroll");
|
|
4589
5025
|
if (a.ContainsKey("x") && a.ContainsKey("y")) Move(a);
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
5026
|
+
IntPtr scrollTarget = InputPointReceiver();
|
|
5027
|
+
if (_stickyHwnd != IntPtr.Zero && scrollTarget != _stickyHwnd && InputWindowPid(scrollTarget) != InputWindowPid(_stickyHwnd))
|
|
5028
|
+
throw InputInterrupted("INPUT_TARGET_CHANGED", "Scroll receiver differs from the observed target. No wheel input was sent.");
|
|
5029
|
+
using (GuardReceiver(scrollTarget, InputWindowPid(scrollTarget), false)) {
|
|
5030
|
+
for (int i = 0; i < lines; i++) {
|
|
5031
|
+
SendMouse(flag, 0, 0, unchecked((uint)(sign * (int)Native.WHEEL_DELTA)));
|
|
5032
|
+
Thread.Sleep(25);
|
|
5033
|
+
}
|
|
4594
5034
|
}
|
|
4595
5035
|
POINT p; Native.GetCursorPos(out p);
|
|
4596
5036
|
return PtDict(p.X, p.Y);
|
|
@@ -4634,6 +5074,14 @@ namespace DshComputerUse.Worker
|
|
|
4634
5074
|
|
|
4635
5075
|
static Dictionary<string, object> Indicator(Dictionary<string, object> a)
|
|
4636
5076
|
{
|
|
5077
|
+
if (a.ContainsKey("hold"))
|
|
5078
|
+
{
|
|
5079
|
+
SceneControl.SetHolding(GetB(a, "hold", false));
|
|
5080
|
+
if (SceneControl.Holding) Glow.Reveal();
|
|
5081
|
+
if (Panic.CycleLit && !Panic.Exited) Glow.Thinking(0);
|
|
5082
|
+
return Dict("holding", Glow.Holding, "cycleActive", Panic.CycleLit && !Panic.Exited,
|
|
5083
|
+
"indicator", Glow.Holding ? "holding" : "free");
|
|
5084
|
+
}
|
|
4637
5085
|
bool on = GetB(a, "on", true);
|
|
4638
5086
|
bool soft = GetB(a, "soft", false);
|
|
4639
5087
|
if (on) { Glow.Enabled = true; Glow.Touch(GetI(a, "ms", 30000)); }
|
|
@@ -4717,20 +5165,38 @@ namespace DshComputerUse.Worker
|
|
|
4717
5165
|
{
|
|
4718
5166
|
int x = GetI(a, "x", int.MinValue), y = GetI(a, "y", int.MinValue);
|
|
4719
5167
|
if (x == int.MinValue || y == int.MinValue) throw new Exception("x,y required");
|
|
5168
|
+
Rectangle witness = VerificationSampleRegion(a, x, y, new Rectangle(
|
|
5169
|
+
Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
|
|
5170
|
+
Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
|
|
4720
5171
|
HostGuardPoint(x, y, "shiftClick");
|
|
5172
|
+
POINT targetPoint; targetPoint.X = x; targetPoint.Y = y;
|
|
5173
|
+
IntPtr targetWindow = TopLevel(Native.WindowFromPoint(targetPoint));
|
|
5174
|
+
if (targetWindow == IntPtr.Zero) throw InputInterrupted("INPUT_TARGET_CHANGED", "Shift-click target is unavailable; no input sent.");
|
|
5175
|
+
uint targetPid = InputWindowPid(targetWindow);
|
|
4721
5176
|
int clicks = Math.Max(1, Math.Min(3, GetI(a, "clicks", 1)));
|
|
4722
5177
|
AgentActing(a, "shiftClick");
|
|
4723
5178
|
CheckHumanInput();
|
|
4724
5179
|
AnimatedMove(x, y, Math.Max(60, GetI(a, "moveDurationMs", 200)));
|
|
4725
5180
|
Thread.Sleep(GetI(a, "settleMs", 90));
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
5181
|
+
byte[] before = a.ContainsKey("verificationRegion") ? FrameSignatureRect(witness.X, witness.Y, witness.Width, witness.Height, 8) : null;
|
|
5182
|
+
using (GuardReceiver(targetWindow, targetPid, false)) {
|
|
5183
|
+
SendKey(0x10, false); // VK_SHIFT down
|
|
5184
|
+
Thread.Sleep(40);
|
|
5185
|
+
ClickTimes(clicks);
|
|
5186
|
+
Thread.Sleep(40);
|
|
5187
|
+
SendKey(0x10, true); // VK_SHIFT up
|
|
5188
|
+
}
|
|
4731
5189
|
Thread.Sleep(GetI(a, "settleMs", 90));
|
|
4732
5190
|
CheckHumanInput();
|
|
4733
|
-
|
|
5191
|
+
var result = Dict("x", x, "y", y, "clicks", clicks);
|
|
5192
|
+
if (a.ContainsKey("verificationRegion")) {
|
|
5193
|
+
EndActuationForReadback();
|
|
5194
|
+
Thread.Sleep(240);
|
|
5195
|
+
byte[] after = FrameSignatureRect(witness.X, witness.Y, witness.Width, witness.Height, 8);
|
|
5196
|
+
CheckHumanInput();
|
|
5197
|
+
AddPixelVerification(result, a, witness, before, after);
|
|
5198
|
+
}
|
|
5199
|
+
return result;
|
|
4734
5200
|
}
|
|
4735
5201
|
|
|
4736
5202
|
static void ClickTimes(int n)
|
|
@@ -4819,7 +5285,11 @@ namespace DshComputerUse.Worker
|
|
|
4819
5285
|
IntPtr dib = Native.CreateDIBSection(screen, ref bmi, 0, out bits, IntPtr.Zero, 0);
|
|
4820
5286
|
if (dib == IntPtr.Zero) { Native.DeleteDC(mem); Native.ReleaseDC(IntPtr.Zero, screen); return null; }
|
|
4821
5287
|
IntPtr old = Native.SelectObject(mem, dib);
|
|
4822
|
-
Native.BitBlt(mem, 0, 0, rw, rh, screen, rx, ry, 0x00CC0020 | 0x40000000)
|
|
5288
|
+
if (!Native.BitBlt(mem, 0, 0, rw, rh, screen, rx, ry, 0x00CC0020 | 0x40000000)) {
|
|
5289
|
+
Native.SelectObject(mem, old);
|
|
5290
|
+
Native.DeleteObject(dib); Native.DeleteDC(mem); Native.ReleaseDC(IntPtr.Zero, screen);
|
|
5291
|
+
return null;
|
|
5292
|
+
}
|
|
4823
5293
|
int n = ((rw - 1) / step + 1) * ((rh - 1) / step + 1);
|
|
4824
5294
|
byte[] sig = new byte[n];
|
|
4825
5295
|
int k = 0;
|
|
@@ -5242,6 +5712,35 @@ namespace DshComputerUse.Worker
|
|
|
5242
5712
|
}
|
|
5243
5713
|
throw new Exception("unknown key: " + k);
|
|
5244
5714
|
}
|
|
5715
|
+
static string NormalizeKeyCombo(string combo)
|
|
5716
|
+
{
|
|
5717
|
+
if (String.IsNullOrWhiteSpace(combo)) throw new Exception("key combo required");
|
|
5718
|
+
var ordered = new List<string>(); var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
5719
|
+
foreach (string raw in combo.Split('+')) {
|
|
5720
|
+
string key = raw.Trim().ToLowerInvariant();
|
|
5721
|
+
if (key.Length == 0) throw new Exception("key combo contains an empty token; use plus for the + key");
|
|
5722
|
+
switch (key) {
|
|
5723
|
+
case "control": key = "ctrl"; break;
|
|
5724
|
+
case "lwin": case "windows": case "meta": key = "win"; break;
|
|
5725
|
+
case "return": key = "enter"; break;
|
|
5726
|
+
case "escape": key = "esc"; break;
|
|
5727
|
+
case "bksp": key = "backspace"; break;
|
|
5728
|
+
case "del": key = "delete"; break;
|
|
5729
|
+
case "ins": key = "insert"; break;
|
|
5730
|
+
case "pageup": key = "pgup"; break;
|
|
5731
|
+
case "pagedown": key = "pgdn"; break;
|
|
5732
|
+
case "capslock": key = "caps"; break;
|
|
5733
|
+
case "printscreen": key = "prtsc"; break;
|
|
5734
|
+
}
|
|
5735
|
+
VkOf(key); // parser and native dispatch accept the same key set
|
|
5736
|
+
if (seen.Add(key)) ordered.Add(key);
|
|
5737
|
+
}
|
|
5738
|
+
var result = new List<string>();
|
|
5739
|
+
foreach (string modifier in new string[] { "ctrl", "alt", "shift", "win", "rwin" })
|
|
5740
|
+
if (seen.Contains(modifier)) { result.Add(modifier); ordered.Remove(modifier); }
|
|
5741
|
+
result.AddRange(ordered);
|
|
5742
|
+
return String.Join("+", result);
|
|
5743
|
+
}
|
|
5245
5744
|
|
|
5246
5745
|
static void SendKey(ushort vk, bool up)
|
|
5247
5746
|
{
|
|
@@ -5281,11 +5780,11 @@ namespace DshComputerUse.Worker
|
|
|
5281
5780
|
|
|
5282
5781
|
static Dictionary<string, object> Key(Dictionary<string, object> a)
|
|
5283
5782
|
{
|
|
5783
|
+
if (_receiverCheck != null) _receiverCheck(); // nested paste may not silently repair a stolen receiver
|
|
5284
5784
|
string combo = GetS(a, "combo", null);
|
|
5285
5785
|
if (combo == null) throw new Exception("combo required (e.g. 'ctrl+shift+t', 'enter')");
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
if (keys.Count == 0) throw new Exception("empty combo");
|
|
5786
|
+
combo = NormalizeKeyCombo(combo);
|
|
5787
|
+
List<string> keys = new List<string>(combo.Split('+'));
|
|
5289
5788
|
AgentActing(a, "key");
|
|
5290
5789
|
// Keyboard input goes to the FOREGROUND window — re-assert the window the agent is
|
|
5291
5790
|
// working in, and report which window actually received the keys.
|
|
@@ -5300,18 +5799,38 @@ namespace DshComputerUse.Worker
|
|
|
5300
5799
|
HostGuardForeground("key");
|
|
5301
5800
|
List<ushort> vks = new List<ushort>();
|
|
5302
5801
|
foreach (string k in keys) { ushort vk = VkOf(k); if (!vks.Contains(vk)) vks.Add(vk); }
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5802
|
+
bool windowManagement = WindowManagementChord.IsExact(combo);
|
|
5803
|
+
using (GuardReceiver(new IntPtr(Convert.ToInt64(focus["hwnd"])), Convert.ToUInt32(focus["pid"]), true))
|
|
5804
|
+
{
|
|
5805
|
+
if (windowManagement)
|
|
5806
|
+
WindowManagementChord.Run(vks, SendKey, ReleaseKeyForCleanup,
|
|
5807
|
+
delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
|
|
5808
|
+
delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); },
|
|
5809
|
+
delegate { WaitForKeyHold(0); }, InvalidateKeyboardTarget);
|
|
5810
|
+
else
|
|
5811
|
+
KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
|
|
5812
|
+
delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
|
|
5813
|
+
delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
|
|
5814
|
+
}
|
|
5306
5815
|
EndActuationForReadback();
|
|
5307
5816
|
Thread.Sleep(30);
|
|
5308
|
-
Dictionary<string, object> keyOut = Dict("combo", combo,
|
|
5817
|
+
Dictionary<string, object> keyOut = AddFocusReadback(Dict("combo", combo), a, focus);
|
|
5818
|
+
if (windowManagement)
|
|
5819
|
+
{
|
|
5820
|
+
var after = (Dictionary<string, object>)keyOut["focusAfter"];
|
|
5821
|
+
keyOut["receiverChanged"] = Convert.ToInt64(focus["hwnd"]) != Convert.ToInt64(after["hwnd"]) ||
|
|
5822
|
+
Convert.ToUInt32(focus["pid"]) != Convert.ToUInt32(after["pid"]);
|
|
5823
|
+
keyOut["reobserveRequired"] = true;
|
|
5824
|
+
keyOut["effectVerified"] = false;
|
|
5825
|
+
keyOut["hint"] = KeyboardReobserveHint;
|
|
5826
|
+
}
|
|
5309
5827
|
if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
|
|
5310
5828
|
return keyOut;
|
|
5311
5829
|
}
|
|
5312
5830
|
|
|
5313
5831
|
static Dictionary<string, object> Type(Dictionary<string, object> a)
|
|
5314
5832
|
{
|
|
5833
|
+
if (_receiverCheck != null) _receiverCheck();
|
|
5315
5834
|
string text = GetS(a, "text", null);
|
|
5316
5835
|
if (text == null) throw new Exception("text required");
|
|
5317
5836
|
AgentActing(a, "type");
|
|
@@ -5320,6 +5839,7 @@ namespace DshComputerUse.Worker
|
|
|
5320
5839
|
HostGuardNamed(a, "type");
|
|
5321
5840
|
Dictionary<string, object> focus = FocusReport(a, true);
|
|
5322
5841
|
HostGuardForeground("type"); // same rule as `key`: the foreground window is the victim
|
|
5842
|
+
using (GuardReceiver(new IntPtr(Convert.ToInt64(focus["hwnd"])), Convert.ToUInt32(focus["pid"]), true)) {
|
|
5323
5843
|
// ...and whether whatever owns the focus can hold text at all. Typing into a control
|
|
5324
5844
|
// with no editable pattern is silently discarded, or interpreted as shortcuts.
|
|
5325
5845
|
string typeWarn = null;
|
|
@@ -5372,7 +5892,7 @@ namespace DshComputerUse.Worker
|
|
|
5372
5892
|
downgraded["clipboardWarning"] = "The previous clipboard could not be preserved (private or oversized formats) and was overwritten by this paste. Restore it manually if it mattered.";
|
|
5373
5893
|
if (typeWarn != null) downgraded["warning"] = typeWarn;
|
|
5374
5894
|
if (_lastRefocus != null) downgraded["refocused"] = _lastRefocus;
|
|
5375
|
-
return downgraded;
|
|
5895
|
+
return AddFocusReadback(downgraded, a, focus);
|
|
5376
5896
|
}
|
|
5377
5897
|
Exception inputError = null;
|
|
5378
5898
|
try
|
|
@@ -5406,7 +5926,7 @@ namespace DshComputerUse.Worker
|
|
|
5406
5926
|
pr["clipboardWarning"] = "Clipboard changed or could not be restored. Verify the target value before repeating input.";
|
|
5407
5927
|
if (typeWarn != null) pr["warning"] = typeWarn;
|
|
5408
5928
|
if (_lastRefocus != null) pr["refocused"] = _lastRefocus;
|
|
5409
|
-
return pr;
|
|
5929
|
+
return AddFocusReadback(pr, a, focus);
|
|
5410
5930
|
}
|
|
5411
5931
|
int charDelay = GetI(a, "charDelayMs", 8);
|
|
5412
5932
|
foreach (char ch in text)
|
|
@@ -5430,10 +5950,17 @@ namespace DshComputerUse.Worker
|
|
|
5430
5950
|
SendInputChecked(new INPUT[] { down, up });
|
|
5431
5951
|
Thread.Sleep(charDelay);
|
|
5432
5952
|
}
|
|
5433
|
-
Dictionary<string, object> tr = Dict("mode", "unicode", "chars", text.Length
|
|
5953
|
+
Dictionary<string, object> tr = Dict("mode", "unicode", "chars", text.Length);
|
|
5434
5954
|
if (typeWarn != null) tr["warning"] = typeWarn;
|
|
5435
5955
|
if (_lastRefocus != null) tr["refocused"] = _lastRefocus;
|
|
5436
|
-
return tr;
|
|
5956
|
+
return AddFocusReadback(tr, a, focus);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
static Dictionary<string, object> AddFocusReadback(Dictionary<string, object> result, Dictionary<string, object> args, Dictionary<string, object> before)
|
|
5960
|
+
{
|
|
5961
|
+
var after = FocusReport(args, false);
|
|
5962
|
+
result["focusBefore"] = before; result["focusAfter"] = after; result["focus"] = after;
|
|
5963
|
+
return result;
|
|
5437
5964
|
}
|
|
5438
5965
|
|
|
5439
5966
|
// ---------- clipboard ----------
|
|
@@ -5492,7 +6019,34 @@ namespace DshComputerUse.Worker
|
|
|
5492
6019
|
list.Add(WinDict(h));
|
|
5493
6020
|
return true;
|
|
5494
6021
|
}, IntPtr.Zero);
|
|
5495
|
-
return Dict("windows", list)
|
|
6022
|
+
return Dict("windows", list, "virtualScreen", VirtualScreenDict(),
|
|
6023
|
+
"cooperation", Panic.CooperationState(), "indicator", IndicatorSnapshot());
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
static string IndicatorStateName(int want)
|
|
6027
|
+
{
|
|
6028
|
+
switch (want) {
|
|
6029
|
+
case 0: return "idle";
|
|
6030
|
+
case 1: return "acting";
|
|
6031
|
+
case 2: return "capture";
|
|
6032
|
+
case 3: return "thinking";
|
|
6033
|
+
case 4: return "paused";
|
|
6034
|
+
case 5: return "asking";
|
|
6035
|
+
case 6: return "human-handoff";
|
|
6036
|
+
case 7: return "preparing";
|
|
6037
|
+
case 8: return "reserved";
|
|
6038
|
+
default: return "unknown";
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
|
|
6042
|
+
static Dictionary<string, object> IndicatorSnapshot()
|
|
6043
|
+
{
|
|
6044
|
+
// Want/title describe the last rendered frame, not a prediction that
|
|
6045
|
+
// an input will run. The cooperation record supplies the handoff cause.
|
|
6046
|
+
int want = Glow.Want;
|
|
6047
|
+
return Dict("state", IndicatorStateName(want), "want", want,
|
|
6048
|
+
"title", Glow.ShownTitle ?? "", "injecting", Glow.Injecting,
|
|
6049
|
+
"acting", Glow.Acting, "preparing", Glow.Preparing, "holding", Glow.Holding);
|
|
5496
6050
|
}
|
|
5497
6051
|
|
|
5498
6052
|
static IntPtr FindHwnd(Dictionary<string, object> a)
|
|
@@ -5557,6 +6111,7 @@ namespace DshComputerUse.Worker
|
|
|
5557
6111
|
_kickoffFocus.Confirm(h.ToInt64(), confirmedPid);
|
|
5558
6112
|
}
|
|
5559
6113
|
CheckHumanInput();
|
|
6114
|
+
if (how != "failed") _keyboardReobserveRequired = false;
|
|
5560
6115
|
return Dict("activated", how != "failed", "activatedVia", how,
|
|
5561
6116
|
"hwnd", h.ToInt64(), "pid", confirmedPid == 0 ? null : (object)confirmedPid,
|
|
5562
6117
|
"hint", how == "failed"
|
|
@@ -5578,6 +6133,22 @@ namespace DshComputerUse.Worker
|
|
|
5578
6133
|
// 2. if that window belongs to an ELEVATED process while we are not, UIPI silently
|
|
5579
6134
|
// drops every event — say so loudly instead of reporting a successful no-op.
|
|
5580
6135
|
static IntPtr _stickyHwnd = IntPtr.Zero;
|
|
6136
|
+
static volatile bool _keyboardReobserveRequired;
|
|
6137
|
+
const string KeyboardReobserveHint = "A window-management chord was dispatched; this is not proof that a switch succeeded. " +
|
|
6138
|
+
"Inspect computer_state {windows:true}, then use computer_window {hwnd:...,op:'activate'} and require activated:true before further keyboard input.";
|
|
6139
|
+
static void InvalidateKeyboardTarget()
|
|
6140
|
+
{
|
|
6141
|
+
_stickyHwnd = IntPtr.Zero;
|
|
6142
|
+
ResetKickoffFocus();
|
|
6143
|
+
_keyboardReobserveRequired = true;
|
|
6144
|
+
}
|
|
6145
|
+
static void CheckKeyboardReobserve()
|
|
6146
|
+
{
|
|
6147
|
+
if (!_keyboardReobserveRequired) return;
|
|
6148
|
+
var error = NotDispatched("INPUT_REOBSERVE_REQUIRED", "INPUT_REOBSERVE_REQUIRED: " + KeyboardReobserveHint);
|
|
6149
|
+
error.Data["reobserveRequired"] = true;
|
|
6150
|
+
throw error;
|
|
6151
|
+
}
|
|
5581
6152
|
static readonly KickoffFocus _kickoffFocus = new KickoffFocus();
|
|
5582
6153
|
public static void ResetKickoffFocus() { _kickoffFocus.Reset(); }
|
|
5583
6154
|
public static long RecoveryTarget(out uint pid)
|
|
@@ -6170,6 +6741,45 @@ namespace DshComputerUse.Worker
|
|
|
6170
6741
|
catch { return false; }
|
|
6171
6742
|
}
|
|
6172
6743
|
|
|
6744
|
+
static int ProcessIntegrityLevel(uint pid)
|
|
6745
|
+
{
|
|
6746
|
+
if (pid == 0) return -1;
|
|
6747
|
+
IntPtr proc = IntPtr.Zero, token = IntPtr.Zero, buffer = IntPtr.Zero;
|
|
6748
|
+
try {
|
|
6749
|
+
proc = Native.OpenProcess(Native.PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
|
6750
|
+
if (proc == IntPtr.Zero || !Native.OpenProcessToken(proc, Native.TOKEN_QUERY, out token)) return -1;
|
|
6751
|
+
uint needed;
|
|
6752
|
+
Native.GetTokenInformationBuffer(token, 25, IntPtr.Zero, 0, out needed); // TokenIntegrityLevel
|
|
6753
|
+
if (needed < IntPtr.Size || needed > 4096) return -1;
|
|
6754
|
+
buffer = Marshal.AllocHGlobal((int)needed);
|
|
6755
|
+
if (!Native.GetTokenInformationBuffer(token, 25, buffer, needed, out needed)) return -1;
|
|
6756
|
+
IntPtr sid = Marshal.ReadIntPtr(buffer);
|
|
6757
|
+
if (sid == IntPtr.Zero) return -1;
|
|
6758
|
+
IntPtr countPointer = Native.GetSidSubAuthorityCount(sid);
|
|
6759
|
+
if (countPointer == IntPtr.Zero) return -1;
|
|
6760
|
+
byte count = Marshal.ReadByte(countPointer);
|
|
6761
|
+
if (count == 0 || count > 15) return -1;
|
|
6762
|
+
IntPtr rid = Native.GetSidSubAuthority(sid, (uint)(count - 1));
|
|
6763
|
+
return rid == IntPtr.Zero ? -1 : Marshal.ReadInt32(rid);
|
|
6764
|
+
} catch { return -1; }
|
|
6765
|
+
finally {
|
|
6766
|
+
if (buffer != IntPtr.Zero) Marshal.FreeHGlobal(buffer);
|
|
6767
|
+
if (token != IntPtr.Zero) Native.CloseHandle(token);
|
|
6768
|
+
if (proc != IntPtr.Zero) Native.CloseHandle(proc);
|
|
6769
|
+
}
|
|
6770
|
+
}
|
|
6771
|
+
static bool AsyncKeyIntegrityAllows(int self, int foreground)
|
|
6772
|
+
{ return self >= 0 && foreground >= 0 && foreground <= self; }
|
|
6773
|
+
internal static bool CanObserveAsyncKeys(out IntPtr foreground)
|
|
6774
|
+
{
|
|
6775
|
+
foreground = Native.GetForegroundWindow();
|
|
6776
|
+
if (foreground == IntPtr.Zero) return false;
|
|
6777
|
+
uint pid = PidOf(foreground);
|
|
6778
|
+
// Unknown is NOT unelevated. GetAsyncKeyState reports zero for UIPI
|
|
6779
|
+
// failures too, so only known same/lower integrity is usable evidence.
|
|
6780
|
+
return AsyncKeyIntegrityAllows(ProcessIntegrityLevel((uint)Process.GetCurrentProcess().Id), ProcessIntegrityLevel(pid)) &&
|
|
6781
|
+
Native.GetForegroundWindow() == foreground && PidOf(foreground) == pid;
|
|
6782
|
+
}
|
|
6173
6783
|
static bool ProcessElevated(uint pid)
|
|
6174
6784
|
{
|
|
6175
6785
|
if (pid == 0) return false;
|
|
@@ -6309,6 +6919,7 @@ namespace DshComputerUse.Worker
|
|
|
6309
6919
|
/// reach because of integrity levels.
|
|
6310
6920
|
static Dictionary<string, object> FocusReport(Dictionary<string, object> a, bool refocus)
|
|
6311
6921
|
{
|
|
6922
|
+
if (refocus) CheckKeyboardReobserve(); // explicit old expectHwnd is not fresh authorization
|
|
6312
6923
|
IntPtr want = IntPtr.Zero;
|
|
6313
6924
|
if (a != null && a.ContainsKey("expectHwnd")) want = new IntPtr(GetI(a, "expectHwnd", 0));
|
|
6314
6925
|
else if (a != null && a.ContainsKey("hwnd")) want = new IntPtr(GetI(a, "hwnd", 0));
|
|
@@ -6386,6 +6997,43 @@ namespace DshComputerUse.Worker
|
|
|
6386
6997
|
}
|
|
6387
6998
|
|
|
6388
6999
|
// ---------- UIA ----------
|
|
7000
|
+
static void AddUiGeometry(Dictionary<string, object> d, System.Windows.Rect? bounds,
|
|
7001
|
+
bool? offscreen, Rectangle virtualScreen)
|
|
7002
|
+
{
|
|
7003
|
+
bool valid = false;
|
|
7004
|
+
if (bounds.HasValue) {
|
|
7005
|
+
System.Windows.Rect r = bounds.Value;
|
|
7006
|
+
// Empty UIA rectangles contain infinities. Casting these to int
|
|
7007
|
+
// manufactured INT32_MIN coordinates that looked like real layout.
|
|
7008
|
+
valid = !r.IsEmpty && r.Width > 0 && r.Height > 0 &&
|
|
7009
|
+
!Double.IsNaN(r.X) && !Double.IsNaN(r.Y) &&
|
|
7010
|
+
!Double.IsNaN(r.Width) && !Double.IsNaN(r.Height) &&
|
|
7011
|
+
r.X > Int32.MinValue && r.Y > Int32.MinValue &&
|
|
7012
|
+
r.Right < Int32.MaxValue && r.Bottom < Int32.MaxValue &&
|
|
7013
|
+
r.Width < Int32.MaxValue && r.Height < Int32.MaxValue;
|
|
7014
|
+
if (valid) {
|
|
7015
|
+
long left = (long)Math.Floor(r.Left), top = (long)Math.Floor(r.Top);
|
|
7016
|
+
long right = (long)Math.Ceiling(r.Right), bottom = (long)Math.Ceiling(r.Bottom);
|
|
7017
|
+
valid = left > Int32.MinValue && top > Int32.MinValue &&
|
|
7018
|
+
right - left <= Int32.MaxValue && bottom - top <= Int32.MaxValue;
|
|
7019
|
+
if (valid) {
|
|
7020
|
+
d["rect"] = Dict("x", (int)left, "y", (int)top, "width", (int)(right - left), "height", (int)(bottom - top));
|
|
7021
|
+
d["intersectsVirtualScreen"] = r.Right > virtualScreen.Left && r.Left < virtualScreen.Right &&
|
|
7022
|
+
r.Bottom > virtualScreen.Top && r.Top < virtualScreen.Bottom;
|
|
7023
|
+
}
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
d["rectValid"] = valid;
|
|
7027
|
+
d["isOffscreen"] = offscreen.HasValue ? (object)offscreen.Value : null;
|
|
7028
|
+
if (!valid) {
|
|
7029
|
+
d["rect"] = null;
|
|
7030
|
+
d["intersectsVirtualScreen"] = null;
|
|
7031
|
+
d["rectInvalidReason"] = bounds.HasValue ? "empty-or-invalid-provider-bounds" : "provider-bounds-unavailable";
|
|
7032
|
+
}
|
|
7033
|
+
// Neither a valid rectangle nor IsOffscreen=false proves that this
|
|
7034
|
+
// element owns a pixel; use the point probe for occlusion/hit testing.
|
|
7035
|
+
}
|
|
7036
|
+
|
|
6389
7037
|
static Dictionary<string, object> ElemDict(AutomationElement e)
|
|
6390
7038
|
{
|
|
6391
7039
|
Dictionary<string, object> d = new Dictionary<string, object>();
|
|
@@ -6395,10 +7043,15 @@ namespace DshComputerUse.Worker
|
|
|
6395
7043
|
string ctype = "";
|
|
6396
7044
|
try { if (e.Current.ControlType != null) ctype = e.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""); }
|
|
6397
7045
|
catch { }
|
|
6398
|
-
System.Windows.Rect r = e.Current.BoundingRectangle;
|
|
6399
7046
|
d["name"] = name;
|
|
6400
7047
|
d["role"] = ctype;
|
|
6401
|
-
|
|
7048
|
+
System.Windows.Rect? bounds = null;
|
|
7049
|
+
bool? offscreen = null;
|
|
7050
|
+
try { bounds = e.Current.BoundingRectangle; } catch { }
|
|
7051
|
+
try { offscreen = e.Current.IsOffscreen; } catch { }
|
|
7052
|
+
AddUiGeometry(d, bounds, offscreen, new Rectangle(
|
|
7053
|
+
Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN),
|
|
7054
|
+
Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
|
|
6402
7055
|
try { d["automationId"] = e.Current.AutomationId; } catch { }
|
|
6403
7056
|
try { d["className"] = e.Current.ClassName; } catch { }
|
|
6404
7057
|
try { d["runtimeId"] = String.Join(".", e.GetRuntimeId()); d["pid"] = e.Current.ProcessId; } catch { }
|
|
@@ -6424,61 +7077,205 @@ namespace DshComputerUse.Worker
|
|
|
6424
7077
|
return d;
|
|
6425
7078
|
}
|
|
6426
7079
|
|
|
7080
|
+
// >>>UIA-QUERY-BUDGET<<<
|
|
7081
|
+
// Cooperative limits between provider calls. A synchronous UIA/COM call
|
|
7082
|
+
// already in progress is NOT interruptible by this traversal budget.
|
|
7083
|
+
internal sealed class UiBudgetStop : Exception { }
|
|
7084
|
+
internal sealed class UiReadBudget
|
|
7085
|
+
{
|
|
7086
|
+
readonly Func<long> clock;
|
|
7087
|
+
readonly long started;
|
|
7088
|
+
readonly List<string> reasons = new List<string>();
|
|
7089
|
+
public readonly int BudgetMs;
|
|
7090
|
+
public int Calls, Scanned;
|
|
7091
|
+
public long MaxCallMs;
|
|
7092
|
+
public UiReadBudget(int budgetMs, Func<long> now)
|
|
7093
|
+
{ BudgetMs = Math.Max(100, Math.Min(5000, budgetMs)); clock = now; started = now(); }
|
|
7094
|
+
public long ElapsedMs { get { return Math.Max(0, clock() - started); } }
|
|
7095
|
+
public bool Truncated { get { return reasons.Count > 0; } }
|
|
7096
|
+
public void Mark(string reason) { if (!reasons.Contains(reason)) reasons.Add(reason); }
|
|
7097
|
+
public bool Continue()
|
|
7098
|
+
{ if (ElapsedMs < BudgetMs) return true; Mark("deadline"); return false; }
|
|
7099
|
+
public T Read<T>(Func<T> read)
|
|
7100
|
+
{
|
|
7101
|
+
if (!Continue()) throw new UiBudgetStop();
|
|
7102
|
+
long before = clock(); Calls++;
|
|
7103
|
+
try {
|
|
7104
|
+
T value = read();
|
|
7105
|
+
if (!Continue()) throw new UiBudgetStop();
|
|
7106
|
+
return value;
|
|
7107
|
+
} finally { MaxCallMs = Math.Max(MaxCallMs, Math.Max(0, clock() - before)); }
|
|
7108
|
+
}
|
|
7109
|
+
public Dictionary<string, object> Report(int maxNodes, int depth)
|
|
7110
|
+
{
|
|
7111
|
+
return Dict("budgetMs", BudgetMs, "elapsedMs", ElapsedMs, "maxNodes", maxNodes, "depth", depth,
|
|
7112
|
+
"scanned", Scanned, "providerCalls", Calls, "maxCallMs", MaxCallMs,
|
|
7113
|
+
"truncatedReasons", reasons.ToArray(), "singleCallInterruptible", false,
|
|
7114
|
+
"note", "Soft traversal budget checked between provider calls; one synchronous UIA/COM call can exceed it. A partial result cannot prove absence or uniqueness.");
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
static long UiClockMs()
|
|
7118
|
+
{ return (long)(Stopwatch.GetTimestamp() * (1000.0 / Stopwatch.Frequency)); }
|
|
7119
|
+
static UiReadBudget NewUiBudget(Dictionary<string, object> a)
|
|
7120
|
+
{ return new UiReadBudget(GetI(a, "budgetMs", 1200), UiClockMs); }
|
|
7121
|
+
|
|
7122
|
+
internal static void ValidateUiQueryScope(bool scoped, bool filtered, bool anchored, int depth, int maxNodes)
|
|
7123
|
+
{
|
|
7124
|
+
if (filtered && !scoped)
|
|
7125
|
+
throw new Exception("UIA_SCOPE_REQUIRED: filtered UIA queries require hwnd or an unambiguous window title; use computer_state to identify the window, or computer_uia at for a point probe.");
|
|
7126
|
+
if (!anchored && (depth > 6 || maxNodes > 300))
|
|
7127
|
+
throw new Exception("UIA_ANCHOR_REQUIRED: broad UIA traversal requires an observed anchor point in the requested window; use computer_uia at first, then anchor with depth/maxNodes limits. An exact id does not avoid traversal.");
|
|
7128
|
+
}
|
|
7129
|
+
internal static bool UiAnchorOwned(long requestedTop, uint requestedPid, long actualTop, uint actualPid)
|
|
7130
|
+
{ return requestedTop != 0 && requestedPid != 0 && requestedTop == actualTop && requestedPid == actualPid; }
|
|
7131
|
+
static AutomationElement UiQueryRoot(Dictionary<string, object> a, IntPtr root, UiReadBudget budget)
|
|
7132
|
+
{
|
|
7133
|
+
string anchor = GetS(a, "anchor", null);
|
|
7134
|
+
if (String.IsNullOrWhiteSpace(anchor)) return budget.Read(delegate { return AutomationElement.FromHandle(root); });
|
|
7135
|
+
string[] xy = anchor.Split(','); int x, y;
|
|
7136
|
+
if (xy.Length != 2 || !Int32.TryParse(xy[0].Trim(), out x) || !Int32.TryParse(xy[1].Trim(), out y))
|
|
7137
|
+
throw new Exception("UIA_ANCHOR_INVALID: anchor must be physical screen x,y");
|
|
7138
|
+
POINT point; point.X = x; point.Y = y;
|
|
7139
|
+
uint pid = PidOf(root); IntPtr expectedTop = TopLevel(root), hit = Native.WindowFromPoint(point);
|
|
7140
|
+
if (!Native.IsWindow(root) || !UiAnchorOwned(expectedTop.ToInt64(), pid, TopLevel(hit).ToInt64(), PidOf(hit)))
|
|
7141
|
+
throw new Exception("UIA_ANCHOR_MISMATCH: anchor is not currently owned by the requested window and process; no provider query was started");
|
|
7142
|
+
AutomationElement element = budget.Read(delegate { return AutomationElement.FromPoint(new System.Windows.Point(x, y)); });
|
|
7143
|
+
IntPtr afterHit = Native.WindowFromPoint(point);
|
|
7144
|
+
if (element == null || budget.Read(delegate { return element.Current.ProcessId; }) != (int)pid ||
|
|
7145
|
+
PidOf(root) != pid || !UiAnchorOwned(expectedTop.ToInt64(), pid, TopLevel(afterHit).ToInt64(), PidOf(afterHit)))
|
|
7146
|
+
throw new Exception("UIA_ANCHOR_MISMATCH: provider hit or window changed during the point probe; repeat the observation");
|
|
7147
|
+
return element;
|
|
7148
|
+
}
|
|
7149
|
+
|
|
7150
|
+
// Generic only so deterministic fake trees exercise the identical walk.
|
|
7151
|
+
// Child/sibling navigation is incremental: no FindAll collection can be
|
|
7152
|
+
// built before the client-side node/depth/deadline checks run.
|
|
7153
|
+
internal static List<T> UiWalkBounded<T>(T start, int depth, int maxNodes, int limit, UiReadBudget budget,
|
|
7154
|
+
Func<T, bool> matches, Func<T, T> firstChild, Func<T, T> nextSibling) where T : class
|
|
7155
|
+
{
|
|
7156
|
+
var found = new List<T>(); var queue = new Queue<KeyValuePair<T, int>>();
|
|
7157
|
+
if (start == null) return found;
|
|
7158
|
+
// A queue entry is a parent whose direct children form the next BFS
|
|
7159
|
+
// segment. Visit each child before asking for its next sibling: a
|
|
7160
|
+
// wide provider can no longer consume the budget before any match
|
|
7161
|
+
// in that sibling list has been inspected/materialized.
|
|
7162
|
+
queue.Enqueue(new KeyValuePair<T, int>(start, -1));
|
|
7163
|
+
while (queue.Count > 0 && budget.Scanned < maxNodes && budget.Continue())
|
|
7164
|
+
{
|
|
7165
|
+
var segment = queue.Dequeue(); int level = segment.Value + 1;
|
|
7166
|
+
try
|
|
7167
|
+
{
|
|
7168
|
+
T node = segment.Value < 0 ? segment.Key : budget.Read(delegate { return firstChild(segment.Key); });
|
|
7169
|
+
while (node != null && budget.Continue())
|
|
7170
|
+
{
|
|
7171
|
+
if (budget.Scanned >= maxNodes) { budget.Mark("max-nodes"); return found; }
|
|
7172
|
+
budget.Scanned++;
|
|
7173
|
+
try {
|
|
7174
|
+
if (matches(node)) {
|
|
7175
|
+
found.Add(node);
|
|
7176
|
+
if (found.Count >= limit) { budget.Mark("match-limit"); return found; }
|
|
7177
|
+
}
|
|
7178
|
+
if (!budget.Continue()) return found;
|
|
7179
|
+
if (level < depth) queue.Enqueue(new KeyValuePair<T, int>(node, level));
|
|
7180
|
+
else if (budget.Read(delegate { return firstChild(node); }) != null) budget.Mark("depth");
|
|
7181
|
+
} catch (UiBudgetStop) { return found; }
|
|
7182
|
+
catch { budget.Mark("provider-error"); }
|
|
7183
|
+
if (segment.Value < 0) break;
|
|
7184
|
+
if (budget.Scanned >= maxNodes) { budget.Mark("max-nodes"); return found; }
|
|
7185
|
+
T previous = node;
|
|
7186
|
+
node = budget.Read(delegate { return nextSibling(previous); });
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
catch (UiBudgetStop) { break; }
|
|
7190
|
+
catch { budget.Mark("provider-error"); }
|
|
7191
|
+
}
|
|
7192
|
+
if (queue.Count > 0) budget.Mark(budget.Scanned >= maxNodes ? "max-nodes" : "pending-nodes");
|
|
7193
|
+
return found;
|
|
7194
|
+
}
|
|
7195
|
+
// <<<UIA-QUERY-BUDGET<<<
|
|
7196
|
+
|
|
7197
|
+
static Dictionary<string, object> ElemDictBudgeted(AutomationElement e, UiReadBudget budget)
|
|
7198
|
+
{
|
|
7199
|
+
var d = new Dictionary<string, object>();
|
|
7200
|
+
string name = "", role = ""; System.Windows.Rect? bounds = null; bool? offscreen = null;
|
|
7201
|
+
try { name = budget.Read(delegate { return e.Current.Name; }) ?? ""; } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7202
|
+
try { ControlType ct = budget.Read(delegate { return e.Current.ControlType; }); if (ct != null) role = ct.ProgrammaticName.Replace("ControlType.", ""); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7203
|
+
d["name"] = name.Length > 160 ? name.Substring(0, 160) : name; d["role"] = role;
|
|
7204
|
+
try { bounds = budget.Read(delegate { return e.Current.BoundingRectangle; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7205
|
+
try { offscreen = budget.Read(delegate { return e.Current.IsOffscreen; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7206
|
+
AddUiGeometry(d, bounds, offscreen, new Rectangle(Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN), Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN)));
|
|
7207
|
+
try { d["automationId"] = budget.Read(delegate { return e.Current.AutomationId; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7208
|
+
try { d["className"] = budget.Read(delegate { return e.Current.ClassName; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7209
|
+
try { d["runtimeId"] = String.Join(".", budget.Read(delegate { return e.GetRuntimeId(); })); d["pid"] = budget.Read(delegate { return e.Current.ProcessId; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7210
|
+
try { d["isPassword"] = budget.Read(delegate { return e.Current.IsPassword; }); } catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7211
|
+
try {
|
|
7212
|
+
var patterns = new List<string>();
|
|
7213
|
+
foreach (AutomationPattern pattern in budget.Read(delegate { return e.GetSupportedPatterns(); })) {
|
|
7214
|
+
string text = pattern.ProgrammaticName.Replace("PatternIdentifiers", "").Replace("Pattern", "").TrimEnd('.', ' ');
|
|
7215
|
+
if (text.Length > 0) patterns.Add(text);
|
|
7216
|
+
}
|
|
7217
|
+
d["patterns"] = patterns;
|
|
7218
|
+
} catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7219
|
+
try {
|
|
7220
|
+
object value = budget.Read(delegate { object p; return e.TryGetCurrentPattern(ValuePattern.Pattern, out p) ? p : null; });
|
|
7221
|
+
if (value != null) d["value"] = budget.Read(delegate { return ((ValuePattern)value).Current.Value; });
|
|
7222
|
+
} catch (UiBudgetStop) { throw; } catch { budget.Mark("provider-error"); }
|
|
7223
|
+
return d;
|
|
7224
|
+
}
|
|
7225
|
+
|
|
6427
7226
|
static Dictionary<string, object> Uia(Dictionary<string, object> a)
|
|
6428
7227
|
{
|
|
6429
7228
|
IntPtr root = FindHwnd(a);
|
|
6430
|
-
AutomationElement rootEl = root != IntPtr.Zero ? AutomationElement.FromHandle(root) : AutomationElement.RootElement;
|
|
6431
7229
|
int maxDepth = Math.Max(1, Math.Min(24, GetI(a, "depth", 6)));
|
|
6432
|
-
int maxNodes = Math.Max(10, Math.Min(1200, GetI(a, "maxNodes",
|
|
6433
|
-
|
|
7230
|
+
int maxNodes = Math.Max(10, Math.Min(1200, GetI(a, "maxNodes", 120)));
|
|
7231
|
+
bool query = GetB(a, "query", false), anchored = !String.IsNullOrWhiteSpace(GetS(a, "anchor", null));
|
|
7232
|
+
ValidateUiQueryScope(root != IntPtr.Zero, query, anchored, maxDepth, maxNodes);
|
|
7233
|
+
if (root == IntPtr.Zero) root = Native.GetForegroundWindow();
|
|
7234
|
+
if (root == IntPtr.Zero) throw new Exception("UIA_SCOPE_REQUIRED: no foreground window; specify hwnd");
|
|
7235
|
+
UiReadBudget budget = NewUiBudget(a);
|
|
7236
|
+
AutomationElement rootEl;
|
|
7237
|
+
try { rootEl = UiQueryRoot(a, root, budget); }
|
|
7238
|
+
catch (UiBudgetStop) { return Dict("flat", new List<object>(), "count", 0, "scanned", 0, "truncated", true, "budget", budget.Report(maxNodes, maxDepth), "hwnd", root.ToInt64(), "pid", PidOf(root)); }
|
|
7239
|
+
if (query)
|
|
6434
7240
|
{
|
|
6435
|
-
int scanned; bool truncated;
|
|
6436
|
-
List<AutomationElement> matches = FindUiMatches(rootEl, GetS(a, "nameContains", null),
|
|
6437
|
-
GetS(a, "automationId", null), GetS(a, "role", null), maxDepth, maxNodes,
|
|
6438
|
-
Math.Max(1, Math.Min(80, GetI(a, "limit", 80))), out scanned, out truncated);
|
|
6439
7241
|
List<object> selected = new List<object>();
|
|
6440
7242
|
string witness = root != IntPtr.Zero && GetB(a, "targets", false) ? UiWindowWitness(root) : null;
|
|
6441
7243
|
var ancestorCache = new Dictionary<string, List<object>>();
|
|
6442
|
-
|
|
6443
|
-
{
|
|
6444
|
-
try {
|
|
6445
|
-
|
|
7244
|
+
AutomationElement evidenceRoot = rootEl;
|
|
7245
|
+
if (witness != null && anchored) {
|
|
7246
|
+
try { evidenceRoot = budget.Read(delegate { return AutomationElement.FromHandle(root); }); }
|
|
7247
|
+
catch (UiBudgetStop) { evidenceRoot = null; }
|
|
7248
|
+
}
|
|
7249
|
+
string nameFilter = GetS(a, "nameContains", null), idFilter = GetS(a, "automationId", null), roleFilter = GetS(a, "role", null);
|
|
7250
|
+
CacheRequest selectorCache = UiSelectorCache();
|
|
7251
|
+
UiWalkBounded(rootEl, maxDepth, maxNodes, Math.Max(1, Math.Min(80, GetI(a, "limit", 80))), budget,
|
|
7252
|
+
delegate(AutomationElement match) {
|
|
7253
|
+
if (!ElementMatchesBudgeted(match, nameFilter, idFilter, roleFilter, budget)) return false;
|
|
7254
|
+
var item = ElemDictBudgeted(match, budget);
|
|
6446
7255
|
if (witness != null) {
|
|
6447
|
-
try {
|
|
7256
|
+
try {
|
|
7257
|
+
if (evidenceRoot == null) throw new UiBudgetStop();
|
|
7258
|
+
item["target"] = RememberUiTarget(match, root, witness, evidenceRoot, ancestorCache, item, budget);
|
|
7259
|
+
}
|
|
7260
|
+
catch (UiBudgetStop) { item["targetUnavailable"] = true; }
|
|
6448
7261
|
catch { item["targetUnavailable"] = true; }
|
|
6449
7262
|
}
|
|
6450
7263
|
selected.Add(item);
|
|
6451
|
-
|
|
6452
|
-
|
|
7264
|
+
return true;
|
|
7265
|
+
},
|
|
7266
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element, selectorCache); },
|
|
7267
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element, selectorCache); });
|
|
6453
7268
|
if (witness != null && witness != UiWindowWitness(root)) throw new Exception("TARGET_STALE: window changed during observation; retry the query");
|
|
6454
|
-
return Dict("flat", selected, "count", selected.Count, "scanned",
|
|
6455
|
-
"hwnd", root.ToInt64(), "pid", root
|
|
7269
|
+
return Dict("flat", selected, "count", selected.Count, "scanned", budget.Scanned, "truncated", budget.Truncated,
|
|
7270
|
+
"hwnd", root.ToInt64(), "pid", PidOf(root), "budget", budget.Report(maxNodes, maxDepth));
|
|
6456
7271
|
}
|
|
6457
7272
|
List<object> flat = new List<object>();
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
KeyValuePair<AutomationElement, int> kv = q.Dequeue();
|
|
6465
|
-
try { flat.Add(ElemDict(kv.Key)); }
|
|
6466
|
-
catch { treeTruncated = true; continue; }
|
|
6467
|
-
if (kv.Value < maxDepth)
|
|
6468
|
-
{
|
|
6469
|
-
try
|
|
6470
|
-
{
|
|
6471
|
-
foreach (AutomationElement c in kv.Key.FindAll(TreeScope.Children, Condition.TrueCondition))
|
|
6472
|
-
{
|
|
6473
|
-
if (flat.Count + q.Count >= maxNodes) { treeTruncated = true; break; }
|
|
6474
|
-
q.Enqueue(new KeyValuePair<AutomationElement, int>(c, kv.Value + 1));
|
|
6475
|
-
}
|
|
6476
|
-
}
|
|
6477
|
-
catch { treeTruncated = true; }
|
|
6478
|
-
}
|
|
6479
|
-
else try { if (kv.Key.FindFirst(TreeScope.Children, Condition.TrueCondition) != null) treeTruncated = true; } catch { treeTruncated = true; }
|
|
6480
|
-
}
|
|
6481
|
-
return Dict("root", ElemDict(rootEl), "flat", flat, "count", flat.Count, "truncated", treeTruncated || q.Count > 0);
|
|
7273
|
+
UiWalkBounded(rootEl, maxDepth, maxNodes, maxNodes + 1, budget,
|
|
7274
|
+
delegate(AutomationElement element) { flat.Add(ElemDictBudgeted(element, budget)); return false; },
|
|
7275
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element); },
|
|
7276
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element); });
|
|
7277
|
+
return Dict("root", flat.Count > 0 ? flat[0] : null, "flat", flat, "count", flat.Count,
|
|
7278
|
+
"truncated", budget.Truncated, "budget", budget.Report(maxNodes, maxDepth), "hwnd", root.ToInt64(), "pid", PidOf(root));
|
|
6482
7279
|
}
|
|
6483
7280
|
|
|
6484
7281
|
// Observed native identities are private to this worker, bounded, and never reconstructed
|
|
@@ -6502,31 +7299,35 @@ namespace DshComputerUse.Worker
|
|
|
6502
7299
|
return Json.Serialize(new object[] { hwnd.ToInt64(), pid, started, WinTitleOf(hwnd), ClassOf(hwnd), rect.Left, rect.Top, rect.Right, rect.Bottom });
|
|
6503
7300
|
}
|
|
6504
7301
|
static string UiText(string text) { return (text ?? "").Normalize(NormalizationForm.FormKC).Trim(); }
|
|
6505
|
-
static
|
|
7302
|
+
static T UiReadMaybe<T>(UiReadBudget budget, Func<T> read)
|
|
7303
|
+
{ return budget == null ? read() : budget.Read(read); }
|
|
7304
|
+
static object UiSemanticPart(AutomationElement element, UiReadBudget budget = null)
|
|
6506
7305
|
{
|
|
6507
|
-
return new object[] {
|
|
7306
|
+
return new object[] { UiReadMaybe(budget, delegate { return element.Current.ControlType; }).ProgrammaticName,
|
|
7307
|
+
UiText(UiReadMaybe(budget, delegate { return element.Current.Name; })),
|
|
7308
|
+
UiReadMaybe(budget, delegate { return element.Current.AutomationId; }), UiReadMaybe(budget, delegate { return element.Current.ClassName; }) };
|
|
6508
7309
|
}
|
|
6509
|
-
static string UiTargetEvidence(AutomationElement element, IntPtr hwnd)
|
|
7310
|
+
static string UiTargetEvidence(AutomationElement element, IntPtr hwnd, UiReadBudget budget = null)
|
|
6510
7311
|
{
|
|
6511
|
-
return UiTargetEvidence(element, AutomationElement.FromHandle(hwnd), new Dictionary<string, List<object>>(), null);
|
|
7312
|
+
return UiTargetEvidence(element, UiReadMaybe(budget, delegate { return AutomationElement.FromHandle(hwnd); }), new Dictionary<string, List<object>>(), null, budget);
|
|
6512
7313
|
}
|
|
6513
|
-
static string UiTargetEvidence(AutomationElement element, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item)
|
|
7314
|
+
static string UiTargetEvidence(AutomationElement element, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item, UiReadBudget budget = null)
|
|
6514
7315
|
{
|
|
6515
7316
|
var ancestors = new List<object>();
|
|
6516
|
-
AutomationElement current = TreeWalker.ControlViewWalker.GetParent(element);
|
|
6517
|
-
string parentId = current == null ? "" : String.Join(".", current.GetRuntimeId());
|
|
7317
|
+
AutomationElement current = UiReadMaybe(budget, delegate { return TreeWalker.ControlViewWalker.GetParent(element); });
|
|
7318
|
+
string parentId = current == null ? "" : String.Join(".", UiReadMaybe(budget, delegate { return current.GetRuntimeId(); }));
|
|
6518
7319
|
if (parentId.Length == 0 || !cache.TryGetValue(parentId, out ancestors)) {
|
|
6519
7320
|
ancestors = new List<object>();
|
|
6520
|
-
bool rooted = Automation.Compare(element, root);
|
|
7321
|
+
bool rooted = UiReadMaybe(budget, delegate { return Automation.Compare(element, root); });
|
|
6521
7322
|
for (int depth = 0; current != null && depth < 24; depth++) {
|
|
6522
|
-
ancestors.Add(UiSemanticPart(current));
|
|
6523
|
-
if (Automation.Compare(current, root)) { rooted = true; break; }
|
|
6524
|
-
current = TreeWalker.ControlViewWalker.GetParent(current);
|
|
7323
|
+
ancestors.Add(UiSemanticPart(current, budget));
|
|
7324
|
+
if (UiReadMaybe(budget, delegate { return Automation.Compare(current, root); })) { rooted = true; break; }
|
|
7325
|
+
current = UiReadMaybe(budget, delegate { return TreeWalker.ControlViewWalker.GetParent(current); });
|
|
6525
7326
|
}
|
|
6526
7327
|
if (!rooted) throw new Exception("TARGET_STALE: control is outside the observed window or ancestry is incomplete");
|
|
6527
7328
|
if (parentId.Length > 0) cache[parentId] = ancestors;
|
|
6528
7329
|
}
|
|
6529
|
-
if (item == null) item = ElemDict(element);
|
|
7330
|
+
if (item == null) item = budget == null ? ElemDict(element) : ElemDictBudgeted(element, budget);
|
|
6530
7331
|
var patterns = new List<string>();
|
|
6531
7332
|
object raw;
|
|
6532
7333
|
if (!item.TryGetValue("patterns", out raw)) throw new Exception("TARGET_STALE: native patterns unavailable");
|
|
@@ -6534,14 +7335,14 @@ namespace DshComputerUse.Worker
|
|
|
6534
7335
|
patterns.Sort(StringComparer.Ordinal);
|
|
6535
7336
|
// AutomationId is a separate identity channel, so a unique semantic replacement can
|
|
6536
7337
|
// still be recognized if a provider regenerates IDs. Ancestor identities remain strict.
|
|
6537
|
-
return Json.Serialize(new object[] { item["role"], UiText(element.Current.Name),
|
|
7338
|
+
return Json.Serialize(new object[] { item["role"], UiText(UiReadMaybe(budget, delegate { return element.Current.Name; })),
|
|
6538
7339
|
item["className"], item["isPassword"], patterns, ancestors });
|
|
6539
7340
|
}
|
|
6540
|
-
static string RememberUiTarget(AutomationElement element, IntPtr hwnd, string witness, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item)
|
|
7341
|
+
static string RememberUiTarget(AutomationElement element, IntPtr hwnd, string witness, AutomationElement root, Dictionary<string, List<object>> cache, Dictionary<string, object> item, UiReadBudget budget = null)
|
|
6541
7342
|
{
|
|
6542
7343
|
string runtimeId = GetS(item, "runtimeId", "");
|
|
6543
7344
|
if (runtimeId.Length == 0) throw new Exception("native control has no runtime identity");
|
|
6544
|
-
string evidence = UiTargetEvidence(element, root, cache, item);
|
|
7345
|
+
string evidence = UiTargetEvidence(element, root, cache, item, budget);
|
|
6545
7346
|
string autoId = GetS(item, "automationId", "");
|
|
6546
7347
|
string key = Json.Serialize(new object[] { witness, runtimeId, evidence, autoId });
|
|
6547
7348
|
string existing; ObservedUiTarget old;
|
|
@@ -6550,7 +7351,7 @@ namespace DshComputerUse.Worker
|
|
|
6550
7351
|
}
|
|
6551
7352
|
string token = "T" + Guid.NewGuid().ToString("N");
|
|
6552
7353
|
var target = new ObservedUiTarget { Token = token, Key = key, Hwnd = hwnd, Witness = witness, RuntimeId = runtimeId,
|
|
6553
|
-
Evidence = evidence, AutoId = autoId, Name = element.Current.Name,
|
|
7354
|
+
Evidence = evidence, AutoId = autoId, Name = UiReadMaybe(budget, delegate { return element.Current.Name; }),
|
|
6554
7355
|
Role = GetS(item, "role", ""), Element = element, At = NowMs() };
|
|
6555
7356
|
UiTargets[token] = target; UiTargetKeys[key] = token; UiTargetOrder.Enqueue(token);
|
|
6556
7357
|
while (UiTargets.Count > 512) {
|
|
@@ -6568,36 +7369,38 @@ namespace DshComputerUse.Worker
|
|
|
6568
7369
|
throw NotDispatched("TARGET_STALE", "TARGET_STALE: window identity, title or bounds changed; query again; no action sent");
|
|
6569
7370
|
return target;
|
|
6570
7371
|
}
|
|
6571
|
-
static AutomationElement ResolveUiTarget(ObservedUiTarget target, bool rebind, int depth, int budget, out string mode, out int seen
|
|
7372
|
+
static AutomationElement ResolveUiTarget(ObservedUiTarget target, bool rebind, int depth, int budget, out string mode, out int seen,
|
|
7373
|
+
UiReadBudget readBudget = null, Dictionary<string, object> scope = null)
|
|
6572
7374
|
{
|
|
7375
|
+
if (readBudget == null) readBudget = new UiReadBudget(1200, UiClockMs);
|
|
6573
7376
|
mode = "runtime-id"; seen = 0;
|
|
6574
7377
|
try {
|
|
6575
|
-
if (String.Join(".", target.Element.GetRuntimeId()) == target.RuntimeId &&
|
|
6576
|
-
target.Element.Current.AutomationId == target.AutoId && UiTargetEvidence(target.Element, target.Hwnd) == target.Evidence)
|
|
7378
|
+
if (String.Join(".", readBudget.Read(delegate { return target.Element.GetRuntimeId(); })) == target.RuntimeId &&
|
|
7379
|
+
readBudget.Read(delegate { return target.Element.Current.AutomationId; }) == target.AutoId && UiTargetEvidence(target.Element, target.Hwnd, readBudget) == target.Evidence)
|
|
6577
7380
|
return target.Element;
|
|
6578
|
-
} catch { }
|
|
7381
|
+
} catch (UiBudgetStop) { throw; } catch { }
|
|
6579
7382
|
if (!rebind) throw new Exception("TARGET_STALE: observed control was replaced or changed; query again or explicitly request rebind; no action sent");
|
|
6580
|
-
var start = AutomationElement.FromHandle(target.Hwnd);
|
|
7383
|
+
var start = scope == null ? readBudget.Read(delegate { return AutomationElement.FromHandle(target.Hwnd); }) : UiQueryRoot(scope, target.Hwnd, readBudget);
|
|
6581
7384
|
bool truncated; int visited;
|
|
6582
7385
|
List<AutomationElement> candidates = new List<AutomationElement>();
|
|
6583
7386
|
if (!String.IsNullOrEmpty(target.AutoId)) {
|
|
6584
|
-
candidates =
|
|
7387
|
+
candidates = FindUiMatchesBudgeted(start, null, target.AutoId, null, depth, budget, budget, readBudget, out visited, out truncated);
|
|
6585
7388
|
seen += visited;
|
|
6586
7389
|
if (truncated) throw new Exception("INCOMPLETE_UIA_SEARCH: cannot prove unique target recovery; no action sent");
|
|
6587
7390
|
if (candidates.Count > 1) throw new Exception("AMBIGUOUS_UIA_TARGET: native identity matches multiple controls; no action sent");
|
|
6588
7391
|
if (candidates.Count == 1) {
|
|
6589
|
-
if (UiTargetEvidence(candidates[0], target.Hwnd) != target.Evidence)
|
|
7392
|
+
if (UiTargetEvidence(candidates[0], target.Hwnd, readBudget) != target.Evidence)
|
|
6590
7393
|
throw new Exception("TARGET_SEMANTICS_CHANGED: native ID now names a different control/context; no action sent");
|
|
6591
7394
|
mode = "native-identifier";
|
|
6592
7395
|
}
|
|
6593
7396
|
}
|
|
6594
7397
|
if (candidates.Count == 0) {
|
|
6595
7398
|
if (String.IsNullOrWhiteSpace(target.Name)) throw new Exception("TARGET_STALE: target has no usable semantic name; no action sent");
|
|
6596
|
-
var matches =
|
|
7399
|
+
var matches = FindUiMatchesBudgeted(start, target.Name, null, target.Role, depth, budget, budget, readBudget, out visited, out truncated);
|
|
6597
7400
|
seen += visited;
|
|
6598
7401
|
if (truncated) throw new Exception("INCOMPLETE_UIA_SEARCH: cannot prove unique semantic recovery; no action sent");
|
|
6599
7402
|
foreach (AutomationElement element in matches)
|
|
6600
|
-
if (UiTargetEvidence(element, target.Hwnd) == target.Evidence) candidates.Add(element);
|
|
7403
|
+
if (UiTargetEvidence(element, target.Hwnd, readBudget) == target.Evidence) candidates.Add(element);
|
|
6601
7404
|
if (candidates.Count > 1) throw new Exception("AMBIGUOUS_UIA_TARGET: semantic recovery matches multiple controls; no action sent");
|
|
6602
7405
|
if (candidates.Count == 0) throw new Exception("TARGET_STALE: no control retains the observed identity/context; no action sent");
|
|
6603
7406
|
mode = "semantic";
|
|
@@ -6615,9 +7418,28 @@ namespace DshComputerUse.Worker
|
|
|
6615
7418
|
// button, expanding a dropdown, filling a field.
|
|
6616
7419
|
static bool ElementMatches(AutomationElement e, string nameFilter, string autoIdFilter, string roleFilter)
|
|
6617
7420
|
{
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
7421
|
+
return ElementMatchesBudgeted(e, nameFilter, autoIdFilter, roleFilter, null);
|
|
7422
|
+
}
|
|
7423
|
+
static CacheRequest UiSelectorCache()
|
|
7424
|
+
{
|
|
7425
|
+
// Only the current navigated element, never a cached subtree. This
|
|
7426
|
+
// batches the three selector properties into the navigation call.
|
|
7427
|
+
var cache = new CacheRequest(); cache.TreeScope = TreeScope.Element;
|
|
7428
|
+
cache.AutomationElementMode = AutomationElementMode.Full;
|
|
7429
|
+
cache.Add(AutomationElement.NameProperty); cache.Add(AutomationElement.AutomationIdProperty);
|
|
7430
|
+
cache.Add(AutomationElement.ControlTypeProperty); return cache;
|
|
7431
|
+
}
|
|
7432
|
+
static T UiSelectorRead<T>(UiReadBudget budget, Func<T> cached, Func<T> current)
|
|
7433
|
+
{
|
|
7434
|
+
if (budget != null && !budget.Continue()) throw new UiBudgetStop();
|
|
7435
|
+
try { return cached(); }
|
|
7436
|
+
catch (InvalidOperationException) { return UiReadMaybe(budget, current); }
|
|
7437
|
+
}
|
|
7438
|
+
static bool ElementMatchesBudgeted(AutomationElement e, string nameFilter, string autoIdFilter, string roleFilter, UiReadBudget budget)
|
|
7439
|
+
{
|
|
7440
|
+
if (autoIdFilter != null && !String.Equals(UiSelectorRead(budget, delegate { return e.Cached.AutomationId; }, delegate { return e.Current.AutomationId; }), autoIdFilter, StringComparison.Ordinal)) return false;
|
|
7441
|
+
if (roleFilter != null && !String.Equals(UiSelectorRead(budget, delegate { return e.Cached.ControlType; }, delegate { return e.Current.ControlType; }).ProgrammaticName.Replace("ControlType.", ""), roleFilter, StringComparison.OrdinalIgnoreCase)) return false;
|
|
7442
|
+
if (nameFilter != null && (UiSelectorRead(budget, delegate { return e.Cached.Name; }, delegate { return e.Current.Name; }) ?? "").IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0) return false;
|
|
6621
7443
|
return true;
|
|
6622
7444
|
}
|
|
6623
7445
|
|
|
@@ -6626,32 +7448,19 @@ namespace DshComputerUse.Worker
|
|
|
6626
7448
|
static List<AutomationElement> FindUiMatches(AutomationElement start, string name, string autoId,
|
|
6627
7449
|
string role, int depth, int maxNodes, int limit, out int seen, out bool truncated)
|
|
6628
7450
|
{
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
}
|
|
6643
|
-
if (kv.Value < depth)
|
|
6644
|
-
foreach (AutomationElement child in kv.Key.FindAll(TreeScope.Children, Condition.TrueCondition))
|
|
6645
|
-
{
|
|
6646
|
-
if (seen + q.Count >= maxNodes) { truncated = true; break; }
|
|
6647
|
-
q.Enqueue(new KeyValuePair<AutomationElement, int>(child, kv.Value + 1));
|
|
6648
|
-
}
|
|
6649
|
-
else if (kv.Key.FindFirst(TreeScope.Children, Condition.TrueCondition) != null)
|
|
6650
|
-
truncated = true;
|
|
6651
|
-
}
|
|
6652
|
-
catch { truncated = true; }
|
|
6653
|
-
}
|
|
6654
|
-
if (q.Count > 0) truncated = true;
|
|
7451
|
+
return FindUiMatchesBudgeted(start, name, autoId, role, depth, maxNodes, limit,
|
|
7452
|
+
new UiReadBudget(1200, UiClockMs), out seen, out truncated);
|
|
7453
|
+
}
|
|
7454
|
+
static List<AutomationElement> FindUiMatchesBudgeted(AutomationElement start, string name, string autoId,
|
|
7455
|
+
string role, int depth, int maxNodes, int limit, UiReadBudget budget, out int seen, out bool truncated)
|
|
7456
|
+
{
|
|
7457
|
+
int before = budget.Scanned;
|
|
7458
|
+
CacheRequest selectorCache = UiSelectorCache();
|
|
7459
|
+
var matches = UiWalkBounded(start, depth, maxNodes, limit, budget,
|
|
7460
|
+
delegate(AutomationElement element) { return ElementMatchesBudgeted(element, name, autoId, role, budget); },
|
|
7461
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetFirstChild(element, selectorCache); },
|
|
7462
|
+
delegate(AutomationElement element) { return TreeWalker.RawViewWalker.GetNextSibling(element, selectorCache); });
|
|
7463
|
+
seen = budget.Scanned - before; truncated = budget.Truncated;
|
|
6655
7464
|
return matches;
|
|
6656
7465
|
}
|
|
6657
7466
|
|
|
@@ -6665,35 +7474,46 @@ namespace DshComputerUse.Worker
|
|
|
6665
7474
|
IntPtr root = observed == null ? FindHwnd(a) : observed.Hwnd;
|
|
6666
7475
|
if (observed != null && a.ContainsKey("hwnd") && FindHwnd(a) != root) throw new Exception("TARGET_STALE: target belongs to another window; no action sent");
|
|
6667
7476
|
if (root != IntPtr.Zero) HostGuard(root, "uiaAct");
|
|
6668
|
-
AutomationElement start = root != IntPtr.Zero ? AutomationElement.FromHandle(root) : AutomationElement.RootElement;
|
|
6669
7477
|
string name = GetS(a, "nameContains", null);
|
|
6670
7478
|
string autoId = GetS(a, "automationId", null);
|
|
6671
7479
|
string role = GetS(a, "role", null);
|
|
6672
7480
|
int index = Math.Max(0, GetI(a, "index", 0));
|
|
6673
|
-
int depth = Math.Max(1, Math.Min(24, GetI(a, "depth",
|
|
6674
|
-
int maxNodes = Math.Max(20, Math.Min(
|
|
7481
|
+
int depth = Math.Max(1, Math.Min(24, GetI(a, "depth", 6)));
|
|
7482
|
+
int maxNodes = Math.Max(20, Math.Min(1200, GetI(a, "maxNodes", 120)));
|
|
7483
|
+
bool rebind = GetB(a, "allowRebind", false);
|
|
7484
|
+
if (observed == null || rebind) ValidateUiQueryScope(root != IntPtr.Zero, true,
|
|
7485
|
+
!String.IsNullOrWhiteSpace(GetS(a, "anchor", null)), depth, maxNodes);
|
|
7486
|
+
UiReadBudget readBudget = NewUiBudget(a);
|
|
6675
7487
|
bool unique = GetB(a, "requireUnique", !a.ContainsKey("index"));
|
|
6676
7488
|
List<object> skipped = new List<object>();
|
|
6677
7489
|
int seen = 0; bool truncated = false; string resolution = null;
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
7490
|
+
List<AutomationElement> matches;
|
|
7491
|
+
try {
|
|
7492
|
+
matches = observed == null ? FindUiMatchesBudgeted(UiQueryRoot(a, root, readBudget), name, autoId, role, depth, maxNodes,
|
|
7493
|
+
unique ? 2 : index + 1, readBudget, out seen, out truncated) : new List<AutomationElement> {
|
|
7494
|
+
ResolveUiTarget(observed, rebind, depth, maxNodes, out resolution, out seen, readBudget, a) };
|
|
7495
|
+
} catch (UiBudgetStop) {
|
|
7496
|
+
throw new Exception("INCOMPLETE_UIA_SEARCH: soft provider-read budget exhausted; no action sent. Observe a narrower point/anchor or refresh once; a synchronous COM call cannot be interrupted by this budget.");
|
|
7497
|
+
}
|
|
7498
|
+
truncated = truncated || readBudget.Truncated;
|
|
7499
|
+
if (!readBudget.Continue())
|
|
7500
|
+
throw new Exception("INCOMPLETE_UIA_SEARCH: provider-read budget exhausted; no action sent; use a narrower point/anchor");
|
|
6681
7501
|
if (unique && matches.Count > 1)
|
|
6682
7502
|
throw new Exception("AMBIGUOUS_UIA_TARGET: multiple controls match; no action sent. Query computer_uia, then specify hwnd and an exact id.");
|
|
6683
7503
|
if (unique && truncated)
|
|
6684
7504
|
throw new Exception("INCOMPLETE_UIA_SEARCH: uniqueness could not be verified; no action sent. Narrow the target window or query with greater depth.");
|
|
6685
7505
|
AutomationElement found = matches.Count > index ? matches[index] : null;
|
|
6686
7506
|
if (!unique) for (int i = 0; i < Math.Min(index, matches.Count); i++)
|
|
6687
|
-
try { skipped.Add(
|
|
7507
|
+
try { skipped.Add(ElemDictBudgeted(matches[i], readBudget)); }
|
|
7508
|
+
catch (UiBudgetStop) { throw new Exception("INCOMPLETE_UIA_SEARCH: provider-read budget exhausted before action; no action sent"); } catch { }
|
|
6688
7509
|
if (found == null)
|
|
6689
7510
|
throw new Exception("uiaAct: no element matched (nameContains=" + (name ?? "-") +
|
|
6690
7511
|
", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
|
|
6691
7512
|
", index=" + index + ") among " + seen + " elements");
|
|
6692
7513
|
|
|
6693
7514
|
// A UIA mutation shares the same actual-action boundary and takeover checks.
|
|
6694
|
-
//
|
|
6695
|
-
//
|
|
6696
|
-
// its automation pattern either.
|
|
7515
|
+
// Scoped selectors and observed targets still need a final process
|
|
7516
|
+
// guard; a host control is never driven through its automation pattern.
|
|
6697
7517
|
int foundPid = 0;
|
|
6698
7518
|
try { foundPid = found.Current.ProcessId; } catch { }
|
|
6699
7519
|
HostGuardPid(foundPid, "uiaAct");
|
|
@@ -7027,6 +7847,8 @@ namespace DshComputerUse.Worker
|
|
|
7027
7847
|
{
|
|
7028
7848
|
var result = Err(error.Message);
|
|
7029
7849
|
if (error.Data["glowHealth"] != null) result["glowHealth"] = error.Data["glowHealth"];
|
|
7850
|
+
foreach (string key in new string[] { "dragStarted", "expectedCursor", "actualCursor", "cursorCheck" })
|
|
7851
|
+
if (error.Data.Contains(key)) result[key] = error.Data[key];
|
|
7030
7852
|
if (error.Data["code"] != null)
|
|
7031
7853
|
{
|
|
7032
7854
|
result["code"] = (string)error.Data["code"];
|
|
@@ -7120,3 +7942,742 @@ namespace DshComputerUse.Worker
|
|
|
7120
7942
|
}
|
|
7121
7943
|
}
|
|
7122
7944
|
}
|
|
7945
|
+
|
|
7946
|
+
namespace DshComputerUse.Worker
|
|
7947
|
+
{
|
|
7948
|
+
/// A small, capture-excluded visual replacement at the native pointer hotspot.
|
|
7949
|
+
/// Native cursor resources remain intact for hit evidence and screenshot burn-in.
|
|
7950
|
+
/// Update/Hide/Dispose are called only by Glow's window-owning render thread.
|
|
7951
|
+
public static class AgentPointer
|
|
7952
|
+
{
|
|
7953
|
+
const int LogicalWidth = KitePointerPainter.LogicalWidth, LogicalHeight = KitePointerPainter.LogicalHeight;
|
|
7954
|
+
const int WS_POPUP = unchecked((int)0x80000000), WS_DISABLED = 0x08000000;
|
|
7955
|
+
const int CompanionStyle = 0x00080000 | 0x00000020 | 0x00000080 | 0x08000000 | 0x00000008;
|
|
7956
|
+
[StructLayout(LayoutKind.Sequential)] struct Pt { public int X, Y; }
|
|
7957
|
+
[StructLayout(LayoutKind.Sequential)] struct Sz { public int X, Y; }
|
|
7958
|
+
[StructLayout(LayoutKind.Sequential)] struct Rc { public int Left, Top, Right, Bottom; }
|
|
7959
|
+
[StructLayout(LayoutKind.Sequential)] struct MonitorInfo { public uint Size; public Rc Monitor, Work; public uint Flags; }
|
|
7960
|
+
[StructLayout(LayoutKind.Sequential)] struct CursorInfo { public uint Size, Flags; public IntPtr Cursor; public Pt Position; }
|
|
7961
|
+
[StructLayout(LayoutKind.Sequential, Pack = 1)] struct Blend { public byte Op, Flags, Alpha, Format; }
|
|
7962
|
+
[StructLayout(LayoutKind.Sequential)] struct BitmapHeader
|
|
7963
|
+
{
|
|
7964
|
+
public uint Size; public int Width, Height; public ushort Planes, BitCount;
|
|
7965
|
+
public uint Compression, SizeImage; public int XPelsPerMeter, YPelsPerMeter;
|
|
7966
|
+
public uint ClrUsed, ClrImportant;
|
|
7967
|
+
}
|
|
7968
|
+
[StructLayout(LayoutKind.Sequential)] struct BitmapInfo { public BitmapHeader Header; public uint Color; }
|
|
7969
|
+
[StructLayout(LayoutKind.Sequential)] struct Msg
|
|
7970
|
+
{
|
|
7971
|
+
public IntPtr Hwnd; public uint Message; public UIntPtr WParam;
|
|
7972
|
+
public IntPtr LParam; public uint Time; public Pt Point; public uint Private;
|
|
7973
|
+
}
|
|
7974
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
7975
|
+
static extern IntPtr CreateWindowEx(int ex, string cls, string title, int style, int x, int y, int w, int h, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
|
|
7976
|
+
[DllImport("user32.dll")] static extern bool DestroyWindow(IntPtr hwnd);
|
|
7977
|
+
[DllImport("user32.dll")] static extern bool ShowWindow(IntPtr hwnd, int command);
|
|
7978
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd);
|
|
7979
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool SetWindowPos(IntPtr hwnd, IntPtr after, int x, int y, int w, int h, uint flags);
|
|
7980
|
+
[DllImport("user32.dll")] static extern bool GetCursorInfo(ref CursorInfo info);
|
|
7981
|
+
[DllImport("user32.dll")] static extern IntPtr MonitorFromPoint(Pt point, uint flags);
|
|
7982
|
+
[DllImport("user32.dll")] static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
|
|
7983
|
+
[DllImport("shcore.dll")] static extern int GetDpiForMonitor(IntPtr monitor, int kind, out uint x, out uint y);
|
|
7984
|
+
[DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
|
|
7985
|
+
[DllImport("user32.dll")] static extern IntPtr GetDC(IntPtr hwnd);
|
|
7986
|
+
[DllImport("user32.dll")] static extern int ReleaseDC(IntPtr hwnd, IntPtr dc);
|
|
7987
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool PeekMessage(out Msg msg, IntPtr hwnd, uint min, uint max, uint remove);
|
|
7988
|
+
[DllImport("user32.dll")] static extern bool ValidateRect(IntPtr hwnd, IntPtr rect);
|
|
7989
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern IntPtr DispatchMessage(ref Msg msg);
|
|
7990
|
+
[DllImport("gdi32.dll")] static extern IntPtr CreateCompatibleDC(IntPtr dc);
|
|
7991
|
+
[DllImport("gdi32.dll")] static extern bool DeleteDC(IntPtr dc);
|
|
7992
|
+
[DllImport("gdi32.dll")] static extern IntPtr SelectObject(IntPtr dc, IntPtr value);
|
|
7993
|
+
[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr value);
|
|
7994
|
+
[DllImport("gdi32.dll")] static extern IntPtr CreateDIBSection(IntPtr dc, ref BitmapInfo info, uint use, out IntPtr bits, IntPtr section, uint offset);
|
|
7995
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr dc, ref Pt dest, ref Sz size, IntPtr source, ref Pt origin, int key, ref Blend blend, int flags);
|
|
7996
|
+
|
|
7997
|
+
static IntPtr window, screenDc, memoryDc, dib, oldBitmap, pixels, monitor;
|
|
7998
|
+
static int width, height, lastX = int.MinValue, lastY = int.MinValue, lastFrame = -1;
|
|
7999
|
+
static bool visible, failed, lastHolding;
|
|
8000
|
+
static string lastShape;
|
|
8001
|
+
static float scale = 1;
|
|
8002
|
+
static Rc monitorBounds;
|
|
8003
|
+
static byte[] copy;
|
|
8004
|
+
public static string Fault { get; private set; }
|
|
8005
|
+
|
|
8006
|
+
// Only a protected hold animates. Sixteen brightness steps over 2.5 seconds
|
|
8007
|
+
// avoid a repaint on every Glow tick; active input has a static bitmap.
|
|
8008
|
+
public static int AnimationFrame(bool holding, int tick)
|
|
8009
|
+
{
|
|
8010
|
+
return KitePointerPainter.AnimationFrame(holding, tick);
|
|
8011
|
+
}
|
|
8012
|
+
|
|
8013
|
+
public static void Update(bool show, bool holding)
|
|
8014
|
+
{
|
|
8015
|
+
if (!show || failed) { Hide(); return; }
|
|
8016
|
+
try
|
|
8017
|
+
{
|
|
8018
|
+
CursorInfo cursor = new CursorInfo(); cursor.Size = (uint)Marshal.SizeOf(typeof(CursorInfo));
|
|
8019
|
+
// Follow native cursor visibility, including text-entry apps which hide it.
|
|
8020
|
+
if (!GetCursorInfo(ref cursor) || (cursor.Flags & 1) == 0) { Hide(); return; }
|
|
8021
|
+
IntPtr nextMonitor = MonitorFromPoint(cursor.Position, 2);
|
|
8022
|
+
if (nextMonitor != monitor || window == IntPtr.Zero)
|
|
8023
|
+
{
|
|
8024
|
+
MonitorInfo info = new MonitorInfo(); info.Size = (uint)Marshal.SizeOf(typeof(MonitorInfo));
|
|
8025
|
+
if (!GetMonitorInfo(nextMonitor, ref info)) { Hide(); return; }
|
|
8026
|
+
monitorBounds = info.Monitor; monitor = nextMonitor;
|
|
8027
|
+
uint dpiX = 96, dpiY = 96;
|
|
8028
|
+
try { if (GetDpiForMonitor(monitor, 0, out dpiX, out dpiY) != 0) dpiX = 96; }
|
|
8029
|
+
catch (EntryPointNotFoundException) { dpiX = 96; }
|
|
8030
|
+
catch (DllNotFoundException) { dpiX = 96; }
|
|
8031
|
+
scale = Math.Max(1.0f, Math.Min(4.0f, dpiX / 96.0f));
|
|
8032
|
+
EnsureSurface((int)Math.Ceiling(LogicalWidth * scale), (int)Math.Ceiling(LogicalHeight * scale));
|
|
8033
|
+
lastFrame = -1;
|
|
8034
|
+
}
|
|
8035
|
+
// The drawn tip IS the native click point. Never flip or clamp the
|
|
8036
|
+
// surface near an edge: that would lie about the actual hotspot.
|
|
8037
|
+
System.Drawing.Point hotspot = KitePointerPainter.Hotspot(scale);
|
|
8038
|
+
int x = cursor.Position.X - hotspot.X, y = cursor.Position.Y - hotspot.Y;
|
|
8039
|
+
string shape = Program.StandardCursorShape(cursor.Cursor,
|
|
8040
|
+
delegate(int id) { return Native.LoadCursor(IntPtr.Zero, new IntPtr(id)); });
|
|
8041
|
+
int frame = AnimationFrame(holding, Environment.TickCount);
|
|
8042
|
+
if (frame != lastFrame || holding != lastHolding || shape != lastShape)
|
|
8043
|
+
{
|
|
8044
|
+
using (Bitmap bitmap = RenderPreview(scale, holding, frame, shape))
|
|
8045
|
+
{
|
|
8046
|
+
BitmapData data = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb);
|
|
8047
|
+
try
|
|
8048
|
+
{
|
|
8049
|
+
if (copy == null || copy.Length != width * height * 4) copy = new byte[width * height * 4];
|
|
8050
|
+
for (int row = 0; row < height; row++)
|
|
8051
|
+
Marshal.Copy(IntPtr.Add(data.Scan0, row * data.Stride), copy, row * width * 4, width * 4);
|
|
8052
|
+
Marshal.Copy(copy, 0, pixels, copy.Length);
|
|
8053
|
+
}
|
|
8054
|
+
finally { bitmap.UnlockBits(data); }
|
|
8055
|
+
}
|
|
8056
|
+
Pt target = new Pt { X = x, Y = y }, origin = new Pt();
|
|
8057
|
+
Sz size = new Sz { X = width, Y = height };
|
|
8058
|
+
Blend blend = new Blend { Alpha = 255, Format = 1 };
|
|
8059
|
+
if (!UpdateLayeredWindow(window, screenDc, ref target, ref size, memoryDc, ref origin, 0, ref blend, 2))
|
|
8060
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
8061
|
+
lastFrame = frame; lastHolding = holding; lastShape = shape; lastX = x; lastY = y;
|
|
8062
|
+
}
|
|
8063
|
+
else if (x != lastX || y != lastY)
|
|
8064
|
+
{
|
|
8065
|
+
// Moving a cached tiny layered surface does not redraw the desktop.
|
|
8066
|
+
if (!SetWindowPos(window, new IntPtr(-1), x, y, 0, 0, 0x0001 | 0x0010))
|
|
8067
|
+
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
8068
|
+
lastX = x; lastY = y;
|
|
8069
|
+
}
|
|
8070
|
+
if (!visible) { ShowWindow(window, 4); visible = true; }
|
|
8071
|
+
if (!IsWindowVisible(window)) throw new InvalidOperationException("replacement pointer surface is not visible");
|
|
8072
|
+
// Publish a short hide lease only AFTER a successful visible paint.
|
|
8073
|
+
// While the independent guardian starts, retain the native cursor.
|
|
8074
|
+
if (!RequestReplacement(true))
|
|
8075
|
+
{
|
|
8076
|
+
ShowWindow(window, 0); visible = false;
|
|
8077
|
+
if (PointerVisibilityLease.Fault != null) throw new InvalidOperationException(PointerVisibilityLease.Fault);
|
|
8078
|
+
return;
|
|
8079
|
+
}
|
|
8080
|
+
Msg msg;
|
|
8081
|
+
for (int n = 0; n < 8 && PeekMessage(out msg, window, 0, 0, 1); n++)
|
|
8082
|
+
{
|
|
8083
|
+
if (msg.Message == 0x000F) ValidateRect(window, IntPtr.Zero);
|
|
8084
|
+
else DispatchMessage(ref msg);
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
catch (Exception error)
|
|
8088
|
+
{
|
|
8089
|
+
// This presentation is optional. A display failure must never block input,
|
|
8090
|
+
// leak a global cursor, or turn a recoverable Glow frame into a fatal one.
|
|
8091
|
+
Fault = error.GetType().Name + ": " + error.Message; failed = true; Dispose();
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
|
|
8095
|
+
static void EnsureSurface(int w, int h)
|
|
8096
|
+
{
|
|
8097
|
+
if (window == IntPtr.Zero)
|
|
8098
|
+
{
|
|
8099
|
+
window = CreateWindowEx(CompanionStyle, "STATIC", "DSH kite pointer", WS_POPUP | WS_DISABLED,
|
|
8100
|
+
0, 0, w, h, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
|
8101
|
+
if (window == IntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
8102
|
+
// If exclusion is unavailable, omit the decoration rather than paint it
|
|
8103
|
+
// into evidence. WS_DISABLED + TRANSPARENT prevent input interception.
|
|
8104
|
+
if (!SetWindowDisplayAffinity(window, 0x11)) throw new InvalidOperationException("pointer capture exclusion unavailable");
|
|
8105
|
+
screenDc = GetDC(IntPtr.Zero); memoryDc = CreateCompatibleDC(screenDc);
|
|
8106
|
+
if (screenDc == IntPtr.Zero || memoryDc == IntPtr.Zero) throw new InvalidOperationException("pointer DC unavailable");
|
|
8107
|
+
}
|
|
8108
|
+
if (width == w && height == h && dib != IntPtr.Zero) return;
|
|
8109
|
+
ReleaseSurface();
|
|
8110
|
+
BitmapInfo info = new BitmapInfo(); info.Header.Size = (uint)Marshal.SizeOf(typeof(BitmapHeader));
|
|
8111
|
+
info.Header.Width = w; info.Header.Height = -h; info.Header.Planes = 1; info.Header.BitCount = 32;
|
|
8112
|
+
dib = CreateDIBSection(screenDc, ref info, 0, out pixels, IntPtr.Zero, 0);
|
|
8113
|
+
if (dib == IntPtr.Zero) throw new InvalidOperationException("pointer surface unavailable");
|
|
8114
|
+
oldBitmap = SelectObject(memoryDc, dib);
|
|
8115
|
+
if (oldBitmap == IntPtr.Zero || oldBitmap == new IntPtr(-1)) throw new InvalidOperationException("pointer bitmap unavailable");
|
|
8116
|
+
width = w; height = h;
|
|
8117
|
+
}
|
|
8118
|
+
|
|
8119
|
+
// Offscreen previews and the live pointer share the exact painter/hotspot.
|
|
8120
|
+
public static Bitmap RenderPreview(float dpiScale, bool holding, int frame, string semantic = "arrow")
|
|
8121
|
+
{ return KitePointerPainter.RenderPreview(dpiScale, holding, frame, semantic); }
|
|
8122
|
+
|
|
8123
|
+
// A narrow seam for private-desktop tests; production uses the guardian lease.
|
|
8124
|
+
static bool RequestReplacement(bool show)
|
|
8125
|
+
{ if (show) return PointerVisibilityLease.TryHide(); PointerVisibilityLease.Show(); return false; }
|
|
8126
|
+
|
|
8127
|
+
public static void Hide()
|
|
8128
|
+
{
|
|
8129
|
+
RequestReplacement(false);
|
|
8130
|
+
if (window != IntPtr.Zero && visible) ShowWindow(window, 0);
|
|
8131
|
+
visible = false;
|
|
8132
|
+
}
|
|
8133
|
+
static void ReleaseSurface()
|
|
8134
|
+
{
|
|
8135
|
+
if (oldBitmap != IntPtr.Zero && oldBitmap != new IntPtr(-1) && memoryDc != IntPtr.Zero) SelectObject(memoryDc, oldBitmap);
|
|
8136
|
+
oldBitmap = IntPtr.Zero;
|
|
8137
|
+
if (dib != IntPtr.Zero) DeleteObject(dib);
|
|
8138
|
+
dib = IntPtr.Zero; pixels = IntPtr.Zero; width = height = 0; copy = null;
|
|
8139
|
+
}
|
|
8140
|
+
public static void Dispose()
|
|
8141
|
+
{
|
|
8142
|
+
Hide(); ReleaseSurface();
|
|
8143
|
+
PointerVisibilityLease.Dispose();
|
|
8144
|
+
if (memoryDc != IntPtr.Zero) DeleteDC(memoryDc);
|
|
8145
|
+
if (screenDc != IntPtr.Zero) ReleaseDC(IntPtr.Zero, screenDc);
|
|
8146
|
+
if (window != IntPtr.Zero) DestroyWindow(window);
|
|
8147
|
+
memoryDc = screenDc = window = monitor = IntPtr.Zero;
|
|
8148
|
+
lastFrame = -1; lastX = lastY = int.MinValue;
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8152
|
+
|
|
8153
|
+
namespace DshComputerUse.Worker
|
|
8154
|
+
{
|
|
8155
|
+
public static class KitePointerPainter
|
|
8156
|
+
{
|
|
8157
|
+
public const int LogicalWidth = 39, LogicalHeight = 44;
|
|
8158
|
+
public const float BodyScale = 0.75f, TiltDegrees = -25f;
|
|
8159
|
+
public static readonly PointF LogicalHotspot = new PointF(8, 4);
|
|
8160
|
+
static readonly double Angle = TiltDegrees * Math.PI / 180.0;
|
|
8161
|
+
|
|
8162
|
+
static float Scale(float value)
|
|
8163
|
+
{
|
|
8164
|
+
if (float.IsNaN(value) || float.IsInfinity(value)) return 1f;
|
|
8165
|
+
return Math.Max(1f, Math.Min(4f, value));
|
|
8166
|
+
}
|
|
8167
|
+
|
|
8168
|
+
public static Size CanvasSize(float dpiScale)
|
|
8169
|
+
{
|
|
8170
|
+
float s = Scale(dpiScale);
|
|
8171
|
+
return new Size((int)Math.Ceiling(LogicalWidth * s), (int)Math.Ceiling(LogicalHeight * s));
|
|
8172
|
+
}
|
|
8173
|
+
|
|
8174
|
+
public static Point Hotspot(float dpiScale)
|
|
8175
|
+
{
|
|
8176
|
+
float s = Scale(dpiScale);
|
|
8177
|
+
return new Point((int)Math.Round(LogicalHotspot.X * s, MidpointRounding.AwayFromZero),
|
|
8178
|
+
(int)Math.Round(LogicalHotspot.Y * s, MidpointRounding.AwayFromZero));
|
|
8179
|
+
}
|
|
8180
|
+
|
|
8181
|
+
// Source kite tip is (27,7). Rotation is counterclockwise in screen coordinates:
|
|
8182
|
+
// an upward-pointing head tilts toward the upper-left. Device-pixel H is shared
|
|
8183
|
+
// by the painter and the future cursor hotspot; it never depends on animation.
|
|
8184
|
+
public static PointF Transform(float x, float y, float dpiScale)
|
|
8185
|
+
{
|
|
8186
|
+
float s = Scale(dpiScale);
|
|
8187
|
+
Point h = Hotspot(s);
|
|
8188
|
+
double dx = x - 27, dy = y - 7;
|
|
8189
|
+
return new PointF(h.X + (float)(s * BodyScale * (Math.Cos(Angle) * dx - Math.Sin(Angle) * dy)),
|
|
8190
|
+
h.Y + (float)(s * BodyScale * (Math.Sin(Angle) * dx + Math.Cos(Angle) * dy)));
|
|
8191
|
+
}
|
|
8192
|
+
|
|
8193
|
+
public static int AnimationFrame(bool holding, int tick)
|
|
8194
|
+
{
|
|
8195
|
+
if (!holding) return 16;
|
|
8196
|
+
double phase = (unchecked((uint)tick) % 2500) * 2 * Math.PI / 2500.0;
|
|
8197
|
+
return 7 + (int)Math.Round(4 * (1 - Math.Cos(phase)));
|
|
8198
|
+
}
|
|
8199
|
+
|
|
8200
|
+
static PointF[] Shape(float s, params float[] xy)
|
|
8201
|
+
{
|
|
8202
|
+
PointF[] points = new PointF[xy.Length / 2];
|
|
8203
|
+
for (int i = 0; i < points.Length; i++) points[i] = Transform(xy[2 * i], xy[2 * i + 1], s);
|
|
8204
|
+
return points;
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
public static Bitmap RenderPreview(float dpiScale, bool holding, int frame, string semantic = "arrow")
|
|
8208
|
+
{
|
|
8209
|
+
float s = Scale(dpiScale);
|
|
8210
|
+
Size size = CanvasSize(s);
|
|
8211
|
+
Bitmap bitmap = new Bitmap(size.Width, size.Height, PixelFormat.Format32bppPArgb);
|
|
8212
|
+
float pulse = holding ? Math.Max(0f, Math.Min(1f, (frame - 7) / 8f)) : 1f;
|
|
8213
|
+
int bodyAlpha = holding ? 208 + (int)(36 * pulse) : 248;
|
|
8214
|
+
PointF[] kite = Shape(s, 27, 7, 41, 24, 27, 43, 13, 24);
|
|
8215
|
+
using (Graphics g = Graphics.FromImage(bitmap))
|
|
8216
|
+
{
|
|
8217
|
+
g.Clear(Color.Transparent);
|
|
8218
|
+
g.SmoothingMode = SmoothingMode.AntiAlias;
|
|
8219
|
+
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
8220
|
+
|
|
8221
|
+
// A slim visible string attaches to the lower point. No satellite star,
|
|
8222
|
+
// duplicate arrow, end bead or external status symbol crowds the tip.
|
|
8223
|
+
using (Pen tail = new Pen(Color.FromArgb(holding ? 185 + (int)(35 * pulse) : 230, 39, 118, 224), 1.1f * s))
|
|
8224
|
+
{
|
|
8225
|
+
tail.StartCap = tail.EndCap = LineCap.Round;
|
|
8226
|
+
g.DrawBezier(tail, Transform(27, 43, s), Transform(25, 53, s),
|
|
8227
|
+
Transform(11, 41, s), Transform(10, 53, s));
|
|
8228
|
+
}
|
|
8229
|
+
|
|
8230
|
+
using (Pen halo = new Pen(Color.FromArgb(holding ? 8 + (int)(20 * pulse) : 28, 54, 132, 255), 2.5f * s))
|
|
8231
|
+
g.DrawPolygon(halo, kite);
|
|
8232
|
+
using (LinearGradientBrush fill = new LinearGradientBrush(Transform(22, 9, s), Transform(33, 43, s),
|
|
8233
|
+
Color.FromArgb(bodyAlpha, holding ? 49 : 57, holding ? 133 : 150, 250),
|
|
8234
|
+
Color.FromArgb(bodyAlpha, 20, 51, holding ? 128 : 151)))
|
|
8235
|
+
g.FillPolygon(fill, kite);
|
|
8236
|
+
using (SolidBrush facet = new SolidBrush(Color.FromArgb(holding ? 78 + (int)(25 * pulse) : 115, 160, 225, 255)))
|
|
8237
|
+
g.FillPolygon(facet, Shape(s, 27, 7, 41, 24, 27, 25));
|
|
8238
|
+
using (SolidBrush shade = new SolidBrush(Color.FromArgb(78, 6, 24, 79)))
|
|
8239
|
+
g.FillPolygon(shade, Shape(s, 27, 25, 41, 24, 27, 43));
|
|
8240
|
+
using (Pen rim = new Pen(Color.FromArgb(242, 191, 231, 255), Math.Max(0.85f, 0.85f * s)))
|
|
8241
|
+
{
|
|
8242
|
+
rim.LineJoin = LineJoin.Miter;
|
|
8243
|
+
g.DrawPolygon(rim, kite);
|
|
8244
|
+
}
|
|
8245
|
+
using (Pen spine = new Pen(Color.FromArgb(72, 213, 242, 255), 0.55f * s))
|
|
8246
|
+
g.DrawLine(spine, Transform(27, 8, s), Transform(27, 42, s));
|
|
8247
|
+
|
|
8248
|
+
// The state mark stays legible at the dimmest hold frame. Geometry and
|
|
8249
|
+
// hotspot remain stationary; only light/alpha changes during breathing.
|
|
8250
|
+
if (NormalizeSemantic(semantic) == "arrow")
|
|
8251
|
+
using (SolidBrush mark = new SolidBrush(Color.FromArgb(245, 236, 249, 255)))
|
|
8252
|
+
{
|
|
8253
|
+
if (holding)
|
|
8254
|
+
{
|
|
8255
|
+
g.FillPolygon(mark, Shape(s, 23.3f, 20.2f, 25.2f, 20.2f, 25.2f, 28.2f, 23.3f, 28.2f));
|
|
8256
|
+
g.FillPolygon(mark, Shape(s, 29.1f, 20.2f, 31.0f, 20.2f, 31.0f, 28.2f, 29.1f, 28.2f));
|
|
8257
|
+
}
|
|
8258
|
+
else g.FillPolygon(mark, Shape(s, 24, 19.8f, 31.4f, 24.2f, 24, 28.6f));
|
|
8259
|
+
}
|
|
8260
|
+
DrawSemantic(g, s, semantic);
|
|
8261
|
+
}
|
|
8262
|
+
return bitmap;
|
|
8263
|
+
}
|
|
8264
|
+
|
|
8265
|
+
public static string NormalizeSemantic(string semantic)
|
|
8266
|
+
{
|
|
8267
|
+
if (String.IsNullOrEmpty(semantic)) return "arrow";
|
|
8268
|
+
string value = semantic.Trim().ToLowerInvariant();
|
|
8269
|
+
if (value == "ibeam" || value == "i-beam") return "text";
|
|
8270
|
+
switch (value)
|
|
8271
|
+
{
|
|
8272
|
+
case "arrow": case "text": case "resize-ew": case "resize-ns":
|
|
8273
|
+
case "resize-nwse": case "resize-nesw": case "move": case "hand":
|
|
8274
|
+
case "cross": case "wait": case "app-starting": case "not-allowed": case "help":
|
|
8275
|
+
return value;
|
|
8276
|
+
default: return "unknown";
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
|
|
8280
|
+
// Every semantic is a small upright center mark. Neither the canvas, kite,
|
|
8281
|
+
// tail nor hotspot moves; holding keeps the same mark and only breathes.
|
|
8282
|
+
// An unknown native cursor gets a neutral dash, never a guessed resize/text mark.
|
|
8283
|
+
static void DrawSemantic(Graphics g, float s, string semantic)
|
|
8284
|
+
{
|
|
8285
|
+
semantic = NormalizeSemantic(semantic);
|
|
8286
|
+
if (semantic == "arrow") return;
|
|
8287
|
+
PointF center = Transform(27, 25, s);
|
|
8288
|
+
using (GraphicsPath path = new GraphicsPath())
|
|
8289
|
+
using (Matrix matrix = new Matrix(s, 0, 0, s, center.X, center.Y))
|
|
8290
|
+
{
|
|
8291
|
+
if (semantic == "text")
|
|
8292
|
+
{
|
|
8293
|
+
path.AddLine(-2, -4, 2, -4);
|
|
8294
|
+
path.StartFigure(); path.AddLine(0, -4, 0, 4);
|
|
8295
|
+
path.StartFigure(); path.AddLine(-2, 4, 2, 4);
|
|
8296
|
+
}
|
|
8297
|
+
else if (semantic.StartsWith("resize-"))
|
|
8298
|
+
{
|
|
8299
|
+
float angle = semantic == "resize-ns" ? 90 : semantic == "resize-nwse" ? 45 : semantic == "resize-nesw" ? -45 : 0;
|
|
8300
|
+
AddDoubleArrow(path, 4.0f, 1.9f);
|
|
8301
|
+
using (Matrix rotate = new Matrix()) { rotate.Rotate(angle); path.Transform(rotate); }
|
|
8302
|
+
}
|
|
8303
|
+
else if (semantic == "move")
|
|
8304
|
+
{
|
|
8305
|
+
AddDoubleArrow(path, 3.9f, 1.5f);
|
|
8306
|
+
using (GraphicsPath vertical = new GraphicsPath())
|
|
8307
|
+
using (Matrix rotate = new Matrix())
|
|
8308
|
+
{
|
|
8309
|
+
AddDoubleArrow(vertical, 3.9f, 1.5f); rotate.Rotate(90); vertical.Transform(rotate);
|
|
8310
|
+
path.AddPath(vertical, false);
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
else if (semantic == "cross")
|
|
8314
|
+
{
|
|
8315
|
+
path.AddLine(-4, 0, 4, 0); path.StartFigure(); path.AddLine(0, -4, 0, 4);
|
|
8316
|
+
}
|
|
8317
|
+
else if (semantic == "not-allowed")
|
|
8318
|
+
{
|
|
8319
|
+
path.AddEllipse(-3.6f, -3.6f, 7.2f, 7.2f);
|
|
8320
|
+
path.StartFigure(); path.AddLine(-2.5f, 2.5f, 2.5f, -2.5f);
|
|
8321
|
+
}
|
|
8322
|
+
else if (semantic == "wait")
|
|
8323
|
+
{
|
|
8324
|
+
AddHourglass(path, 0, 0, 2.6f, 3.8f);
|
|
8325
|
+
}
|
|
8326
|
+
else if (semantic == "app-starting")
|
|
8327
|
+
{
|
|
8328
|
+
path.AddLines(new PointF[] { new PointF(-3.5f, 2.7f), new PointF(-3.5f, -3.8f), new PointF(-0.2f, -0.4f) });
|
|
8329
|
+
path.StartFigure(); AddHourglass(path, 1.8f, 1.2f, 1.5f, 2.6f);
|
|
8330
|
+
}
|
|
8331
|
+
else if (semantic == "help")
|
|
8332
|
+
{
|
|
8333
|
+
path.AddBezier(-2.4f, -2.4f, -2.0f, -5.3f, 4.1f, -4.0f, 1.7f, -1.3f);
|
|
8334
|
+
path.AddBezier(1.7f, -1.3f, 0.6f, -0.5f, 0, -0.4f, 0, 1.2f);
|
|
8335
|
+
path.StartFigure(); path.AddLine(0, 3.3f, 0, 3.6f);
|
|
8336
|
+
}
|
|
8337
|
+
else if (semantic == "hand")
|
|
8338
|
+
{
|
|
8339
|
+
path.AddPolygon(new PointF[] { new PointF(-3.7f, 0), new PointF(-2.4f, -1),
|
|
8340
|
+
new PointF(-1, 0.4f), new PointF(-1, -4.2f), new PointF(0.6f, -4.2f),
|
|
8341
|
+
new PointF(0.6f, -0.8f), new PointF(1.8f, -1.4f), new PointF(3, -0.5f),
|
|
8342
|
+
new PointF(3.7f, 0.6f), new PointF(3.1f, 3.6f), new PointF(-0.2f, 3.9f), new PointF(-2, 2) });
|
|
8343
|
+
}
|
|
8344
|
+
else
|
|
8345
|
+
{
|
|
8346
|
+
path.AddLine(-2.1f, 0, 2.1f, 0);
|
|
8347
|
+
}
|
|
8348
|
+
path.Transform(matrix);
|
|
8349
|
+
using (Pen contrast = new Pen(Color.FromArgb(110, 8, 32, 89), 2.0f * s))
|
|
8350
|
+
using (Pen ink = new Pen(Color.FromArgb(semantic == "unknown" ? 180 : 248, 232, 247, 255), 1.0f * s))
|
|
8351
|
+
{
|
|
8352
|
+
contrast.LineJoin = ink.LineJoin = LineJoin.Round;
|
|
8353
|
+
contrast.StartCap = contrast.EndCap = ink.StartCap = ink.EndCap = LineCap.Round;
|
|
8354
|
+
g.DrawPath(contrast, path); g.DrawPath(ink, path);
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8359
|
+
static void AddDoubleArrow(GraphicsPath path, float radius, float head)
|
|
8360
|
+
{
|
|
8361
|
+
path.AddLine(-radius, 0, radius, 0); path.StartFigure();
|
|
8362
|
+
path.AddLines(new PointF[] { new PointF(-radius + head, -head), new PointF(-radius, 0), new PointF(-radius + head, head) });
|
|
8363
|
+
path.StartFigure();
|
|
8364
|
+
path.AddLines(new PointF[] { new PointF(radius - head, -head), new PointF(radius, 0), new PointF(radius - head, head) });
|
|
8365
|
+
}
|
|
8366
|
+
|
|
8367
|
+
static void AddHourglass(GraphicsPath path, float x, float y, float width, float height)
|
|
8368
|
+
{
|
|
8369
|
+
path.AddLines(new PointF[] { new PointF(x - width, y - height), new PointF(x + width, y - height),
|
|
8370
|
+
new PointF(x - width, y + height), new PointF(x + width, y + height), new PointF(x - width, y - height) });
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
|
|
8374
|
+
}
|
|
8375
|
+
|
|
8376
|
+
namespace DshComputerUse.Worker
|
|
8377
|
+
{
|
|
8378
|
+
public static class PointerVisibilityLease
|
|
8379
|
+
{
|
|
8380
|
+
const int Version = 1, Bytes = 128, ReadyOffset = 48, RestoredOffset = 52;
|
|
8381
|
+
const int LeaseMs = 500, RefreshMs = 100, StartupMs = 2000;
|
|
8382
|
+
const string Prefix = "Local\\DSH-CursorLease-";
|
|
8383
|
+
static readonly object Gate = new object();
|
|
8384
|
+
static Session current;
|
|
8385
|
+
static string fault;
|
|
8386
|
+
public static string Fault { get { lock (Gate) return fault; } }
|
|
8387
|
+
internal static long NowMs()
|
|
8388
|
+
{ return (long)(System.Diagnostics.Stopwatch.GetTimestamp() * (1000.0 / System.Diagnostics.Stopwatch.Frequency)); }
|
|
8389
|
+
|
|
8390
|
+
sealed class Session
|
|
8391
|
+
{
|
|
8392
|
+
public System.IO.MemoryMappedFiles.MemoryMappedFile Map;
|
|
8393
|
+
public System.IO.MemoryMappedFiles.MemoryMappedViewAccessor View;
|
|
8394
|
+
public System.Diagnostics.Process Guardian;
|
|
8395
|
+
public bool Ready, Revoked, Requested, Finished;
|
|
8396
|
+
public long Sequence, LastRefresh;
|
|
8397
|
+
}
|
|
8398
|
+
|
|
8399
|
+
// Invoke ONLY after the overlay has been successfully presented at the
|
|
8400
|
+
// native hotspot. True means the ready guardian accepted a current lease,
|
|
8401
|
+
// not proof that pixels disappeared. Never blocks waiting for readiness.
|
|
8402
|
+
public static bool TryHide()
|
|
8403
|
+
{
|
|
8404
|
+
lock (Gate)
|
|
8405
|
+
{
|
|
8406
|
+
if (fault != null) return false;
|
|
8407
|
+
if (current != null && current.Finished) current = null;
|
|
8408
|
+
if (current == null)
|
|
8409
|
+
{
|
|
8410
|
+
current = new Session(); Session launch = current;
|
|
8411
|
+
System.Threading.ThreadPool.QueueUserWorkItem(delegate { StartAndMonitor(launch); });
|
|
8412
|
+
return false;
|
|
8413
|
+
}
|
|
8414
|
+
Session s = current;
|
|
8415
|
+
if (s.Revoked || !s.Ready || s.View == null) return false;
|
|
8416
|
+
long now = NowMs();
|
|
8417
|
+
if (!s.Requested || now - s.LastRefresh >= RefreshMs)
|
|
8418
|
+
{
|
|
8419
|
+
s.Requested = true; // Set before writing: unknown write outcome also needs cleanup.
|
|
8420
|
+
try { WriteRequest(s, true, false, now); s.LastRefresh = now; }
|
|
8421
|
+
catch (System.Exception e) { fault = "cursor lease write: " + e.Message; s.Revoked = true; return false; }
|
|
8422
|
+
}
|
|
8423
|
+
return true;
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
|
|
8427
|
+
// A nonblocking release request. Guardian observes it within its next
|
|
8428
|
+
// progressing <=20 ms poll; it is not a synchronous compositor fence.
|
|
8429
|
+
public static void Show()
|
|
8430
|
+
{
|
|
8431
|
+
lock (Gate)
|
|
8432
|
+
{
|
|
8433
|
+
Session s = current;
|
|
8434
|
+
if (s == null || s.Finished || s.Revoked) return;
|
|
8435
|
+
s.Revoked = true;
|
|
8436
|
+
if (s.View != null)
|
|
8437
|
+
{
|
|
8438
|
+
try { WriteRequest(s, false, true, NowMs()); }
|
|
8439
|
+
catch (System.Exception e) { fault = "cursor lease release: " + e.Message; }
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
public static void Dispose() { Show(); }
|
|
8444
|
+
|
|
8445
|
+
static void WriteRequest(Session s, bool desired, bool revoked, long stamp)
|
|
8446
|
+
{
|
|
8447
|
+
long even = s.Sequence + 2;
|
|
8448
|
+
s.View.Write(16, even - 1); System.Threading.Thread.MemoryBarrier();
|
|
8449
|
+
s.View.Write(24, desired ? 1 : 0); s.View.Write(28, revoked ? 1 : 0); s.View.Write(32, stamp);
|
|
8450
|
+
System.Threading.Thread.MemoryBarrier(); s.View.Write(16, even); s.Sequence = even;
|
|
8451
|
+
}
|
|
8452
|
+
internal sealed class Request
|
|
8453
|
+
{
|
|
8454
|
+
public long Sequence, Stamp;
|
|
8455
|
+
public bool Desired, Revoked;
|
|
8456
|
+
public Request(long sequence, long stamp, bool desired, bool revoked)
|
|
8457
|
+
{ Sequence = sequence; Stamp = stamp; Desired = desired; Revoked = revoked; }
|
|
8458
|
+
}
|
|
8459
|
+
internal static Request ReadRequest(System.IO.MemoryMappedFiles.MemoryMappedViewAccessor view)
|
|
8460
|
+
{
|
|
8461
|
+
long first = view.ReadInt64(16);
|
|
8462
|
+
if (first <= 0 || (first & 1) != 0) return null;
|
|
8463
|
+
System.Threading.Thread.MemoryBarrier();
|
|
8464
|
+
int desired = view.ReadInt32(24), revoked = view.ReadInt32(28); long stamp = view.ReadInt64(32);
|
|
8465
|
+
System.Threading.Thread.MemoryBarrier(); long second = view.ReadInt64(16);
|
|
8466
|
+
if (first != second || (desired != 0 && desired != 1) || (revoked != 0 && revoked != 1)) return null;
|
|
8467
|
+
return new Request(second, stamp, desired == 1, revoked == 1);
|
|
8468
|
+
}
|
|
8469
|
+
|
|
8470
|
+
static void StartAndMonitor(Session s)
|
|
8471
|
+
{
|
|
8472
|
+
System.IO.MemoryMappedFiles.MemoryMappedFile map = null;
|
|
8473
|
+
System.IO.MemoryMappedFiles.MemoryMappedViewAccessor view = null;
|
|
8474
|
+
System.Diagnostics.Process child = null;
|
|
8475
|
+
bool restoreNeeded = false, exitConfirmed = false, stopAttempted = false;
|
|
8476
|
+
try
|
|
8477
|
+
{
|
|
8478
|
+
string name = Prefix + System.Guid.NewGuid().ToString("N");
|
|
8479
|
+
using (System.Diagnostics.Process owner = System.Diagnostics.Process.GetCurrentProcess())
|
|
8480
|
+
{
|
|
8481
|
+
map = System.IO.MemoryMappedFiles.MemoryMappedFile.CreateNew(name, Bytes);
|
|
8482
|
+
view = map.CreateViewAccessor(0, Bytes);
|
|
8483
|
+
view.Write(0, Version); view.Write(4, owner.Id);
|
|
8484
|
+
long start = owner.StartTime.ToUniversalTime().Ticks; view.Write(8, start);
|
|
8485
|
+
lock (Gate)
|
|
8486
|
+
{
|
|
8487
|
+
s.Map = map; s.View = view;
|
|
8488
|
+
if (s.Revoked) return; // No child and no Mag API if released during startup.
|
|
8489
|
+
WriteRequest(s, false, false, NowMs());
|
|
8490
|
+
}
|
|
8491
|
+
var info = new System.Diagnostics.ProcessStartInfo(
|
|
8492
|
+
System.Reflection.Assembly.GetExecutingAssembly().Location,
|
|
8493
|
+
"--pointer-guardian " + name + " " + owner.Id + " " + start);
|
|
8494
|
+
info.UseShellExecute = false; info.CreateNoWindow = true;
|
|
8495
|
+
info.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
|
|
8496
|
+
child = System.Diagnostics.Process.Start(info);
|
|
8497
|
+
if (child == null) throw new System.InvalidOperationException("guardian did not start");
|
|
8498
|
+
lock (Gate) s.Guardian = child;
|
|
8499
|
+
}
|
|
8500
|
+
long began = NowMs();
|
|
8501
|
+
while (!child.WaitForExit(20))
|
|
8502
|
+
{
|
|
8503
|
+
int status = view.ReadInt32(ReadyOffset);
|
|
8504
|
+
lock (Gate) s.Ready = status == 1 && !s.Revoked;
|
|
8505
|
+
if (status == 0 && NowMs() - began > StartupMs)
|
|
8506
|
+
{
|
|
8507
|
+
// Only our own Process handle is terminated. It was not
|
|
8508
|
+
// ready, so TryHide has never authorized a hide request.
|
|
8509
|
+
stopAttempted = true;
|
|
8510
|
+
exitConfirmed = ConfirmExit(delegate { return child.HasExited; }, child.Kill, child.WaitForExit);
|
|
8511
|
+
if (!exitConfirmed) throw new System.TimeoutException("guardian termination unconfirmed after 1000 ms");
|
|
8512
|
+
throw new System.TimeoutException("guardian startup timeout");
|
|
8513
|
+
}
|
|
8514
|
+
}
|
|
8515
|
+
exitConfirmed = true;
|
|
8516
|
+
lock (Gate)
|
|
8517
|
+
{
|
|
8518
|
+
s.Ready = false;
|
|
8519
|
+
restoreNeeded = s.Requested && view.ReadInt32(RestoredOffset) != 1;
|
|
8520
|
+
if (!s.Revoked) fault = "pointer guardian ended; native cursor fallback";
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
catch (System.Exception e)
|
|
8524
|
+
{
|
|
8525
|
+
lock (Gate) { s.Ready = false; fault = "pointer guardian: " + e.Message; restoreNeeded = s.Requested; }
|
|
8526
|
+
// An alive guardian remains the sole hide writer. Never race an
|
|
8527
|
+
// emergency show against a process that can still issue hide.
|
|
8528
|
+
if (child != null && !exitConfirmed && !stopAttempted)
|
|
8529
|
+
{
|
|
8530
|
+
stopAttempted = true;
|
|
8531
|
+
exitConfirmed = ConfirmExit(delegate { return child.HasExited; }, child.Kill, child.WaitForExit);
|
|
8532
|
+
}
|
|
8533
|
+
if (!exitConfirmed)
|
|
8534
|
+
{
|
|
8535
|
+
restoreNeeded = false;
|
|
8536
|
+
if (child != null) lock (Gate) fault += "; guardian exit unconfirmed, emergency show suppressed and further leases disabled";
|
|
8537
|
+
}
|
|
8538
|
+
}
|
|
8539
|
+
finally
|
|
8540
|
+
{
|
|
8541
|
+
// Guardian has ended before emergency restoration and before a
|
|
8542
|
+
// subsequent session may start. Old cleanup cannot unhide a new lease.
|
|
8543
|
+
if (restoreNeeded && exitConfirmed) EmergencyShow();
|
|
8544
|
+
lock (Gate)
|
|
8545
|
+
{
|
|
8546
|
+
s.Ready = false; s.View = null; s.Map = null; s.Guardian = null;
|
|
8547
|
+
if (view != null) view.Dispose(); if (map != null) map.Dispose();
|
|
8548
|
+
if (child != null) child.Dispose(); s.Finished = true;
|
|
8549
|
+
}
|
|
8550
|
+
}
|
|
8551
|
+
}
|
|
8552
|
+
|
|
8553
|
+
// Bounded process shutdown, shared with fake process tests. Failure is
|
|
8554
|
+
// explicit: the caller must not race show against a possible late hide.
|
|
8555
|
+
internal static bool ConfirmExit(System.Func<bool> hasExited, System.Action kill, System.Func<int, bool> wait)
|
|
8556
|
+
{
|
|
8557
|
+
try
|
|
8558
|
+
{
|
|
8559
|
+
if (hasExited()) return true;
|
|
8560
|
+
try { kill(); } catch { return hasExited(); }
|
|
8561
|
+
return wait(1000) || hasExited();
|
|
8562
|
+
}
|
|
8563
|
+
catch { return false; }
|
|
8564
|
+
}
|
|
8565
|
+
|
|
8566
|
+
static void EmergencyShow()
|
|
8567
|
+
{
|
|
8568
|
+
var native = new NativeBackend(); bool initialized = false;
|
|
8569
|
+
try { initialized = native.Initialize(); if (!initialized || !native.Show(true)) lock (Gate) fault = "emergency cursor restore API failed"; }
|
|
8570
|
+
catch (System.Exception e) { lock (Gate) fault = "emergency cursor restore: " + e.Message; }
|
|
8571
|
+
finally { if (initialized) try { native.Uninitialize(); } catch { } }
|
|
8572
|
+
}
|
|
8573
|
+
|
|
8574
|
+
internal interface Backend { bool Initialize(); bool Show(bool visible); void Uninitialize(); }
|
|
8575
|
+
sealed class NativeBackend : Backend
|
|
8576
|
+
{
|
|
8577
|
+
[System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagInitialize();
|
|
8578
|
+
[System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagShowSystemCursor(bool show);
|
|
8579
|
+
[System.Runtime.InteropServices.DllImport("Magnification.dll")] static extern bool MagUninitialize();
|
|
8580
|
+
public bool Initialize() { return MagInitialize(); }
|
|
8581
|
+
public bool Show(bool visible) { return MagShowSystemCursor(visible); }
|
|
8582
|
+
public void Uninitialize() { MagUninitialize(); }
|
|
8583
|
+
}
|
|
8584
|
+
internal sealed class Outcome
|
|
8585
|
+
{ public bool HideAttempted, RestoreConfirmed, Success; public string Reason; }
|
|
8586
|
+
|
|
8587
|
+
// Same loop is used in the actual guardian and deterministic fake-backend
|
|
8588
|
+
// tests. No wall-clock waiting or native API is required in those tests.
|
|
8589
|
+
internal static Outcome RunLease(Backend api, System.Func<Request> read,
|
|
8590
|
+
System.Func<long> now, System.Func<bool> ownerAlive, System.Action pause, System.Action ready)
|
|
8591
|
+
{
|
|
8592
|
+
var result = new Outcome(); bool initialized = false, hidden = false;
|
|
8593
|
+
long started = now(), latestSequence = 0, stamp = started; bool everRequested = false;
|
|
8594
|
+
try
|
|
8595
|
+
{
|
|
8596
|
+
initialized = api.Initialize();
|
|
8597
|
+
if (!initialized) { result.Reason = "initialize-failed"; return result; }
|
|
8598
|
+
ready();
|
|
8599
|
+
while (true)
|
|
8600
|
+
{
|
|
8601
|
+
if (!ownerAlive()) { result.Reason = "owner-exited"; break; }
|
|
8602
|
+
// Expiration is terminal even when a newer request has
|
|
8603
|
+
// arrived while this process was not scheduled.
|
|
8604
|
+
if (everRequested && now() - stamp >= LeaseMs) { result.Reason = "lease-expired"; break; }
|
|
8605
|
+
Request request = read(); long currentTime = now();
|
|
8606
|
+
if (request != null && request.Sequence > latestSequence)
|
|
8607
|
+
{
|
|
8608
|
+
latestSequence = request.Sequence;
|
|
8609
|
+
if (request.Revoked || (!request.Desired && everRequested)) { result.Reason = "released"; break; }
|
|
8610
|
+
if (request.Desired)
|
|
8611
|
+
{
|
|
8612
|
+
if (request.Stamp > currentTime || currentTime - request.Stamp >= LeaseMs)
|
|
8613
|
+
{ result.Reason = "expired-request"; break; }
|
|
8614
|
+
everRequested = true; stamp = request.Stamp;
|
|
8615
|
+
if (!hidden)
|
|
8616
|
+
{
|
|
8617
|
+
// Re-read the current complete request before the
|
|
8618
|
+
// sole hide writer acts; queued old commands never apply.
|
|
8619
|
+
Request newest = read();
|
|
8620
|
+
if (newest == null || newest.Sequence != request.Sequence) { pause(); continue; }
|
|
8621
|
+
if (!ownerAlive() || now() - stamp >= LeaseMs) { result.Reason = "expired-before-hide"; break; }
|
|
8622
|
+
result.HideAttempted = true;
|
|
8623
|
+
if (!api.Show(false)) { result.Reason = "hide-failed"; break; }
|
|
8624
|
+
hidden = true;
|
|
8625
|
+
// A release concurrent with the API is processed
|
|
8626
|
+
// before sleeping or accepting another lease.
|
|
8627
|
+
Request after = read();
|
|
8628
|
+
if (after != null && after.Sequence > latestSequence && (after.Revoked || !after.Desired))
|
|
8629
|
+
{ result.Reason = "released-during-hide"; break; }
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
}
|
|
8633
|
+
if (everRequested && currentTime - stamp >= LeaseMs) { result.Reason = "lease-expired"; break; }
|
|
8634
|
+
if (!everRequested && currentTime - started >= StartupMs) { result.Reason = "unused"; break; }
|
|
8635
|
+
pause();
|
|
8636
|
+
}
|
|
8637
|
+
result.Success = result.Reason != "hide-failed";
|
|
8638
|
+
}
|
|
8639
|
+
catch (System.Exception e) { result.Reason = "fault: " + e.Message; }
|
|
8640
|
+
finally
|
|
8641
|
+
{
|
|
8642
|
+
if (initialized)
|
|
8643
|
+
{
|
|
8644
|
+
if (result.HideAttempted)
|
|
8645
|
+
{
|
|
8646
|
+
try { result.RestoreConfirmed = api.Show(true); }
|
|
8647
|
+
catch { result.RestoreConfirmed = false; }
|
|
8648
|
+
if (!result.RestoreConfirmed) result.Success = false;
|
|
8649
|
+
}
|
|
8650
|
+
try { api.Uninitialize(); } catch { result.Success = false; }
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
8653
|
+
return result;
|
|
8654
|
+
}
|
|
8655
|
+
|
|
8656
|
+
// Dispatch this at the FIRST line of worker Main, before Panic.Init,
|
|
8657
|
+
// hooks, global brake, protocol stdout, or any other worker lifecycle.
|
|
8658
|
+
public static int GuardianMain(string name, int ownerPid, long ownerStartTicks)
|
|
8659
|
+
{
|
|
8660
|
+
if (System.IntPtr.Size != 8 || name == null || !name.StartsWith(Prefix, System.StringComparison.Ordinal) ||
|
|
8661
|
+
name.Length != Prefix.Length + 32) return 2;
|
|
8662
|
+
try
|
|
8663
|
+
{
|
|
8664
|
+
using (System.Diagnostics.Process owner = System.Diagnostics.Process.GetProcessById(ownerPid))
|
|
8665
|
+
using (System.Diagnostics.Process self = System.Diagnostics.Process.GetCurrentProcess())
|
|
8666
|
+
using (var map = System.IO.MemoryMappedFiles.MemoryMappedFile.OpenExisting(name))
|
|
8667
|
+
using (var view = map.CreateViewAccessor(0, Bytes))
|
|
8668
|
+
{
|
|
8669
|
+
if (owner.StartTime.ToUniversalTime().Ticks != ownerStartTicks || owner.SessionId != self.SessionId ||
|
|
8670
|
+
view.ReadInt32(0) != Version || view.ReadInt32(4) != ownerPid || view.ReadInt64(8) != ownerStartTicks) return 3;
|
|
8671
|
+
Outcome result = RunLease(new NativeBackend(), delegate { return ReadRequest(view); }, NowMs,
|
|
8672
|
+
delegate { try { return !owner.HasExited; } catch { return false; } },
|
|
8673
|
+
delegate { System.Threading.Thread.Sleep(20); },
|
|
8674
|
+
delegate { view.Write(ReadyOffset, 1); });
|
|
8675
|
+
view.Write(RestoredOffset, result.RestoreConfirmed ? 1 : 0);
|
|
8676
|
+
System.Threading.Thread.MemoryBarrier(); view.Write(ReadyOffset, result.Success ? 2 : 3);
|
|
8677
|
+
return result.Success ? 0 : 4;
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
catch { return 5; }
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
8683
|
+
}
|