/* Copyright (C) 2002-2005 RealVNC Ltd. All Rights Reserved. * Copyright 2009-2013 Pierre Ossman for Cendio AB * Copyright (C) 2011-2013 D. R. Commander. All Rights Reserved. * Copyright (C) 2011-2015 Brian P. Hinz * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. */ // // CConn // // Methods on CConn are called from both the GUI thread and the thread which // processes incoming RFB messages ("the RFB thread"). This means we need to // be careful with synchronization here. // // Any access to writer() must not only be synchronized, but we must also make // sure that the connection is in RFBSTATE_NORMAL. We are guaranteed this for // any code called after serverInit() has been called. Since the DesktopWindow // isn't created until then, any methods called only from DesktopWindow can // assume that we are in RFBSTATE_NORMAL. package com.tigervnc.vncviewer; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.event.*; import java.awt.Toolkit; import java.io.IOException; import java.io.InputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.util.jar.Attributes; import java.util.jar.Manifest; import javax.swing.*; import javax.swing.ImageIcon; import java.net.InetSocketAddress; import java.net.SocketException; import java.util.*; import java.util.prefs.*; import com.tigervnc.rdr.*; import com.tigervnc.rfb.*; import com.tigervnc.rfb.Point; import com.tigervnc.rfb.Exception; import com.tigervnc.network.Socket; import com.tigervnc.network.TcpSocket; import static com.tigervnc.vncviewer.Parameters.*; public class CConn extends CConnection implements UserPasswdGetter, FdInStreamBlockCallback, ActionListener { // 8 colours (1 bit per component) static final PixelFormat verylowColorPF = new PixelFormat(8, 3, false, true, 1, 1, 1, 2, 1, 0); // 64 colours (2 bits per component) static final PixelFormat lowColorPF = new PixelFormat(8, 6, false, true, 3, 3, 3, 4, 2, 0); // 256 colours (2-3 bits per component) static final PixelFormat mediumColorPF = new PixelFormat(8, 8, false, true, 7, 7, 3, 5, 2, 0); static final int KEY_LOC_SHIFT_R = 0; static final int KEY_LOC_SHIFT_L = 16; static final int SUPER_MASK = 1<<15; //////////////////////////////////////////////////////////////////// // The following methods are all called from the RFB thread public CConn(String vncServerName, Socket socket) { serverHost = null; serverPort = 0; desktop = null; pendingPFChange = false; currentEncoding = Encodings.encodingTight; lastServerEncoding = -1; formatChange = false; encodingChange = false; firstUpdate = true; pendingUpdate = false; continuousUpdates = false; forceNonincremental = true; supportsSyncFence = false; setShared(shared.getValue()); sock = socket; downKeySym = new HashMap(); upg = this; int encNum = Encodings.encodingNum(preferredEncoding.getValue()); if (encNum != -1) currentEncoding = encNum; cp.supportsLocalCursor = true; cp.supportsDesktopResize = true; cp.supportsExtendedDesktopSize = true; cp.supportsDesktopRename = true; cp.supportsSetDesktopSize = false; cp.supportsClientRedirect = true; if (customCompressLevel.getValue()) cp.compressLevel = compressLevel.getValue(); else cp.compressLevel = -1; if (!noJpeg.getValue()) cp.qualityLevel = qualityLevel.getValue(); else cp.qualityLevel = -1; if (sock == null) { setServerName(Hostname.getHost(vncServerName)); setServerPort(Hostname.getPort(vncServerName)); try { if (tunnel.getValue() || !via.getValue().isEmpty()) { int localPort = TcpSocket.findFreeTcpPort(); if (localPort == 0) throw new Exception("Could not obtain free TCP port"); Tunnel.createTunnel(this, localPort); sock = new TcpSocket("localhost", localPort); } else { sock = new TcpSocket(getServerName(), getServerPort()); } } catch (java.lang.Exception e) { throw new Exception(e.getMessage()); } vlog.info("connected to host "+getServerName()+" port "+getServerPort()); } else { String name = sock.getPeerEndpoint(); if (listenMode.getValue()) vlog.info("Accepted connection from " + name); else vlog.info("connected to host "+Hostname.getHost(name)+" port "+Hostname.getPort(name)); } // See callback below sock.inStream().setBlockCallback(this); setStreams(sock.inStream(), sock.outStream()); initialiseProtocol(); OptionsDialog.addCallback("handleOptions", this); } public void refreshFramebuffer() { forceNonincremental = true; // Without fences, we cannot safely trigger an update request directly // but must wait for the next update to arrive. if (supportsSyncFence) requestNewUpdate(); } public String connectionInfo() { String info = new String("Desktop name: %s%n"+ "Host: %s:%d%n"+ "Size: %dx%d%n"+ "Pixel format: %s%n"+ " (server default: %s)%n"+ "Requested encoding: %s%n"+ "Last used encoding: %s%n"+ "Line speed estimate: %d kbit/s%n"+ "Protocol version: %d.%d%n"+ "Security method: %s [%s]%n"); String infoText = String.format(info, cp.name(), sock.getPeerName(), sock.getPeerPort(), cp.width, cp.height, cp.pf().print(), serverPF.print(), Encodings.encodingName(currentEncoding), Encodings.encodingName(lastServerEncoding), sock.inStream().kbitsPerSecond(), cp.majorVersion, cp.minorVersion, Security.secTypeName(csecurity.getType()), csecurity.description()); return infoText; } // The RFB core is not properly asynchronous, so it calls this callback // whenever it needs to block to wait for more data. Since FLTK is // monitoring the socket, we just make sure FLTK gets to run. public void blockCallback() { try { synchronized(this) { wait(1); } } catch (java.lang.InterruptedException e) { throw new Exception(e.getMessage()); } } // getUserPasswd() is called by the CSecurity object when it needs us to read // a password from the user. public final boolean getUserPasswd(StringBuffer user, StringBuffer passwd) { String title = ("VNC Authentication [" +csecurity.description() + "]"); String passwordFileStr = passwordFile.getValue(); PasswdDialog dlg; if (user == null && !passwordFileStr.equals("")) { InputStream fp = null; try { fp = new FileInputStream(passwordFileStr); } catch(FileNotFoundException e) { throw new Exception("Opening password file failed"); } byte[] obfPwd = new byte[256]; try { fp.read(obfPwd); fp.close(); } catch(IOException e) { throw new Exception("Failed to read VncPasswd file"); } String PlainPasswd = VncAuth.unobfuscatePasswd(obfPwd); passwd.append(PlainPasswd); passwd.setLength(PlainPasswd.length()); return true; } if (user == null) { dlg = new PasswdDialog(title, (user == null), (passwd == null)); } else { if ((passwd == null) && sendLocalUsername.getValue()) { user.append((String)System.getProperties().get("user.name")); return true; } dlg = new PasswdDialog(title, sendLocalUsername.getValue(), (passwd == null)); } dlg.showDialog(); if (user != null) { if (sendLocalUsername.getValue()) { user.append((String)System.getProperties().get("user.name")); } else { user.append(dlg.userEntry.getText()); } } if (passwd != null) passwd.append(new String(dlg.passwdEntry.getPassword())); return true; } ////////////////////// CConnection callback methods ////////////////////// // serverInit() is called when the serverInit message has been received. At // this point we create the desktop window and display it. We also tell the // server the pixel format and encodings to use and request the first update. public void serverInit() { super.serverInit(); // If using AutoSelect with old servers, start in FullColor // mode. See comment in autoSelectFormatAndEncoding. if (cp.beforeVersion(3, 8) && autoSelect.getValue()) fullColor.setParam(true); serverPF = cp.pf(); desktop = new DesktopWindow(cp.width, cp.height, cp.name(), serverPF, this); fullColorPF = desktop.getPreferredPF(); // Force a switch to the format and encoding we'd like formatChange = true; encodingChange = true; // And kick off the update cycle requestNewUpdate(); // This initial update request is a bit of a corner case, so we need // to help out setting the correct format here. assert(pendingPFChange); cp.setPF(pendingPF); pendingPFChange = false; } // setDesktopSize() is called when the desktop size changes (including when // it is set initially). public void setDesktopSize(int w, int h) { super.setDesktopSize(w, h); resizeFramebuffer(); } // setExtendedDesktopSize() is a more advanced version of setDesktopSize() public void setExtendedDesktopSize(int reason, int result, int w, int h, ScreenSet layout) { super.setExtendedDesktopSize(reason, result, w, h, layout); if ((reason == screenTypes.reasonClient) && (result != screenTypes.resultSuccess)) { vlog.error("SetDesktopSize failed: " + result); return; } resizeFramebuffer(); } // clientRedirect() migrates the client to another host/port public void clientRedirect(int port, String host, String x509subject) { try { sock.close(); sock = new TcpSocket(host, port); vlog.info("Redirected to "+host+":"+port); setServerName(host); setServerPort(port); sock.inStream().setBlockCallback(this); setStreams(sock.inStream(), sock.outStream()); if (desktop != null) desktop.dispose(); initialiseProtocol(); } catch (java.lang.Exception e) { throw new Exception(e.getMessage()); } } // setName() is called when the desktop name changes public void setName(String name) { super.setName(name); if (desktop != null) desktop.setName(name); } // framebufferUpdateStart() is called at the beginning of an update. // Here we try to send out a new framebuffer update request so that the // next update can be sent out in parallel with us decoding the current // one. public void framebufferUpdateStart() { ModifiablePixelBuffer pb; PlatformPixelBuffer ppb; super.framebufferUpdateStart(); // Note: This might not be true if sync fences are supported pendingUpdate = false; requestNewUpdate(); // We might still be rendering the previous update pb = getFramebuffer(); assert(pb != null); ppb = (PlatformPixelBuffer)pb; assert(ppb != null); //FIXME } // framebufferUpdateEnd() is called at the end of an update. // For each rectangle, the FdInStream will have timed the speed // of the connection, allowing us to select format and encoding // appropriately, and then request another incremental update. public void framebufferUpdateEnd() { super.framebufferUpdateEnd(); desktop.updateWindow(); if (firstUpdate) { // We need fences to make extra update requests and continuous // updates "safe". See fence() for the next step. if (cp.supportsFence) writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext, 0, null); firstUpdate = false; } // A format change has been scheduled and we are now past the update // with the old format. Time to active the new one. if (pendingPFChange) { cp.setPF(pendingPF); pendingPFChange = false; } // Compute new settings based on updated bandwidth values if (autoSelect.getValue()) autoSelectFormatAndEncoding(); } // The rest of the callbacks are fairly self-explanatory... public void setColourMapEntries(int firstColor, int nColors, int[] rgbs) { vlog.error("Invalid SetColourMapEntries from server!"); } public void bell() { if (acceptBell.getValue()) desktop.getToolkit().beep(); } public void serverCutText(String str, int len) { StringSelection buffer; if (!acceptClipboard.getValue()) return; ClipboardDialog.serverCutText(str); } public void dataRect(Rect r, int encoding) { sock.inStream().startTiming(); if (encoding != Encodings.encodingCopyRect) lastServerEncoding = encoding; super.dataRect(r, encoding); sock.inStream().stopTiming(); } public void setCursor(int width, int height, Point hotspot, byte[] data) { desktop.setCursor(width, height, hotspot, data); } public void fence(int flags, int len, byte[] data) { // can't call super.super.fence(flags, len, data); cp.supportsFence = true; if ((flags & fenceTypes.fenceFlagRequest) != 0) { // We handle everything synchronously so we trivially honor these modes flags = flags & (fenceTypes.fenceFlagBlockBefore | fenceTypes.fenceFlagBlockAfter); writer().writeFence(flags, len, data); return; } if (len == 0) { // Initial probe if ((flags & fenceTypes.fenceFlagSyncNext) != 0) { supportsSyncFence = true; if (cp.supportsContinuousUpdates) { vlog.info("Enabling continuous updates"); continuousUpdates = true; writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height); } } } else { // Pixel format change MemInStream memStream = new MemInStream(data, 0, len); PixelFormat pf = new PixelFormat(); pf.read(memStream); cp.setPF(pf); } } ////////////////////// Internal methods ////////////////////// private void resizeFramebuffer() { if (desktop == null) return; if (continuousUpdates) writer().writeEnableContinuousUpdates(true, 0, 0, cp.width, cp.height); desktop.resizeFramebuffer(cp.width, cp.height); } // autoSelectFormatAndEncoding() chooses the format and encoding appropriate // to the connection speed: // // First we wait for at least one second of bandwidth measurement. // // Above 16Mbps (i.e. LAN), we choose the second highest JPEG quality, // which should be perceptually lossless. // // If the bandwidth is below that, we choose a more lossy JPEG quality. // // If the bandwidth drops below 256 Kbps, we switch to palette mode. // // Note: The system here is fairly arbitrary and should be replaced // with something more intelligent at the server end. // private void autoSelectFormatAndEncoding() { long kbitsPerSecond = sock.inStream().kbitsPerSecond(); long timeWaited = sock.inStream().timeWaited(); boolean newFullColor = fullColor.getValue(); int newQualityLevel = qualityLevel.getValue(); // Always use Tight if (currentEncoding != Encodings.encodingTight) { currentEncoding = Encodings.encodingTight; encodingChange = true; } // Check that we have a decent bandwidth measurement if ((kbitsPerSecond == 0) || (timeWaited < 100)) return; // Select appropriate quality level if (!noJpeg.getValue()) { if (kbitsPerSecond > 16000) newQualityLevel = 8; else newQualityLevel = 6; if (newQualityLevel != qualityLevel.getValue()) { vlog.info("Throughput "+kbitsPerSecond+ " kbit/s - changing to quality "+newQualityLevel); cp.qualityLevel = newQualityLevel; qualityLevel.setParam(newQualityLevel); encodingChange = true; } } if (cp.beforeVersion(3, 8)) { // Xvnc from TightVNC 1.2.9 sends out FramebufferUpdates with // cursors "asynchronously". If this happens in the middle of a // pixel format change, the server will encode the cursor with // the old format, but the client will try to decode it // according to the new format. This will lead to a // crash. Therefore, we do not allow automatic format change for // old servers. return; } // Select best color level newFullColor = (kbitsPerSecond > 256); if (newFullColor != fullColor.getValue()) { vlog.info("Throughput "+kbitsPerSecond+ " kbit/s - full color is now "+ (newFullColor ? "enabled" : "disabled")); fullColor.setParam(newFullColor); formatChange = true; } } // checkEncodings() sends a setEncodings message if one is needed. private void checkEncodings() { if (encodingChange && (writer() != null)) { vlog.info("Using " + Encodings.encodingName(currentEncoding) + " encoding"); writer().writeSetEncodings(currentEncoding, true); encodingChange = false; } } // requestNewUpdate() requests an update from the server, having set the // format and encoding appropriately. private void requestNewUpdate() { if (formatChange) { PixelFormat pf; /* Catch incorrect requestNewUpdate calls */ assert(!pendingUpdate || supportsSyncFence); if (fullColor.getValue()) { pf = fullColorPF; } else { if (lowColorLevel.getValue() == 0) { pf = verylowColorPF; } else if (lowColorLevel.getValue() == 1) { pf = lowColorPF; } else { pf = mediumColorPF; } } if (supportsSyncFence) { // We let the fence carry the pixel format and switch once we // get the response back. That way we will be synchronised with // when the server switches. MemOutStream memStream = new MemOutStream(); pf.write(memStream); writer().writeFence(fenceTypes.fenceFlagRequest | fenceTypes.fenceFlagSyncNext, memStream.length(), (byte[])memStream.data()); } else { // New requests are sent out at the start of processing the last // one, so we cannot switch our internal format right now (doing so // would mean misdecoding the current update). pendingPFChange = true; pendingPF = pf; } String str = pf.print(); vlog.info("Using pixel format " + str); writer().writeSetPixelFormat(pf); formatChange = false; } checkEncodings(); if (forceNonincremental || !continuousUpdates) { pendingUpdate = true; writer().writeFramebufferUpdateRequest(new Rect(0, 0, cp.width, cp.height), !forceNonincremental); } forceNonincremental = false; } public void handleOptions() { // Checking all the details of the current set of encodings is just // a pain. Assume something has changed, as resending the encoding // list is cheap. Avoid overriding what the auto logic has selected // though. if (!autoSelect.getValue()) { int encNum = Encodings.encodingNum(preferredEncoding.getValue()); if (encNum != -1) this.currentEncoding = encNum; } this.cp.supportsLocalCursor = true; if (customCompressLevel.getValue()) this.cp.compressLevel = compressLevel.getValue(); else this.cp.compressLevel = -1; if (!noJpeg.getValue() && !autoSelect.getValue()) this.cp.qualityLevel = qualityLevel.getValue(); else this.cp.qualityLevel = -1; this.encodingChange = true; // Format changes refreshes the entire screen though and are therefore // very costly. It's probably worth the effort to see if it is necessary // here. PixelFormat pf; if (fullColor.getValue()) { pf = fullColorPF; } else { if (lowColorLevel.getValue() == 0) pf = verylowColorPF; else if (lowColorLevel.getValue() == 1) pf = lowColorPF; else pf = mediumColorPF; } if (!pf.equal(this.cp.pf())) { this.formatChange = true; // Without fences, we cannot safely trigger an update request directly // but must wait for the next update to arrive. if (this.supportsSyncFence) this.requestNewUpdate(); } } //////////////////////////////////////////////////////////////////// // The following methods are all called from the GUI thread // close() shuts down the socket, thus waking up the RFB thread. public void close() { if (closeListener != null) { embed.setParam(true); JFrame f = (JFrame)SwingUtilities.getAncestorOfClass(JFrame.class, desktop); if (f != null) f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING)); } shuttingDown = true; try { if (sock != null) sock.shutdown(); } catch (java.lang.Exception e) { throw new Exception(e.getMessage()); } } void showInfo() { Window fullScreenWindow = DesktopWindow.getFullScreenWindow(); if (fullScreenWindow != null) DesktopWindow.setFullScreenWindow(null); String info = new String("Desktop name: %s%n"+ "Host: %s:%d%n"+ "Size: %dx%d%n"+ "Pixel format: %s%n"+ " (server default: %s)%n"+ "Requested encoding: %s%n"+ "Last used encoding: %s%n"+ "Line speed estimate: %d kbit/s%n"+ "Protocol version: %d.%d%n"+ "Security method: %s [%s]%n"); String msg = String.format(info, cp.name(), sock.getPeerName(), sock.getPeerPort(), cp.width, cp.height, cp.pf().print(), serverPF.print(), Encodings.encodingName(currentEncoding), Encodings.encodingName(lastServerEncoding), sock.inStream().kbitsPerSecond(), cp.majorVersion, cp.minorVersion, Security.secTypeName(csecurity.getType()), csecurity.description()); JOptionPane op = new JOptionPane(msg, JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION); JDialog dlg = op.createDialog(desktop, "VNC connection info"); dlg.setIconImage(VncViewer.frameIcon); dlg.setAlwaysOnTop(true); dlg.setVisible(true); if (fullScreenWindow != null) DesktopWindow.setFullScreenWindow(fullScreenWindow); } public void refresh() { writer().writeFramebufferUpdateRequest(new Rect(0,0,cp.width,cp.height), false); pendingUpdate = true; } // writeClientCutText() is called from the clipboard dialog public void writeClientCutText(String str, int len) { if (state() != RFBSTATE_NORMAL || shuttingDown) return; writer().writeClientCutText(str, len); } public void writeKeyEvent(int keysym, boolean down) { if (state() != RFBSTATE_NORMAL || shuttingDown) return; writer().writeKeyEvent(keysym, down); } public void writeKeyEvent(KeyEvent ev) { if (viewOnly.getValue() || shuttingDown) return; boolean down = (ev.getID() == KeyEvent.KEY_PRESSED); int keySym, keyCode = ev.getKeyCode(); // If neither the keyCode or keyChar are defined, then there's // really nothing that we can do with this. The fn key on OS-X // fires events like this when pressed but does not fire a // corresponding release event. if (keyCode == 0 && ev.getKeyChar() == KeyEvent.CHAR_UNDEFINED) return; if (!down) { Integer iter = downKeySym.get(keyCode); if (iter == null) { // Note that dead keys will raise this sort of error falsely // See https://bugs.openjdk.java.net/browse/JDK-6534883 vlog.debug("Unexpected key release of keyCode "+keyCode); String fmt = ev.paramString().replaceAll("%","%%"); vlog.debug(String.format(fmt.replaceAll(",","%n "))); return; } vlog.debug(String.format("Key released: 0x%04x => 0x%04x", keyCode, iter)); writeKeyEvent(iter, false); downKeySym.remove(keyCode); return; } keySym = Keysyms.translateKeyEvent(ev); if (keySym == Keysyms.VoidSymbol) return; boolean need_cheat = true; if (VncViewer.os.startsWith("windows")) { // Windows doesn't have a proper AltGr, but handles it using fake // Ctrl+Alt. Unfortunately X11 doesn't generally like the combination // Ctrl+Alt+AltGr, which we usually end up with when Xvnc tries to // get everything in the correct state. Cheat and temporarily release // Ctrl and Alt whenever we get a key with a symbol. if (KeyEvent.getKeyText(keyCode).isEmpty()) need_cheat = false; else if (!downKeySym.containsValue(Keysyms.Control_L) && !downKeySym.containsValue(Keysyms.Control_R)) need_cheat = false; else if (!downKeySym.containsValue(Keysyms.Alt_L) && !downKeySym.containsValue(Keysyms.Alt_R)) need_cheat = false; if (need_cheat) { vlog.info("Faking release of AltGr (Ctrl+Alt)"); if (downKeySym.containsValue(Keysyms.Control_L)) writeKeyEvent(Keysyms.Control_L, false); if (downKeySym.containsValue(Keysyms.Control_R)) writeKeyEvent(Keysyms.Control_R, false); if (downKeySym.containsValue(Keysyms.Alt_L)) writeKeyEvent(Keysyms.Alt_L, false); if (downKeySym.containsValue(Keysyms.Alt_R)) writeKeyEvent(Keysyms.Alt_R, false); } } vlog.debug(String.format("Key pressed: 0x%04x '%s' => 0x%04x", keyCode, Character.toString(ev.getKeyChar()), keySym)); downKeySym.put(keyCode, keySym); writeKeyEvent(keySym, down); if (VncViewer.os.startsWith("windows")) { if (need_cheat) { vlog.debug("Restoring AltGr state"); if (downKeySym.containsValue(Keysyms.Control_L)) writeKeyEvent(Keysyms.Control_L, true); if (downKeySym.containsValue(Keysyms.Control_R)) writeKeyEvent(Keysyms.Control_R, true); if (downKeySym.containsValue(Keysyms.Alt_L)) writeKeyEvent(Keysyms.Alt_L, true); if (downKeySym.containsValue(Keysyms.Alt_R)) writeKeyEvent(Keysyms.Alt_R, true); } } } public void writePointerEvent(MouseEvent ev) { if (state() != RFBSTATE_NORMAL || shuttingDown) return; switch (ev.getID()) { case MouseEvent.MOUSE_PRESSED: buttonMask = 1; if ((ev.getModifiers() & KeyEvent.ALT_MASK) != 0) buttonMask = 2; if ((ev.getModifiers() & KeyEvent.META_MASK) != 0) buttonMask = 4; break; case MouseEvent.MOUSE_RELEASED: buttonMask = 0; break; } writer().writePointerEvent(new Point(ev.getX(), ev.getY()), buttonMask); } public void writeWheelEvent(MouseWheelEvent ev) { if (state() != RFBSTATE_NORMAL || shuttingDown) return; int x, y; int clicks = ev.getWheelRotation(); if (clicks < 0) { buttonMask = 8; } else { buttonMask = 16; } for (int i = 0; i < Math.abs(clicks); i++) { x = ev.getX(); y = ev.getY(); writer().writePointerEvent(new Point(x, y), buttonMask); buttonMask = 0; writer().writePointerEvent(new Point(x, y), buttonMask); } } synchronized void releaseDownKeys() { for (Map.Entry entry : downKeySym.entrySet()) writeKeyEvent(entry.getValue(), false); downKeySym.clear(); } // this is a special ActionListener passed in by the // Java Plug-in software to control applet's close behavior public void setCloseListener(ActionListener cl) { closeListener = cl; } public void actionPerformed(ActionEvent e) {} public Socket getSocket() { return sock; } //////////////////////////////////////////////////////////////////// // The following methods are called from both RFB and GUI threads // the following never change so need no synchronization: // access to desktop by different threads is specified in DesktopWindow // the following need no synchronization: public static UserPasswdGetter upg; // shuttingDown is set by the GUI thread and only ever tested by the RFB // thread after the window has been destroyed. boolean shuttingDown = false; // reading and writing int and boolean is atomic in java, so no // synchronization of the following flags is needed: // All menu, options, about and info stuff is done in the GUI thread (apart // from when constructed). // the following are only ever accessed by the GUI thread: int buttonMask; private String serverHost; private int serverPort; private Socket sock; protected DesktopWindow desktop; private PixelFormat serverPF; private PixelFormat fullColorPF; private boolean pendingPFChange; private PixelFormat pendingPF; private int currentEncoding, lastServerEncoding; private boolean formatChange; private boolean encodingChange; private boolean firstUpdate; private boolean pendingUpdate; private boolean continuousUpdates; private boolean forceNonincremental; private boolean supportsSyncFence; private HashMap downKeySym; public ActionListener closeListener = null; static LogWriter vlog = new LogWriter("CConn"); }