view src/main/java/jp/ac/u_ryukyu/treevnc/MyRfbProto.java @ 205:614db20d8fab

try to separate Network management
author Shinji KONO <kono@ie.u-ryukyu.ac.jp>
date Wed, 02 Jul 2014 16:31:25 +0900
parents d9d234715853
children 2b3eb4a9492f
line wrap: on
line source

package jp.ac.u_ryukyu.treevnc;

import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import jp.ac.u_ryukyu.treevnc.server.RequestScreenThread;
import jp.ac.u_ryukyu.treevnc.server.TreeManagement;
import jp.ac.u_ryukyu.treevnc.server.TreeRootFinderListener;

import com.glavsoft.exceptions.TransportException;
import com.glavsoft.rfb.client.ClientToServerMessage;
import com.glavsoft.rfb.encoding.EncodingType;
import com.glavsoft.rfb.protocol.Protocol;
import com.glavsoft.rfb.protocol.ProtocolContext;
import com.glavsoft.transport.Reader;
import com.glavsoft.transport.Writer;
import com.glavsoft.viewer.ViewerInterface;
import com.glavsoft.viewer.swing.ConnectionParams;


public abstract class MyRfbProto {
	final static int FramebufferUpdateRequest = 3;
	final static int CheckDelay = 11;
	protected final static int FramebufferUpdate = 0;
	protected ProtocolContext context;
	private int clients = 0;
	public MulticastQueue<LinkedList<ByteBuffer>> multicastqueue = new MulticastQueue<LinkedList<ByteBuffer>>();
	private RequestScreenThread rThread;
	public int acceptPort = 0;
    private String myAddress;
	protected boolean readyReconnect = false;
	private boolean cuiVersion;
	private long counter = 0; // packet serial number
    public ServerSocket servSock;
    private boolean permitChangeScreen = true;
    private static final int INFLATE_BUFSIZE = 1024 * 100;

    private Inflater inflater = new Inflater();
    private Deflater deflater = new Deflater();
    ViewerInterface viewer;
	private short id;  // my tree node id ( = 0 in root )
    private boolean leader;
    private TreeVncCommandChannelListener acceptThread;
    private boolean firstTime = true;
    private TreeRootFinderListener getCast;
    private CreateConnectionParam cp;
    private boolean hasViewer = false;
    private boolean reconnecting;
    private short reconnectingId;  // Change Server Request to id's node VNC server
    private TreeVNCNetwork nets = new TreeVNCNetwork();

	public MyRfbProto() {
		rThread = new RequestScreenThread(this);
		nets.setMyRfb(this);
	}
	
	abstract public boolean isTreeManager() ;
	
	public ProtocolContext getContext() {
		return context;
	}
	
