package com.cloud.consoleproxy; import java.awt.Image; import java.awt.Rectangle; import java.util.List; import org.apache.log4j.Logger; import com.cloud.consoleproxy.util.TileInfo; import com.cloud.consoleproxy.util.TileTracker; import com.cloud.consoleproxy.vnc.FrameBufferCanvas; /** * * @author Kelven Yang * * ConsoleProxyClientBase implements a base console client class that can be extended to realize * an instance of specialized console protocol implementation, such as VNC or RDP * * It mainly implements the features needed by front-end AJAX viewer * */ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, ConsoleProxyClientListener { private static final Logger s_logger = Logger.getLogger(ConsoleProxyClientBase.class); private static int s_nextClientId = 0; protected int clientId = getNextClientId(); protected long ajaxSessionId = 0; protected boolean dirtyFlag = false; protected Object tileDirtyEvent = new Object(); protected TileTracker tracker; protected AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2); protected String host; protected int port; protected String passwordParam; protected String tag = ""; protected String ticket = ""; protected long createTime = System.currentTimeMillis(); protected long lastFrontEndActivityTime = System.currentTimeMillis(); protected boolean framebufferResized = false; protected int resizedFramebufferWidth; protected int resizedFramebufferHeight; public ConsoleProxyClientBase() { tracker = new TileTracker(); tracker.initTracking(64, 64, 800, 600); } // // interface ConsoleProxyClient // @Override public int getClientId() { return clientId; } public abstract boolean isHostConnected(); public abstract boolean isFrontEndAlive(); @Override public long getAjaxSessionId() { return this.ajaxSessionId; } @Override public AjaxFIFOImageCache getAjaxImageCache() { return ajaxImageCache; } public Image getClientScaledImage(int width, int height) { FrameBufferCanvas canvas = getFrameBufferCavas(); if(canvas != null) return canvas.getFrameBufferScaledImage(width, height); return null; } public abstract void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); public abstract void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers); @Override public long getClientCreateTime() { return createTime; } @Override public long getClientLastFrontEndActivityTime() { return lastFrontEndActivityTime; } @Override public String getClientHostAddress() { return host; } @Override public int getClientHostPort() { return port; } @Override public String getClientHostPassword() { return passwordParam; } @Override public String getClientTag() { return tag; } @Override public abstract void initClient(String clientHostAddress, int clientHostPort, String clientHostPassword, String clientTag, String ticket); @Override public abstract void closeClient(); // // interface FrameBufferEventListener // @Override public void onFramebufferSizeChange(int w, int h) { tracker.resize(w, h); synchronized(this) { framebufferResized = true; resizedFramebufferWidth = w; resizedFramebufferHeight = h; } signalTileDirtyEvent(); } @Override public void onFramebufferUpdate(int x, int y, int w, int h) { if(s_logger.isTraceEnabled()) s_logger.trace("Frame buffer update {" + x + "," + y + "," + w + "," + h + "}"); tracker.invalidate(new Rectangle(x, y, w, h)); signalTileDirtyEvent(); } // // AJAX Image manipulation // public byte[] getFrameBufferJpeg() { FrameBufferCanvas canvas = getFrameBufferCavas(); if(canvas != null) return canvas.getFrameBufferJpeg(); return null; } public byte[] getTilesMergedJpeg(List tileList, int tileWidth, int tileHeight) { FrameBufferCanvas canvas = getFrameBufferCavas(); if(canvas != null) return canvas.getTilesMergedJpeg(tileList, tileWidth, tileHeight); return null; } private String prepareAjaxImage(List tiles, boolean init) { byte[] imgBits; if(init) imgBits = getFrameBufferJpeg(); else imgBits = getTilesMergedJpeg(tiles, tracker.getTileWidth(), tracker.getTileHeight()); if(imgBits == null) { s_logger.warn("Unable to generate jpeg image"); } else { if(s_logger.isTraceEnabled()) s_logger.trace("Generated jpeg image size: " + imgBits.length); } int key = ajaxImageCache.putImage(imgBits); StringBuffer sb = new StringBuffer("/ajaximg?host="); sb.append(host).append("&port=").append(port).append("&sid=").append(passwordParam); sb.append("&key=").append(key).append("&ts=").append(System.currentTimeMillis()); return sb.toString(); } private String prepareAjaxSession(boolean init) { if(init) { synchronized(this) { ajaxSessionId++; } } StringBuffer sb = new StringBuffer(); sb.append("/ajax?host=").append(host).append("&port=").append(port); sb.append("&sid=").append(passwordParam).append("&sess=").append(ajaxSessionId); return sb.toString(); } @Override public String onAjaxClientKickoff() { return "onKickoff();"; } private boolean waitForViewerReady() { long startTick = System.currentTimeMillis(); while(System.currentTimeMillis() - startTick < 5000) { if(getFrameBufferCavas() != null) return true; try { Thread.sleep(100); } catch (InterruptedException e) { } } return false; } private String onAjaxClientConnectFailed() { return "

