feat: add msglib readonly sidecar
This commit is contained in:
329
native/MsgLibReadSidecar/Program.cs
Normal file
329
native/MsgLibReadSidecar/Program.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.Common;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MsgLibReadSidecar
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private const string Version = "0.1.0";
|
||||
private const string DefaultPassword = "123";
|
||||
|
||||
private static readonly HashSet<string> DefaultTargetTables = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"TD_Roster", "TD_CustomEffigy", "tblRecent", "tblChatLevel", "tblPersonMsg", "tblMsgGroupPersonMsg",
|
||||
"TD_WorkGroupAuth", "TD_ReceiveFileRecord", "TD_SendFileRecord", "TD_SystemMessageRecord", "TD_MsgReadBurn"
|
||||
};
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
string requestText = Console.In.ReadToEnd();
|
||||
Dictionary<string, object> request;
|
||||
string requestId = "";
|
||||
string op = "";
|
||||
|
||||
if (!SidecarProtocol.TryParseRequest(requestText, out request, out requestId, out op))
|
||||
{
|
||||
Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "INVALID_REQUEST", "request must be a valid isphere.msglib.v1 JSON object")));
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<string, object> opArgs = SidecarProtocol.GetObject(request, "args");
|
||||
Dictionary<string, object> response;
|
||||
switch (op)
|
||||
{
|
||||
case "self_check":
|
||||
response = SidecarProtocol.Success(requestId, op, SelfCheckData());
|
||||
break;
|
||||
case "schema_summary":
|
||||
response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, false));
|
||||
break;
|
||||
case "display_sources":
|
||||
response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, true));
|
||||
break;
|
||||
default:
|
||||
response = SidecarProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
|
||||
break;
|
||||
}
|
||||
Console.WriteLine(SidecarProtocol.ToJson(response));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(ex.ToString());
|
||||
Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "SIDECAR_FAILED", ex.Message)));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> SelfCheckData()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "sidecar_name", "MsgLibReadSidecar" },
|
||||
{ "sidecar_version", Version },
|
||||
{ "protocol", SidecarProtocol.Protocol },
|
||||
{ "runtime", ".NET Framework " + Environment.Version },
|
||||
{ "process_bits", IntPtr.Size * 8 },
|
||||
{ "requires_process_bits", 32 },
|
||||
{ "db_mutation_allowed", false },
|
||||
{ "message_body_reads_allowed", false }
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> SchemaSummary(Dictionary<string, object> args, bool displayOnly)
|
||||
{
|
||||
string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", "");
|
||||
string dbPath = SidecarProtocol.GetString(args, "db_path", "");
|
||||
string password = SidecarProtocol.GetString(args, "password", DefaultPassword);
|
||||
bool includeCounts = SidecarProtocol.GetBool(args, "include_counts", true);
|
||||
int maxTables = Clamp(SidecarProtocol.GetInt(args, "max_tables", 128), 1, 256);
|
||||
List<string> requestedTargets = SidecarProtocol.GetStringList(args, "target_tables");
|
||||
HashSet<string> targetTables = requestedTargets.Count == 0 ? DefaultTargetTables : new HashSet<string>(requestedTargets, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ValidateInputs(sqliteDllPath, dbPath);
|
||||
|
||||
var result = new Dictionary<string, object>
|
||||
{
|
||||
{ "metadata_only", true },
|
||||
{ "read_only", true },
|
||||
{ "message_body_values_returned", false },
|
||||
{ "sqlite_dll_path", sqliteDllPath },
|
||||
{ "db_path", dbPath },
|
||||
{ "provider", "System.Data.SQLite" },
|
||||
{ "process_bits", IntPtr.Size * 8 }
|
||||
};
|
||||
|
||||
using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password))
|
||||
{
|
||||
connection.Open();
|
||||
TryExecuteNonQuery(connection, "PRAGMA query_only=ON");
|
||||
result["sqlite_version"] = ExecuteScalar(connection, "select sqlite_version()");
|
||||
List<Dictionary<string, object>> tables = ReadMasterTables(connection, maxTables);
|
||||
List<Dictionary<string, object>> columns = ReadColumns(connection, tables, displayOnly ? targetTables : null);
|
||||
result["tables"] = tables;
|
||||
result["columns"] = columns;
|
||||
result["target_row_counts"] = includeCounts ? ReadTargetCounts(connection, tables, targetTables) : new List<Dictionary<string, object>>();
|
||||
if (displayOnly)
|
||||
{
|
||||
result["display_sources"] = BuildDisplaySources(tables, columns);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateInputs(string sqliteDllPath, string dbPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sqliteDllPath) || !File.Exists(sqliteDllPath))
|
||||
{
|
||||
throw new ArgumentException("sqlite_dll_path must point to an existing System.Data.SQLite.dll");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(dbPath) || !File.Exists(dbPath))
|
||||
{
|
||||
throw new ArgumentException("db_path must point to an existing copied MsgLib.db");
|
||||
}
|
||||
if (!string.Equals(Path.GetFileName(dbPath), "MsgLib.db", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentException("db_path must point to a copied MsgLib.db file");
|
||||
}
|
||||
}
|
||||
|
||||
private static DbConnection OpenSqliteConnection(string sqliteDllPath, string dbPath, string password)
|
||||
{
|
||||
Assembly assembly = Assembly.LoadFrom(sqliteDllPath);
|
||||
Type builderType = assembly.GetType("System.Data.SQLite.SQLiteConnectionStringBuilder", true);
|
||||
object builder = Activator.CreateInstance(builderType);
|
||||
SetPropertyIfExists(builderType, builder, "DataSource", dbPath);
|
||||
SetPropertyIfExists(builderType, builder, "Password", password);
|
||||
SetPropertyIfExists(builderType, builder, "ReadOnly", true);
|
||||
SetPropertyIfExists(builderType, builder, "Pooling", false);
|
||||
SetPropertyIfExists(builderType, builder, "FailIfMissing", true);
|
||||
string connectionString = Convert.ToString(builderType.GetProperty("ConnectionString").GetValue(builder, null));
|
||||
Type connectionType = assembly.GetType("System.Data.SQLite.SQLiteConnection", true);
|
||||
object connection = Activator.CreateInstance(connectionType, new object[] { connectionString });
|
||||
DbConnection dbConnection = connection as DbConnection;
|
||||
if (dbConnection == null)
|
||||
{
|
||||
throw new InvalidOperationException("SQLiteConnection is not a DbConnection");
|
||||
}
|
||||
return dbConnection;
|
||||
}
|
||||
|
||||
private static void SetPropertyIfExists(Type type, object target, string name, object value)
|
||||
{
|
||||
PropertyInfo property = type.GetProperty(name);
|
||||
if (property != null && property.CanWrite)
|
||||
{
|
||||
property.SetValue(target, value, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> ReadMasterTables(DbConnection connection, int maxTables)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
using (DbCommand command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "select type, name, tbl_name from sqlite_master where type in ('table','view') order by type, name";
|
||||
using (DbDataReader reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read() && rows.Count < maxTables)
|
||||
{
|
||||
rows.Add(new Dictionary<string, object>
|
||||
{
|
||||
{ "type", Convert.ToString(reader["type"]) },
|
||||
{ "name", Convert.ToString(reader["name"]) },
|
||||
{ "tbl_name", Convert.ToString(reader["tbl_name"]) }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> ReadColumns(DbConnection connection, List<Dictionary<string, object>> tables, HashSet<string> onlyTables)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
foreach (Dictionary<string, object> table in tables)
|
||||
{
|
||||
string tableName = Convert.ToString(table["name"]);
|
||||
string tableType = Convert.ToString(table["type"]);
|
||||
if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (onlyTables != null && !onlyTables.Contains(tableName)) continue;
|
||||
|
||||
using (DbCommand command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = "PRAGMA table_info(" + QuoteIdent(tableName) + ")";
|
||||
using (DbDataReader reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
rows.Add(new Dictionary<string, object>
|
||||
{
|
||||
{ "table", tableName },
|
||||
{ "cid", Convert.ToString(reader["cid"]) },
|
||||
{ "name", Convert.ToString(reader["name"]) },
|
||||
{ "type", Convert.ToString(reader["type"]) },
|
||||
{ "notnull", Convert.ToString(reader["notnull"]) },
|
||||
{ "pk", Convert.ToString(reader["pk"]) }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> ReadTargetCounts(DbConnection connection, List<Dictionary<string, object>> tables, HashSet<string> targetTables)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
foreach (Dictionary<string, object> table in tables)
|
||||
{
|
||||
string tableName = Convert.ToString(table["name"]);
|
||||
string tableType = Convert.ToString(table["type"]);
|
||||
if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!targetTables.Contains(tableName)) continue;
|
||||
rows.Add(new Dictionary<string, object>
|
||||
{
|
||||
{ "table", tableName },
|
||||
{ "row_count", ExecuteScalar(connection, "select count(*) from " + QuoteIdent(tableName)) }
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, object>> BuildDisplaySources(List<Dictionary<string, object>> tables, List<Dictionary<string, object>> columns)
|
||||
{
|
||||
var tableSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Dictionary<string, object> table in tables)
|
||||
{
|
||||
if (string.Equals(Convert.ToString(table["type"]), "table", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tableSet.Add(Convert.ToString(table["name"]));
|
||||
}
|
||||
}
|
||||
|
||||
var columnMap = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Dictionary<string, object> column in columns)
|
||||
{
|
||||
string table = Convert.ToString(column["table"]);
|
||||
string name = Convert.ToString(column["name"]);
|
||||
if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
columnMap[table].Add(name);
|
||||
}
|
||||
|
||||
var sources = new List<Dictionary<string, object>>();
|
||||
AddSource(sources, tableSet, columnMap, "contacts_roster", "TD_Roster", new string[] { "JId", "Nick", "GroupName" });
|
||||
AddSource(sources, tableSet, columnMap, "contacts_effigy", "TD_CustomEffigy", new string[] { "PersonJid", "PersonName" });
|
||||
AddSource(sources, tableSet, columnMap, "recent_display", "tblRecent", new string[] { "PersonId", "PersonName", "FType" });
|
||||
AddSource(sources, tableSet, columnMap, "person_message_names", "tblPersonMsg", new string[] { "SndPerson", "SndPersonId", "RecvPerson", "RecvPersonId", "ObjPerson", "ObjPersonId" });
|
||||
AddSource(sources, tableSet, columnMap, "group_message_names", "tblMsgGroupPersonMsg", new string[] { "MsgGroupId", "MsgGroupName", "SessionPersonId", "SessionPersonName", "GroupType" });
|
||||
AddSource(sources, tableSet, columnMap, "work_group_auth", "TD_WorkGroupAuth", new string[] { "GroupJID", "GroupName", "ChildGroupJid" });
|
||||
AddSource(sources, tableSet, columnMap, "received_file_metadata", "TD_ReceiveFileRecord", new string[] { "FileName", "SendPersonJid", "SendPersonName" });
|
||||
AddSource(sources, tableSet, columnMap, "sent_file_metadata", "TD_SendFileRecord", new string[] { "FileName", "ReceivePersonJid", "ReceivePersonName" });
|
||||
return sources;
|
||||
}
|
||||
|
||||
private static void AddSource(List<Dictionary<string, object>> sources, HashSet<string> tableSet, Dictionary<string, HashSet<string>> columnMap, string sourceName, string table, string[] columns)
|
||||
{
|
||||
var presentColumns = new List<string>();
|
||||
if (columnMap.ContainsKey(table))
|
||||
{
|
||||
foreach (string column in columns)
|
||||
{
|
||||
if (columnMap[table].Contains(column)) presentColumns.Add(column);
|
||||
}
|
||||
}
|
||||
sources.Add(new Dictionary<string, object>
|
||||
{
|
||||
{ "source", sourceName },
|
||||
{ "table", table },
|
||||
{ "available", tableSet.Contains(table) && presentColumns.Count > 0 },
|
||||
{ "required_columns", columns },
|
||||
{ "present_columns", presentColumns.ToArray() }
|
||||
});
|
||||
}
|
||||
|
||||
private static string ExecuteScalar(DbConnection connection, string sql)
|
||||
{
|
||||
using (DbCommand command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = sql;
|
||||
object value = command.ExecuteScalar();
|
||||
return Convert.ToString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryExecuteNonQuery(DbConnection connection, string sql)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (DbCommand command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = sql;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string QuoteIdent(string value)
|
||||
{
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private static int Clamp(int value, int min, int max)
|
||||
{
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
142
native/MsgLibReadSidecar/SidecarProtocol.cs
Normal file
142
native/MsgLibReadSidecar/SidecarProtocol.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace MsgLibReadSidecar
|
||||
{
|
||||
internal static class SidecarProtocol
|
||||
{
|
||||
public const string Protocol = "isphere.msglib.v1";
|
||||
private static readonly JavaScriptSerializer Serializer = new JavaScriptSerializer { MaxJsonLength = 16 * 1024 * 1024, RecursionLimit = 256 };
|
||||
|
||||
public static bool TryParseRequest(string text, out Dictionary<string, object> request, out string requestId, out string op)
|
||||
{
|
||||
request = null;
|
||||
requestId = "";
|
||||
op = "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object decoded = Serializer.DeserializeObject(text);
|
||||
request = decoded as Dictionary<string, object>;
|
||||
if (request == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string protocol = GetString(request, "protocol", "");
|
||||
requestId = GetString(request, "request_id", "");
|
||||
op = GetString(request, "op", "");
|
||||
|
||||
return protocol == Protocol && !string.IsNullOrWhiteSpace(op);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> Success(string requestId, string op, object data)
|
||||
{
|
||||
return Envelope(requestId, op, true, data, null);
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> Failure(string requestId, string op, string code, string message)
|
||||
{
|
||||
return Envelope(requestId, op, false, null, new Dictionary<string, object> { { "code", code }, { "message", message } });
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> Envelope(string requestId, string op, bool ok, object data, object error)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "protocol", Protocol },
|
||||
{ "request_id", requestId ?? "" },
|
||||
{ "op", op ?? "" },
|
||||
{ "ok", ok },
|
||||
{ "data", data },
|
||||
{ "error", error },
|
||||
{ "warnings", new object[0] }
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToJson(object value)
|
||||
{
|
||||
return Serializer.Serialize(value);
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> GetObject(Dictionary<string, object> source, string key)
|
||||
{
|
||||
if (source == null || !source.ContainsKey(key) || source[key] == null)
|
||||
{
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
return source[key] as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public static string GetString(Dictionary<string, object> source, string key, string defaultValue)
|
||||
{
|
||||
if (source == null || !source.ContainsKey(key) || source[key] == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return Convert.ToString(source[key]);
|
||||
}
|
||||
|
||||
public static bool GetBool(Dictionary<string, object> source, string key, bool defaultValue)
|
||||
{
|
||||
if (source == null || !source.ContainsKey(key) || source[key] == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
try { return Convert.ToBoolean(source[key]); }
|
||||
catch { return defaultValue; }
|
||||
}
|
||||
|
||||
public static int GetInt(Dictionary<string, object> source, string key, int defaultValue)
|
||||
{
|
||||
if (source == null || !source.ContainsKey(key) || source[key] == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
try { return Convert.ToInt32(source[key]); }
|
||||
catch { return defaultValue; }
|
||||
}
|
||||
|
||||
public static List<string> GetStringList(Dictionary<string, object> source, string key)
|
||||
{
|
||||
var values = new List<string>();
|
||||
if (source == null || !source.ContainsKey(key) || source[key] == null)
|
||||
{
|
||||
return values;
|
||||
}
|
||||
object raw = source[key];
|
||||
object[] array = raw as object[];
|
||||
if (array != null)
|
||||
{
|
||||
foreach (object item in array)
|
||||
{
|
||||
if (item != null) values.Add(Convert.ToString(item));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
System.Collections.ArrayList list = raw as System.Collections.ArrayList;
|
||||
if (list != null)
|
||||
{
|
||||
foreach (object item in list)
|
||||
{
|
||||
if (item != null) values.Add(Convert.ToString(item));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
string single = Convert.ToString(raw);
|
||||
if (!string.IsNullOrWhiteSpace(single)) values.Add(single);
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user