	/**
	 * handle new client accept 
	 *     it also handle TreeVNC Command
	 * @param acceptThread
	 * @param newCli
	 * @param os
	 * @param is
	 * @throws IOException
	 * @throws TransportException
	 */
	public void newClient(final Socket newCli,final Writer os, final Reader is) { 

		final int myId = clients;
		final MulticastQueue.Client<LinkedList<ByteBuffer>> c = multicastqueue.newClient();
		final AtomicInteger writerRunning = new AtomicInteger();
		writerRunning.set(1);
		/**
		 * Timeout thread. If a client is suspended, it has top of queue
		 * indefinitely, which caused memory overflow. After the timeout, we
		 * poll the queue and discard it. Start long wait if writer is running.
		 */
		final Runnable timer = new Runnable() {
			public void run() {
				int count = 0;
				for (;;) {
					long timeout = 50000 / 8;
					try {
						synchronized (this) {
							int state, flag;
							writerRunning.set(0);
							wait(timeout);
							flag = 0;
							while ((state = writerRunning.get()) == 0) {
								c.poll(); // discard, should be timeout
								count++;
								if (flag == 0) {
									System.out.println("Discarding " + myId
											+ " count=" + count);
									flag = 1;
								}
								wait(10); // if this is too short, writer cannot
											// take the poll, if this is too
											// long, memory will overflow...
							}
							if (flag == 1)
								System.out.println("Resuming " + myId
										+ " count=" + count);
							if (state != 1) {
								System.out.println("Client died " + myId);
								break;
							}
						}
					} catch (InterruptedException e) {
					}
				}
			}
		};
		new Thread(timer, "timer-discard-multicastqueue").start();
		/**
		 * handle command from lower node
		 */
		final Runnable reader = new Runnable() {

            public void run() {
				for (;;) {
					try {
		                final byte b[] = new byte[4096];
						final int c = is.readByte(b);
						if (c <= 0)
							throw new IOException();
						if (isTreeManager()) {
							if (b[0] == ClientToServerMessage.SERVER_CHANGE_REQUEST) {
								if (permitChangeScreen()) {
									ByteBuffer buf = ByteBuffer.wrap(b);
									buf.order(ByteOrder.BIG_ENDIAN);
								    short id = buf.getShort(2);
									int length = buf.getInt(4);
									if (length == 0) 
										continue;
				                	String newHostName = new String(b, 8, length);
				                	System.out.println("Root server change request :" + newHostName);
									// please remove these numbers.
				                	if (viewer != null) {
					                    changeVNCServer(viewer, newHostName, 3200, 1980, id);				                		
				                	}
				                } else {
				                    continue;
				                }
							}
						} else if (b[0] == ClientToServerMessage.SERVER_CHANGE_REQUEST) {
						    ClientToServerMessage sc = new ClientToServerMessage() {
                                @Override
                                public void send(Writer writer)
                                        throws TransportException {
                                    writer.write(b,0,c);
                                }
						    };
							context.sendMessage(sc);
						}
						// System.out.println("client read "+c);
					} catch (Exception e) {
						try {
							writerRunning.set(2);
							os.close();
							is.close();
							break;
						} catch (IOException e1) {
						} catch (TransportException e1) {
							e1.printStackTrace();
						}
						return;
					}
				}
			}

		};
		/**
		 * send packets to a client (one thread for each client )
		 */
		Runnable sender = new Runnable() {
			public void run() {
				writerRunning.set(1);
				try {
					requestThreadNotify();

					// after this, we discard upward packet.
					new Thread(reader, "upward-packet-processing").start(); 

					for (;;) {
						LinkedList<ByteBuffer> bufs = c.poll();
						int inputIndex = 0;
						ByteBuffer header = bufs.get(inputIndex);
						if (header == null)
							continue;
						else if (header.get(0) == CheckDelay) {
							writeToClient(os, bufs, inputIndex);
							continue;
						} else if (header.get(0) == FramebufferUpdate) {
							 //System.out.println("client "+ myId);
						}
						/*
						 * if(i%20==0){ sendDataCheckDelay(); } i++;
						 */
						writeToClient(os, bufs, inputIndex);
						writerRunning.set(1); // yes my client is awaking.
					}
				} catch (Exception e) {
					try {
						writerRunning.set(2);
						os.close();
					} catch (IOException e1) {
						System.out.println("root writer close faild :" + e1);
					}
					System.out.println("root writer faild :" + e);
					/* if socket closed cliList.remove(newCli); */
				}
			}

			public void writeToClient(final Writer os,
					LinkedList<ByteBuffer> bufs, int inputIndex)
					throws TransportException {
				while (inputIndex < bufs.size()) {
					ByteBuffer b = bufs.get(inputIndex++);
					os.write(b.array(), b.position(), b.limit());
				}
				os.flush();
				bufs = null;
				multicastqueue.heapAvailable();
			}
		};
		clients++;
		new Thread(sender, "writer-to-lower-node").start();

	}

	

    public boolean permitChangeScreen() {
        return permitChangeScreen;
    }
    

    public void setPermitChangeScreen(boolean v) {
         permitChangeScreen = v;
    }
    
	public void requestThreadNotify() {
		rThread.reStart();
	}
	
	
    public void setProtocolContext(Protocol workingProtocol) {
        context = workingProtocol;
    }

    public Socket accept() throws IOException {
        return servSock.accept();
    }

