using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; namespace Plugin { /// /// Base class for plugins. /// 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; /// /// Queries are temporary handlers. They parse the result and are removed from the list. /// private readonly List _pendingQueries = new List(); /// /// Command handlers /// 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 /// /// Invoke a function on the UI thread allowing commands to be executed in GUI context. /// protected abstract void InvokeOnMainThreadAsync(Action action); /// /// Reports a message to the user to let him know that the call to the engine failed. /// Typically, a balloon is shown ... /// public abstract void ReportTimeoutFailure(); #endregion #region Methods /// /// Start the thread to deal with engine remote exchanges /// public void Start() { _tread = new Thread(ThreadTask) { IsBackground = true }; _tread.Start(); Disposed += ClosePluginEventHandler; } /// /// Stop the thread, asking it to exit. /// 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); } } /// /// 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 /// 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); } } /// /// Network thread /// 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"); } } /// /// Send a message to the engine. /// public void Send(string command, Action fillBlobAction = null) { var connection = _connection; if (connection != null) { connection.Send(command, fillBlobAction); } } /// /// Receive blob from connection and handle it /// 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); } }); } /// /// Register a command handler for a command name. /// /// Two handlers cannot be registered for the same command. private void RegisterHandler(HandlerDictionary handlerDict, string commandName, Action handler, bool handleOnMainThread = true) { handlerDict.Register(new CommandHandler(commandName, handleOnMainThread, handler)); } /// /// Register a command handler for a command name. /// /// AS two handlers cannot be registered for the same command, we only need command name. private void UnregisterHandler(HandlerDictionary handlerDict, string commandName) { handlerDict.Unregister(commandName); } /// /// Register a command handler for a command name. /// /// Two handlers cannot be registered for the same command. public void RegisterHandler(string commandName, Action handler, bool handleOnMainThread = true) { RegisterHandler(_commandHandlers, commandName, handler, handleOnMainThread); } /// /// Register a command handler for a command name. /// /// AS two handlers cannot be registered for the same command, we only need command name. 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 /// /// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data. /// public IQueryResult Query(string queryName, Converter resultParser) { return Query(queryName, null, queryName, resultParser); } /// /// Queries the engine for a set of data using the specified command. Parse the blob, and call the handler with the final data. /// public IQueryResult Query(string queryName, Action writeCommand, string responseName, Converter resultParser) { var result = new QueryResult(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 /// /// Triggered when connected/disconnected. /// 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 HandleResultAction { get; private set; } public PendingQuery(string commandName, Action handleResultAction) { CommandName = commandName; HandleResultAction = handleResultAction; } } /// /// Class to store all handlers, associated with their command name /// private class HandlerDictionary { private readonly Dictionary _handlers = new Dictionary(); 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 _handle; public CommandHandler(string commandName, bool handleOnMainThread, Action handle) { _handleOnMainThread = handleOnMainThread; _handle = handle; _commandName = commandName; } public bool HandleOnMainThread { get { return _handleOnMainThread; } } public Action 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(); } } /// /// Send a message to the engine. /// public void Send(string command, Action fillBlobAction = null) { Blob blob = SendStart(command); if (blob != null) { if (fillBlobAction != null) { fillBlobAction(blob); } SendEnd(blob); } } public IEnumerable Receive() { if (_plugin != null) { _engine.update(); var blob = new Blob(false); while (_plugin.dispatch(blob)) { yield return blob; blob = new Blob(); } blob.Dispose(); } } } #endregion } }