Files
isphere-ai-bridge/native/ISphereOfflineLabHelper/Program.cs
2026-07-05 14:52:23 +08:00

1872 lines
82 KiB
C#

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<string, object>
{
{"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<string, string> 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<string, object>
{
{"ok", false},
{"error", ex.GetType().FullName},
{"message", ex.Message},
{"stack", ex.ToString()}
});
return 1;
}
}
private static int RunInspect(Dictionary<string, string> options)
{
string clientDir = Required(options, "client-dir");
Dictionary<string, object> result = NativeInspector.Inspect(clientDir);
PrintJson(result);
return Convert.ToBoolean(result["ok"]) ? 0 : 2;
}
private static int RunProbeMainWindow(Dictionary<string, string> 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<string, object> 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<string, string> 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<string, object>
{
{"workspace", FullPath(workspace)}
}));
SaveState(statePath, reused);
PrintJson(SessionResponse(reused, statePath, true));
return 0;
}
Dictionary<string, object> 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<string, object>
{
{"client_dir", FullPath(clientDir)},
{"inspect_ok", inspection["ok"]}
}));
SaveState(statePath, state);
PrintJson(SessionResponse(state, statePath, false));
return 0;
}
private static int RunStatus(Dictionary<string, string> options)
{
string workspace = Get(options, "workspace", Path.Combine("runs", "native-offline-lab"));
string statePath = StatePath(workspace);
if (!File.Exists(statePath))
{
PrintJson(new Dictionary<string, object>
{
{"started", false},
{"workspace", FullPath(workspace)},
{"state_path", FullPath(statePath)},
{"next_step", "run start --client-dir <copy>"}
});
return 0;
}
OfflineState state = LoadState(statePath);
PrintJson(new Dictionary<string, object>
{
{"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<string, string> 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<UserRecord> 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<string, object>
{
{"query", query},
{"result_count", users.Count}
}));
state.updated_at = Now();
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"mode", state.mode},
{"query", query},
{"limit", limit},
{"count", users.Count},
{"users", users}
});
return 0;
}
private static int RunOpenChat(Dictionary<string, string> 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<string, object>
{
{"conversation_id", conversation.conversation_id},
{"user_jid", userJid}
}));
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"opened", true},
{"mode", state.mode},
{"main_window", state.main_window},
{"conversation", ConversationView(conversation, 0)}
});
return 0;
}
private static int RunSendMessage(Dictionary<string, string> 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<AttachmentRecord>());
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<string, object>
{
{"conversation_id", conversation.conversation_id},
{"message_id", message.message_id},
{"content_type", "text"}
}));
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"sent", true},
{"simulated", true},
{"mode", state.mode},
{"transport", "native-helper-state"},
{"message", message}
});
return 0;
}
private static int RunReceiveMessage(Dictionary<string, string> 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<AttachmentRecord>());
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<string, object>
{
{"conversation_id", conversation.conversation_id},
{"message_id", message.message_id},
{"content_type", "text"}
}));
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"received", true},
{"simulated", true},
{"mode", state.mode},
{"transport", "native-helper-state"},
{"message", message}
});
return 0;
}
private static int RunSendFile(Dictionary<string, string> 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<AttachmentRecord> { 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<string, object>
{
{"conversation_id", conversation.conversation_id},
{"message_id", message.message_id},
{"file_name", fileName},
{"exists", attachment.exists}
}));
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"sent", true},
{"simulated", true},
{"mode", state.mode},
{"transport", "native-helper-state"},
{"file_event", message}
});
return 0;
}
private static int RunReceiveFile(Dictionary<string, string> 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<AttachmentRecord> { 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<string, object>
{
{"conversation_id", conversation.conversation_id},
{"message_id", message.message_id},
{"file_name", fileName},
{"size", size}
}));
SaveState(statePath, state);
PrintJson(new Dictionary<string, object>
{
{"received", true},
{"simulated", true},
{"mode", state.mode},
{"transport", "native-helper-state"},
{"file_event", message}
});
return 0;
}
private static int RunReadConversation(Dictionary<string, string> 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<string, object>
{
{"mode", state.mode},
{"limit", limit},
{"conversation", ConversationView(conversation, limit)}
});
return 0;
}
private static Dictionary<string, object> SessionResponse(OfflineState state, string statePath, bool reused)
{
return new Dictionary<string, object>
{
{"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<AttachmentRecord> 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<MessageRecord> messages = limit <= 0
? new List<MessageRecord>()
: 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<OfflineState>(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<string, object> 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<string, string> ParseOptions(string[] args)
{
Dictionary<string, string> options = new Dictionary<string, string>(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<string, string> options, string key)
{
if (!options.ContainsKey(key) || string.IsNullOrWhiteSpace(options[key]))
{
throw new ArgumentException("missing --" + key);
}
return options[key];
}
private static string Get(Dictionary<string, string> options, string key, string defaultValue)
{
return options.ContainsKey(key) ? options[key] : defaultValue;
}
private static bool HasFlag(Dictionary<string, string> 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<string, object> Inspect(string clientDir)
{
string fullClientDir = Path.GetFullPath(clientDir);
string exe = Path.Combine(fullClientDir, "IMPlatformClient.exe");
Dictionary<string, object> result = new Dictionary<string, object>
{
{"ok", false},
{"client_dir", fullClientDir},
{"client_exe", exe},
{"client_exe_exists", File.Exists(exe)},
{"mode", "reflection-only-inspection"},
{"types", new List<Dictionary<string, object>>()},
{"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<string> loadErrors = new List<string>();
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<Dictionary<string, object>> typeResults = new List<Dictionary<string, object>>();
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<string, object>
{
{"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<string, object> InspectType(Assembly clientAssembly, TypeProbe probe)
{
Type type = ResolveType(clientAssembly, probe.Name);
Dictionary<string, object> result = new Dictionary<string, object>
{
{"name", probe.Name},
{"required", probe.Required},
{"exists", type != null},
{"members_ok", false},
{"methods", new List<Dictionary<string, object>>()},
{"constructors", new List<Dictionary<string, object>>()}
};
if (type == null)
{
return result;
}
bool methodsOk = true;
List<Dictionary<string, object>> methodResults = new List<Dictionary<string, object>>();
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<string, object>
{
{"name", methodName},
{"exists", exists},
{"is_public", exists && method.IsPublic},
{"is_static", exists && method.IsStatic}
});
}
bool constructorsOk = true;
List<Dictionary<string, object>> ctorResults = new List<Dictionary<string, object>>();
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<string, object>
{
{"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<string, object> 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<Dictionary<string, object>> steps = new List<Dictionary<string, object>>();
Dictionary<string, object> result = new Dictionary<string, object>
{
{"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<string> threadExceptions = new List<string>();
try
{
Step(steps, "set_current_directory", delegate
{
Directory.SetCurrentDirectory(fullClientDir);
return new Dictionary<string, object> { { "current_directory", Directory.GetCurrentDirectory() } };
});
Step(steps, "load_client_assemblies", delegate
{
clientAssembly = LoadClientAssembly(fullClientDir);
return new Dictionary<string, object> { { "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<string, object> { { "enabled", true } };
});
Step(steps, "ensure_connsel_xml", delegate
{
string helperConnSel = EnsureConnSelXml(fullClientDir);
return new Dictionary<string, object> { { "path", helperConnSel } };
});
Step(steps, "configpath_init", delegate
{
Type configPath = ResolveType(clientAssembly, "IMPP.Helper.Config.ConfigPath");
InvokeStatic(configPath, "Init");
return new Dictionary<string, object> { { "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<string, object> { { "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<string, object> { { "user", "offline-self" } };
});
Step(steps, "wincommand_init", delegate
{
Type winCommand = ResolveType(clientAssembly, "IMPP.Common.WinCommand");
InvokeStatic(winCommand, "Init");
return new Dictionary<string, object> { { "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<string, object> { { "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<string, object> { { "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<string, object> { { "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<string, object> { { "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<string, object> { { "type", managerType.FullName }, { "has_instance", imppManager != null } };
});
Step(steps, "imppmanager_init", delegate
{
InvokeInstance(imppManager, "Init");
return new Dictionary<string, object> { { "called", "IMPPManager.Init" } };
});
Step(steps, "imppmanager_update_user", delegate
{
InvokeInstance(imppManager, "UpdateUserInfo", fakeUser, false);
return new Dictionary<string, object> { { "called", "IMPPManager.UpdateUserInfo" } };
});
Step(steps, "configsystem_get_settings", delegate
{
Type configSystemManager = ResolveType(clientAssembly, "IMPP.Client.ConfigSystemManager");
object settings = GetStaticProperty(configSystemManager, "GetSettingsInstance");
return new Dictionary<string, object> { { "settings_created", settings != null }, { "settings_type", settings == null ? "" : settings.GetType().FullName } };
}, optional: true);
if (construct)
{
Step(steps, "patch_problematic_ui_methods", delegate
{
List<string> patched = new List<string>();
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<string, object> { { "patched", patched.ToArray() } };
});
Step(steps, "instantiate_frmLogin", delegate
{
Type frmLoginType = ResolveType(clientAssembly, "IMPP.Client.frmLogin");
loginForm = Activator.CreateInstance(frmLoginType);
return new Dictionary<string, object> { { "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<string, object> { { "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "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<Dictionary<string, object>> steps, string name, Func<Dictionary<string, object>> action, bool optional = false)
{
Dictionary<string, object> entry = new Dictionary<string, object>
{
{"name", name},
{"ok", false},
{"optional", optional}
};
steps.Add(entry);
try
{
Dictionary<string, object> 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<Dictionary<string, object>> steps, string name, int timeoutMs, Func<Dictionary<string, object>> action, bool optional = false)
{
Dictionary<string, object> entry = new Dictionary<string, object>
{
{"name", name},
{"ok", false},
{"optional", optional},
{"timeout_ms", timeoutMs}
};
steps.Add(entry);
Dictionary<string, object> 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,
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<Section>\r\n <conn linktype=\"http\" str=\"127.0.0.1:10083\"/>\r\n</Section>\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<string, string> CreateBaseConfigDictionary()
{
return new Dictionary<string, string>(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<UserRecord> users { get; set; }
public Dictionary<string, Conversation> conversations { get; set; }
public List<AuditRecord> audit { get; set; }
public static OfflineState Create(string clientDir)
{
string now = DateTimeOffset.Now.ToString("o");
List<UserRecord> users = new List<UserRecord>
{
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<string, Conversation> conversations = new Dictionary<string, Conversation>();
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<AuditRecord>()
};
}
}
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<MessageRecord> 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<MessageRecord>()
};
}
}
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<MessageRecord> 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<AttachmentRecord> 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<string, object> data { get; set; }
}
}