    public int selectPort(int p) {
        int port = p;
        while (true) {
            try {
                servSock = new ServerSocket(port);
                acceptPort = port;
                myAddress = "127.0.0.1";
                getNetworkInterfaces();
                break;
            } catch (BindException e) {
                port++;
                continue;
            } catch (IOException e) {

            }
        }
        System.out.println("accept port = " + port);
        return port;
    }



	public void writeFramebufferUpdateRequest(int x, int y, int w, int h,
			boolean incremental) throws TransportException {
		byte[] b = new byte[10];

		b[0] = (byte) FramebufferUpdateRequest; // 3 is FrameBufferUpdateRequest
		b[1] = (byte) (incremental ? 1 : 0);
		b[2] = (byte) ((x >> 8) & 0xff);
		b[3] = (byte) (x & 0xff);
		b[4] = (byte) ((y >> 8) & 0xff);
		b[5] = (byte) (y & 0xff);
		b[6] = (byte) ((w >> 8) & 0xff);
		b[7] = (byte) (w & 0xff);
		b[8] = (byte) ((h >> 8) & 0xff);
		b[9] = (byte) (h & 0xff);
	}
	
	public void setViewer(ViewerInterface v) {
		viewer = v;
	}
	
	public ViewerInterface getViewer() {
		return viewer;
	}

	public void setTerminationType(boolean setType) {
		/*nop*/
	}

	public boolean getTerminationType() {
		/*nop*/
		return true;
	}

    public void close() {
	    // none
	}
	
	public int getAcceptPort() {
		return acceptPort;
	}
	
	public boolean getReadyReconnect() {
		return readyReconnect;
	}


	public boolean getCuiVersion() {
		return cuiVersion;
	} 
	
	public void  setCuiVersion(boolean flag) {
		cuiVersion = flag;
	}

	public void readCheckDelay(Reader reader) throws TransportException {
		
	}

	public synchronized void vncConnected(boolean ready) {
	    if (ready && reconnecting) {
	    	Socket vncSocket = viewer.getVNCSocket();
	    	NetworkInterface ni = getInterface(vncSocket);
	    	if (ni!=null) {
	    		vncInterface = ni.getName();
	    		System.out.println("VNCNetworkInterface :" + vncInterface);
	    	}
	        sendDesktopSizeChange(reconnectingId);
	        reconnecting = false;
	        if (reconnectingId == 0) {
	        	viewer.setVisible(false);
	        }
	    } 
	    if (reconnectingId!=0) {
	        readyReconnect = ready;
	        if (ready) {
	            notifyAll();
	        }
	    }
	}	


	public synchronized void waitForVNCConnection() throws InterruptedException {
	    if (reconnectingId!=0) {
	        while (!readyReconnect) {
	            wait();
	        }
	    }
	}


	public void sendDesktopSizeChange(short id) {
		LinkedList<ByteBuffer> desktopSize = new LinkedList<ByteBuffer>();
		int width = context.getFbWidth();
		int height = context.getFbHeight();
		desktopSize.add(new UpdateRectangleMessage(width, height, EncodingType.INIT_DATA, context.getInitData(),id).getMessage());
		addSerialNumber(desktopSize);
		multicastqueue.put(desktopSize);
	}


	public void addSerialNumber(LinkedList<ByteBuffer> bufs) {
		ByteBuffer serialNum = multicastqueue.allocate(8);
		serialNum.putLong(counter++);
		serialNum.flip();
		bufs.addFirst(serialNum);
	}


    public void resetDecoder() {
        context.resetDecoder();
    }

    public void stopReceiverTask() {
        if (context!=null)
            context.cleanUpSession(null);
        // cleanup zlib decoder for new VNCServer
        if (isTreeManager())
            inflater = new Inflater();
    }

    public String getMyAddress() {
        return myAddress;
    }

