1872 lines
82 KiB
C#
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; }
|
|
}
|
|
}
|