@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/stop.cs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Threading;
|
|
6
|
+
using System.Web.Script.Serialization;
|
|
7
|
+
using System.Windows.Forms;
|
|
8
|
+
|
|
9
|
+
// The hook and overlays own an STA message pump and never enter UIA providers.
|
|
10
|
+
partial class StopBridge {
|
|
11
|
+
delegate IntPtr KeyboardHook(int code, IntPtr wParam, IntPtr lParam);
|
|
12
|
+
[DllImport("user32.dll", SetLastError=true)] static extern IntPtr SetWindowsHookEx(int hook, KeyboardHook callback, IntPtr module, uint thread);
|
|
13
|
+
[DllImport("user32.dll")] static extern bool UnhookWindowsHookEx(IntPtr hook);
|
|
14
|
+
[DllImport("user32.dll")] static extern IntPtr CallNextHookEx(IntPtr hook, int code, IntPtr wParam, IntPtr lParam);
|
|
15
|
+
[DllImport("user32.dll")] static extern short GetAsyncKeyState(int key);
|
|
16
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode)] static extern IntPtr GetModuleHandle(string module);
|
|
17
|
+
[DllImport("user32.dll")] static extern bool SetProcessDpiAwarenessContext(IntPtr context);
|
|
18
|
+
[DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags,bool inherit,uint access);
|
|
19
|
+
[DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr desktop);
|
|
20
|
+
[DllImport("user32.dll")] static extern IntPtr GetThreadDesktop(uint thread);
|
|
21
|
+
[DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
|
|
22
|
+
[DllImport("user32.dll",EntryPoint="GetUserObjectInformationW",CharSet=CharSet.Unicode)] static extern bool DesktopInfo(IntPtr desktop,int kind,StringBuilder value,uint bytes,out uint needed);
|
|
23
|
+
static readonly object gate = new object();
|
|
24
|
+
static readonly JavaScriptSerializer json = new JavaScriptSerializer();
|
|
25
|
+
static readonly KeyboardHook callback = OnKey;
|
|
26
|
+
static readonly KeyboardHook mouseCallback = OnMouse;
|
|
27
|
+
static IntPtr installed, mouseHook;
|
|
28
|
+
static string operation;
|
|
29
|
+
static EventWaitHandle cancelled;
|
|
30
|
+
static EventWaitHandle exclusive;
|
|
31
|
+
static readonly HashSet<int> physicalDown = new HashSet<int>(), swallowed = new HashSet<int>();
|
|
32
|
+
static readonly HashSet<int> mouseDown = new HashSet<int>(), swallowedButtons = new HashSet<int>();
|
|
33
|
+
static int suppressedKeys, suppressedMouse,suppressedButtons;
|
|
34
|
+
static bool armed, stopped;
|
|
35
|
+
static string shortcut = "";
|
|
36
|
+
static int shortcutKey, shortcutModifiers;
|
|
37
|
+
static Presentation presentation;
|
|
38
|
+
static Control dispatcher;
|
|
39
|
+
|
|
40
|
+
static void Send(object value) { lock(gate) { Console.WriteLine(json.Serialize(value)); Console.Out.Flush(); } }
|
|
41
|
+
static void Stop() {
|
|
42
|
+
if (!armed || stopped) return;
|
|
43
|
+
stopped = true; cancelled.Set(); ReleaseControl();
|
|
44
|
+
string stoppedOperation=operation;
|
|
45
|
+
dispatcher.BeginInvoke((Action)delegate {
|
|
46
|
+
if(operation==stoppedOperation && stopped)presentation.Stop();Send(new Dictionary<string,object> {{"event","stopped"},{"operationId",stoppedOperation}});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static int Modifiers() {
|
|
50
|
+
return (physicalDown.Contains(0x11)||physicalDown.Contains(0xA2)||physicalDown.Contains(0xA3) ? 1 : 0) |
|
|
51
|
+
(physicalDown.Contains(0x12)||physicalDown.Contains(0xA4)||physicalDown.Contains(0xA5) ? 2 : 0) |
|
|
52
|
+
(physicalDown.Contains(0x10)||physicalDown.Contains(0xA0)||physicalDown.Contains(0xA1) ? 4 : 0) |
|
|
53
|
+
(physicalDown.Contains(0x5B)||physicalDown.Contains(0x5C) ? 8 : 0);
|
|
54
|
+
}
|
|
55
|
+
static IntPtr OnKey(int code, IntPtr wParam, IntPtr lParam) {
|
|
56
|
+
if (code >= 0) {
|
|
57
|
+
int flags=Marshal.ReadInt32(lParam,8), key=Marshal.ReadInt32(lParam);
|
|
58
|
+
// Agent-injected Escape dismisses UI; it must not stop its own operation.
|
|
59
|
+
if ((flags & 0x12)==0) {
|
|
60
|
+
bool down=wParam.ToInt64()==0x100 || wParam.ToInt64()==0x104;
|
|
61
|
+
if(down)physicalDown.Add(key);else physicalDown.Remove(key);
|
|
62
|
+
bool consume=(exclusive!=null && exclusive.WaitOne(0)) || swallowed.Contains(key);
|
|
63
|
+
if(down && armed && !stopped && (key==0x1B || (shortcutKey!=0 && key==shortcutKey && Modifiers()==shortcutModifiers))) {consume=true;Stop();}
|
|
64
|
+
if(down && consume)swallowed.Add(key);
|
|
65
|
+
if(!down && swallowed.Remove(key))consume=true;
|
|
66
|
+
if(consume){suppressedKeys++;return new IntPtr(1);}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return CallNextHookEx(installed,code,wParam,lParam);
|
|
70
|
+
}
|
|
71
|
+
static IntPtr OnMouse(int code,IntPtr message,IntPtr data) {
|
|
72
|
+
if(code>=0 && (Marshal.ReadInt32(data,12)&3)==0) {
|
|
73
|
+
int kind=message.ToInt32(),button=0;
|
|
74
|
+
bool down=kind==0x201 || kind==0x204 || kind==0x207 || kind==0x20B;
|
|
75
|
+
bool up=kind==0x202 || kind==0x205 || kind==0x208 || kind==0x20C;
|
|
76
|
+
if(kind==0x201 || kind==0x202)button=1;
|
|
77
|
+
else if(kind==0x204 || kind==0x205)button=2;
|
|
78
|
+
else if(kind==0x207 || kind==0x208)button=4;
|
|
79
|
+
else if(down || up)button=(Marshal.ReadInt32(data,8)>>16)==1?5:6;
|
|
80
|
+
if(down)mouseDown.Add(button);if(up)mouseDown.Remove(button);
|
|
81
|
+
bool consume=(exclusive!=null && exclusive.WaitOne(0)) || (button!=0 && swallowedButtons.Contains(button));
|
|
82
|
+
if(down && consume)swallowedButtons.Add(button);if(up)swallowedButtons.Remove(button);
|
|
83
|
+
if(consume){suppressedMouse++;if(down)suppressedButtons++;return new IntPtr(1);}
|
|
84
|
+
}
|
|
85
|
+
return CallNextHookEx(mouseHook,code,message,data);
|
|
86
|
+
}
|
|
87
|
+
static void ReleaseControl() {
|
|
88
|
+
if(exclusive!=null)exclusive.Reset();
|
|
89
|
+
}
|
|
90
|
+
static void TakeControl() {
|
|
91
|
+
if(!armed || stopped || exclusive==null)throw new InvalidOperationException("STOP_OPERATION_NOT_ARMED");
|
|
92
|
+
if(exclusive.WaitOne(0))return;
|
|
93
|
+
if(!InteractiveDesktop())throw new InvalidOperationException("DESKTOP_SESSION_UNAVAILABLE: unlock the interactive desktop");
|
|
94
|
+
if(physicalDown.Count>0 || mouseDown.Count>0)throw new InvalidOperationException("USER_INPUT_ACTIVE: release physical input before control begins");
|
|
95
|
+
for(int key=1;key<255;key++)if(GetAsyncKeyState(key)<0)throw new InvalidOperationException("USER_INPUT_ACTIVE: release held input before control begins");
|
|
96
|
+
if(mouseHook==IntPtr.Zero)mouseHook=SetWindowsHookEx(14,mouseCallback,GetModuleHandle(null),0);
|
|
97
|
+
if(mouseHook==IntPtr.Zero)throw new InvalidOperationException("EXCLUSIVE_CONTROL_UNAVAILABLE: mouse hook failed");
|
|
98
|
+
try {
|
|
99
|
+
presentation.Begin();if(stopped || cancelled.WaitOne(0))throw new InvalidOperationException("COMPUTER_USE_STOPPED");exclusive.Set();
|
|
100
|
+
} catch {ReleaseControl();presentation.Stop();throw;}
|
|
101
|
+
}
|
|
102
|
+
static bool InteractiveDesktop() {
|
|
103
|
+
IntPtr input=OpenInputDesktop(0,false,1);if(input==IntPtr.Zero)return false;
|
|
104
|
+
try {
|
|
105
|
+
StringBuilder current=new StringBuilder(256),own=new StringBuilder(256);uint needed;
|
|
106
|
+
return DesktopInfo(input,2,current,512,out needed) && DesktopInfo(GetThreadDesktop(GetCurrentThreadId()),2,own,512,out needed) && current.ToString()==own.ToString();
|
|
107
|
+
}finally {CloseDesktop(input);}
|
|
108
|
+
}
|
|
109
|
+
static void ConfigureShortcut(string value) {
|
|
110
|
+
shortcut=value; shortcutKey=0; shortcutModifiers=0;
|
|
111
|
+
if (String.IsNullOrEmpty(value)) return;
|
|
112
|
+
string[] parts=value.Split('+');
|
|
113
|
+
for(int i=0;i<parts.Length-1;i++) {
|
|
114
|
+
if(parts[i]=="Ctrl")shortcutModifiers|=1;else if(parts[i]=="Alt")shortcutModifiers|=2;
|
|
115
|
+
else if(parts[i]=="Shift")shortcutModifiers|=4;else if(parts[i]=="Win")shortcutModifiers|=8;
|
|
116
|
+
else throw new InvalidOperationException("INVALID_STOP_SHORTCUT");
|
|
117
|
+
}
|
|
118
|
+
string key=parts[parts.Length-1];
|
|
119
|
+
if(key.Length==1 && ((key[0]>='A' && key[0]<='Z') || (key[0]>='0' && key[0]<='9')))shortcutKey=key[0];
|
|
120
|
+
else if(key.StartsWith("F",StringComparison.Ordinal) && key.Length<=3) {
|
|
121
|
+
int number;if(Int32.TryParse(key.Substring(1),out number) && number>=1 && number<=24)shortcutKey=0x6F+number;
|
|
122
|
+
} else {
|
|
123
|
+
Dictionary<string,int> keys=new Dictionary<string,int> {{"Space",32},{"Enter",13},{"Tab",9},{"Backspace",8},{"Delete",46},{"Insert",45},{"Home",36},{"End",35},{"PageUp",33},{"PageDown",34},{"Up",38},{"Down",40},{"Left",37},{"Right",39},{"Pause",19},{"CapsLock",20},{"NumLock",144},{"ScrollLock",145}};
|
|
124
|
+
keys.TryGetValue(key,out shortcutKey);
|
|
125
|
+
}
|
|
126
|
+
if(shortcutKey==0)throw new InvalidOperationException("INVALID_STOP_SHORTCUT");
|
|
127
|
+
}
|
|
128
|
+
static void Configure(Dictionary<string,object> values) {
|
|
129
|
+
object value;
|
|
130
|
+
ConfigureShortcut(values.TryGetValue("stopShortcut",out value) ? Convert.ToString(value) : "");
|
|
131
|
+
presentation.Configure(values);
|
|
132
|
+
}
|
|
133
|
+
static Dictionary<string,object> Status(string action) {
|
|
134
|
+
return new Dictionary<string,object> {
|
|
135
|
+
{"ok",true},{"action",action},{"listenerProcessId",System.Diagnostics.Process.GetCurrentProcess().Id},
|
|
136
|
+
{"hookInstalled",installed!=IntPtr.Zero},{"armed",armed},{"stopped",stopped},{"operationId",operation},
|
|
137
|
+
{"exclusiveControl",exclusive!=null && exclusive.WaitOne(0)},{"suppressedKeys",suppressedKeys},{"suppressedMouse",suppressedMouse},
|
|
138
|
+
{"suppressedButtons",suppressedButtons},
|
|
139
|
+
{"physicalKeysHeld",physicalDown.Count},{"physicalButtonsHeld",mouseDown.Count},
|
|
140
|
+
{"cancelEventSet",cancelled!=null && cancelled.WaitOne(0)}, {"stopShortcut",shortcut},
|
|
141
|
+
{"shortcutVirtualKey",shortcutKey},{"shortcutModifiers",shortcutModifiers},{"escAlwaysEnabled",true},
|
|
142
|
+
{"injectedKeysIgnored",true},{"presentation",presentation.Status()}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
static void Handle(Dictionary<string,object> request) {
|
|
146
|
+
string action=request.ContainsKey("action") ? Convert.ToString(request["action"]) : "invalid_request";
|
|
147
|
+
try {
|
|
148
|
+
Dictionary<string,object> args=(Dictionary<string,object>)request["args"];
|
|
149
|
+
string wanted=args.ContainsKey("operationId") ? Convert.ToString(args["operationId"]) : "";
|
|
150
|
+
if(action=="configure")Configure((Dictionary<string,object>)args["settings"]);
|
|
151
|
+
else if(action=="arm") {
|
|
152
|
+
if(String.IsNullOrWhiteSpace(wanted))throw new InvalidOperationException("INVALID_OPERATION");
|
|
153
|
+
ReleaseControl();if(exclusive!=null)exclusive.Dispose();if(cancelled!=null)cancelled.Dispose();
|
|
154
|
+
presentation.Stop(); operation=wanted; armed=true; stopped=false;
|
|
155
|
+
cancelled=new EventWaitHandle(false,EventResetMode.ManualReset,Convert.ToString(args["cancelEvent"]));
|
|
156
|
+
exclusive=new EventWaitHandle(false,EventResetMode.ManualReset,Convert.ToString(args["cancelEvent"])+"-exclusive");
|
|
157
|
+
suppressedKeys=0;suppressedMouse=0;suppressedButtons=0;
|
|
158
|
+
} else if(action=="stop") { if(wanted==operation)Stop(); }
|
|
159
|
+
else if(action=="take_control") {if(wanted!=operation)throw new InvalidOperationException("STOP_OPERATION_NOT_ARMED");TakeControl();}
|
|
160
|
+
else if(action=="disarm") { if(wanted==operation) { ReleaseControl();armed=false; presentation.Stop(); } }
|
|
161
|
+
else if(action=="capture") presentation.Suspend(Convert.ToBoolean(args["suspended"]));
|
|
162
|
+
else if(action=="preview") presentation.Preview(args.ContainsKey("show") ? Convert.ToBoolean(args["show"]) : true);
|
|
163
|
+
else if(action!="status")throw new InvalidOperationException("UNKNOWN_ACTION");
|
|
164
|
+
Send(new Dictionary<string,object> {{"id",request["id"]},{"result",Status(action)}});
|
|
165
|
+
} catch(Exception error) {
|
|
166
|
+
Send(new Dictionary<string,object> {{"id",request["id"]},{"result",new Dictionary<string,object> {{"ok",false},{"action",action},{"error",error.Message}}}});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
[STAThread]
|
|
170
|
+
static void Main() {
|
|
171
|
+
Console.InputEncoding=new UTF8Encoding(false);Console.OutputEncoding=new UTF8Encoding(false);
|
|
172
|
+
try { SetProcessDpiAwarenessContext(new IntPtr(-4)); } catch(EntryPointNotFoundException) { }
|
|
173
|
+
Application.EnableVisualStyles();
|
|
174
|
+
dispatcher=new Control();IntPtr dispatcherHandle=dispatcher.Handle;
|
|
175
|
+
presentation=new Presentation();
|
|
176
|
+
var health=new System.Windows.Forms.Timer {Interval=100};
|
|
177
|
+
health.Tick+=delegate {if(exclusive!=null && exclusive.WaitOne(0) && (!InteractiveDesktop() || !presentation.BorderReady))Stop();};health.Start();
|
|
178
|
+
installed=SetWindowsHookEx(13,callback,GetModuleHandle(null),0);
|
|
179
|
+
if(installed==IntPtr.Zero)throw new InvalidOperationException("STOP_LISTENER_UNAVAILABLE: keyboard hook failed: "+Marshal.GetLastWin32Error());
|
|
180
|
+
Thread reader=new Thread(delegate() {
|
|
181
|
+
JavaScriptSerializer readerJson=new JavaScriptSerializer();
|
|
182
|
+
string line;
|
|
183
|
+
while((line=Console.ReadLine())!=null) {
|
|
184
|
+
try {
|
|
185
|
+
Dictionary<string,object> request=readerJson.Deserialize<Dictionary<string,object>>(line);
|
|
186
|
+
if(request.ContainsKey("cancel"))continue;
|
|
187
|
+
Dictionary<string,object> captured=request;
|
|
188
|
+
dispatcher.BeginInvoke((Action)delegate { Handle(captured); });
|
|
189
|
+
} catch(Exception error) { Console.Error.WriteLine("STOP_PROTOCOL_ERROR: "+error.Message); }
|
|
190
|
+
}
|
|
191
|
+
dispatcher.BeginInvoke((Action)delegate { Application.ExitThread(); });
|
|
192
|
+
});
|
|
193
|
+
reader.IsBackground=true;reader.Start();
|
|
194
|
+
try { Application.Run(); }
|
|
195
|
+
finally { health.Dispose();if(cancelled!=null)cancelled.Set();ReleaseControl();if(mouseHook!=IntPtr.Zero)UnhookWindowsHookEx(mouseHook);UnhookWindowsHookEx(installed);presentation.Dispose();if(exclusive!=null)exclusive.Dispose();if(cancelled!=null)cancelled.Dispose();dispatcher.Dispose(); }
|
|
196
|
+
}
|
|
197
|
+
}
|
package/assets/text.cs
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections;
|
|
3
|
+
using System.Collections.Generic;
|
|
4
|
+
using System.Diagnostics;
|
|
5
|
+
using System.Runtime.InteropServices;
|
|
6
|
+
using System.Text;
|
|
7
|
+
using System.Threading;
|
|
8
|
+
using System.Threading.Tasks;
|
|
9
|
+
using System.Web.Script.Serialization;
|
|
10
|
+
using System.Windows.Automation;
|
|
11
|
+
|
|
12
|
+
partial class WindowsBridge {
|
|
13
|
+
[StructLayout(LayoutKind.Sequential)] struct KeyboardInput {public ushort Key,Scan;public uint Flags,Time;public UIntPtr Extra;}
|
|
14
|
+
const int UnicodeInputDelayMs=50;
|
|
15
|
+
static bool textInputDispatched;
|
|
16
|
+
sealed class HeldKey {public ushort Key,Scan;public uint Flags;}
|
|
17
|
+
[DllImport("user32.dll")] static extern IntPtr GetKeyboardLayout(uint thread);
|
|
18
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern short VkKeyScanEx(char value,IntPtr layout);
|
|
19
|
+
[DllImport("user32.dll",EntryPoint="SendMessageTimeoutW",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr ReadEditText(IntPtr window,uint message,UIntPtr count,StringBuilder text,uint flags,uint timeout,out UIntPtr result);
|
|
20
|
+
[StructLayout(LayoutKind.Sequential)] struct LastTextInput {public uint Size,Tick;}
|
|
21
|
+
[DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LastTextInput info);
|
|
22
|
+
sealed class KnownTextFocus {public IntPtr Handle;public uint InputTick;}
|
|
23
|
+
static uint LastTextTick() {LastTextInput info=new LastTextInput{Size=(uint)Marshal.SizeOf(typeof(LastTextInput))};if(!GetLastInputInfo(ref info))throw new InvalidOperationException("TARGET_FOCUS_UNKNOWN: cannot verify user input history");return info.Tick;}
|
|
24
|
+
static readonly Dictionary<string,KnownTextFocus> textFocus=new Dictionary<string,KnownTextFocus>();
|
|
25
|
+
static void ForgetTextFocus() {textFocus.Clear();}
|
|
26
|
+
static void RememberTextFocus(IntPtr window,IntPtr focus) {if(focus!=IntPtr.Zero && IsWindow(focus) && GetAncestor(focus,2)==window && WindowOwner(focus)==WindowOwner(window)){textFocus.Clear();textFocus[PointerOriginKey(window)]=new KnownTextFocus {Handle=focus,InputTick=LastTextTick()};}}
|
|
27
|
+
static IntPtr TextFocus(IntPtr window,bool allowRemembered=true) {
|
|
28
|
+
uint owner;uint thread=GetWindowThreadProcessId(window,out owner);ControlGuiInfo info=new ControlGuiInfo{Size=(uint)Marshal.SizeOf(typeof(ControlGuiInfo))};
|
|
29
|
+
if(GetGUIThreadInfo(thread,ref info) && info.Focus!=IntPtr.Zero && GetAncestor(info.Focus,2)==window && WindowOwner(info.Focus)==owner){KnownTextFocus known;if(textFocus.TryGetValue(PointerOriginKey(window),out known) && known.Handle!=info.Focus)textFocus.Remove(PointerOriginKey(window));return info.Focus;}
|
|
30
|
+
KnownTextFocus remembered;
|
|
31
|
+
if(allowRemembered && textFocus.TryGetValue(PointerOriginKey(window),out remembered)) {
|
|
32
|
+
if(remembered.InputTick!=LastTextTick()){textFocus.Remove(PointerOriginKey(window));throw new InvalidOperationException("TARGET_FOCUS_CHANGED: user input invalidated remembered focus; focus the intended control again");}
|
|
33
|
+
if(IsWindow(remembered.Handle) && GetAncestor(remembered.Handle,2)==window && WindowOwner(remembered.Handle)==owner)return remembered.Handle;
|
|
34
|
+
textFocus.Remove(PointerOriginKey(window));throw new InvalidOperationException("TARGET_FOCUS_CHANGED: remembered native control is stale");
|
|
35
|
+
}
|
|
36
|
+
return IntPtr.Zero;
|
|
37
|
+
}
|
|
38
|
+
static bool IsNativeText(IntPtr focus) {
|
|
39
|
+
if(focus==IntPtr.Zero)return false;StringBuilder name=new StringBuilder(256);GetClassName(focus,name,name.Capacity);string value=name.ToString();
|
|
40
|
+
return value.Equals("Edit",StringComparison.OrdinalIgnoreCase) || value.StartsWith("WindowsForms10.EDIT",StringComparison.OrdinalIgnoreCase) || value.StartsWith("RichEdit",StringComparison.OrdinalIgnoreCase);
|
|
41
|
+
}
|
|
42
|
+
static string NativeTextValue(IntPtr focus) {
|
|
43
|
+
if((ControlStyle(focus,-16).ToInt64() & 0x20)!=0)return null;
|
|
44
|
+
StringBuilder text=new StringBuilder(201);UIntPtr ignored;
|
|
45
|
+
return ReadEditText(focus,0xD,new UIntPtr(201),text,2,200,out ignored)==IntPtr.Zero?null:text.ToString();
|
|
46
|
+
}
|
|
47
|
+
static void AssertWritableText(IntPtr focus) {
|
|
48
|
+
if(focus!=IntPtr.Zero && (!IsWindowEnabled(focus) || (IsNativeText(focus) && (ControlStyle(focus,-16).ToInt64() & 0x800)!=0)))throw new InvalidOperationException("READ_ONLY: focused control is disabled or read-only");
|
|
49
|
+
}
|
|
50
|
+
static bool BackgroundEdit(IntPtr window,string text,Dictionary<string,object> result) {
|
|
51
|
+
if(text.IndexOf('\r')>=0 || text.IndexOf('\n')>=0)return false;
|
|
52
|
+
IntPtr focus=TextFocus(window);if(!IsNativeText(focus))return false;AssertWritableText(focus);
|
|
53
|
+
UIntPtr before,after,ignored;
|
|
54
|
+
if(SendMessageTimeout(focus,0xE,UIntPtr.Zero,IntPtr.Zero,2,500,out before)==IntPtr.Zero)throw new InvalidOperationException("BACKGROUND_READ_UNCONFIRMED: no text sent");
|
|
55
|
+
CheckWindowReference(window);
|
|
56
|
+
BeginWindowDispatch();
|
|
57
|
+
// Never retry a refused/uncertain EM_REPLACESEL through physical input.
|
|
58
|
+
if(ControlText(focus,0xC2,new UIntPtr(1),text,2,1000,out ignored)==IntPtr.Zero)throw new InvalidOperationException("TEXT_DISPATCH_UNCONFIRMED: edit may have received partial text; observe before retrying");
|
|
59
|
+
string value=NativeTextValue(focus);bool lengthKnown=SendMessageTimeout(focus,0xE,UIntPtr.Zero,IntPtr.Zero,2,500,out after)!=IntPtr.Zero;
|
|
60
|
+
RememberTextFocus(window,focus);
|
|
61
|
+
result["method"]="background:EM_REPLACESEL";result["verification"]="sent";
|
|
62
|
+
result["verified"]=lengthKnown && ((long)after.ToUInt64()-(long)before.ToUInt64()==text.Length || (value!=null && value.Contains(text)));
|
|
63
|
+
if(value!=null)result["verifyValue"]=value;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
static AutomationElement MainTextControl(IntPtr window) {
|
|
67
|
+
AutomationElement root=AutomationElement.FromHandle(window),best=null;double area=-1;
|
|
68
|
+
Condition controls=new OrCondition(new PropertyCondition(AutomationElement.ControlTypeProperty,ControlType.Edit),new PropertyCondition(AutomationElement.ControlTypeProperty,ControlType.Document));
|
|
69
|
+
AutomationElementCollection candidates=root.FindAll(TreeScope.Descendants,controls);
|
|
70
|
+
for(int i=0;i<candidates.Count;i++) {
|
|
71
|
+
AutomationElement candidate=candidates[i];var current=candidate.Current;if(!current.IsEnabled || !current.IsKeyboardFocusable || current.IsOffscreen || current.ProcessId!=(int)WindowOwner(window))continue;
|
|
72
|
+
object raw;if(candidate.TryGetCurrentPattern(ValuePattern.Pattern,out raw) && ((ValuePattern)raw).Current.IsReadOnly)continue;
|
|
73
|
+
double size=current.BoundingRectangle.Width*current.BoundingRectangle.Height;if(size>area){area=size;best=candidate;}
|
|
74
|
+
}
|
|
75
|
+
if(best==null)throw new InvalidOperationException("TEXT_FOCUS_NOT_FOUND: no writable main text control");return best;
|
|
76
|
+
}
|
|
77
|
+
static void TextCheck(EventWaitHandle cancel,Process listener,IntPtr window,uint owner,IntPtr expectedFocus,bool checkFocus,InteractionGuard interaction,bool requireForeground=true) {
|
|
78
|
+
CheckWindowReference(window);
|
|
79
|
+
if(cancel.WaitOne(0))throw new InvalidOperationException("COMPUTER_USE_STOPPED");
|
|
80
|
+
interaction.Check();
|
|
81
|
+
if(listener.HasExited)throw new InvalidOperationException("STOP_LISTENER_UNAVAILABLE");
|
|
82
|
+
if(!IsWindow(window) || WindowOwner(window)!=owner || !IsWindowEnabled(window) || !IsWindowVisible(window) || IsIconic(window))throw new InvalidOperationException("TARGET_IDENTITY_CHANGED");
|
|
83
|
+
if(requireForeground && GetForegroundWindow()!=window)throw new InvalidOperationException("TARGET_FOREGROUND_CHANGED: no more text sent");
|
|
84
|
+
if(checkFocus)foreach(int key in new int[]{1,2,4,16,17,18,91,92})if(GetAsyncKeyState(key)<0)throw new InvalidOperationException("USER_INPUT_ACTIVE: user button or modifier changed during typing");
|
|
85
|
+
if(checkFocus && TextFocus(window,false)!=expectedFocus)throw new InvalidOperationException("TARGET_FOCUS_CHANGED: no more text sent");
|
|
86
|
+
}
|
|
87
|
+
// SetFocus may return before the target thread applies it. Observe the exact
|
|
88
|
+
// requested field before input; never resend focus or type into the old field.
|
|
89
|
+
static IntPtr ConfirmTextFocus(AutomationElement requested,EventWaitHandle cancel,Process listener,IntPtr window,uint owner,InteractionGuard interaction) {
|
|
90
|
+
IntPtr requestedHandle=new IntPtr(requested.Current.NativeWindowHandle);
|
|
91
|
+
Stopwatch watch=Stopwatch.StartNew();
|
|
92
|
+
do {
|
|
93
|
+
TextCheck(cancel,listener,window,owner,IntPtr.Zero,false,interaction,false);
|
|
94
|
+
IntPtr focus=TextFocus(window,false);
|
|
95
|
+
if(GetForegroundWindow()==window && focus!=IntPtr.Zero && (requestedHandle!=IntPtr.Zero?focus==requestedHandle:requested.Current.HasKeyboardFocus))return focus;
|
|
96
|
+
if(cancel.WaitOne(10))throw new InvalidOperationException("COMPUTER_USE_STOPPED");
|
|
97
|
+
}while(watch.ElapsedMilliseconds<1000);
|
|
98
|
+
throw new InvalidOperationException("TARGET_FOCUS_CHANGED: requested text field focus was not confirmed; no text sent");
|
|
99
|
+
}
|
|
100
|
+
static void KeyEvent(HeldKey key,bool release) {
|
|
101
|
+
if(!release){BeginWindowDispatch();NotifyPhysicalInput(true);}
|
|
102
|
+
PointerInput input=new PointerInput{Type=1,Data=new InputUnion{Keyboard=new KeyboardInput{Key=key.Key,Scan=key.Scan,Flags=key.Flags | (release?2U:0U),Extra=OwnedInputMarker}}};
|
|
103
|
+
if(SendInput(1,new PointerInput[]{input},Marshal.SizeOf(typeof(PointerInput)))!=1)throw new InvalidOperationException("KEY_DISPATCH_UNCONFIRMED: partial input must not be replayed");
|
|
104
|
+
if(!release)textInputDispatched=true;
|
|
105
|
+
}
|
|
106
|
+
static void Down(List<HeldKey> held,HeldKey key) {held.Add(key);KeyEvent(key,false);}
|
|
107
|
+
static void Up(List<HeldKey> held,HeldKey key) {KeyEvent(key,true);held.Remove(key);}
|
|
108
|
+
static ushort SpecialKey(string key) {
|
|
109
|
+
switch(key){case "enter":case "return":return 13;case "tab":return 9;case "esc":case "escape":return 27;case "backspace":return 8;case "delete":case "del":return 46;case "home":return 36;case "end":return 35;case "pageup":case "pgup":return 33;case "pagedown":case "pgdn":return 34;case "up":return 38;case "down":return 40;case "left":return 37;case "right":return 39;case "space":return 32;case "insert":case "ins":return 45;case "pause":return 19;case "capslock":return 20;case "numlock":return 144;case "scrolllock":return 145;}
|
|
110
|
+
int number;if(key.StartsWith("f") && Int32.TryParse(key.Substring(1),out number) && number>=1 && number<=24)return (ushort)(111+number);return 0;
|
|
111
|
+
}
|
|
112
|
+
static void Chord(IEnumerable values,EventWaitHandle cancel,Process listener,IntPtr window,uint owner,List<HeldKey> held,InteractionGuard interaction,long holdMs=0,InputTiming timing=null) {
|
|
113
|
+
Action holdCheck=delegate {
|
|
114
|
+
TextCheck(cancel,listener,window,owner,IntPtr.Zero,false,interaction);
|
|
115
|
+
};
|
|
116
|
+
if(timing!=null)timing.Check();
|
|
117
|
+
List<ushort> modifiers=new List<ushort>();string main=null;
|
|
118
|
+
foreach(object item in values) {string key=Convert.ToString(item),name=key.Trim().ToLowerInvariant();if(name=="ctrl" || name=="control")modifiers.Add(17);else if(name=="alt" || name=="option")modifiers.Add(18);else if(name=="shift")modifiers.Add(16);else if(name=="win" || name=="windows" || name=="cmd" || name=="meta")throw new InvalidOperationException("KEY_BLOCKED");else if(main!=null)throw new InvalidOperationException("INVALID_ARGUMENTS: exactly one non-modifier key required");else main=key;}
|
|
119
|
+
if(main==null && modifiers.Count!=1)throw new InvalidOperationException("INVALID_ARGUMENTS: one main key or standalone modifier required");
|
|
120
|
+
ushort code=main==null?(ushort)0:SpecialKey(main.Trim().ToLowerInvariant());
|
|
121
|
+
if(main!=null && code==0) {
|
|
122
|
+
if(main.Length!=1)throw new InvalidOperationException("INVALID_ARGUMENTS: unsupported key");
|
|
123
|
+
uint ignored;uint thread=GetWindowThreadProcessId(window,out ignored);short mapping=VkKeyScanEx(main[0],GetKeyboardLayout(thread));
|
|
124
|
+
if(mapping==-1) {if(modifiers.Count>0)throw new InvalidOperationException("KEY_LAYOUT_UNSUPPORTED: chord cannot represent this character");TextCheck(cancel,listener,window,owner,IntPtr.Zero,false,interaction);HeldKey unicode=new HeldKey{Scan=main[0],Flags=4};Down(held,unicode);if(timing!=null && holdMs>0)timing.Wait(holdMs,cancel,holdCheck);Up(held,unicode);return;}
|
|
125
|
+
code=(ushort)(mapping & 255);
|
|
126
|
+
// A named shortcut letter is layout-mapped without adding implicit Shift.
|
|
127
|
+
if(modifiers.Count==0){if((mapping & 0x100)!=0)modifiers.Add(16);if((mapping & 0x200)!=0)modifiers.Add(17);if((mapping & 0x400)!=0)modifiers.Add(18);}
|
|
128
|
+
}
|
|
129
|
+
foreach(ushort modifier in modifiers){TextCheck(cancel,listener,window,owner,IntPtr.Zero,false,interaction);Down(held,new HeldKey{Key=modifier});}
|
|
130
|
+
TextCheck(cancel,listener,window,owner,IntPtr.Zero,false,interaction);
|
|
131
|
+
if(main!=null){HeldKey primary=new HeldKey{Key=code,Flags=(code>=33 && code<=46)?1U:0U};Down(held,primary);}
|
|
132
|
+
if(timing!=null && holdMs>0)timing.Wait(holdMs,cancel,holdCheck);
|
|
133
|
+
// Immediate presses release without post-dispatch checks: their effect may move focus.
|
|
134
|
+
// Positive holds retain the existing keypress target/protection checks during the wait.
|
|
135
|
+
// Release modifiers regardless of target/focus changes caused by the chord.
|
|
136
|
+
for(int i=held.Count-1;i>=0;i--){HeldKey key=held[i];Up(held,key);}
|
|
137
|
+
}
|
|
138
|
+
static Task<string> clipboardRead;
|
|
139
|
+
static Dictionary<string,object> ClipboardReply(Process child,int milliseconds) {
|
|
140
|
+
if(clipboardRead==null)clipboardRead=Task.Factory.StartNew(()=>child.StandardOutput.ReadLine());
|
|
141
|
+
if(!clipboardRead.Wait(milliseconds))throw new InvalidOperationException("CLIPBOARD_HELPER_TIMEOUT");
|
|
142
|
+
string line=clipboardRead.Result;clipboardRead=null;
|
|
143
|
+
if(line==null)throw new InvalidOperationException("CLIPBOARD_HELPER_EXITED");
|
|
144
|
+
return new JavaScriptSerializer().Deserialize<Dictionary<string,object>>(line);
|
|
145
|
+
}
|
|
146
|
+
static Dictionary<string,object> ClipboardPhase(Process child,string expected,int milliseconds) {
|
|
147
|
+
Dictionary<string,object> reply=ClipboardReply(child,milliseconds);
|
|
148
|
+
if(!Convert.ToBoolean(reply["ok"]) || Convert.ToString(reply["phase"])!=expected)throw new InvalidOperationException(Convert.ToString(reply["error"])??"CLIPBOARD_PHASE_FAILED");
|
|
149
|
+
return reply;
|
|
150
|
+
}
|
|
151
|
+
static Dictionary<string,object> ConfirmClipboardRestore(Process child) {
|
|
152
|
+
Stopwatch watch=Stopwatch.StartNew();
|
|
153
|
+
for(int i=0;i<4 && watch.ElapsedMilliseconds<2500;i++) {
|
|
154
|
+
var reply=ClipboardReply(child,Math.Max(1,2500-(int)watch.ElapsedMilliseconds));
|
|
155
|
+
if(Convert.ToString(reply["phase"])=="restored" && Convert.ToBoolean(reply["ok"]))return reply;
|
|
156
|
+
}
|
|
157
|
+
throw new InvalidOperationException("CLIPBOARD_RESTORE_UNCONFIRMED");
|
|
158
|
+
}
|
|
159
|
+
static object ExecuteText(string action,Dictionary<string,object> args) {
|
|
160
|
+
Dictionary<string,object> result=Result(true,action);result["cleanupConfirmed"]=true;textInputDispatched=false;
|
|
161
|
+
IntPtr before=IntPtr.Zero,window=IntPtr.Zero;ControlPoint pointer=new ControlPoint();
|
|
162
|
+
List<HeldKey> held=new List<HeldKey>();HashSet<long> popups=new HashSet<long>();
|
|
163
|
+
EventWaitHandle cancel=null;Process listener=null,clipboard=null;bool clipboardMayChange=false,clipboardReplaced=false;
|
|
164
|
+
InteractionGuard interaction=null;
|
|
165
|
+
AutomationElement main=null;bool cleanup=true;
|
|
166
|
+
try {
|
|
167
|
+
long holdMs=0,inputDelayMs=UnicodeInputDelayMs;InputTiming timing=null;
|
|
168
|
+
string text=TextOption(args,"text",""),method=TextOption(args,"method","auto");
|
|
169
|
+
if(text.IndexOf('\0')>=0)throw new InvalidOperationException("INVALID_ARGUMENTS: NUL text cannot be entered safely");
|
|
170
|
+
if(action=="type_text") {
|
|
171
|
+
inputDelayMs=InputTiming.Milliseconds(args,"inputDelayMs",UnicodeInputDelayMs);
|
|
172
|
+
if((args.ContainsKey("inputDelayMs") || args.ContainsKey("timeoutMs")) && (method!="sendinput" || args.ContainsKey("browserTab")))throw new InvalidOperationException("TIMING_UNSUPPORTED: text timing requires explicit native sendinput");
|
|
173
|
+
if(method=="sendinput") {
|
|
174
|
+
long units=text.Replace("\r\n","\n").Length;
|
|
175
|
+
if(inputDelayMs!=0 && units>9007199254740991L/inputDelayMs)throw new InvalidOperationException("INVALID_ARGUMENTS: requested pacing duration is not representable");
|
|
176
|
+
long duration=args.ContainsKey("inputDelayMs")?units*inputDelayMs:0;
|
|
177
|
+
timing=new InputTiming(args,duration,args.ContainsKey("inputDelayMs"),40000);
|
|
178
|
+
var policy=timing.Describe();policy["inputDelayMs"]=inputDelayMs;result["timing"]=policy;
|
|
179
|
+
result["inputDelayMs"]=inputDelayMs;result["inputUnitsDispatched"]=0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if(action=="keypress") {
|
|
183
|
+
holdMs=InputTiming.Milliseconds(args,"holdMs",0);
|
|
184
|
+
timing=new InputTiming(args,holdMs,args.ContainsKey("holdMs"),40000);
|
|
185
|
+
var policy=timing.Describe();policy["holdMs"]=holdMs;result["timing"]=policy;
|
|
186
|
+
if((args.ContainsKey("holdMs") || args.ContainsKey("timeoutMs")) && TextOption(args,"dispatch","foreground")=="background")throw new InvalidOperationException("TIMING_UNSUPPORTED: keypress timing requires native foreground dispatch");
|
|
187
|
+
}
|
|
188
|
+
interaction=new InteractionGuard(TextOption(args,"_cancelEvent",""),NumberOption(args,"_stopProcessId",0));before=GetForegroundWindow();GetCursorPos(out pointer);
|
|
189
|
+
Dictionary<string,object> quiet=new Dictionary<string,object>(args);quiet["activate"]=false;
|
|
190
|
+
if(args.ContainsKey("_elementId")) {
|
|
191
|
+
main=ResolveObservedElement(TextOption(args,"_elementId",""),quiet,out window);
|
|
192
|
+
if(!main.Current.IsEnabled)throw new InvalidOperationException("ELEMENT_DISABLED");
|
|
193
|
+
object raw;if(main.TryGetCurrentPattern(ValuePattern.Pattern,out raw) && ((ValuePattern)raw).Current.IsReadOnly)throw new InvalidOperationException("READ_ONLY: fallback cannot bypass a read-only value");
|
|
194
|
+
} else window=ResolveObservationWindow(quiet);
|
|
195
|
+
ApplyVisibleIntent(args,window);
|
|
196
|
+
if(textFocus.Count>0 && !textFocus.ContainsKey(PointerOriginKey(window)))ForgetTextFocus();
|
|
197
|
+
uint owner=WindowOwner(window);popups=ControlPopups(owner,window);
|
|
198
|
+
if(action=="type_text" && method=="auto" && TextOption(args,"focusMode","preserve")=="preserve" && !Option(args,"activate",false) && BackgroundEdit(window,text,result)) {textInputDispatched=true;result["inputTargetHandle"]=window.ToInt64();result["inputTargetProcessId"]=owner;return result;}
|
|
199
|
+
bool background=action=="type_text"?method=="background":TextOption(args,"dispatch","foreground")=="background";
|
|
200
|
+
if(background) {
|
|
201
|
+
if(!args.ContainsKey("windowRef") && !args.ContainsKey("windowTitle") && !args.ContainsKey("processId") && !args.ContainsKey("nativeWindowHandle"))throw new InvalidOperationException("BACKGROUND_TARGET_REQUIRED");
|
|
202
|
+
string characters=action=="type_text"?text:Convert.ToString(((IList)args["keys"])[0]);
|
|
203
|
+
IntPtr recipient=TextFocus(window);if(recipient==IntPtr.Zero)recipient=window;AssertWritableText(recipient);
|
|
204
|
+
foreach(char character in characters) {CheckWindowReference(window);if(!IsWindow(window) || WindowOwner(window)!=owner || WindowOwner(recipient)!=owner)throw new InvalidOperationException("TARGET_IDENTITY_CHANGED");BeginWindowDispatch();if(!ControlPost(recipient,0x102,new IntPtr(character),new IntPtr(1)))throw new InvalidOperationException("TEXT_DISPATCH_UNCONFIRMED: some WM_CHAR messages may have been delivered; never replay");textInputDispatched=true;}
|
|
205
|
+
result["method"]="postmessage-wmchar";result["verification"]="unconfirmed";result["verified"]=false;result["inputTargetHandle"]=window.ToInt64();result["inputTargetProcessId"]=owner;return result;
|
|
206
|
+
}
|
|
207
|
+
if(TextOption(args,"_cancelEvent","").Length==0)throw new InvalidOperationException("WCU_NEEDS_INPUT: physical text/key input requires independent stopping");
|
|
208
|
+
cancel=EventWaitHandle.OpenExisting(Convert.ToString(args["_cancelEvent"]));listener=Process.GetProcessById(NumberOption(args,"_stopProcessId",0));IntPtr listenerHandle=listener.Handle;
|
|
209
|
+
if(cancel.WaitOne(0) || listener.HasExited)throw new InvalidOperationException("COMPUTER_USE_STOPPED");
|
|
210
|
+
ManagementEnsureUnlockedSession();
|
|
211
|
+
foreach(int key in new int[]{1,2,4,16,17,18,91,92})if(GetAsyncKeyState(key)<0)throw new InvalidOperationException("USER_INPUT_ACTIVE: button or modifier already held");
|
|
212
|
+
interaction.Check();
|
|
213
|
+
if(GetForegroundWindow()!=window) {
|
|
214
|
+
Dictionary<string,object> activation=new Dictionary<string,object>();
|
|
215
|
+
if(!ManagementActivate(window,owner,activation) || GetForegroundWindow()!=window){activation["foregroundAtRefusal"]=ManagementForegroundRelationship(GetForegroundWindow(),window,owner);result["activationDiagnostic"]=activation;throw new InvalidOperationException("TARGET_ACTIVATION_REFUSED");}
|
|
216
|
+
}
|
|
217
|
+
if(args.ContainsKey("_elementId")){CheckWindowReference(window);BeginWindowDispatch();main.SetFocus();}
|
|
218
|
+
else if(action=="type_text" && TextOption(args,"focusMode","preserve")=="main") {main=MainTextControl(window);CheckWindowReference(window);BeginWindowDispatch();main.SetFocus();result["focusedControl"]=main.Current.ControlType.ProgrammaticName;}
|
|
219
|
+
if(action=="type_text" && main==null && TextFocus(window,false)==IntPtr.Zero) {IntPtr remembered=TextFocus(window);if(remembered!=IntPtr.Zero){CheckWindowReference(window);BeginWindowDispatch();main=AutomationElement.FromHandle(remembered);main.SetFocus();}}
|
|
220
|
+
IntPtr expectedFocus=main!=null?ConfirmTextFocus(main,cancel,listener,window,owner,interaction):TextFocus(window,false);if(main!=null){result["focusConfirmed"]=true;result["inputFocusHandle"]=expectedFocus.ToInt64();}if(action=="type_text")AssertWritableText(expectedFocus);
|
|
221
|
+
TextCheck(cancel,listener,window,owner,expectedFocus,action=="type_text",interaction);
|
|
222
|
+
if(action=="type_text")RememberTextFocus(window,expectedFocus);
|
|
223
|
+
if(action=="keypress") {Chord((IEnumerable)args["keys"],cancel,listener,window,owner,held,interaction,holdMs,timing);result["method"]="sendinput-keychord";}
|
|
224
|
+
else if(method=="sendinput") {
|
|
225
|
+
// Preserve complete UTF-16 pairs and newline/tab handling at the selected pace.
|
|
226
|
+
// Dispatch is still not application verification.
|
|
227
|
+
for(int index=0;index<text.Length;index++) {
|
|
228
|
+
char character=text[index];
|
|
229
|
+
if(character=='\r' && index+1<text.Length && text[index+1]=='\n')index++;
|
|
230
|
+
timing.Check();TextCheck(cancel,listener,window,owner,expectedFocus,true,interaction);
|
|
231
|
+
HeldKey key=character=='\r' || character=='\n'?new HeldKey{Key=13}:character=='\t'?new HeldKey{Key=9}:new HeldKey{Scan=character,Flags=4};
|
|
232
|
+
Down(held,key);Up(held,key);
|
|
233
|
+
result["inputUnitsDispatched"]=Convert.ToInt32(result["inputUnitsDispatched"])+1;
|
|
234
|
+
timing.Wait(inputDelayMs,cancel,delegate {TextCheck(cancel,listener,window,owner,expectedFocus,true,interaction);});
|
|
235
|
+
}
|
|
236
|
+
result["method"]="sendinput-unicode";
|
|
237
|
+
}else {
|
|
238
|
+
// All UIA focus/provider calls finish BEFORE clipboard replacement.
|
|
239
|
+
ProcessStartInfo info=new ProcessStartInfo{FileName=TextOption(args,"_clipboardExecutable",""),UseShellExecute=false,CreateNoWindow=true,RedirectStandardInput=true,RedirectStandardOutput=true,RedirectStandardError=true,StandardOutputEncoding=Encoding.UTF8,StandardErrorEncoding=Encoding.UTF8};
|
|
240
|
+
clipboardRead=null;clipboard=Process.Start(info);clipboard.ErrorDataReceived+=delegate(object sender,DataReceivedEventArgs e){};clipboard.BeginErrorReadLine();
|
|
241
|
+
clipboard.StandardInput.WriteLine(new JavaScriptSerializer().Serialize(new Dictionary<string,object>{{"text",text},{"cancelEvent",args["_cancelEvent"]},{"parentProcessId",Process.GetCurrentProcess().Id}}));clipboard.StandardInput.Flush();
|
|
242
|
+
var snapshotReply=ClipboardPhase(clipboard,"snapshot",5000);
|
|
243
|
+
result["clipboardSnapshot"]=new Dictionary<string,object>{{"preservedFormats",snapshotReply["formats"]},{"oleMetadataExcluded",snapshotReply["oleMetadataExcluded"]},{"oleFormatsValidated",snapshotReply["oleFormatsValidated"]}};
|
|
244
|
+
TextCheck(cancel,listener,window,owner,expectedFocus,true,interaction);
|
|
245
|
+
clipboardMayChange=true;clipboard.StandardInput.WriteLine("replace");clipboard.StandardInput.Flush();ClipboardPhase(clipboard,"replaced",2000);clipboardReplaced=true;
|
|
246
|
+
TextCheck(cancel,listener,window,owner,expectedFocus,true,interaction);
|
|
247
|
+
if(Option(args,"_replaceAll",false)){Chord(new string[]{"Ctrl","A"},cancel,listener,window,owner,held,interaction);TextCheck(cancel,listener,window,owner,expectedFocus,true,interaction);}
|
|
248
|
+
Chord(new string[]{"Ctrl","V"},cancel,listener,window,owner,held,interaction);
|
|
249
|
+
if(cancel.WaitOne(60))throw new InvalidOperationException("COMPUTER_USE_STOPPED");
|
|
250
|
+
UIntPtr ignored;if(SendMessageTimeout(window,0,UIntPtr.Zero,IntPtr.Zero,2,200,out ignored)==IntPtr.Zero)throw new InvalidOperationException("PASTE_DISPATCH_UNCONFIRMED: target did not complete its bounded drain; observe before retrying");
|
|
251
|
+
result["method"]="clipboard-paste";result["restoreClipboard"]=Option(args,"restoreClipboard",true);
|
|
252
|
+
}
|
|
253
|
+
if(action=="type_text")RememberTextFocus(window,expectedFocus);
|
|
254
|
+
result["verification"]="sent";result["inputTargetHandle"]=window.ToInt64();result["inputTargetProcessId"]=owner;
|
|
255
|
+
}catch(Exception error){result["ok"]=false;result["error"]=error.Message;if(error.Message.StartsWith("WCU_NEEDS_INPUT",StringComparison.Ordinal))result["inputRequired"]=true;}
|
|
256
|
+
finally {
|
|
257
|
+
for(int i=held.Count-1;i>=0;i--)try{KeyEvent(held[i],true);}catch(Exception error){cleanup=false;result["ok"]=false;result["error"]="INPUT_CLEANUP_UNCONFIRMED: "+error.Message;}
|
|
258
|
+
if(clipboard!=null) {
|
|
259
|
+
try {
|
|
260
|
+
if(clipboardMayChange) {
|
|
261
|
+
if(!clipboard.HasExited){clipboard.StandardInput.WriteLine(result.ContainsKey("error") || !clipboardReplaced || Option(args,"restoreClipboard",true)?"restore":"keep");clipboard.StandardInput.Flush();}
|
|
262
|
+
var restoration=ConfirmClipboardRestore(clipboard);result["clipboardRestoreOutcome"]=restoration["restoreOutcome"];
|
|
263
|
+
if(!clipboard.WaitForExit(2000))throw new InvalidOperationException("CLIPBOARD_EXIT_UNCONFIRMED");
|
|
264
|
+
}else {clipboard.StandardInput.Close();if(!clipboard.WaitForExit(500)){clipboard.Kill();if(!clipboard.WaitForExit(1000))throw new InvalidOperationException("CLIPBOARD_EXIT_UNCONFIRMED");}result["clipboardRestoreOutcome"]="unchanged";}
|
|
265
|
+
result["clipboardCleanupConfirmed"]=true;
|
|
266
|
+
}catch(Exception error){if(!clipboardMayChange){try{if(!clipboard.HasExited){clipboard.Kill();clipboard.WaitForExit(1000);}}catch{} }else {cleanup=false;result["ok"]=false;result["error"]="CLIPBOARD_CLEANUP_UNCONFIRMED: "+error.Message;}}
|
|
267
|
+
if(!clipboardMayChange || cleanup)clipboard.Dispose();
|
|
268
|
+
}
|
|
269
|
+
if(cancel!=null)cancel.Dispose();if(listener!=null)listener.Dispose();
|
|
270
|
+
result["cleanupConfirmed"]=cleanup;
|
|
271
|
+
if(action=="keypress" || action=="type_text"){result["inputDispatched"]=textInputDispatched;result["interrupted"]=!Convert.ToBoolean(result["ok"]); }
|
|
272
|
+
// No potentially blocked UIA verification is permitted before clipboard cleanup.
|
|
273
|
+
if(cleanup)RestoreControlState(result,before,pointer,window,popups,Option(args,"activate",false),interaction);
|
|
274
|
+
else {result["pointer"]="left: cleanup unconfirmed";result["foreground"]="left: cleanup unconfirmed";}
|
|
275
|
+
if(cleanup)NotifyPhysicalInput(false);
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
}
|