bridgerator 1.0.7 → 1.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/README.md +78 -15
- package/assets/bridgerator.ico +0 -0
- package/assets/icon-16.png +0 -0
- package/assets/icon-20.png +0 -0
- package/assets/icon-24.png +0 -0
- package/assets/icon-256.png +0 -0
- package/assets/icon-32.png +0 -0
- package/assets/icon-48.png +0 -0
- package/assets/icon-64.png +0 -0
- package/assets/logo.svg +1 -0
- package/install.ps1 +465 -43
- package/npm-shrinkwrap.json +8621 -0
- package/package.json +55 -49
- package/recipes/backup-folder.json +31 -0
- package/recipes/clean-downloads.json +48 -0
- package/recipes/disk-report.json +66 -0
- package/recipes/sort-folder.json +49 -0
- package/recipes/update-apps.json +25 -0
- package/src/ask-lib.ps1 +230 -0
- package/src/ask.mjs +51 -0
- package/src/ask.ps1 +17 -0
- package/src/automations.mjs +673 -0
- package/src/batch.mjs +221 -0
- package/src/bridge.mjs +743 -29
- package/src/cli.mjs +561 -53
- package/src/confirm.mjs +418 -0
- package/src/dc-config-hooks.mjs +18 -8
- package/src/dc-isolate.mjs +5 -0
- package/src/delegate.mjs +555 -0
- package/src/docs-tool.mjs +494 -0
- package/src/doctor.mjs +200 -0
- package/src/events.mjs +21 -0
- package/src/extra-tools.mjs +122 -0
- package/src/guard.mjs +34 -0
- package/src/notice.mjs +86 -23
- package/src/pair.mjs +41 -6
- package/src/pc-check.mjs +112 -0
- package/src/pc-check.ps1 +103 -0
- package/src/pc-text.mjs +94 -0
- package/src/pdf-font.mjs +245 -0
- package/src/photos-tool.mjs +187 -0
- package/src/photos.ps1 +145 -0
- package/src/plan.mjs +90 -0
- package/src/policy.mjs +293 -109
- package/src/queue.mjs +194 -0
- package/src/recent.mjs +48 -0
- package/src/recipes.mjs +108 -0
- package/src/recycle.mjs +185 -0
- package/src/recycle.ps1 +110 -0
- package/src/screen-tool.mjs +185 -0
- package/src/screen.ps1 +285 -0
- package/src/search-tool.mjs +189 -0
- package/src/search.ps1 +129 -0
- package/src/settings-tool.mjs +186 -0
- package/src/setup-tool.mjs +129 -0
- package/src/software-tool.mjs +185 -0
- package/src/telemetry.mjs +86 -0
- package/src/tool-lib.mjs +177 -0
- package/src/tool-text.mjs +45 -0
- package/src/tray.mjs +97 -0
- package/src/tray.ps1 +359 -0
- package/src/undo.mjs +313 -0
- package/src/win.mjs +17 -0
- package/ui/app.css +388 -0
- package/ui/app.js +868 -0
- package/ui/i18n/en.json +598 -0
- package/ui/i18n/ru.json +598 -0
- package/ui/index.html +26 -0
- package/vendor/webview2/LICENSE.txt +27 -0
- package/vendor/webview2/Microsoft.Web.WebView2.Core.dll +0 -0
- package/vendor/webview2/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/vendor/webview2/NOTICE.txt +85 -0
- package/vendor/webview2/SHA256SUMS.txt +4 -0
- package/vendor/webview2/SOURCE.txt +1 -0
- package/vendor/webview2/arm64/WebView2Loader.dll +0 -0
- package/vendor/webview2/x64/WebView2Loader.dll +0 -0
package/src/screen.ps1
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# LOGIC: see critical-logic/bridge.md ("Extra tools", screen). Screenshot / window list / OCR for the
|
|
2
|
+
# `screen` tool (screen-tool.mjs). Windows PowerShell 5.1, no extra programs: user32 + System.Drawing for
|
|
3
|
+
# the picture, the built-in Windows.Media.Ocr engine for text. Input only through BRX_* environment
|
|
4
|
+
# variables; output = ONE JSON line. Never shows a window, never moves or restores the person's windows.
|
|
5
|
+
# BRX_ACTION windows | capture | ocr | langs
|
|
6
|
+
# BRX_HWND window id (capture / ocr of one window; BRX_PID = its process then, must still match), BRX_MONITOR 1-based monitor (0 = all)
|
|
7
|
+
# BRX_FILE image file to recognize (ocr), BRX_LANG OCR language tag (empty = the user's languages)
|
|
8
|
+
# BRX_IMAGE 1 = return the compressed picture, BRX_MAXSIDE / BRX_MAXBYTES its limits, BRX_SAVE full-size copy path
|
|
9
|
+
|
|
10
|
+
function Out-Json($o) { [Console]::Out.WriteLine((ConvertTo-Json -InputObject $o -Depth 6 -Compress)) }
|
|
11
|
+
# taken = a picture was already made: the bridge shows the screenshot notice even when a later step fails.
|
|
12
|
+
$script:taken = $false
|
|
13
|
+
function Fail([string]$code, [string]$message, $extra = @{}) { $o = @{ ok = $false; code = $code; message = $message }; if ($script:taken) { $o.taken = $true }; foreach ($k in $extra.Keys) { $o[$k] = $extra[$k] }; Out-Json $o; exit 0 }
|
|
14
|
+
|
|
15
|
+
# Compiled helpers are cached like the tray helper's (%LOCALAPPDATA%\Bridgerator\cache, br-<hash>.dll).
|
|
16
|
+
function Add-CachedType([string]$code, [string[]]$refs = @()) {
|
|
17
|
+
try {
|
|
18
|
+
$dir = Join-Path $env:LOCALAPPDATA 'Bridgerator\cache'
|
|
19
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes("$code|$($refs -join ',')|$($PSVersionTable.CLRVersion)")
|
|
20
|
+
$key = -join ([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)[0..11] | ForEach-Object { $_.ToString('x2') })
|
|
21
|
+
$dll = Join-Path $dir "br-$key.dll"
|
|
22
|
+
if (-not [IO.File]::Exists($dll)) {
|
|
23
|
+
[void][IO.Directory]::CreateDirectory($dir)
|
|
24
|
+
$tmp = Join-Path $dir "br-$key.$PID.dll"
|
|
25
|
+
Add-Type -TypeDefinition $code -ReferencedAssemblies $refs -OutputAssembly $tmp -OutputType Library
|
|
26
|
+
try { [IO.File]::Move($tmp, $dll) } catch {
|
|
27
|
+
# Another run compiled the same helper at the same moment: use its file, drop ours.
|
|
28
|
+
if ([IO.File]::Exists($dll)) { try { [IO.File]::Delete($tmp) } catch { } } else { $dll = $tmp }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
Add-Type -Path $dll
|
|
32
|
+
} catch {
|
|
33
|
+
Add-Type -TypeDefinition $code -ReferencedAssemblies $refs
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Add-CachedType -refs System.Drawing @'
|
|
38
|
+
using System; using System.Text; using System.Collections.Generic; using System.Runtime.InteropServices;
|
|
39
|
+
using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.IO;
|
|
40
|
+
public static class BrShot {
|
|
41
|
+
delegate bool EnumProc(IntPtr h, IntPtr l);
|
|
42
|
+
delegate bool MonProc(IntPtr m, IntPtr dc, ref RECT r, IntPtr l);
|
|
43
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
|
|
44
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc f, IntPtr l);
|
|
45
|
+
[DllImport("user32.dll")] static extern bool EnumDisplayMonitors(IntPtr dc, IntPtr clip, MonProc f, IntPtr l);
|
|
46
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
|
|
47
|
+
[DllImport("user32.dll")] static extern bool IsWindow(IntPtr h);
|
|
48
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr h);
|
|
49
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
50
|
+
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
|
|
51
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
52
|
+
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int idx);
|
|
53
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
54
|
+
[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
|
|
55
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out int v, int size);
|
|
56
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int a, out RECT r, int size);
|
|
57
|
+
[DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags, bool inherit, uint access);
|
|
58
|
+
[DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr h);
|
|
59
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool GetUserObjectInformation(IntPtr h, int idx, StringBuilder buf, int len, out int needed);
|
|
60
|
+
[DllImport("user32.dll")] static extern IntPtr SetThreadDpiAwarenessContext(IntPtr c);
|
|
61
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
62
|
+
|
|
63
|
+
// Physical pixels on every monitor (per-monitor v2; older Windows: system aware).
|
|
64
|
+
public static void Dpi() {
|
|
65
|
+
try { if (SetThreadDpiAwarenessContext(new IntPtr(-4)) != IntPtr.Zero) return; } catch (Exception) { }
|
|
66
|
+
try { SetProcessDPIAware(); } catch (Exception) { }
|
|
67
|
+
}
|
|
68
|
+
// The desktop that gets the keyboard: "Default" when the person is working; locked screen, sign-in
|
|
69
|
+
// screen and UAC prompts are on the secure desktop (not readable from here) -> null.
|
|
70
|
+
public static string InputDesktop() {
|
|
71
|
+
IntPtr d = OpenInputDesktop(0, false, 0x0001);
|
|
72
|
+
if (d == IntPtr.Zero) return null;
|
|
73
|
+
try { StringBuilder b = new StringBuilder(256); int n; return GetUserObjectInformation(d, 2, b, b.Capacity * 2, out n) ? b.ToString() : null; }
|
|
74
|
+
finally { CloseDesktop(d); }
|
|
75
|
+
}
|
|
76
|
+
public class Win { public long Id; public string Title; public uint Pid; public bool Minimized; public int W, H; }
|
|
77
|
+
public static List<Win> Windows() {
|
|
78
|
+
List<Win> list = new List<Win>();
|
|
79
|
+
EnumWindows(delegate(IntPtr h, IntPtr l) {
|
|
80
|
+
if (!IsWindowVisible(h)) return true;
|
|
81
|
+
int len = GetWindowTextLength(h);
|
|
82
|
+
if (len <= 0) return true;
|
|
83
|
+
if ((GetWindowLong(h, -20) & 0x80) != 0) return true; // WS_EX_TOOLWINDOW
|
|
84
|
+
int cloaked = 0;
|
|
85
|
+
if (DwmGetWindowAttribute(h, 14, out cloaked, 4) == 0 && cloaked != 0) return true; // hidden store apps
|
|
86
|
+
StringBuilder sb = new StringBuilder(len + 1); GetWindowText(h, sb, sb.Capacity);
|
|
87
|
+
RECT r; GetWindowRect(h, out r);
|
|
88
|
+
uint pid; GetWindowThreadProcessId(h, out pid);
|
|
89
|
+
Win w = new Win(); w.Id = h.ToInt64(); w.Title = sb.ToString(); w.Pid = pid; w.Minimized = IsIconic(h); w.W = r.R - r.L; w.H = r.B - r.T;
|
|
90
|
+
if (!w.Minimized && (w.W <= 1 || w.H <= 1)) return true;
|
|
91
|
+
list.Add(w);
|
|
92
|
+
return true;
|
|
93
|
+
}, IntPtr.Zero);
|
|
94
|
+
return list;
|
|
95
|
+
}
|
|
96
|
+
public static Rectangle[] Monitors() {
|
|
97
|
+
List<Rectangle> m = new List<Rectangle>();
|
|
98
|
+
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, delegate(IntPtr hm, IntPtr dc, ref RECT r, IntPtr l) { m.Add(Rectangle.FromLTRB(r.L, r.T, r.R, r.B)); return true; }, IntPtr.Zero);
|
|
99
|
+
return m.ToArray();
|
|
100
|
+
}
|
|
101
|
+
public static Bitmap Screen(int monitor) {
|
|
102
|
+
Rectangle[] mons = Monitors();
|
|
103
|
+
Rectangle b;
|
|
104
|
+
if (monitor > 0) { if (monitor > mons.Length) return null; b = mons[monitor - 1]; }
|
|
105
|
+
else { b = mons.Length > 0 ? mons[0] : Rectangle.Empty; foreach (Rectangle x in mons) b = Rectangle.Union(b, x); }
|
|
106
|
+
if (b.Width <= 0 || b.Height <= 0) return null;
|
|
107
|
+
Bitmap bmp = new Bitmap(b.Width, b.Height, PixelFormat.Format32bppArgb);
|
|
108
|
+
using (Graphics g = Graphics.FromImage(bmp)) { g.Clear(Color.Black); g.CopyFromScreen(b.Left, b.Top, 0, 0, b.Size, CopyPixelOperation.SourceCopy); }
|
|
109
|
+
return bmp;
|
|
110
|
+
}
|
|
111
|
+
public static bool IsMinimized(long id) { return IsIconic(new IntPtr(id)); }
|
|
112
|
+
public static uint Owner(long id) { uint pid; GetWindowThreadProcessId(new IntPtr(id), out pid); return pid; }
|
|
113
|
+
public static bool Exists(long id) { IntPtr h = new IntPtr(id); return IsWindow(h) && IsWindowVisible(h); }
|
|
114
|
+
// PrintWindow with PW_RENDERFULLCONTENT: the window's own picture, also when other windows cover it.
|
|
115
|
+
// Cropped to the visible frame (without the invisible resize borders).
|
|
116
|
+
public static Bitmap Window(long id) {
|
|
117
|
+
IntPtr h = new IntPtr(id);
|
|
118
|
+
RECT r; if (!GetWindowRect(h, out r)) return null;
|
|
119
|
+
int w = r.R - r.L, ht = r.B - r.T;
|
|
120
|
+
if (w <= 1 || ht <= 1) return null;
|
|
121
|
+
Bitmap full = new Bitmap(w, ht, PixelFormat.Format32bppArgb);
|
|
122
|
+
using (Graphics g = Graphics.FromImage(full)) {
|
|
123
|
+
IntPtr hdc = g.GetHdc();
|
|
124
|
+
bool ok;
|
|
125
|
+
try { ok = PrintWindow(h, hdc, 2); } finally { g.ReleaseHdc(hdc); }
|
|
126
|
+
if (!ok) { full.Dispose(); return null; }
|
|
127
|
+
}
|
|
128
|
+
RECT f;
|
|
129
|
+
if (DwmGetWindowAttribute(h, 9, out f, Marshal.SizeOf(typeof(RECT))) == 0) {
|
|
130
|
+
Rectangle c = Rectangle.Intersect(new Rectangle(f.L - r.L, f.T - r.T, f.R - f.L, f.B - f.T), new Rectangle(0, 0, w, ht));
|
|
131
|
+
if (c.Width > 1 && c.Height > 1 && (c.Width < w || c.Height < ht)) { Bitmap crop = full.Clone(c, full.PixelFormat); full.Dispose(); return crop; }
|
|
132
|
+
}
|
|
133
|
+
return full;
|
|
134
|
+
}
|
|
135
|
+
public static Bitmap Scale(Bitmap src, double k) {
|
|
136
|
+
int w = Math.Max(1, (int)Math.Round(src.Width * k)), h = Math.Max(1, (int)Math.Round(src.Height * k));
|
|
137
|
+
Bitmap dst = new Bitmap(w, h, PixelFormat.Format24bppRgb);
|
|
138
|
+
using (Graphics g = Graphics.FromImage(dst)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.DrawImage(src, 0, 0, w, h); }
|
|
139
|
+
return dst;
|
|
140
|
+
}
|
|
141
|
+
static ImageCodecInfo Jpeg() { foreach (ImageCodecInfo c in ImageCodecInfo.GetImageEncoders()) if (c.MimeType == "image/jpeg") return c; return null; }
|
|
142
|
+
public static byte[] ToJpeg(Bitmap b, long quality) {
|
|
143
|
+
using (MemoryStream ms = new MemoryStream()) {
|
|
144
|
+
EncoderParameters p = new EncoderParameters(1); p.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
|
145
|
+
b.Save(ms, Jpeg(), p); return ms.ToArray();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Long side <= maxSide, then lower quality / size until it fits maxBytes.
|
|
149
|
+
public class Packed { public byte[] Data; public int W, H; public long Quality; }
|
|
150
|
+
public static Packed Pack(Bitmap src, int maxSide, int maxBytes) {
|
|
151
|
+
double k = Math.Min(1.0, (double)maxSide / Math.Max(src.Width, src.Height));
|
|
152
|
+
long[] qs = new long[] { 80, 65, 50 };
|
|
153
|
+
for (int round = 0; round < 6; round++) {
|
|
154
|
+
using (Bitmap s = Scale(src, k)) {
|
|
155
|
+
foreach (long q in qs) {
|
|
156
|
+
byte[] d = ToJpeg(s, q);
|
|
157
|
+
if (d.Length <= maxBytes) { Packed p = new Packed(); p.Data = d; p.W = s.Width; p.H = s.Height; p.Quality = q; return p; }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
k *= 0.75;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
// A NEW file only (CreateNew): never overwrites, also when the name was taken a moment ago.
|
|
165
|
+
public static void Save(Bitmap b, string path) {
|
|
166
|
+
string ext = Path.GetExtension(path).ToLowerInvariant();
|
|
167
|
+
byte[] data = ext == ".png" ? ToPng(b) : ToJpeg(b, 90);
|
|
168
|
+
FileStream f = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
|
169
|
+
try { f.Write(data, 0, data.Length); f.Dispose(); }
|
|
170
|
+
catch (Exception) { f.Dispose(); try { File.Delete(path); } catch (Exception) { } throw; }
|
|
171
|
+
}
|
|
172
|
+
public static byte[] ToPng(Bitmap b) { using (MemoryStream ms = new MemoryStream()) { b.Save(ms, ImageFormat.Png); return ms.ToArray(); } }
|
|
173
|
+
}
|
|
174
|
+
'@
|
|
175
|
+
|
|
176
|
+
# --- OCR (Windows.Media.Ocr, WinRT from PowerShell 5.1) ---------------------------------------
|
|
177
|
+
function Get-Await {
|
|
178
|
+
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
|
179
|
+
$script:AsTaskOp = [System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } | Select-Object -First 1
|
|
180
|
+
}
|
|
181
|
+
function Await($op, [Type]$type) {
|
|
182
|
+
$task = $script:AsTaskOp.MakeGenericMethod($type).Invoke($null, @($op))
|
|
183
|
+
[void]$task.Wait(-1)
|
|
184
|
+
$task.Result
|
|
185
|
+
}
|
|
186
|
+
function Get-OcrLanguages {
|
|
187
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
188
|
+
@([Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages | ForEach-Object { $_.LanguageTag })
|
|
189
|
+
}
|
|
190
|
+
function Invoke-Ocr([byte[]]$bytes, [string]$lang) {
|
|
191
|
+
Get-Await
|
|
192
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
193
|
+
[void][Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
|
194
|
+
[void][Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]
|
|
195
|
+
[void][Windows.Storage.Streams.IRandomAccessStream, Windows.Foundation, ContentType = WindowsRuntime]
|
|
196
|
+
$langs = @(Get-OcrLanguages) # @(): one language must stay a list, not a string
|
|
197
|
+
if (-not $langs.Count) { Fail 'NO_OCR_LANG' 'No text recognition language is installed in Windows.' }
|
|
198
|
+
if ($lang) {
|
|
199
|
+
$hit = $langs | Where-Object { $_ -ieq $lang -or $_ -ilike "$lang-*" } | Select-Object -First 1
|
|
200
|
+
if (-not $hit) { Fail 'BAD_LANG' "Text recognition for ""$lang"" is not installed." @{ languages = $langs } }
|
|
201
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new($hit))
|
|
202
|
+
} else {
|
|
203
|
+
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
|
204
|
+
if (-not $engine) { $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage([Windows.Globalization.Language]::new($langs[0])) }
|
|
205
|
+
}
|
|
206
|
+
if (-not $engine) { Fail 'NO_OCR_LANG' 'Text recognition could not start for the installed languages.' @{ languages = $langs } }
|
|
207
|
+
$ms = New-Object IO.MemoryStream (, $bytes)
|
|
208
|
+
$stream = [System.IO.WindowsRuntimeStreamExtensions]::AsRandomAccessStream($ms)
|
|
209
|
+
try {
|
|
210
|
+
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
|
211
|
+
} catch { Fail 'BAD_IMAGE' 'This file is not an image Windows can read (PNG, JPEG, BMP, GIF or TIFF).' }
|
|
212
|
+
$max = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension
|
|
213
|
+
if ($decoder.PixelWidth -gt $max -or $decoder.PixelHeight -gt $max) { Fail 'IMAGE_TOO_LARGE' "The image is larger than $max pixels on a side." }
|
|
214
|
+
$bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
|
215
|
+
$res = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
|
|
216
|
+
@{ language = $engine.RecognizerLanguage.LanguageTag; lines = @($res.Lines | ForEach-Object { $_.Text }); width = [int]$decoder.PixelWidth; height = [int]$decoder.PixelHeight }
|
|
217
|
+
}
|
|
218
|
+
# Text recognition works best on the full-size picture; above the engine's limit it is scaled down.
|
|
219
|
+
function Get-OcrBytes($bmp) {
|
|
220
|
+
[void][Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
|
221
|
+
$max = [Windows.Media.Ocr.OcrEngine]::MaxImageDimension
|
|
222
|
+
$side = [Math]::Max($bmp.Width, $bmp.Height)
|
|
223
|
+
if ($side -gt $max) { $s = [BrShot]::Scale($bmp, ($max - 1) / $side); try { return , [BrShot]::ToPng($s) } finally { $s.Dispose() } }
|
|
224
|
+
, [BrShot]::ToPng($bmp)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
[BrShot]::Dpi()
|
|
229
|
+
$action = $env:BRX_ACTION
|
|
230
|
+
if ($action -eq 'langs') { Out-Json @{ ok = $true; languages = @(Get-OcrLanguages) }; exit 0 }
|
|
231
|
+
if ($action -eq 'ocr' -and $env:BRX_FILE) {
|
|
232
|
+
$r = Invoke-Ocr ([IO.File]::ReadAllBytes($env:BRX_FILE)) $env:BRX_LANG
|
|
233
|
+
Out-Json (@{ ok = $true; kind = 'file' } + $r); exit 0
|
|
234
|
+
}
|
|
235
|
+
if (([BrShot]::InputDesktop()) -ne 'Default') { Fail 'SCREEN_LOCKED' 'The screen is locked, signed out or showing a Windows security prompt.' }
|
|
236
|
+
if ($action -eq 'windows') {
|
|
237
|
+
$wins = @([BrShot]::Windows() | ForEach-Object {
|
|
238
|
+
$p = ''; try { $p = (Get-Process -Id $_.Pid -ErrorAction Stop).ProcessName } catch { }
|
|
239
|
+
@{ id = $_.Id; pid = [int64]$_.Pid; title = $_.Title; process = $p; minimized = $_.Minimized; width = $_.W; height = $_.H } })
|
|
240
|
+
Out-Json @{ ok = $true; windows = $wins }; exit 0
|
|
241
|
+
}
|
|
242
|
+
if ($action -ne 'capture' -and $action -ne 'ocr') { Fail 'BAD_ACTION' "Unknown action $action." }
|
|
243
|
+
$hwnd = [long]('0' + $env:BRX_HWND)
|
|
244
|
+
$monitor = [int]('0' + $env:BRX_MONITOR)
|
|
245
|
+
$minimized = $false
|
|
246
|
+
if ($hwnd) {
|
|
247
|
+
if (-not [BrShot]::Exists($hwnd)) { Fail 'NO_WINDOW' 'That window is not open any more.' }
|
|
248
|
+
# The id was looked up in an earlier run: it must still belong to the same program (ids are reused).
|
|
249
|
+
if ($env:BRX_PID -and [string][BrShot]::Owner($hwnd) -ne $env:BRX_PID) { Fail 'NO_WINDOW' 'That window closed meanwhile.' }
|
|
250
|
+
$minimized = [BrShot]::IsMinimized($hwnd)
|
|
251
|
+
$bmp = [BrShot]::Window($hwnd)
|
|
252
|
+
$kind = 'window'
|
|
253
|
+
} else {
|
|
254
|
+
$count = ([BrShot]::Monitors()).Count
|
|
255
|
+
if ($monitor -gt $count) { Fail 'NO_MONITOR' "There is no monitor $monitor (this PC has $count)." @{ monitors = $count } }
|
|
256
|
+
try { $bmp = [BrShot]::Screen($monitor) } catch { Fail 'SCREEN_LOCKED' 'Windows gave no picture of the screen (locked, or a remote session without a display).' }
|
|
257
|
+
$kind = $(if ($monitor) { 'monitor' } else { 'screen' })
|
|
258
|
+
}
|
|
259
|
+
if (-not $bmp) { Fail 'CAPTURE_FAILED' 'Windows could not take the picture.' }
|
|
260
|
+
$script:taken = $true
|
|
261
|
+
try {
|
|
262
|
+
$out = @{ ok = $true; kind = $kind; width = $bmp.Width; height = $bmp.Height; minimized = $minimized }
|
|
263
|
+
if ($action -eq 'ocr') {
|
|
264
|
+
$r = Invoke-Ocr (Get-OcrBytes $bmp) $env:BRX_LANG
|
|
265
|
+
$out.language = $r.language; $out.lines = $r.lines
|
|
266
|
+
}
|
|
267
|
+
if ($env:BRX_IMAGE -eq '1') {
|
|
268
|
+
$p = [BrShot]::Pack($bmp, [int]$env:BRX_MAXSIDE, [int]$env:BRX_MAXBYTES)
|
|
269
|
+
if (-not $p) { Fail 'IMAGE_TOO_LARGE' 'The picture could not be made small enough.' }
|
|
270
|
+
$out.mime = 'image/jpeg'; $out.data = [Convert]::ToBase64String($p.Data); $out.outWidth = $p.W; $out.outHeight = $p.H; $out.bytes = $p.Data.Length
|
|
271
|
+
}
|
|
272
|
+
# The full-size copy last: no file is left on disk when an earlier step failed.
|
|
273
|
+
if ($env:BRX_SAVE) {
|
|
274
|
+
try { [BrShot]::Save($bmp, $env:BRX_SAVE) } catch {
|
|
275
|
+
$h = $_.Exception.HResult; if ($_.Exception.InnerException) { $h = $_.Exception.InnerException.HResult }
|
|
276
|
+
if ($h -eq -2147024816 -or $h -eq -2147024713) { Fail 'EXISTS' 'A file with this name appeared meanwhile; nothing was overwritten.' }
|
|
277
|
+
throw
|
|
278
|
+
}
|
|
279
|
+
$out.saved = $env:BRX_SAVE
|
|
280
|
+
}
|
|
281
|
+
Out-Json $out
|
|
282
|
+
} finally { $bmp.Dispose() }
|
|
283
|
+
} catch {
|
|
284
|
+
Out-Json @{ ok = $false; code = 'PS_ERROR'; message = [string]$_.Exception.Message; taken = [bool]$script:taken }
|
|
285
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// LOGIC: see critical-logic/bridge.md ("Extra tools" / fast_search). Instant search: Everything (if
|
|
2
|
+
// installed and running) → the Windows Search index (SystemIndex over OLE DB) → the engine's own
|
|
3
|
+
// search (start_search, already under the policy). Every hit goes through the same folder rules as the
|
|
4
|
+
// file tools (visiblePath): excluded folders and folders outside the allowed list never show up.
|
|
5
|
+
// Queries are built here (escaped); search.ps1 only runs them.
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { psJson, guard, visiblePath, fail, json, needAction, str, int, ToolError, errorText } from './tool-lib.mjs';
|
|
9
|
+
|
|
10
|
+
const PS = fileURLToPath(new URL('./search.ps1', import.meta.url));
|
|
11
|
+
const ACTIONS = ['files', 'text', 'status'];
|
|
12
|
+
const MAX = 500;
|
|
13
|
+
const POLL_MS = 400, BASIC_WAIT_MS = 20000;
|
|
14
|
+
|
|
15
|
+
export const defs = [{
|
|
16
|
+
name: 'fast_search',
|
|
17
|
+
title: 'Fast search',
|
|
18
|
+
description: 'Fast file search on this PC. action "files": names or masks like *.pdf; "text": words inside documents (Windows Search index); "status": which search sources this PC has. Uses Everything when it is installed, then the Windows Search index, then a plain folder scan. Results follow the folder rules of this PC.',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object', additionalProperties: false, required: ['action'],
|
|
21
|
+
properties: {
|
|
22
|
+
action: { type: 'string', enum: ACTIONS },
|
|
23
|
+
query: { type: 'string', maxLength: 300, description: 'Name, mask (* ?) or words' },
|
|
24
|
+
folder: { type: 'string', description: 'Search only inside this folder (full path)' },
|
|
25
|
+
kind: { type: 'string', maxLength: 12, description: 'File extension, e.g. pdf or .docx' },
|
|
26
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX, description: 'Default 100' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
30
|
+
}];
|
|
31
|
+
|
|
32
|
+
const b64 = (s) => Buffer.from(String(s), 'utf8').toString('base64');
|
|
33
|
+
const sq = (s) => String(s).replace(/'/g, "''"); // SQL string literal
|
|
34
|
+
const ctl = (s) => String(s).replace(/[\u0000-\u001f\u007f]/g, ' ').trim();
|
|
35
|
+
export function extOf(kind) {
|
|
36
|
+
if (kind === undefined) return undefined;
|
|
37
|
+
const k = String(kind).trim().replace(/^\*?\./, '');
|
|
38
|
+
if (!/^[\p{L}\p{N}_-]{1,10}$/u.test(k)) fail('BAD_ARGS', 'kind must be a file extension like pdf or .docx.');
|
|
39
|
+
return k.toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
/** Name / mask → LIKE pattern: LIKE's own specials are literal, * ? are the wildcards. */
|
|
42
|
+
export function likePattern(q) {
|
|
43
|
+
const lit = ctl(q).replace(/[[%_]/g, (c) => `[${c}]`);
|
|
44
|
+
const w = lit.replace(/\*/g, '%').replace(/\?/g, '_');
|
|
45
|
+
return /[*?]/.test(q) ? w : `%${w}%`;
|
|
46
|
+
}
|
|
47
|
+
const scopeUrl = (d) => `file:${d.replace(/\\/g, '/')}`;
|
|
48
|
+
/** The SystemIndex query for files / text, scoped to folders (none = whole index). */
|
|
49
|
+
export function indexSql({ action, query, folders = [], ext, limit }) {
|
|
50
|
+
const where = [];
|
|
51
|
+
if (folders.length) where.push(`(${folders.map((d) => `SCOPE='${sq(scopeUrl(d))}'`).join(' OR ')})`);
|
|
52
|
+
if (action === 'text') { where.push(`FREETEXT('${sq(ctl(query))}')`); where.push("System.ItemType <> 'Directory'"); }
|
|
53
|
+
else if (query) where.push(`System.FileName LIKE '${sq(likePattern(query))}'`);
|
|
54
|
+
if (ext) where.push(`System.FileExtension = '.${sq(ext)}'`);
|
|
55
|
+
return `SELECT TOP ${limit} System.ItemUrl, System.Size, System.DateModified FROM SystemIndex${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY System.DateModified DESC`;
|
|
56
|
+
}
|
|
57
|
+
export const coverSql = (folder) => `SELECT TOP 1 System.ItemUrl FROM SystemIndex WHERE SCOPE='${sq(scopeUrl(folder))}'`;
|
|
58
|
+
/** es.exe arguments: words only — nothing that starts like an option (-export-…, /a) reaches it. Every
|
|
59
|
+
* double quote goes first (a quoted "-export-txt" or a"b must never turn into a switch), then a leading - or /. */
|
|
60
|
+
export function esArgs({ query, folder, ext, limit }) {
|
|
61
|
+
const words = ctl(query || '').replace(/["\u201c\u201d]/g, '').split(/\s+/).map((w) => w.replace(/^[-/]+/, '')).filter(Boolean);
|
|
62
|
+
return ['-n', String(limit), ...(folder ? ['-path', folder] : []), ...(ext ? [`ext:${ext}`] : []), ...words];
|
|
63
|
+
}
|
|
64
|
+
export const urlToPath = (u) => String(u).replace(/^file:/i, '').replace(/\//g, '\\');
|
|
65
|
+
|
|
66
|
+
const run = (ctx, env, timeout = 30000) => psJson(ctx, { files: [PS], env, timeout });
|
|
67
|
+
async function status(ctx, folder) {
|
|
68
|
+
return run(ctx, { BRX_MODE: 'status', ...(folder ? { BRX_SCOPE_SQL: b64(coverSql(folder)) } : {}) }, 20000);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function keep(ctx, items, limit) {
|
|
72
|
+
const out = [];
|
|
73
|
+
let hidden = 0;
|
|
74
|
+
for (const it of items) {
|
|
75
|
+
if (!it?.path || typeof it.path !== 'string') continue;
|
|
76
|
+
if (!visiblePath(ctx, it.path)) { hidden++; continue; }
|
|
77
|
+
out.push({ path: it.path, ...(Number.isFinite(it.size) ? { size: it.size } : {}), ...(it.modified ? { modified: it.modified } : {}), ...(it.dir ? { folder: true } : {}) });
|
|
78
|
+
if (out.length >= limit) break;
|
|
79
|
+
}
|
|
80
|
+
return { items: out, hidden };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The engine's own search: "📁 path" (files) / "📄 path:line - text" (content) lines.
|
|
84
|
+
export function parseBasic(text) {
|
|
85
|
+
const items = [];
|
|
86
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
87
|
+
let m = /^📁 (.+)$/u.exec(line);
|
|
88
|
+
if (m) { items.push({ path: m[1].trim() }); continue; }
|
|
89
|
+
m = /^📄 (.+?):(\d+) - (.*)$/u.exec(line);
|
|
90
|
+
if (m && !items.some((x) => x.path === m[1])) items.push({ path: m[1], line: Number(m[2]), match: m[3].slice(0, 200) });
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
const textOf = (r) => (r?.content || []).filter((c) => c?.type === 'text').map((c) => c.text).join('\n');
|
|
95
|
+
async function basic(ctx, { action, query, folder, ext, limit }, wait = BASIC_WAIT_MS) {
|
|
96
|
+
if (!ctx.engine) throw new ToolError('NO_BASIC', 'The plain folder search is not available here.');
|
|
97
|
+
const root = folder || ctx.cfg?.allowedDirectories?.[0] || process.env.USERPROFILE;
|
|
98
|
+
if (!root) throw new ToolError('NO_FOLDER', 'Say which folder to search.', 'Pass folder.');
|
|
99
|
+
// Names: the engine takes masks (* ?) as globs and plain text as "contains"; content: literal text.
|
|
100
|
+
const started = await ctx.engine('start_search', { path: root, pattern: query, searchType: action === 'text' ? 'content' : 'files', ...(ext ? { filePattern: `*.${ext}` } : {}), maxResults: limit, literalSearch: true });
|
|
101
|
+
const t0 = textOf(started);
|
|
102
|
+
if (started?.isError) throw new ToolError('BASIC_FAILED', t0.slice(0, 300) || 'The folder search did not start.');
|
|
103
|
+
const id = /session:\s*(\S+)/i.exec(t0)?.[1];
|
|
104
|
+
if (!id) return parseBasic(t0);
|
|
105
|
+
let all = [], done = /COMPLETED/.test(t0);
|
|
106
|
+
const until = Date.now() + wait;
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const r = await ctx.engine('get_more_search_results', { sessionId: id, offset: 0, length: limit });
|
|
110
|
+
const t = textOf(r);
|
|
111
|
+
all = parseBasic(t);
|
|
112
|
+
done = /Status:\s*COMPLETED/i.test(t);
|
|
113
|
+
if (done || r?.isError || Date.now() > until) break;
|
|
114
|
+
await new Promise((res) => setTimeout(res, POLL_MS));
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
// Also when a poll throws (engine timeout / restart): the search must not keep running.
|
|
118
|
+
if (!done) await Promise.resolve().then(() => ctx.engine('stop_search', { sessionId: id })).catch(() => null);
|
|
119
|
+
}
|
|
120
|
+
return all;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function handle(name, args, ctx) {
|
|
124
|
+
const action = needAction(args, ACTIONS);
|
|
125
|
+
const folder = args.folder !== undefined ? guard(ctx, str(args, 'folder', { max: 1000 })) : undefined;
|
|
126
|
+
if (action === 'status') {
|
|
127
|
+
const s = await status(ctx, folder);
|
|
128
|
+
return json({
|
|
129
|
+
everything: s.everything?.path ? (s.everything.running ? 'ready' : 'installed, not running') : s.everything?.untrusted ? 'not used (es.exe is outside Program Files and not signed by voidtools)' : 'not installed',
|
|
130
|
+
windows_index: s.index?.service === 'Running' ? 'ready' : `not available (service ${String(s.index?.service || 'unknown')})`,
|
|
131
|
+
...(folder ? { folder, folder_indexed: s.index?.covered === true ? 'yes' : s.index?.covered === false ? 'no (not in the index — the plain scan is used)' : 'unknown' } : {}),
|
|
132
|
+
plain_scan: 'always available',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const query = str(args, 'query', { required: true, max: 300 });
|
|
136
|
+
const limit = int(args, 'limit', { min: 1, max: MAX, def: 100 });
|
|
137
|
+
const ext = extOf(args.kind);
|
|
138
|
+
const allowed = ctx.cfg?.allowedDirectories || [];
|
|
139
|
+
const folders = folder ? [folder] : allowed.map((d) => path.resolve(d));
|
|
140
|
+
const want = Math.min(MAX, limit * 2); // room for hits the folder rules hide
|
|
141
|
+
const tried = [];
|
|
142
|
+
let firstError = null;
|
|
143
|
+
const note = (src, e) => { tried.push(`${src}: ${e instanceof ToolError ? e.code : e?.code || 'error'}`); firstError ||= e; };
|
|
144
|
+
// null = every hit was hidden by the folder rules → the next source is tried.
|
|
145
|
+
const answer = (source, items, extra = {}) => {
|
|
146
|
+
const k = keep(ctx, items, limit);
|
|
147
|
+
if (!k.items.length && k.hidden) { tried.push(`${source}: ${k.hidden} result(s) hidden by this PC's folder rules`); return null; }
|
|
148
|
+
return json({ source, count: k.items.length, results: k.items, ...(k.hidden ? { hidden: `${k.hidden} result(s) hidden by this PC's folder rules` } : {}), ...(tried.length ? { skipped_sources: tried } : {}), ...extra });
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let st = null;
|
|
152
|
+
try { st = await status(ctx, folder); } catch (e) { note('status', e); }
|
|
153
|
+
|
|
154
|
+
if (action === 'files' && st?.everything?.path && st.everything.running) {
|
|
155
|
+
try {
|
|
156
|
+
// Scoped like the index: the folder, or the one allowed folder (several: hits are filtered below).
|
|
157
|
+
const esFolder = folder || (folders.length === 1 ? folders[0] : undefined);
|
|
158
|
+
const r = await run(ctx, { BRX_MODE: 'everything', BRX_ES: st.everything.path, BRX_ARGS: b64(JSON.stringify(esArgs({ query, folder: esFolder, ext, limit: want }))) });
|
|
159
|
+
if (r.error) throw new ToolError(r.error, r.error === 'ES_START_FAILED' ? 'Everything\'s es.exe could not start (an antivirus may block it).' : 'Everything did not answer.');
|
|
160
|
+
const a = r.items?.length ? answer('everything', r.items) : null;
|
|
161
|
+
if (a) return a;
|
|
162
|
+
if (!r.items?.length) tried.push('everything: nothing found');
|
|
163
|
+
} catch (e) { note('everything', e); }
|
|
164
|
+
} else if (action === 'files') tried.push(`everything: ${st?.everything?.path ? 'not running' : 'not installed'}`);
|
|
165
|
+
|
|
166
|
+
let indexNote;
|
|
167
|
+
if (st?.index?.service === 'Running') {
|
|
168
|
+
try {
|
|
169
|
+
const r = await run(ctx, { BRX_MODE: 'index', BRX_SQL: b64(indexSql({ action, query, folders, ext, limit: want })) });
|
|
170
|
+
if (r.error) throw new ToolError(r.error, 'The Windows Search index did not answer.');
|
|
171
|
+
const items = (r.items || []).map((x) => ({ ...x, path: urlToPath(x.url) }));
|
|
172
|
+
const a = items.length ? answer('windows_index', items, folder && st.index.covered === false ? { note: 'This folder is not in the Windows Search index; results may be incomplete.' } : {}) : null;
|
|
173
|
+
if (a) return a;
|
|
174
|
+
if (!items.length) {
|
|
175
|
+
indexNote = folder && st.index.covered === false ? 'This folder is not in the Windows Search index' : 'Nothing found in the Windows Search index';
|
|
176
|
+
tried.push(`windows_index: ${indexNote.toLowerCase()}`);
|
|
177
|
+
}
|
|
178
|
+
} catch (e) { note('windows_index', e); }
|
|
179
|
+
} else tried.push(`windows_index: service ${String(st?.index?.service || 'unknown')}`);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const items = await basic(ctx, { action, query, folder, ext, limit: want });
|
|
183
|
+
return answer('basic', items, { note: `Plain folder scan${indexNote ? ` (${indexNote.toLowerCase()})` : ''}.` })
|
|
184
|
+
|| json({ source: 'basic', count: 0, results: [], skipped_sources: tried });
|
|
185
|
+
} catch (e) {
|
|
186
|
+
note('basic', e);
|
|
187
|
+
throw new ToolError('SEARCH_FAILED', `Search did not work: ${errorText(firstError).replace(/\s*\(code [A-Z_0-9]+\)$/, '')}`, 'Pass a folder, or check the PC with bridgerator_doctor.');
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/search.ps1
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# LOGIC: see critical-logic/bridge.md ("Extra tools" / fast_search). Instant search sources for the
|
|
2
|
+
# bridge (search-tool.mjs builds every query; this script only runs it).
|
|
3
|
+
# In (env): BRX_MODE = status | everything | index; BRX_ES (es.exe path), BRX_ARGS (base64 UTF-8 JSON
|
|
4
|
+
# array of es.exe arguments), BRX_SQL (base64 UTF-8 SystemIndex query), BRX_SCOPE_SQL (status: the
|
|
5
|
+
# coverage query, optional), BRX_ES_WAIT (ms es.exe may run, default 20000). Out: ONE JSON line. Errors are codes, never localized message text.
|
|
6
|
+
function Out-Json($o) { [Console]::Out.WriteLine(($o | ConvertTo-Json -Compress -Depth 5)) }
|
|
7
|
+
function From-B64($s) { if (-not $s) { return '' }; [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)) }
|
|
8
|
+
|
|
9
|
+
# es.exe runs only from a place a standard user cannot write (Program Files, Windows) or when it is
|
|
10
|
+
# signed by voidtools: a planted es.exe on the PATH / in a user folder never runs.
|
|
11
|
+
function Test-TrustedEs([string]$p) {
|
|
12
|
+
if (-not $p -or -not (Test-Path -LiteralPath $p -PathType Leaf)) { return $false }
|
|
13
|
+
$full = [IO.Path]::GetFullPath($p)
|
|
14
|
+
foreach ($d in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
|
|
15
|
+
if ($d -and $full.StartsWith(([IO.Path]::GetFullPath($d).TrimEnd('\') + '\'), [StringComparison]::OrdinalIgnoreCase)) { return $true }
|
|
16
|
+
}
|
|
17
|
+
# Windows itself: only the folder and System32 (subfolders like Temp are writable by users).
|
|
18
|
+
$parent = [IO.Path]::GetDirectoryName($full)
|
|
19
|
+
if ($env:SystemRoot -and @($env:SystemRoot, (Join-Path $env:SystemRoot 'System32')) -contains $parent) { return $true }
|
|
20
|
+
try {
|
|
21
|
+
$sig = Get-AuthenticodeSignature -LiteralPath $full
|
|
22
|
+
return ($sig.Status -eq 'Valid' -and [string]$sig.SignerCertificate.Subject -match '(^|,\s*)(CN|O)=voidtools(,|$)')
|
|
23
|
+
} catch { return $false }
|
|
24
|
+
}
|
|
25
|
+
function Find-Es {
|
|
26
|
+
$found = $null
|
|
27
|
+
$cands = @(Get-Command es.exe -CommandType Application -All -ErrorAction SilentlyContinue | ForEach-Object { $_.Source })
|
|
28
|
+
foreach ($d in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, (Join-Path $env:LOCALAPPDATA 'Programs'))) { if ($d) { $cands += (Join-Path $d 'Everything\es.exe') } }
|
|
29
|
+
foreach ($p in $cands) {
|
|
30
|
+
if (-not $p -or -not (Test-Path -LiteralPath $p -PathType Leaf)) { continue }
|
|
31
|
+
if (Test-TrustedEs $p) { return @{ path = $p } }
|
|
32
|
+
if (-not $found) { $found = $p }
|
|
33
|
+
}
|
|
34
|
+
return @{ path = $null; untrusted = $found }
|
|
35
|
+
}
|
|
36
|
+
# One Windows command-line argument (words carry no quotes: search-tool.mjs strips them). Quoted, a
|
|
37
|
+
# trailing \ is doubled: "C:\My Files\" would end in \" (a literal quote) and swallow the next arguments.
|
|
38
|
+
function Quote-Arg([string]$a) { if ($a -match '[\s"]' -or -not $a) { return '"' + (($a -replace '"', '') -replace '(\\+)$', '$1$1') + '"' }; return $a }
|
|
39
|
+
|
|
40
|
+
function Invoke-Index($sql) {
|
|
41
|
+
$conn = $null; $rs = $null
|
|
42
|
+
try {
|
|
43
|
+
$conn = New-Object -ComObject ADODB.Connection
|
|
44
|
+
$conn.Open("Provider=Search.CollatorDSO;Extended Properties='Application=Windows';")
|
|
45
|
+
$rs = New-Object -ComObject ADODB.Recordset
|
|
46
|
+
$rs.Open($sql, $conn)
|
|
47
|
+
$items = New-Object System.Collections.ArrayList
|
|
48
|
+
while (-not $rs.EOF) {
|
|
49
|
+
$url = [string]$rs.Fields.Item('System.ItemUrl').Value
|
|
50
|
+
$size = $rs.Fields.Item('System.Size').Value
|
|
51
|
+
$mod = $rs.Fields.Item('System.DateModified').Value
|
|
52
|
+
$o = @{ url = $url }
|
|
53
|
+
if ($size -ne $null -and $size -isnot [DBNull]) { $o.size = [int64]$size }
|
|
54
|
+
if ($mod -is [DateTime]) { $o.modified = $mod.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
|
|
55
|
+
[void]$items.Add($o)
|
|
56
|
+
$rs.MoveNext()
|
|
57
|
+
}
|
|
58
|
+
return @{ items = @($items) }
|
|
59
|
+
} catch {
|
|
60
|
+
return @{ error = 'INDEX_UNAVAILABLE'; hresult = ('0x{0:X8}' -f $_.Exception.HResult) }
|
|
61
|
+
} finally {
|
|
62
|
+
if ($rs) { try { $rs.Close() } catch { } }
|
|
63
|
+
if ($conn) { try { $conn.Close() } catch { } }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$mode = $env:BRX_MODE
|
|
68
|
+
if ($mode -eq 'status') {
|
|
69
|
+
$es = Find-Es
|
|
70
|
+
$running = [bool](Get-Process -Name 'Everything', 'Everything64' -ErrorAction SilentlyContinue)
|
|
71
|
+
$svc = Get-Service -Name WSearch -ErrorAction SilentlyContinue
|
|
72
|
+
$state = if ($svc) { [string]$svc.Status } else { 'Missing' }
|
|
73
|
+
$covered = $null
|
|
74
|
+
$scopeSql = From-B64 $env:BRX_SCOPE_SQL
|
|
75
|
+
if ($scopeSql -and $state -eq 'Running') {
|
|
76
|
+
$r = Invoke-Index $scopeSql
|
|
77
|
+
$covered = if ($r.error) { $null } else { $r.items.Count -gt 0 }
|
|
78
|
+
}
|
|
79
|
+
Out-Json @{ everything = @{ path = $es.path; untrusted = [bool]$es.untrusted; running = $running }; index = @{ service = $state; covered = $covered } }
|
|
80
|
+
exit 0
|
|
81
|
+
}
|
|
82
|
+
if ($mode -eq 'everything') {
|
|
83
|
+
$es = $env:BRX_ES
|
|
84
|
+
if (-not $es -or -not (Test-Path -LiteralPath $es)) { Out-Json @{ error = 'ES_MISSING' }; exit 0 }
|
|
85
|
+
# Held open (others may only read) from the trust check until it has started: it can't be swapped in between.
|
|
86
|
+
$hold = $null; try { $hold = [IO.File]::Open($es, 'Open', 'Read', 'Read') } catch { Out-Json @{ error = 'ES_START_FAILED' }; exit 0 }
|
|
87
|
+
if (-not (Test-TrustedEs $es)) { $hold.Dispose(); Out-Json @{ error = 'ES_UNTRUSTED' }; exit 0 }
|
|
88
|
+
# PowerShell 5.1 ConvertFrom-Json emits an array as ONE object: enumerate it into separate arguments.
|
|
89
|
+
$argv = @((From-B64 $env:BRX_ARGS) | ConvertFrom-Json | ForEach-Object { $_ })
|
|
90
|
+
# The export file lives in our own cache folder (not TEMP); es.exe is stopped when it takes too long.
|
|
91
|
+
$dir = Join-Path $env:LOCALAPPDATA 'Bridgerator\cache'
|
|
92
|
+
[void][IO.Directory]::CreateDirectory($dir)
|
|
93
|
+
$tmp = Join-Path $dir ('es-' + [guid]::NewGuid().ToString('N') + '.efu')
|
|
94
|
+
$wait = 20000; $w = 0; if ([int]::TryParse([string]$env:BRX_ES_WAIT, [ref]$w) -and $w -gt 0) { $wait = $w }
|
|
95
|
+
$proc = $null
|
|
96
|
+
try {
|
|
97
|
+
$psi = New-Object Diagnostics.ProcessStartInfo
|
|
98
|
+
$psi.FileName = $es
|
|
99
|
+
$psi.Arguments = ((@($argv) + @('-export-efu', $tmp)) | ForEach-Object { Quote-Arg ([string]$_) }) -join ' '
|
|
100
|
+
# Its own pipes (our stdout stays the one JSON line for the bridge), both drained at once: a full pipe
|
|
101
|
+
# (usage / error text) would block es.exe until the kill.
|
|
102
|
+
$psi.UseShellExecute = $false; $psi.CreateNoWindow = $true; $psi.RedirectStandardOutput = $true; $psi.RedirectStandardError = $true
|
|
103
|
+
try { $proc = [Diagnostics.Process]::Start($psi) } catch { Out-Json @{ error = 'ES_START_FAILED' }; exit 0 } finally { $hold.Dispose() }
|
|
104
|
+
[void]$proc.StandardOutput.ReadToEndAsync(); [void]$proc.StandardError.ReadToEndAsync()
|
|
105
|
+
if (-not $proc.WaitForExit($wait)) { try { $proc.Kill() } catch { }; [void]$proc.WaitForExit(5000); Out-Json @{ error = 'ES_TIMEOUT' }; exit 0 }
|
|
106
|
+
$code = $proc.ExitCode
|
|
107
|
+
if ($code -ne 0) { Out-Json @{ error = ('ES_EXIT_' + $code) }; exit 0 }
|
|
108
|
+
$items = New-Object System.Collections.ArrayList
|
|
109
|
+
if (Test-Path -LiteralPath $tmp) {
|
|
110
|
+
foreach ($row in (Import-Csv -LiteralPath $tmp -Encoding UTF8)) {
|
|
111
|
+
$o = @{ path = [string]$row.Filename }
|
|
112
|
+
$s = 0L; if ([int64]::TryParse([string]$row.Size, [ref]$s)) { $o.size = $s }
|
|
113
|
+
$ft = 0L; if ([int64]::TryParse([string]$row.'Date Modified', [ref]$ft) -and $ft -gt 0) { $o.modified = [DateTime]::FromFileTimeUtc($ft).ToString('yyyy-MM-ddTHH:mm:ssZ') }
|
|
114
|
+
$a = 0; if ([int]::TryParse([string]$row.Attributes, [ref]$a)) { $o.dir = [bool]($a -band 16) }
|
|
115
|
+
[void]$items.Add($o)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
Out-Json @{ items = @($items) }
|
|
119
|
+
} finally {
|
|
120
|
+
if ($proc) { try { if (-not $proc.HasExited) { $proc.Kill() } } catch { }; $proc.Dispose() }
|
|
121
|
+
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
|
|
122
|
+
}
|
|
123
|
+
exit 0
|
|
124
|
+
}
|
|
125
|
+
if ($mode -eq 'index') {
|
|
126
|
+
Out-Json (Invoke-Index (From-B64 $env:BRX_SQL))
|
|
127
|
+
exit 0
|
|
128
|
+
}
|
|
129
|
+
Out-Json @{ error = 'BAD_MODE' }
|