" + "Unable to start console session as connection is refused by the machine you are accessing" + "

"; } @Override public String onAjaxClientStart(String title, List languages, String guest) { updateFrontEndActivityTime(); if(!waitForViewerReady()) return onAjaxClientConnectFailed(); synchronized(this) { ajaxSessionId++; framebufferResized = false; } int tileWidth = tracker.getTileWidth(); int tileHeight = tracker.getTileHeight(); int width = tracker.getTrackWidth(); int height = tracker.getTrackHeight(); if(s_logger.isTraceEnabled()) s_logger.trace("Ajax client start, frame buffer w: " + width + ", " + height); int retry = 0; tracker.initCoverageTest(); while(!tracker.hasFullCoverage() && retry < 10) { try { Thread.sleep(1000); } catch (InterruptedException e) { } retry++; } List tiles = tracker.scan(true); String imgUrl = prepareAjaxImage(tiles, true); String updateUrl = prepareAjaxSession(true); StringBuffer sbTileSequence = new StringBuffer(); int i = 0; for(TileInfo tile : tiles) { sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); if(i < tiles.size() - 1) sbTileSequence.append(","); i++; } return getAjaxViewerPageContent(sbTileSequence.toString(), imgUrl, updateUrl, width, height, tileWidth, tileHeight, title, ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW, languages, guest); } private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width, int height, int tileWidth, int tileHeight, String title, boolean rawKeyboard, List languages, String guest) { StringBuffer sbLanguages = new StringBuffer(""); if(languages != null) { for(String lang : languages) { if(sbLanguages.length() > 0) { sbLanguages.append(","); } sbLanguages.append(lang); } } String[] content = new String[] { "", "", "", "", "", "", "", "", "" + title + "", "", "", "
", "", "", "
", "
", "", "", "" }; StringBuffer sb = new StringBuffer(); for(int i = 0; i < content.length; i++) sb.append(content[i]); return sb.toString(); } public String onAjaxClientDisconnected() { return "onDisconnect();"; } @Override public String onAjaxClientUpdate() { updateFrontEndActivityTime(); if(!waitForViewerReady()) return onAjaxClientDisconnected(); synchronized(tileDirtyEvent) { if(!dirtyFlag) { try { tileDirtyEvent.wait(3000); } catch(InterruptedException e) { } } } boolean doResize = false; synchronized(this) { if(framebufferResized) { framebufferResized = false; doResize = true; } } List tiles; if(doResize) tiles = tracker.scan(true); else tiles = tracker.scan(false); dirtyFlag = false; String imgUrl = prepareAjaxImage(tiles, false); StringBuffer sbTileSequence = new StringBuffer(); int i = 0; for(TileInfo tile : tiles) { sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]"); if(i < tiles.size() - 1) sbTileSequence.append(","); i++; } return getAjaxViewerUpdatePageContent(sbTileSequence.toString(), imgUrl, doResize, resizedFramebufferWidth, resizedFramebufferHeight, tracker.getTileWidth(), tracker.getTileHeight()); } private String getAjaxViewerUpdatePageContent(String tileSequence, String imgUrl, boolean resized, int width, int height, int tileWidth, int tileHeight) { String[] content = new String[] { "tileMap = [ " + tileSequence + " ];", resized ? "ajaxViewer.resize('main_panel', " + width + ", " + height + " , " + tileWidth + ", " + tileHeight + ");" : "", "ajaxViewer.refresh('" + imgUrl + "', tileMap, false);" }; StringBuffer sb = new StringBuffer(); for(int i = 0; i < content.length; i++) sb.append(content[i]); return sb.toString(); } // // Helpers // private synchronized static int getNextClientId() { return ++s_nextClientId; } private void signalTileDirtyEvent() { synchronized(tileDirtyEvent) { dirtyFlag = true; tileDirtyEvent.notifyAll(); } } public void updateFrontEndActivityTime() { lastFrontEndActivityTime = System.currentTimeMillis(); } protected abstract FrameBufferCanvas getFrameBufferCavas(); }