feat: add csharp win helper probe

This commit is contained in:
zhaoyilun
2026-07-05 14:29:29 +08:00
parent d58b9a0274
commit f2dd1c7e57
7 changed files with 816 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Web.Script.Serialization;
namespace ISphereWinHelper
{
internal static class HelperProtocol
{
public const string Protocol = "isphere.helper.v1";
private static readonly JavaScriptSerializer Serializer = new JavaScriptSerializer { MaxJsonLength = 16 * 1024 * 1024, RecursionLimit = 256 };
public static bool TryParseRequest(string text, out Dictionary<string, object> request, out string requestId, out string op)
{
request = null;
requestId = "";
op = "";
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
try
{
object decoded = Serializer.DeserializeObject(text);
request = decoded as Dictionary<string, object>;
if (request == null)
{
return false;
}
string protocol = GetString(request, "protocol", "");
requestId = GetString(request, "request_id", "");
op = GetString(request, "op", "");
if (protocol != Protocol || string.IsNullOrWhiteSpace(op))
{
return false;
}
return true;
}
catch
{
return false;
}
}
public static Dictionary<string, object> Success(string requestId, string op, object data)
{
return Envelope(requestId, op, true, data, null);
}
public static Dictionary<string, object> Failure(string requestId, string op, string code, string message)
{
var error = new Dictionary<string, object>
{
{ "code", code },
{ "message", message }
};
return Envelope(requestId, op, false, null, error);
}
private static Dictionary<string, object> Envelope(string requestId, string op, bool ok, object data, object error)
{
return new Dictionary<string, object>
{
{ "protocol", Protocol },
{ "request_id", requestId ?? "" },
{ "op", op ?? "" },
{ "ok", ok },
{ "data", data },
{ "error", error },
{ "warnings", new object[0] }
};
}
public static string ToJson(object value)
{
return Serializer.Serialize(value);
}
public static Dictionary<string, object> GetObject(Dictionary<string, object> source, string key)
{
if (source == null || !source.ContainsKey(key) || source[key] == null)
{
return new Dictionary<string, object>();
}
return source[key] as Dictionary<string, object> ?? new Dictionary<string, object>();
}
public static string GetString(Dictionary<string, object> source, string key, string defaultValue)
{
if (source == null || !source.ContainsKey(key) || source[key] == null)
{
return defaultValue;
}
return Convert.ToString(source[key]);
}
public static bool GetBool(Dictionary<string, object> source, string key, bool defaultValue)
{
if (source == null || !source.ContainsKey(key) || source[key] == null)
{
return defaultValue;
}
try
{
return Convert.ToBoolean(source[key]);
}
catch
{
return defaultValue;
}
}
public static int GetInt(Dictionary<string, object> source, string key, int defaultValue)
{
if (source == null || !source.ContainsKey(key) || source[key] == null)
{
return defaultValue;
}
try
{
return Convert.ToInt32(source[key]);
}
catch
{
return defaultValue;
}
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace ISphereWinHelper
{
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
string requestText = Console.In.ReadToEnd();
Dictionary<string, object> request;
string requestId = "";
string op = "";
if (!HelperProtocol.TryParseRequest(requestText, out request, out requestId, out op))
{
Console.WriteLine(HelperProtocol.ToJson(HelperProtocol.Failure(requestId, op, "INVALID_REQUEST", "request must be valid JSON object")));
return 0;
}
try
{
Dictionary<string, object> opArgs = HelperProtocol.GetObject(request, "args");
Dictionary<string, object> response;
switch (op)
{
case "version":
response = HelperProtocol.Success(requestId, op, VersionData());
break;
case "self_check":
response = HelperProtocol.Success(requestId, op, SelfCheckData());
break;
case "scan_windows":
response = HelperProtocol.Success(requestId, op, WindowScanner.Scan(opArgs));
break;
case "dump_uia":
response = UiaDumper.Dump(requestId, op, opArgs);
break;
default:
response = HelperProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
break;
}
Console.WriteLine(HelperProtocol.ToJson(response));
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString());
Console.WriteLine(HelperProtocol.ToJson(HelperProtocol.Failure(requestId, op, "HELPER_FAILED", ex.Message)));
return 0;
}
}
private static Dictionary<string, object> VersionData()
{
return new Dictionary<string, object>
{
{ "helper_name", "ISphereWinHelper" },
{ "helper_version", "0.1.0" },
{ "protocol", HelperProtocol.Protocol },
{ "runtime", ".NET Framework " + Environment.Version }
};
}
private static Dictionary<string, object> SelfCheckData()
{
bool uiaAvailable;
try
{
uiaAvailable = System.Windows.Automation.AutomationElement.RootElement != null;
}
catch
{
uiaAvailable = false;
}
return new Dictionary<string, object>
{
{ "desktop_access", Environment.UserInteractive },
{ "uia_available", uiaAvailable }
};
}
}
}

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Windows.Automation;
namespace ISphereWinHelper
{
internal static class UiaDumper
{
public static Dictionary<string, object> Dump(string requestId, string op, Dictionary<string, object> args)
{
string hwndText = HelperProtocol.GetString(args, "hwnd", "");
IntPtr hwnd;
if (!WindowScanner.TryParseHwnd(hwndText, out hwnd))
{
return HelperProtocol.Failure(requestId, op, "WINDOW_NOT_FOUND", "invalid or empty hwnd");
}
int maxDepth = HelperProtocol.GetInt(args, "max_depth", 8);
if (maxDepth < 0) maxDepth = 0;
if (maxDepth > 20) maxDepth = 20;
bool includeText = HelperProtocol.GetBool(args, "include_text", true);
int maxChildren = HelperProtocol.GetInt(args, "max_children", 200);
if (maxChildren < 1) maxChildren = 1;
if (maxChildren > 1000) maxChildren = 1000;
try
{
AutomationElement rootElement = AutomationElement.FromHandle(hwnd);
if (rootElement == null)
{
return HelperProtocol.Failure(requestId, op, "WINDOW_NOT_FOUND", "no UI Automation element for hwnd");
}
var data = new Dictionary<string, object>
{
{ "hwnd", WindowScanner.FormatHwnd(hwnd) },
{ "root", DumpElement(rootElement, 0, maxDepth, includeText, maxChildren) }
};
return HelperProtocol.Success(requestId, op, data);
}
catch (Exception ex)
{
return HelperProtocol.Failure(requestId, op, "UIA_DUMP_FAILED", ex.Message);
}
}
private static Dictionary<string, object> DumpElement(AutomationElement element, int depth, int maxDepth, bool includeText, int maxChildren)
{
var node = new Dictionary<string, object>
{
{ "name", SafeString(delegate { return element.Current.Name; }) },
{ "control_type", SafeControlType(element) },
{ "automation_id", SafeString(delegate { return element.Current.AutomationId; }) },
{ "class_name", SafeString(delegate { return element.Current.ClassName; }) },
{ "localized_control_type", SafeString(delegate { return element.Current.LocalizedControlType; }) },
{ "framework_id", SafeString(delegate { return element.Current.FrameworkId; }) },
{ "is_enabled", SafeBool(delegate { return element.Current.IsEnabled; }) },
{ "is_offscreen", SafeBool(delegate { return element.Current.IsOffscreen; }) },
{ "bounds", SafeBounds(element) }
};
if (includeText)
{
node["value"] = SafeValue(element);
}
var children = new List<Dictionary<string, object>>();
if (depth < maxDepth)
{
try
{
AutomationElementCollection found = element.FindAll(TreeScope.Children, Condition.TrueCondition);
int limit = Math.Min(found.Count, maxChildren);
for (int i = 0; i < limit; i++)
{
children.Add(DumpElement(found[i], depth + 1, maxDepth, includeText, maxChildren));
}
if (found.Count > limit)
{
node["truncated_children"] = found.Count - limit;
}
}
catch
{
node["children_error"] = true;
}
}
node["children"] = children;
return node;
}
private delegate string StringGetter();
private delegate bool BoolGetter();
private static string SafeString(StringGetter getter)
{
try { return getter() ?? ""; }
catch { return ""; }
}
private static bool SafeBool(BoolGetter getter)
{
try { return getter(); }
catch { return false; }
}
private static string SafeControlType(AutomationElement element)
{
try
{
string name = element.Current.ControlType.ProgrammaticName ?? "";
return name.StartsWith("ControlType.", StringComparison.Ordinal) ? name.Substring("ControlType.".Length) : name;
}
catch
{
return "";
}
}
private static Dictionary<string, object> SafeBounds(AutomationElement element)
{
try
{
System.Windows.Rect rect = element.Current.BoundingRectangle;
return new Dictionary<string, object>
{
{ "x", rect.X },
{ "y", rect.Y },
{ "width", rect.Width },
{ "height", rect.Height }
};
}
catch
{
return new Dictionary<string, object>
{
{ "x", 0 },
{ "y", 0 },
{ "width", 0 },
{ "height", 0 }
};
}
}
private static string SafeValue(AutomationElement element)
{
try
{
object pattern;
if (element.TryGetCurrentPattern(ValuePattern.Pattern, out pattern))
{
return ((ValuePattern)pattern).Current.Value ?? "";
}
}
catch
{
}
return "";
}
}
}

