/// François-Régis Colin /// http://www.tls.cena.fr/products/ivy/ /// * /// (C) CENA /// * /// namespace IvyBus { using System; using System.IO; using System.Collections; using System.Collections.Specialized; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; using System.Configuration; using System.Windows.Forms; using System.Globalization; using System.Text.RegularExpressions; using System.Text; using System.Reflection; using System.ComponentModel; using System.Diagnostics; /// The Main bus Class /// /// #if (!PocketPC) [ToolboxItemFilter("System.Windows.Forms.Form",ToolboxItemFilterType.Allow)] [DesignerCategory("Component")] [Description("IVY Main API")] #endif public class Ivy : System.ComponentModel.Component { /* return value for Die Event */ public enum ApplicationExit { FORCE_EXIT, DONT_EXIT }; /* Event handler */ public delegate void DirectMessageHandler(IvyClient app, int id, string arg); public delegate void ClientConnectedHandler(IvyClient app); public delegate void ClientDisconnectedHandler(IvyClient app); public delegate ApplicationExit DieHandler(IvyClient app, int id, string arg); public delegate void ClientAddBindingHandler(IvyClient app, string arg); public delegate void ClientRemoveBindingHandler(IvyClient app, string arg); public delegate void MessageHandler(IvyClient app, string[] args); /* Event */ /// fires when a new client connect to the bus /// A callback handling the notification of connexions and /// disconnections, may be null /// /// public event ClientConnectedHandler ClientConnected; /// fires when a client discconnect from the bus public event ClientDisconnectedHandler ClientDisconnected; /// fires when a client receive a direct message from another client public event DirectMessageHandler DirectMessageReceived; /// fires when somebody ask for killing every client on the bus public event DieHandler dieReceived; /// fires when a client receive a add binding from another client public event ClientAddBindingHandler BindingAdd; /// fires when a client receive a remove binding from another client public event ClientRemoveBindingHandler BindingRemove; #if (!PocketPC) [Category("Ivy")] [Bindable(true)] [DefaultValue(false)] #endif public static bool VerboseDebug { get { return debug; } set { debug = value; } } #if (!PocketPC) [Category("Ivy")] #endif public CultureInfo Culture { get { return culture; } set { culture = value; } } /// IvyClients accesses the list of the connected clients #if (!PocketPC) [Browsable(false)] #endif public List IvyClients { get { return clients; } } /// ClientNames accesses the name list of the connected clientsAppName the application name #if (!PocketPC) [Category("Ivy")] [Bindable(true)] [DefaultValue(null)] #endif public string AppName { set { appName = value; } get { return appName; } } /// AppId the Application Unique ID #if (!PocketPC) [Browsable(false)] #endif public string AppId { get { return applicationUniqueId; } } /// AppPriority the Application Priority: the clients list is sorted against priority #if (!PocketPC) [Category("Ivy")] [DefaultValue(DEFAULT_PRIORITY)] #endif public ushort AppPriority { set { applicationPriority = value; lock (clients) { foreach (IvyClient client in clients) { client.stream.TokenApplicationId(applicationPriority, AppId); } } } get { return applicationPriority; } } #if (!PocketPC) [Browsable(false)] #endif public int ProtocolVersion { get { return protocolVersion; } } /// IsRunning is the bus Started and connected ? #if (!PocketPC) [Browsable(false)] #endif public bool IsRunning { get { return !stopped; } } ///SentMessageClasses the first word token of sent messages /// optimise the parsing process when sending messages /// #if (!PocketPC) [Category("Ivy")] [DefaultValue(null)] #endif public string[] SentMessageClasses { get { return sent_messageClasses; } set { sent_messageClasses = value; } } /// ReadyMessage message send when Application receive all the regexp at the connection of the client #if (!PocketPC) [Category("Ivy")] [Bindable(true)] [DefaultValue(null)] #endif public string ReadyMessage { get { return ready_message; } set { ready_message = value; } } #if (!PocketPC) [Category("Ivy")] [DefaultValue(null)] #endif public Control SyncControl { get { return syncControl; } set { syncControl = value; if (syncControl != null) AutoBinding(syncControl); } } internal class MyTcpListener : TcpListener { public MyTcpListener(IPAddress address, int port) : base(address, port) { } public bool IsActive() { return this.Active; } } #if PocketPC internal class Createdelegate { object eventtarget; MethodInfo mymethod; object [] methodparams = new Object[2]; public void Invoke(object sender, System.EventArgs e) { methodparams[1] = e; mymethod.Invoke(eventtarget, methodparams); } public Createdelegate(MethodInfo mi, object eventTarget, object eventObject) { eventtarget = eventTarget; mymethod = mi; methodparams[0] = eventObject; methodparams[1] = null; } } #endif /// the name of the application on the bus internal string appName; /// the port for the UDP rendez vous, if none is supplied internal const ushort DEFAULT_PORT = 2010; // client default priority internal const ushort DEFAULT_PRIORITY = 100; /// the domain for the UDP rendez vous private static readonly string DEFAULT_DOMAIN = "127.255.255.255:" + DEFAULT_PORT; private const string libVersion = "2.0.0"; internal int protocolVersion = 3; private static bool debug = false; private static ushort serial = 0; /* an unique ID for each regexp */ private MyTcpListener app; private List watchers; private volatile Thread serverThread; // to ensure quick communication of the end internal enum BindingType { BindRegexp, BindSimple }; internal struct ApplicationBinding /* Self Applications bindings */ { internal BindingType type; internal ushort key; internal MessageHandler callback; internal string expression; internal object[] args; } internal Dictionary bindings; private List clients; private string[] sent_messageClasses = null; private bool stopped = true; internal ushort applicationPort; /* Application port number */ internal IPAddress applicationHost; /* Application host number */ internal string applicationUniqueId; /* identifier Application unique timestamp-ipaddress-port */ internal ushort applicationPriority = DEFAULT_PRIORITY; private string ready_message = null; private CultureInfo culture = new CultureInfo("en-us"); // for synchronous event private Control syncControl = null; public Ivy() { clients = new List(); bindings = new Dictionary(); #if (!PocketPC ) debug = Properties.Settings.Default.IvyDebug; protocolVersion = Properties.Settings.Default.IvyProtocolVersion; #endif } /// Readies the structures for the software bus connexion. /// /// This sample shows how to start working with Ivy. /// /// Ivy bus = new Ivy("Dummy agent","ready"); /// bus.bindMsg("(.*)",myMessageListener); /// bus.start(null); /// /// How to send & receive: /// the Ivy agent A performs b.bindMsg("^Hello (*)",cb); /// the Ivy agent B performs b2.sendMsg("Hello world"); /// a thread in A will run the callback cb with its second argument set /// to a array of string, with one single element, "world" /// /// /// the real work begin in the start() method /// /// /// The name of your Ivy agent on the software bus /// /// The hellow message you will send once ready /// public Ivy(string name, string message) : this() { appName = name; ready_message = message; } public Ivy(Control sync) : this() { syncControl = sync; } public Ivy(string name, string message, Control sync) : this(name, message) { syncControl = sync; } protected override void Dispose(bool disposing) { if (disposing) { stop(); } base.Dispose(disposing); } // Autobinding on static method public void AutoBinding(Type type) { //Get instance of the IvyBindingAttribute. foreach (MethodInfo m in type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic)) { foreach (IvyBindingAttribute attr in Attribute.GetCustomAttributes(m, typeof(IvyBindingAttribute))) { //TODO check paramater type MessageHandler Debug.WriteLine("IvyBinding '" + attr.GetExpression(null) + "' to Method " + m.Name); MessageHandler handler; #if (PocketPC) //Createdelegate mydlg = new Createdelegate(m, null, EventArgs.Empty); //bindMsg(attr.GetExpression(null), mydlg); handler = null; #else handler = (MessageHandler)Delegate.CreateDelegate(typeof(MessageHandler), m); #endif bindMsg(attr.GetExpression(null), handler); } } } // Autobinding on instance method public void AutoBinding(object obj) { Type type = obj.GetType(); //Get instance of the IvyBindingAttribute. foreach (MethodInfo m in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)) { foreach (IvyBindingAttribute attr in Attribute.GetCustomAttributes(m, typeof(IvyBindingAttribute))) { //TODO check paramater type MessageHandler traceDebug("IvyBinding '" + attr.GetExpression(obj) + "' to Method " + m.Name); MessageHandler handler; #if (PocketPC) handler = null; // TODO #else handler = (MessageHandler)Delegate.CreateDelegate(typeof(MessageHandler), obj, m); #endif bindMsg(attr.GetExpression(obj), handler); } } } /// connects the Ivy bus to a domain or list of domains. /// /// /// One thread (IvyWatcher) for each traffic rendezvous (either UDP broadcast or TCP Multicast). /// One thread (serverThread/Ivy) to accept incoming connexions on server socket. /// a thread for each IvyClient when the connexion has been done. /// /// a domain of the form 10.0.0:1234, it is similar to the /// netmask without the trailing .255. This will determine the meeting point /// of the different applications. Right now, this is done with an UDP /// broadcast. Beware of routing problems ! You can also use a comma /// separated list of domains. /// * /// /// public void start(string domainbus) { domainbus = getDomain(domainbus); try { app = new MyTcpListener(IPAddress.Any, 0); app.Start(); // should be started before getting port applicationHost = getLocalIP(); applicationPort = (ushort)((IPEndPoint)app.LocalEndpoint).Port; applicationUniqueId = string.Format("{0}-{1}-{2}", DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond, applicationHost.ToString().Replace(".", ""), applicationPort); } catch (IOException e) { throw new IvyException("can't open TCP service socket " + e); } traceDebug("lib: " + libVersion + " protocol: " + ProtocolVersion + " TCP service open on port " + applicationPort); watchers = new List(); Domain[] d = parseDomains(domainbus); // readies the rendezvous : an IvyWatcher (thread) per domain bus for (int index = 0; index < d.Length; index++) { IvyWatcher watcher = new IvyWatcher(this, d[index].Domainaddr, d[index].Port); watchers.Add(watcher); } serverThread = new Thread(new ThreadStart(this.Run)); serverThread.Name = "Ivy Tcp Server Thread"; stopped = false; serverThread.Start(); #if (PocketPC ) traceDebug(" Ivy Threading start in progress..."); Thread.Sleep(100); #else // Wait for readyness while ( serverThread.ThreadState != System.Threading.ThreadState.Running || !app.IsActive()) { traceDebug( " Ivy Threading start in progress..." ); Thread.Sleep( 100 ); } #endif // sends the broadcasts and listen to incoming connexions for (int i = 0; i < watchers.Count; i++) { watchers[i].start(); } } internal void SortClients() { lock (clients) { //clients.Sort(new IvyClient.IvyClientPriority()); clients.Sort(); } } /* a small private method for debbugging purposes */ public string domains(string toparse) { StringBuilder s = new StringBuilder("broadcasting on "); Ivy.Domain[] d = parseDomains(toparse); for (int index = 0; index < d.Length; index++) { s.Append(d[index].Domainaddr); s.Append(":"); s.Append(d[index].Port); s.Append(" "); } return s.ToString(); } internal Domain[] parseDomains(string domainbus) { string[] st = domainbus.Split(','); Domain[] d = new Domain[st.Length]; for (int i = 0; i < st.Length; i++) { d[i] = new Domain(IvyWatcher.getDomain(st[i]), IvyWatcher.getPort(st[i])); } return d; } /// disconnects from the Ivy bus /// public void stop() { if (stopped) return; lock (app) { stopped = true; traceDebug("beginning stopping the bus"); try { // stopping the serverThread if (serverThread != null) { app.Stop(); // Wait for Thread to end. bool term = serverThread.Join(10000); if (!term && (serverThread != null)) serverThread.Abort(); serverThread = null; } // The serverThread might be stopped even before having been created if (app != null) app.Stop(); // stopping the IvyWatchers if (watchers != null) for (int i = 0; i < watchers.Count; i++) { ((IvyWatcher)watchers[i]).stop(); } // stopping the remaining IvyClients // copy the values in temporary variable to eliminate Thread modifying collection if (clients.Count != 0) { IvyClient[] copyClient; copyClient = new IvyClient[clients.Count]; lock (clients) { clients.CopyTo(copyClient, 0); } foreach (IvyClient client in copyClient) { client.close(true); // will notify the reading thread //removeClient(client); already donne in the thread } } } catch (IOException e) { traceDebug("IOexception Stop " + e.Message); } traceDebug("the bus should have stopped so far"); } } /// Send a formated message to someone on the bus /// /// /// Performs a pattern matching according to everyone's regexps, and sends /// the results to the relevant ivy agents. /// There is one thread for each client connected, we could also /// create another thread each time we send a message. /// /// A string message format to build the message /// args used in message format /// /// the number of messages actually sent /// /// public int sendMsg(string format, params object[] args) { string msg = string.Format(culture, format, args); int count = 0; // an alternate implementation would one sender thread per client // instead of one for all the clients. It might be a performance issue lock (clients) { foreach (IvyClient client in clients) { count += client.sendMsg(msg); } } return count; } /// Subscribes to a regular expression. /// /// /// The callback will be executed with /// the saved parameters of the regexp as arguments when a message will sent /// by another agent. A program doesn't receive its own messages. /// /// /// a regular expression, groups are done with parenthesis /// /// any objects implementing the Ivy.MessageListener /// /// the id of the regular expression /// /// // // for compatibility raison with old IVY #if OLD_API [Obsolete("Will be removed in next version")] public int bindMsg(string regexp, MessageHandler callback) { return bindMsg( regexp, callback, null); } #endif public int bindMsg(string regexp, MessageHandler callback, params object[] args) { // creates a new binding (regexp,callback) ApplicationBinding newbind; newbind.type = BindingType.BindRegexp; newbind.key = serial++; newbind.expression = regexp; newbind.callback = callback; newbind.args = args; lock (bindings) bindings.Add(newbind.key, newbind); // notifies the other clients this new regexp lock (clients) { foreach (IvyClient c in clients) { c.stream.TokenAddBinding(newbind); } } return newbind.key; } /// unsubscribes a regular expression /// /// the id of the regular expression, returned when it was bound /// /// public void unBindMsg(ushort id) { if (!bindings.ContainsKey(id)) { throw new IvyException("client wants to remove an unexistant regexp " + id); } ApplicationBinding bind = (ApplicationBinding)bindings[id]; lock (clients) { foreach (IvyClient c in clients) { c.stream.TokenDelBinding(id); } } lock (bindings) bindings.Remove(id); } /// unsubscribes a regular expression /// /// a boolean, true if the regexp existed, false otherwise or /// whenever an exception occured during unbinding /// /// the string for the regular expression /// /// public bool unBindMsg(string re) { foreach (ApplicationBinding bind in bindings.Values) { if (bind.expression == re) { try { unBindMsg(bind.key); } catch (IvyException) { return false; } return true; } } return false; } /// Subscribes to a simple expression ( msg ar1 arg2 arg3 etc). /// /// /// The callback will be executed with /// the saved parameters of the regexp as arguments when a message will sent /// by another agent. A program doesn't receive its own messages. /// /// /// a regular expression, groups are done with parenthesis /// /// any objects implementing the Ivy.MessageListener /// /// the id of the regular expression /// /// public int bindSimpleMsg(string expression, MessageHandler callback, params object[] args) { // creates a new binding (regexp,callback) ApplicationBinding newbind; newbind.type = BindingType.BindSimple; newbind.key = serial++; newbind.expression = expression; newbind.callback = callback; newbind.args = args; lock (bindings) bindings.Add(newbind.key, newbind); // notifies the other clients this new regexp lock (clients) { foreach (IvyClient c in clients) { c.stream.TokenAddBinding(newbind); } } return newbind.key; } public int Die(string target, string message) { List v = getIvyClientsByName(target); for (int i = 0; i < v.Count; i++) v[i].stream.TokenDie(0, message); return v.Count; } public int Ping(string target, string message) { List v = getIvyClientsByName(target); for (int i = 0; i < v.Count; i++) v[i].stream.TokenPing(message); return v.Count; } internal bool IsSynchronous() { if (syncControl != null #if (!PocketPC ) && syncControl.Created #endif ) return true; else return false; } internal void FireDirectMessage(IvyClient app, int id, string arg) { if (DirectMessageReceived != null) { if (IsSynchronous()) syncControl.Invoke(DirectMessageReceived, new object[] { app, id, arg }); else DirectMessageReceived(app, id, arg); } } internal void FireClientConnected(IvyClient app) { if (ClientConnected != null) { if (IsSynchronous()) syncControl.Invoke(ClientConnected, new object[] { app }); else ClientConnected(app); } } internal void FireClientDisconnected(IvyClient app) { if (ClientDisconnected != null) { if (IsSynchronous()) syncControl.Invoke(ClientDisconnected, new object[] { app }); else ClientDisconnected(app); } } internal void FireClientAddBinding(IvyClient app, string regexp) { if (BindingAdd != null) { if (IsSynchronous()) syncControl.Invoke(BindingAdd, new object[] { app, regexp }); else BindingAdd(app, regexp); } } internal void FireClientRemoveBinding(IvyClient app, string regexp) { if (BindingRemove != null) { if (IsSynchronous()) syncControl.Invoke(BindingRemove, new object[] { app, regexp }); else BindingRemove(app, regexp); } } internal ApplicationExit FireDie(IvyClient app, int id, string arg) { if (dieReceived != null) { if (IsSynchronous()) return (ApplicationExit)syncControl.Invoke(dieReceived, new object[] { app, id, arg }); else return dieReceived(app, id, arg); } return ApplicationExit.FORCE_EXIT; } /* * removes a client from the list */ internal void removeClient(IvyClient c) { lock (clients) { clients.Remove(c); } } /// gives a list of IvyClient(s) with the name given in parameter /// /// The name of the Ivy agent you're looking for /// public List getIvyClientsByName(string name) { List v = new List(); foreach (IvyClient ic in clients) { if (ic.ApplicationName.CompareTo(name) == 0) v.Add(ic); } return v; } /////////////////////////////////////////////////////////////////: // // Protected methods // /////////////////////////////////////////////////////////////////: internal void addClient(Socket socket, string appname) { if (stopped) return; IvyClient client = new IvyClient(this, socket, appname); lock (clients) { clients.Add(client); } traceDebug(ClientNames); } internal void FireCallback(IvyClient client, int key, string[] arg) { ApplicationBinding bind = bindings[key]; if (bind.callback == null) { throw new IvyException("(callCallback) Not regexp matching id " + key); } if (IsSynchronous()) syncControl.Invoke(bind.callback, new object[] { client, arg }); else bind.callback(client, arg); } public static IPAddress getLocalIP() { IPAddress returnaddr = null; //TODO remove ALL reverse DNS search !!!! IPHostEntry ip = Dns.GetHostEntry(Dns.GetHostName()); foreach (IPAddress addr in ip.AddressList) { returnaddr = addr; if (IPAddress.IsLoopback(addr)) continue; break; } return returnaddr; } public static string getDomain(string domainbus) { #if (PocketPC) if (domainbus == null || domainbus.Length == 0) { IPAddress addr = getLocalIP(); //TODO Find Braodcast addr from IP; byte[] bytes = addr.GetAddressBytes(); if (IPAddress.IsLoopback(addr)) { // 127.255.255.255 bytes[1] = 255; bytes[2] = 255; bytes[3] = 255; } else { // else assume class C network // TODO find exact netmask bytes[3] = 255; } IPAddress bcast = new IPAddress(bytes); domainbus = bcast + ":" + IvyWatcher.getPort(domainbus); } #else if (domainbus == null || domainbus.Length == 0 ) { domainbus = Environment.GetEnvironmentVariable("IVYBUS"); } if (domainbus == null || domainbus.Length == 0) { domainbus = Properties.Settings.Default.IvyBus; } #endif if (domainbus == null || domainbus.Length == 0) domainbus = DEFAULT_DOMAIN; return domainbus; } /// checks the "validity" of a regular expression. //TODO put in IvyBinding internal bool CheckRegexp(string exp) { bool regexp_ok = true; // Attention Bug // ClockStop ClockStart & ^Clock(Start|Pause) // should Stop to the first parent if ((sent_messageClasses != null) && exp.StartsWith("^")) { regexp_ok = false; // extract first word from regexp... string token = Regex.Replace(exp, @"^\^(?[a-zA-Z_0-9-]+).*", @"${token}"); foreach (string exp_class in sent_messageClasses) { if (exp_class.StartsWith(token)) return true; } } return regexp_ok; } /* * prevents two clients from connecting to each other at the same time * there might still be a lingering bug here, that we could avoid with the * SchizoToken. */ //TODO bug multiple instance Reco car en 127.0.0.1 internal bool checkConnected(IvyClient clnt) { if (clnt.AppPort == 0) return false; lock (clients) { foreach (IvyClient client in clients) { if (clnt != client && client.sameClient(clnt)) return true; } } return false; } /* * the service socket thread reader main loop */ private void Run() { traceDebug("Ivy service Thread started"); bool running = true; while (running) { try { Socket socket = app.AcceptSocket(); if (stopped) break; // early disconnexion addClient(socket, "Unkown(waiting for name reception)"); // the peer called me } catch (IOException e) { traceDebug("Ivy server socket reader caught an exception: " + e.Message); } catch (SocketException e) { traceDebug("my server socket has been closed " + e.Message); running = false; } } traceDebug("Ivy service Thread stopped"); } [Conditional("DEBUG")] private void traceDebug(string s) { Trace.Assert(!debug, "-->ivy<-- " + s); } internal class Domain { public virtual string Domainaddr { get { return domainaddr; } } public virtual int Port { get { return port; } } private string domainaddr; private int port; public Domain(string domainaddr, int port) { this.domainaddr = domainaddr; this.port = port; } public override string ToString() { return domainaddr + ":" + port; } } public static bool ValidatingDomain(string p) { // domain if of the form ip1[:port][,ip2[:port]] with ip of the form n1.n2.n3.n4 return Regex.IsMatch(p, @"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+"); } } }