using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Windows.Automation; namespace ISphereWinHelper { internal static class SendConnectorPreflight { private sealed class EntrypointSpec { public readonly string Id; public readonly string Capability; public readonly string AssemblyName; public readonly string TypeName; public readonly string MethodName; public readonly bool Required; public EntrypointSpec(string id, string capability, string assemblyName, string typeName, string methodName, bool required) { Id = id; Capability = capability; AssemblyName = assemblyName; TypeName = typeName; MethodName = methodName; Required = required; } } private sealed class UiaMatch { public string Path = ""; public string ControlType = ""; public string AutomationId = ""; public string ClassName = ""; public string SafeName = ""; public bool IsEnabled; public bool IsOffscreen; public Dictionary Bounds = new Dictionary(); public Dictionary ToDictionary() { return new Dictionary { { "path", Path }, { "control_type", ControlType }, { "automation_id", AutomationId }, { "class_name", ClassName }, { "safe_name", SafeName }, { "is_enabled", IsEnabled }, { "is_offscreen", IsOffscreen }, { "bounds", Bounds } }; } } private static readonly EntrypointSpec[] DefaultEntrypoints = new[] { new EntrypointSpec( "text_high_level_by_jid", "send_message", "IMPlatformClient.exe", "IMPP.Client.Core.Plugins.Manager.AppContextManager", "SendTxtMessageByJid", true), new EntrypointSpec( "text_message_scheduler", "send_message", "IMPlatformClient.exe", "IMPP.Client.Core.MessageEngine.MessageScheduling", "SendChatMessage", true), new EntrypointSpec( "text_wire_chat_send", "send_message", "smack.dll", "com.vision.smack.Chat", "SendMessage", true), new EntrypointSpec( "file_service_upload", "send_file", "IMPP.Service.dll", "IMPP.Service.BLL.FileTransfer", "FileUpload", true), new EntrypointSpec( "file_service_upload_async", "send_file", "IMPP.Service.dll", "IMPP.Service.BLL.FileTransfer", "AsynFileUpload", false), new EntrypointSpec( "file_offline_send", "send_file", "IMPlatformClient.exe", "IMPP.Client.OffLineFileSend", "sendFile", true), new EntrypointSpec( "file_wire_message", "send_file", "smack.dll", "com.vision.smack.Chat", "sendFileMessage", false) }; public static Dictionary ProbeSendEntrypoints(Dictionary args) { var installDirs = CandidateInstallDirs(args); var assemblyPaths = ResolveAssemblyPaths(installDirs); var assemblySummaries = SummarizeAssemblies(assemblyPaths); var entrypoints = new List>(); int requiredCount = 0; int requiredAvailable = 0; int availableCount = 0; ResolveEventHandler resolveHandler = delegate(object sender, ResolveEventArgs eventArgs) { return ResolveReflectionOnlyAssembly(eventArgs, installDirs); }; AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveHandler; try { foreach (EntrypointSpec spec in DefaultEntrypoints) { if (spec.Required) { requiredCount++; } Dictionary result = ProbeEntrypoint(spec, assemblyPaths); bool available = GetBool(result, "available"); if (available) { availableCount++; if (spec.Required) { requiredAvailable++; } } entrypoints.Add(result); } } finally { AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveHandler; } return new Dictionary { { "probe_mode", "read_only_send_entrypoint_metadata" }, { "safety", SafetyData() }, { "install_dirs_checked", installDirs }, { "assemblies", assemblySummaries }, { "entrypoints", entrypoints }, { "summary", new Dictionary { { "entrypoint_count", entrypoints.Count }, { "available_count", availableCount }, { "required_count", requiredCount }, { "required_available_count", requiredAvailable }, { "all_required_available", requiredCount > 0 && requiredAvailable == requiredCount }, { "b_route_entrypoint_preflight", requiredCount > 0 && requiredAvailable == requiredCount ? "pass" : "incomplete" } } } }; } public static Dictionary ProbeSendUiaControls(string requestId, string op, Dictionary 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; int maxChildren = HelperProtocol.GetInt(args, "max_children", 200); if (maxChildren < 1) maxChildren = 1; if (maxChildren > 1000) maxChildren = 1000; try { AutomationElement root = AutomationElement.FromHandle(hwnd); if (root == null) { return HelperProtocol.Failure(requestId, op, "WINDOW_NOT_FOUND", "no UI Automation element for hwnd"); } Dictionary data = ClassifyUiaTree(hwnd, root, maxDepth, maxChildren); return HelperProtocol.Success(requestId, op, data); } catch (Exception ex) { return HelperProtocol.Failure(requestId, op, "UIA_SEND_PREFLIGHT_FAILED", ex.Message); } } private static Dictionary ClassifyUiaTree(IntPtr hwnd, AutomationElement root, int maxDepth, int maxChildren) { UiaMatch searchEdit = null; UiaMatch receiveDocument = null; UiaMatch sendEditor = null; UiaMatch sendButton = null; UiaMatch fileMenu = null; UiaMatch offlineBlocker = null; int visited = 0; bool truncated = false; Traverse(root, "root", 0, maxDepth, maxChildren, ref visited, ref truncated, delegate(AutomationElement element, string path) { string automationId = SafeString(delegate { return element.Current.AutomationId; }); string name = SafeString(delegate { return element.Current.Name; }); string value = SafeValue(element); string controlType = SafeControlType(element); string combined = (automationId + " " + name + " " + value).ToLowerInvariant(); if (searchEdit == null && (StringEquals(automationId, "skinAlphaTxt") || name.IndexOf("按帐号、姓名、拼音、标签查询联系人", StringComparison.OrdinalIgnoreCase) >= 0)) { searchEdit = MatchFrom(element, path, "按帐号、姓名、拼音、标签查询联系人"); } if (receiveDocument == null && StringEquals(automationId, "rtbRecvMessage")) { receiveDocument = MatchFrom(element, path, ""); } if (sendEditor == null && StringEquals(automationId, "rtbSendMessage")) { sendEditor = MatchFrom(element, path, ""); } if (sendButton == null && (StringEquals(automationId, "btnSend") || name.IndexOf("发送(S)", StringComparison.OrdinalIgnoreCase) >= 0)) { sendButton = MatchFrom(element, path, "发送(S)"); } if (fileMenu == null && (StringEquals(automationId, "btnFile") || StringEquals(automationId, "btnSendFile") || name.IndexOf("发送文件", StringComparison.OrdinalIgnoreCase) >= 0)) { fileMenu = MatchFrom(element, path, "发送文件"); } if (offlineBlocker == null && (StringEquals(automationId, "offlineSendBlocker") || combined.IndexOf("离线状态", StringComparison.OrdinalIgnoreCase) >= 0 || combined.IndexOf("无法发送消息", StringComparison.OrdinalIgnoreCase) >= 0)) { offlineBlocker = MatchFrom(element, path, "您已处于离线状态,无法发送消息,请上线后再次尝试!"); } }); bool hasSearchEdit = searchEdit != null; bool hasSendEditor = sendEditor != null; bool hasSendButton = sendButton != null; bool hasFileMenu = fileMenu != null; bool hasOfflineBlocker = offlineBlocker != null; bool sendButtonEnabled = sendButton != null && sendButton.IsEnabled && !sendButton.IsOffscreen; string routeHint = hasOfflineBlocker ? "A_ROUTE_OFFLINE_BLOCKED" : (hasSendEditor && hasSendButton ? "A_ROUTE_CONTROLS_PRESENT" : "A_ROUTE_INCOMPLETE"); return new Dictionary { { "probe_mode", "read_only_uia_send_control_classifier" }, { "safety", SafetyData() }, { "hwnd", WindowScanner.FormatHwnd(hwnd) }, { "root", new Dictionary { { "automation_id", SafeString(delegate { return root.Current.AutomationId; }) }, { "control_type", SafeControlType(root) }, { "class_name", SafeString(delegate { return root.Current.ClassName; }) }, { "framework_id", SafeString(delegate { return root.Current.FrameworkId; }) }, { "name_present", !string.IsNullOrEmpty(SafeString(delegate { return root.Current.Name; })) } } }, { "flags", new Dictionary { { "has_search_edit", hasSearchEdit }, { "has_receive_document", receiveDocument != null }, { "has_send_editor", hasSendEditor }, { "has_send_button", hasSendButton }, { "has_file_menu", hasFileMenu }, { "offline_blocker_visible", hasOfflineBlocker }, { "send_button_enabled", sendButtonEnabled }, { "route_hint", routeHint }, { "visited_nodes", visited }, { "truncated", truncated } } }, { "controls", new Dictionary { { "search_edit", MatchOrNull(searchEdit) }, { "receive_document", MatchOrNull(receiveDocument) }, { "send_editor", MatchOrNull(sendEditor) }, { "send_button", MatchOrNull(sendButton) }, { "file_menu", MatchOrNull(fileMenu) }, { "offline_blocker", MatchOrNull(offlineBlocker) } } } }; } private delegate void ElementVisitor(AutomationElement element, string path); private static void Traverse(AutomationElement element, string path, int depth, int maxDepth, int maxChildren, ref int visited, ref bool truncated, ElementVisitor visitor) { if (element == null) { return; } visited++; visitor(element, path); if (depth >= maxDepth) { return; } AutomationElementCollection children; try { children = element.FindAll(TreeScope.Children, Condition.TrueCondition); } catch { return; } int limit = Math.Min(children.Count, maxChildren); if (children.Count > limit) { truncated = true; } for (int i = 0; i < limit; i++) { Traverse(children[i], path + "/" + i.ToString(CultureInfo.InvariantCulture), depth + 1, maxDepth, maxChildren, ref visited, ref truncated, visitor); } } private static UiaMatch MatchFrom(AutomationElement element, string path, string safeName) { return new UiaMatch { Path = path, ControlType = SafeControlType(element), AutomationId = SafeString(delegate { return element.Current.AutomationId; }), ClassName = SafeString(delegate { return element.Current.ClassName; }), SafeName = safeName, IsEnabled = SafeBool(delegate { return element.Current.IsEnabled; }), IsOffscreen = SafeBool(delegate { return element.Current.IsOffscreen; }), Bounds = SafeBounds(element) }; } private static object MatchOrNull(UiaMatch match) { if (match == null) { return null; } return match.ToDictionary(); } private static Dictionary ProbeEntrypoint(EntrypointSpec spec, Dictionary assemblyPaths) { string assemblyPath = ""; assemblyPaths.TryGetValue(spec.AssemblyName, out assemblyPath); bool assemblyFound = !string.IsNullOrEmpty(assemblyPath) && File.Exists(assemblyPath); bool reflectionTypeFound = false; bool reflectionMethodFound = false; string reflectionError = ""; var signatures = new List(); if (assemblyFound) { try { Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyPath); Type targetType = FindType(assembly, spec.TypeName); reflectionTypeFound = targetType != null; if (targetType != null) { MethodInfo[] methods = targetType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance); foreach (MethodInfo method in methods) { if (!StringEquals(method.Name, spec.MethodName)) { continue; } reflectionMethodFound = true; if (signatures.Count < 8) { signatures.Add(MethodSignature(method)); } } } } catch (Exception ex) { reflectionError = ex.GetType().Name + ": " + ex.Message; } } bool stringTypeHint = false; bool stringFullTypeHint = false; bool stringShortTypeHint = false; bool stringMethodHint = false; string stringError = ""; if (assemblyFound) { try { byte[] bytes = File.ReadAllBytes(assemblyPath); string ascii = Encoding.ASCII.GetString(bytes); string unicode = Encoding.Unicode.GetString(bytes); string shortTypeName = ShortTypeName(spec.TypeName); stringFullTypeHint = ascii.IndexOf(spec.TypeName, StringComparison.OrdinalIgnoreCase) >= 0 || unicode.IndexOf(spec.TypeName, StringComparison.OrdinalIgnoreCase) >= 0 || ascii.IndexOf(spec.TypeName.Replace(".", "/"), StringComparison.OrdinalIgnoreCase) >= 0; stringShortTypeHint = !string.IsNullOrEmpty(shortTypeName) && (ascii.IndexOf(shortTypeName, StringComparison.OrdinalIgnoreCase) >= 0 || unicode.IndexOf(shortTypeName, StringComparison.OrdinalIgnoreCase) >= 0); stringTypeHint = stringFullTypeHint || stringShortTypeHint; stringMethodHint = ascii.IndexOf(spec.MethodName, StringComparison.OrdinalIgnoreCase) >= 0 || unicode.IndexOf(spec.MethodName, StringComparison.OrdinalIgnoreCase) >= 0; } catch (Exception ex) { stringError = ex.GetType().Name + ": " + ex.Message; } } bool available = assemblyFound && ((reflectionTypeFound && reflectionMethodFound) || (stringTypeHint && stringMethodHint)); return new Dictionary { { "id", spec.Id }, { "capability", spec.Capability }, { "assembly", spec.AssemblyName }, { "assembly_found", assemblyFound }, { "assembly_path_present", assemblyFound }, { "type_name", spec.TypeName }, { "method_name", spec.MethodName }, { "required", spec.Required }, { "reflection_type_found", reflectionTypeFound }, { "reflection_method_found", reflectionMethodFound }, { "reflection_error", reflectionError }, { "string_type_hint", stringTypeHint }, { "string_full_type_hint", stringFullTypeHint }, { "string_short_type_hint", stringShortTypeHint }, { "string_method_hint", stringMethodHint }, { "string_scan_error", stringError }, { "available", available }, { "signatures", signatures } }; } private static Type FindType(Assembly assembly, string typeName) { try { Type exact = assembly.GetType(typeName, false, true); if (exact != null) { return exact; } } catch { } try { foreach (Type type in assembly.GetTypes()) { if (StringEquals(type.FullName, typeName) || StringEquals(type.Name, typeName)) { return type; } } } catch (ReflectionTypeLoadException ex) { foreach (Type type in ex.Types) { if (type != null && (StringEquals(type.FullName, typeName) || StringEquals(type.Name, typeName))) { return type; } } } catch { } return null; } private static string ShortTypeName(string typeName) { if (string.IsNullOrEmpty(typeName)) { return ""; } int dot = typeName.LastIndexOf('.'); int plus = typeName.LastIndexOf('+'); int slash = typeName.LastIndexOf('/'); int cut = Math.Max(dot, Math.Max(plus, slash)); return cut >= 0 && cut + 1 < typeName.Length ? typeName.Substring(cut + 1) : typeName; } private static string MethodSignature(MethodInfo method) { var sb = new StringBuilder(); sb.Append(method.IsStatic ? "static " : "instance "); sb.Append(method.Name); sb.Append("("); ParameterInfo[] parameters = method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(SafeTypeName(parameters[i].ParameterType)); } sb.Append(") -> "); sb.Append(SafeTypeName(method.ReturnType)); return sb.ToString(); } private static string SafeTypeName(Type type) { try { return type.FullName ?? type.Name; } catch { return ""; } } private static Assembly ResolveReflectionOnlyAssembly(ResolveEventArgs args, List installDirs) { try { string simpleName = new AssemblyName(args.Name).Name + ".dll"; foreach (string dir in installDirs) { string candidate = Path.Combine(dir, simpleName); if (File.Exists(candidate)) { return Assembly.ReflectionOnlyLoadFrom(candidate); } } } catch { } return null; } private static List CandidateInstallDirs(Dictionary args) { var dirs = new List(); string explicitDir = HelperProtocol.GetString(args, "install_dir", ""); AddDir(dirs, explicitDir); foreach (string item in GetStringList(args, "install_dirs")) { AddDir(dirs, item); } foreach (Process process in Process.GetProcessesByName("IMPlatformClient")) { using (process) { try { if (process.MainModule != null) { AddDir(dirs, Path.GetDirectoryName(process.MainModule.FileName)); } } catch { } } } AddDir(dirs, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Impp")); AddDir(dirs, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Impp")); return dirs; } private static void AddDir(List dirs, string dir) { if (string.IsNullOrWhiteSpace(dir)) { return; } try { string full = Path.GetFullPath(dir); foreach (string existing in dirs) { if (StringEquals(existing, full)) { return; } } dirs.Add(full); } catch { } } private static Dictionary ResolveAssemblyPaths(List dirs) { var paths = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (EntrypointSpec spec in DefaultEntrypoints) { if (!paths.ContainsKey(spec.AssemblyName)) { paths.Add(spec.AssemblyName, ""); } } foreach (string dir in dirs) { foreach (EntrypointSpec spec in DefaultEntrypoints) { if (!string.IsNullOrEmpty(paths[spec.AssemblyName])) { continue; } string candidate = Path.Combine(dir, spec.AssemblyName); if (File.Exists(candidate)) { paths[spec.AssemblyName] = candidate; } } } return paths; } private static List> SummarizeAssemblies(Dictionary paths) { var list = new List>(); foreach (KeyValuePair item in paths) { bool exists = !string.IsNullOrEmpty(item.Value) && File.Exists(item.Value); string version = ""; long length = 0; string sha256 = ""; if (exists) { try { var info = new FileInfo(item.Value); length = info.Length; FileVersionInfo vi = FileVersionInfo.GetVersionInfo(item.Value); version = vi == null ? "" : (vi.FileVersion ?? ""); sha256 = Sha256(item.Value); } catch { } } list.Add(new Dictionary { { "name", item.Key }, { "found", exists }, { "path_present", exists }, { "length", length }, { "file_version", version }, { "sha256", sha256 } }); } return list; } private static Dictionary SafetyData() { return new Dictionary { { "sent_message", false }, { "sent_file", false }, { "uploaded_file", false }, { "clicked_ui", false }, { "typed_text", false }, { "captured_network", false }, { "attached_hook", false }, { "modified_client_data", false } }; } private static List GetStringList(Dictionary source, string key) { var values = new List(); object raw; if (source != null && source.TryGetValue(key, out raw) && raw != null) { object[] rawArray = raw as object[]; if (rawArray != null) { foreach (object item in rawArray) { string value = Convert.ToString(item); if (!string.IsNullOrWhiteSpace(value)) { values.Add(value.Trim()); } } return values; } string single = Convert.ToString(raw); if (!string.IsNullOrWhiteSpace(single)) { foreach (string part in single.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)) { string value = part.Trim(); if (!string.IsNullOrWhiteSpace(value)) { values.Add(value); } } } } return values; } 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 SafeBounds(AutomationElement element) { try { System.Windows.Rect rect = element.Current.BoundingRectangle; return new Dictionary { { "x", rect.X }, { "y", rect.Y }, { "width", rect.Width }, { "height", rect.Height } }; } catch { return new Dictionary { { "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 ""; } private static bool StringEquals(string left, string right) { return string.Equals(left ?? "", right ?? "", StringComparison.OrdinalIgnoreCase); } private static bool GetBool(Dictionary source, string key) { object value; if (source.TryGetValue(key, out value) && value != null) { try { return Convert.ToBoolean(value); } catch { } } return false; } private static string Sha256(string path) { try { using (SHA256 sha = SHA256.Create()) using (FileStream stream = File.OpenRead(path)) { byte[] hash = sha.ComputeHash(stream); var sb = new StringBuilder(hash.Length * 2); foreach (byte b in hash) { sb.Append(b.ToString("x2", CultureInfo.InvariantCulture)); } return sb.ToString(); } } catch { return ""; } } } }