CSj/CS.java
changeset 0 5c129dd80d4f
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CSj/CS.java	Thu Nov 21 14:55:10 2019 +0100
@@ -0,0 +1,1135 @@
+import java.util.*;
+import java.io.*;
+import java.net.*;
+import javax.net.ssl.*;
+import java.security.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.event.*;
+import javax.swing.*;
+import javax.swing.border.*;
+
+class Debug {
+	static int debug;
+	String debid = "";
+	Debug(Debug d) { debid = d.debid; }
+	Debug(String debid) { this.debid = debid; }
+	Debug() {}
+	static void pRe(Object o) {
+		System.err.println((new Date().getTime()) + " " + o);
+		System.err.flush();
+	}
+	void log(int level, Object o) {
+		if(debug == 7) { if(level == 7) pRe(debid + ": " + o); }
+		else if(debug >= level) pRe(debid + ": " + o);
+	}
+	boolean abendMsg(String msg, Exception x) {
+		log(0, "ABEND: " + msg + (x == null ? "" : (": " + x.getClass().getSimpleName() + ": " + x.getMessage())));
+		return false;
+	}
+	String prBuf(ByteBuffer b) { return b.position() + "/" + b.remaining() + "/" + b.limit(); }
+}
+class Data extends Debug implements Serializable {
+	static final long serialVersionUID = 42;
+	class DataObj implements Serializable {
+		static final long serialVersionUID = 42;
+		String text;
+		int ttl;
+		int lport;
+		int rport;
+	   DataObj(String s, int ttl) {		
+		this.text = s;
+		this.ttl = ttl;
+		this.lport = 0;
+		this.rport = 0;
+	   }	   
+	}
+	ByteBuffer buf;
+	DataObj data;
+	Data(Debug deb, String s, int ttl) throws Exception {
+		super(deb.debid + " DATA");
+		data = new DataObj(s, ttl);
+		if(CS.fake != 0 && debid.equals("DEMO SSL MASH DATA")) throw new Exception("faked error");		// <-----------------------------------------------
+		load();
+	}
+	Data(Debug deb, ByteBuffer b) throws Exception {
+		super(deb);
+		debid += " DATA";
+		log(5, "unloading data objects from buffer...");
+		unload();
+	}	
+	Data(Debug deb) {
+		super(deb);
+		debid += " DATA";
+	}
+	void load() throws IOException {
+		log(5, "loading data objects to buffer...");
+		ByteArrayOutputStream bas = new ByteArrayOutputStream();
+		try { 
+			new ObjectOutputStream(bas).writeObject(data);
+			buf = ByteBuffer.wrap(bas.toByteArray());
+		} catch(IOException x) { abendMsg("data buffer " + data + " load error", x); throw new IOException(x); }
+	}
+	void unload() throws Exception {
+		buf.rewind();
+		data = (DataObj) new ObjectInputStream(new ByteArrayInputStream(buf.array())).readObject();
+	}
+	int dttl() { 
+		return --data.ttl;
+	}
+	int ttl() { 
+		return data.ttl;
+	}
+	boolean equals(Data data) {
+		return this.data.text.equals(data.data.text);
+	}
+	public String toString() {
+		return "data: ttl=" + data.ttl + ", text=" + 
+			(data.text.length() < 24 ? data.text : data.text.substring(0, 8) + "-------" + data.text.substring(data.text.length() - 8));
+	}
+}
+class Node extends Debug implements Runnable {
+	boolean kicker, closing = false;
+	int thisNode, nextNode, closingThreshHold = 0;
+	Data data;
+	Selector selector;
+	ServerSocketChannel ssc;
+	SelectionKey ssk;
+	SSLContext sslC = null;
+	HashMap<Integer, SockIO> cliSide = new HashMap<Integer, SockIO>();
+	HashMap<SelectionKey, SockIO> srvSide = new HashMap<SelectionKey, SockIO>();
+	Constellation con;
+	Node(Constellation con, int port) {
+		super(con);
+		debid = (con.ssl ? "" : "non") + "SSL " + (con.mash ? "mash" : "ring") + " node " + port;
+		log(4, "initalizing...");
+		try {
+			this.con = con;
+			thisNode = port;
+			kicker = (thisNode == con.first);
+			if(con.ssl) try { prepareSSL(); } catch(Exception x) { abend("SSL preparation", x); throw new Exception(); }
+			try { bind(); } catch(Exception x) { abend("bind", x); throw new Exception(); }
+			data = new Data(this);		
+			data.buf = ByteBuffer.allocate(con.data.buf.limit());
+			log(5, "initalized");
+		} catch(Exception x) { abend("initialization", x); }
+	}
+	public void run() {
+		runLoop();		
+		log(2, "ended");
+		synchronized(con.glob) { con.glob.spawned--; con.glob.notify(); }
+	}
+	public void runLoop() {
+		if(con.glob.doForward && kicker) {		// kick off
+			log(1, "ready to initial send (" + con.data.buf.limit() + " bytes) " + con.data.toString());
+			data.buf.put(con.data.buf); data.buf.flip();
+		   	try { data.unload(); data.load(); } catch(Exception x) {};
+		   	doForward();
+		}
+		//if(con.glob.doForward && !con.glob.abend) do {	// main loop
+		if(con.glob.doForward) do {	// main loop
+			int k = 0;
+			log(5, "main loop, waiting for " + (con.glob.doForward ? "data" : "close") + 
+					" from " + srvSide.size() + " open sockets, timeout=" + CS.selTO + " msecs, closingThreshHold=" + closingThreshHold);
+			if(!con.glob.doForward && closingThreshHold == 0) closingThreshHold = 5000 / CS.selTO;					
+			try {
+				try { while((k = selector.select(CS.selTO)) == 0 && con.glob.doForward) {}
+				} catch(Exception x) {	abend("socket channel select", x); throw new Exception(x); }
+				if(k > 0) {
+					for(Iterator<SelectionKey> ski = selector.selectedKeys().iterator(); ski.hasNext();) {
+						SelectionKey sk = ski.next();
+						if(sk.isAcceptable()) 
+							try { acc(); } catch(Exception x) { abend("main loop accept error", x); throw new Exception(x); }
+						if(sk.isReadable()) 
+							try { forward(sk); } catch(Exception x) { abend("main loop forward error", x); throw new Exception(x); }
+						ski.remove();	
+					}
+				}
+			} catch (Exception x) {	abend("main loop", x); }
+			if(con.glob.stop) {
+				log(1, "constellation stop, closing all connections");
+				stop(); return;
+			}			
+		} while(con.glob.doForward || ((srvSide.size() > 0) && (closingThreshHold-- > 0)));
+		//} while((con.glob.doForward || (srvSide.size() > 0)) && !con.glob.abend);
+		log(5, "closing ssc...");
+		try { ssc.close(); } catch (Exception x) { abend("ssc close", x); } 
+		if(kicker && !con.glob.stop && !con.glob.abend) con.glob.abend = !data.equals(con.data);
+	}
+	private void forward(SelectionKey sk) {
+		log(5, "entering 'forward'...");
+		SockIO si = srvSide.get(sk);
+		if(!si.get()) {
+			stop(); 
+			si.close();
+			srvSide.remove(sk);
+		}
+		else {
+			log(5, "received " + prBuf(data.buf));
+			try { data.unload(); } catch(Exception x) { abend("data unload at forwarding", x); return; }
+			if(kicker) {
+				log(3, "received from " + (con.mash ? "mash: " : "ring: ") + data.toString());
+				if(data.dttl() <= 0) {	 
+					if(CS.isGui) con.cBox.ttl = data.ttl(); 
+					log(1, "TTL 0 reached, received " + data.toString() + ", leaving 'forward' closing all connections");
+					stop(); return;
+				}
+				try { data.load(); } catch(Exception x) { abend("data load at forwarding", x); return; }
+			}
+			if(con.glob.doForward) doForward();
+		}
+		log(4, "leaving 'forward'");
+	}
+	private void doForward() {
+		if((nextNode = chooseNextNode()) == 0) { stop(); return; }
+		log(3, "forwarding data to " + nextNode);
+		if(CS.pacing) pacing(nextNode);
+		if(CS.pace > 0) try { Thread.sleep(CS.pace); } catch(InterruptedException i) {};
+		if(!cliSide.get(nextNode).put()) stop();
+	}
+	private int chooseNextNode() {
+		int next;
+		if(con.mash) while((next = (con.first +	CS.r.nextInt(con.nodes))) == thisNode);
+		else { next = thisNode + 1; if(next > con.last) next = con.first; }
+		if(cliSide.containsKey(next)) return next;
+		else return (conn(next) ? next : 0);
+	}
+	private void pacing(int nextNode) {
+		log(4, "constls.pacing");
+		synchronized(CS.constls) {
+			if(!con.glob.pacingGo) try { CS.constls.wait(); } catch(InterruptedException x) {}
+			if(nextNode > 0) {
+				con.cBox.currLink[0] = thisNode - con.first; 
+				con.cBox.currLink[1] = nextNode - con.first;
+			}	 
+			con.cBox.ttl = data.ttl();
+			log(4, "constls.notify"); CS.constls.notify();
+			con.glob.pacingGo = false;
+		}	
+	}	
+	public void stop() {
+		con.glob.doForward = false;
+		synchronized(con) { con.notify(); }
+		closeNode();
+	}
+	private void abend(String msg, Exception x) {
+		abendMsg(msg, x);
+		con.glob.abend = true;
+		CS.isRun = false;
+		stop();
+	}
+	private void closeNode() {
+		if(!closing) new Thread(new closeClients(this)).start();
+		closing = true;
+	}
+	private class closeClients extends Debug implements Runnable {
+		closeClients(Debug deb) { this.debid = deb.debid + " clients CLOSE"; }
+		public void run() {
+			log(4, "starting...");
+			for(Iterator<Integer> i = cliSide.keySet().iterator(); i.hasNext();) {
+				int port = i.next();
+				log(4, "closing conn to " + port);
+				cliSide.get(port).close();
+			}
+			log(4, "end");
+		}
+	}
+	private void prepareSSL() throws Exception {
+		char[] passphrase = "passphrase".toCharArray();
+		KeyStore ks = KeyStore.getInstance("JKS");
+		String ksFile = (System.getenv("KSF") == null) ? CS.cePath + "/testkeys" : System.getenv("KSF");
+		FileInputStream kfs = new FileInputStream(ksFile);
+		ks.load(kfs, passphrase);
+		KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+		kmf.init(ks, passphrase);
+		KeyStore ts = KeyStore.getInstance("JKS");
+		FileInputStream tfs = new FileInputStream(ksFile);
+		ts.load(tfs, passphrase);
+		TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+		tmf.init(ts);
+		sslC = SSLContext.getInstance("TLS");
+		sslC.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+		kfs.close();
+		tfs.close();
+	}
+	private void bind() throws IOException {
+		log(5, "binding...");
+		selector = Selector.open();
+		ssc = ServerSocketChannel.open();
+		ssc.configureBlocking(false);
+		ssc.socket().bind(new InetSocketAddress(thisNode), 1024);
+		ssk = ssc.register(selector, SelectionKey.OP_ACCEPT);
+		log(2, "bound to " + thisNode);
+	}
+	private boolean conn(int remPort) {
+		int retry = CS.connThreshold;
+		boolean connected = false;
+		SocketChannel sc = null;
+		log(4, "connecting to " + remPort + ", timeout=" + (CS.connThreshold*CS.connTO)/1000 + " secs. ...");
+		while(!connected && retry-- > 0 && con.glob.doForward) {
+			try {
+				sc = SocketChannel.open(new InetSocketAddress("localhost", remPort));
+				connected = true;
+			} catch(Exception x) {
+				if(x.getMessage().equals("Connection refused")) {
+					try { Thread.sleep(CS.connTO); } catch(InterruptedException i) {}
+				} else { abendMsg("connection to " + remPort, x); return false; }
+			}
+		}
+		if(!con.glob.doForward) return false;
+		if(connected) {
+			try { cliSide.put(remPort, new SockIO(this, sc, false)); 
+			} catch(Exception x) { abendMsg("connection to " + remPort, x); return false; }
+			log(2, "connected after " + (CS.connThreshold - retry + 1) + " retries"); 
+			return true;
+		}
+		else { abendMsg("connection to " + remPort + " timeout", null); return false; }
+	}
+	private void acc() throws Exception {
+		try { 
+			SocketChannel sc = ssc.accept();
+			sc.configureBlocking(false);
+			SockIO si = new SockIO(this, sc, true);
+			SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);
+			srvSide.put(sk, si);
+			CS.connCnt++;
+			log(2, "connection accepted");
+		} catch(Exception x) { abend("connection accept", x); throw new Exception(x); }
+	}
+	private class SockIO extends Debug {
+		SocketChannel sc;
+		Selector handshakeSelector;
+		SelectionKey sk;
+		Runnable ru;
+		SSLSession session;
+		SSLEngine e;
+		SSLEngineResult r;
+		ByteBuffer ci, co, ib;
+		boolean wrapper;
+		SockIO(Debug deb, SocketChannel sc, boolean server) throws Exception {
+			this.debid = deb.debid + " socket " + (server ? "(server)" : "(client)"); 
+			log(5, "initializing...");
+			this.sc = sc;
+			if(con.ssl) {
+				handshakeSelector = Selector.open();
+				sc.configureBlocking(false);
+				sk = sc.register(handshakeSelector, SelectionKey.OP_READ);
+				e = sslC.createSSLEngine();
+				session = e.getSession();
+				int am = session.getApplicationBufferSize();
+				int pm = session.getPacketBufferSize();
+				ib = ByteBuffer.allocate(am);	// pišvejcova konstanta
+				co = ByteBuffer.allocateDirect(pm);
+				ci = ByteBuffer.allocateDirect(pm);
+				if(server) {
+					e.setUseClientMode(false);
+					e.setNeedClientAuth(true);
+				} else e.setUseClientMode(true);
+			}
+			log(4, "initialized");
+		}
+		String prBuf(ByteBuffer b) { return b.position() + "/" + b.remaining() + "/" + b.limit() + "/" + b.capacity(); }
+		private String eStat() {
+			String stat;
+			if (r != null)
+				stat = r.getStatus() + "/" + r.getHandshakeStatus() + "/" + e.getHandshakeStatus() + 
+					", bytes: " + r.bytesConsumed() + "/" + r.bytesProduced();
+			else stat = "-/-/" + e.getHandshakeStatus() + " -/-";
+			return stat;
+		}
+		//-- result status
+		private boolean isOK() {
+			return r.getStatus() == SSLEngineResult.Status.OK; }		
+		private boolean isClosed() {
+			return r.getStatus() == SSLEngineResult.Status.CLOSED; }		
+		private boolean isBad() {
+			return r.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW; }
+		//-- result handshake status
+		private boolean handShake() { 
+			return r.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; }
+		private boolean handShakeEnd() {
+			return r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.FINISHED; }
+		private boolean needTask() {
+			return r.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK; }
+		//-- engine handshake status
+		private boolean needUnwrap() {
+			return e.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP; }
+		private boolean needWrap() {
+			return e.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP;  }
+		private int read(SocketChannel sc, ByteBuffer b) {
+			int n = -1, k = 0;
+			log(5, "read, " + prBuf(b) + " ...");
+			if(con.ssl) {
+				try { while((k = handshakeSelector.select(CS.selTO)) == 0) { if(!con.glob.doForward) break; } 
+				} catch (Exception x) {	abendMsg("handshake select", x); k = 0; }
+				if(k>0 && sk.isReadable()) {
+					try { n = sc.read(b); } catch(Exception x) { abendMsg("socket channel read", x); n = -1; }
+					handshakeSelector.selectedKeys().remove(sk);
+				}
+			} else 
+				try { while(b.hasRemaining()) { n += sc.read(b); if(n < 0) break; }; n++; 
+				} catch(Exception x) { abendMsg("socket channel read", x); n = -1; };
+			log(4, "read " + n);
+			return n;
+		}		
+		private int write(SocketChannel sc, ByteBuffer b) {
+			log(5, "writing " + prBuf(b) + " ...");
+			int n = 0;
+			try { n = sc.write(b); } catch(Exception x) { abendMsg("socket channel write", x); n = -1; }
+			log(4, "written " + n);
+			return n;
+		}		
+		private boolean replenish() {
+			log(5, "replenishing ci...");
+			ci.clear();
+			int n = read(sc, ci);
+			ci.flip();
+			return (n >= 0);
+			
+		}
+		void handleUnWrapStatus() {
+			ByteBuffer b;
+			if(r.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) {
+				log(5, "unwrap: ib BUFFER_OVERFLOW " + prBuf(ib));
+				if(ib.position() > 0) { ib.flip(); data.buf.put(ib); ib.clear(); }
+				else {
+					b = ByteBuffer.allocate((int)(1.25 * ib.capacity()));
+					ib.flip(); b.put(ib); ib = b; }
+				log(5, "ib " + prBuf(ib));
+			}
+			if(r.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) {
+				int n;
+				log(5, "unwrap: ci BUFFER_UNDERFLOW " + prBuf(ci));
+				if(ci.limit() < ci.capacity()) { 
+					ci.mark(); ci.position(ci.limit()); ci.limit(ci.capacity());
+					n = read(sc, ci); ci.limit(ci.position()); ci.reset(); }
+				else {
+					n = ci.capacity(); if(ci.position() == 0) n *= 2;
+					b = ByteBuffer.allocate(n); b.put(ci); ci = b;
+					n = read(sc, ci); ci.flip(); }
+				log(5, "additional " + n + " bytes read: " + prBuf(ci)); }
+		}
+		boolean doWrap() {
+			log(5, "entering " + "wrap, ob: " + prBuf(data.buf) + ", co: " + prBuf(co) + " ...");
+			try { r = e.wrap(data.buf, co); } catch (Exception x) { abend("SSL engine wrap", x); return false; }
+			//if(isBad()) { con.glob.abend = true; return abendMsg("SSL engine status: " + r.getStatus().toString(), null); }
+			if(isBad()) { abend("SSL engine status: " + r.getStatus().toString(), null); return false; }
+			log(4, "after " + (wrapper ? "wrapper" : "unwrapper") + " wrap: " + eStat() + ", ob: " + prBuf(data.buf) + ", co: " + prBuf(co));
+			if(isClosed()) return false;
+			return true;
+		}
+		private boolean wrap() {
+			do {co.clear();
+				if(!doWrap()) return false;
+				if(needTask()) while((ru = e.getDelegatedTask()) != null) ru.run();
+				co.flip();
+				if(handShake() && (write(sc, co) == -1)) return false;
+			} while (needWrap());
+			if(needUnwrap() && !( replenish() && unwrap() )) return false;
+			log(5, (wrapper ? "wrapper" : "unwrapper") + " wrap HS finished=" + handShakeEnd());
+			if(wrapper && handShakeEnd()) {
+				co.clear();
+				if(!doWrap()) return false;
+				co.flip();
+			}
+			return true;
+		}
+		boolean doUnwrap() {
+			log(5, "entering " + "unwrap, ib: " + prBuf(ib) + ", ci: " + prBuf(ci) + " ...");
+			try { r = e.unwrap(ci, ib); } catch (Exception x) { abend("SSL engine unwrap", x); return false; }
+			log(4, "after " + (wrapper ? "wrapper" : "unwrapper") + " unwrap: " + eStat() + ", ib: " + prBuf(ib) + ", ci: " + prBuf(ci));
+			if(isClosed()) return false;
+			return true;
+		}
+		private boolean unwrap() {
+			do {				// while( needUnwrap() )
+				do {			// while( needUnwrap() && ci.hasRemaining() )
+					if(!doUnwrap()) return false;
+					if(needTask()) while((ru = e.getDelegatedTask()) != null) ru.run();
+				} while(needUnwrap() && ci.hasRemaining());
+				if(needUnwrap() && !replenish()) return false;
+			} while(needUnwrap());
+			if(needWrap() && !wrap()) return false;
+			if(!wrapper) {
+				if(handShakeEnd() && !replenish()) return false; 
+				if(handShakeEnd() || !handShake()) {
+					handleUnWrapStatus();
+					while(ci.hasRemaining()) {
+						do {
+							if(!doUnwrap()) return false;
+							handleUnWrapStatus();
+						} while(!isOK());
+					}
+				}
+			}
+			return true; 
+		}		
+		boolean get() {
+			wrapper = false;
+			boolean got = false;
+			data.buf.clear();
+			if(con.ssl) {
+				while(data.buf.hasRemaining()) {
+					ib.clear();
+					int n;
+					ci.clear(); n  = read(sc, ci); ci.flip();
+					if(n < 0) { got = false; break; }
+					else got = (unwrap() && !isClosed());
+					if(got) { ib.flip(); data.buf.put(ib); log(4, "partially got " + prBuf(data.buf)); }
+					else break;
+				}
+			} 
+			else try { got = (read(sc, data.buf) >= 0); } catch(Exception x) { got = abendMsg("socket read", x); }
+			return got;
+		}
+		boolean put() {
+			wrapper = true;
+			boolean put = false;
+			log(4, "put: ob: " + prBuf(data.buf));
+			try {
+				do {				
+					if(con.ssl) {
+						if(!wrap() || isClosed()) return false;
+						else put = (write(sc, co) >= 0);
+					} else put = (write(sc, data.buf) >= 0);	
+				} while(put && data.buf.hasRemaining());
+				if(put) CS.forwCnt++;
+			} catch(Exception x) { put = abendMsg("socket write", x); }
+			return put;
+		}
+		void close() {
+			log(4, "terminating connection...");
+			try {
+				if(con.ssl) {
+					data.buf.put(ByteBuffer.wrap("".getBytes()));
+					e.closeOutbound();
+					wrap();
+				}
+				sc.close();
+			} catch(Exception x) {}
+		}
+	}
+}
+class Constellation extends Debug implements Runnable {
+	class Glob { 
+		boolean doForward = true;
+		boolean pacingGo = false;
+		boolean stop = false;
+		boolean abend = false;
+		int D = 0;
+		int spawned = 0;
+		int from = 0, to = 0;
+	}
+	volatile Glob glob;
+	boolean mash, ssl;
+	int first, last, nodes;
+	Data data;
+	CS cs;
+	Gui.CBox cBox;
+	String label;
+	Constellation(CS cs, Gui.CBox cBox, boolean mash, boolean ssl) { 
+		label = (ssl ? "" : "non") + "SSL " + (mash ? "MASH" : "RING");
+		debid = "DEMO " + label;
+		this.cs = cs;
+		this.cBox = cBox;
+		this.mash = mash;
+		this.ssl = ssl;
+		if(mash) { first = CS.mp0; nodes = CS.mn; }
+		else { first = CS.rp0; nodes = CS.rn; }
+		glob = new Glob();
+	}
+	Constellation(Constellation con) {
+		super(con);
+		this.cs = con.cs;
+		this.mash = con.mash;
+		this.ssl = con.ssl;
+		this.first = con.first;
+		this.last = con.last;
+		this.nodes = con.nodes;	
+		this.glob = con.glob;
+	}
+	void stop() { glob.stop = true; }
+	//void reset() {
+		//first = mash ? CS.mp0 : CS.rp0;
+		//first += nodes;
+		//glob.doForward = true;
+		//glob.abend = false;
+		//glob.spawned = 0;
+	//}
+	public void run() {
+		log(4, "starting");		
+		glob.pacingGo = true;
+		runNodes();
+	   	synchronized(CS.constls) { CS.constls.notify(); }
+	   	synchronized(cs) { 
+			if(--CS.notFinished == 0) cs.notifyAll(); 
+			else try { cs.wait(); } catch(InterruptedException e) {}; 
+		}
+		log(1, glob.abend ? "BAD: constellation not finished correctly" : "OK, constellation finished correctly");
+		if(glob.abend) CS.abend = true;
+		CS.spawned--;
+		synchronized(cs) { cs.notify(); }
+	}
+	void runNodes() {	
+		log(1, nodes + " nodes constellation, ttl=" + CS.pttl + " starting...");
+		first += (ssl ? 500 : 0);
+		last = first + nodes - 1;
+		if(data == null) 
+			try { data = new Data(this, CS.text, CS.pttl);
+			} catch(Exception x) { abendMsg("creating initial data", x); return; }
+        synchronized(glob) { 
+			for(int port = first; (port < first + nodes); port++) {
+				try {
+					new Thread(new Node(this, port), "Node " + port).start();
+					log(3, "node " + port + " established");
+					glob.spawned++;
+				} catch(Exception x) { glob.doForward = false; glob.abend = true; break; } 
+			}
+			if(glob.doForward) log(2, "all nodes established");
+			while(glob.spawned > 0) try { glob.wait(); } catch(InterruptedException e) {};		
+		}
+		log(2, "all nodes finished");
+	}
+}
+class Gui extends Debug implements Runnable {
+	class Parms extends JPanel {
+		static final long serialVersionUID = 43;
+		class Parm implements ActionListener {
+			JComboBox<Number> valueEntry;
+			JLabel valueLabel;
+			Parm(Number[] values, Number value, String label, boolean editable, boolean rowEnd) {			
+				log(5, "parm " + label);
+				valueLabel = new JLabel(label);
+				valueLabel.setBorder(b);
+				if(orientation == HORIZONTAL) gridC.gridwidth = 1;      
+				else gridC.gridwidth = GridBagConstraints.REMAINDER;
+				gridL.setConstraints(valueLabel, gridC); 
+				add(valueLabel);
+				valueEntry = new JComboBox<Number>(values);
+				valueEntry.setPreferredSize(new Dimension(prefComboWidth, prefComboHeight));
+				if(value != null) valueEntry.setSelectedItem(value);
+				valueEntry.setEditable(editable);
+				valueEntry.addActionListener(this);       
+				if(orientation == HORIZONTAL && rowEnd) gridC.gridwidth = GridBagConstraints.REMAINDER; 
+				gridL.setConstraints(valueEntry, gridC);
+				add(valueEntry);
+			}
+			public void actionPerformed(ActionEvent e) {
+				try { setVal((Number)((JComboBox)e.getSource()).getSelectedItem()); } catch(Exception x) { log(0, x.getMessage()); } 
+			}
+			void getEnv() {}
+			void setVal(Number v) {}
+		}
+		class Buttons extends Box {
+			class GoButton extends JButton implements ActionListener {
+			   static final long serialVersionUID = 44;
+			   GoButton() {
+			      super("go");
+			      addActionListener(this);
+			      gridC.gridwidth = 1;
+			      gridL.setConstraints(this, gridC);
+			      CS.go = true;
+			   }
+			   public void actionPerformed(ActionEvent e) {
+			      log(5, getText() + " button pressed");
+			      if(getText().equals("go")) { setText("pause"); CS.go = true; CS.pacing = true; CS.isRun = true; awake(); }
+			      else { setText("go"); CS.go = false; }
+			   }
+			}
+			class StepButton extends JButton implements ActionListener {
+			   static final long serialVersionUID = 44;
+			   StepButton() {
+			      super("step");
+			      addActionListener(this);
+			      gridC.gridwidth = 1;
+			      gridL.setConstraints(this, gridC);
+			   }
+			   public void actionPerformed(ActionEvent e) {
+			      log(5, getText() + " button pressed");
+			      CS.go = false; CS.pacing = true; CS.isRun = true;
+			      setGo(); 
+			      awake();
+			   }
+			}
+			class ResetButton extends JButton implements ActionListener {
+			   static final long serialVersionUID = 45;
+			   ResetButton() {
+			      super("reset");
+			      addActionListener(this);
+			      gridC.gridwidth = 1;
+			      gridL.setConstraints(this, gridC);
+			   }
+			   public void actionPerformed(ActionEvent e) { CS.isRun = false; CS.isReset = true; CS.go = true; awake(); }
+			}
+			class StopButton extends JButton implements ActionListener {
+			   static final long serialVersionUID = 46;
+			   StopButton() {
+			      super("end");
+			      addActionListener(this);
+			      gridC.gridwidth = 1;
+			      //gridC.gridwidth = GridBagConstraints.REMAINDER;
+			      gridL.setConstraints(this, gridC);
+			   }
+			   public void actionPerformed(ActionEvent e) { closeUI(); }
+			}
+			GoButton go;
+			ResetButton reset;
+			StepButton step;
+			StopButton stop;
+			Box row1, row2;
+			Buttons() {
+				super(BoxLayout.Y_AXIS);
+				setBorder(b);
+				add(row1 = new Box(BoxLayout.X_AXIS));
+				add(row2 = new Box(BoxLayout.X_AXIS));
+				row1.add(go = new GoButton());
+				row1.add(step = new StepButton());
+				row2.add(reset = new ResetButton());
+				row2.add(stop = new StopButton());
+			}
+			void setGo() { go.setText("go"); }	
+			void setPause() { go.setText("pause"); }			
+			void enableStep(boolean b) { step.setEnabled(b); }					
+		}
+		static final boolean EDITABLE = true;
+		static final boolean ROW_END = true;
+		boolean orientation;
+		EmptyBorder b = new EmptyBorder(0,7,0,7);
+	    GridBagLayout gridL = new GridBagLayout();
+	    GridBagConstraints gridC = new GridBagConstraints();
+		int prefComboWidth, prefComboHeight;
+		Buttons buttons;
+		Parms(boolean orientation) {
+			textHeight = (int)Math.round(1.5 * getFontMetrics(getFont()).getHeight());
+			prefComboWidth = (int)Math.round(1.5 * getFontMetrics(getFont()).bytesWidth("000000".getBytes(), 0, 6));
+			prefComboHeight = textHeight;
+			this.orientation = orientation; 
+			gridC.fill = GridBagConstraints.BOTH;
+			setFont(new Font("SansSerif", Font.PLAIN, 9));
+			setLayout(gridL);
+			new Parm(new Integer[] {0,1,2,3,4,5,7,9}, new Integer(CS.debug), "Debug level", !EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.debug = v.intValue(); } };
+			new Parm(new Integer[] {0,1,2}, new Integer(CS.issl), "SSL", !EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.issl = v.intValue(); } };
+			new Parm(new Integer[] {CS.pttl}, null, "TTL", EDITABLE, ROW_END) { 
+				void setVal(Number v) { CS.pttl = v.intValue(); } };
+			new Parm(new Double[] {(double)CS.ipace/1000}, null, "pace in secs.", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.ipace = (int)(1000.0 * v.doubleValue()); CS.pace = CS.ipace; } };
+			new Parm(new Integer[] {CS.mp0}, null, "listen port of first MASH node", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.mp0 = v.intValue(); } };
+			new Parm(new Integer[] {CS.rp0}, null, "listen port of first RING node", EDITABLE, ROW_END) { 
+				void setVal(Number v) { CS.rp0 = v.intValue(); } };
+			new Parm(new Integer[] {CS.mn}, null, "MASH constellation size", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.mn = v.intValue(); } };
+			new Parm(new Integer[] {CS.rn}, null, "RING constellation size", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.rn = v.intValue(); } };
+			new Parm(new Double[] {(double)CS.selTO/1000}, null, "I/O selection timeout in secs.", EDITABLE, ROW_END) { 
+				void setVal(Number v) { CS.selTO = 1000 * v.intValue(); } };
+			new Parm(new Integer[] {CS.fake}, null, "point of faked exception (integer)", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.fake = v.intValue(); } };
+			new Parm(new Integer[] {CS.rs}, null, "random seed (integer)", EDITABLE, !ROW_END) { 
+				void setVal(Number v) { CS.rs = v.intValue(); } };
+			//gridC.gridwidth = 0;
+			gridC.gridwidth = GridBagConstraints.REMAINDER;
+			add(buttons = new Buttons());
+		}
+	}
+	class CBoxBg extends BufferedImage {
+		final int nodeC[][];				// node centers
+		CBoxBg(int n) {
+			super(cBoxSize , cBoxSize, BufferedImage.TYPE_INT_RGB);
+			nodeC = new int[n][2];
+			log(5, "cnstlltn bg image beg, node centers array length=" + nodeC.length);
+			final double a0 = Math.PI / 2;
+			final double aN = 2 * Math.PI / n;
+			final int b = 3;				// border
+			final int r = 5;				// node diameter
+			int cx, cy;						// constellation center coordinates
+			cx = cy = cBoxSize/2;
+			int R = cBoxSize/2 - r - 2*b;	// distance of node centers from constellation center 
+			int dx, dy;						// deltas of node center coordinates 
+			final Graphics2D g2 = (Graphics2D)this.getGraphics();
+			g2.setBackground(Color.WHITE);
+			g2.clearRect(0, 0, cBoxSize, cBoxSize);
+			g2.setColor(Color.BLACK);
+			g2.draw3DRect(b, b, cBoxSize - 2*b, cBoxSize - 2*b, true);
+			if(n < 2) return;
+			for(int i=0; i<n; i++) {
+				dx = (int)Math.round(Math.cos(a0 + i * aN) * R);
+				dy = (int)Math.round(Math.sin(a0 + i * aN) * R);
+				nodeC[i][0] = dx; nodeC[i][1] = dy;
+				g2.drawOval(cx-dx-r, cy-dy-r, 2*r, 2*r);
+			}
+			log(5, "cnstlltn bg image end, node centers array length=" + nodeC.length);
+		}
+	}
+   	class Arrow extends Polygon {
+	   final double z, D, sin, cos, xd, yd, dx, dy;
+	   final int x0, y0, x3, y3;
+	   Arrow(int x1, int y1, int x2, int y2, int d) {
+	      super();
+	      z=d/(2*1.618034);
+	      D = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
+	      sin = (x2-x1)/D;
+	      cos = (y2-y1)/D;
+	      xd = x2-d*sin;
+	      yd = y2-d*cos;
+	      dx = z*cos;
+	      dy = z*sin;
+	      x0 = (int)Math.round(xd-dx);
+	      y0 = (int)Math.round(yd+dy);
+	      x3 = (int)Math.round(xd+dx);
+	      y3 = (int)Math.round(yd-dy);
+	      addPoint(x0, y0);
+	      addPoint(x2, y2);
+	      addPoint(x3, y3);
+	   }
+	}
+	class CBox extends Box {
+		static final long serialVersionUID = 45;
+		class CHead extends JPanel {
+			final JLabel field = new JLabel();
+			CHead() {
+				setPreferredSize(new Dimension(cBoxSize, textHeight - 4));
+				add(field);
+			}
+			public void paint(Graphics g) {
+				super.paint(g);
+				field.setText(label + ttl);
+			}
+		}	
+		class CPanel extends JPanel {
+			CPanel() { setPreferredSize(new Dimension(cBoxSize, cBoxSize));	}
+			public void paint(Graphics g) {
+				super.paint(g);
+				final Graphics2D g2 = (Graphics2D)g;
+			   	Polygon p;
+				g2.drawImage(bg, 0, 0, Color.WHITE, null);
+			   	final int n1 = currLink[0], n2 = currLink[1];
+			   	if(n1 > -1) {
+				   final int x1 = cx-bg.nodeC[n1][0], y1 = cy-bg.nodeC[n1][1];
+				   final int x2 = cx-bg.nodeC[n2][0], y2 = cy-bg.nodeC[n2][1];
+				   log(5, label + " paint, n1=" + n1 + ", n2=" + n2 + ", nodeC.length=" + bg.nodeC.length);
+				   g2.setColor(Color.CYAN);
+				   g2.setStroke(new BasicStroke(2));
+				   g2.drawLine(x1, y1, x2, y2);
+				   g2.setColor(Color.BLUE);
+				   g2.setStroke(new BasicStroke(0));
+				   g2.drawPolygon(p = new Arrow(x1, y1, x2, y2, 12));
+				   g2.fill(p);
+				}
+			}	
+		}
+		volatile int[] currLink = {-1,-1}; 
+		volatile int ttl;
+		CBoxBg bg;
+	   	final int cx = cBoxSize / 2, cy = cx;
+		final String label;
+		CBox(String label, CBoxBg bg) {
+			super(BoxLayout.Y_AXIS);
+			setMaximumSize(new Dimension(cBoxSize, cBoxSize + textHeight));
+			this.bg = bg;
+			this.label = label + ", ttl=";
+			ttl = CS.pttl;
+			add(new CHead());
+			add(new CPanel());
+		}
+		void reset(CBoxBg bg) { currLink[0] = -1; currLink[1] = -1; ttl = CS.pttl; this.bg = bg; }
+	}
+	class CcBox extends Box {
+		CBox ringBox, mashBox;
+		CcBox(String label) {
+			super(BoxLayout.X_AXIS);
+			add(mashBox = new CBox("MASH " + label + " SSL", mashBg));
+			add(ringBox = new CBox("RING " + label + " SSL", ringBg));
+		}
+	}
+	CS cs;
+	JFrame ui = new JFrame(debid);
+	Container dashboard;
+	static final boolean HORIZONTAL = true;
+	static final boolean VERTICAL = false;
+	boolean dashboardLayout = HORIZONTAL;
+	int textHeight;
+	Parms parms;
+	Box resultBox = null;  
+	CcBox sslBox = null, nonSslBox = null; 
+	CBoxBg ringBg = null, mashBg = null;
+	CBox nonSslMashBox, nonSslRingBox, sslMashBox, sslRingBox;
+	int cBoxSize;
+	WindowAdapter uiLstnr = new WindowAdapter() {
+		public void windowOpened(WindowEvent e) { log(5, "window opened"); }
+		public void windowClosing(WindowEvent e) { closeUI(); } 
+		public void windowClosed(WindowEvent e) { log(5, "window closed"); synchronized(CS.gui) { CS.gui.notify(); }
+		}
+	};
+	Gui(CS cs) {
+		super(cs);
+		this.cs = cs;
+		debid = cs.debid + " GUI";
+		log(5, "start parms panel");
+		ui.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+		ui.addWindowListener(uiLstnr);
+		ui.setLocation(600, 100);
+		dashboard = ui.getContentPane();
+		dashboard.setFont(new Font("SansSerif", Font.PLAIN, 9));
+		dashboard.setLayout(new BoxLayout(dashboard, dashboardLayout == HORIZONTAL ? BoxLayout.X_AXIS : BoxLayout.Y_AXIS));
+		ui.add(parms = new Parms(!dashboardLayout));
+		ui.pack(); 
+	}
+	public void run() { 
+		log(5, "repaint");
+		ui.setVisible(true); 
+		ui.repaint(); 
+	}
+	void cboxes() {
+		log(5, "create constellation panels");
+		cBoxSize = (dashboardLayout == VERTICAL ? dashboard.getSize().width : dashboard.getSize().height)/2 - textHeight;
+		ringBg = new CBoxBg(CS.rn);
+		mashBg = new CBoxBg(CS.mn);
+		if(resultBox != null) dashboard.remove(resultBox);
+		dashboard.add(resultBox = new Box(BoxLayout.Y_AXIS));
+		if(CS.issl > 0) {
+			resultBox.add(sslBox = new CcBox("w/")); 
+			sslMashBox = sslBox.mashBox; 
+			sslRingBox = sslBox.ringBox;
+		}
+		if(CS.issl != 1) {
+			resultBox.add(nonSslBox = new CcBox("non")); 
+			nonSslMashBox = nonSslBox.mashBox; 
+			nonSslRingBox = nonSslBox.ringBox; 
+		}
+		ui.pack();
+	}
+	void awake() { synchronized(CS.gui) { CS.gui.notify(); } }
+	void dashboardReset() {
+		//parms.buttons.setGo();
+		parms.buttons.enableStep(true);
+	}
+	void closeUI() { 
+		log(5, "closing window"); 
+		CS.isGui = false; CS.isRun = false; CS.go = true; awake();
+	}
+}
+public class CS extends Debug {
+	volatile static int 
+		connCnt = 0,
+		forwCnt = 0,
+		notFinished = 0,	
+		spawned = 0;
+	volatile static boolean abend = false;
+	static String text = "bla bla";
+	static String clsPath;
+	static String cePath;
+	static final String sslPathSuffP = "../CS";
+	static Random r;
+	static int 
+		mn = 0, 
+		mp0 = 11000, 
+		rn = 0, 
+		rp0 = 12000, 
+		pttl = 3, 
+		issl = 0, 
+		connThreshold = 77,	// connection retries threshold
+		connTO = 99,		// connection sleep time in msecs
+		selTO = 999,		// selection timeout in msecs
+		ipace = 0,
+		pace = 0,
+		rs = 0,
+		fake = 0;
+	static boolean isGui = false, isRun = true, isReset = false, go = true, pacing = false;
+	static ArrayList<Constellation> constls;
+	public static Constellation nonSslMashCon, nonSslRingCon, sslMashCon, sslRingCon;
+	static Gui gui;
+	static Gui.CBox nonSslMashBox, nonSslRingBox, sslMashBox, sslRingBox;
+	CS() {
+		debid = "client server DEMO";
+		getArgs();
+	}
+	boolean isArg(String a) { return System.getenv(a) != null; }
+	int getArgI(String a) throws Exception {
+		int i = -1;
+		if(System.getenv(a) != null)
+			if(!System.getenv(a).equals(""))
+				try { i = Integer.valueOf(System.getenv(a));
+				} catch(NumberFormatException x) { throw new Exception(a + "=\terror in number format"); }
+		return i;
+	}
+	double getArgF(String a) throws Exception {
+		double d = -1;
+		if(System.getenv(a) != null)
+			if(!System.getenv(a).equals(""))
+				try { d = Double.valueOf(System.getenv(a));
+				} catch(NumberFormatException x) { throw new Exception(a + "=\terror in number format"); }
+		return d;
+	}
+	int getMArg(String a) throws Exception {
+		int i;
+		if((i = getArgI(a)) == 0) throw new Exception(a + " is mandatory");
+		return i;
+	}
+	void getArgs() {
+		try {
+			clsPath = getClass().getProtectionDomain().getCodeSource().getLocation().getPath();
+			if(getArgI("DEB") >= 0) debug = getArgI("DEB");
+			if(System.getenv("T") != null) text = System.getenv("T");
+			if(getArgI("TTL") > 0) pttl = getArgI("TTL");
+			if(getArgF("P") >= 0) ipace = (int)(getArgF("P") * 1000);
+			if(getArgI("MP0") > 0) mp0 = getArgI("MP0");
+			if(getArgI("RP0") > 0) rp0 = getArgI("RP0");
+			if(getArgI("N") >= 0) { mn = getArgI("N"); rn = mn; }
+			if(getArgI("SSL") >= 0) issl = getArgI("SSL");
+			if(System.getenv("CEP") != null) cePath = System.getenv("CEP");
+			else cePath = clsPath + sslPathSuffP;
+			if(getArgI("MN") >= 0) mn = getArgI("MN");
+			if(getArgI("RN") >= 0) rn = getArgI("RN");
+			if(getArgF("STO") >= 0) selTO = (int)(getArgF("STO") * 1000);
+			if(getArgI("FAKE") >= 0) fake = getArgI("FAKE");
+			if(isArg("RS")) rs = getArgI("RS");
+			if(isArg("G")) isGui = getArgI("G") == 1;
+		} catch(Exception x) { log(0, x.getMessage()); return; }
+	}
+	void dashboard() {
+		gui = new Gui(this);
+		log(4, "wait for args from GUI"); 
+		synchronized(gui) {  
+			try { SwingUtilities.invokeAndWait(gui); } catch(Exception x) { throw new Error(x); }
+			try { gui.wait(); } catch(InterruptedException x) {} 
+		}
+		if(isGui) cboxes();	
+	}
+	void cboxes() {
+		log(4, "cboxes");
+		try { gui.cboxes(); } catch(Exception x) { log(0, x.getMessage()); }
+		try { SwingUtilities.invokeAndWait(gui); } catch(Exception x) { throw new Error(x); }
+		nonSslMashBox = gui.nonSslMashBox; 
+		nonSslRingBox = gui.nonSslRingBox; 
+		sslMashBox = gui.sslMashBox; 
+		sslRingBox = gui.sslRingBox;
+		
+	}
+	void constellations() {
+		pace = ipace;
+		spawned = 0; notFinished = 0; 
+		constls = new ArrayList<Constellation>();
+		if(mn == 1) log(0, "one-node MASH configuration not implemented");
+		if(mn > 1) {
+			if(issl > 0) { sslMashCon = new Constellation(this, sslMashBox, true, true); constls.add(sslMashCon); }
+			if(issl != 1) { nonSslMashCon = new Constellation(this, nonSslMashBox, true, false); constls.add(nonSslMashCon); }
+		}
+		if(rn == 1) log(0, "one-node RING configuration not implemented");
+		if(rn > 1) {
+			if(issl > 0) { sslRingCon = new Constellation(this, sslRingBox, false, true); constls.add(sslRingCon); }
+			if(issl != 1) { nonSslRingCon = new Constellation(this, nonSslRingBox, false, false); constls.add(nonSslRingCon); }
+		}
+	}
+	void stop() {
+		if(sslMashCon != null) sslMashCon.stop();
+		if(sslRingCon != null) sslRingCon.stop();
+		if(nonSslMashCon != null) nonSslMashCon.stop();
+		if(nonSslRingCon != null) nonSslRingCon.stop();
+		pacing = false;
+		pace = 0;
+		go = true;
+	}	
+	void reset() {
+		gui.dashboardReset();
+		cboxes();
+		mp0 += mn; rp0 += rn;		// port is unusable 30 secs after port close due to special timeout
+		constellations();
+		isReset = false;
+	}
+	String switches(String label) {
+		return label + ": isGui=" + isGui + ", isRun=" + isRun + ", go=" + go + ", pacing=" + pacing + ", spawned=" + spawned;
+	}
+	void pacingGo() { for(Constellation con : constls) con.glob.pacingGo = true; }
+	void runGuiCon() {
+	   	synchronized(constls) {
+			while(isGui && isRun && spawned > 0) {
+		   		log(4, switches("runCons constls.wait"));
+		   		try { constls.wait(); } catch(InterruptedException x) {}
+		   		pacingGo();
+		   		log(4, switches("runCons constls.paint"));
+				if(!isGui) break; 
+				try { SwingUtilities.invokeAndWait(CS.gui); } catch(Exception x) { throw new Error(x); }
+				if(!isGui || !isRun) break; 
+				if(!go) {
+		   			log(4, switches("runCons constls.gui.wait"));
+					synchronized(gui) { try { gui.wait(); } catch(InterruptedException x) {} }
+				}
+				if(!isGui || !isRun) break; 
+	   			log(4, switches("runCons constls.notifyAll"));
+				constls.notifyAll();
+			}	
+		}
+		if(isGui && isRun) {
+	   		log(4, switches("runCons last repaint"));
+			try { SwingUtilities.invokeAndWait(CS.gui); } catch(Exception x) { throw new Error(x); }
+			if(!go) synchronized(gui) { try { gui.wait(); } catch(InterruptedException x) {} } 
+		}
+		else {
+	   		log(4, switches("runCons stop"));
+			stop(); 
+			synchronized(constls) { constls.notifyAll(); }
+			while(spawned > 0) synchronized(this) { try { wait(); } catch(InterruptedException x) {} }
+		}	
+	}
+	void runCons() {
+		if(isGui) pacing = true;
+		else pacing = false;
+		for(Constellation con : constls) { notFinished++; new Thread(con, con.label).start(); spawned++; }
+		if(isGui) runGuiCon();	
+		else while(spawned > 0) synchronized(this) { try { wait(); } catch(InterruptedException x) {} }		
+	}
+	public void run() {
+		if(isGui) dashboard();
+		if(isRun) {
+			log(1, "pgm=" + clsPath + getClass().getName() +
+					", ttl=" + pttl + ", pace=" + ipace + "msecs, seed=" + rs + ", SSL=" + issl + ", fake=" + fake + ", debug=" + debug);
+			if(issl > 0) log(3, "cePath=" + cePath);
+			constellations();
+			if(!isGui) { r = new Random(rs); runCons(); } 
+			else while(isGui) {
+				r = new Random(rs);
+				runCons();
+				log(1, "all constellations finished");
+				if(!isGui) break;
+				gui.parms.buttons.setGo();
+				if(!go) gui.parms.buttons.step.setEnabled(false);
+				if(abend) { 
+					gui.parms.buttons.go.setEnabled(false); 
+					gui.parms.buttons.step.setEnabled(false); 
+					gui.parms.buttons.reset.setEnabled(false); 
+				}
+				try { SwingUtilities.invokeAndWait(gui); } catch(Exception x) { throw new Error(x); }
+				if(!isReset) synchronized(gui) { try { gui.wait(); } catch(InterruptedException x) {} }
+				if(isGui) {
+					reset();
+					do synchronized(gui) {
+						try { SwingUtilities.invokeAndWait(gui); } catch(Exception x) { throw new Error(x); }					 
+						if(!isRun) try { gui.wait(); } catch(InterruptedException x) {}
+						if(isGui && isReset) reset();
+					} while(isGui && !isRun);
+				}	
+			}
+		}
+		log(1, "final balance, connections=" + connCnt + ", forwards=" + forwCnt);
+		if(gui != null) gui.ui.dispose();
+		log(2, "run end");
+	}
+	public static void main(String[] args) throws Exception {
+		CS cs = new CS();
+		cs.run();
+		cs.log(1, "cs end");
+		//try { cs.run(); } catch(Exception x) { cs.log(0, "interrupted execution"); };
+	}
+}
+// rozeznání konce ve step-módu
+// pacing slide
+// input fields
+// ukládání parametrů
+// too many open files
+// exceptions
+// stavové zprávy na dashboardu
\ No newline at end of file