@ycheung5/pi-computer-use 0.0.0-stage → 0.1.1
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/GLOSSARY.md +38 -0
- package/LICENSE +21 -0
- package/README.md +165 -2
- package/assets/accessibility.cs +208 -0
- package/assets/browser-indicators.cs +37 -0
- package/assets/capture.cs +215 -0
- package/assets/clipboard.cs +197 -0
- package/assets/controls.cs +249 -0
- package/assets/indicator-preview.png +0 -0
- package/assets/indicators.cs +252 -0
- package/assets/input-timing.cs +42 -0
- package/assets/node-LICENSE.txt +2732 -0
- package/assets/ocr.cs +86 -0
- package/assets/ownership.cs +59 -0
- package/assets/pointer.cs +241 -0
- package/assets/puppeteer-LICENSE.txt +202 -0
- package/assets/readme-banner.svg +33 -0
- package/assets/stop.cs +197 -0
- package/assets/text.cs +279 -0
- package/assets/wgc.cs +167 -0
- package/assets/window-management.cs +217 -0
- package/assets/window-marker.cs +23 -0
- package/assets/window-references.cs +80 -0
- package/assets/windows.cs +127 -0
- package/dist/accessibility.d.ts +29 -0
- package/dist/accessibility.js +258 -0
- package/dist/browser-actions.d.ts +22 -0
- package/dist/browser-actions.js +246 -0
- package/dist/browser-indicators.d.ts +23 -0
- package/dist/browser-indicators.js +69 -0
- package/dist/browser-profiles.d.ts +10 -0
- package/dist/browser-profiles.js +174 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +2071 -0
- package/dist/capture.d.ts +74 -0
- package/dist/capture.js +199 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +384 -0
- package/dist/desktop.d.ts +20 -0
- package/dist/desktop.js +42 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +409 -0
- package/dist/input-timing.d.ts +24 -0
- package/dist/input-timing.js +70 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +85 -0
- package/dist/native.d.ts +11 -0
- package/dist/native.js +146 -0
- package/dist/ocr.d.ts +52 -0
- package/dist/ocr.js +246 -0
- package/dist/ownership.d.ts +4 -0
- package/dist/ownership.js +24 -0
- package/dist/pointer.d.ts +128 -0
- package/dist/pointer.js +198 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +72 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +449 -0
- package/dist/stop.d.ts +24 -0
- package/dist/stop.js +156 -0
- package/dist/text.d.ts +120 -0
- package/dist/text.js +212 -0
- package/dist/waits.d.ts +19 -0
- package/dist/waits.js +262 -0
- package/dist/window-management.d.ts +25 -0
- package/dist/window-management.js +167 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +553 -0
- package/docs/README.md +55 -0
- package/docs/adr/0001-preserve-reference-behavior.md +9 -0
- package/docs/adr/0002-window-lifetime-references.md +11 -0
- package/docs/adr/0003-exclusive-desktop-control.md +11 -0
- package/docs/adr/0004-pi-first-acceptance.md +9 -0
- package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
- package/docs/agents/domain.md +9 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +9 -0
- package/docs/browser.md +113 -0
- package/docs/implementation-plan.md +153 -0
- package/docs/indicators.md +53 -0
- package/docs/native-interop-research.md +17 -0
- package/docs/ownership.md +25 -0
- package/docs/pi-extension-research.md +57 -0
- package/docs/pointer.md +77 -0
- package/docs/runtime.md +42 -0
- package/docs/settings.md +33 -0
- package/docs/spec.md +230 -0
- package/docs/stopping.md +83 -0
- package/docs/text.md +99 -0
- package/docs/tickets.md +85 -0
- package/docs/validation.md +1420 -0
- package/docs/windows.md +171 -0
- package/package.json +134 -4
package/assets/wgc.cs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Diagnostics;
|
|
4
|
+
using System.Drawing;
|
|
5
|
+
using System.Drawing.Imaging;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using System.Runtime.InteropServices.WindowsRuntime;
|
|
9
|
+
using System.Text;
|
|
10
|
+
using System.Threading;
|
|
11
|
+
using System.Web.Script.Serialization;
|
|
12
|
+
using Windows.Foundation;
|
|
13
|
+
using Windows.Graphics.Capture;
|
|
14
|
+
using Windows.Graphics.DirectX;
|
|
15
|
+
using Windows.Graphics.DirectX.Direct3D11;
|
|
16
|
+
using Windows.Graphics.Imaging;
|
|
17
|
+
|
|
18
|
+
// Isolated composited-frame fallback. It owns no UI and never sends input.
|
|
19
|
+
class WgcBridge {
|
|
20
|
+
[ComImport, Guid("3628E81B-3CAC-4C60-B7F4-23CE0E0C3356"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
21
|
+
interface CaptureInterop {
|
|
22
|
+
IntPtr CreateForWindow(IntPtr window, ref Guid iid);
|
|
23
|
+
IntPtr CreateForMonitor(IntPtr monitor, ref Guid iid);
|
|
24
|
+
}
|
|
25
|
+
[ComImport, Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
26
|
+
interface MemoryBytes { void GetBuffer(out IntPtr bytes, out uint capacity); }
|
|
27
|
+
[StructLayout(LayoutKind.Sequential)] struct Rect { public int Left, Top, Right, Bottom; }
|
|
28
|
+
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr window);
|
|
29
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr window);
|
|
30
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr window);
|
|
31
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr window, out Rect rect);
|
|
32
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window, out uint process);
|
|
33
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr window, int attribute, out Rect rect, int bytes);
|
|
34
|
+
[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr context);
|
|
35
|
+
[DllImport("d3d11.dll")] static extern int D3D11CreateDevice(IntPtr adapter, int driver, IntPtr software, uint flags, IntPtr levels, uint count, uint sdk, out IntPtr device, out int level, out IntPtr context);
|
|
36
|
+
[DllImport("d3d11.dll")] static extern int CreateDirect3D11DeviceFromDXGIDevice(IntPtr device, out IntPtr inspectable);
|
|
37
|
+
|
|
38
|
+
static Rectangle Bounds(IntPtr window) {
|
|
39
|
+
Rect rect;
|
|
40
|
+
if (DwmGetWindowAttribute(window, 9, out rect, 16) != 0 && !GetWindowRect(window, out rect)) throw new InvalidOperationException("WINDOW_BOUNDS_UNAVAILABLE");
|
|
41
|
+
return Rectangle.FromLTRB(rect.Left, rect.Top, rect.Right, rect.Bottom);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static void Identity(IntPtr window, uint owner) {
|
|
45
|
+
uint current; GetWindowThreadProcessId(window, out current);
|
|
46
|
+
if (!IsWindow(window) || current != owner) throw new InvalidOperationException("WINDOW_IDENTITY_CHANGED");
|
|
47
|
+
if (!IsWindowVisible(window) || IsIconic(window)) throw new InvalidOperationException("WGC_SURFACE_UNAVAILABLE: target is hidden or minimized");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static IDirect3DDevice Device() {
|
|
51
|
+
IntPtr raw = IntPtr.Zero, context = IntPtr.Zero, dxgi = IntPtr.Zero, projected = IntPtr.Zero;
|
|
52
|
+
try {
|
|
53
|
+
int level;
|
|
54
|
+
Marshal.ThrowExceptionForHR(D3D11CreateDevice(IntPtr.Zero, 1, IntPtr.Zero, 0x20, IntPtr.Zero, 0, 7, out raw, out level, out context));
|
|
55
|
+
Guid iid = new Guid("54EC77FA-1377-44E6-8C32-88FD5F44C84C");
|
|
56
|
+
Marshal.ThrowExceptionForHR(Marshal.QueryInterface(raw, ref iid, out dxgi));
|
|
57
|
+
Marshal.ThrowExceptionForHR(CreateDirect3D11DeviceFromDXGIDevice(dxgi, out projected));
|
|
58
|
+
return (IDirect3DDevice)Marshal.GetObjectForIUnknown(projected);
|
|
59
|
+
} finally {
|
|
60
|
+
foreach (IntPtr pointer in new IntPtr[] { projected, dxgi, context, raw }) if (pointer != IntPtr.Zero) Marshal.Release(pointer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static Bitmap CopyPixels(SoftwareBitmap software) {
|
|
65
|
+
using (BitmapBuffer buffer = software.LockBuffer(BitmapBufferAccessMode.Read))
|
|
66
|
+
using (IMemoryBufferReference reference = buffer.CreateReference()) {
|
|
67
|
+
IntPtr source; uint capacity;
|
|
68
|
+
((MemoryBytes)reference).GetBuffer(out source, out capacity);
|
|
69
|
+
BitmapPlaneDescription plane = buffer.GetPlaneDescription(0);
|
|
70
|
+
int width = software.PixelWidth, height = software.PixelHeight, rowLength = checked(width * 4);
|
|
71
|
+
if (width <= 0 || height <= 0 || (long)width * height > 40000000 || plane.StartIndex < 0 || plane.Stride < rowLength || (long)plane.StartIndex + (long)(height - 1) * plane.Stride + rowLength > capacity) throw new InvalidOperationException("WGC_INVALID_PIXEL_BUFFER");
|
|
72
|
+
Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
|
|
73
|
+
BitmapData output = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
|
74
|
+
try {
|
|
75
|
+
byte[] row = new byte[rowLength];
|
|
76
|
+
for (int y = 0; y < height; y++) {
|
|
77
|
+
Marshal.Copy(IntPtr.Add(source, checked(plane.StartIndex + y * plane.Stride)), row, 0, rowLength);
|
|
78
|
+
Marshal.Copy(row, 0, IntPtr.Add(output.Scan0, y * output.Stride), rowLength);
|
|
79
|
+
}
|
|
80
|
+
} catch { bitmap.UnlockBits(output); bitmap.Dispose(); throw; }
|
|
81
|
+
bitmap.UnlockBits(output); return bitmap;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static void ValidateMarker(Dictionary<string,object> args,IntPtr window,uint owner) {
|
|
86
|
+
object raw;if(!args.TryGetValue("_windowMarker",out raw) || raw==null)return;
|
|
87
|
+
var marker=raw as Dictionary<string,object>;object name;
|
|
88
|
+
if(marker==null || !marker.TryGetValue("name",out name) || !WindowMarker.Matches(window,owner,Convert.ToString(name)))throw new InvalidOperationException("WINDOW_REFERENCE_LOST: delegated capture lifetime proof is unavailable");
|
|
89
|
+
}
|
|
90
|
+
static object Capture(Dictionary<string, object> args) {
|
|
91
|
+
IntPtr window = new IntPtr(Convert.ToInt64(args["nativeWindowHandle"]));
|
|
92
|
+
uint owner = Convert.ToUInt32(args["processId"]);
|
|
93
|
+
int maximum = Convert.ToInt32(args["maxWidth"]);
|
|
94
|
+
ValidateMarker(args,window,owner);Identity(window, owner);
|
|
95
|
+
Rectangle bounds = Bounds(window);
|
|
96
|
+
if (bounds.Width <= 0 || bounds.Height <= 0 || (long)bounds.Width * bounds.Height > 40000000) throw new InvalidOperationException("WGC_INVALID_BOUNDS");
|
|
97
|
+
if (!GraphicsCaptureSession.IsSupported()) throw new InvalidOperationException("WGC_UNSUPPORTED");
|
|
98
|
+
object factory = WindowsRuntimeMarshal.GetActivationFactory(typeof(GraphicsCaptureItem));
|
|
99
|
+
Guid itemIid = new Guid("79C3F95B-31F7-4EC2-A464-632EF5D30760");
|
|
100
|
+
IntPtr itemPointer = ((CaptureInterop)factory).CreateForWindow(window, ref itemIid);
|
|
101
|
+
GraphicsCaptureItem item;
|
|
102
|
+
try { item = (GraphicsCaptureItem)Marshal.GetObjectForIUnknown(itemPointer); }
|
|
103
|
+
finally { Marshal.Release(itemPointer); }
|
|
104
|
+
Stopwatch deadline = Stopwatch.StartNew();
|
|
105
|
+
using (IDirect3DDevice device = Device())
|
|
106
|
+
using (Direct3D11CaptureFramePool pool = Direct3D11CaptureFramePool.CreateFreeThreaded(device, DirectXPixelFormat.B8G8R8A8UIntNormalized, 1, item.Size))
|
|
107
|
+
using (GraphicsCaptureSession session = pool.CreateCaptureSession(item)) {
|
|
108
|
+
session.IsCursorCaptureEnabled = true;
|
|
109
|
+
session.StartCapture();
|
|
110
|
+
Direct3D11CaptureFrame frame = null;
|
|
111
|
+
while (frame == null && deadline.ElapsedMilliseconds < 4000) { frame = pool.TryGetNextFrame(); if (frame == null) Thread.Sleep(20); }
|
|
112
|
+
if (frame == null) throw new TimeoutException("WGC_FRAME_TIMEOUT");
|
|
113
|
+
using (frame) {
|
|
114
|
+
IAsyncOperation<SoftwareBitmap> copy = SoftwareBitmap.CreateCopyFromSurfaceAsync(frame.Surface, BitmapAlphaMode.Premultiplied);
|
|
115
|
+
while (copy.Status == AsyncStatus.Started && deadline.ElapsedMilliseconds < 4000) Thread.Sleep(10);
|
|
116
|
+
if (copy.Status == AsyncStatus.Started) { copy.Cancel(); throw new TimeoutException("WGC_COPY_TIMEOUT"); }
|
|
117
|
+
if (copy.Status != AsyncStatus.Completed) throw new InvalidOperationException("WGC_COPY_FAILED: " + copy.Status);
|
|
118
|
+
using (SoftwareBitmap software = copy.GetResults())
|
|
119
|
+
using (Bitmap bitmap = CopyPixels(software)) {
|
|
120
|
+
ValidateMarker(args,window,owner);Identity(window, owner);
|
|
121
|
+
if (Bounds(window) != bounds) throw new InvalidOperationException("WINDOW_MOVED_DURING_CAPTURE");
|
|
122
|
+
Bitmap output = bitmap;
|
|
123
|
+
try {
|
|
124
|
+
if (maximum > 0 && bitmap.Width > maximum) {
|
|
125
|
+
output = new Bitmap(maximum, Math.Max(1, (int)Math.Round((double)bitmap.Height * maximum / bitmap.Width)), PixelFormat.Format32bppArgb);
|
|
126
|
+
using (Graphics graphics = Graphics.FromImage(output)) {
|
|
127
|
+
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
128
|
+
graphics.DrawImage(bitmap, new Rectangle(0, 0, output.Width, output.Height));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
byte[] png;
|
|
132
|
+
using (MemoryStream memory = new MemoryStream()) { output.Save(memory, ImageFormat.Png); png = memory.ToArray(); }
|
|
133
|
+
ValidateMarker(args,window,owner);
|
|
134
|
+
return new Dictionary<string, object> {
|
|
135
|
+
{ "ok", true }, { "action", "screenshot" }, {"windowRef",args.ContainsKey("_windowMarker") && args["_windowMarker"] is Dictionary<string,object>?((Dictionary<string,object>)args["_windowMarker"])["windowRef"]:null}, { "screenshot", new Dictionary<string, object> {
|
|
136
|
+
{ "base64", Convert.ToBase64String(png) }, { "mimeType", "image/png" }, { "bytes", png.Length },
|
|
137
|
+
{ "width", output.Width }, { "height", output.Height }, { "method", "wgc" },
|
|
138
|
+
{ "bounds", new Dictionary<string, object> { { "x", bounds.X }, { "y", bounds.Y }, { "width", bounds.Width }, { "height", bounds.Height } } },
|
|
139
|
+
{ "origin", new Dictionary<string, object> { { "x", bounds.X }, { "y", bounds.Y } } },
|
|
140
|
+
{ "imageScale", (double)output.Width / bounds.Width }, { "imageScaleY", (double)output.Height / bounds.Height },
|
|
141
|
+
{ "windowRef",args.ContainsKey("_windowMarker") && args["_windowMarker"] is Dictionary<string,object>?((Dictionary<string,object>)args["_windowMarker"])["windowRef"]:null }, { "source", "window" }, { "occludedPossible", false }, { "windowCaptureFailed", false }, { "captureWarnings", new string[0] }
|
|
142
|
+
} }
|
|
143
|
+
};
|
|
144
|
+
} finally { if (output != bitmap) output.Dispose(); }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
[MTAThread]
|
|
151
|
+
static void Main() {
|
|
152
|
+
Console.InputEncoding = new UTF8Encoding(false); Console.OutputEncoding = new UTF8Encoding(false);
|
|
153
|
+
SetProcessDpiAwarenessContext(new IntPtr(-4));
|
|
154
|
+
JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = 32 * 1024 * 1024 };
|
|
155
|
+
string line;
|
|
156
|
+
while ((line = Console.ReadLine()) != null) {
|
|
157
|
+
try {
|
|
158
|
+
Dictionary<string, object> request = json.Deserialize<Dictionary<string, object>>(line);
|
|
159
|
+
if (request.ContainsKey("cancel")) continue;
|
|
160
|
+
object result;
|
|
161
|
+
try { result = Capture((Dictionary<string, object>)request["args"]); }
|
|
162
|
+
catch (Exception error) { result = new Dictionary<string, object> { { "ok", false }, { "action", "screenshot" }, { "error", "WGC_CAPTURE_FAILED: " + error.Message } }; }
|
|
163
|
+
Console.WriteLine(json.Serialize(new Dictionary<string, object> { { "id", request["id"] }, { "result", result } })); Console.Out.Flush();
|
|
164
|
+
} catch (Exception error) { Console.Error.WriteLine("WGC_PROTOCOL_ERROR: " + error.Message); }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Runtime.InteropServices;
|
|
5
|
+
using System.Text;
|
|
6
|
+
using System.Threading;
|
|
7
|
+
using Microsoft.Win32;
|
|
8
|
+
|
|
9
|
+
partial class WindowsBridge {
|
|
10
|
+
[DllImport("user32.dll",EntryPoint="ShowWindowAsync")] static extern bool ManagementShow(IntPtr window,int command);
|
|
11
|
+
[DllImport("user32.dll",EntryPoint="SetWindowPos",SetLastError=true)] static extern bool ManagementMove(IntPtr window,IntPtr after,int x,int y,int width,int height,uint flags);
|
|
12
|
+
[DllImport("user32.dll",EntryPoint="PostMessageW",SetLastError=true)] static extern bool ManagementPost(IntPtr window,uint message,IntPtr wParam,IntPtr lParam);
|
|
13
|
+
[DllImport("user32.dll",EntryPoint="AttachThreadInput")] static extern bool ManagementAttach(uint from,uint to,bool attach);
|
|
14
|
+
[DllImport("kernel32.dll",EntryPoint="GetCurrentThreadId")] static extern uint ManagementThread();
|
|
15
|
+
static uint ManagementOwner(IntPtr window) {uint owner;GetWindowThreadProcessId(window,out owner);return owner;}
|
|
16
|
+
static void ManagementAssert(IntPtr window,uint owner) {CheckWindowReference(window);if(!IsWindow(window)||ManagementOwner(window)!=owner)throw new InvalidOperationException("WINDOW_IDENTITY_CHANGED: refresh window observations before acting");}
|
|
17
|
+
static bool? ManagementResponsive(IntPtr window) {UIntPtr ignored;bool answered=SendMessageTimeout(window,0,UIntPtr.Zero,IntPtr.Zero,3,100,out ignored)!=IntPtr.Zero;int error=Marshal.GetLastWin32Error();return !answered&&error==5?(bool?)null:answered;}
|
|
18
|
+
static Dictionary<string,object> ManagementWindow(IntPtr window,uint owner) {
|
|
19
|
+
Rect bounds;if(!GetWindowRect(window,out bounds))throw new InvalidOperationException("WINDOW_BOUNDS_UNAVAILABLE");
|
|
20
|
+
StringBuilder title=new StringBuilder(2048);GetWindowText(window,title,title.Capacity);
|
|
21
|
+
return new Dictionary<string,object>{{"nativeWindowHandle",window.ToInt64()},{"processId",owner},{"name",title.ToString()},{"boundingBox",new Dictionary<string,object>{{"x",bounds.Left},{"y",bounds.Top},{"width",bounds.Right-bounds.Left},{"height",bounds.Bottom-bounds.Top}}}};
|
|
22
|
+
}
|
|
23
|
+
// Read only the documented, 8-byte-aligned WTSINFOEX level-1 prefix;
|
|
24
|
+
// account-name fields are never copied or reported.
|
|
25
|
+
[StructLayout(LayoutKind.Explicit,Size=20)]struct ManagementSessionHeader {
|
|
26
|
+
[FieldOffset(0)]public uint Level;[FieldOffset(8)]public uint Session;
|
|
27
|
+
[FieldOffset(12)]public uint Connection;[FieldOffset(16)]public int Flags;
|
|
28
|
+
}
|
|
29
|
+
[DllImport("kernel32.dll",EntryPoint="GetCurrentProcessId")]static extern uint ManagementProcessId();
|
|
30
|
+
[DllImport("kernel32.dll",EntryPoint="ProcessIdToSessionId")]static extern bool ManagementSessionId(uint process,out uint session);
|
|
31
|
+
[DllImport("wtsapi32.dll",EntryPoint="WTSQuerySessionInformationW")]static extern bool ManagementQuerySession(IntPtr server,uint session,int kind,out IntPtr buffer,out uint bytes);
|
|
32
|
+
[DllImport("wtsapi32.dll",EntryPoint="WTSFreeMemory")]static extern void ManagementFreeSession(IntPtr buffer);
|
|
33
|
+
static void ManagementEnsureUnlockedSession() {
|
|
34
|
+
uint session;if(!ManagementSessionId(ManagementProcessId(),out session))return;
|
|
35
|
+
IntPtr buffer=IntPtr.Zero;uint bytes;
|
|
36
|
+
try {
|
|
37
|
+
if(!ManagementQuerySession(IntPtr.Zero,session,25,out buffer,out bytes) || buffer==IntPtr.Zero || bytes<20)return;
|
|
38
|
+
ManagementSessionHeader state=(ManagementSessionHeader)Marshal.PtrToStructure(buffer,typeof(ManagementSessionHeader));
|
|
39
|
+
if(state.Level==1 && state.Session==session && state.Flags==0)throw new InvalidOperationException("DESKTOP_SESSION_LOCKED: unlock Windows before requesting foreground input");
|
|
40
|
+
}finally {if(buffer!=IntPtr.Zero)ManagementFreeSession(buffer);}
|
|
41
|
+
// Unknown/unavailable session state does not invent a lock diagnosis;
|
|
42
|
+
// the existing identity, foreground and input guards still apply.
|
|
43
|
+
}
|
|
44
|
+
[DllImport("user32.dll",EntryPoint="BringWindowToTop")]static extern bool ManagementBringToTop(IntPtr window);
|
|
45
|
+
sealed class ActivationRefusedException : InvalidOperationException {
|
|
46
|
+
public readonly Dictionary<string,object> Diagnostic;
|
|
47
|
+
public ActivationRefusedException(Dictionary<string,object> diagnostic) : base("TARGET_ACTIVATION_REFUSED: Windows did not grant foreground access") {Diagnostic=diagnostic;}
|
|
48
|
+
}
|
|
49
|
+
static string ManagementForegroundRelationship(IntPtr foreground,IntPtr window,uint owner) {
|
|
50
|
+
if(foreground==IntPtr.Zero)return "unavailable";
|
|
51
|
+
if(foreground==window)return "target";
|
|
52
|
+
if(GetAncestor(foreground,2)==window)return "target_child";
|
|
53
|
+
return ManagementOwner(foreground)==owner?"same_process":"other_process";
|
|
54
|
+
}
|
|
55
|
+
static bool ManagementActivate(IntPtr window,uint owner) {return ManagementActivate(window,owner,null);}
|
|
56
|
+
// One active operation per desktop; retain the existing lifetime witness,
|
|
57
|
+
// not another element cache. A helper restart forgets visible intent safely.
|
|
58
|
+
static string visibleOperation;
|
|
59
|
+
static WindowReference visibleTarget;
|
|
60
|
+
static void ApplyVisibleIntent(Dictionary<string,object> args,IntPtr window) {
|
|
61
|
+
if(args.ContainsKey("activate") || TextOption(args,"dispatch","")=="background" || TextOption(args,"method","")=="background")return;
|
|
62
|
+
if(visibleTarget!=null && visibleOperation==pointerOperation && visibleTarget.Window==window && visibleTarget.Owner==WindowOwner(window) && WindowMarker.Matches(window,visibleTarget.Owner,visibleTarget.Marker))args["activate"]=true;
|
|
63
|
+
}
|
|
64
|
+
static bool ManagementActivate(IntPtr window,uint owner,Dictionary<string,object> diagnostics) {
|
|
65
|
+
ManagementAssert(window,owner);ManagementEnsureUnlockedSession();
|
|
66
|
+
if(IsIconic(window)){BeginWindowDispatch();ManagementShow(window,9);}
|
|
67
|
+
if(GetForegroundWindow()==window)return true;
|
|
68
|
+
BeginWindowDispatch();bool initial=SetForegroundWindow(window);
|
|
69
|
+
if(diagnostics!=null)diagnostics["initialRequestAccepted"]=initial;
|
|
70
|
+
if(GetForegroundWindow()==window)return true;
|
|
71
|
+
IntPtr front=GetForegroundWindow();uint ignored;uint frontThread=GetWindowThreadProcessId(front,out ignored),own=ManagementThread();bool attached=false;
|
|
72
|
+
if(diagnostics!=null)diagnostics["foregroundBeforeFallback"]=ManagementForegroundRelationship(front,window,owner);
|
|
73
|
+
try {
|
|
74
|
+
bool attempted=frontThread!=0&&frontThread!=own;
|
|
75
|
+
if(attempted)attached=ManagementAttach(own,frontThread,true);
|
|
76
|
+
if(diagnostics!=null){diagnostics["inputThreadAttachAttempted"]=attempted;if(attempted)diagnostics["inputThreadAttached"]=attached;}
|
|
77
|
+
ManagementAssert(window,owner);BeginWindowDispatch();bool raised=ManagementBringToTop(window);ManagementAssert(window,owner);BeginWindowDispatch();bool requested=SetForegroundWindow(window);
|
|
78
|
+
if(diagnostics!=null){diagnostics["raiseAccepted"]=raised;diagnostics["fallbackRequestAccepted"]=requested;}
|
|
79
|
+
}
|
|
80
|
+
finally {if(attached)ManagementAttach(own,frontThread,false);}
|
|
81
|
+
for(int index=0;index<8&&GetForegroundWindow()!=window;index++)Thread.Sleep(10);
|
|
82
|
+
IntPtr finalForeground=GetForegroundWindow();
|
|
83
|
+
if(diagnostics!=null)diagnostics["foregroundAfterFallback"]=ManagementForegroundRelationship(finalForeground,window,owner);
|
|
84
|
+
return finalForeground==window;
|
|
85
|
+
}
|
|
86
|
+
static object ManageNativeWindow(string action,Dictionary<string,object> args) {
|
|
87
|
+
if(action=="launch_app")return LaunchFromDesktop(args);
|
|
88
|
+
// Shared Win32 capture resolver performs HWND/PID/title matching without resolving UIA.
|
|
89
|
+
IntPtr window=ResolveCaptureWindow(args);uint owner=ManagementOwner(window);ManagementAssert(window,owner);
|
|
90
|
+
Dictionary<string,object> result=Result(true,action);result["nativeWindowHandle"]=window.ToInt64();result["processId"]=owner;result["window"]=ManagementWindow(window,owner);
|
|
91
|
+
bool? responsive=ManagementResponsive(window);result["targetResponsive"]=responsive;
|
|
92
|
+
ManagementAssert(window,owner);
|
|
93
|
+
if(action=="close_window") {
|
|
94
|
+
BeginWindowDispatch();bool posted=ManagementPost(window,0x0010,IntPtr.Zero,IntPtr.Zero);result["posted"]=posted;result["verification"]=posted?"sent":"unconfirmed";result["note"]=posted?"WM_CLOSE was dispatched; the application may veto closure or show a save dialog. Observe windows to verify.":"Windows refused posting WM_CLOSE; no closure was confirmed.";if(!posted)result["nativeError"]=Marshal.GetLastWin32Error();return result;
|
|
95
|
+
}
|
|
96
|
+
if(action=="move_window") {
|
|
97
|
+
int x=Convert.ToInt32(args["x"]),y=Convert.ToInt32(args["y"]);
|
|
98
|
+
BeginWindowDispatch();bool moved=ManagementMove(window,IntPtr.Zero,x,y,0,0,0x0001|0x0004|0x0010|0x4000);
|
|
99
|
+
result["moved"]=moved;result["x"]=x;result["y"]=y;result["verification"]=moved?"sent":"unconfirmed";
|
|
100
|
+
if(!moved)result["warning"]="SetWindowPos refused the move; the window may be unavailable or maximized.";
|
|
101
|
+
if(!args.ContainsKey("activate")||!Convert.ToBoolean(args["activate"]))return result;
|
|
102
|
+
}
|
|
103
|
+
bool activated=responsive==true&&ManagementActivate(window,owner);result["activated"]=activated;
|
|
104
|
+
if(activated){visibleOperation=pointerOperation;visibleTarget=ObserveWindowReference(window,result);if(visibleTarget!=null){currentReference=visibleTarget;result["windowRef"]=visibleTarget.Id;}}
|
|
105
|
+
if(!activated){result["verification"]="unconfirmed";result["warning"]=responsive==true?"Windows foreground rules refused activation; no keyboard or mouse workaround was sent.":"The target is unresponsive or inaccessible; activation was not attempted.";}
|
|
106
|
+
else if(action=="activate_window")result["verification"]="sent";
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static readonly HashSet<string> ManagementCommandHosts=new HashSet<string>(StringComparer.OrdinalIgnoreCase){"cmd","powershell","pwsh","wt","wsl","bash","conhost","wscript","cscript","mshta","rundll32","regsvr32","msiexec","forfiles","schtasks"};
|
|
111
|
+
[DllImport("shlwapi.dll",CharSet=CharSet.Unicode,EntryPoint="AssocQueryStringW")]static extern int ManagementAssociation(uint flags,int kind,string association,string verb,StringBuilder output,ref uint length);
|
|
112
|
+
static string AssociationString(string association,bool protocol,int kind) {
|
|
113
|
+
// ASSOCF_INIT_IGNOREUNKNOWN refuses the generic Open-With fallback;
|
|
114
|
+
// ASSOCF_IS_PROTOCOL honors current-user HTTP(S) associations.
|
|
115
|
+
uint length=0;uint flags=0x400u|(protocol?0x1000u:0u);
|
|
116
|
+
ManagementAssociation(flags,kind,association,"open",null,ref length);
|
|
117
|
+
if(length==0 || length>32768)return "";
|
|
118
|
+
StringBuilder output=new StringBuilder((int)length);
|
|
119
|
+
return ManagementAssociation(flags,kind,association,"open",output,ref length)==0?output.ToString():"";
|
|
120
|
+
}
|
|
121
|
+
static Dictionary<string,object> DefaultLaunchHandler(string association,bool protocol) {
|
|
122
|
+
string executable=AssociationString(association,protocol,2),name=AssociationString(association,protocol,4),progId=AssociationString(association,protocol,20);
|
|
123
|
+
if(executable.Length==0 && name.Length==0 && progId.Length==0)throw new InvalidOperationException("DEFAULT_APP_UNAVAILABLE: Windows did not resolve an open handler for "+association);
|
|
124
|
+
return new Dictionary<string,object>{{"association",association},{"executable",executable},{"name",name},{"progId",progId},{"source","windows-association"}};
|
|
125
|
+
}
|
|
126
|
+
static void ReleaseLaunchCom(object value) {try{if(value!=null&&Marshal.IsComObject(value))Marshal.ReleaseComObject(value);}catch(InvalidComObjectException){}}
|
|
127
|
+
static bool FullyQualifiedLaunchPath(string value) {return value.Length>=3&&Char.IsLetter(value[0])&&value[1]==':'&&(value[2]=='\\'||value[2]=='/')||value.StartsWith("\\\\",StringComparison.Ordinal)&&value.TrimStart('\\').Split('\\').Length>=2;}
|
|
128
|
+
static string ResolveLaunchFile(string target) {
|
|
129
|
+
if(Path.IsPathRooted(target)) {
|
|
130
|
+
if(!FullyQualifiedLaunchPath(target))throw new InvalidOperationException("LAUNCH_INVALID_TARGET: use a fully qualified absolute path");
|
|
131
|
+
if(!File.Exists(target))throw new InvalidOperationException("LAUNCH_NOT_FOUND: target file does not exist");
|
|
132
|
+
return Path.GetFullPath(target);
|
|
133
|
+
}
|
|
134
|
+
if(target.IndexOfAny(new char[]{'\\','/',':'})>=0)return null;
|
|
135
|
+
string[] extensions=Path.GetExtension(target).Length>0?new string[]{""}:new string[]{".exe",".com",".lnk"};
|
|
136
|
+
foreach(string directory in (Environment.GetEnvironmentVariable("PATH")??"").Split(';'))foreach(string extension in extensions) {
|
|
137
|
+
string candidate;try{candidate=Path.Combine(directory.Trim('"'),target+extension);}catch(ArgumentException){continue;}
|
|
138
|
+
if(File.Exists(candidate))return Path.GetFullPath(candidate);
|
|
139
|
+
}
|
|
140
|
+
string name=target.EndsWith(".exe",StringComparison.OrdinalIgnoreCase)?target:target+".exe";
|
|
141
|
+
foreach(RegistryHive hive in new RegistryHive[]{RegistryHive.CurrentUser,RegistryHive.LocalMachine})foreach(RegistryView view in new RegistryView[]{RegistryView.Registry64,RegistryView.Registry32}) {
|
|
142
|
+
try{using(RegistryKey root=RegistryKey.OpenBaseKey(hive,view))using(RegistryKey key=root.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\"+name)){
|
|
143
|
+
string candidate=key==null?null:Convert.ToString(key.GetValue(""));if(String.IsNullOrEmpty(candidate))continue;candidate=Environment.ExpandEnvironmentVariables(candidate.Trim('"'));if(File.Exists(candidate))return Path.GetFullPath(candidate);
|
|
144
|
+
}}catch(System.Security.SecurityException){}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
static string ResolveInstalledLaunch(string target) {
|
|
149
|
+
object application=null,folder=null,items=null;List<string> matches=new List<string>();
|
|
150
|
+
try{
|
|
151
|
+
application=Activator.CreateInstance(Type.GetTypeFromProgID("Shell.Application"));folder=((dynamic)application).NameSpace("shell:AppsFolder");if(folder==null)throw new InvalidOperationException("Installed app enumeration is unavailable");items=((dynamic)folder).Items();int count=((dynamic)items).Count;
|
|
152
|
+
for(int index=0;index<count;index++) {
|
|
153
|
+
object item=null;try{item=((dynamic)items).Item(index);string name=Convert.ToString(((dynamic)item).Name),id=Convert.ToString(((dynamic)item).ExtendedProperty("System.AppUserModel.ID"));
|
|
154
|
+
if(String.Equals(target,name,StringComparison.OrdinalIgnoreCase)||String.Equals(target,id,StringComparison.OrdinalIgnoreCase)){if(String.IsNullOrEmpty(id))id=Convert.ToString(((dynamic)item).Path);if(!String.IsNullOrEmpty(id))matches.Add(id);}
|
|
155
|
+
}finally{ReleaseLaunchCom(item);}
|
|
156
|
+
}
|
|
157
|
+
}finally{ReleaseLaunchCom(items);ReleaseLaunchCom(folder);ReleaseLaunchCom(application);}
|
|
158
|
+
if(matches.Count==0)throw new InvalidOperationException("LAUNCH_NOT_FOUND: use an executable path or exact installed app name/AppUserModelID");
|
|
159
|
+
if(matches.Count!=1)throw new InvalidOperationException("LAUNCH_AMBIGUOUS: use a full executable path or AppUserModelID");
|
|
160
|
+
return "shell:AppsFolder\\"+matches[0];
|
|
161
|
+
}
|
|
162
|
+
static void CheckLaunchProgram(string file,string arguments) {
|
|
163
|
+
string program=file,embedded="";HashSet<string> seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
164
|
+
while(String.Equals(Path.GetExtension(program),".lnk",StringComparison.OrdinalIgnoreCase)){
|
|
165
|
+
if(!seen.Add(program)||seen.Count>8)throw new InvalidOperationException("LAUNCH_INVALID_TARGET: shortcut chain is cyclic or too deep");
|
|
166
|
+
object shell=null,link=null;try{shell=Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell"));link=((dynamic)shell).CreateShortcut(program);embedded+=" "+Convert.ToString(((dynamic)link).Arguments);program=Environment.ExpandEnvironmentVariables(Convert.ToString(((dynamic)link).TargetPath));if(String.IsNullOrWhiteSpace(program))throw new InvalidOperationException("LAUNCH_INVALID_TARGET: shortcut has no executable target");}finally{ReleaseLaunchCom(link);ReleaseLaunchCom(shell);}
|
|
167
|
+
}
|
|
168
|
+
if((arguments.Trim().Length>0||embedded.Trim().Length>0)&&ManagementCommandHosts.Contains(Path.GetFileNameWithoutExtension(program)))throw new InvalidOperationException("LAUNCH_COMMAND_REFUSED: command/script hosts accept no arguments through launch_app; use Pi's shell tool");
|
|
169
|
+
}
|
|
170
|
+
static object LaunchFromDesktop(Dictionary<string,object> args) {
|
|
171
|
+
Dictionary<string,object> result=null;Exception failure=null;
|
|
172
|
+
Thread thread=new Thread(delegate(){
|
|
173
|
+
try{
|
|
174
|
+
string target=Convert.ToString(args["target"]).Trim(),arguments=Convert.ToString(args["arguments"]),directory=Convert.ToString(args["workingDirectory"]);
|
|
175
|
+
Uri uri;bool protocol=Uri.TryCreate(target,UriKind.Absolute,out uri)&&(uri.Scheme=="http"||uri.Scheme=="https");
|
|
176
|
+
if(!protocol)target=Environment.ExpandEnvironmentVariables(target);
|
|
177
|
+
if(protocol && String.IsNullOrEmpty(uri.Host))throw new InvalidOperationException("LAUNCH_INVALID_TARGET: HTTP(S) URL requires a host");
|
|
178
|
+
bool browser=String.Equals(target,"browser",StringComparison.OrdinalIgnoreCase);
|
|
179
|
+
string file=protocol||browser?null:ResolveLaunchFile(target),kind=browser?"default-browser":protocol?"url":file==null?"installed-app":"file";
|
|
180
|
+
Dictionary<string,object> handler=null;
|
|
181
|
+
if(file!=null){string extension=Path.GetExtension(file);if(String.Equals(extension,".exe",StringComparison.OrdinalIgnoreCase)||String.Equals(extension,".com",StringComparison.OrdinalIgnoreCase)||String.Equals(extension,".lnk",StringComparison.OrdinalIgnoreCase))CheckLaunchProgram(file,arguments);else {
|
|
182
|
+
if(new HashSet<string>(StringComparer.OrdinalIgnoreCase){".bat",".cmd",".ps1",".vbs",".vbe",".js",".jse",".wsf",".wsh",".hta",".reg",".msi",".msp",".scr",".cpl"}.Contains(extension))throw new InvalidOperationException("LAUNCH_COMMAND_REFUSED: use Pi's shell tool for scripts/installers");
|
|
183
|
+
kind="document";handler=DefaultLaunchHandler(Path.GetExtension(file),false);
|
|
184
|
+
}target=file;}
|
|
185
|
+
else if(browser)handler=DefaultLaunchHandler("https",true);
|
|
186
|
+
else if(protocol)handler=DefaultLaunchHandler(uri.Scheme,true);
|
|
187
|
+
else target=ResolveInstalledLaunch(target);
|
|
188
|
+
if(handler!=null) {
|
|
189
|
+
if(arguments.Length>0||directory.Length>0)throw new InvalidOperationException("LAUNCH_INVALID_ARGUMENTS: default-handler URLs/documents do not accept arguments or workingDirectory");
|
|
190
|
+
string handlerExecutable=Convert.ToString(handler["executable"]);
|
|
191
|
+
if(ManagementCommandHosts.Contains(Path.GetFileNameWithoutExtension(handlerExecutable)))throw new InvalidOperationException("LAUNCH_COMMAND_REFUSED: default handler is a command/script host; use Pi's shell tool");
|
|
192
|
+
}
|
|
193
|
+
if(directory.Length>0){directory=Environment.ExpandEnvironmentVariables(directory);if(!FullyQualifiedLaunchPath(directory)||!Directory.Exists(directory))throw new InvalidOperationException("LAUNCH_INVALID_DIRECTORY: use an existing fully qualified directory");directory=Path.GetFullPath(directory);}
|
|
194
|
+
if(kind=="installed-app"&&(arguments.Length>0||directory.Length>0))throw new InvalidOperationException("LAUNCH_INVALID_ARGUMENTS: installed AppUserModelID launches do not accept arguments or workingDirectory");
|
|
195
|
+
if(Option(args,"resolveOnly",false)) {
|
|
196
|
+
result=Result(true,"launch_app");result["target"]=target;result["targetKind"]=kind;result["handler"]=handler;result["verification"]="resolved";result["launched"]=false;
|
|
197
|
+
result["note"]="Resolution only; no app was opened. Select and observe the intended existing window before creating a task tab/document. Handler metadata is not a window identity.";return;
|
|
198
|
+
}
|
|
199
|
+
if(browser){target=Convert.ToString(handler["executable"]);if(target.Length==0)throw new InvalidOperationException("DEFAULT_APP_UNAVAILABLE: Windows resolved the browser but did not provide a blank-window launch executable; reuse its observed window or open a requested HTTP(S) URL");}
|
|
200
|
+
object windows=null,desktop=null,document=null,application=null;
|
|
201
|
+
try{
|
|
202
|
+
windows=Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("9BA05972-F6A8-11CF-A442-00A0C90A8F39")));
|
|
203
|
+
object location=0,root=null;int handle=0;desktop=((dynamic)windows).FindWindowSW(ref location,ref root,8,out handle,1);
|
|
204
|
+
if(desktop==null)throw new InvalidOperationException("interactive Explorer desktop is unavailable");
|
|
205
|
+
document=((dynamic)desktop).Document;application=((dynamic)document).Application;
|
|
206
|
+
((dynamic)application).ShellExecute(target,arguments,directory,"open",Convert.ToBoolean(args["activate"])?1:4);
|
|
207
|
+
}catch(Exception error){throw new InvalidOperationException("LAUNCH_SHELL_FAILED: "+error.Message+"; no child-process fallback or replay was attempted",error);}
|
|
208
|
+
finally{ReleaseLaunchCom(application);ReleaseLaunchCom(document);ReleaseLaunchCom(desktop);ReleaseLaunchCom(windows);}
|
|
209
|
+
result=Result(true,"launch_app");result["target"]=target;result["targetKind"]=kind;if(handler!=null)result["handler"]=handler;result["launchMethod"]="explorer-shell";result["verification"]="dispatched";result["note"]="Explorer accepted the launch. New windows are observations, not confirmed matches. The app can reuse a window or activate itself; use list_windows/wait_for to verify. No elevation was requested.";
|
|
210
|
+
}catch(Exception error){failure=error;}
|
|
211
|
+
});
|
|
212
|
+
thread.IsBackground=true;thread.SetApartmentState(ApartmentState.STA);thread.Start();
|
|
213
|
+
if(!thread.Join(15000))throw new InvalidOperationException("LAUNCH_PENDING_TIMEOUT: Explorer outcome is unknown; do not replay the launch");
|
|
214
|
+
if(failure!=null)throw failure;
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Runtime.InteropServices;
|
|
3
|
+
|
|
4
|
+
// Stateless witness checks shared by the authoritative bridge and delegated WGC.
|
|
5
|
+
static class WindowMarker {
|
|
6
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool SetProp(IntPtr window,string name,IntPtr value);
|
|
7
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern IntPtr GetProp(IntPtr window,string name);
|
|
8
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern IntPtr RemoveProp(IntPtr window,string name);
|
|
9
|
+
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr window);
|
|
10
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr window,out uint owner);
|
|
11
|
+
internal static bool Matches(IntPtr window,uint owner,string name) {
|
|
12
|
+
uint actual;GetWindowThreadProcessId(window,out actual);
|
|
13
|
+
return owner!=0 && actual==owner && IsWindow(window) && GetProp(window,name)==new IntPtr(1);
|
|
14
|
+
}
|
|
15
|
+
internal static bool Bind(IntPtr window,uint owner,string name,out int error) {
|
|
16
|
+
uint actual;GetWindowThreadProcessId(window,out actual);error=0;
|
|
17
|
+
if(actual!=owner || owner==0 || !IsWindow(window))return false;
|
|
18
|
+
if(!SetProp(window,name,new IntPtr(1))){error=Marshal.GetLastWin32Error();return false;}
|
|
19
|
+
if(Matches(window,owner,name))return true;
|
|
20
|
+
Remove(window,owner,name);return false;
|
|
21
|
+
}
|
|
22
|
+
internal static void Remove(IntPtr window,uint owner,string name) {if(Matches(window,owner,name))RemoveProp(window,name);}
|
|
23
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
|
|
4
|
+
partial class WindowsBridge {
|
|
5
|
+
sealed class WindowReference {
|
|
6
|
+
internal string Id,Marker;internal IntPtr Window;internal uint Owner;internal DateTime Seen;
|
|
7
|
+
}
|
|
8
|
+
static readonly string referenceEpoch=Convert.ToBase64String(Guid.NewGuid().ToByteArray()).TrimEnd('=').Replace('+','-').Replace('/','_');
|
|
9
|
+
static long referenceSequence,elementSequence;
|
|
10
|
+
static readonly Dictionary<string,WindowReference> windowReferences=new Dictionary<string,WindowReference>();
|
|
11
|
+
static WindowReference currentReference;
|
|
12
|
+
static bool windowDispatchAttempted;
|
|
13
|
+
static void BeginWindowDispatch(){CheckReferenceBeforeDispatch();windowDispatchAttempted=true;}
|
|
14
|
+
static void FinishWindowReference(Dictionary<string,object> payload){
|
|
15
|
+
if(currentReference==null)return;
|
|
16
|
+
bool cleanupFailed=payload.ContainsKey("cleanupConfirmed") && !Convert.ToBoolean(payload["cleanupConfirmed"]);
|
|
17
|
+
if(!cleanupFailed && Convert.ToString(payload["action"])=="close_window" && windowDispatchAttempted && payload.ContainsKey("posted") && Convert.ToBoolean(payload["posted"]) && !IsWindow(currentReference.Window)){
|
|
18
|
+
// WM_CLOSE was posted; observing its intended disappearance is not a failed action.
|
|
19
|
+
payload["targetState"]="absent";payload["windowRef"]=currentReference.Id;return;
|
|
20
|
+
}
|
|
21
|
+
if(!CurrentReferenceIntact()){
|
|
22
|
+
payload["ok"]=false;payload.Remove("inputRequired");payload["dispatchAttempted"]=windowDispatchAttempted;
|
|
23
|
+
string warning=windowDispatchAttempted?"WINDOW_REFERENCE_LOST_AFTER_DISPATCH: app effects may have occurred; observe before further action; never replay":"WINDOW_REFERENCE_LOST: lifetime proof is unavailable; observe again";
|
|
24
|
+
if(payload.ContainsKey("cleanupConfirmed") && !Convert.ToBoolean(payload["cleanupConfirmed"]))payload["identityWarning"]=warning;else payload["error"]=warning;
|
|
25
|
+
if(windowDispatchAttempted){payload["effectsUncertain"]=true;if(!payload.ContainsKey("verification"))payload["verification"]="unconfirmed";}
|
|
26
|
+
}
|
|
27
|
+
payload["windowRef"]=currentReference.Id;
|
|
28
|
+
}
|
|
29
|
+
static bool CurrentReferenceIntact() {return currentReference==null || WindowMarker.Matches(currentReference.Window,currentReference.Owner,currentReference.Marker);}
|
|
30
|
+
static void CheckReferenceBeforeDispatch() {if(currentReference!=null)CheckWindowReference(currentReference.Window);}
|
|
31
|
+
static void CheckWindowReference(IntPtr window) {
|
|
32
|
+
if(currentReference!=null && currentReference.Window==window && !WindowMarker.Matches(window,currentReference.Owner,currentReference.Marker))throw new InvalidOperationException("WINDOW_REFERENCE_LOST: lifetime proof is unavailable; observe again; never replay uncertain effects");
|
|
33
|
+
}
|
|
34
|
+
static WindowReference ResolveWindowReference(Dictionary<string,object> args) {
|
|
35
|
+
if(!args.ContainsKey("windowRef"))return null;
|
|
36
|
+
string id=Convert.ToString(args["windowRef"]);WindowReference entry;
|
|
37
|
+
if(!windowReferences.TryGetValue(id,out entry))throw new InvalidOperationException("STALE_WINDOW_REFERENCE: unknown or expired reference; observe again");
|
|
38
|
+
if(entry.Seen<DateTime.UtcNow.AddMinutes(-5)){WindowMarker.Remove(entry.Window,entry.Owner,entry.Marker);windowReferences.Remove(id);throw new InvalidOperationException("STALE_WINDOW_REFERENCE: reference expired; observe again");}
|
|
39
|
+
if(!WindowMarker.Matches(entry.Window,entry.Owner,entry.Marker))throw new InvalidOperationException("WINDOW_REFERENCE_LOST: lifetime proof is unavailable; observe again");
|
|
40
|
+
if((args.ContainsKey("nativeWindowHandle") && Convert.ToInt64(args["nativeWindowHandle"])!=entry.Window.ToInt64()) || (args.ContainsKey("processId") && Convert.ToUInt32(args["processId"])!=entry.Owner) || (args.ContainsKey("windowTitle") && WindowTitle(entry.Window).IndexOf(Convert.ToString(args["windowTitle"]),StringComparison.OrdinalIgnoreCase)<0))throw new InvalidOperationException("WINDOW_REFERENCE_CONFLICT: selectors disagree with the observed lifetime");
|
|
41
|
+
if(TextOption(args,"scope","")=="desktop" || args.ContainsKey("browserTab"))throw new InvalidOperationException("WINDOW_REFERENCE_CONFLICT: window reference cannot target desktop or browser channel");
|
|
42
|
+
entry.Seen=DateTime.UtcNow;currentReference=entry;return entry;
|
|
43
|
+
}
|
|
44
|
+
static object ObserveReferencePresence(Dictionary<string,object> args) {
|
|
45
|
+
WindowReference entry;string id=Convert.ToString(args["windowRef"]);
|
|
46
|
+
if(windowReferences.TryGetValue(id,out entry) && ((args.ContainsKey("nativeWindowHandle") && Convert.ToInt64(args["nativeWindowHandle"])!=entry.Window.ToInt64()) || (args.ContainsKey("processId") && Convert.ToUInt32(args["processId"])!=entry.Owner)))throw new InvalidOperationException("WINDOW_REFERENCE_CONFLICT: selectors disagree with the observed lifetime");
|
|
47
|
+
if(windowReferences.TryGetValue(id,out entry) && entry.Seen>=DateTime.UtcNow.AddMinutes(-5) && !IsWindow(entry.Window)){
|
|
48
|
+
if(args.ContainsKey("windowTitle"))throw new InvalidOperationException("WINDOW_REFERENCE_CONSTRAINT_UNAVAILABLE: destroyed window title cannot be verified");
|
|
49
|
+
return new Dictionary<string,object>{{"ok",true},{"action","list_windows"},{"referencePresent",false},{"windows",new List<object>()}};
|
|
50
|
+
}
|
|
51
|
+
entry=ResolveWindowReference(args);
|
|
52
|
+
return new Dictionary<string,object>{{"ok",true},{"action","list_windows"},{"referencePresent",true},{"windows",new List<object>()}};
|
|
53
|
+
}
|
|
54
|
+
static void PruneWindowReferences() {
|
|
55
|
+
var remove=new List<string>();
|
|
56
|
+
foreach(var pair in windowReferences)if(pair.Value.Seen<DateTime.UtcNow.AddMinutes(-5) || !WindowMarker.Matches(pair.Value.Window,pair.Value.Owner,pair.Value.Marker))remove.Add(pair.Key);
|
|
57
|
+
foreach(string id in remove){WindowReference entry=windowReferences[id];WindowMarker.Remove(entry.Window,entry.Owner,entry.Marker);windowReferences.Remove(id);}
|
|
58
|
+
}
|
|
59
|
+
static WindowReference ObserveWindowReference(IntPtr window,Dictionary<string,object> metadata) {
|
|
60
|
+
if(window==IntPtr.Zero){metadata["windowRefUnavailable"]="WINDOW_REFERENCE_UNAVAILABLE: no native root could be established";return null;}
|
|
61
|
+
CheckWindowReference(window);uint owner=WindowOwner(window);
|
|
62
|
+
foreach(var pair in windowReferences)if(pair.Value.Window==window && pair.Value.Owner==owner && pair.Value.Seen>=DateTime.UtcNow.AddMinutes(-5) && WindowMarker.Matches(window,owner,pair.Value.Marker)){pair.Value.Seen=DateTime.UtcNow;return pair.Value;}
|
|
63
|
+
PruneWindowReferences();
|
|
64
|
+
if(windowReferences.Count>=4096){metadata["windowRefUnavailable"]="REFERENCE_CAPACITY: observe again after expiry";return null;}
|
|
65
|
+
string serial=(++referenceSequence).ToString("x"),marker="PiComputerUse.Window."+referenceEpoch+"."+serial;int error;
|
|
66
|
+
if(!WindowMarker.Bind(window,owner,marker,out error)){metadata["windowRefUnavailable"]="WINDOW_REFERENCE_UNAVAILABLE: marker acquisition failed (native error "+error+")";return null;}
|
|
67
|
+
var entry=new WindowReference {Id="w:"+referenceEpoch+":"+serial,Marker=marker,Window=window,Owner=owner,Seen=DateTime.UtcNow};windowReferences.Add(entry.Id,entry);return entry;
|
|
68
|
+
}
|
|
69
|
+
static void AddWindowReference(Dictionary<string,object> metadata,IntPtr window) {
|
|
70
|
+
WindowReference entry=ObserveWindowReference(window,metadata);if(entry!=null)metadata["windowRef"]=entry.Id;
|
|
71
|
+
}
|
|
72
|
+
static Dictionary<string,object> WindowReferenceDescriptor(IntPtr window) {
|
|
73
|
+
WindowReference entry=currentReference!=null && currentReference.Window==window?currentReference:ObserveWindowReference(window,new Dictionary<string,object>());
|
|
74
|
+
return entry==null?null:new Dictionary<string,object>{{"name",entry.Marker},{"windowRef",entry.Id}};
|
|
75
|
+
}
|
|
76
|
+
static void CleanupWindowReferences() {
|
|
77
|
+
foreach(var pair in windowReferences)WindowMarker.Remove(pair.Value.Window,pair.Value.Owner,pair.Value.Marker);
|
|
78
|
+
windowReferences.Clear();observed.Clear();elementKeys.Clear();
|
|
79
|
+
}
|
|
80
|
+
}
|