    /**
     * gzip byte arrays
     * 
     * @param deflater
     * @param inputs
     *            byte data[]
     * @param inputIndex
     * @param outputs
     *            byte data[]
     * @return byte length in last byte array
     * @throws IOException
     */
    public int zip(Deflater deflater, LinkedList<ByteBuffer> inputs,
            int inputIndex, LinkedList<ByteBuffer> outputs) throws IOException {
        int len = 0;
        ByteBuffer c1 = multicastqueue.allocate(INFLATE_BUFSIZE);
        while (inputIndex < inputs.size()) {
            ByteBuffer b1 = inputs.get(inputIndex++);
            deflater.setInput(b1.array(), b1.position(), b1.remaining());
            /**
             * If we finish() stream and reset() it, Deflater start new gzip
             * stream, this makes continuous zlib reader unhappy. if we remove
             * finish(), Deflater.deflate() never flushes its output. The
             * original zlib deflate has flush flag. I'm pretty sure this a kind
             * of bug of Java library.
             */
            if (inputIndex == inputs.size())
                deflater.finish();
            int len1 = 0;
            do {
                len1 = deflater.deflate(c1.array(), c1.position(),
                        c1.remaining());
                if (len1 > 0) {
                    len += len1;
                    c1.position(c1.position() + len1);
                    if (c1.remaining() == 0) {
                        c1.flip();
                        outputs.addLast(c1);
                        c1 = multicastqueue.allocate(INFLATE_BUFSIZE);
                    }
                }
            } while (len1 > 0 || !deflater.needsInput()); // &&!deflater.finished());
        }
        if (c1.position() != 0) {
            c1.flip();
            outputs.addLast(c1);
        }
        deflater.reset();
        return len;
    }

    /**
     * gunzip byte arrays
     * 
     * @param inflater
     * @param inputs
     *            byte data[]
     * @param bytes
     *            byte data[]
     * @return number of total bytes
     * @throws IOException
     */
    public int unzip(Inflater inflater, LinkedList<ByteBuffer> inputs,
            int inputIndex, byte[] bytes, int bufSize)
            throws DataFormatException {
        int position = 0;
        int limit = bytes.length;
        while (inputIndex < inputs.size()) {
            ByteBuffer input = inputs.get(inputIndex++);
            inflater.setInput(input.array(), input.position(), input.limit());
            // if (inputIndex==inputs.size()) if inflater/deflater has symmetry,
            // we need this
            // inflater.end(); but this won't work
            do {
                int len0 = inflater.inflate(bytes, position,
                        limit-position);
                if (len0 > 0) {
                	position += len0;
                    if (position > limit) {
                    	throw new DataFormatException();
                    }
                }
            } while (!inflater.needsInput());
        }
        return position;
    }

    /**
     * read FrameBuffferUpdate. If it is ZLE, make it ZLEE which is self contained compressed packet.
     * put the packet to the multicastqueue. Then normal rendering engine read the same stream using is.reset().
     * @param dataLen
     * @param reader
     * @throws TransportException
     */
    public void readSendData(int dataLen, Reader reader, byte[] bytes)
            throws TransportException {
        LinkedList<ByteBuffer> bufs = new LinkedList<ByteBuffer>();
        ByteBuffer header = multicastqueue.allocate(16);
        ByteBuffer serial = multicastqueue.allocate(8);
        if (!isTreeManager()) {
            reader.readBytes(serial.array(),0,8);
            serial.limit(8);
        }
        reader.mark(dataLen);
        reader.readBytes(header.array(), 0, 16);
        header.limit(16);
        if (header.get(0) == FramebufferUpdate) {
            int encoding = header.getInt(12);
            if (encoding == EncodingType.ZRLE.getId()
                    || encoding == EncodingType.ZLIB.getId()) { 
                // recompress into ZREE
                // uncompressed result is remain in bytes
                ByteBuffer len = multicastqueue.allocate(4);
                reader.readBytes(len.array(), 0, 4);
                len.limit(4);
                ByteBuffer inputData = multicastqueue.allocate(dataLen - 20);
                reader.readBytes(inputData.array(), 0, inputData.capacity());
                inputData.limit(dataLen - 20);
                LinkedList<ByteBuffer> inputs = new LinkedList<ByteBuffer>();
                inputs.add(inputData);

                header.putInt(12, EncodingType.ZRLEE.getId()); // means
                                                                // recompress
                                                                // every time
                // using new Deflecter every time is incompatible with the
                // protocol, clients have to be modified.
                Deflater nDeflater = deflater; // new Deflater();
                LinkedList<ByteBuffer> out = new LinkedList<ByteBuffer>();
                try {
                    unzip(inflater, inputs, 0, bytes, INFLATE_BUFSIZE);
                    // dump32(inputs);
                    out.add(ByteBuffer.wrap(bytes));
                    int len2 = zip(nDeflater, out, 0, bufs);
                    ByteBuffer blen = multicastqueue.allocate(4);
                    blen.putInt(len2);
                    blen.flip();
                    bufs.addFirst(blen);
                    bufs.addFirst(header);
                    addSerialNumber(bufs);
                    multicastqueue.put(bufs);
                } catch (DataFormatException e) {
                    throw new TransportException(e);
                } catch (IOException e) {
                    throw new TransportException(e);
                }
                return;
            }
            //    ZRLEE is already compressed
            bufs.add(header);
            if (dataLen > 16) {
                ByteBuffer b = multicastqueue.allocate(dataLen - 16);
                reader.readBytes(b.array(), 0, dataLen - 16);
                b.limit(dataLen - 16);
                bufs.add(b);
            }
            this.addSerialNumber(bufs);
            multicastqueue.put(bufs);
            return;
        }
        // It may be compressed. We can inflate here to avoid repeating clients
        // decompressing here,
        // but it may generate too many large data. It is better to do it in
        // each client.
        // But we have do inflation for all input data, so we have to do it
        // here.
    }

