using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Web.Script.Serialization; using System.Windows.Forms; namespace ISphereOfflineLabHelper { internal static class Program { private const int MaxLimit = 100; [STAThread] private static int Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; try { if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") { PrintJson(new Dictionary { {"ok", true}, {"helper", "ISphereOfflineLabHelper"}, {"commands", new[] { "inspect", "probe-main-window", "start", "status", "search-users", "open-chat", "send-message", "receive-message", "send-file", "receive-file", "read-conversation" }} }); return 0; } string command = args[0]; Dictionary options = ParseOptions(args.Skip(1).ToArray()); switch (command) { case "inspect": return RunInspect(options); case "probe-main-window": return RunProbeMainWindow(options); case "start": return RunStart(options); case "status": return RunStatus(options); case "search-users": return RunSearchUsers(options); case "open-chat": return RunOpenChat(options); case "send-message": return RunSendMessage(options); case "receive-message": return RunReceiveMessage(options); case "send-file": return RunSendFile(options); case "receive-file": return RunReceiveFile(options); case "read-conversation": return RunReadConversation(options); default: throw new ArgumentException("unknown command: " + command); } } catch (Exception ex) { PrintJson(new Dictionary { {"ok", false}, {"error", ex.GetType().FullName}, {"message", ex.Message}, {"stack", ex.ToString()} }); return 1; } } private static int RunInspect(Dictionary options) { string clientDir = Required(options, "client-dir"); Dictionary result = NativeInspector.Inspect(clientDir); PrintJson(result); return Convert.ToBoolean(result["ok"]) ? 0 : 2; } private static int RunProbeMainWindow(Dictionary options) { string clientDir = Required(options, "client-dir"); bool construct = HasFlag(options, "construct"); bool probeChat = HasFlag(options, "probe-chat"); bool probeActions = HasFlag(options, "probe-actions"); bool probeFiles = HasFlag(options, "probe-files"); bool probeSendText = probeActions || HasFlag(options, "probe-send-text"); bool probeReceiveText = probeActions || HasFlag(options, "probe-receive-text"); bool probeSendFile = probeFiles || HasFlag(options, "probe-send-file"); bool probeReceiveFile = probeFiles || HasFlag(options, "probe-receive-file"); if (probeChat || probeSendText || probeReceiveText || probeSendFile || probeReceiveFile) { construct = true; } string userJid = options.ContainsKey("user-jid") ? options["user-jid"] : "mock-bob@offline.lab"; string text = options.ContainsKey("text") ? options["text"] : "native helper direct chat probe"; string filePath = options.ContainsKey("file-path") ? options["file-path"] : ""; string fileName = options.ContainsKey("file-name") ? options["file-name"] : ""; long fileSize = options.ContainsKey("file-size") ? long.Parse(options["file-size"]) : 0L; int timeoutMs = options.ContainsKey("timeout-ms") ? int.Parse(options["timeout-ms"]) : 10000; Dictionary result = NativeMainWindowProbe.Probe(clientDir, construct, timeoutMs, probeChat, probeActions, probeFiles, probeSendText, probeReceiveText, probeSendFile, probeReceiveFile, userJid, text, filePath, fileName, fileSize); PrintJson(result); Console.Out.Flush(); Console.Error.Flush(); int exitCode = Convert.ToBoolean(result["ok"]) ? 0 : 3; Environment.Exit(exitCode); return exitCode; } private static int RunStart(Dictionary options) { string clientDir = Required(options, "client-dir"); string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); bool reset = HasFlag(options, "reset"); string statePath = StatePath(workspace); if (File.Exists(statePath) && !reset) { OfflineState reused = LoadState(statePath); reused.main_window.active_panel = "contacts"; reused.updated_at = Now(); reused.audit.Add(Audit("native_lab_reopened", new Dictionary { {"workspace", FullPath(workspace)} })); SaveState(statePath, reused); PrintJson(SessionResponse(reused, statePath, true)); return 0; } Dictionary inspection = NativeInspector.Inspect(clientDir); OfflineState state = OfflineState.Create(clientDir); state.native_bridge.status = Convert.ToBoolean(inspection["ok"]) ? "inspected" : "inspect-failed"; state.native_bridge.backend = "dotnet-framework-helper"; state.native_bridge.client_dir = FullPath(clientDir); state.native_bridge.inspection = inspection; state.audit.Add(Audit("native_lab_started", new Dictionary { {"client_dir", FullPath(clientDir)}, {"inspect_ok", inspection["ok"]} })); SaveState(statePath, state); PrintJson(SessionResponse(state, statePath, false)); return 0; } private static int RunStatus(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string statePath = StatePath(workspace); if (!File.Exists(statePath)) { PrintJson(new Dictionary { {"started", false}, {"workspace", FullPath(workspace)}, {"state_path", FullPath(statePath)}, {"next_step", "run start --client-dir "} }); return 0; } OfflineState state = LoadState(statePath); PrintJson(new Dictionary { {"started", true}, {"session_id", state.session_id}, {"mode", state.mode}, {"workspace", FullPath(workspace)}, {"state_path", FullPath(statePath)}, {"main_window", state.main_window}, {"active_conversation_id", state.active_conversation_id}, {"user_count", state.users.Count}, {"conversation_count", state.conversations.Count}, {"message_count", state.conversations.Values.Sum(c => c.messages.Count)}, {"native_bridge", state.native_bridge}, {"updated_at", state.updated_at} }); return 0; } private static int RunSearchUsers(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string query = Get(options, "query", ""); int limit = BoundedInt(Get(options, "limit", "20")); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); string q = query.ToLowerInvariant(); List users = state.users .Where(u => string.IsNullOrWhiteSpace(q) || (u.user_id + " " + u.jid + " " + u.name + " " + u.department + " " + u.company_id).ToLowerInvariant().Contains(q)) .Take(limit) .ToList(); state.audit.Add(Audit("native_lab_search_users", new Dictionary { {"query", query}, {"result_count", users.Count} })); state.updated_at = Now(); SaveState(statePath, state); PrintJson(new Dictionary { {"mode", state.mode}, {"query", query}, {"limit", limit}, {"count", users.Count}, {"users", users} }); return 0; } private static int RunOpenChat(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); state.active_conversation_id = conversation.conversation_id; state.main_window.active_panel = "p2p-chat"; state.updated_at = Now(); state.audit.Add(Audit("native_lab_open_chat", new Dictionary { {"conversation_id", conversation.conversation_id}, {"user_jid", userJid} })); SaveState(statePath, state); PrintJson(new Dictionary { {"opened", true}, {"mode", state.mode}, {"main_window", state.main_window}, {"conversation", ConversationView(conversation, 0)} }); return 0; } private static int RunSendMessage(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); string text = Required(options, "text"); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); MessageRecord message = MakeMessage(conversation, "outgoing", "offline-operator", "离线操作员", "text", text, new List()); conversation.messages.Add(message); state.active_conversation_id = conversation.conversation_id; state.updated_at = Now(); state.audit.Add(Audit("native_lab_send_message_simulated", new Dictionary { {"conversation_id", conversation.conversation_id}, {"message_id", message.message_id}, {"content_type", "text"} })); SaveState(statePath, state); PrintJson(new Dictionary { {"sent", true}, {"simulated", true}, {"mode", state.mode}, {"transport", "native-helper-state"}, {"message", message} }); return 0; } private static int RunReceiveMessage(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); string text = Required(options, "text"); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); MessageRecord message = MakeMessage(conversation, "incoming", user.user_id, user.name, "text", text, new List()); conversation.messages.Add(message); state.active_conversation_id = conversation.conversation_id; state.updated_at = Now(); state.audit.Add(Audit("native_lab_receive_message_simulated", new Dictionary { {"conversation_id", conversation.conversation_id}, {"message_id", message.message_id}, {"content_type", "text"} })); SaveState(statePath, state); PrintJson(new Dictionary { {"received", true}, {"simulated", true}, {"mode", state.mode}, {"transport", "native-helper-state"}, {"message", message} }); return 0; } private static int RunSendFile(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); string filePath = Required(options, "file-path"); string fileName = Get(options, "file-name", Path.GetFileName(filePath)); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); FileInfo info = new FileInfo(filePath); AttachmentRecord attachment = new AttachmentRecord { name = fileName, path = filePath, exists = info.Exists, size = info.Exists ? info.Length : 0, direction = "outgoing", transfer_state = "simulated-sent" }; MessageRecord message = MakeMessage(conversation, "outgoing", "offline-operator", "离线操作员", "file", fileName, new List { attachment }); conversation.messages.Add(message); state.active_conversation_id = conversation.conversation_id; state.updated_at = Now(); state.audit.Add(Audit("native_lab_send_file_simulated", new Dictionary { {"conversation_id", conversation.conversation_id}, {"message_id", message.message_id}, {"file_name", fileName}, {"exists", attachment.exists} })); SaveState(statePath, state); PrintJson(new Dictionary { {"sent", true}, {"simulated", true}, {"mode", state.mode}, {"transport", "native-helper-state"}, {"file_event", message} }); return 0; } private static int RunReceiveFile(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); string fileName = Required(options, "file-name"); long size = BoundedLong(Get(options, "size", "0")); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); AttachmentRecord attachment = new AttachmentRecord { name = fileName, path = "", exists = false, size = size, direction = "incoming", transfer_state = "simulated-received" }; MessageRecord message = MakeMessage(conversation, "incoming", user.user_id, user.name, "file", fileName, new List { attachment }); conversation.messages.Add(message); state.active_conversation_id = conversation.conversation_id; state.updated_at = Now(); state.audit.Add(Audit("native_lab_receive_file_simulated", new Dictionary { {"conversation_id", conversation.conversation_id}, {"message_id", message.message_id}, {"file_name", fileName}, {"size", size} })); SaveState(statePath, state); PrintJson(new Dictionary { {"received", true}, {"simulated", true}, {"mode", state.mode}, {"transport", "native-helper-state"}, {"file_event", message} }); return 0; } private static int RunReadConversation(Dictionary options) { string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab")); string userJid = Required(options, "user-jid"); int limit = BoundedInt(Get(options, "limit", "50")); string statePath = StatePath(workspace); OfflineState state = RequireState(statePath); UserRecord user = RequireUser(state, userJid); Conversation conversation = EnsureConversation(state, user); PrintJson(new Dictionary { {"mode", state.mode}, {"limit", limit}, {"conversation", ConversationView(conversation, limit)} }); return 0; } private static Dictionary SessionResponse(OfflineState state, string statePath, bool reused) { return new Dictionary { {"started", true}, {"reused", reused}, {"session_id", state.session_id}, {"mode", state.mode}, {"workspace", FullPath(Path.GetDirectoryName(statePath))}, {"state_path", FullPath(statePath)}, {"main_window", state.main_window}, {"user_count", state.users.Count}, {"native_bridge", state.native_bridge} }; } private static MessageRecord MakeMessage(Conversation conversation, string direction, string senderId, string senderName, string contentType, string contentText, List attachments) { string now = Now(); string messageId = direction + "-" + Guid.NewGuid().ToString("N").Substring(0, 12); return new MessageRecord { message_id = messageId, conversation_id = conversation.conversation_id, conversation_name = conversation.conversation_name, conversation_type = conversation.conversation_type, sender_id = senderId, sender_name = senderName, direction = direction, content_type = contentType, content_text = contentText, attachments = attachments, created_at = now, received_at = now, source = "native-helper-mock", raw_ref = "native-helper:" + conversation.conversation_id + ":" + messageId, ui_hook = UiHook(contentType, direction) }; } private static string UiHook(string contentType, string direction) { if (contentType == "file" && direction == "incoming") { return "frmP2PChat.ProccessRecvMessage -> UC_FileReceive"; } if (contentType == "file") { return "frmP2PChat.SendTcpFiles/SendOfflineFiles -> UC_FileSend"; } if (direction == "incoming") { return "frmP2PChat.ProccessRecvMessage"; } return "frmP2PChat.SaveAndShowMessage"; } private static ConversationView ConversationView(Conversation conversation, int limit) { List messages = limit <= 0 ? new List() : conversation.messages.Skip(Math.Max(0, conversation.messages.Count - limit)).ToList(); return new ConversationView { conversation_id = conversation.conversation_id, conversation_name = conversation.conversation_name, conversation_type = conversation.conversation_type, user_jid = conversation.user_jid, messages = messages, message_count = conversation.messages.Count }; } private static Conversation EnsureConversation(OfflineState state, UserRecord user) { if (!state.conversations.ContainsKey(user.jid)) { state.conversations[user.jid] = Conversation.Create(user); } return state.conversations[user.jid]; } private static UserRecord RequireUser(OfflineState state, string userJid) { UserRecord user = state.users.FirstOrDefault(u => u.jid == userJid); if (user == null) { throw new ArgumentException("unknown offline lab user_jid: " + userJid); } return user; } private static OfflineState RequireState(string statePath) { if (!File.Exists(statePath)) { throw new FileNotFoundException("offline lab state not found; run start first", statePath); } return LoadState(statePath); } private static OfflineState LoadState(string statePath) { JavaScriptSerializer serializer = new JavaScriptSerializer(); return serializer.Deserialize(File.ReadAllText(statePath, Encoding.UTF8)); } private static void SaveState(string statePath, OfflineState state) { Directory.CreateDirectory(Path.GetDirectoryName(FullPath(statePath))); JavaScriptSerializer serializer = new JavaScriptSerializer(); serializer.MaxJsonLength = int.MaxValue; File.WriteAllText(statePath, serializer.Serialize(state), Encoding.UTF8); } private static AuditRecord Audit(string eventType, Dictionary data) { return new AuditRecord { event_id = "audit-" + Guid.NewGuid().ToString("N").Substring(0, 12), event_type = eventType, created_at = Now(), data = data }; } private static int BoundedInt(string value) { int parsed; if (!int.TryParse(value, out parsed) || parsed <= 0) { return 0; } return Math.Min(parsed, MaxLimit); } private static long BoundedLong(string value) { long parsed; if (!long.TryParse(value, out parsed) || parsed <= 0) { return 0; } return parsed; } private static Dictionary ParseOptions(string[] args) { Dictionary options = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < args.Length; i++) { string arg = args[i]; if (!arg.StartsWith("--")) { continue; } string key = arg.Substring(2); if (i + 1 < args.Length && !args[i + 1].StartsWith("--")) { options[key] = args[++i]; } else { options[key] = "true"; } } return options; } private static string Required(Dictionary options, string key) { if (!options.ContainsKey(key) || string.IsNullOrWhiteSpace(options[key])) { throw new ArgumentException("missing --" + key); } return options[key]; } private static string Get(Dictionary options, string key, string defaultValue) { return options.ContainsKey(key) ? options[key] : defaultValue; } private static bool HasFlag(Dictionary options, string key) { return options.ContainsKey(key) && options[key].Equals("true", StringComparison.OrdinalIgnoreCase); } private static string StatePath(string workspace) { return Path.Combine(workspace, "native-session.json"); } private static string FullPath(string path) { return Path.GetFullPath(path); } private static string Now() { return DateTimeOffset.Now.ToString("o"); } private static void PrintJson(object value) { JavaScriptSerializer serializer = new JavaScriptSerializer(); serializer.MaxJsonLength = int.MaxValue; Console.WriteLine(serializer.Serialize(value)); } } internal static class NativeInspector { public static Dictionary Inspect(string clientDir) { string fullClientDir = Path.GetFullPath(clientDir); string exe = Path.Combine(fullClientDir, "IMPlatformClient.exe"); Dictionary result = new Dictionary { {"ok", false}, {"client_dir", fullClientDir}, {"client_exe", exe}, {"client_exe_exists", File.Exists(exe)}, {"mode", "reflection-only-inspection"}, {"types", new List>()}, {"required_ready", false} }; if (!File.Exists(exe)) { result["error"] = "IMPlatformClient.exe not found"; return result; } AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs args) { AssemblyName name = new AssemblyName(args.Name); string dll = Path.Combine(fullClientDir, name.Name + ".dll"); if (File.Exists(dll)) { return Assembly.LoadFrom(dll); } string exeCandidate = Path.Combine(fullClientDir, name.Name + ".exe"); if (File.Exists(exeCandidate)) { return Assembly.LoadFrom(exeCandidate); } return null; }; List loadErrors = new List(); foreach (string dll in Directory.GetFiles(fullClientDir, "*.dll")) { try { Assembly.LoadFrom(dll); } catch (Exception ex) { loadErrors.Add(Path.GetFileName(dll) + ": " + ex.GetType().Name + ": " + ex.Message); } } Assembly clientAssembly = Assembly.LoadFrom(exe); TypeProbe[] probes = new[] { new TypeProbe("IMPP.Client.Program", true, new[] {"Main"}, new string[0], false), new TypeProbe("IMPP.Client.frmLogin", true, new[] {"LogonSuccessUI"}, new string[0], true), new TypeProbe("IMPP.Client.frmMain", true, new string[0], new[] {"frmLogin,NotifyIcon"}, true), new TypeProbe("IMPP.Client.IMPPManager", true, new[] {"Init", "UpdateUserInfo"}, new string[0], true), new TypeProbe("IMPP.Client.LoginInfo", true, new string[0], new string[0], true), new TypeProbe("IMPP.Common.UserInfo", true, new string[0], new string[0], true), new TypeProbe("IMPP.Client.frmSearchLinkman", false, new[] {"btnSearch_Click", "searchBw_DoWork"}, new string[0], true), new TypeProbe("IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat", false, new[] {"SendMessage", "SaveAndShowMessage", "ProccessRecvMessage", "SendTcpFiles", "SendOfflineFiles"}, new string[0], true), new TypeProbe("IMPP.Client.UC_FileSend", false, new string[0], new string[0], true), new TypeProbe("IMPP.Client.UC_FileReceive", false, new string[0], new string[0], true) }; List> typeResults = new List>(); foreach (TypeProbe probe in probes) { typeResults.Add(InspectType(clientAssembly, probe)); } bool requiredReady = typeResults.Where(t => Convert.ToBoolean(t["required"])) .All(t => Convert.ToBoolean(t["exists"]) && Convert.ToBoolean(t["members_ok"])); result["ok"] = requiredReady; result["types"] = typeResults; result["load_errors"] = loadErrors; result["required_ready"] = requiredReady; result["reverse_map"] = new Dictionary { {"open_main_window", "frmLogin.LogonSuccessUI -> new frmMain(frmLogin, notifyIcon)"}, {"send_text_offline", "frmP2PChat.SaveAndShowMessage"}, {"receive_text_offline", "frmP2PChat.ProccessRecvMessage"}, {"send_file_offline", "frmP2PChat.SendTcpFiles/SendOfflineFiles -> UC_FileSend"}, {"receive_file_offline", "frmP2PChat.ProccessRecvMessage -> UC_FileReceive"} }; return result; } private static Dictionary InspectType(Assembly clientAssembly, TypeProbe probe) { Type type = ResolveType(clientAssembly, probe.Name); Dictionary result = new Dictionary { {"name", probe.Name}, {"required", probe.Required}, {"exists", type != null}, {"members_ok", false}, {"methods", new List>()}, {"constructors", new List>()} }; if (type == null) { return result; } bool methodsOk = true; List> methodResults = new List>(); foreach (string methodName in probe.MethodNames) { MethodInfo method = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) .FirstOrDefault(m => m.Name == methodName); bool exists = method != null; methodsOk = methodsOk && exists; methodResults.Add(new Dictionary { {"name", methodName}, {"exists", exists}, {"is_public", exists && method.IsPublic}, {"is_static", exists && method.IsStatic} }); } bool constructorsOk = true; List> ctorResults = new List>(); foreach (string ctorLabel in probe.ConstructorLabels) { ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); bool exists = constructors.Any(c => c.GetParameters().Length == 2); constructorsOk = constructorsOk && exists; ctorResults.Add(new Dictionary { {"label", ctorLabel}, {"exists", exists} }); } result["methods"] = methodResults; result["constructors"] = ctorResults; result["members_ok"] = methodsOk && constructorsOk; return result; } private static Type ResolveType(Assembly clientAssembly, string fullName) { Type type = clientAssembly.GetType(fullName, false); if (type != null) { return type; } foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { type = assembly.GetType(fullName, false); if (type != null) { return type; } } return null; } private sealed class TypeProbe { public readonly string Name; public readonly bool Required; public readonly string[] MethodNames; public readonly string[] ConstructorLabels; public TypeProbe(string name, bool required, string[] methodNames, string[] constructorLabels, bool unused) { Name = name; Required = required; MethodNames = methodNames; ConstructorLabels = constructorLabels; } } } internal static class NativeMainWindowProbe { public static Dictionary Probe(string clientDir, bool construct, int timeoutMs, bool probeChat, bool probeActions, bool probeFiles, bool probeSendText, bool probeReceiveText, bool probeSendFile, bool probeReceiveFile, string userJid, string text, string filePath, string fileName, long fileSize) { string fullClientDir = Path.GetFullPath(clientDir); List> steps = new List>(); Dictionary result = new Dictionary { {"ok", false}, {"client_dir", fullClientDir}, {"mode", construct ? "bootstrap-and-construct-frmMain" : "bootstrap-only"}, {"construct_requested", construct}, {"construct_timeout_ms", timeoutMs}, {"probe_chat_requested", probeChat}, {"probe_actions_requested", probeActions}, {"probe_files_requested", probeFiles}, {"probe_send_text_requested", probeSendText}, {"probe_receive_text_requested", probeReceiveText}, {"probe_send_file_requested", probeSendFile}, {"probe_receive_file_requested", probeReceiveFile}, {"probe_chat_user_jid", userJid}, {"probe_file_path", filePath}, {"probe_file_name", fileName}, {"probe_file_size", fileSize}, {"probe_receive_file_size", fileSize}, {"steps", steps}, {"main_window_constructed", false}, {"chat_model_constructed", false}, {"chat_window_constructed", false}, {"send_text_invoked", false}, {"receive_text_invoked", false}, {"file_send_control_constructed", false}, {"file_receive_control_constructed", false} }; Assembly clientAssembly = null; object loginForm = null; object mainForm = null; object p2pChat = null; object chatForm = null; object fileSendControl = null; object fileReceiveControl = null; NotifyIcon notifyIcon = null; List threadExceptions = new List(); try { Step(steps, "set_current_directory", delegate { Directory.SetCurrentDirectory(fullClientDir); return new Dictionary { { "current_directory", Directory.GetCurrentDirectory() } }; }); Step(steps, "load_client_assemblies", delegate { clientAssembly = LoadClientAssembly(fullClientDir); return new Dictionary { { "client_assembly", clientAssembly.FullName } }; }); Step(steps, "application_visual_styles", delegate { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException); Application.ThreadException += delegate(object sender, ThreadExceptionEventArgs e) { lock (threadExceptions) { threadExceptions.Add(e.Exception.ToString()); } }; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); return new Dictionary { { "enabled", true } }; }); Step(steps, "ensure_connsel_xml", delegate { string helperConnSel = EnsureConnSelXml(fullClientDir); return new Dictionary { { "path", helperConnSel } }; }); Step(steps, "configpath_init", delegate { Type configPath = ResolveType(clientAssembly, "IMPP.Helper.Config.ConfigPath"); InvokeStatic(configPath, "Init"); return new Dictionary { { "type", configPath.FullName } }; }); Step(steps, "configpath_init_organization", delegate { Type configPath = ResolveType(clientAssembly, "IMPP.Helper.Config.ConfigPath"); InvokeStatic(configPath, "InitOrganizationDirectory", "offline-org"); return new Dictionary { { "organize", "offline-org" } }; }); Step(steps, "configpath_init_user", delegate { Type configPath = ResolveType(clientAssembly, "IMPP.Helper.Config.ConfigPath"); InvokeStatic(configPath, "InitUserDirectory", "offline-org", "offline-self"); return new Dictionary { { "user", "offline-self" } }; }); Step(steps, "wincommand_init", delegate { Type winCommand = ResolveType(clientAssembly, "IMPP.Common.WinCommand"); InvokeStatic(winCommand, "Init"); return new Dictionary { { "type", winCommand.FullName } }; }, optional: true); Step(steps, "seed_config_static_values", delegate { Type config = ResolveType(clientAssembly, "IMPP.Client.Config"); SetStaticProperty(config, "OrganizeID", "offline-org"); SetStaticProperty(config, "OrganizeName", "iSphere Offline Lab"); SetStaticProperty(config, "DNS", "offline.lab"); object companies = CreateCompanyList(clientAssembly); SetStaticProperty(config, "Companys", companies); return new Dictionary { { "organize", "offline-org" }, { "company_count", 1 } }; }); Step(steps, "seed_base_config", delegate { Type config = ResolveType(clientAssembly, "IMPP.Client.Config"); Type baseConfigType = ResolveType(clientAssembly, "IMPP.Client.BaseConfig"); object baseConfig = Activator.CreateInstance(baseConfigType, CreateBaseConfigDictionary()); SetStaticProperty(config, "BaseConfig", baseConfig); return new Dictionary { { "type", baseConfigType.FullName } }; }); Step(steps, "seed_login_info", delegate { Type config = ResolveType(clientAssembly, "IMPP.Client.Config"); Type loginInfoType = ResolveType(clientAssembly, "IMPP.Client.LoginInfo"); object loginInfo = Activator.CreateInstance(loginInfoType); SetProperty(loginInfo, "Trait", "offline-self"); SetProperty(loginInfo, "TrueTraid", "offline-self"); SetProperty(loginInfo, "Riddle", "offline-riddle"); SetProperty(loginInfo, "Organize", "offline-org"); SetProperty(loginInfo, "Status", "在线"); SetProperty(loginInfo, "ProductName", "iSphere Offline Lab"); SetStaticProperty(config, "LoginInfo", loginInfo); return new Dictionary { { "trait", "offline-self" }, { "status", "在线" } }; }); object fakeUser = null; Step(steps, "seed_user_info", delegate { Type userInfoType = ResolveType(clientAssembly, "IMPP.Common.UserInfo"); fakeUser = Activator.CreateInstance(userInfoType); SetProperty(fakeUser, "Id", "offline-self"); SetProperty(fakeUser, "Jid", "offline-self@offline.lab"); SetProperty(fakeUser, "LoginName", "offline-self"); SetProperty(fakeUser, "Domain", "offline.lab"); SetProperty(fakeUser, "Name", "离线演示用户"); SetProperty(fakeUser, "CompanyID", "offline-company"); SetProperty(fakeUser, "DeptID", "offline-dept"); SetProperty(fakeUser, "DeptName", "离线实验室"); SetProperty(fakeUser, "Enable", "1"); SetProperty(fakeUser, "chatAble", "1"); Type config = ResolveType(clientAssembly, "IMPP.Client.Config"); SetStaticProperty(config, "UserInfo", fakeUser); return new Dictionary { { "jid", "offline-self@offline.lab" } }; }); object imppManager = null; Step(steps, "imppmanager_instance", delegate { Type managerType = ResolveType(clientAssembly, "IMPP.Client.IMPPManager"); imppManager = GetStaticProperty(managerType, "Instance"); return new Dictionary { { "type", managerType.FullName }, { "has_instance", imppManager != null } }; }); Step(steps, "imppmanager_init", delegate { InvokeInstance(imppManager, "Init"); return new Dictionary { { "called", "IMPPManager.Init" } }; }); Step(steps, "imppmanager_update_user", delegate { InvokeInstance(imppManager, "UpdateUserInfo", fakeUser, false); return new Dictionary { { "called", "IMPPManager.UpdateUserInfo" } }; }); Step(steps, "configsystem_get_settings", delegate { Type configSystemManager = ResolveType(clientAssembly, "IMPP.Client.ConfigSystemManager"); object settings = GetStaticProperty(configSystemManager, "GetSettingsInstance"); return new Dictionary { { "settings_created", settings != null }, { "settings_type", settings == null ? "" : settings.GetType().FullName } }; }, optional: true); if (construct) { Step(steps, "patch_problematic_ui_methods", delegate { List patched = new List(); Type ucAppPanelType = ResolveType(clientAssembly, "IMPP.Client.Core.Plugins.Manager.UCAppPanel"); MethodInfo drawClient = ucAppPanelType.GetMethod("DrawClient", BindingFlags.NonPublic | BindingFlags.Instance); MethodInfo drawClientNoop = typeof(UiPatchStubs).GetMethod("DrawClientNoop", BindingFlags.Public | BindingFlags.Instance); PatchManagedMethod(drawClient, drawClientNoop); patched.Add("IMPP.Client.Core.Plugins.Manager.UCAppPanel.DrawClient"); Type pluginManagerType = ResolveType(clientAssembly, "IMPP.Client.Core.Plugins.Manager.PluginManager"); UiPatchStubs.PluginManagerType = pluginManagerType; MethodInfo getInstance = pluginManagerType.GetMethod("GetInstance", BindingFlags.Public | BindingFlags.Static); MethodInfo getInstanceNoop = typeof(UiPatchStubs).GetMethod("PluginManagerGetInstanceNoop", BindingFlags.Public | BindingFlags.Static); PatchManagedMethod(getInstance, getInstanceNoop); patched.Add("IMPP.Client.Core.Plugins.Manager.PluginManager.GetInstance"); return new Dictionary { { "patched", patched.ToArray() } }; }); Step(steps, "instantiate_frmLogin", delegate { Type frmLoginType = ResolveType(clientAssembly, "IMPP.Client.frmLogin"); loginForm = Activator.CreateInstance(frmLoginType); return new Dictionary { { "type", frmLoginType.FullName }, { "created", loginForm != null } }; }); StepWithTimeout(steps, "instantiate_frmMain", timeoutMs, delegate { Type frmMainType = ResolveType(clientAssembly, "IMPP.Client.frmMain"); notifyIcon = new NotifyIcon(); ConstructorInfo constructor = frmMainType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .First(c => c.GetParameters().Length == 2); mainForm = constructor.Invoke(new object[] { loginForm, notifyIcon }); result["main_window_constructed"] = mainForm != null; return new Dictionary { { "type", frmMainType.FullName }, { "created", mainForm != null } }; }); if (probeChat || probeSendText || probeReceiveText || probeSendFile || probeReceiveFile) { Step(steps, "instantiate_p2p_chat_model", delegate { p2pChat = CreateOfflineP2PChat(clientAssembly, imppManager, userJid); result["chat_model_constructed"] = p2pChat != null; return new Dictionary { { "type", p2pChat == null ? "" : p2pChat.GetType().FullName }, { "user_jid", userJid }, { "created", p2pChat != null } }; }); StepWithTimeout(steps, "instantiate_frmP2PChat", timeoutMs, delegate { Type frmP2PChatType = ResolveType(clientAssembly, "IMPP.Client.Business.ChatManager.SingleChat.frmP2PChat"); ConstructorInfo constructor = frmP2PChatType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .First(c => c.GetParameters().Length >= 1); ParameterInfo[] parameters = constructor.GetParameters(); object[] args = new object[parameters.Length]; args[0] = p2pChat; for (int i = 1; i < parameters.Length; i++) { args[i] = parameters[i].ParameterType == typeof(string) ? "" : Type.Missing; } chatForm = constructor.Invoke(args); result["chat_window_constructed"] = chatForm != null; return new Dictionary { { "type", frmP2PChatType.FullName }, { "created", chatForm != null }, { "constructor_parameters", parameters.Length } }; }); if (probeSendText) { StepWithTimeout(steps, "invoke_frmP2PChat_SaveAndShowMessage", timeoutMs, delegate { object outgoing = CreateTextXmppMessage(clientAssembly, "offline-self@offline.lab", userJid, text); Type imageUploaderType = ResolveType(clientAssembly, "IMPP.Client.Business.ChatManager.ImagesUpload.ImageUploader"); object images = Activator.CreateInstance(typeof(List<>).MakeGenericType(imageUploaderType)); InvokeInstance(chatForm, "SaveAndShowMessage", outgoing, images); result["send_text_invoked"] = true; return new Dictionary { { "hook", "frmP2PChat.SaveAndShowMessage" }, { "text_length", text == null ? 0 : text.Length } }; }); } if (probeReceiveText) { StepWithTimeout(steps, "invoke_frmP2PChat_ProccessRecvMessage", timeoutMs, delegate { object incoming = CreateTextXmppMessage(clientAssembly, userJid, "offline-self@offline.lab", text); InvokeInstance(chatForm, "ProccessRecvMessage", incoming); result["receive_text_invoked"] = true; return new Dictionary { { "hook", "frmP2PChat.ProccessRecvMessage" }, { "text_length", text == null ? 0 : text.Length } }; }); } if (probeSendFile) { StepWithTimeout(steps, "instantiate_UC_FileSend", timeoutMs, delegate { string outgoingPath = EnsureProbeFilePath(filePath); FileInfo outgoingInfo = new FileInfo(outgoingPath); object transferType = CreateFileTransferType(clientAssembly, "Ordinary"); Type ucFileSendType = ResolveType(clientAssembly, "IMPP.Client.UC_FileSend"); fileSendControl = Activator.CreateInstance( ucFileSendType, p2pChat, outgoingPath, outgoingInfo.Length, transferType, null); result["probe_file_path"] = outgoingPath; result["probe_file_size"] = outgoingInfo.Length; result["file_send_control_constructed"] = fileSendControl != null; return new Dictionary { { "hook", "UC_FileSend(Chat,string,long,FileTransferType,TcpSenderInfo)" }, { "type", ucFileSendType.FullName }, { "created", fileSendControl != null }, { "file_path", outgoingPath }, { "file_size", outgoingInfo.Length }, { "file_type", transferType.ToString() } }; }); } if (probeReceiveFile) { StepWithTimeout(steps, "instantiate_UC_FileReceive", timeoutMs, delegate { string outgoingPath = Convert.ToString(result["probe_file_path"]); string incomingName = string.IsNullOrWhiteSpace(fileName) ? Path.GetFileName(outgoingPath) : fileName; if (string.IsNullOrWhiteSpace(incomingName)) { incomingName = "incoming-native-probe.txt"; } long incomingSize = fileSize > 0L ? fileSize : Convert.ToInt64(result["probe_file_size"]); string remoteFileName = "/offline-probe/" + Guid.NewGuid().ToString("N") + "/" + incomingName; object incomingFileMessage = CreateFileXmppMessage(clientAssembly, userJid, "offline-self@offline.lab", incomingName, "Ordinary"); Type ucFileReceiveType = ResolveType(clientAssembly, "IMPP.Client.UC_FileReceive"); fileReceiveControl = Activator.CreateInstance( ucFileReceiveType, p2pChat, incomingName, incomingSize, remoteFileName, "离线演示用户", incomingFileMessage); result["probe_file_name"] = incomingName; result["probe_receive_file_size"] = incomingSize; result["file_receive_control_constructed"] = fileReceiveControl != null; return new Dictionary { { "hook", "UC_FileReceive(Chat,string,long,string,string,XmppMessage)" }, { "type", ucFileReceiveType.FullName }, { "created", fileReceiveControl != null }, { "file_name", incomingName }, { "file_size", incomingSize }, { "remote_file_name", remoteFileName }, { "message_guid", Convert.ToString(GetProperty(incomingFileMessage, "id")) }, { "file_type", Convert.ToString(GetProperty(incomingFileMessage, "FileType")) } }; }); } } } result["thread_exceptions"] = threadExceptions.ToArray(); result["ok"] = construct ? Convert.ToBoolean(result["main_window_constructed"]) : true; if (probeChat) { result["ok"] = Convert.ToBoolean(result["ok"]) && Convert.ToBoolean(result["chat_window_constructed"]); } if (probeSendText) { result["ok"] = Convert.ToBoolean(result["ok"]) && Convert.ToBoolean(result["send_text_invoked"]); } if (probeReceiveText) { result["ok"] = Convert.ToBoolean(result["ok"]) && Convert.ToBoolean(result["receive_text_invoked"]); } if (probeSendFile) { result["ok"] = Convert.ToBoolean(result["ok"]) && Convert.ToBoolean(result["file_send_control_constructed"]); } if (probeReceiveFile) { result["ok"] = Convert.ToBoolean(result["ok"]) && Convert.ToBoolean(result["file_receive_control_constructed"]); } return result; } catch (Exception ex) { Exception unwrapped = Unwrap(ex); result["ok"] = false; result["error"] = unwrapped.GetType().FullName; result["message"] = unwrapped.Message; result["stack"] = unwrapped.ToString(); result["thread_exceptions"] = threadExceptions.ToArray(); return result; } finally { DisposeIfPossible(fileReceiveControl); DisposeIfPossible(fileSendControl); DisposeIfPossible(chatForm); DisposeIfPossible(mainForm); DisposeIfPossible(loginForm); if (notifyIcon != null) { notifyIcon.Dispose(); } } } private static void Step(List> steps, string name, Func> action, bool optional = false) { Dictionary entry = new Dictionary { {"name", name}, {"ok", false}, {"optional", optional} }; steps.Add(entry); try { Dictionary data = action(); entry["ok"] = true; entry["data"] = data; } catch (Exception ex) { entry["ok"] = optional; entry["error"] = ex.GetType().FullName; entry["message"] = Unwrap(ex).Message; entry["stack"] = Unwrap(ex).ToString(); if (!optional) { throw; } } } private static void StepWithTimeout(List> steps, string name, int timeoutMs, Func> action, bool optional = false) { Dictionary entry = new Dictionary { {"name", name}, {"ok", false}, {"optional", optional}, {"timeout_ms", timeoutMs} }; steps.Add(entry); Dictionary data = null; Exception captured = null; Thread worker = new Thread(delegate() { try { Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); Application.ThreadException += delegate(object sender, ThreadExceptionEventArgs e) { captured = new InvalidOperationException("WinForms thread exception during offline main-window probe", e.Exception); try { Application.ExitThread(); } catch { } }; data = action(); } catch (Exception ex) { captured = ex; } }); worker.SetApartmentState(ApartmentState.STA); worker.IsBackground = true; worker.Start(); if (!worker.Join(timeoutMs)) { try { worker.Suspend(); entry["worker_stack"] = new StackTrace(worker, true).ToString(); } catch (Exception stackEx) { entry["worker_stack_error"] = stackEx.GetType().FullName + ": " + stackEx.Message; } finally { try { worker.Resume(); } catch { } } try { worker.Abort(); } catch { // Best-effort cleanup; RunProbeMainWindow force-exits after JSON is printed. } TimeoutException timeout = new TimeoutException(name + " timed out after " + timeoutMs + " ms"); entry["ok"] = optional; entry["error"] = timeout.GetType().FullName; entry["message"] = timeout.Message; entry["stack"] = timeout.ToString(); if (!optional) { throw timeout; } return; } if (captured != null) { Exception unwrapped = Unwrap(captured); entry["ok"] = optional; entry["error"] = captured.GetType().FullName; entry["message"] = unwrapped.Message; entry["stack"] = unwrapped.ToString(); if (!optional) { throw captured; } return; } entry["ok"] = true; entry["data"] = data; } private static Exception Unwrap(Exception ex) { TargetInvocationException target = ex as TargetInvocationException; return target != null && target.InnerException != null ? target.InnerException : ex; } private static Assembly LoadClientAssembly(string fullClientDir) { string exe = Path.Combine(fullClientDir, "IMPlatformClient.exe"); if (!File.Exists(exe)) { throw new FileNotFoundException("IMPlatformClient.exe not found", exe); } AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs args) { AssemblyName name = new AssemblyName(args.Name); string dll = Path.Combine(fullClientDir, name.Name + ".dll"); if (File.Exists(dll)) { return Assembly.LoadFrom(dll); } string exeCandidate = Path.Combine(fullClientDir, name.Name + ".exe"); if (File.Exists(exeCandidate)) { return Assembly.LoadFrom(exeCandidate); } return null; }; foreach (string dll in Directory.GetFiles(fullClientDir, "*.dll")) { try { Assembly.LoadFrom(dll); } catch { // Native and mixed-mode dependencies are loaded by the original client as needed. } } return Assembly.LoadFrom(exe); } private static string EnsureProbeFilePath(string filePath) { string path = filePath; if (string.IsNullOrWhiteSpace(path)) { path = Path.Combine(Application.StartupPath, "probe-files", "outgoing-native-probe.txt"); } path = Path.GetFullPath(path); string directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } if (!File.Exists(path)) { File.WriteAllText(path, "iSphere native helper file probe\r\n", Encoding.UTF8); } return path; } private static object CreateFileTransferType(Assembly clientAssembly, string name) { Type fileTransferType = ResolveType(clientAssembly, "com.vision.smack.packet.FileTransferType"); return Enum.Parse(fileTransferType, name); } private static object CreateFileXmppMessage(Assembly clientAssembly, string from, string to, string fileName, string fileTypeName) { object message = CreateTextXmppMessage(clientAssembly, from, to, fileName ?? string.Empty); SetProperty(message, "FileType", CreateFileTransferType(clientAssembly, fileTypeName)); return message; } private static object CreateOfflineP2PChat(Assembly clientAssembly, object imppManager, string userJid) { Type p2pChatType = ResolveType(clientAssembly, "com.vision.smack.P2PChat"); Type jidType = ResolveType(clientAssembly, "com.vision.smack.Jid"); object connection = GetProperty(imppManager, "Connection"); if (connection == null) { Type connectionType = ResolveType(clientAssembly, "com.vision.smack.XMPPConnection"); connection = Activator.CreateInstance(connectionType, new object[] { true }); } object myJid = GetProperty(connection, "Jid"); if (myJid == null) { myJid = Activator.CreateInstance(jidType, "offline-self@offline.lab/pc"); SetProperty(connection, "Jid", myJid); } object chat = Activator.CreateInstance(p2pChatType, connection, userJid); SetProperty(chat, "IsSaveToDB", false); return chat; } private static object CreateTextXmppMessage(Assembly clientAssembly, string from, string to, string text) { Type messageType = ResolveType(clientAssembly, "com.vision.smack.packet.XmppMessage"); Type bodyType = ResolveType(clientAssembly, "com.vision.smack.packet.XmppMessage+Body"); object message = Activator.CreateInstance(messageType, Guid.NewGuid().ToString(), from, to, "chat"); object body = Activator.CreateInstance(bodyType, text ?? string.Empty); SetProperty(message, "body", body); long unixMs = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds; SetProperty(message, "Time", unixMs.ToString()); return message; } private static void PatchManagedMethod(MethodInfo target, MethodInfo replacement) { if (target == null) { throw new ArgumentNullException("target"); } if (replacement == null) { throw new ArgumentNullException("replacement"); } if (IntPtr.Size != 4) { throw new PlatformNotSupportedException("runtime detour is intentionally limited to x86 helper builds"); } RuntimeHelpers.PrepareMethod(target.MethodHandle); RuntimeHelpers.PrepareMethod(replacement.MethodHandle); IntPtr targetPtr = target.MethodHandle.GetFunctionPointer(); IntPtr replacementPtr = replacement.MethodHandle.GetFunctionPointer(); uint oldProtect; if (!VirtualProtect(targetPtr, new UIntPtr(5), 0x40, out oldProtect)) { throw new InvalidOperationException("VirtualProtect failed for method patch"); } int offset = replacementPtr.ToInt32() - targetPtr.ToInt32() - 5; Marshal.WriteByte(targetPtr, 0, 0xE9); Marshal.WriteInt32(new IntPtr(targetPtr.ToInt32() + 1), offset); uint ignored; VirtualProtect(targetPtr, new UIntPtr(5), oldProtect, out ignored); } [DllImport("kernel32.dll")] private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); private sealed class UiPatchStubs { public static Type PluginManagerType; private static object PluginManagerInstance; public void DrawClientNoop() { } public static object PluginManagerGetInstanceNoop() { if (PluginManagerInstance == null) { PluginManagerInstance = FormatterServices.GetUninitializedObject(PluginManagerType); FieldInfo staticInstance = PluginManagerType.GetField("_pluginManager", BindingFlags.NonPublic | BindingFlags.Static); if (staticInstance != null) { staticInstance.SetValue(null, PluginManagerInstance); } PropertyInfo initCompleted = PluginManagerType.GetProperty("InitCompleted", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (initCompleted != null && initCompleted.CanWrite) { initCompleted.SetValue(PluginManagerInstance, false, null); } PropertyInfo pluginGroupInfo = PluginManagerType.GetProperty("PluginGroupInfo", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (pluginGroupInfo != null && pluginGroupInfo.CanWrite) { pluginGroupInfo.SetValue(PluginManagerInstance, Activator.CreateInstance(pluginGroupInfo.PropertyType), null); } } return PluginManagerInstance; } } private static string EnsureConnSelXml(string fullClientDir) { string helperConnSel = Path.Combine(Application.StartupPath, "ConnSel.xml"); string clientConnSel = Path.Combine(fullClientDir, "ConnSel.xml"); if (File.Exists(clientConnSel)) { File.Copy(clientConnSel, helperConnSel, true); } else { File.WriteAllText( helperConnSel, "\r\n
\r\n \r\n
\r\n", Encoding.UTF8); } return helperConnSel; } private static Type ResolveType(Assembly clientAssembly, string fullName) { Type type = clientAssembly.GetType(fullName, false); if (type != null) { return type; } foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { type = assembly.GetType(fullName, false); if (type != null) { return type; } } throw new TypeLoadException(fullName); } private static object CreateCompanyList(Assembly clientAssembly) { Type companyType = ResolveType(clientAssembly, "IMPP.Client.Company"); Type listType = typeof(List<>).MakeGenericType(companyType); object list = Activator.CreateInstance(listType); object company = Activator.CreateInstance(companyType); SetProperty(company, "ID", "offline-org"); SetProperty(company, "Name", "iSphere Offline Lab"); SetProperty(company, "MessageDNS", "offline.lab"); listType.GetMethod("Add").Invoke(list, new object[] { company }); return list; } private static Dictionary CreateBaseConfigDictionary() { return new Dictionary(StringComparer.OrdinalIgnoreCase) { {"productname", "iSphere Offline Lab"}, {"orgname_s", "iSphere Offline Lab"}, {"security_control", "0"}, {"visualphone_enable", "false"}, {"businesscontactsurl", ""}, {"companylogoserverurl", ""}, {"muc_room_disable", "1"}, {"customerphone", ""}, {"customercn", ""}, {"customerservicesuffix", ""}, {"grouplimit", "300"}, {"font_color", ""}, {"weather_default_city", ""}, {"isshowmailinvite", ""}, {"pc_only_active_im", ""}, {"devicebindenablepc", "false"}, {"isenablemultiav", "false"}, {"workbenchcollapseswitch", "true"}, {"account_enable", "false"}, {"apigateway", ""}, {"org_provider", "false"} }; } private static object GetStaticProperty(Type type, string name) { PropertyInfo property = type.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); if (property == null) { throw new MissingMemberException(type.FullName, name); } return property.GetValue(null, null); } private static object GetProperty(object target, string name) { PropertyInfo property = target.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (property == null) { throw new MissingMemberException(target.GetType().FullName, name); } return property.GetValue(target, null); } private static void SetStaticProperty(Type type, string name, object value) { PropertyInfo property = type.GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); if (property == null) { throw new MissingMemberException(type.FullName, name); } property.SetValue(null, value, null); } private static void SetProperty(object target, string name, object value) { PropertyInfo property = target.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (property == null) { throw new MissingMemberException(target.GetType().FullName, name); } property.SetValue(target, value, null); } private static void InvokeStatic(Type type, string name, params object[] args) { MethodInfo method = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == args.Length); if (method == null) { throw new MissingMethodException(type.FullName, name); } method.Invoke(null, args); } private static void InvokeInstance(object target, string name, params object[] args) { MethodInfo method = target.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == args.Length); if (method == null) { throw new MissingMethodException(target.GetType().FullName, name); } method.Invoke(target, args); } private static void DisposeIfPossible(object target) { IDisposable disposable = target as IDisposable; if (disposable != null) { disposable.Dispose(); } } } public sealed class OfflineState { public string session_id { get; set; } public string mode { get; set; } public string created_at { get; set; } public string updated_at { get; set; } public string client_dir { get; set; } public MainWindowState main_window { get; set; } public NativeBridgeState native_bridge { get; set; } public string active_conversation_id { get; set; } public List users { get; set; } public Dictionary conversations { get; set; } public List audit { get; set; } public static OfflineState Create(string clientDir) { string now = DateTimeOffset.Now.ToString("o"); List users = new List { UserRecord.Create("U1001", "mock-alice@offline.lab", "张小真", "研发部"), UserRecord.Create("U1002", "mock-bob@offline.lab", "李文件", "运营部"), UserRecord.Create("U1003", "mock-carol@offline.lab", "王搜索", "客服部"), UserRecord.Create("U1004", "mock-dave@offline.lab", "陈接收", "测试部") }; Dictionary conversations = new Dictionary(); foreach (UserRecord user in users) { conversations[user.jid] = Conversation.Create(user); } return new OfflineState { session_id = "native-lab-" + Guid.NewGuid().ToString("N").Substring(0, 12), mode = "offline-lab-native-helper", created_at = now, updated_at = now, client_dir = Path.GetFullPath(clientDir), main_window = new MainWindowState { opened = true, caption = "iSphere Offline Lab Main", source = "reverse-map: frmLogin.LogonSuccessUI -> frmMain(frmLogin, notifyIcon)", active_panel = "contacts" }, native_bridge = new NativeBridgeState(), active_conversation_id = null, users = users, conversations = conversations, audit = new List() }; } } public sealed class MainWindowState { public bool opened { get; set; } public string caption { get; set; } public string source { get; set; } public string active_panel { get; set; } } public sealed class NativeBridgeState { public string status { get; set; } public string backend { get; set; } public string client_dir { get; set; } public object inspection { get; set; } public string next_step { get; set; } public NativeBridgeState() { status = "not-attached"; backend = "dotnet-framework-helper"; next_step = "use a patch/injection layer to replace simulated state commands with direct frmMain/frmP2PChat calls"; } } public sealed class UserRecord { public string user_id { get; set; } public string jid { get; set; } public string name { get; set; } public string domain { get; set; } public string company_id { get; set; } public string department { get; set; } public bool enable { get; set; } public bool chat_able { get; set; } public string source { get; set; } public static UserRecord Create(string userId, string jid, string name, string department) { return new UserRecord { user_id = userId, jid = jid, name = name, domain = "offline.lab", company_id = "offline-company", department = department, enable = true, chat_able = true, source = "native-helper-mock" }; } } public sealed class Conversation { public string conversation_id { get; set; } public string conversation_name { get; set; } public string conversation_type { get; set; } public string user_jid { get; set; } public List messages { get; set; } public static Conversation Create(UserRecord user) { return new Conversation { conversation_id = user.jid, conversation_name = user.name, conversation_type = "direct", user_jid = user.jid, messages = new List() }; } } public sealed class ConversationView { public string conversation_id { get; set; } public string conversation_name { get; set; } public string conversation_type { get; set; } public string user_jid { get; set; } public List messages { get; set; } public int message_count { get; set; } } public sealed class MessageRecord { public string message_id { get; set; } public string conversation_id { get; set; } public string conversation_name { get; set; } public string conversation_type { get; set; } public string sender_id { get; set; } public string sender_name { get; set; } public string direction { get; set; } public string content_type { get; set; } public string content_text { get; set; } public List attachments { get; set; } public string created_at { get; set; } public string received_at { get; set; } public string source { get; set; } public string raw_ref { get; set; } public string ui_hook { get; set; } } public sealed class AttachmentRecord { public string name { get; set; } public string path { get; set; } public bool exists { get; set; } public long size { get; set; } public string direction { get; set; } public string transfer_state { get; set; } } public sealed class AuditRecord { public string event_id { get; set; } public string event_type { get; set; } public string created_at { get; set; } public Dictionary data { get; set; } } }