/// 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("(.*)",new Ivy.MessageHandler(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;
}
// 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 + ":" + DEFAULT_PORT;
}
#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+");
}
}
}