dsh-click 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +201 -0
- package/README.es.md +174 -0
- package/README.hi.md +174 -0
- package/README.md +174 -0
- package/README.pt.md +174 -0
- package/README.zh.md +174 -0
- package/SECURITY.md +38 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/cordis.patch.yml +53 -0
- package/lib/index.js +1810 -0
- package/lib/types/actions.d.ts +94 -0
- package/lib/types/actions.d.ts.map +1 -0
- package/lib/types/config.d.ts +119 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/events.d.ts +111 -0
- package/lib/types/events.d.ts.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/observe.d.ts +76 -0
- package/lib/types/observe.d.ts.map +1 -0
- package/lib/types/platform/runner.d.ts +40 -0
- package/lib/types/platform/runner.d.ts.map +1 -0
- package/lib/types/platform/selection.d.ts +44 -0
- package/lib/types/platform/selection.d.ts.map +1 -0
- package/lib/types/platform/types.d.ts +220 -0
- package/lib/types/platform/types.d.ts.map +1 -0
- package/lib/types/sanitize.d.ts +45 -0
- package/lib/types/sanitize.d.ts.map +1 -0
- package/lib/types/tools.d.ts +113 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/version.d.ts +13 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/vision.d.ts +22 -0
- package/lib/types/vision.d.ts.map +1 -0
- package/native/win32/dsh-click-helper.ps1 +870 -0
- package/package.json +143 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
# dsh-click native helper (Windows PowerShell 5.1).
|
|
2
|
+
#
|
|
3
|
+
# One request = one process: JSON request on stdin, JSON response on stdout
|
|
4
|
+
# (exit code 0 even for handled failures; the error envelope is the response).
|
|
5
|
+
# The harness drives this through ctx.subprocess; nothing here ever writes to
|
|
6
|
+
# any other stream, and nothing here steals foreground focus unless the
|
|
7
|
+
# `focusFallback` flag is explicitly true (config-gated, approval-gated).
|
|
8
|
+
#
|
|
9
|
+
# Operations: windows, apps, shot, tree, snapshot, click, type, scroll, key, launch.
|
|
10
|
+
# The helper is NOT a security boundary by itself — the harness-side
|
|
11
|
+
# freshness check, approval gate, and process-identity check are. This file
|
|
12
|
+
# only refuses to invent global input: actions address one window by handle.
|
|
13
|
+
|
|
14
|
+
$ErrorActionPreference = 'Stop'
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
18
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
19
|
+
} catch { }
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Win32 interop (one Add-Type block; PS 5.1 safe C#).
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
Add-Type -TypeDefinition @'
|
|
25
|
+
using System;
|
|
26
|
+
using System.Collections.Generic;
|
|
27
|
+
using System.Runtime.InteropServices;
|
|
28
|
+
using System.Text;
|
|
29
|
+
|
|
30
|
+
public static class DshClickWin32 {
|
|
31
|
+
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
32
|
+
|
|
33
|
+
[DllImport("user32.dll")]
|
|
34
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
35
|
+
|
|
36
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
37
|
+
public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
|
38
|
+
|
|
39
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
40
|
+
public static extern int GetClassName(IntPtr hWnd, StringBuilder text, int count);
|
|
41
|
+
|
|
42
|
+
[DllImport("user32.dll")]
|
|
43
|
+
public static extern bool IsWindowVisible(IntPtr hWnd);
|
|
44
|
+
|
|
45
|
+
[DllImport("user32.dll")]
|
|
46
|
+
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
|
47
|
+
|
|
48
|
+
[DllImport("user32.dll")]
|
|
49
|
+
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
|
50
|
+
|
|
51
|
+
[DllImport("user32.dll")]
|
|
52
|
+
public static extern IntPtr GetForegroundWindow();
|
|
53
|
+
|
|
54
|
+
[DllImport("user32.dll")]
|
|
55
|
+
public static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
|
56
|
+
|
|
57
|
+
[DllImport("user32.dll")]
|
|
58
|
+
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
59
|
+
|
|
60
|
+
[DllImport("user32.dll")]
|
|
61
|
+
public static extern int GetSystemMetrics(int index);
|
|
62
|
+
|
|
63
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
64
|
+
public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);
|
|
65
|
+
|
|
66
|
+
[DllImport("kernel32.dll")]
|
|
67
|
+
public static extern bool CloseHandle(IntPtr handle);
|
|
68
|
+
|
|
69
|
+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
70
|
+
public static extern bool QueryFullProcessImageName(IntPtr handle, uint flags, StringBuilder buffer, ref uint size);
|
|
71
|
+
|
|
72
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
73
|
+
public struct RECT {
|
|
74
|
+
public int Left; public int Top; public int Right; public int Bottom;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static IntPtr[] EnumVisibleWindows() {
|
|
78
|
+
var list = new List<IntPtr>();
|
|
79
|
+
EnumWindows(delegate(IntPtr hWnd, IntPtr lParam) {
|
|
80
|
+
if (IsWindowVisible(hWnd)) list.Add(hWnd);
|
|
81
|
+
return true;
|
|
82
|
+
}, IntPtr.Zero);
|
|
83
|
+
return list.ToArray();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public static string WindowTitle(IntPtr hWnd) {
|
|
87
|
+
var sb = new StringBuilder(1024);
|
|
88
|
+
GetWindowText(hWnd, sb, sb.Capacity);
|
|
89
|
+
return sb.ToString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public static string WindowClass(IntPtr hWnd) {
|
|
93
|
+
var sb = new StringBuilder(512);
|
|
94
|
+
GetClassName(hWnd, sb, sb.Capacity);
|
|
95
|
+
return sb.ToString();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
'@ | Out-Null
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Response plumbing.
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
function Write-DshResponse($payload) {
|
|
104
|
+
$json = $payload | ConvertTo-Json -Depth 24 -Compress
|
|
105
|
+
[Console]::Out.WriteLine($json)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Write-DshError($code, $message) {
|
|
109
|
+
Write-DshResponse @{ ok = $false; error = @{ code = $code; message = $message } }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ConvertTo-DshJsonValue($value) {
|
|
113
|
+
# PS 5.1 lacks -AsHashtable; round-trip through ConvertTo-Json/ConvertFrom-Json
|
|
114
|
+
# is unnecessary here — property bags serialized directly.
|
|
115
|
+
return $value
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# UIA + drawing assemblies.
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
try {
|
|
122
|
+
Add-Type -AssemblyName UIAutomationClient | Out-Null
|
|
123
|
+
Add-Type -AssemblyName UIAutomationTypes | Out-Null
|
|
124
|
+
Add-Type -AssemblyName System.Drawing | Out-Null
|
|
125
|
+
} catch {
|
|
126
|
+
Write-DshError 'UIA_FAILED' ("cannot load UIAutomation/System.Drawing: " + ($_.Exception.Message))
|
|
127
|
+
exit 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Small helpers.
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
function Get-WindowRectInfo($hwnd) {
|
|
134
|
+
$rect = New-Object DshClickWin32+RECT
|
|
135
|
+
[void][DshClickWin32]::GetWindowRect($hwnd, [ref]$rect)
|
|
136
|
+
return @{
|
|
137
|
+
x = [int]$rect.Left; y = [int]$rect.Top
|
|
138
|
+
width = [int]($rect.Right - $rect.Left); height = [int]($rect.Bottom - $rect.Top)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function Get-ProcessFacts($hwnd) {
|
|
143
|
+
$pidValue = 0
|
|
144
|
+
[void][DshClickWin32]::GetWindowThreadProcessId($hwnd, [ref]$pidValue)
|
|
145
|
+
$pidInt = [int]$pidValue
|
|
146
|
+
$path = $null
|
|
147
|
+
if ($pidInt -gt 0) {
|
|
148
|
+
$handle = [DshClickWin32]::OpenProcess(0x1000, $false, [uint32]$pidInt) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
149
|
+
if ($handle -ne [IntPtr]::Zero) {
|
|
150
|
+
try {
|
|
151
|
+
$size = [uint32]1024
|
|
152
|
+
$buffer = New-Object System.Text.StringBuilder 1024
|
|
153
|
+
if ([DshClickWin32]::QueryFullProcessImageName($handle, 0, $buffer, [ref]$size)) {
|
|
154
|
+
$path = $buffer.ToString()
|
|
155
|
+
}
|
|
156
|
+
} finally {
|
|
157
|
+
[void][DshClickWin32]::CloseHandle($handle)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return @{ pid = $pidInt; executablePath = $path }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Get-StringHash($text) {
|
|
165
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
166
|
+
try {
|
|
167
|
+
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
|
168
|
+
$hashBytes = $sha.ComputeHash($bytes)
|
|
169
|
+
return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
|
|
170
|
+
} finally {
|
|
171
|
+
$sha.Dispose()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Get-BitmapHash($bitmap) {
|
|
176
|
+
$rect = New-Object System.Drawing.Rectangle(0, 0, $bitmap.Width, $bitmap.Height)
|
|
177
|
+
$data = $bitmap.LockBits($rect, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
178
|
+
try {
|
|
179
|
+
$byteCount = [Math]::Abs($data.Stride) * $bitmap.Height
|
|
180
|
+
$bytes = New-Object byte[] $byteCount
|
|
181
|
+
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $byteCount)
|
|
182
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
183
|
+
try {
|
|
184
|
+
$hashBytes = $sha.ComputeHash($bytes)
|
|
185
|
+
return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
|
|
186
|
+
} finally {
|
|
187
|
+
$sha.Dispose()
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
$bitmap.UnlockBits($data)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function Get-UiaElement($hwnd) {
|
|
195
|
+
return [System.Windows.Automation.AutomationElement]::FromHandle([IntPtr]$hwnd)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function ConvertTo-Hwnd($value) {
|
|
199
|
+
if ($value -eq $null) { return [IntPtr]::Zero }
|
|
200
|
+
if ($value -is [int]) { return [IntPtr][int64]$value }
|
|
201
|
+
if ($value -is [int64]) { return [IntPtr]$value }
|
|
202
|
+
if ($value -is [double]) { return [IntPtr][int64]$value }
|
|
203
|
+
return [IntPtr]::Zero
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function Resolve-Window($target) {
|
|
207
|
+
if ($target -eq $null) { $target = @{} }
|
|
208
|
+
$hwnd = ConvertTo-Hwnd $target.windowId
|
|
209
|
+
if ($hwnd -ne [IntPtr]::Zero) {
|
|
210
|
+
$rect = Get-WindowRectInfo $hwnd
|
|
211
|
+
if ($rect.width -le 0 -and $rect.height -le 0) {
|
|
212
|
+
# A handle that no longer resolves to a window.
|
|
213
|
+
throw "window $($target.windowId) not found"
|
|
214
|
+
}
|
|
215
|
+
return $hwnd
|
|
216
|
+
}
|
|
217
|
+
$title = $target.windowTitle
|
|
218
|
+
if ($title -ne $null -and $title -is [string] -and $title.Length -gt 0) {
|
|
219
|
+
foreach ($candidate in [DshClickWin32]::EnumVisibleWindows()) {
|
|
220
|
+
if ([DshClickWin32]::WindowTitle($candidate) -like "*$title*") { return $candidate }
|
|
221
|
+
}
|
|
222
|
+
throw "no visible window matches title '$title'"
|
|
223
|
+
}
|
|
224
|
+
$processId = $target.processId
|
|
225
|
+
if ($processId -ne $null -and $processId -is [int] -and $processId -gt 0) {
|
|
226
|
+
foreach ($candidate in [DshClickWin32]::EnumVisibleWindows()) {
|
|
227
|
+
$pidValue = 0
|
|
228
|
+
[void][DshClickWin32]::GetWindowThreadProcessId($candidate, [ref]$pidValue)
|
|
229
|
+
if ([int]$pidValue -eq [int]$processId) { return $candidate }
|
|
230
|
+
}
|
|
231
|
+
throw "no visible window owned by process $processId"
|
|
232
|
+
}
|
|
233
|
+
$foreground = [DshClickWin32]::GetForegroundWindow()
|
|
234
|
+
if ($foreground -eq [IntPtr]::Zero) { throw 'no foreground window and no target given' }
|
|
235
|
+
return $foreground
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Accessibility walk (canonical: the same walk builds treeHash everywhere).
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
$script:TreeNodes = @()
|
|
242
|
+
|
|
243
|
+
function Get-RuntimeIdString($element) {
|
|
244
|
+
try {
|
|
245
|
+
$ids = $element.GetRuntimeId()
|
|
246
|
+
$parts = @()
|
|
247
|
+
foreach ($id in $ids) { $parts += [string][int]$id }
|
|
248
|
+
return ($parts -join '.')
|
|
249
|
+
} catch {
|
|
250
|
+
return ''
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function Get-ElementPatterns($element) {
|
|
255
|
+
$patterns = @()
|
|
256
|
+
$value = $null
|
|
257
|
+
if ($element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$value)) { $patterns += 'value' }
|
|
258
|
+
$invoke = $null
|
|
259
|
+
if ($element.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$invoke)) { $patterns += 'invoke' }
|
|
260
|
+
$scroll = $null
|
|
261
|
+
if ($element.TryGetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern, [ref]$scroll)) { $patterns += 'scroll' }
|
|
262
|
+
return $patterns
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function Get-ElementRecord($element) {
|
|
266
|
+
$name = ''
|
|
267
|
+
$controlType = ''
|
|
268
|
+
$automationId = ''
|
|
269
|
+
$enabled = $false
|
|
270
|
+
$rect = @{ x = 0; y = 0; width = 0; height = 0 }
|
|
271
|
+
try {
|
|
272
|
+
$name = [string]$element.Current.Name
|
|
273
|
+
$controlType = [string]$element.Current.ControlType.ProgrammaticName
|
|
274
|
+
if ($controlType -like 'ControlType.*') { $controlType = $controlType.Substring(12) }
|
|
275
|
+
$automationId = [string]$element.Current.AutomationId
|
|
276
|
+
$enabled = [bool]$element.Current.IsEnabled
|
|
277
|
+
$bounds = $element.Current.BoundingRectangle
|
|
278
|
+
if (-not $bounds.IsEmpty) {
|
|
279
|
+
$rect = @{ x = [int]$bounds.X; y = [int]$bounds.Y; width = [int]$bounds.Width; height = [int]$bounds.Height }
|
|
280
|
+
}
|
|
281
|
+
} catch { }
|
|
282
|
+
return @{
|
|
283
|
+
elementId = (Get-RuntimeIdString $element)
|
|
284
|
+
controlType = $controlType
|
|
285
|
+
name = $name
|
|
286
|
+
automationId = $automationId
|
|
287
|
+
rect = $rect
|
|
288
|
+
enabled = $enabled
|
|
289
|
+
patterns = @(Get-ElementPatterns $element)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function Walk-UiaTree($element, $depth, $maxDepth, $maxElements, $includePixels) {
|
|
294
|
+
if ($script:TreeNodes.Count -ge $maxElements -or $depth -gt $maxDepth -or $element -eq $null) { return }
|
|
295
|
+
$record = Get-ElementRecord $element
|
|
296
|
+
if ($record.elementId -ne '') {
|
|
297
|
+
$script:TreeNodes += $record
|
|
298
|
+
}
|
|
299
|
+
if ($script:TreeNodes.Count -ge $maxElements) { return }
|
|
300
|
+
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
|
301
|
+
try {
|
|
302
|
+
$child = $walker.GetFirstChild($element)
|
|
303
|
+
while ($child -ne $null -and $script:TreeNodes.Count -lt $maxElements -and $depth -lt $maxDepth) {
|
|
304
|
+
Walk-UiaTree $child ($depth + 1) $maxDepth $maxElements $includePixels
|
|
305
|
+
if ($script:TreeNodes.Count -ge $maxElements) { break }
|
|
306
|
+
$child = $walker.GetNextSibling($child)
|
|
307
|
+
}
|
|
308
|
+
} catch { }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function Get-TreeHashText($nodes) {
|
|
312
|
+
$material = @()
|
|
313
|
+
foreach ($node in $nodes) {
|
|
314
|
+
$r = $node.rect
|
|
315
|
+
$material += "$($node.elementId)|$($node.controlType)|$($node.name)|$($r.x),$($r.y),$($r.width),$($r.height)"
|
|
316
|
+
}
|
|
317
|
+
return ($material -join ';')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Screenshot.
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
function Get-WindowBitmap($hwnd) {
|
|
324
|
+
$rectInfo = Get-WindowRectInfo $hwnd
|
|
325
|
+
if ($rectInfo.width -le 0 -or $rectInfo.height -le 0) { throw "window $hwnd has no drawable rect" }
|
|
326
|
+
$bitmap = New-Object System.Drawing.Bitmap($rectInfo.width, $rectInfo.height)
|
|
327
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
328
|
+
try {
|
|
329
|
+
$graphics.CopyFromScreen($rectInfo.x, $rectInfo.y, 0, 0, (New-Object System.Drawing.Size($rectInfo.width, $rectInfo.height)))
|
|
330
|
+
} finally {
|
|
331
|
+
$graphics.Dispose()
|
|
332
|
+
}
|
|
333
|
+
return $bitmap
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function Get-PrimaryBitmap() {
|
|
337
|
+
$width = [DshClickWin32]::GetSystemMetrics(0) # SM_CXSCREEN
|
|
338
|
+
$height = [DshClickWin32]::GetSystemMetrics(1) # SM_CYSCREEN
|
|
339
|
+
$bitmap = New-Object System.Drawing.Bitmap($width, $height)
|
|
340
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
341
|
+
try {
|
|
342
|
+
$graphics.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size($width, $height)))
|
|
343
|
+
} finally {
|
|
344
|
+
$graphics.Dispose()
|
|
345
|
+
}
|
|
346
|
+
return $bitmap
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function Resize-BitmapIfNeeded($bitmap, $maxSide) {
|
|
350
|
+
$longest = [Math]::Max($bitmap.Width, $bitmap.Height)
|
|
351
|
+
if ($longest -le $maxSide) { return $bitmap }
|
|
352
|
+
$scale = $maxSide / $longest
|
|
353
|
+
$newWidth = [int][Math]::Max(1, [Math]::Round($bitmap.Width * $scale))
|
|
354
|
+
$newHeight = [int][Math]::Max(1, [Math]::Round($bitmap.Height * $scale))
|
|
355
|
+
$resized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
|
|
356
|
+
$graphics = [System.Drawing.Graphics]::FromImage($resized)
|
|
357
|
+
try {
|
|
358
|
+
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
359
|
+
$graphics.DrawImage($bitmap, 0, 0, $newWidth, $newHeight)
|
|
360
|
+
} finally {
|
|
361
|
+
$graphics.Dispose()
|
|
362
|
+
}
|
|
363
|
+
$bitmap.Dispose()
|
|
364
|
+
return $resized
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function Get-BitmapBase64($bitmap) {
|
|
368
|
+
$stream = New-Object System.IO.MemoryStream
|
|
369
|
+
try {
|
|
370
|
+
$bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
371
|
+
return [Convert]::ToBase64String($stream.ToArray())
|
|
372
|
+
} finally {
|
|
373
|
+
$stream.Dispose()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function Get-PixelColorText($bitmap, $x, $y) {
|
|
378
|
+
if ($x -lt 0 -or $y -lt 0 -or $x -ge $bitmap.Width -or $y -ge $bitmap.Height) { return $null }
|
|
379
|
+
$color = $bitmap.GetPixel($x, $y)
|
|
380
|
+
return "rgb($($color.R), $($color.G), $($color.B))"
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# Element lookup.
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
function Find-ElementByRuntimeId($windowElement, $runtimeIdText) {
|
|
387
|
+
$ids = @()
|
|
388
|
+
foreach ($part in ($runtimeIdText -split '\.')) { $ids += [int]$part }
|
|
389
|
+
if ($ids.Count -eq 0) { return $null }
|
|
390
|
+
$condition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::RuntimeIdProperty, $ids)
|
|
391
|
+
try {
|
|
392
|
+
return $windowElement.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition)
|
|
393
|
+
} catch {
|
|
394
|
+
return $null
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function Get-ElementCenter($element) {
|
|
399
|
+
$bounds = $element.Current.BoundingRectangle
|
|
400
|
+
if ($bounds.IsEmpty) { throw 'element has no bounding rectangle' }
|
|
401
|
+
return @{ x = [int]($bounds.X + $bounds.Width / 2); y = [int]($bounds.Y + $bounds.Height / 2) }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function Get-ClientPoint($windowRect, $screenX, $screenY) {
|
|
405
|
+
$cx = $screenX - $windowRect.x
|
|
406
|
+
$cy = $screenY - $windowRect.y
|
|
407
|
+
if ($cx -lt 0 -or $cy -lt 0 -or $cx -ge $windowRect.width -or $cy -ge $windowRect.height) {
|
|
408
|
+
throw "point ($screenX, $screenY) lies outside the target window"
|
|
409
|
+
}
|
|
410
|
+
return @{ x = $cx; y = $cy }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function Get-LParam($cx, $cy) {
|
|
414
|
+
return [IntPtr](($cy -shl 16) -bor ($cx -band 0xFFFF))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function Post-Click($hwnd, $cx, $cy, $button) {
|
|
418
|
+
$down = if ($button -eq 'right') { 0x204 } else { 0x201 }
|
|
419
|
+
$up = if ($button -eq 'right') { 0x205 } else { 0x202 }
|
|
420
|
+
$lparam = Get-LParam $cx $cy
|
|
421
|
+
[void][DshClickWin32]::PostMessage($hwnd, $down, [IntPtr]1, $lparam)
|
|
422
|
+
[void][DshClickWin32]::PostMessage($hwnd, $up, [IntPtr]0, $lparam)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function Get-KeyMap() {
|
|
426
|
+
$map = @{}
|
|
427
|
+
$map['ENTER'] = @(0x0D, $true)
|
|
428
|
+
$map['RETURN'] = @(0x0D, $true)
|
|
429
|
+
$map['TAB'] = @(0x09, $true)
|
|
430
|
+
$map['ESC'] = @(0x1B, $true)
|
|
431
|
+
$map['ESCAPE'] = @(0x1B, $true)
|
|
432
|
+
$map['BACKSPACE'] = @(0x08, $true)
|
|
433
|
+
$map['DELETE'] = @(0x2E, $true)
|
|
434
|
+
$map['DEL'] = @(0x2E, $true)
|
|
435
|
+
$map['SPACE'] = @(0x20, $true)
|
|
436
|
+
$map['INSERT'] = @(0x2D, $true)
|
|
437
|
+
$map['HOME'] = @(0x24, $true)
|
|
438
|
+
$map['END'] = @(0x23, $true)
|
|
439
|
+
$map['PAGEUP'] = @(0x21, $true)
|
|
440
|
+
$map['PGUP'] = @(0x21, $true)
|
|
441
|
+
$map['PAGEDOWN'] = @(0x22, $true)
|
|
442
|
+
$map['PGDN'] = @(0x22, $true)
|
|
443
|
+
$map['UP'] = @(0x26, $true)
|
|
444
|
+
$map['DOWN'] = @(0x28, $true)
|
|
445
|
+
$map['LEFT'] = @(0x25, $true)
|
|
446
|
+
$map['RIGHT'] = @(0x27, $true)
|
|
447
|
+
$map['CTRL'] = @(0x11, $false)
|
|
448
|
+
$map['CONTROL'] = @(0x11, $false)
|
|
449
|
+
$map['SHIFT'] = @(0x10, $false)
|
|
450
|
+
$map['ALT'] = @(0x12, $false)
|
|
451
|
+
$map['MENU'] = @(0x12, $false)
|
|
452
|
+
$map['WIN'] = @(0x5B, $false)
|
|
453
|
+
$map['WINDOWS'] = @(0x5B, $false)
|
|
454
|
+
foreach ($n in 1..12) { $map["F$n"] = @(0x6F + $n, $true) }
|
|
455
|
+
foreach ($c in 0..9) { $map["$c"] = @(0x30 + $c, $true) }
|
|
456
|
+
foreach ($c in 'A'..'Z') {
|
|
457
|
+
$code = [int][char]$c
|
|
458
|
+
$map[$c] = @(0x41 + ($code - 65), $true)
|
|
459
|
+
}
|
|
460
|
+
return $map
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function Post-KeyCombo($hwnd, $keysText) {
|
|
464
|
+
$map = Get-KeyMap
|
|
465
|
+
$tokens = @()
|
|
466
|
+
foreach ($token in ($keysText -split '\+')) {
|
|
467
|
+
$trimmed = $token.Trim()
|
|
468
|
+
if ($trimmed.Length -eq 0) { continue }
|
|
469
|
+
$tokens += $trimmed.ToUpperInvariant()
|
|
470
|
+
}
|
|
471
|
+
if ($tokens.Count -eq 0) { throw 'empty key combination' }
|
|
472
|
+
$modifiers = @()
|
|
473
|
+
$mainKeys = @()
|
|
474
|
+
foreach ($token in $tokens) {
|
|
475
|
+
if ($token -in @('CTRL', 'CONTROL', 'SHIFT', 'ALT', 'MENU', 'WIN', 'WINDOWS')) {
|
|
476
|
+
$modifiers += $token
|
|
477
|
+
} elseif ($map.ContainsKey($token)) {
|
|
478
|
+
$mainKeys += $token
|
|
479
|
+
} elseif ($token.Length -eq 1) {
|
|
480
|
+
$mainKeys += $token
|
|
481
|
+
} else {
|
|
482
|
+
throw "unknown key '$token'"
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
foreach ($modifier in $modifiers) {
|
|
486
|
+
$entry = $map[$modifier]
|
|
487
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$entry[0], [IntPtr]0)
|
|
488
|
+
}
|
|
489
|
+
foreach ($key in $mainKeys) {
|
|
490
|
+
$isSpecial = $map.ContainsKey($key)
|
|
491
|
+
if ($isSpecial) {
|
|
492
|
+
$entry = $map[$key]
|
|
493
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$entry[0], [IntPtr]0)
|
|
494
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$entry[0], [IntPtr]0)
|
|
495
|
+
} else {
|
|
496
|
+
$charCode = [int][char]$key
|
|
497
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$charCode, [IntPtr]0)
|
|
498
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x102, [IntPtr]$charCode, [IntPtr]0)
|
|
499
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$charCode, [IntPtr]0)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
for ($i = $modifiers.Count - 1; $i -ge 0; $i--) {
|
|
503
|
+
$entry = $map[$modifiers[$i]]
|
|
504
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$entry[0], [IntPtr]0)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# ---------------------------------------------------------------------------
|
|
509
|
+
# Operations.
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
function Invoke-OpWindows {
|
|
512
|
+
$windows = @()
|
|
513
|
+
foreach ($hwnd in [DshClickWin32]::EnumVisibleWindows()) {
|
|
514
|
+
$pidValue = 0
|
|
515
|
+
[void][DshClickWin32]::GetWindowThreadProcessId($hwnd, [ref]$pidValue)
|
|
516
|
+
$windows += @{
|
|
517
|
+
windowId = [int64]$hwnd
|
|
518
|
+
processId = if ([int]$pidValue -gt 0) { [int]$pidValue } else { $null }
|
|
519
|
+
title = [DshClickWin32]::WindowTitle($hwnd)
|
|
520
|
+
className = [DshClickWin32]::WindowClass($hwnd)
|
|
521
|
+
rect = (Get-WindowRectInfo $hwnd)
|
|
522
|
+
executablePath = $null
|
|
523
|
+
visible = $true
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
# Attach executable paths per window (one handle open per unique pid).
|
|
527
|
+
$pidCache = @{}
|
|
528
|
+
foreach ($window in $windows) {
|
|
529
|
+
if ($window.processId -eq $null) { continue }
|
|
530
|
+
if (-not $pidCache.ContainsKey($window.processId)) {
|
|
531
|
+
$facts = Get-ProcessFacts ([IntPtr][int64]$window.windowId)
|
|
532
|
+
$pidCache[$window.processId] = $facts.executablePath
|
|
533
|
+
}
|
|
534
|
+
$window.executablePath = $pidCache[$window.processId]
|
|
535
|
+
}
|
|
536
|
+
return $windows
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function Invoke-OpApps {
|
|
540
|
+
$byPid = @{}
|
|
541
|
+
foreach ($hwnd in [DshClickWin32]::EnumVisibleWindows()) {
|
|
542
|
+
$pidValue = 0
|
|
543
|
+
[void][DshClickWin32]::GetWindowThreadProcessId($hwnd, [ref]$pidValue)
|
|
544
|
+
$pidInt = [int]$pidValue
|
|
545
|
+
if ($pidInt -le 0) { continue }
|
|
546
|
+
if (-not $byPid.ContainsKey($pidInt)) { $byPid[$pidInt] = @() }
|
|
547
|
+
$byPid[$pidInt] += @{
|
|
548
|
+
windowId = [int64]$hwnd
|
|
549
|
+
processId = $pidInt
|
|
550
|
+
title = [DshClickWin32]::WindowTitle($hwnd)
|
|
551
|
+
className = [DshClickWin32]::WindowClass($hwnd)
|
|
552
|
+
rect = (Get-WindowRectInfo $hwnd)
|
|
553
|
+
executablePath = $null
|
|
554
|
+
visible = $true
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
$apps = @()
|
|
558
|
+
foreach ($pidInt in ($byPid.Keys | Sort-Object)) {
|
|
559
|
+
$windows = $byPid[$pidInt]
|
|
560
|
+
$facts = Get-ProcessFacts ([IntPtr][int64]$windows[0].windowId)
|
|
561
|
+
$name = ''
|
|
562
|
+
try {
|
|
563
|
+
$process = Get-Process -Id $pidInt -ErrorAction Stop
|
|
564
|
+
$name = [string]$process.ProcessName
|
|
565
|
+
} catch { }
|
|
566
|
+
foreach ($window in $windows) { $window.executablePath = $facts.executablePath }
|
|
567
|
+
$apps += @{
|
|
568
|
+
processId = $pidInt
|
|
569
|
+
name = $name
|
|
570
|
+
executablePath = $facts.executablePath
|
|
571
|
+
windows = $windows
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return $apps
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function Get-SnapshotRecord($hwnd, $maxElements, $maxDepth) {
|
|
578
|
+
$facts = Get-ProcessFacts $hwnd
|
|
579
|
+
$rect = Get-WindowRectInfo $hwnd
|
|
580
|
+
$foreground = ([DshClickWin32]::GetForegroundWindow() -eq $hwnd)
|
|
581
|
+
$script:TreeNodes = @()
|
|
582
|
+
$element = Get-UiaElement $hwnd
|
|
583
|
+
Walk-UiaTree $element 0 $maxDepth $maxElements $false
|
|
584
|
+
$treeHash = Get-StringHash (Get-TreeHashText $script:TreeNodes)
|
|
585
|
+
$bitmap = Get-WindowBitmap $hwnd
|
|
586
|
+
$shotHash = $null
|
|
587
|
+
try {
|
|
588
|
+
$shotHash = Get-BitmapHash $bitmap
|
|
589
|
+
} finally {
|
|
590
|
+
$bitmap.Dispose()
|
|
591
|
+
}
|
|
592
|
+
return @{
|
|
593
|
+
windowId = [int64]$hwnd
|
|
594
|
+
processId = $facts.pid
|
|
595
|
+
executablePath = $facts.executablePath
|
|
596
|
+
title = [DshClickWin32]::WindowTitle($hwnd)
|
|
597
|
+
className = [DshClickWin32]::WindowClass($hwnd)
|
|
598
|
+
rect = $rect
|
|
599
|
+
foreground = $foreground
|
|
600
|
+
treeHash = $treeHash
|
|
601
|
+
shotHash = $shotHash
|
|
602
|
+
elementCount = $script:TreeNodes.Count
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function Invoke-OpSnapshot($opArgs) {
|
|
607
|
+
$hwnd = Resolve-Window @{ windowId = $opArgs.windowId }
|
|
608
|
+
$maxElements = if ($opArgs.maxElements -ne $null) { [int]$opArgs.maxElements } else { 500 }
|
|
609
|
+
$maxDepth = if ($opArgs.maxDepth -ne $null) { [int]$opArgs.maxDepth } else { 32 }
|
|
610
|
+
return (Get-SnapshotRecord $hwnd $maxElements $maxDepth)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function Invoke-OpShot($opArgs) {
|
|
614
|
+
$target = if ($opArgs.target -ne $null) { $opArgs.target } else { @{} }
|
|
615
|
+
$maxSide = if ($opArgs.maxSide -ne $null) { [int]$opArgs.maxSide } else { 2560 }
|
|
616
|
+
$maxElements = if ($opArgs.maxElements -ne $null) { [int]$opArgs.maxElements } else { 500 }
|
|
617
|
+
$maxDepth = if ($opArgs.maxDepth -ne $null) { [int]$opArgs.maxDepth } else { 32 }
|
|
618
|
+
$hasTarget = ($target.windowId -ne $null) -or ($target.windowTitle -ne $null) -or ($target.processId -ne $null)
|
|
619
|
+
if ($hasTarget) {
|
|
620
|
+
$hwnd = Resolve-Window $target
|
|
621
|
+
$bitmap = Get-WindowBitmap $hwnd
|
|
622
|
+
$snapshot = Get-SnapshotRecord $hwnd $maxElements $maxDepth
|
|
623
|
+
} else {
|
|
624
|
+
$hwnd = $null
|
|
625
|
+
$bitmap = Get-PrimaryBitmap
|
|
626
|
+
$snapshot = @{
|
|
627
|
+
windowId = 0; processId = 0; executablePath = $null
|
|
628
|
+
title = 'primary screen'; className = 'Screen'
|
|
629
|
+
rect = @{ x = 0; y = 0; width = $bitmap.Width; height = $bitmap.Height }
|
|
630
|
+
foreground = $true; treeHash = ''; shotHash = ''; elementCount = 0
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
$bitmap = Resize-BitmapIfNeeded $bitmap $maxSide
|
|
635
|
+
$base64 = Get-BitmapBase64 $bitmap
|
|
636
|
+
return @{
|
|
637
|
+
pngBase64 = $base64
|
|
638
|
+
width = $bitmap.Width
|
|
639
|
+
height = $bitmap.Height
|
|
640
|
+
snapshot = $snapshot
|
|
641
|
+
}
|
|
642
|
+
} finally {
|
|
643
|
+
$bitmap.Dispose()
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function Invoke-OpTree($opArgs) {
|
|
648
|
+
$target = if ($opArgs.target -ne $null) { $opArgs.target } else { @{} }
|
|
649
|
+
$maxElements = if ($opArgs.maxElements -ne $null) { [int]$opArgs.maxElements } else { 500 }
|
|
650
|
+
$maxDepth = if ($opArgs.maxDepth -ne $null) { [int]$opArgs.maxDepth } else { 32 }
|
|
651
|
+
$includePixels = if ($opArgs.includePixels -ne $null) { [bool]$opArgs.includePixels } else { $true }
|
|
652
|
+
$hwnd = Resolve-Window $target
|
|
653
|
+
$snapshot = Get-SnapshotRecord $hwnd $maxElements $maxDepth
|
|
654
|
+
$script:TreeNodes = @()
|
|
655
|
+
$element = Get-UiaElement $hwnd
|
|
656
|
+
Walk-UiaTree $element 0 $maxDepth $maxElements $includePixels
|
|
657
|
+
$pixels = @()
|
|
658
|
+
if ($includePixels) {
|
|
659
|
+
$bitmap = Get-WindowBitmap $hwnd
|
|
660
|
+
try {
|
|
661
|
+
$rectInfo = Get-WindowRectInfo $hwnd
|
|
662
|
+
$count = 0
|
|
663
|
+
foreach ($node in $script:TreeNodes) {
|
|
664
|
+
if ($count -ge 200) { break }
|
|
665
|
+
$r = $node.rect
|
|
666
|
+
if ($r.width -le 0 -or $r.height -le 0) { continue }
|
|
667
|
+
$cx = $r.x + [int]($r.width / 2) - $rectInfo.x
|
|
668
|
+
$cy = $r.y + [int]($r.height / 2) - $rectInfo.y
|
|
669
|
+
$color = Get-PixelColorText $bitmap $cx $cy
|
|
670
|
+
if ($color -ne $null) {
|
|
671
|
+
$label = if ($node.name -ne '') { $node.name } else { "$($node.controlType)[$($node.elementId)]" }
|
|
672
|
+
$pixels += @{ label = $label; x = $r.x + [int]($r.width / 2); y = $r.y + [int]($r.height / 2); color = $color }
|
|
673
|
+
$count += 1
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} finally {
|
|
677
|
+
$bitmap.Dispose()
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return @{
|
|
681
|
+
snapshot = $snapshot
|
|
682
|
+
elements = @($script:TreeNodes)
|
|
683
|
+
pixels = $pixels
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function Get-ActionOutcome($hwnd, $action, $delivered, $restored, $detail) {
|
|
688
|
+
$before = Get-ProcessFacts $hwnd
|
|
689
|
+
$after = Get-ProcessFacts $hwnd
|
|
690
|
+
$outcome = @{
|
|
691
|
+
windowId = [int64]$hwnd
|
|
692
|
+
action = $action
|
|
693
|
+
delivered = $delivered
|
|
694
|
+
processBefore = $before
|
|
695
|
+
processAfter = $after
|
|
696
|
+
}
|
|
697
|
+
if ($restored -ne $null) { $outcome.restored = [bool]$restored }
|
|
698
|
+
if ($detail -ne $null) { $outcome.detail = [string]$detail }
|
|
699
|
+
return $outcome
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function Invoke-OpClick($opArgs) {
|
|
703
|
+
$request = $opArgs.request
|
|
704
|
+
$focusFallback = if ($opArgs.focusFallback -ne $null) { [bool]$opArgs.focusFallback } else { $false }
|
|
705
|
+
$hwnd = Resolve-Window @{ windowId = $request.windowId }
|
|
706
|
+
if ($focusFallback) { [void][DshClickWin32]::SetForegroundWindow($hwnd) }
|
|
707
|
+
$windowRect = Get-WindowRectInfo $hwnd
|
|
708
|
+
$delivered = 'posted'
|
|
709
|
+
$windowElement = Get-UiaElement $hwnd
|
|
710
|
+
if ($request.elementId -ne $null -and $request.elementId -is [string] -and $request.elementId.Length -gt 0) {
|
|
711
|
+
$element = Find-ElementByRuntimeId $windowElement ([string]$request.elementId)
|
|
712
|
+
if ($element -eq $null) { throw "element '$($request.elementId)' not found in window $hwnd (re-run screen_read)" }
|
|
713
|
+
$invoke = $null
|
|
714
|
+
if ($element.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$invoke)) {
|
|
715
|
+
$invoke.Invoke()
|
|
716
|
+
$delivered = 'uia'
|
|
717
|
+
} else {
|
|
718
|
+
$center = Get-ElementCenter $element
|
|
719
|
+
$client = Get-ClientPoint $windowRect $center.x $center.y
|
|
720
|
+
Post-Click $hwnd $client.x $client.y ([string]$request.button)
|
|
721
|
+
$delivered = 'posted'
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
if ($request.x -eq $null -or $request.y -eq $null) { throw 'click requires elementId or (x, y)' }
|
|
725
|
+
$client = Get-ClientPoint $windowRect ([int]$request.x) ([int]$request.y)
|
|
726
|
+
Post-Click $hwnd $client.x $client.y ([string]$request.button)
|
|
727
|
+
$delivered = 'posted'
|
|
728
|
+
}
|
|
729
|
+
return (Get-ActionOutcome $hwnd 'click' $delivered $null $null)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function Invoke-OpType($opArgs) {
|
|
733
|
+
$request = $opArgs.request
|
|
734
|
+
$focusFallback = if ($opArgs.focusFallback -ne $null) { [bool]$opArgs.focusFallback } else { $false }
|
|
735
|
+
$rollback = if ($request.rollback -ne $null) { [bool]$request.rollback } else { $true }
|
|
736
|
+
$hwnd = Resolve-Window @{ windowId = $request.windowId }
|
|
737
|
+
if ($focusFallback) { [void][DshClickWin32]::SetForegroundWindow($hwnd) }
|
|
738
|
+
$windowElement = Get-UiaElement $hwnd
|
|
739
|
+
$element = Find-ElementByRuntimeId $windowElement ([string]$request.elementId)
|
|
740
|
+
if ($element -eq $null) { throw "element '$($request.elementId)' not found in window $hwnd (re-run screen_read)" }
|
|
741
|
+
$value = $null
|
|
742
|
+
if ($element.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$value)) {
|
|
743
|
+
$original = [string]$value.Current.Value
|
|
744
|
+
try {
|
|
745
|
+
$value.SetValue([string]$request.text)
|
|
746
|
+
return (Get-ActionOutcome $hwnd 'type' 'uia' $false $null)
|
|
747
|
+
} catch {
|
|
748
|
+
$restored = $false
|
|
749
|
+
if ($rollback) {
|
|
750
|
+
try {
|
|
751
|
+
$value.SetValue($original)
|
|
752
|
+
$restored = $true
|
|
753
|
+
} catch { }
|
|
754
|
+
}
|
|
755
|
+
throw "type failed and rollback $(if ($restored) { 'restored the original text' } else { 'could NOT restore the original text' }): $($_.Exception.Message)"
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if ($focusFallback) {
|
|
759
|
+
foreach ($ch in [char[]][string]$request.text) {
|
|
760
|
+
$charCode = [int]$ch
|
|
761
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x102, [IntPtr]$charCode, [IntPtr]0)
|
|
762
|
+
}
|
|
763
|
+
return (Get-ActionOutcome $hwnd 'type' 'posted' $null 'typed via posted WM_CHAR (no value pattern on the element)')
|
|
764
|
+
}
|
|
765
|
+
throw "element '$($request.elementId)' exposes no value pattern and focusFallback is disabled — refusing to type into it"
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function Invoke-OpScroll($opArgs) {
|
|
769
|
+
$request = $opArgs.request
|
|
770
|
+
$focusFallback = if ($opArgs.focusFallback -ne $null) { [bool]$opArgs.focusFallback } else { $false }
|
|
771
|
+
$amount = if ($request.amount -ne $null) { [int]$request.amount } else { 3 }
|
|
772
|
+
if ($amount -lt 1) { throw 'scroll amount must be positive' }
|
|
773
|
+
$hwnd = Resolve-Window @{ windowId = $request.windowId }
|
|
774
|
+
$windowElement = Get-UiaElement $hwnd
|
|
775
|
+
$scroll = $null
|
|
776
|
+
$element = $null
|
|
777
|
+
if ($request.elementId -ne $null -and $request.elementId -is [string] -and $request.elementId.Length -gt 0) {
|
|
778
|
+
$element = Find-ElementByRuntimeId $windowElement ([string]$request.elementId)
|
|
779
|
+
if ($element -eq $null) { throw "element '$($request.elementId)' not found in window $hwnd (re-run screen_read)" }
|
|
780
|
+
if (-not $element.TryGetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern, [ref]$scroll)) {
|
|
781
|
+
$scroll = $null
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if ($scroll -ne $null) {
|
|
785
|
+
if ($focusFallback) { [void][DshClickWin32]::SetForegroundWindow($hwnd) }
|
|
786
|
+
$vertical = [System.Windows.Automation.ScrollAmount]::NoAmount
|
|
787
|
+
switch ([string]$request.direction) {
|
|
788
|
+
'up' { $vertical = [System.Windows.Automation.ScrollAmount]::SmallIncrement }
|
|
789
|
+
'down' { $vertical = [System.Windows.Automation.ScrollAmount]::SmallDecrement }
|
|
790
|
+
'page-up' { $vertical = [System.Windows.Automation.ScrollAmount]::LargeIncrement }
|
|
791
|
+
'page-down' { $vertical = [System.Windows.Automation.ScrollAmount]::LargeDecrement }
|
|
792
|
+
default { throw "unknown scroll direction '$($request.direction)'" }
|
|
793
|
+
}
|
|
794
|
+
for ($i = 0; $i -lt $amount; $i++) {
|
|
795
|
+
$scroll.Scroll([System.Windows.Automation.ScrollAmount]::NoAmount, $vertical)
|
|
796
|
+
}
|
|
797
|
+
return (Get-ActionOutcome $hwnd 'scroll' 'uia' $null $null)
|
|
798
|
+
}
|
|
799
|
+
if ($focusFallback) { [void][DshClickWin32]::SetForegroundWindow($hwnd) }
|
|
800
|
+
$direction = [string]$request.direction
|
|
801
|
+
$page = ($direction -eq 'page-up' -or $direction -eq 'page-down')
|
|
802
|
+
$sign = if ($direction -eq 'up' -or $direction -eq 'page-up') { 1 } else { -1 }
|
|
803
|
+
$notches = if ($page) { 3 } else { 1 }
|
|
804
|
+
$delta = [int]($sign * $notches * $amount * 120)
|
|
805
|
+
$wparam = [IntPtr][int64]($delta * 65536)
|
|
806
|
+
$rect = Get-WindowRectInfo $hwnd
|
|
807
|
+
$client = Get-ClientPoint $rect ([int]($rect.x + $rect.width / 2)) ([int]($rect.y + $rect.height / 2))
|
|
808
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x20A, $wparam, (Get-LParam $client.x $client.y))
|
|
809
|
+
return (Get-ActionOutcome $hwnd 'scroll' 'posted' $null 'posted wheel message to the window')
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function Invoke-OpKey($opArgs) {
|
|
813
|
+
$request = $opArgs.request
|
|
814
|
+
$focusFallback = if ($opArgs.focusFallback -ne $null) { [bool]$opArgs.focusFallback } else { $false }
|
|
815
|
+
$hwnd = Resolve-Window @{ windowId = $request.windowId }
|
|
816
|
+
if ($focusFallback) { [void][DshClickWin32]::SetForegroundWindow($hwnd) }
|
|
817
|
+
Post-KeyCombo $hwnd ([string]$request.keys)
|
|
818
|
+
return (Get-ActionOutcome $hwnd 'key' 'posted' $null $null)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function Invoke-OpLaunch($opArgs) {
|
|
822
|
+
$name = [string]$opArgs.name
|
|
823
|
+
$launchArgs = @()
|
|
824
|
+
if ($opArgs.args -ne $null) {
|
|
825
|
+
foreach ($item in @($opArgs.args)) { $launchArgs += [string]$item }
|
|
826
|
+
}
|
|
827
|
+
$target = $name
|
|
828
|
+
if ($name -notmatch '[\\/]' -and $name -notmatch '\.exe$') {
|
|
829
|
+
$command = Get-Command $name -ErrorAction SilentlyContinue
|
|
830
|
+
if ($command -ne $null) { $target = $command.Source } else { throw "cannot resolve application '$name' on the search path" }
|
|
831
|
+
}
|
|
832
|
+
$process = Start-Process -FilePath $target -ArgumentList $launchArgs -PassThru
|
|
833
|
+
$path = $null
|
|
834
|
+
try {
|
|
835
|
+
$path = $process.Path
|
|
836
|
+
} catch {
|
|
837
|
+
$path = $null
|
|
838
|
+
}
|
|
839
|
+
return @{ processId = [int]$process.Id; executablePath = $path }
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
# ---------------------------------------------------------------------------
|
|
843
|
+
# Entry point.
|
|
844
|
+
# ---------------------------------------------------------------------------
|
|
845
|
+
try {
|
|
846
|
+
$raw = [Console]::In.ReadToEnd()
|
|
847
|
+
$request = $raw | ConvertFrom-Json
|
|
848
|
+
$opArgs = if ($request.args -ne $null) { $request.args } else { @{} }
|
|
849
|
+
$result = $null
|
|
850
|
+
switch ([string]$request.op) {
|
|
851
|
+
'windows' { $result = Invoke-OpWindows }
|
|
852
|
+
'apps' { $result = Invoke-OpApps }
|
|
853
|
+
'snapshot' { $result = Invoke-OpSnapshot $opArgs }
|
|
854
|
+
'shot' { $result = Invoke-OpShot $opArgs }
|
|
855
|
+
'tree' { $result = Invoke-OpTree $opArgs }
|
|
856
|
+
'click' { $result = Invoke-OpClick $opArgs }
|
|
857
|
+
'type' { $result = Invoke-OpType $opArgs }
|
|
858
|
+
'scroll' { $result = Invoke-OpScroll $opArgs }
|
|
859
|
+
'key' { $result = Invoke-OpKey $opArgs }
|
|
860
|
+
'launch' { $result = Invoke-OpLaunch $opArgs }
|
|
861
|
+
default { throw "unknown op '$($request.op)'" }
|
|
862
|
+
}
|
|
863
|
+
Write-DshResponse @{ ok = $true; result = $result }
|
|
864
|
+
exit 0
|
|
865
|
+
} catch {
|
|
866
|
+
$message = $_.Exception.Message
|
|
867
|
+
if ($message -eq $null -or $message.Length -eq 0) { $message = 'unknown helper failure' }
|
|
868
|
+
Write-DshError 'HELPER_ERROR' $message
|
|
869
|
+
exit 0
|
|
870
|
+
}
|