	public abstract void addHostToSelectionPanel(int port, String hostname,String myHostName) ;

	public void createRootSelectionPanel(CreateConnectionParam createConnectionParam) {
		
	}

	public void setId(short id) {
		this.id = id;
	}

	public short getId() {
		return id;
	}

    public void setMyAddress(String myHostName) {
        this.myAddress = myHostName;
        
    }

    public void setLeader(boolean leader) {
        this.leader = leader;
    }

    public boolean isLeader() {
        return leader;
    }

    public void setTreeManager(String intf, TreeManagement clients) {
    	interfaces.put(intf, clients);
    }
    
    public TreeManagement getTreeManager(String intf) {
        return interfaces.get(intf);
    }

    /**
     * chnageVNCServer is called when host change.
     * 
     * @param vncProxyService 
     * @param hostName
     *            HostAddress
     * @param width
     *            FrameWidth
     * @param height
     *            FrameHeight
     * @param id 
     * @throws InterruptedException 
     */
    public void changeVNCServer(ViewerInterface vncProxyService, String hostName, int width, int height, short id)
    		throws UnknownHostException, IOException, InterruptedException {
    	// stop reader stop
        stopReceiverTask();
        reconnectingId = id;
    	vncProxyService.inhelitClients(vncProxyService, hostName);
        // after connecting VNC server, rfb send SEND_INIT_DATA command and wakes me up if necessary 
    	reconnecting = true;
    	if (reconnectingId!=0) {
    	    waitForVNCConnection();
    	}
    }

    /**
     * start accepting children
     * run rootFinderListener if necessary
     */
    public void createConnectionAndStart(ViewerInterface v) {
    	selectPort(ConnectionParams.DEFAULT_VNC_ROOT);
        startTreeVncCommandListener();
    	if(isTreeManager() && firstTime) {
    		getCast = new TreeRootFinderListener(v);
    		Thread thread = new Thread(getCast, "tree-root-find-listener");
    		thread.start();
    		firstTime = false;
    	}
    }

	public void startTreeVncCommandListener() {
        acceptThread = new TreeVncCommandChannelListener(this, getAcceptPort());
    	Thread thread = new Thread(acceptThread, "TreeVNC-accept");
    	thread.start();
    }

    public TreeVncCommandChannelListener getAcceptThread() {
        return acceptThread;
    }

    public void setConnectionParam(CreateConnectionParam createConnectionParam) {
        cp =  createConnectionParam;
    }

    public CreateConnectionParam getConnectionParam() {
        return cp;
    }

    public boolean hasViewer() {
        return hasViewer;
    }

    public void setHasViewer(boolean b) {
        hasViewer = b;
    }

    public void setReconnecting(boolean b) {
        reconnecting = b;
    }

	public int getReconnectingId() {
		return reconnectingId;
	}

	
}