@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.
Files changed (95) hide show
  1. package/GLOSSARY.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/assets/accessibility.cs +208 -0
  5. package/assets/browser-indicators.cs +37 -0
  6. package/assets/capture.cs +215 -0
  7. package/assets/clipboard.cs +197 -0
  8. package/assets/controls.cs +249 -0
  9. package/assets/indicator-preview.png +0 -0
  10. package/assets/indicators.cs +252 -0
  11. package/assets/input-timing.cs +42 -0
  12. package/assets/node-LICENSE.txt +2732 -0
  13. package/assets/ocr.cs +86 -0
  14. package/assets/ownership.cs +59 -0
  15. package/assets/pointer.cs +241 -0
  16. package/assets/puppeteer-LICENSE.txt +202 -0
  17. package/assets/readme-banner.svg +33 -0
  18. package/assets/stop.cs +197 -0
  19. package/assets/text.cs +279 -0
  20. package/assets/wgc.cs +167 -0
  21. package/assets/window-management.cs +217 -0
  22. package/assets/window-marker.cs +23 -0
  23. package/assets/window-references.cs +80 -0
  24. package/assets/windows.cs +127 -0
  25. package/dist/accessibility.d.ts +29 -0
  26. package/dist/accessibility.js +258 -0
  27. package/dist/browser-actions.d.ts +22 -0
  28. package/dist/browser-actions.js +246 -0
  29. package/dist/browser-indicators.d.ts +23 -0
  30. package/dist/browser-indicators.js +69 -0
  31. package/dist/browser-profiles.d.ts +10 -0
  32. package/dist/browser-profiles.js +174 -0
  33. package/dist/browser.d.ts +11 -0
  34. package/dist/browser.js +2071 -0
  35. package/dist/capture.d.ts +74 -0
  36. package/dist/capture.js +199 -0
  37. package/dist/client.d.ts +39 -0
  38. package/dist/client.js +384 -0
  39. package/dist/desktop.d.ts +20 -0
  40. package/dist/desktop.js +42 -0
  41. package/dist/extension.d.ts +4 -0
  42. package/dist/extension.js +409 -0
  43. package/dist/input-timing.d.ts +24 -0
  44. package/dist/input-timing.js +70 -0
  45. package/dist/launcher.d.ts +6 -0
  46. package/dist/launcher.js +85 -0
  47. package/dist/native.d.ts +11 -0
  48. package/dist/native.js +146 -0
  49. package/dist/ocr.d.ts +52 -0
  50. package/dist/ocr.js +246 -0
  51. package/dist/ownership.d.ts +4 -0
  52. package/dist/ownership.js +24 -0
  53. package/dist/pointer.d.ts +128 -0
  54. package/dist/pointer.js +198 -0
  55. package/dist/protocol.d.ts +15 -0
  56. package/dist/protocol.js +1 -0
  57. package/dist/runtime.d.ts +8 -0
  58. package/dist/runtime.js +72 -0
  59. package/dist/settings.d.ts +85 -0
  60. package/dist/settings.js +449 -0
  61. package/dist/stop.d.ts +24 -0
  62. package/dist/stop.js +156 -0
  63. package/dist/text.d.ts +120 -0
  64. package/dist/text.js +212 -0
  65. package/dist/waits.d.ts +19 -0
  66. package/dist/waits.js +262 -0
  67. package/dist/window-management.d.ts +25 -0
  68. package/dist/window-management.js +167 -0
  69. package/dist/worker.d.ts +1 -0
  70. package/dist/worker.js +553 -0
  71. package/docs/README.md +55 -0
  72. package/docs/adr/0001-preserve-reference-behavior.md +9 -0
  73. package/docs/adr/0002-window-lifetime-references.md +11 -0
  74. package/docs/adr/0003-exclusive-desktop-control.md +11 -0
  75. package/docs/adr/0004-pi-first-acceptance.md +9 -0
  76. package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
  77. package/docs/agents/domain.md +9 -0
  78. package/docs/agents/issue-tracker.md +10 -0
  79. package/docs/agents/triage-labels.md +9 -0
  80. package/docs/browser.md +113 -0
  81. package/docs/implementation-plan.md +153 -0
  82. package/docs/indicators.md +53 -0
  83. package/docs/native-interop-research.md +17 -0
  84. package/docs/ownership.md +25 -0
  85. package/docs/pi-extension-research.md +57 -0
  86. package/docs/pointer.md +77 -0
  87. package/docs/runtime.md +42 -0
  88. package/docs/settings.md +33 -0
  89. package/docs/spec.md +230 -0
  90. package/docs/stopping.md +83 -0
  91. package/docs/text.md +99 -0
  92. package/docs/tickets.md +85 -0
  93. package/docs/validation.md +1420 -0
  94. package/docs/windows.md +171 -0
  95. package/package.json +134 -4
