| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| 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; | |
| /** | |
| * | |
| * 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 ConsoleProxyClientParam clientParam; | |
| protected String clientToken; | |
| 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; | |
| } | |
| @Override | |
| public abstract boolean isHostConnected(); | |
| @Override | |
| public abstract boolean isFrontEndAlive(); | |
| @Override | |
| public long getAjaxSessionId() { | |
| return this.ajaxSessionId; | |
| } | |
| @Override | |
| public AjaxFIFOImageCache getAjaxImageCache() { | |
| return ajaxImageCache; | |
| } | |
| @Override | |
| public Image getClientScaledImage(int width, int height) { | |
| FrameBufferCanvas canvas = getFrameBufferCavas(); | |
| if (canvas != null) | |
| return canvas.getFrameBufferScaledImage(width, height); | |
| return null; | |
| } | |
| @Override | |
| public abstract void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers); | |
| @Override | |
| 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 clientParam.getClientHostAddress(); | |
| } | |
| @Override | |
| public int getClientHostPort() { | |
| return clientParam.getClientHostPort(); | |
| } | |
| @Override | |
| public String getClientHostPassword() { | |
| return clientParam.getClientHostPassword(); | |
| } | |
| @Override | |
| public String getClientTag() { | |
| if (clientParam.getClientTag() != null) | |
| return clientParam.getClientTag(); | |
| return ""; | |
| } | |
| @Override | |
| public abstract void initClient(ConsoleProxyClientParam param); | |
| @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<TileInfo> tileList, int tileWidth, int tileHeight) { | |
| FrameBufferCanvas canvas = getFrameBufferCavas(); | |
| if (canvas != null) | |
| return canvas.getTilesMergedJpeg(tileList, tileWidth, tileHeight); | |
| return null; | |
| } | |
| private String prepareAjaxImage(List<TileInfo> 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(); | |
| sb.append("/ajaximg?token=").append(clientToken); | |
| sb.append("&key=").append(key); | |
| sb.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?token=").append(clientToken).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 "<html><head></head><body><div id=\"main_panel\" tabindex=\"1\"><p>" | |
| + "Unable to start console session as connection is refused by the machine you are accessing" + "</p></div></body></html>"; | |
| } | |
| @Override | |
| public String onAjaxClientStart(String title, List<String> 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<TileInfo> 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, this.clientParam.getLocale()); | |
| } | |
| private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width, int height, int tileWidth, int tileHeight, String title, | |
| boolean rawKeyboard, List<String> languages, String guest, String locale) { | |
| StringBuffer sbLanguages = new StringBuffer(""); | |
| if (languages != null) { | |
| for (String lang : languages) { | |
| if (sbLanguages.length() > 0) { | |
| sbLanguages.append(","); | |
| } | |
| sbLanguages.append(lang); | |
| } | |
| } | |
| String[] content = | |
| new String[] {"<html>", "<head>", "<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/jquery.js\"></script>", | |
| "<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/cloud.logger.js\"></script>", | |
| "<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/ajaxkeys.js\"></script>", | |
| "<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/ajaxviewer.js\"></script>", | |
| "<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/handler.js\"></script>", | |
| "<link rel=\"stylesheet\" type=\"text/css\" href=\"/resource/css/ajaxviewer.css\"></link>", | |
| "<link rel=\"stylesheet\" type=\"text/css\" href=\"/resource/css/logger.css\"></link>", "<title>" + title + "</title>", "</head>", "<body>", | |
| "<div id=\"toolbar\">", "<ul>", "<li>", "<a href=\"#\" cmd=\"sendCtrlAltDel\">", | |
| "<span><img align=\"left\" src=\"/resource/images/cad.gif\" alt=\"Ctrl-Alt-Del\" />Ctrl-Alt-Del</span>", "</a>", "</li>", "<li>", | |
| "<a href=\"#\" cmd=\"sendCtrlEsc\">", | |
| "<span><img align=\"left\" src=\"/resource/images/winlog.png\" alt=\"Ctrl-Esc\" style=\"width:16px;height:16px\"/>Ctrl-Esc</span>", "</a>", "</li>", | |
| "<li class=\"pulldown\">", "<a href=\"#\">", | |
| "<span><img align=\"left\" src=\"/resource/images/winlog.png\" alt=\"Keyboard\" style=\"width:16px;height:16px\"/>Keyboard</span>", "</a>", "<ul>", | |
| "<li><a href=\"#\" cmd=\"keyboard_us\"><span>Standard (US) keyboard</span></a></li>", | |
| "<li><a href=\"#\" cmd=\"keyboard_uk\"><span>UK keyboard</span></a></li>", | |
| "<li><a href=\"#\" cmd=\"keyboard_jp\"><span>Japanese keyboard</span></a></li>", "</ul>", "</li>", "</ul>", | |
| "<span id=\"light\" class=\"dark\" cmd=\"toggle_logwin\"></span>", "</div>", "<div id=\"main_panel\" tabindex=\"1\"></div>", | |
| "<script language=\"javascript\">", "var acceptLanguages = '" + sbLanguages.toString() + "';", "var tileMap = [ " + tileSequence + " ];", | |
| "var ajaxViewer = new AjaxViewer('main_panel', '" + imgUrl + "', '" + updateUrl + "', '" + locale + "', '" + guest + "', tileMap, ", | |
| String.valueOf(width) + ", " + String.valueOf(height) + ", " + String.valueOf(tileWidth) + ", " + String.valueOf(tileHeight) + ");", | |
| "$(function() {", "ajaxViewer.start();", "});", | |
| "</script>", "</body>", "</html>"}; | |
| 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<TileInfo> 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(); | |
| public ConsoleProxyClientParam getClientParam() { | |
| return clientParam; | |
| } | |
| public void setClientParam(ConsoleProxyClientParam clientParam) { | |
| this.clientParam = clientParam; | |
| ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword()); | |
| this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam); | |
| } | |
| } |