View File

@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace ISphereWinHelper
{
internal static class WindowScanner
{
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
public static Dictionary<string, object> Scan(Dictionary<string, object> args)
{
bool includeAllVisible = HelperProtocol.GetBool(args, "include_all_visible", false);
var windows = new List<Dictionary<string, object>>();
EnumWindows(delegate(IntPtr hwnd, IntPtr lParam)
{
if (!IsWindowVisible(hwnd))
{
return true;
}
string title = GetWindowTextValue(hwnd);
string className = GetClassNameValue(hwnd);
uint pidRaw;
GetWindowThreadProcessId(hwnd, out pidRaw);
int pid = unchecked((int)pidRaw);
string processName = GetProcessName(pid);
int score = ScoreWindow(processName, title, className);
if (!includeAllVisible && score <= 0)
{
return true;
}
RECT rect;
GetWindowRect(hwnd, out rect);
windows.Add(new Dictionary<string, object>
{
{ "hwnd", FormatHwnd(hwnd) },
{ "pid", pid },
{ "process_name", processName },
{ "title", title },
{ "class_name", className },
{ "visible", true },
{ "bounds", new Dictionary<string, object>
{
{ "x", rect.Left },
{ "y", rect.Top },
{ "width", Math.Max(0, rect.Right - rect.Left) },
{ "height", Math.Max(0, rect.Bottom - rect.Top) }
}
},
{ "score", score }
});
return true;
}, IntPtr.Zero);
windows.Sort(delegate(Dictionary<string, object> left, Dictionary<string, object> right)
{
return Convert.ToInt32(right["score"]).CompareTo(Convert.ToInt32(left["score"]));
});
return new Dictionary<string, object> { { "windows", windows } };
}
public static bool TryParseHwnd(string text, out IntPtr hwnd)
{
hwnd = IntPtr.Zero;
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
string normalized = text.Trim();
try
{
long value;
if (normalized.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
value = Convert.ToInt64(normalized.Substring(2), 16);
}
else
{
value = Convert.ToInt64(normalized, 10);
}
hwnd = new IntPtr(value);
return hwnd != IntPtr.Zero;
}
catch
{
return false;
}
}
public static string FormatHwnd(IntPtr hwnd)
{
return "0x" + hwnd.ToInt64().ToString("X");
}
private static string GetWindowTextValue(IntPtr hwnd)
{
var buffer = new StringBuilder(512);
GetWindowText(hwnd, buffer, buffer.Capacity);
return buffer.ToString();
}
private static string GetClassNameValue(IntPtr hwnd)
{
var buffer = new StringBuilder(256);
GetClassName(hwnd, buffer, buffer.Capacity);
return buffer.ToString();
}
private static string GetProcessName(int pid)
{
try
{
using (Process process = Process.GetProcessById(pid))
{
return process.ProcessName;
}
}
catch
{
return "";
}
}
private static int ScoreWindow(string processName, string title, string className)
{
int score = 0;
string combined = ((processName ?? "") + " " + (title ?? "") + " " + (className ?? "")).ToLowerInvariant();
if (combined.Contains("implatformclient")) score += 60;
if (combined.Contains("isphere")) score += 50;
if (combined.Contains("impp")) score += 30;
if (combined.Contains("importal")) score += 20;
if ((className ?? "").IndexOf("WindowsForms", StringComparison.OrdinalIgnoreCase) >= 0) score += 5;
return score;
}
}
}