@@ -0,0 +1,215 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Drawing;
4
+ using System.Drawing.Drawing2D;
5
+ using System.Drawing.Imaging;
6
+ using System.IO;
7
+ using System.Runtime.InteropServices;
8
+ using System.Windows.Forms;
9
+
10
+ partial class WindowsBridge {
11
+ [DllImport("user32.dll")] static extern bool PrintWindow(IntPtr window, IntPtr dc, uint flags);
12
+ [DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr window, int attribute, out Rect bounds, int bytes);
13
+
14
+ static IntPtr ResolveCaptureWindow(Dictionary<string, object> args) {
15
+ WindowReference bound=ResolveWindowReference(args);
16
+ object value;
17
+ long requested = bound!=null?bound.Window.ToInt64():args.TryGetValue("nativeWindowHandle", out value) ? Convert.ToInt64(value) : 0;
18
+ uint process = args.TryGetValue("processId", out value) ? Convert.ToUInt32(value) : 0;
19
+ string title = args.TryGetValue("windowTitle", out value) ? Convert.ToString(value) : null;
20
+ if (requested != 0) {
21
+ IntPtr handle = new IntPtr(requested);
22
+ uint actual;
23
+ GetWindowThreadProcessId(handle, out actual);
24
+ if (!IsWindow(handle) || actual == 0 || (process != 0 && actual != process)) throw new InvalidOperationException("WINDOW_IDENTITY_CHANGED: refresh window observations");
25
+ if (title != null) {
26
+ System.Text.StringBuilder text = new System.Text.StringBuilder(2048);
27
+ GetWindowText(handle, text, text.Capacity);
28
+ if (text.ToString().IndexOf(title, StringComparison.OrdinalIgnoreCase) < 0) throw new InvalidOperationException("WINDOW_TARGET_MISMATCH");
29
+ }
30
+ return handle;
31
+ }
32
+ if (process == 0 && title == null) {
33
+ IntPtr foreground = GetForegroundWindow();
34
+ if (foreground == IntPtr.Zero) throw new InvalidOperationException("WINDOW_NOT_FOUND: no foreground window");
35
+ return foreground;
36
+ }
37
+ List<IntPtr> matches = new List<IntPtr>();
38
+ EnumWindows(delegate(IntPtr handle, IntPtr unused) {
39
+ uint actual;
40
+ GetWindowThreadProcessId(handle, out actual);
41
+ if (!IsWindowVisible(handle) || (process != 0 && actual != process)) return true;
42
+ System.Text.StringBuilder text = new System.Text.StringBuilder(2048);
43
+ GetWindowText(handle, text, text.Capacity);
44
+ if (title == null || text.ToString().IndexOf(title, StringComparison.OrdinalIgnoreCase) >= 0) matches.Add(handle);
45
+ return true;
46
+ }, IntPtr.Zero);
47
+ if (matches.Count == 0) throw new InvalidOperationException("WINDOW_NOT_FOUND");
48
+ if (matches.Count > 1) throw new InvalidOperationException("WINDOW_TARGET_AMBIGUOUS: use nativeWindowHandle and processId");
49
+ return matches[0];
50
+ }
51
+
52
+ static Rectangle WindowCaptureBounds(IntPtr window) {
53
+ Rect bounds;
54
+ if (DwmGetWindowAttribute(window, 9, out bounds, Marshal.SizeOf(typeof(Rect))) != 0 || bounds.Right <= bounds.Left || bounds.Bottom <= bounds.Top) {
55
+ if (!GetWindowRect(window, out bounds)) throw new InvalidOperationException("WINDOW_BOUNDS_UNAVAILABLE");
56
+ }
57
+ return Rectangle.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
58
+ }
59
+
60
+ static bool ClearCaptureRegion(IntPtr window, Rectangle bounds, Rectangle desktop) {
61
+ if (GetForegroundWindow() != window || !IsWindowVisible(window) || IsIconic(window) || !desktop.Contains(bounds)) return false;
62
+ bool found = false, clear = true;
63
+ EnumWindows(delegate(IntPtr other, IntPtr unused) {
64
+ if (other == window) { found = true; return false; }
65
+ if (!IsWindowVisible(other) || IsIconic(other)) return true;
66
+ Rect box;
67
+ if (!GetWindowRect(other, out box)) { clear = false; return false; }
68
+ if (Rectangle.FromLTRB(box.Left, box.Top, box.Right, box.Bottom).IntersectsWith(bounds)) { clear = false; return false; }
69
+ return true;
70
+ }, IntPtr.Zero);
71
+ return found && clear;
72
+ }
73
+
74
+ static Bitmap CopyCaptureRegion(Rectangle bounds) {
75
+ if (bounds.Width <= 0 || bounds.Height <= 0 || (long)bounds.Width * bounds.Height > 40000000) throw new InvalidOperationException("CAPTURE_BOUNDS_UNAVAILABLE: region is empty or exceeds 40 million pixels");
76
+ Bitmap image = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
77
+ try {
78
+ using (Graphics graphics = Graphics.FromImage(image)) graphics.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
79
+ return image;
80
+ } catch { image.Dispose(); throw; }
81
+ }
82
+
83
+ static bool CaptureIsBlank(Bitmap image) {
84
+ for (int y = 0; y < image.Height; y += Math.Max(1, image.Height / 32)) {
85
+ for (int x = 0; x < image.Width; x += Math.Max(1, image.Width / 32)) {
86
+ Color color = image.GetPixel(x, y);
87
+ if (color.R > 10 || color.G > 10 || color.B > 10) return false;
88
+ }
89
+ }
90
+ return true;
91
+ }
92
+
93
+ static Bitmap PrintCapture(IntPtr window, Rectangle wanted) {
94
+ Rect windowBox;
95
+ if (!GetWindowRect(window, out windowBox)) return null;
96
+ Rectangle full = Rectangle.FromLTRB(windowBox.Left, windowBox.Top, windowBox.Right, windowBox.Bottom);
97
+ if (full.Width <= 0 || full.Height <= 0 || (long)full.Width * full.Height > 40000000 || !full.Contains(wanted)) return null;
98
+ using (Bitmap entire = new Bitmap(full.Width, full.Height, PixelFormat.Format32bppArgb)) {
99
+ bool rendered;
100
+ using (Graphics graphics = Graphics.FromImage(entire)) {
101
+ IntPtr dc = graphics.GetHdc();
102
+ try { rendered = PrintWindow(window, dc, 2); }
103
+ finally { graphics.ReleaseHdc(dc); }
104
+ }
105
+ if (!rendered) return null;
106
+ Bitmap cropped = entire.Clone(new Rectangle(wanted.X - full.X, wanted.Y - full.Y, wanted.Width, wanted.Height), PixelFormat.Format32bppArgb);
107
+ if (CaptureIsBlank(cropped)) { cropped.Dispose(); return null; }
108
+ return cropped;
109
+ }
110
+ }
111
+
112
+ static object CaptureScreenshot(Dictionary<string, object> args) {
113
+ if(args.ContainsKey("windowRef") && TextOption(args,"scope","active_window")=="desktop")throw new InvalidOperationException("WINDOW_REFERENCE_CONFLICT: desktop capture cannot use a window reference");
114
+ bool desktopScope = Convert.ToString(args["scope"]) == "desktop";
115
+ bool windowOnly = args.ContainsKey("windowOnly") && Convert.ToBoolean(args["windowOnly"]);
116
+ if(windowOnly && desktopScope)throw new InvalidOperationException("INVALID_ARGUMENTS: windowOnly requires window scope");
117
+ int maximum = Convert.ToInt32(args["maxWidth"]);
118
+ Rectangle desktop = SystemInformation.VirtualScreen;
119
+ Rectangle bounds = desktop;
120
+ Bitmap image = null;
121
+ string method = "screen";
122
+ string pixelFailure = null;
123
+ bool occluded = false, failed = false;
124
+ List<string> warnings = new List<string>();
125
+ IntPtr target = IntPtr.Zero;
126
+ uint owner = 0;
127
+ var referenceMetadata=new Dictionary<string,object>();
128
+ try {
129
+ if (!desktopScope) {
130
+ target = ResolveCaptureWindow(args);
131
+ WindowReference binding=ObserveWindowReference(target,referenceMetadata);if(binding!=null){currentReference=binding;referenceMetadata["windowRef"]=binding.Id;}
132
+ GetWindowThreadProcessId(target, out owner);CheckWindowReference(target);
133
+ object requestedActivation;
134
+ if (args.TryGetValue("activate", out requestedActivation) && Convert.ToBoolean(requestedActivation)){BeginWindowDispatch();if(!SetForegroundWindow(target))warnings.Add("WINDOW_ACTIVATION_REFUSED: Windows did not bring the target to foreground");}
135
+ bounds = WindowCaptureBounds(target);
136
+ if (!IsIconic(target) && IsWindowVisible(target) && bounds.Width > 0 && bounds.Height > 0) {
137
+ if (!windowOnly && ClearCaptureRegion(target, bounds, desktop)) {
138
+ Bitmap candidate = null;
139
+ try {
140
+ candidate = CopyCaptureRegion(bounds);
141
+ if (WindowCaptureBounds(target) == bounds && ClearCaptureRegion(target, bounds, desktop)) { image = candidate; candidate = null; method = "screen-region"; }
142
+ } catch (Exception error) { warnings.Add("SCREEN_REGION_RETRY: " + error.Message); }
143
+ finally { if (candidate != null) candidate.Dispose(); }
144
+ }
145
+ if (image == null) { image = PrintCapture(target, bounds); method = "printwindow"; }
146
+ if (image == null) {
147
+ warnings.Add("PRINTWINDOW_FAILED_OR_BLANK");
148
+ Rectangle visible = Rectangle.Intersect(bounds, desktop);
149
+ if (!windowOnly && visible.Width > 0 && visible.Height > 0) {
150
+ Rectangle windowBounds = bounds;
151
+ bounds = visible; method = "screen-region"; occluded = true;
152
+ try { image = CopyCaptureRegion(bounds); }
153
+ catch (System.ComponentModel.Win32Exception error) {
154
+ // Only this failed pixel stage may hand off without a native image.
155
+ if (!IsWindowVisible(target) || IsIconic(target) || WindowCaptureBounds(target) != windowBounds) throw new InvalidOperationException("WINDOW_MOVED_DURING_CAPTURE: observe the target again");
156
+ pixelFailure = "SCREEN_REGION_CAPTURE_FAILED: " + error.Message;
157
+ }
158
+ }
159
+ }
160
+ }
161
+ if (image == null && pixelFailure == null) {
162
+ if(windowOnly)pixelFailure="WINDOW_CAPTURE_UNAVAILABLE: target is hidden, minimized or PrintWindow failed/blank; screen fallback refused";
163
+ else {
164
+ failed = true; bounds = desktop; method = "screen";
165
+ warnings.Add("WINDOW_CAPTURE_FAILED: returning desktop pixels instead of the unavailable target surface");
166
+ }
167
+ }
168
+ }
169
+ if (image == null && pixelFailure == null) image = CopyCaptureRegion(bounds);
170
+ if (target != IntPtr.Zero) {
171
+ CheckWindowReference(target);
172
+ uint ownerAfter;
173
+ GetWindowThreadProcessId(target, out ownerAfter);
174
+ if (!IsWindow(target) || owner != ownerAfter) throw new InvalidOperationException("WINDOW_IDENTITY_CHANGED_DURING_CAPTURE");
175
+ if (!failed && WindowCaptureBounds(target) != bounds && !occluded) throw new InvalidOperationException("WINDOW_MOVED_DURING_CAPTURE: observe the target again");
176
+ }
177
+ if (pixelFailure != null) {
178
+ Dictionary<string, object> unavailable = Result(false, "screenshot");
179
+ unavailable["error"] = windowOnly?"WINDOW_CAPTURE_UNAVAILABLE":"SCREEN_REGION_CAPTURE_FAILED";
180
+ unavailable["nativeCaptureError"] = pixelFailure;
181
+ unavailable["captureWarnings"] = warnings;
182
+ foreach(var pair in referenceMetadata)unavailable[pair.Key]=pair.Value;
183
+ if(!windowOnly || (IsWindowVisible(target) && !IsIconic(target)))unavailable["wgcTarget"] = new Dictionary<string, object> { { "nativeWindowHandle", target.ToInt64() }, { "processId", owner }, { "_windowMarker", WindowReferenceDescriptor(target) } };
184
+ return unavailable;
185
+ }
186
+ if (maximum > 0 && image.Width > maximum) {
187
+ int height = Math.Max(1, (int)Math.Round((double)image.Height * maximum / image.Width));
188
+ Bitmap resized = new Bitmap(maximum, height, PixelFormat.Format32bppArgb);
189
+ using (Graphics graphics = Graphics.FromImage(resized)) {
190
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
191
+ graphics.DrawImage(image, new Rectangle(0, 0, resized.Width, resized.Height));
192
+ }
193
+ image.Dispose(); image = resized;
194
+ }
195
+ byte[] png;
196
+ using (MemoryStream bytes = new MemoryStream()) { image.Save(bytes, ImageFormat.Png); png = bytes.ToArray(); }
197
+ Dictionary<string, object> metadata = new Dictionary<string, object> {
198
+ { "mimeType", "image/png" }, { "base64", Convert.ToBase64String(png) }, { "bytes", png.Length },
199
+ { "width", image.Width }, { "height", image.Height }, { "method", method },
200
+ { "bounds", new Dictionary<string, object> { { "x", bounds.X }, { "y", bounds.Y }, { "width", bounds.Width }, { "height", bounds.Height } } },
201
+ { "origin", new Dictionary<string, object> { { "x", bounds.X }, { "y", bounds.Y } } },
202
+ { "imageScale", (double)image.Width / bounds.Width },
203
+ { "imageScaleY", (double)image.Height / bounds.Height },
204
+ { "occludedPossible", occluded }, { "windowCaptureFailed", failed }, { "captureWarnings", warnings }
205
+ };
206
+ Dictionary<string, object> result = Result(true, "screenshot");
207
+ metadata["source"]=desktopScope?"desktop":"window";
208
+ foreach(var pair in referenceMetadata){result[pair.Key]=pair.Value;metadata[pair.Key]=pair.Value;}
209
+ result["screenshot"] = metadata;
210
+ if(target!=IntPtr.Zero && !failed){RememberPointerOrigin(target);RememberPointerTitle(args,target);}
211
+ if (target != IntPtr.Zero && !failed && occluded) result["wgcTarget"] = new Dictionary<string, object> { { "nativeWindowHandle", target.ToInt64() }, { "processId", owner }, {"_windowMarker",WindowReferenceDescriptor(target)} };
212
+ return result;
213
+ } finally { if (image != null) image.Dispose(); }
214
+ }
215
+ }
@@ -0,0 +1,197 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Diagnostics;
4
+ using System.Runtime.InteropServices;
5
+ using System.Runtime.InteropServices.ComTypes;
6
+ using System.Text;
7
+ using System.Threading;
8
+ using System.Web.Script.Serialization;
9
+
10
+ // Isolated STA process. Snapshot is a separate protocol phase: it can be killed
11
+ // on a delayed-render/provider timeout before any clipboard mutation is allowed.
12
+ class ClipboardBridge {
13
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern IntPtr CreateWindowEx(uint extended,string name,string title,uint style,int x,int y,int width,int height,IntPtr parent,IntPtr menu,IntPtr instance,IntPtr parameter);
14
+ [DllImport("user32.dll")] static extern bool DestroyWindow(IntPtr window);
15
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern int GetClipboardFormatName(uint format,StringBuilder name,int count);
16
+ [StructLayout(LayoutKind.Sequential)] struct Message {public IntPtr Hwnd;public uint Id;public UIntPtr W;public IntPtr L;public uint Time;public int X,Y;public uint Private;}
17
+ [DllImport("user32.dll")] static extern bool PeekMessage(out Message message,IntPtr window,uint first,uint last,uint remove);
18
+ [DllImport("user32.dll")] static extern bool TranslateMessage(ref Message message);
19
+ [DllImport("user32.dll")] static extern IntPtr DispatchMessage(ref Message message);
20
+ static void Pump() {Message message;while(PeekMessage(out message,IntPtr.Zero,0,0,1)){TranslateMessage(ref message);DispatchMessage(ref message);}}
21
+ static IntPtr clipboardOwner;
22
+ [DllImport("user32.dll",SetLastError=true)] static extern bool OpenClipboard(IntPtr owner);
23
+ [DllImport("user32.dll")] static extern bool CloseClipboard();
24
+ [DllImport("user32.dll",SetLastError=true)] static extern bool EmptyClipboard();
25
+ [DllImport("user32.dll",SetLastError=true)] static extern uint EnumClipboardFormats(uint format);
26
+ [DllImport("user32.dll",SetLastError=true)] static extern IntPtr GetClipboardData(uint format);
27
+ [DllImport("user32.dll",SetLastError=true)] static extern IntPtr SetClipboardData(uint format,IntPtr data);
28
+ [DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern uint RegisterClipboardFormat(string name);
29
+ [DllImport("user32.dll")] static extern uint GetClipboardSequenceNumber();
30
+ [DllImport("user32.dll")] static extern IntPtr GetClipboardOwner();
31
+ [DllImport("ole32.dll")] static extern int OleInitialize(IntPtr reserved);
32
+ [DllImport("ole32.dll")] static extern void OleUninitialize();
33
+ [DllImport("ole32.dll")] static extern int OleGetClipboard(out IDataObject data);
34
+ [DllImport("ole32.dll")] static extern IntPtr OleDuplicateData(IntPtr data,ushort format,uint flags);
35
+ [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr GlobalAlloc(uint flags,UIntPtr size);
36
+ [DllImport("kernel32.dll")] static extern IntPtr GlobalLock(IntPtr data);
37
+ [DllImport("kernel32.dll")] static extern UIntPtr GlobalSize(IntPtr data);
38
+ [DllImport("kernel32.dll")] static extern bool GlobalUnlock(IntPtr data);
39
+ [DllImport("kernel32.dll")] static extern IntPtr GlobalFree(IntPtr data);
40
+ [DllImport("kernel32.dll")] static extern void SetLastError(uint code);
41
+ [DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr data);
42
+ [DllImport("gdi32.dll",CharSet=CharSet.Unicode)] static extern IntPtr CopyEnhMetaFile(IntPtr data,string filename);
43
+ [DllImport("gdi32.dll")] static extern bool DeleteEnhMetaFile(IntPtr data);
44
+ [DllImport("gdi32.dll")] static extern bool DeleteMetaFile(IntPtr data);
45
+ [StructLayout(LayoutKind.Sequential)] struct MetaPicture {public int Mode,X,Y;public IntPtr Meta;}
46
+ sealed class Item {public uint Format;public IntPtr Data;}
47
+ static readonly List<Item> snapshot=new List<Item>();
48
+ static readonly object phaseGate=new object();static bool mayMutate;
49
+ static uint transactionFormat;static readonly byte[] transactionToken=Guid.NewGuid().ToByteArray();
50
+ static uint sequence;static bool changed;static bool replaced;static bool restoring;static int oleMetadataExcluded,oleFormatsValidated;static string restoreOutcome="unchanged";
51
+ static readonly JavaScriptSerializer json=new JavaScriptSerializer {MaxJsonLength=8*1024*1024};
52
+ static void Reply(string phase,bool ok,string error) {Console.WriteLine(json.Serialize(new Dictionary<string,object>{{"phase",phase},{"ok",ok},{"error",error},{"formats",snapshot.Count},{"restored",!changed},{"oleMetadataExcluded",oleMetadataExcluded},{"oleFormatsValidated",oleFormatsValidated},{"restoreOutcome",restoreOutcome}}));Console.Out.Flush();}
53
+ static void Open() {Stopwatch watch=Stopwatch.StartNew();while(!OpenClipboard(clipboardOwner)){if(watch.ElapsedMilliseconds>=500)throw new InvalidOperationException("CLIPBOARD_BUSY: could not acquire clipboard");Thread.Sleep(10);}}
54
+ static void Free(Item item) {
55
+ if(item.Data==IntPtr.Zero)return;
56
+ if(item.Format==2 || item.Format==9 || item.Format==0x82)DeleteObject(item.Data);
57
+ else if(item.Format==14 || item.Format==0x8E)DeleteEnhMetaFile(item.Data);
58
+ else {if(item.Format==3 || item.Format==0x83){IntPtr memory=GlobalLock(item.Data);if(memory!=IntPtr.Zero){MetaPicture picture=(MetaPicture)Marshal.PtrToStructure(memory,typeof(MetaPicture));DeleteMetaFile(picture.Meta);GlobalUnlock(item.Data);}}GlobalFree(item.Data);}
59
+ item.Data=IntPtr.Zero;
60
+ }
61
+ static IntPtr Duplicate(IntPtr data,uint format) {return format==14 || format==0x8E?CopyEnhMetaFile(data,null):OleDuplicateData(data,(ushort)(format==0x82?2:format==0x83?3:format),0);}
62
+ static string FormatName(uint format) {StringBuilder name=new StringBuilder(256);GetClipboardFormatName(format,name,name.Capacity);return name.ToString();}
63
+ static bool OleTransportFormat(uint format) {string name=FormatName(format);return name.Equals("DataObject",StringComparison.OrdinalIgnoreCase) || name.Equals("Ole Private Data",StringComparison.OrdinalIgnoreCase);}
64
+ static HashSet<uint> ValidateOleFormats() {
65
+ IDataObject data=null;IEnumFORMATETC iterator=null;HashSet<uint> formats=new HashSet<uint>();
66
+ try {
67
+ Marshal.ThrowExceptionForHR(OleGetClipboard(out data));
68
+ if(data==null)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: missing OLE object; unchanged");
69
+ iterator=data.EnumFormatEtc(DATADIR.DATADIR_GET);FORMATETC[] item=new FORMATETC[1];int[] fetched=new int[1];
70
+ for(int count=0;;count++) {
71
+ fetched[0]=0;int result=iterator.Next(1,item,fetched);if(result==1 && fetched[0]==0)break;
72
+ if(result!=0 || fetched[0]!=1 || count>=1000)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: incomplete OLE format enumeration; unchanged");
73
+ FORMATETC format=item[0];
74
+ try {
75
+ uint id=unchecked((ushort)format.cfFormat);
76
+ uint medium=id==2 || id==9 || id==0x82?16U:id==3 || id==0x83?32U:id==14 || id==0x8E?64U:1U;
77
+ if(format.lindex!=-1 || format.dwAspect!=DVASPECT.DVASPECT_CONTENT || format.ptd!=IntPtr.Zero || ((uint)format.tymed & medium)==0 || OleTransportFormat(id) || FormatName(id).Equals("FileContents",StringComparison.OrdinalIgnoreCase))
78
+ throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: indexed, device-specific or unsupported OLE data; unchanged");
79
+ formats.Add(id);
80
+ }finally {if(format.ptd!=IntPtr.Zero)Marshal.FreeCoTaskMem(format.ptd);}
81
+ }
82
+ if(formats.Count==0)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: no preservable OLE formats; unchanged");
83
+ return formats;
84
+ }catch(COMException){throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: OLE formats could not be validated; unchanged");}
85
+ finally {if(iterator!=null)Marshal.ReleaseComObject(iterator);if(data!=null)Marshal.ReleaseComObject(data);}
86
+ }
87
+ static void Snapshot() {
88
+ List<uint> formats=new List<uint>();HashSet<uint> metadata=new HashSet<uint>();uint before;
89
+ Open();try {
90
+ before=GetClipboardSequenceNumber();uint format=0;SetLastError(0);
91
+ while((format=EnumClipboardFormats(format))!=0) {
92
+ if(formats.Count>=1000 || format>65535)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: too many or invalid formats");
93
+ if(FormatName(format).Equals("FileContents",StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: indexed file contents cannot be preserved; unchanged");
94
+ formats.Add(format);if(OleTransportFormat(format))metadata.Add(format);SetLastError(0);
95
+ }
96
+ if(Marshal.GetLastWin32Error()!=0)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_FAILED: enumeration incomplete");
97
+ }finally {CloseClipboard();}
98
+ // OLE bookkeeping is not transferable payload. Validate the actual advertised
99
+ // formats before omitting it; never copy stale process/interface handles.
100
+ if(metadata.Count>0) {
101
+ HashSet<uint> advertised=ValidateOleFormats();
102
+ foreach(uint id in advertised)if(!formats.Contains(id))throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: OLE format is not available through native clipboard storage; unchanged");
103
+ oleFormatsValidated=advertised.Count;
104
+ }
105
+ Open();try {
106
+ if(GetClipboardSequenceNumber()!=before)throw new InvalidOperationException("CLIPBOARD_CHANGED: clipboard changed while validating snapshot; nothing replaced");
107
+ foreach(uint format in formats) {
108
+ if(metadata.Contains(format)){oleMetadataExcluded++;continue;}
109
+ IntPtr data=GetClipboardData(format);
110
+ if(data==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: format "+format+" could not be materialized; clipboard unchanged");
111
+ IntPtr copy=Duplicate(data,format);
112
+ if(copy==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_SNAPSHOT_UNSUPPORTED: cannot preserve format "+format+"; clipboard unchanged");
113
+ snapshot.Add(new Item {Format=format,Data=copy});
114
+ }
115
+ sequence=GetClipboardSequenceNumber();
116
+ }finally {CloseClipboard();}
117
+ }
118
+ static Item Bytes(uint format,byte[] bytes) {
119
+ IntPtr data=GlobalAlloc(0x42,new UIntPtr((uint)bytes.Length));if(data==IntPtr.Zero)throw new OutOfMemoryException();
120
+ IntPtr memory=GlobalLock(data);if(memory==IntPtr.Zero){GlobalFree(data);throw new InvalidOperationException("CLIPBOARD_ALLOC_FAILED");}
121
+ try {Marshal.Copy(bytes,0,memory,bytes.Length);}finally {GlobalUnlock(data);}return new Item {Format=format,Data=data};
122
+ }
123
+ static void Replace(string text) {
124
+ List<Item> payload=new List<Item>();
125
+ try {
126
+ // Publish exclusion flags first, before text can be seen by monitors.
127
+ foreach(string name in new string[]{"ExcludeClipboardContentFromMonitorProcessing","CanIncludeInClipboardHistory","CanUploadToCloudClipboard"}) {uint format=RegisterClipboardFormat(name);if(format==0)throw new InvalidOperationException("CLIPBOARD_FORMAT_FAILED");payload.Add(Bytes(format,new byte[4]));}
128
+ transactionFormat=RegisterClipboardFormat("PiComputerUseClipboardTransaction");if(transactionFormat==0)throw new InvalidOperationException("CLIPBOARD_FORMAT_FAILED");
129
+ payload.Add(Bytes(transactionFormat,transactionToken));
130
+ payload.Add(Bytes(13,Encoding.Unicode.GetBytes(text+"\0")));
131
+ Open();try {
132
+ if(GetClipboardSequenceNumber()!=sequence)throw new InvalidOperationException("CLIPBOARD_CHANGED: user changed clipboard since snapshot; nothing replaced");
133
+ if(!EmptyClipboard())throw new InvalidOperationException("CLIPBOARD_REPLACE_FAILED");changed=true;replaced=true;restoreOutcome="pending";
134
+ foreach(Item item in payload){if(SetClipboardData(item.Format,item.Data)==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_REPLACE_FAILED: partial replacement must restore");item.Data=IntPtr.Zero;}
135
+ sequence=GetClipboardSequenceNumber();replaced=true;
136
+ }finally {if(changed)sequence=GetClipboardSequenceNumber();CloseClipboard();}
137
+ }finally {foreach(Item item in payload)Free(item);}
138
+ }
139
+ static bool OwnsReplacement() {
140
+ if(GetClipboardOwner()!=clipboardOwner || transactionFormat==0)return false;
141
+ IntPtr data=GetClipboardData(transactionFormat);if(data==IntPtr.Zero || GlobalSize(data).ToUInt64()<(ulong)transactionToken.Length)return false;
142
+ IntPtr memory=GlobalLock(data);if(memory==IntPtr.Zero)return false;
143
+ try {byte[] actual=new byte[transactionToken.Length];Marshal.Copy(memory,actual,0,actual.Length);for(int index=0;index<actual.Length;index++)if(actual[index]!=transactionToken[index])return false;return true;}
144
+ finally {GlobalUnlock(data);}
145
+ }
146
+ static void Restore(bool keep) {
147
+ if(!changed)return;
148
+ Open();try {
149
+ // Synthesized formats can advance the sequence without a new copy. Only
150
+ // our hidden owner AND transaction nonce allow restoration across that change.
151
+ // A failed partial restore has removed the nonce; retain its private owner
152
+ // identity while retrying, but still yield if another window takes ownership.
153
+ if(replaced && GetClipboardSequenceNumber()!=sequence && !OwnsReplacement() && !(restoring && GetClipboardOwner()==clipboardOwner)){changed=false;restoring=false;restoreOutcome="newer-copy-preserved";return;}
154
+ if(keep){changed=false;restoreOutcome="kept";return;}
155
+ List<Item> copies=new List<Item>();bool attempted=false;
156
+ try {
157
+ foreach(Item item in snapshot) {IntPtr copy=Duplicate(item.Data,item.Format);if(copy==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_RESTORE_FAILED: cannot duplicate snapshot");copies.Add(new Item{Format=item.Format,Data=copy});}
158
+ if(!EmptyClipboard())throw new InvalidOperationException("CLIPBOARD_RESTORE_FAILED");attempted=true;restoring=true;
159
+ foreach(Item item in copies){if(SetClipboardData(item.Format,item.Data)==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_RESTORE_FAILED: retained snapshot still needs restoration");item.Data=IntPtr.Zero;}
160
+ changed=false;restoring=false;restoreOutcome="restored";
161
+ }finally {if(attempted)sequence=GetClipboardSequenceNumber();foreach(Item item in copies)Free(item);}
162
+ }finally {CloseClipboard();}
163
+ }
164
+ [STAThread] static void Main() {
165
+ Console.InputEncoding=new UTF8Encoding(false);Console.OutputEncoding=new UTF8Encoding(false);
166
+ clipboardOwner=CreateWindowEx(0,"STATIC","Pi clipboard transaction",0,0,0,0,0,new IntPtr(-3),IntPtr.Zero,IntPtr.Zero,IntPtr.Zero);
167
+ if(clipboardOwner==IntPtr.Zero)throw new InvalidOperationException("CLIPBOARD_OWNER_FAILED");
168
+ EventWaitHandle cancel=null;Process parent=null;int ole=-1;
169
+ try {
170
+ ole=OleInitialize(IntPtr.Zero);Marshal.ThrowExceptionForHR(ole);
171
+ var args=json.Deserialize<Dictionary<string,object>>(Console.ReadLine());
172
+ cancel=EventWaitHandle.OpenExisting(Convert.ToString(args["cancelEvent"]));parent=Process.GetProcessById(Convert.ToInt32(args["parentProcessId"]));IntPtr retained=parent.Handle;
173
+ Stopwatch snapshotDeadline=Stopwatch.StartNew();
174
+ Thread watchdog=new Thread(delegate(){while(true){Thread.Sleep(20);lock(phaseGate){if(mayMutate)return;if(cancel.WaitOne(0) || parent.HasExited || snapshotDeadline.ElapsedMilliseconds>5000){Process.GetCurrentProcess().Kill();return;}}}});watchdog.IsBackground=true;watchdog.Start();
175
+ Snapshot();Reply("snapshot",true,null);
176
+ string command=Console.ReadLine();
177
+ if(command!="replace" || cancel.WaitOne(0) || parent.HasExited){Reply("replaced",false,"COMPUTER_USE_STOPPED");Reply("restored",true,null);return;}
178
+ lock(phaseGate){mayMutate=true;}
179
+ Replace(Convert.ToString(args["text"]));Reply("replaced",true,null);
180
+ string next=null;bool received=false;
181
+ Thread reader=new Thread(delegate(){next=Console.ReadLine();Volatile.Write(ref received,true);});reader.IsBackground=true;reader.Start();
182
+ Stopwatch lifetime=Stopwatch.StartNew();
183
+ while(!Volatile.Read(ref received) && !cancel.WaitOne(20) && !parent.HasExited && lifetime.ElapsedMilliseconds<30000){Pump();}
184
+ bool keep=received && next=="keep" && !cancel.WaitOne(0);
185
+ Restore(keep);Reply("restored",true,null);
186
+ }catch(Exception error){
187
+ Reply(changed?"restore_needed":"failed",false,error.Message);
188
+ }finally {
189
+ // Restoration never calls an app's rendering provider. Retry bounded lock
190
+ // acquisition rather than losing the only retained nontext snapshot.
191
+ while(changed)try {Restore(false);Reply("restored",true,null);}catch(Exception error){Console.Error.WriteLine("CLIPBOARD_RESTORE_FAILED: "+error.Message);Pump();Thread.Sleep(100);}
192
+ foreach(Item item in snapshot)Free(item);
193
+ DestroyWindow(clipboardOwner);
194
+ if(cancel!=null)cancel.Dispose();if(parent!=null)parent.Dispose();if(ole>=0)OleUninitialize();
195
+ }
196
+ }
197
+ }