JD2022-TU1/main/tools/lib/AbstractPlugin/AbstractPlugin.cs

794 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace Plugin
{
/// <summary>
/// Base class for plugins.
/// </summary>
public abstract class AbstractPlugin : IDisposable
{
#region Constants
private const int TimeToThreadSpleep = 1;
private const string Localhost = "127.0.0.1";
#endregion
#region Variables
private readonly string _name;
private Thread _tread = null;
private ManualResetEvent _exit = new ManualResetEvent(false);
private Connection _connection = null;
private int _expectedPort = 1001;
private string _expectedIp = Localhost;
/// <summary>
/// Queries are temporary handlers. They parse the result and are removed from the list.
/// </summary>
private readonly List<PendingQuery> _pendingQueries = new List<PendingQuery>();
/// <summary>
/// Command handlers
/// </summary>
private readonly HandlerDictionary _commandHandlers = new HandlerDictionary();
#endregion
#region Constructor
public AbstractPlugin(string name)
{
_name = name;
}
#endregion
#region Properties
public string Name
{
get { return _name; }
}
public string IP
{
get
{
var connection = _connection;
if (connection != null)
return connection.IP;
return string.Empty;
}
}
public int Port
{
get
{
var connection = _connection;
if (connection != null)
return connection.Port;
return -1;
}
}
public string ExpectedIp
{
get { return _expectedIp; }
set { _expectedIp = value; }
}
public int ExpectedPort
{
get { return _expectedPort; }
set { _expectedPort = value; }
}
public bool Connected
{
get
{
var connection = _connection;
return connection != null && connection.IsConnected;
}
}
public bool Started
{
get
{
return _tread != null;
}
}
public string DataRoot
{
get
{
return Shared.DataRoot.Value;
}
}
#endregion
#region Abstract
/// <summary>
/// Invoke a function on the UI thread allowing commands to be executed in GUI context.
/// </summary>
protected abstract void InvokeOnMainThreadAsync(Action action);
/// <summary>
/// Reports a message to the user to let him know that the call to the engine failed.
/// Typically, a balloon is shown ...
/// </summary>
public abstract void ReportTimeoutFailure();
#endregion
#region Methods
/// <summary>
/// Start the thread to deal with engine remote exchanges
/// </summary>
public void Start()
{
_tread = new Thread(ThreadTask)
{
IsBackground = true
};
_tread.Start();
Disposed += ClosePluginEventHandler;
}
/// <summary>
/// Stop the thread, asking it to exit.
/// </summary>
public void Stop()
{
SendExit();
if (_tread != null)
{
_exit.Set();
_tread.Join();
_tread = null;
}
Disposed -= ClosePluginEventHandler;
}
public virtual void Dispose()
{
OnDisposed(EventArgs.Empty);
}
private bool ConnectionParametersChanged
{
get
{
return _connection != null && (_expectedPort != _connection.Port || _expectedIp != _connection.IP);
}
}
/// <summary>
/// Handle connection status. Function called to update plugin connection to the engine.
/// This handle hot connection/disconnection, and hot reconnection if connection port or ip changed
/// </summary>
private void UpdateConnectedStatus()
{
bool before = Connected;
if (ConnectionParametersChanged)
{
_connection.Disconnect();
_connection.Dispose();
_connection = null;
}
// warn for disconnection
if (before != Connected)
{
InvokeOnMainThreadAsync(OnConnectedChanged);
}
if (_connection == null)
{
_connection = new Connection(Name, _expectedIp, _expectedPort);
}
_connection.Update();
_connection.Connect();
// warn for reconnection
if (before != Connected)
{
InvokeOnMainThreadAsync(OnConnectedChanged);
}
}
/// <summary>
/// Network thread
/// </summary>
private void ThreadTask()
{
int countBeforeUpdate = 100;
UpdateConnectedStatus();
do
{
if (countBeforeUpdate-- == 0)
{
countBeforeUpdate = 100;
UpdateConnectedStatus();
}
//Receive messages
if (_connection.IsConnected)
{
Receive();
}
}
while (!_exit.WaitOne(TimeToThreadSpleep));
// reset Stop event;
_exit.Reset();
if (_connection != null)
{
_connection.Disconnect();
_connection.Dispose();
_connection = null;
// force event client call in this only case.
InvokeOnMainThreadAsync(OnConnectedChanged);
}
}
protected virtual void LogCommand(string command)
{
if (command != CommandNames.UpdateTimer)
{
Debug.WriteLine(string.Format(" =>[ Receive ] {0} ", command), "connection");
}
}
/// <summary>
/// Send a message to the engine.
/// </summary>
public void Send(string command, Action<IBlob> fillBlobAction = null)
{
var connection = _connection;
if (connection != null)
{
connection.Send(command, fillBlobAction);
}
}
/// <summary>
/// Receive blob from connection and handle it
/// </summary>
private void Receive()
{
foreach (var blob in _connection.Receive())
{
string command = "";
blob.extract(ref command);
LogCommand(command);
// if we receive dataroot from engine, and that we are connected to local host, set DataRoot
// else dataroot send by engine is not the good one
if (command == CommandNames.GetSessionInfo && string.CompareOrdinal(IP, Localhost) == 0)
{
string dataRoot = string.Empty;
blob.extractDataRoot(ref dataRoot);
// init cooker datapath
_connection.Cooker.InitDataPath(dataRoot);
blob.Dispose();
}
else
{
HandleCommand(command, blob);
}
}
}
protected virtual void HandleCommand(string command, IBlob blob)
{
try
{
// Queries are not handled on UI thread by default or it could create dead locks.
if (HandleQueryResult(command, blob))
{
return;
}
CommandHandler handler;
if (_commandHandlers.TryGetHandler(command, out handler))
{
if (handler.HandleOnMainThread)
{
InvokeOnMainThread(handler, blob);
blob = null; // Make sure the blob is not disposed.
}
else
{
handler.Handle(blob);
}
}
}
finally
{
var toDispose = blob as IDisposable;
if (toDispose != null)
{
toDispose.Dispose();
}
}
}
private void InvokeOnMainThread(CommandHandler handler, IBlob blob)
{
// Messages are handled asynchronously to allow queries to be executed even in a message is pending
InvokeOnMainThreadAsync(() =>
{
using (blob as IDisposable)
{
handler.Handle(blob);
}
});
}
/// <summary>
/// Register a command handler for a command name.
/// </summary>
/// <remarks>Two handlers cannot be registered for the same command.</remarks>
private void RegisterHandler(HandlerDictionary handlerDict, string commandName, Action<IBlob> handler, bool handleOnMainThread = true)
{
handlerDict.Register(new CommandHandler(commandName, handleOnMainThread, handler));
}
/// <summary>
/// Register a command handler for a command name.
/// </summary>
/// <remarks>AS two handlers cannot be registered for the same command, we only need command name.</remarks>
private void UnregisterHandler(HandlerDictionary handlerDict, string commandName)
{
handlerDict.Unregister(commandName);
}
/// <summary>
/// Register a command handler for a command name.
/// </summary>
/// <remarks>Two handlers cannot be registered for the same command.</remarks>
public void RegisterHandler(string commandName, Action<IBlob> handler, bool handleOnMainThread = true)
{
RegisterHandler(_commandHandlers, commandName, handler, handleOnMainThread);
}
/// <summary>
/// Register a command handler for a command name.
/// </summary>
/// <remarks>AS two handlers cannot be registered for the same command, we only need command name.</remarks>
public void UnregisterHandler(string commandName)
{
UnregisterHandler(_commandHandlers, commandName);
}
public bool CookTexture(string platform, string filePath)
{
var connection = _connection;
if (connection != null)
{
return connection.CookTexture(platform, filePath);
}
return false;
}
#endregion
#region Queries
/// <summary>
/// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data.
/// </summary>
public IQueryResult<TData> Query<TData>(string queryName, Converter<IBlob, TData> resultParser)
{
return Query(queryName, null, queryName, resultParser);
}
/// <summary>
/// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data.
/// </summary>
public IQueryResult<TData> Query<TData>(string queryName, Action<IBlob> writeCommand, string responseName, Converter<IBlob, TData> resultParser)
{
var result = new QueryResult<TData>(this);
var pendingQuery = new PendingQuery(responseName, blob =>
{
result.Content = resultParser(blob);
});
_pendingQueries.Add(pendingQuery);
Send(queryName, writeCommand);
return result;
}
private bool HandleQueryResult(string command, IBlob blob)
{
int index = _pendingQueries.FindIndex(query => query.CommandName == command);
if (index >= 0)
{
var query = _pendingQueries[index];
_pendingQueries.RemoveAt(index);
query.HandleResultAction(blob);
return true;
}
return false;
}
#endregion
#region General Commands
private void SendExit()
{
Send(CommandNames.Exit);
}
public void SendUndo()
{
Send(CommandNames.Undo);
}
public void SendRedo()
{
Send(CommandNames.Redo);
}
public void SendUnselect()
{
Send(CommandNames.Unselect);
}
#endregion
#region Events
/// <summary>
/// Triggered when connected/disconnected.
/// </summary>
public event EventHandler ConnectedChanged;
protected virtual void OnConnectedChanged()
{
var @event = ConnectedChanged;
if (@event != null)
{
@event(this, EventArgs.Empty);
}
}
public event EventHandler Disposed;
protected virtual void OnDisposed(EventArgs e)
{
var disposed = Disposed;
if (disposed != null)
{
disposed(this, e);
}
}
private void ClosePluginEventHandler(object sender, EventArgs e)
{
Stop();
}
#endregion
#region Nested
private class PendingQuery
{
public string CommandName { get; private set; }
public Action<IBlob> HandleResultAction { get; private set; }
public PendingQuery(string commandName, Action<IBlob> handleResultAction)
{
CommandName = commandName;
HandleResultAction = handleResultAction;
}
}
/// <summary>
/// Class to store all handlers, associated with their command name
/// </summary>
private class HandlerDictionary
{
private readonly Dictionary<string, CommandHandler> _handlers = new Dictionary<string, CommandHandler>();
public bool TryRegister(CommandHandler handler)
{
bool handlerAlreadyRegistered = false;
lock (_handlers)
{
handlerAlreadyRegistered = _handlers.ContainsKey(handler.CommandName);
}
if (handlerAlreadyRegistered)
{
Console.WriteLine("A handler with this command name is already registered. Unregister first if you want to change it!");
}
return handlerAlreadyRegistered;
}
public void Register(CommandHandler handler)
{
lock (_handlers)
{
_handlers.Add(handler.CommandName, handler);
}
}
public bool TryGetHandler(string commandName, out CommandHandler handler)
{
lock (_handlers)
{
return _handlers.TryGetValue(commandName, out handler);
}
}
public void Clear()
{
lock (_handlers)
{
_handlers.Clear();
}
}
public void Unregister(string commandName)
{
lock (_handlers)
{
if (_handlers.ContainsKey(commandName) == true)
_handlers.Remove(commandName);
}
}
}
private class CommandHandler
{
private readonly string _commandName;
private readonly bool _handleOnMainThread;
private readonly Action<IBlob> _handle;
public CommandHandler(string commandName, bool handleOnMainThread, Action<IBlob> handle)
{
_handleOnMainThread = handleOnMainThread;
_handle = handle;
_commandName = commandName;
}
public bool HandleOnMainThread
{
get { return _handleOnMainThread; }
}
public Action<IBlob> Handle
{
get { return _handle; }
}
public string CommandName
{
get { return _commandName; }
}
}
private class Cooker
{
private readonly cookerWrapper _cooker = new cookerWrapper();
private bool _initDone = false;
public void Dispose()
{
_cooker.destroy();
_initDone = false;
}
public void InitDataPath(string dataPath)
{
_cooker.init(dataPath);
_cooker.registerVersionCallback();
_initDone = true;
}
public bool CookTexture(string platform, string filePath)
{
if (_cooker != null)
{
if (_initDone == false)
throw new InvalidOperationException("Cooker has not been initialized");
return _cooker.cookTexture(platform, filePath, true); // bool to UseConfigFile at cooking
}
return false;
}
}
private class Connection
{
private pluginWrapper _plugin = null;
private engineWrapper _engine = new engineWrapper(string.Empty);
private Cooker _cooker = new Cooker();
private readonly string _pluginName;
private readonly string _IP = Localhost;
private readonly int _port = -1;
private bool _connected = false;
private int _countAttempt = 0;
public Connection(string pluginName, string ip, int port)
{
_pluginName = pluginName;
_IP = ip;
_port = port;
}
public void Dispose()
{
if (_connected)
Disconnect();
_cooker.Dispose();
_engine.destroy();
}
public Cooker Cooker
{
get { return _cooker; }
}
public string IP
{
get { return _IP; }
}
public int Port
{
get { return _port; }
}
public bool IsConnected
{
get { return _connected; }
}
public bool CookTexture(string platform, string filePath)
{
if (IsConnected)
{
return _cooker.CookTexture(platform, filePath); // bool to UseConfigFile at cooking
}
return false;
}
public void Connect()
{
if (!_connected)
{
_countAttempt++;
_connected = _engine.connectToHost(IP, _port);
if (_connected)
{
_countAttempt = 0;
Debug.WriteLine("Just connected engine", "connection");
_plugin = new pluginWrapper();
_engine.addPlugin(_pluginName, _plugin);
//Send(CommandNames.PluginId);
// ! this fucking only command is reversed !
using (Blob blob = new Blob())
{
blob.push("PluginId");
blob.push(_pluginName);
blob.sendToHost();
}
Send(CommandNames.GetSessionInfo);
}
else
{
Debug.WriteLine("Failed to connect engine " + _countAttempt + " times.", "connection");
}
}
}
public void Disconnect()
{
if (_connected)
{
_engine.disconnect();
_plugin = null;
_connected = false;
Debug.WriteLine("Just disconnected engine", "connection");
}
}
public void Update()
{
if (_connected)
{
using (var blob = new Blob())
{
if (!blob.sendToHost())
{
Disconnect();
}
}
}
}
private Blob SendStart(string command)
{
Debug.WriteLine(string.Format(" <=[ Send ] {0} ", command), "connection");
var blob = new Blob();
blob.push(_pluginName);
blob.push(command);
return blob;
}
//-----------------------------------------------------------------------------------------
// end blob = send and dispose it
//
private void SendEnd(Blob blob)
{
if (blob != null)
{
blob.sendToHost();
blob.Dispose();
}
}
/// <summary>
/// Send a message to the engine.
/// </summary>
public void Send(string command, Action<IBlob> fillBlobAction = null)
{
Blob blob = SendStart(command);
if (blob != null)
{
if (fillBlobAction != null)
{
fillBlobAction(blob);
}
SendEnd(blob);
}
}
public IEnumerable<Blob> Receive()
{
if (_plugin != null)
{
_engine.update();
var blob = new Blob(false);
while (_plugin.dispatch(blob))
{
yield return blob;
blob = new Blob();
}
blob.Dispose();
}
}
}
#endregion
}
}