diff -u -Pr tightvnc-1.2.9/ButtonPanel.java vncplay-1.0/ButtonPanel.java --- tightvnc-1.2.9/ButtonPanel.java 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/ButtonPanel.java 2005-04-10 21:52:01.000000000 -0700 @@ -54,7 +54,8 @@ clipboardButton.addActionListener(this); if (viewer.rec != null) { recordButton = new Button("Record"); - add(recordButton); + // Don't add this record button to avoid confusion + //add(recordButton); recordButton.addActionListener(this); } ctrlAltDelButton = new Button("Send Ctrl-Alt-Del"); @@ -65,6 +66,14 @@ refreshButton.setEnabled(false); add(refreshButton); refreshButton.addActionListener(this); + + disconnectButton.setFocusable(false); + optionsButton.setFocusable(false); + if (recordButton != null) + recordButton.setFocusable(false); + clipboardButton.setFocusable(false); + ctrlAltDelButton.setFocusable(false); + refreshButton.setFocusable(false); } // diff -u -Pr tightvnc-1.2.9/DisplaySyncPoint.java vncplay-1.0/DisplaySyncPoint.java --- tightvnc-1.2.9/DisplaySyncPoint.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/DisplaySyncPoint.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,75 @@ +/* This program takes on stdin a single line of input, + * in particular the "sync" line from record.log, and + * displays the image that the sync point represents. + */ +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.io.*; + +public class DisplaySyncPoint extends Canvas { + private BufferedImage _im; + private int _width, _height; + + public static void main(String[] args) { + new DisplaySyncPoint().go(); + } + + private int getInt(Map m, String key) { + return Integer.parseInt(m.get(key)); + } + + public void go() { + Map m = getInput(); + + int x0 = getInt(m, "x0"); + int y0 = getInt(m, "y0"); + int x1 = getInt(m, "x1"); + int y1 = getInt(m, "y1"); + + _width = x1 - x0 + 1; + _height = y1 - y0 + 1; + + _im = new BufferedImage(_width, _height, BufferedImage.TYPE_INT_ARGB); + for (int x = x0; x <= x1; x++) + for (int y = y0; y <= y1; y++) + _im.setRGB(x-x0, y-y0, getInt(m, "px" + x + "y" + y)); + + System.out.println("Size: " + _width + "x" + _height); + + Frame frame = new Frame("display sync point"); + frame.add("Center", this); + frame.setSize(_width + 50, _height + 50); + frame.show(); + } + + public void paint(Graphics g) { + g.drawImage(_im, 10, 10, null); + } + + private Map getInput() { + Map m = new HashMap(); + + String in; + try { + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + in = br.readLine(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + StringTokenizer st = new StringTokenizer(in); + String delay = st.nextToken(); + + while (st.hasMoreTokens()) { + String t = st.nextToken(); + + StringTokenizer st2 = new StringTokenizer(t, "="); + String key = st2.nextToken(); + String value = st2.nextToken(); + m.put(key, value); + } + + return m; + } +} diff -u -Pr tightvnc-1.2.9/FrameSequence.java vncplay-1.0/FrameSequence.java --- tightvnc-1.2.9/FrameSequence.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/FrameSequence.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,116 @@ +import java.util.*; + +public class FrameSequence implements Iterable { + public static final int MAX_COW_COUNT = 100; + + private List _updates; + private int _width, _height; + private int _cowCount; + + public FrameSequence() { + _updates = new ArrayList(); + _cowCount = 0; + } + + public void setSize(int w, int h) { + if (_width != 0 || _height != 0) { + if (_width != w || _height != h) { + System.out.println("WARNING: Trying to reset width/height to different values!"); + System.out.println(" old: " + _width + "x" + _height); + System.out.println(" new: " + w + "x" + h); + } + } + + _width = w; + _height = h; + } + + public void add(FrameUpdate u) { + if (_updates.size() > 0) + u.setPrev(_updates.get(_updates.size() - 1)); + + if (_cowCount > MAX_COW_COUNT) { + //System.out.println("doing evalFully for frame update at " + u.ts()); + u = u.evalFully(); + _cowCount = 0; + } + + u.setSeq(this); + _updates.add(u); + _cowCount++; + } + + public int size() { + return _updates.size(); + } + + public int totalPixels() { + return _width * _height; + } + + public int width() { return _width; } + public int height() { return _height; } + + private int getMatchPixels(FrameUpdate u1, FrameUpdate u2, + FrameUpdate prev, int prevPixels) + { + if (prev == null) + return u1.getMatchPixels(u2); + + int matches = prevPixels; + + for (int x = 0; x < _width; x++) { + for (int y = 0; y < _height; y++) { + if (u2.contains(x, y)) { + int u1p = u1.get(x, y); + int u2p = u2.get(x, y); + int prevp = prev.get(x, y); + + if (u1p != u2p && u1p == prevp) + matches--; + if (u1p == u2p && u1p != prevp) + matches++; + } + } + } + + return matches; + } + + public FrameUpdate findClosestMatch(FrameUpdate u, long tsLo, long tsCenter, long tsHi) { + FrameUpdate bestMatch = null; + int bestMatchPixels = -1; + + FrameUpdate prev = null; + int prevPixels = 0; + + for (FrameUpdate i: _updates) { + if (i.ts() < tsLo) + continue; + if (i.ts() > tsHi) + continue; + + int m = getMatchPixels(u, i, prev, prevPixels); + + prev = i; + prevPixels = m; + + //System.out.println("comparing with frame at " + i.ts() + ": " + m + " pixel matches"); + if (i.ts() > tsCenter && m > bestMatchPixels) { + bestMatch = i; + bestMatchPixels = m; + } + + if (i.ts() < tsCenter && m >= bestMatchPixels) { + bestMatch = i; + bestMatchPixels = m; + } + } + + return bestMatch; + } + + public Iterator iterator() { + return _updates.iterator(); + } +} diff -u -Pr tightvnc-1.2.9/FrameSequencer.java vncplay-1.0/FrameSequencer.java --- tightvnc-1.2.9/FrameSequencer.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/FrameSequencer.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,138 @@ +import java.io.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; + +public class FrameSequencer implements VncEventListener { + public static final int SCALEDOWN = 4; + + private FrameSequence _seq; + private File _f; + + private VncViewer _viewer; + private VncCanvas _vc; + private long _curTs; + private int _bytesSent = 0; + + private long _lastTsPrinted = 0; + + public FrameSequencer(File f) { + _f = f; + } + + public FrameSequence getFrameSequence() { + return _seq; + } + + public void screenEvent(int x, int y, int w, int h) { + //System.out.println("update x="+x+", y="+y+", w="+w+", h="+h+", ts="+_curTs); + + FrameUpdate u = new FrameUpdate(x / SCALEDOWN, y / SCALEDOWN, + (x + w) / SCALEDOWN - x / SCALEDOWN, + (y + h) / SCALEDOWN - y / SCALEDOWN, + _curTs); + + BufferedImage im = _vc.getBufferedImage(); + for (int mx = x / SCALEDOWN; mx < (x + w) / SCALEDOWN; mx++) + for (int my = y / SCALEDOWN; my < (y + h) / SCALEDOWN; my++) + u.set(mx, my, im.getRGB(mx * SCALEDOWN, my * SCALEDOWN)); + + _seq.add(u); + //System.out.println("added frame update ts=" + _curTs); + } + + public void mouseEvent(MouseEvent e) {} + public void keyEvent(KeyEvent e) {} + + public void create() throws IOException { + FileInputStream fis = new FileInputStream(_f); + BufferedInputStream bis = new BufferedInputStream(fis); + DataInputStream dis = new DataInputStream(bis); + + try { + create(dis); + } finally { + dis.close(); + bis.close(); + fis.close(); + } + } + + private void create(DataInputStream is) throws IOException { + // skip version header + long s = is.skip(12); + if (s != 12) + throw new RuntimeException("couldn't skip 12"); + + _seq = new FrameSequence(); + _viewer = new VncViewer(); + _viewer.mainArgs = new String[] { "host", "pipe-magic", + "port", "12345", + "password", "dummy" }; + _viewer.inAnApplet = false; + _viewer.inSeparateFrame = true; + _viewer.doExit = false; + _viewer.init(); + _viewer.start(); + + try { + _viewer._rfbLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("rfb latch done"); + + processBlob(is); + + try { + _viewer._vcLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + System.out.println("vc latch done"); + + _vc = _viewer.vc; + _vc.setVncEventListener(this); + _vc.repaintEnabled = false; + _viewer.deferUpdateRequests = 0; + + try { + for (;;) { + processBlob(is); + } + } catch (EOFException e) { + System.out.println("EOF: done"); + } + + _seq.setSize(_vc.rfb.framebufferWidth / SCALEDOWN, _vc.rfb.framebufferHeight / SCALEDOWN); + _viewer.disconnect(); + } + + private void processBlob(DataInputStream is) + throws IOException, EOFException + { + int len = is.readInt(); + int fileBlockLen = (len + 3) & 0x7FFFFFFC; + byte[] data = new byte[fileBlockLen]; + is.readFully(data); + _curTs = is.readInt(); + + if (_curTs > _lastTsPrinted + 30000) { + System.out.println(" ... processing ts = " + _curTs); + _lastTsPrinted = _curTs; + } + + // feed this blob into the RfbProto/VncViewer + _viewer.rfb.pipeOs.write(data, 0, len); + _viewer.rfb.pipeOs.flush(); + _bytesSent += len; + + synchronized (_viewer.rfb) { + while (_viewer.rfb.bytesProcessed != _bytesSent) { + try { + _viewer.rfb.wait(); + } catch (InterruptedException e) {} + } + } + } +} diff -u -Pr tightvnc-1.2.9/FrameTimeIterator.java vncplay-1.0/FrameTimeIterator.java --- tightvnc-1.2.9/FrameTimeIterator.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/FrameTimeIterator.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,54 @@ +import java.util.*; + +public class FrameTimeIterator implements Iterator, Iterable { + private Iterator _i; + private FrameUpdate _next; + + public FrameTimeIterator(Iterator i) { + _i = i; + } + + public FrameTimeIterator(Iterable i) { + _i = i.iterator(); + } + + public FrameUpdate next() { + // Pull the next frame update from our one-deep stack. + FrameUpdate r = _next; + _next = null; + + // If our stack was empty, pull it off the iterator. + if (r == null) + r = _i.next(); + + while (_i.hasNext()) { + // Check what's next in the iterator. + FrameUpdate n = _i.next(); + + // If we crossed a timestamp boundary, push the extra guy and return. + if (n.ts() != r.ts()) { + _next = n; + break; + } + + // Otherwise, this is the next guy we're potentially returning. + r = n; + } + + return r; + } + + public boolean hasNext() { + if (_next != null) + return true; + return _i.hasNext(); + } + + public void remove() { + throw new RuntimeException("cannot remove!"); + } + + public Iterator iterator() { + return this; + } +} diff -u -Pr tightvnc-1.2.9/FrameUpdate.java vncplay-1.0/FrameUpdate.java --- tightvnc-1.2.9/FrameUpdate.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/FrameUpdate.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,126 @@ +public class FrameUpdate { + private int _xbase, _ybase, _width, _height; + private int _pixels[]; + private long _ts; + private FrameUpdate _prev; + private FrameSequence _seq; + + public FrameUpdate(int x, int y, int width, int height, long ts) { + _xbase = x; + _ybase = y; + _width = width; + _height = height; + _pixels = new int[width * height]; + _ts = ts; + } + + public boolean contains(int x, int y) { + if (x < _xbase) + return false; + if (y < _ybase) + return false; + if (x >= _xbase + _width) + return false; + if (y >= _ybase + _height) + return false; + + return true; + } + + public int getMatchPixels(FrameUpdate u) { + int matches = 0; + int width = _seq.width(); + int height = _seq.height(); + + for (int x = 0; x < width; x++) + for (int y = 0; y < height; y++) + if (get(x, y) == u.get(x, y)) + matches++; + + return matches; + } + + public void setSeq(FrameSequence s) { + _seq = s; + } + + public FrameSequence seq() { + return _seq; + } + + private int getThis(int x, int y) { + int xoff = x - _xbase; + int yoff = y - _ybase; + + return _pixels[xoff + yoff * _width]; + } + + public int get(int x, int y) { + FrameUpdate f = this; + + while (f != null) { + if (x < f._xbase || y < f._ybase || x >= f._xbase + f._width || y >= f._ybase + f._height) + f = f._prev; + else + return f.getThis(x, y); + } + + return 0; + } + + public void set(int x, int y, int value) { + int xoff = x - _xbase; + int yoff = y - _ybase; + + _pixels[xoff + yoff * _width] = value; + } + + public long ts() { + return _ts; + } + + public FrameUpdate prev() { + return _prev; + } + + public void setPrev(FrameUpdate u) { + _prev = u; + } + + private int maxWidth() { + int m = _xbase + _width; + + if (_prev != null) { + int pm = _prev.maxWidth(); + if (pm > m) + m = pm; + } + + return m; + } + + private int maxHeight() { + int m = _ybase + _height; + + if (_prev != null) { + int pm = _prev.maxHeight(); + if (pm > m) + m = pm; + } + + return m; + } + + public FrameUpdate evalFully() { + int w = maxWidth(); + int h = maxHeight(); + + FrameUpdate n = new FrameUpdate(0, 0, w, h, ts()); + n.setSeq(_seq); + for (int x = 0; x < w; x++) + for (int y = 0; y < h; y++) + n.set(x, y, get(x, y)); + + return n; + } +} diff -u -Pr tightvnc-1.2.9/Makefile vncplay-1.0/Makefile --- tightvnc-1.2.9/Makefile 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/Makefile 2005-04-10 21:52:01.000000000 -0700 @@ -1,40 +1,8 @@ -# -# Making the VNC applet. -# +JC = javac -target 1.5 -source 1.5 -CP = cp -JC = javac -JAR = jar -ARCHIVE = VncViewer.jar -MANIFEST = MANIFEST.MF -PAGES = index.vnc -INSTALL_DIR = /usr/local/vnc/classes +all: + $(JC) -O *.java -CLASSES = VncViewer.class RfbProto.class AuthPanel.class VncCanvas.class \ - OptionsFrame.class ClipboardFrame.class ButtonPanel.class \ - DesCipher.class RecordingFrame.class SessionRecorder.class \ - SocketFactory.class HTTPConnectSocketFactory.class \ - HTTPConnectSocket.class ReloginPanel.class +clean: + $(RM) *.class -SOURCES = VncViewer.java RfbProto.java AuthPanel.java VncCanvas.java \ - OptionsFrame.java ClipboardFrame.java ButtonPanel.java \ - DesCipher.java RecordingFrame.java SessionRecorder.java \ - SocketFactory.java HTTPConnectSocketFactory.java \ - HTTPConnectSocket.java ReloginPanel.java - -all: $(CLASSES) $(ARCHIVE) - -$(CLASSES): $(SOURCES) - $(JC) -O $(SOURCES) - -$(ARCHIVE): $(CLASSES) $(MANIFEST) - $(JAR) cfm $(ARCHIVE) $(MANIFEST) $(CLASSES) - -install: $(CLASSES) $(ARCHIVE) - $(CP) $(CLASSES) $(ARCHIVE) $(PAGES) $(INSTALL_DIR) - -export:: $(CLASSES) $(ARCHIVE) $(PAGES) - @$(ExportJavaClasses) - -clean:: - $(RM) *.class *.jar diff -u -Pr tightvnc-1.2.9/RfbProto.java vncplay-1.0/RfbProto.java --- tightvnc-1.2.9/RfbProto.java 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/RfbProto.java 2005-04-10 21:52:01.000000000 -0700 @@ -80,6 +80,11 @@ String host; int port; Socket sock; + + private PipedInputStream pipeIs; + PipedOutputStream pipeOs; + int bytesProcessed = 0; + DataInputStream is; OutputStream os; SessionRecorder rec; @@ -126,30 +131,40 @@ host = h; port = p; - if (viewer.socketFactory == null) { - sock = new Socket(host, port); + if (h.equals("pipe-magic")) { + pipeIs = new PipedInputStream(); + pipeOs = new PipedOutputStream(pipeIs); + CountingInputStream cis = new CountingInputStream(this, pipeIs); + + is = new DataInputStream(cis); + os = new FileOutputStream(new File("/dev/null")); } else { - try { - Class factoryClass = Class.forName(viewer.socketFactory); - SocketFactory factory = (SocketFactory)factoryClass.newInstance(); - if (viewer.inAnApplet) - sock = factory.createSocket(host, port, viewer); - else - sock = factory.createSocket(host, port, viewer.mainArgs); - } catch(Exception e) { - e.printStackTrace(); - throw new IOException(e.getMessage()); + if (viewer.socketFactory == null) { + sock = new Socket(host, port); + } else { + try { + Class factoryClass = Class.forName(viewer.socketFactory); + SocketFactory factory = (SocketFactory)factoryClass.newInstance(); + if (viewer.inAnApplet) + sock = factory.createSocket(host, port, viewer); + else + sock = factory.createSocket(host, port, viewer.mainArgs); + } catch(Exception e) { + e.printStackTrace(); + throw new IOException(e.getMessage()); + } } + is = new DataInputStream(new BufferedInputStream(sock.getInputStream(), + 16384)); + os = sock.getOutputStream(); } - is = new DataInputStream(new BufferedInputStream(sock.getInputStream(), - 16384)); - os = sock.getOutputStream(); } synchronized void close() { try { - sock.close(); + if (sock != null) + sock.close(); closed = true; System.out.println("RFB socket closed"); if (rec != null) { @@ -302,6 +317,8 @@ zlibWarningShown = false; tightWarningShown = false; + + rec.flush(); } // @@ -325,6 +342,47 @@ framebufferHeight = height; } + private class CountingInputStream extends InputStream { + private int _lastRead; + private RfbProto _rfb; + private InputStream _is; + + public CountingInputStream(RfbProto rfb, InputStream is) { + super(); + _rfb = rfb; + _is = is; + _lastRead = 0; + } + + private synchronized void flushProcessed() { + synchronized (_rfb) { + _rfb.bytesProcessed += _lastRead; + _rfb.notify(); + } + } + + public synchronized int read() throws IOException { + flushProcessed(); + int r = _is.read(); + _lastRead = 1; + return r; + } + + public synchronized long skip(long n) throws IOException { + throw new RuntimeException("CountingInputStream cannot skip"); + } + + public synchronized int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public synchronized int read(byte[] b, int off, int len) throws IOException { + flushProcessed(); + int r = _is.read(b, off, len); + _lastRead = r; + return r; + } + } // // Read the server message type @@ -480,6 +538,20 @@ return len; } + void readColourMapEntries() throws IOException { + // discard padding + is.readByte(); + + int firstColour = is.readUnsignedShort(); + int nColours = is.readUnsignedShort(); + + for (int i = 0; i < nColours; i++) { + int r = is.readUnsignedShort(); + int g = is.readUnsignedShort(); + int b = is.readUnsignedShort(); + } + } + // // Write a FramebufferUpdateRequest message diff -u -Pr tightvnc-1.2.9/ScreenUpdateWatcher.java vncplay-1.0/ScreenUpdateWatcher.java --- tightvnc-1.2.9/ScreenUpdateWatcher.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/ScreenUpdateWatcher.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,40 @@ +import java.util.*; +import java.awt.*; + +public class ScreenUpdateWatcher { + private boolean _givenUp; + private int _maxUpdateCount; + private Rectangle _watchBounds; + private java.util.List _updates; + + public ScreenUpdateWatcher(Rectangle bounds, int maxCount) { + _givenUp = false; + _watchBounds = bounds; + _maxUpdateCount = maxCount; + _updates = new ArrayList(); + } + + public void markUpdated(Rectangle r) { + if (_givenUp) + return; + if (!_watchBounds.intersects(r)) + return; + + _updates.add(r); + if (_updates.size() > _maxUpdateCount) + _givenUp = true; + } + + public boolean isUnchanged(Rectangle r) { + if (_givenUp) + return false; + if (!_watchBounds.contains(r)) + return false; + + for (Rectangle update: _updates) + if (update.intersects(r)) + return false; + + return true; + } +} diff -u -Pr tightvnc-1.2.9/SessionRecorder.java vncplay-1.0/SessionRecorder.java --- tightvnc-1.2.9/SessionRecorder.java 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/SessionRecorder.java 2005-04-10 21:52:01.000000000 -0700 @@ -45,6 +45,10 @@ buffer = new byte[bufferSize]; } + public long getStartTime() { + return startTime; + } + public SessionRecorder(String name) throws IOException { this(name, 65536); } diff -u -Pr tightvnc-1.2.9/SyncSequence.java vncplay-1.0/SyncSequence.java --- tightvnc-1.2.9/SyncSequence.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/SyncSequence.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,40 @@ +import java.util.*; + +public class SyncSequence { + private ArrayList _syncTimes; + + public SyncSequence() { + _syncTimes = new ArrayList(); + } + + public void addSyncTime(long ts) { + _syncTimes.add(ts); + } + + public int size() { + return _syncTimes.size(); + } + + // return the last sync point that happened before ts + public int syncPoint(long ts) { + int sz = _syncTimes.size(); + + for (int i = 0; i < sz; i++) + if (_syncTimes.get(i) > ts) + return i - 1; + + return sz - 1; + } + + // return the time of this sync point + public long syncTime(int num) { + int sz = _syncTimes.size(); + + if (num < 0) + num = 0; + if (num >= sz) + num = sz - 1; + + return _syncTimes.get(num); + } +} diff -u -Pr tightvnc-1.2.9/VncAnalyzer.java vncplay-1.0/VncAnalyzer.java --- tightvnc-1.2.9/VncAnalyzer.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/VncAnalyzer.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,133 @@ +import java.io.*; + +public class VncAnalyzer { + public static final int INTERESTING_PCT_DIFF = 2; + + private static FrameSequence getSeq(String filename) throws IOException { + File f = new File(filename); + FrameSequencer fsr = new FrameSequencer(f); + fsr.create(); + FrameSequence fs = fsr.getFrameSequence(); + + System.out.println("file " + filename + " -> " + + fs.size() + " updates"); + + return fs; + } + + private static SyncSequence getSync(String filename, int i) throws IOException { + File f = new File(filename); + FileReader fr = new FileReader(f); + BufferedReader br = new BufferedReader(fr); + SyncSequence ss = new SyncSequence(); + + ss.addSyncTime(0); + + long totalExtra = 0; + boolean waitingForSync = false; + while (true) { + String line = br.readLine(); + if (line == null) + break; + line.trim(); + String parts[] = line.split(" "); + + if (line.startsWith("Waiting for sync")) + waitingForSync = true; + if (line.startsWith("Sync ok at") && waitingForSync) { + long ts = Long.parseLong(parts[3]); + ss.addSyncTime(ts); + waitingForSync = false; + } + if (line.startsWith("== sending mouse click event at")) + System.out.println("input_ts: " + i + ": " + parts[6]); + if (line.startsWith("== sending keyboard event at")) + System.out.println("input_ts: " + i + ": " + parts[5]); + } + + ss.addSyncTime(Integer.MAX_VALUE); + + System.out.println("sync file " + filename + " -> " + + ss.size() + " sync points"); + + return ss; + } + + public static void main(String[] args) throws IOException { + if (args.length < 4 || (args.length % 2 != 0)) { + System.out.println("Usage: VncAnalyzer log0 run-one-expt0.out log1 run-one-expt1.out ..."); + System.exit(1); + } + + int numSeqs = args.length / 2; + SyncSequence sync[] = new SyncSequence[numSeqs]; + FrameSequence seq[] = new FrameSequence[numSeqs]; + + int syncSize = -1; + for (int i = 0; i < numSeqs; i++) { + System.out.println("Sync log " + i + ": " + args[2*i + 1]); + sync[i] = getSync(args[2*i + 1], i); + if (syncSize == -1) + syncSize = sync[i].size(); + if (syncSize != sync[i].size()) { + System.out.println("Sync point count mismatch: " + sync[i].size() + " != " + syncSize); + System.exit(1); + } + } + + for (int i = 0; i < numSeqs; i++) { + System.out.println("Frame sequence " + i + ": " + args[2*i]); + seq[i] = getSeq(args[2*i]); + } + + FrameUpdate last = null; + for (FrameUpdate u: new FrameTimeIterator(seq[0])) { + boolean analyze = false; + + if (last == null) { + analyze = true; + } else { + int matchPixels = u.getMatchPixels(last); + int totalPixels = u.seq().totalPixels(); + int matchPercent = (100 * matchPixels) / totalPixels; + + System.out.println("-- frame " + u.ts() + " is " + matchPercent + "% similar to " + last.ts()); + if (matchPercent < (100 - INTERESTING_PCT_DIFF)) + analyze = true; + } + + if (analyze) { + int syncLo = sync[0].syncPoint(u.ts() - 500); + int syncCtr = sync[0].syncPoint(u.ts()); + int syncHi = sync[0].syncPoint(u.ts() + 500); + + System.out.println("== sync intervals: " + syncLo + "-" + + syncCtr + "-" + + syncHi); + + for (int i = 0; i < numSeqs; i++) { + long syncTimeLo = sync[i].syncTime(syncLo - 2) - 500; + long syncTimeCtr = sync[i].syncTime(syncCtr); + long syncTimeHi = sync[i].syncTime(syncHi + 2) + 500; + + System.out.println("== search range for " + i + ": " + + syncTimeLo + "-" + syncTimeCtr + "-" + syncTimeHi); + + FrameUpdate m = seq[i].findClosestMatch(u, syncTimeLo, syncTimeCtr, syncTimeHi); + if (m == null) { + System.out.println("== what? findClosestMatch returned null"); + } else { + int pixelMatches = u.getMatchPixels(m); + int pixelTotal = u.seq().totalPixels(); + System.out.println("== " + u.ts() + "@0" + " matches with " + m.ts() + "@" + i + + " (" + pixelMatches + "/" + pixelTotal + ")"); + } + } + + last = u; + } + } + + System.exit(0); + } +} diff -u -Pr tightvnc-1.2.9/VncCanvas.java vncplay-1.0/VncCanvas.java --- tightvnc-1.2.9/VncCanvas.java 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/VncCanvas.java 2005-04-10 21:52:01.000000000 -0700 @@ -26,6 +26,7 @@ import java.io.*; import java.lang.*; import java.util.zip.*; +import java.util.*; // @@ -41,7 +42,7 @@ Color[] colors; int bytesPixel; - Image memImage; + BufferedImage memImage; Graphics memGraphics; Image rawPixelsImage; @@ -67,6 +68,15 @@ // True if we process keyboard and mouse events. boolean inputEnabled; + // Flag to disable repaints for speed + boolean repaintEnabled = true; + + // Used for watching sessions + private VncEventListener vncListener; + private boolean inputBlocked; + private java.util.List holdQueue; + private java.util.Set sentToListener = new HashSet(); + // // The constructor. // @@ -93,6 +103,66 @@ // Keyboard listener is enabled even in view-only mode, to catch // 'r' or 'R' key presses used to request screen update. addKeyListener(this); + + inputBlocked = false; + + setFocusTraversalKeys(DefaultKeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, + new TreeSet()); + + setFocusTraversalKeys(DefaultKeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, + new TreeSet()); + } + + public void setVncEventListener(VncEventListener l) { + vncListener = l; + } + + public void setInputBlock(boolean v) { + inputBlocked = v; + } + + public void holdInput() { + if (holdQueue != null) + return; + + holdQueue = new ArrayList(); + sentToListener = new HashSet(); + } + + public void releaseInput() { + if (holdQueue == null) + return; + + java.util.List q = holdQueue; + holdQueue = null; + + // Some Linux applications, like Star Office, get confused when + // they get a flurry of input events in a row, like a mouse click + // immediately followed by a release. So, try to maintain the + // same timing the events had when they were coming in. + + InputEvent lastEvent = null; + long lastEventSent = 0; + + for (InputEvent e: q) { + if (lastEvent != null) { + while ((e.getWhen() - lastEvent.getWhen()) > + (System.currentTimeMillis() - lastEventSent)) { + try { + Thread.sleep(1); + } catch (InterruptedException ie) {} + } + } + + lastEvent = e; + lastEventSent = System.currentTimeMillis(); + + if (e instanceof KeyEvent) + processLocalKeyEvent((KeyEvent) e); + else if (e instanceof MouseEvent) + processLocalMouseEvent((MouseEvent) e, true); + else throw new RuntimeException("Unknown event type " + e.getClass().getName()); + } } // @@ -192,6 +262,10 @@ updateFramebufferSize(); } + public BufferedImage getBufferedImage() { + return memImage; + } + void updateFramebufferSize() { // Useful shortcuts. @@ -202,12 +276,14 @@ // its geometry should be changed. It's not necessary to replace // existing image if only pixel format should be changed. if (memImage == null) { - memImage = viewer.createImage(fbWidth, fbHeight); + //memImage = viewer.createImage(fbWidth, fbHeight); + memImage = new BufferedImage(fbWidth, fbHeight, BufferedImage.TYPE_INT_ARGB); memGraphics = memImage.getGraphics(); } else if (memImage.getWidth(null) != fbWidth || memImage.getHeight(null) != fbHeight) { synchronized(memImage) { - memImage = viewer.createImage(fbWidth, fbHeight); + //memImage = viewer.createImage(fbWidth, fbHeight); + memImage = new BufferedImage(fbWidth, fbHeight, BufferedImage.TYPE_INT_ARGB); memGraphics = memImage.getGraphics(); } } @@ -604,7 +680,7 @@ } // Finished with a row of tiles, now let's show it. - scheduleRepaint(x, y, w, h); + scheduleRepaint(x, ty, w, th); } } @@ -1157,8 +1233,11 @@ // void scheduleRepaint(int x, int y, int w, int h) { + if (vncListener != null) + vncListener.screenEvent(x, y, w, h); // Request repaint, deferred if necessary. - repaint(viewer.deferScreenUpdates, x, y, w, h); + if (repaintEnabled) + repaint(viewer.deferScreenUpdates, x, y, w, h); } // @@ -1166,26 +1245,28 @@ // public void keyPressed(KeyEvent evt) { - processLocalKeyEvent(evt); + if (!inputBlocked) processLocalKeyEvent(evt); + evt.consume(); } public void keyReleased(KeyEvent evt) { - processLocalKeyEvent(evt); + if (!inputBlocked) processLocalKeyEvent(evt); + evt.consume(); } public void keyTyped(KeyEvent evt) { evt.consume(); } public void mousePressed(MouseEvent evt) { - processLocalMouseEvent(evt, false); + if (!inputBlocked) processLocalMouseEvent(evt, false); } public void mouseReleased(MouseEvent evt) { - processLocalMouseEvent(evt, false); + if (!inputBlocked) processLocalMouseEvent(evt, false); } public void mouseMoved(MouseEvent evt) { - processLocalMouseEvent(evt, true); + if (!inputBlocked) processLocalMouseEvent(evt, true); } public void mouseDragged(MouseEvent evt) { - processLocalMouseEvent(evt, true); + if (!inputBlocked) processLocalMouseEvent(evt, true); } public void processLocalKeyEvent(KeyEvent evt) { @@ -1204,6 +1285,22 @@ } else { // Input enabled. synchronized(rfb) { + if (holdQueue != null) { + evt.consume(); + holdQueue.add(evt); + return; + } + + if (vncListener != null && !sentToListener.contains(evt)) + vncListener.keyEvent(evt); + + if (holdQueue != null) { + evt.consume(); + holdQueue.add(evt); + sentToListener.add(evt); + return; + } + try { rfb.writeKeyEvent(evt); } catch (Exception e) { @@ -1224,7 +1321,26 @@ softCursorMove(evt.getX(), evt.getY()); } synchronized(rfb) { + if (holdQueue != null) { + evt.consume(); + holdQueue.add(evt); + return; + } + + if (vncListener != null && !sentToListener.contains(evt)) + vncListener.mouseEvent(evt); + + if (holdQueue != null) { + evt.consume(); + holdQueue.add(evt); + sentToListener.add(evt); + return; + } + try { + if (evt.getButton() != MouseEvent.NOBUTTON) + System.out.println("sending button event at " + System.currentTimeMillis()); + rfb.writePointerEvent(evt); } catch (Exception e) { e.printStackTrace(); @@ -1279,10 +1395,16 @@ if (viewer.options.ignoreCursorUpdates) { if (encodingType == rfb.EncodingXCursor) { - rfb.is.skipBytes(6 + bytesMaskData * 2); + byte[] crud = new byte[6 + bytesMaskData * 2]; + rfb.is.readFully(crud); + if (rfb.rec != null) + rfb.rec.write(crud); } else { // rfb.EncodingRichCursor - rfb.is.skipBytes(width * height + bytesMaskData); + byte[] crud = new byte[width * height + bytesMaskData]; + rfb.is.readFully(crud); + if (rfb.rec != null) + rfb.rec.write(crud); } return; } @@ -1296,6 +1418,9 @@ // Read foreground and background colors of the cursor. byte[] rgb = new byte[6]; rfb.is.readFully(rgb); + if (rfb.rec != null) + rfb.rec.write(rgb); + int[] colors = { (0xFF000000 | (rgb[3] & 0xFF) << 16 | (rgb[4] & 0xFF) << 8 | (rgb[5] & 0xFF)), (0xFF000000 | (rgb[0] & 0xFF) << 16 | @@ -1304,8 +1429,13 @@ // Read pixel and mask data. byte[] pixBuf = new byte[bytesMaskData]; rfb.is.readFully(pixBuf); + if (rfb.rec != null) + rfb.rec.write(pixBuf); + byte[] maskBuf = new byte[bytesMaskData]; rfb.is.readFully(maskBuf); + if (rfb.rec != null) + rfb.rec.write(maskBuf); // Decode pixel data into softCursorPixels[]. byte pixByte, maskByte; @@ -1340,8 +1470,13 @@ // Read pixel and mask data. byte[] pixBuf = new byte[width * height * bytesPixel]; rfb.is.readFully(pixBuf); + if (rfb.rec != null) + rfb.rec.write(pixBuf); + byte[] maskBuf = new byte[bytesMaskData]; rfb.is.readFully(maskBuf); + if (rfb.rec != null) + rfb.rec.write(maskBuf); // Decode pixel data into softCursorPixels[]. byte pixByte, maskByte; diff -u -Pr tightvnc-1.2.9/VncEventListener.java vncplay-1.0/VncEventListener.java --- tightvnc-1.2.9/VncEventListener.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/VncEventListener.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,7 @@ +import java.awt.event.*; + +public interface VncEventListener { + public void screenEvent(int x, int y, int w, int h); + public void mouseEvent(MouseEvent e); + public void keyEvent(KeyEvent e); +} diff -u -Pr tightvnc-1.2.9/VncInputPlayback.java vncplay-1.0/VncInputPlayback.java --- tightvnc-1.2.9/VncInputPlayback.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/VncInputPlayback.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,339 @@ +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import java.io.*; + +public class VncInputPlayback implements VncEventListener, ActionListener { + // If fewer than SYNC_PIXEL_MATCH_THRESHOLD pixels in a sync point are + // mismatched, then we say that the image looks good and we can proceed. + public static final int SYNC_PIXEL_MATCH_THRESHOLD = 5; + + // Give up if we don't sync within SYNC_TIMEOUT_SEC seconds + public static final int SYNC_TIMEOUT_SEC = 600; + + // Wait some time (msec) after sync matches + public static final int WAIT_AFTER_SYNC = 200; + + private VncCanvas _vc; + + private FileReader _fr; + private BufferedReader _br; + private int _brLine; + private javax.swing.Timer _timer; + private FileWriter _auxwr; + + private boolean _syncWait; + private long _syncWaitStarted; + private long _syncWaitClear; + private Map _syncWaitOp; + private Map _nextOp; + private long _lastEventCompletion; + + private MouseEvent _lastMouseMotion; + private long _lastMouseMotionSent; + + private boolean _autoExit; + + private long _logStartTime; + + private final static boolean debug = false; + private final static int _extraEventDelay = 0; + private final static int _eventDelayPercent = 100; + + public VncInputPlayback(VncCanvas vc) { + _vc = vc; + _vc.setVncEventListener(this); + + // Start the recording, so we can sync with its startTime + try { + _vc.viewer.checkRecordingStatus(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + _logStartTime = System.currentTimeMillis(); + if (_vc.viewer.rfb.rec != null) + _logStartTime = _vc.viewer.rfb.rec.getStartTime(); + + try { + _fr = new FileReader(new File(_vc.viewer.traceFile)); + _br = new BufferedReader(_fr); + _brLine = 0; + + _auxwr = new FileWriter(new File(_vc.viewer.recordFile + ".aux")); + } catch (IOException e) { + e.printStackTrace(); + } + + _lastEventCompletion = System.currentTimeMillis(); + _timer = new javax.swing.Timer(20, this); + _timer.start(); + _autoExit = false; + + _vc.setInputBlock(true); + + log("VNC input playback starting"); + } + + public void stop() { + _vc.setVncEventListener(null); + + try { + _br.close(); + _fr.close(); + _auxwr.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + _timer.stop(); + + System.out.println("VNC input playback stopping on line " + _brLine); + _vc.setInputBlock(false); + + if (_autoExit) + System.exit(0); + } + + private void log(String m) { + System.out.println(m); + try { + _auxwr.write(m + "\n"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void setAutoExit(boolean v) { + _autoExit = v; + } + + private int getInt(Map m, String key) { + return Integer.parseInt(m.get(key)); + } + + public void actionPerformed(ActionEvent e) { + long t = System.currentTimeMillis(); + + if (_syncWait && _lastMouseMotion != null) { + if (t > _lastMouseMotionSent + 1000) { + log("Trying to wiggle the mouse..."); + + MouseEvent wiggle = + new MouseEvent(_vc, + _lastMouseMotion.getID(), + t, + _lastMouseMotion.getModifiers(), + _lastMouseMotion.getX() - 1, + _lastMouseMotion.getY(), + 0, false, + _lastMouseMotion.getButton()); + _vc.processLocalMouseEvent(wiggle, true); + _vc.processLocalMouseEvent(_lastMouseMotion, true); + _lastMouseMotionSent = t; + } + } + + if (_syncWait && + (_syncWaitStarted > 0) && + (t > _syncWaitStarted + SYNC_TIMEOUT_SEC * 1000)) + { + log("Timed out waiting for sync after " + (t - _syncWaitStarted) + " msec"); + stop(); + } + + while (!_syncWait && _syncWaitClear < System.currentTimeMillis()) { + Map m = next(); + if (m == null) { + stop(); + return; + } + + long ts = Long.parseLong(m.get("run-at")); + if (ts < System.currentTimeMillis()) { + doEvent(m); + _lastEventCompletion = System.currentTimeMillis(); + /* _lastEventCompletion = ts; */ + } else { + pushBack(m); + return; + } + } + } + + private long logTime() { + return System.currentTimeMillis() - _logStartTime; + } + + private void doEvent(Map m) { + String type = m.get("type"); + if (type.equals("sync")) { + _syncWait = true; + _syncWaitOp = m; + _syncWaitStarted = 0; + _syncWaitClear = 0; + + log("Waiting for sync... at " + logTime()); + checkSync(); + _syncWaitStarted = System.currentTimeMillis(); + } else if (type.equals("java.awt.event.MouseEvent")) { + int id = getInt(m, "id"); + int modifiers = getInt(m, "modifiers"); + int x = getInt(m, "x"); + int y = getInt(m, "y"); + int button = getInt(m, "button"); + + MouseEvent me = new MouseEvent(_vc, id, System.currentTimeMillis(), + modifiers, x, y, 0, false, button); + _vc.processLocalMouseEvent(me, true); + if (button != 0) + log("== sending mouse click event at " + + logTime()); + + if (id == MouseEvent.MOUSE_DRAGGED || + id == MouseEvent.MOUSE_MOVED) { + _lastMouseMotion = me; + _lastMouseMotionSent = System.currentTimeMillis(); + } + } else if (type.equals("java.awt.event.KeyEvent")) { + int id = getInt(m, "id"); + int modifiers = getInt(m, "modifiers"); + int keyCode = getInt(m, "keycode"); + char keyChar = (char) getInt(m, "keychar"); + + KeyEvent ke = new KeyEvent(_vc, id, System.currentTimeMillis(), + modifiers, keyCode, keyChar); + _vc.processLocalKeyEvent(ke); + log("== sending keyboard event at " + logTime()); + } else { + log("unknown type of event: " + type); + } + } + + private boolean syncOk() { + BufferedImage im = _vc.getBufferedImage(); + + int x0 = getInt(_syncWaitOp, "x0"); + int x1 = getInt(_syncWaitOp, "x1"); + int y0 = getInt(_syncWaitOp, "y0"); + int y1 = getInt(_syncWaitOp, "y1"); + + //log("Checking sync..."); + + Map newMap = new HashMap(); + newMap.put("x0", "" + x0); + newMap.put("x1", "" + x1); + newMap.put("y0", "" + y0); + newMap.put("y1", "" + y1); + + int mismatchCount = 0; + int pixelCount = (x1 - x0 + 1) * (y1 - y0 + 1); + + for (int x = x0; x <= x1; x++) { + for (int y = y0; y <= y1; y++) { + int syncPixel = getInt(_syncWaitOp, "px" + x + "y" + y); + int realPixel = im.getRGB(x, y); + newMap.put("px" + x + "y" + y, "" + realPixel); + if (syncPixel != realPixel) { + mismatchCount++; + } + } + } + + log("Sync mismatches: " + mismatchCount + "/" + pixelCount + + ", threshold " + SYNC_PIXEL_MATCH_THRESHOLD); + if (mismatchCount >= SYNC_PIXEL_MATCH_THRESHOLD && debug) { + log("Original image:"); + printMap(_syncWaitOp); + log("Current image:"); + printMap(newMap); + } + + return (mismatchCount < SYNC_PIXEL_MATCH_THRESHOLD); + } + + private void checkSync() { + if (_syncWait && syncOk()) { + long syncWaitTime = 0; + + if (_syncWaitStarted > 0) { + syncWaitTime = System.currentTimeMillis() - _syncWaitStarted; + _syncWaitClear = System.currentTimeMillis() + WAIT_AFTER_SYNC; + } + + log("Sync ok after " + syncWaitTime + " msec"); + log("Sync ok at " + logTime()); + _syncWait = false; + _lastEventCompletion = System.currentTimeMillis(); + } + } + + private void printMap(Map m) { + String s = "0"; + for (String k: m.keySet()) + s += " " + k + "=" + m.get(k); + log(s); + } + + public void screenEvent(int x, int y, int w, int h) { + checkSync(); + } + + public void mouseEvent(MouseEvent e) { + } + + public void keyEvent(KeyEvent e) { + } + + private Map next() { + Map m; + + if (_nextOp != null) + m = _nextOp; + else + m = readNext(System.currentTimeMillis()); + + _nextOp = null; + return m; + } + + private void pushBack(Map m) { + if (_nextOp != null) + throw new RuntimeException("pushback: nextop is not null"); + + _nextOp = m; + } + + private Map readNext(long lastCompletion) { + Map m = new HashMap(); + + String in; + + try { + in = _br.readLine(); + _brLine++; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + + if (in == null) + return null; + + StringTokenizer st = new StringTokenizer(in); + String delay = st.nextToken(); + m.put("run-at", "" + (_lastEventCompletion + _extraEventDelay + _eventDelayPercent * Integer.parseInt(delay) / 100)); + + while (st.hasMoreTokens()) { + String t = st.nextToken(); + + StringTokenizer st2 = new StringTokenizer(t, "="); + String key = st2.nextToken(); + String value = st2.nextToken(); + m.put(key, value); + } + + return m; + } +} diff -u -Pr tightvnc-1.2.9/VncInputRecorder.java vncplay-1.0/VncInputRecorder.java --- tightvnc-1.2.9/VncInputRecorder.java 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/VncInputRecorder.java 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,187 @@ +import java.util.*; +import java.awt.*; +import java.awt.image.*; +import java.awt.event.*; +import java.io.*; + +public class VncInputRecorder implements VncEventListener, ActionListener { + public static final int SYNC_AREA_SIZE = 5; + public static final int SYNC_WATCH_AREA_SIZE = SYNC_AREA_SIZE + 15; + + private long lastLogTime; + private VncCanvas _vc; + + private FileOutputStream _fos; + private BufferedOutputStream _bos; + private PrintStream _out; + + private long _lastButtonPress; + + private ScreenUpdateWatcher _updatesSinceSync; + + private javax.swing.Timer _quiesceTimer; + private Point _syncPoint; + private Map _syncEvent; + + public VncInputRecorder(VncCanvas vc) { + _vc = vc; + _vc.setVncEventListener(this); + + try { + _fos = new FileOutputStream(new File(_vc.viewer.traceFile)); + _bos = new BufferedOutputStream(_fos); + _out = new PrintStream(_bos); + } catch (IOException e) { + e.printStackTrace(); + } + + _updatesSinceSync = new ScreenUpdateWatcher(new Rectangle(0, 0, 0, 0), 0); + lastLogTime = System.currentTimeMillis(); + + System.out.println("VNC input recorder starting"); + } + + public void stop() { + _vc.setVncEventListener(null); + + try { + _out.close(); + _bos.close(); + _fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + System.out.println("VNC input recorder stopping"); + } + + public long getLogDeltaTime(long ts) { + long dt = ts - lastLogTime; + lastLogTime = ts; + return dt; + } + + public void screenEvent(int x, int y, int w, int h) { + if (_quiesceTimer != null) + System.out.println("screen update @ " + System.currentTimeMillis() + " for <" + x + ", " + y + "> w="+w+ ", h=" + h); + + Rectangle r = new Rectangle(x, y, w, h); + _updatesSinceSync.markUpdated(r); + } + + private int fixRange(int v, int min, int max) { + if (v < min) + return min; + if (v > max) + return max; + return v; + } + + private void syncEvent(Point p, Map em) { + _vc.holdInput(); + + _syncPoint = p; + _syncEvent = em; + + // Empirically, the VMware VNC server sends back all the frame buffer + // updates within ~100 msec (on a 100Mbps LAN connection) + _quiesceTimer = new javax.swing.Timer(150, this); + _quiesceTimer.start(); + System.out.println("scheduling timer to quiesce at " + System.currentTimeMillis()); + } + + public void actionPerformed(ActionEvent e) { + System.out.println("done waiting for quiescing at " + System.currentTimeMillis()); + + _quiesceTimer.stop(); + _quiesceTimer = null; + Point p = _syncPoint; + BufferedImage im = _vc.getBufferedImage(); + + int imWidth = im.getWidth(); + int imHeight = im.getHeight(); + + int x0 = fixRange(p.x - SYNC_AREA_SIZE, 0, imWidth - 1); + int x1 = fixRange(p.x + SYNC_AREA_SIZE, 0, imWidth - 1); + int y0 = fixRange(p.y - SYNC_AREA_SIZE, 0, imHeight - 1); + int y1 = fixRange(p.y + SYNC_AREA_SIZE, 0, imHeight - 1); + + Rectangle syncR = new Rectangle(x0, y0, x1 - x0 + 1, y1 - y0 + 1); + if (_updatesSinceSync.isUnchanged(syncR)) { + _vc.releaseInput(); + return; + } + + Map m = new HashMap(); + m.put("type", "sync"); + m.put("x0", "" + x0); + m.put("y0", "" + y0); + m.put("x1", "" + x1); + m.put("y1", "" + y1); + + for (int x = x0; x <= x1; x++) + for (int y = y0; y <= y1; y++) + m.put("px" + x + "y" + y, "" + im.getRGB(x, y)); + long ts = Long.parseLong(_syncEvent.get("when")); + log(m, ts); + log(_syncEvent, ts); + + Rectangle newWatch = new Rectangle(p.x - SYNC_WATCH_AREA_SIZE, + p.y - SYNC_WATCH_AREA_SIZE, + SYNC_WATCH_AREA_SIZE * 2, + SYNC_WATCH_AREA_SIZE * 2); + _updatesSinceSync = new ScreenUpdateWatcher(newWatch, 100); + + _vc.releaseInput(); + } + + public void mouseEvent(MouseEvent e) { + int id = e.getID(); + int button = e.getButton(); + boolean clickRelease = false; + + if (id == MouseEvent.MOUSE_PRESSED) + _lastButtonPress = e.getWhen(); + + if (id == MouseEvent.MOUSE_RELEASED) { + if (e.getWhen() < _lastButtonPress + 200) { + System.out.println("special-casing click release"); + clickRelease = true; + } + } + + Map em = logEvent(e, "x", e.getX(), "y", e.getY(), "button", e.getButton()); + + if (button != MouseEvent.NOBUTTON && !clickRelease) { + syncEvent(e.getPoint(), em); + } else { + log(em, Long.parseLong(em.get("when"))); + } + } + + public void keyEvent(KeyEvent e) { + log(logEvent(e, "keycode", e.getKeyCode(), "keychar", (int) e.getKeyChar()), e.getWhen()); + } + + private Map logEvent(InputEvent e, Object... args) { + Map m = new HashMap(); + + for (int i = 0; i < args.length; i += 2) + m.put(args[i].toString(), args[i+1].toString()); + + m.put("type", e.getClass().getName()); + m.put("id", "" + e.getID()); + m.put("modifiers", "" + e.getModifiers()); + m.put("when", "" + e.getWhen()); + + return m; + } + + private void log(Map m, long ts) { + long dt = getLogDeltaTime(ts); + _out.print(dt); + for (String k: m.keySet()) + _out.print(" " + k + "=" + m.get(k)); + _out.println(""); + } +} diff -u -Pr tightvnc-1.2.9/VncViewer.java vncplay-1.0/VncViewer.java --- tightvnc-1.2.9/VncViewer.java 2005-04-10 21:53:31.000000000 -0700 +++ vncplay-1.0/VncViewer.java 2005-04-10 21:52:01.000000000 -0700 @@ -29,6 +29,7 @@ import java.awt.event.*; import java.io.*; import java.net.*; +import java.util.concurrent.*; public class VncViewer extends java.applet.Applet implements java.lang.Runnable, WindowListener { @@ -83,11 +84,22 @@ String encPasswordParam; boolean showControls; boolean offerRelogin; + boolean autoRelogin; boolean showOfflineDesktop; int deferScreenUpdates; int deferCursorUpdates; int deferUpdateRequests; + VncInputRecorder inputRecorder; + VncInputPlayback inputPlayback; + boolean autoplayback = false; + boolean autorecord = false; + String traceFile = "record.log"; + String recordFile = "vnc.log"; + + CountDownLatch _vcLatch = new CountDownLatch(1); + CountDownLatch _rfbLatch = new CountDownLatch(1); + boolean doExit = true; // // init() @@ -131,6 +143,32 @@ public void update(Graphics g) { } + public void inputRecordingStart() { + if (inputRecorder == null) { + inputRecorder = new VncInputRecorder(vc); + } + } + + public void inputRecordingStop() { + if (inputRecorder != null) { + inputRecorder.stop(); + inputRecorder = null; + } + } + + public void inputPlaybackStart() { + if (inputPlayback == null) { + inputPlayback = new VncInputPlayback(vc); + } + } + + public void inputPlaybackStop() { + if (inputPlayback != null) { + inputPlayback.stop(); + inputPlayback = null; + } + } + // // run() - executed by the rfbThread to deal with the RFB socket. // @@ -150,6 +188,11 @@ vncContainer.add(buttonPanel); } + if (autoplayback) { + System.out.println("Auto-playback: recording into " + recordFile); + setRecordingStatus(recordFile); + } + try { connectAndAuthenticate(); @@ -192,7 +235,18 @@ if (showControls) buttonPanel.enableButtons(); + if (autoplayback) { + inputPlaybackStart(); + inputPlayback.setAutoExit(true); + } + + if (autorecord) { + inputRecordingStart(); + } + moveFocusToDesktop(); + + _vcLatch.countDown(); vc.processNormalProtocol(); } catch (NoRouteToHostException e) { @@ -347,6 +401,8 @@ rfb = new RfbProto(host, port, this); + _rfbLatch.countDown(); + rfb.readVersionMsg(); System.out.println("RFB server supports protocol version " + @@ -586,12 +642,34 @@ if (str != null && str.equalsIgnoreCase("No")) offerRelogin = false; + autoRelogin = false; + str = readParameter("auto relogin", false); + if (str != null && str.equalsIgnoreCase("yes")) + autoRelogin = true; + // Do we continue showing desktop on remote disconnect? showOfflineDesktop = false; str = readParameter("Show Offline Desktop", false); if (str != null && str.equalsIgnoreCase("Yes")) showOfflineDesktop = true; + // Do we automatically want to start playback? + str = readParameter("autoplay", false); + if (str != null) + autoplayback = true; + + str = readParameter("autorecord", false); + if (str != null) + autorecord = true; + + str = readParameter("tracefile", false); + if (str != null) + traceFile = str; + + str = readParameter("recordfile", false); + if (str != null) + recordFile = str; + // Fine tuning options. deferScreenUpdates = readIntParameter("Defer screen updates", 20); deferCursorUpdates = readIntParameter("Defer cursor updates", 10); @@ -671,7 +749,10 @@ if (inAnApplet) { showMessage("Disconnected"); } else { - System.exit(0); + if (doExit) { + inputRecordingStop(); + System.exit(0); + } } } @@ -687,7 +768,10 @@ // can not present the error to the user. Thread.currentThread().stop(); } else { - System.exit(1); + if (doExit) { + inputRecordingStop(); + System.exit(1); + } } } @@ -709,7 +793,10 @@ if (inAnApplet) { showMessage(str); } else { - System.exit(1); + if (doExit) { + inputRecordingStop(); + System.exit(1); + } } } @@ -723,7 +810,11 @@ Label errLabel = new Label(msg, Label.CENTER); errLabel.setFont(new Font("Helvetica", Font.PLAIN, 12)); - if (offerRelogin) { + if (autoRelogin) { + + getAppletContext().showDocument(getDocumentBase()); + + } else if (offerRelogin) { Panel gridPanel = new Panel(new GridLayout(0, 1)); Panel outerPanel = new Panel(new FlowLayout(FlowLayout.LEFT)); @@ -778,7 +869,10 @@ vncFrame.dispose(); if (!inAnApplet) { - System.exit(0); + if (doExit) { + inputRecordingStop(); + System.exit(0); + } } } diff -u -Pr tightvnc-1.2.9/vncanalyze vncplay-1.0/vncanalyze --- tightvnc-1.2.9/vncanalyze 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/vncanalyze 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,159 @@ +#!/usr/bin/perl + +use strict; + +my ($type, $vncplay_analyze_file) = @ARGV; +if (!(defined $type && defined $vncplay_analyze_file)) { + print + "Usage: vncanalyze type vncplay_analyze_output\n", + " types: raw-latency, cdf, median\n"; + exit(1); +} + +my @screen_ts = (); +my $num_traces = 0; +my @input_ts = (); + +open(VAOUT, $vncplay_analyze_file) or die "cannot open $vncplay_analyze_file"; +while () { + s/[\r\n]*$//; + if (/^Sync log (\d+): (.*)/) { + my $num = $1; + if ($num > $num_traces) { + $num_traces = $num; + } + } + + if (/^== (\d+)@(\d+) matches with (\d+)@(\d+) .*/) { + my ($ats, $anum, $bts, $bnum) = ($1, $2, $3, $4); + + if ($anum == $bnum && $ats != $bts) { + print STDERR "Suspicious line: $_\n"; + } + + push @{$screen_ts[$bnum]}, $bts; + } + + if (/^input_ts: (\d+): (\d+)/) { + push @{$input_ts[$1]}, $2; + } +} +close(VAOUT); + +# here $num_traces is actually the index of the last trace, not the +# count of them +$num_traces++; + +if ($num_traces < 2) { + die "Insufficient number of traces $num_traces!\n"; +} + +for (my $i = 0; $i < $num_traces; $i++) { + @{$screen_ts[$i]} = sort { $a <=> $b } @{$screen_ts[$i]}; +} + +# +# Here we want to find, for each screen update, the last input event +# that happened before this screen update in all runs +# +my @input_match_idx = (); + +for (my $i = 0; $i < $num_traces; $i++) { + my $ts_idx = 0; + + foreach my $ts (@{$screen_ts[$i]}) { + my $input_idx = find_floor_idx_in_seq($ts, @{$input_ts[$i]}); + my $other_idx = $input_match_idx[$ts_idx]; + if ( (!(defined $other_idx)) || ($input_idx < $other_idx) ) { + $input_match_idx[$ts_idx] = $input_idx; + } + + $ts_idx++; + } +} + +# +# Now we calculate the latency between screen updates and the corresponding +# input event... +# + +my @lag = (); +for (my $i = 0; $i < $num_traces; $i++) { + my @input_ts_i = @{$input_ts[$i]}; + my $ts_idx = 0; + + foreach my $ts (@{$screen_ts[$i]}) { + my $input_ts_idx = $input_match_idx[$ts_idx++]; + next if $input_ts_idx == -1; + + my $input_ts = $input_ts_i[$input_ts_idx]; + my $lag = $ts - $input_ts; + push @{$lag[$i]}, $lag; + } +} + +if ($type eq 'median') { + print "Median latencies:"; + for (my $i = 0; $i < $num_traces; $i++) { + my @lats = sort { $a <=> $b } @{$lag[$i]}; + print " ", $lats[$#lats / 2]; + } + print "\n"; +} elsif ($type eq 'raw-latency') { + my $idx = 0; + print "# Screen-update-index latency-from-trace0 latency-from-trace1 ...\n"; + foreach my $foo (@{$lag[0]}) { + print $idx; + for (my $i = 0; $i < $num_traces; $i++) { + print " ", $lag[$i][$idx]; + } + print "\n"; + $idx++; + } +} elsif ($type eq 'cdf') { + my $totcount; + my %cdf = (); + $cdf{0} = (); + for (my $i = 0; $i < $num_traces; $i++) { + my @lats = sort { $a <=> $b } @{$lag[$i]}; + $totcount = $#lats + 1; + my $thislat = 0.0; + foreach my $lat (@lats) { + $thislat++; + my $pct = 100.0 * $thislat / $totcount; + $cdf{$lat}{$i} = $pct; + } + } + + my %cur_cdf_value = (); + for (my $i = 0; $i < $num_traces; $i++) { + $cur_cdf_value{$i} = 0; + } + + foreach my $lat (sort { $a <=> $b } keys %cdf) { + foreach my $i (keys %{$cdf{$lat}}) { + $cur_cdf_value{$i} = $cdf{$lat}{$i}; + } + print "$lat"; + for (my $i = 0; $i < $num_traces; $i++) { + print " ", $cur_cdf_value{$i}; + } + print "\n"; + } +} else { + print "Unknown analysis type: $type\n"; +} + +sub find_floor_idx_in_seq { + my ($ts, @seq) = @_; + + my $lastidx = -1; + my $idx = 0; + foreach my $s (@seq) { + last if $s >= $ts; + $lastidx = $idx; + $idx++; + } + return $lastidx; +} + diff -u -Pr tightvnc-1.2.9/vncplay vncplay-1.0/vncplay --- tightvnc-1.2.9/vncplay 1969-12-31 16:00:00.000000000 -0800 +++ vncplay-1.0/vncplay 2005-04-10 21:52:01.000000000 -0700 @@ -0,0 +1,119 @@ +#!/usr/bin/perl +# +# Front-end wrapper script for VNCplay + +use File::Basename; +my $tooldir = dirname($0); + +my %options = (); +my @args = (); +while ($#ARGV >= 0) { + my $arg = shift @ARGV; + if ($arg =~ /^-/) { + my $val = shift @ARGV; + $options{$arg} = $val; + } else { + push @args, $arg; + } +} + +my $op = shift @args; +if (!(defined $op)) { + print + "Usage: vncplay [options] command ...\n", + " commands: record Record a VNC session\n", + " play Play back a VNC session\n", + " autoplay Playback without display\n", + " analyze Analyze replayed sessions\n", + " options: -pwfile filename Read password from a file\n"; + exit(1); +} + +$ENV{CLASSPATH} .= ":" . $tooldir; +my @vncviewer_args = ( "encoding", "hextile" ); + +if ($op eq 'record' || $op eq 'play' || $op eq 'autoplay') { + my $server = shift @args; + die "Usage: vncplay (record|play|autoplay) host:port ...\n" unless defined $server; + my ($host, $port) = split /:/, $server; + push @vncviewer_args, "host", $host, "port", $port; +} + +if (defined $options{'-pwfile'}) { + my $pwfile = $options{'-pwfile'}; + my $pwfd; + open($pwfd, $pwfile) || die "Cannot open password file $pwfile\n"; + my $pw = <$pwfd>; + close($pwfd); + + $pw =~ s/[\r\n]*$//; + push @vncviewer_args, "password", $pw; +} + +if ($op eq 'record') { + my ($tracefile) = @args; + die "Usage: vncplay record host:port tracefile\n" unless defined $tracefile; + print "Recording into trace log file $tracefile\n"; + + system("java", "VncViewer", @vncviewer_args, + "autorecord", "yes", + "tracefile", $tracefile); +} elsif ($op eq 'play' || $op eq 'autoplay') { + my ($tracefile, $logfile) = @args; + die "Usage: vncplay (play|autoplay) host:port tracefile vnclogfile\n" unless defined $tracefile && defined $logfile; + print "Playing back trace from $tracefile\n"; + + my $xvncpid = -1; + if ($op eq 'autoplay') { + $xvncpid = start_xvnc(); + } + + system("java", "VncViewer", @vncviewer_args, + "autoplay", "yes", + "tracefile", $tracefile, + "recordfile", $logfile); + kill 15, $xvncpid if $xvncpid > 0; +} elsif ($op eq 'analyze') { + my (@rfbfiles) = @args; + die "Usage: vncplay analyze vnclogfile1 vnclogfile2 ...\n" unless $#rfbfiles >= 1; + my $xvncpid = start_xvnc(); + + my @analyzer_args = (); + foreach my $vnclog (@rfbfiles) { + push @analyzer_args, $vnclog; + push @analyzer_args, $vnclog . ".aux"; + } + + system("java", "-Xmx1700m", "VncAnalyzer", @analyzer_args); + + kill 15, $xvncpid; +} else { + print "Unknown operation $op\n"; + exit(1); +} + +exit(0); + +sub start_xvnc { + my $d; + for ($d = 20; $d < 100; $d++) { + next if -e "/tmp/.X${d}-lock"; + next if -e "/tmp/.X11-unix/X${d}"; + + $ENV{DISPLAY}=":${d}.0"; + last; + } + + my $xvncpid = fork(); + die "Cannot fork" if $xvncpid < 0; + if ($xvncpid == 0) { + exec "Xvnc", "-auth", "/dev/null", + "-nolisten", "tcp", + $ENV{DISPLAY}; + die "Cannot start Xvnc -- is it installed?\n"; + } + + sleep 1; + system("xhost >/dev/null 2>&1") and die "Cannot connect to Xvnc\n"; + return $xvncpid; +}