| // 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) { |
| s_logger.debug("[ignored] Console proxy was interupted while waiting for viewer to become ready."); |
| } |
| } |
| 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); |
| |
| 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/jquery.flot.navigate.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>", |
| "<li><a href=\"#\" cmd=\"keyboard_fr\"><span>French AZERTY 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) { |
| s_logger.debug("[ignored] Console proxy ajax update was interupted while waiting for viewer to become ready."); |
| } |
| } |
| } |
| |
| 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); |
| } |
| } |