Console proxy refactoring incremental check-in - build in-memory VncClient contact between VNC client implementation and console proxy

This commit is contained in:
Kelven Yang 2012-03-01 17:02:29 -08:00
parent 7e9393ec2f
commit 4eb4d77746
7 changed files with 540 additions and 13 deletions

View File

@ -0,0 +1,426 @@
package com.cloud.consoleproxy;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.log4j.Logger;
import com.cloud.console.TileInfo;
import com.cloud.console.TileTracker;
import com.cloud.consoleproxy.vnc.VncClientListener;
public class ConsoleProxyVncClient implements VncClientListener {
private static final Logger s_logger = Logger.getLogger(ConsoleProxyVncClient.class);
private TileTracker tracker;
boolean dirtyFlag = false;
private Object tileDirtyEvent = new Object();
private AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2);
@Override
public void onFramebufferSizeChange(int w, int h) {
// TODO
}
@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();
}
private void signalTileDirtyEvent() {
synchronized(tileDirtyEvent) {
dirtyFlag = true;
tileDirtyEvent.notifyAll();
}
}
/*
//
// AJAX Image manipulation
//
public void copyTile(Graphics2D g, int x, int y, Rectangle rc) {
if(vc != null && vc.memImage != null) {
synchronized(vc.memImage) {
g.drawImage(vc.memImage, x, y, x + rc.width, y + rc.height,
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
}
}
}
public byte[] getFrameBufferJpeg() {
int width = 800;
int height = 600;
if(vc != null) {
width = vc.scaledWidth;
height = vc.scaledHeight;
}
if(s_logger.isTraceEnabled())
s_logger.trace("getFrameBufferJpeg, w: " + width + ", h: " + height);
BufferedImage bufferedImage = new BufferedImage(width, height,
BufferedImage.TYPE_3BYTE_BGR);
if(vc != null && vc.memImage != null) {
synchronized(vc.memImage) {
Graphics2D g = bufferedImage.createGraphics();
g.drawImage(vc.memImage, 0, 0, width, height, 0, 0, width, height, null);
}
}
byte[] imgBits = null;
try {
imgBits = jpegFromImage(bufferedImage);
} catch (IOException e) {
}
return imgBits;
}
public byte[] getTilesMergedJpeg(List<TileInfo> tileList, int tileWidth, int tileHeight) {
int width = Math.max(tileWidth, tileWidth*tileList.size());
BufferedImage bufferedImage = new BufferedImage(width, tileHeight,
BufferedImage.TYPE_3BYTE_BGR);
if(s_logger.isTraceEnabled())
s_logger.trace("Create merged image, w: " + width + ", h: " + tileHeight);
if(vc != null && vc.memImage != null) {
synchronized(vc.memImage) {
Graphics2D g = bufferedImage.createGraphics();
int i = 0;
for(TileInfo tile : tileList) {
Rectangle rc = tile.getTileRect();
if(s_logger.isTraceEnabled())
s_logger.trace("Merge tile into jpeg from (" + rc.x + "," + rc.y + "," + (rc.x + rc.width) + "," + (rc.y + rc.height) + ") to (" + i*tileWidth + ",0)" );
g.drawImage(vc.memImage, i*tileWidth, 0, i*tileWidth + rc.width, rc.height,
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
i++;
}
}
}
byte[] imgBits = null;
try {
imgBits = jpegFromImage(bufferedImage);
if(s_logger.isTraceEnabled())
s_logger.trace("Merge jpeg image size: " + imgBits.length + ", tiles: " + tileList.size());
} catch (IOException e) {
}
return imgBits;
}
public byte[] jpegFromImage(BufferedImage image) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(128000);
javax.imageio.ImageIO.write(image, "jpg", bos);
byte[] jpegBits = bos.toByteArray();
bos.close();
return jpegBits;
}
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("/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) {
StringBuffer sb = new StringBuffer();
if(init)
ajaxSessionId++;
sb.append("/ajax?host=").append(host).append("&port=").append(port);
sb.append("&sid=").append(passwordParam).append("&sess=").append(ajaxSessionId);
return sb.toString();
}
public String onAjaxClientKickoff() {
return "onKickoff();";
}
private boolean waitForViewerReady() {
long startTick = System.currentTimeMillis();
while(System.currentTimeMillis() - startTick < 5000) {
if(this.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION)
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>";
}
public String onAjaxClientStart(String title, List<String> languages, String guest) {
if(!waitForViewerReady())
return onAjaxClientConnectFailed();
// make sure we switch to AJAX view on start
setAjaxViewer(true);
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);
synchronized(this) {
if(framebufferResized) {
framebufferResized = false;
}
}
int retry = 0;
if(justCreated()) {
tracker.initCoverageTest();
try {
rfb.writeFramebufferUpdateRequest(0, 0, tracker.getTrackWidth(), tracker.getTrackHeight(), false);
while(!tracker.hasFullCoverage() && retry < 10) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
retry++;
}
} catch (IOException e1) {
s_logger.warn("Connection was broken ");
}
}
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);
}
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) {
StringBuffer sbLanguages = new StringBuffer("");
if(languages != null) {
for(String lang : languages) {
if(sbLanguages.length() > 0) {
sbLanguages.append(",");
}
sbLanguages.append(lang);
}
}
boolean linuxGuest = true;
if(guest != null && guest.equalsIgnoreCase("windows"))
linuxGuest = false;
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/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_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 + "', 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();";
}
public String onAjaxClientUpdate() {
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();
}
public long getAjaxSessionId() {
return this.ajaxSessionId;
}
public AjaxFIFOImageCache getAjaxImageCache() {
return ajaxImageCache;
}
public boolean isAjaxViewer() {
return ajaxViewer;
}
public synchronized void setAjaxViewer(boolean ajaxViewer) {
if(this.ajaxViewer != ajaxViewer) {
if(this.ajaxViewer) {
// previous session was AJAX session
this.ajaxSessionId++; // increase the session id so that it will disconnect existing AJAX viewer
} else {
// close java client session
if(clientStream != null) {
byte[] bs = new byte[2];
bs[0] = (byte)250;
bs[1] = 1;
writeToClientStream(bs);
try {
clientStream.close();
} catch (IOException e) {
}
clientStream = null;
}
}
this.ajaxViewer = ajaxViewer;
}
}
*/
}

View File

@ -0,0 +1,16 @@
package com.cloud.consoleproxy.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ImageHelper {
public static byte[] jpegFromImage(BufferedImage image) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream(128000);
javax.imageio.ImageIO.write(image, "jpg", bos);
byte[] jpegBits = bos.toByteArray();
bos.close();
return jpegBits;
}
}

View File

@ -4,7 +4,13 @@ import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.List;
import com.cloud.console.TileInfo;
import com.cloud.consoleproxy.util.ImageHelper;
/**
* A <code>BuffereImageCanvas</code> component represents frame buffer image on the
@ -52,8 +58,9 @@ public class BufferedImageCanvas extends Canvas {
public void paint(Graphics g) {
// Only part of image, requested with repaint(Rectangle), will be
// painted on screen.
g.drawImage(offlineImage, 0, 0, this);
synchronized(offlineImage) {
g.drawImage(offlineImage, 0, 0, this);
}
// Notify server that update is painted on screen
listener.imagePaintedOnScreen();
}
@ -65,5 +72,57 @@ public class BufferedImageCanvas extends Canvas {
public Graphics2D getOfflineGraphics() {
return graphics;
}
public void copyTile(Graphics2D g, int x, int y, Rectangle rc) {
synchronized(offlineImage) {
g.drawImage(offlineImage, x, y, x + rc.width, y + rc.height,
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
}
}
public byte[] getFrameBufferJpeg() {
int width = 800;
int height = 600;
width = offlineImage.getWidth();
height = offlineImage.getHeight();
BufferedImage bufferedImage = new BufferedImage(width, height,
BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g = bufferedImage.createGraphics();
synchronized(offlineImage) {
g.drawImage(offlineImage, 0, 0, width, height, 0, 0, width, height, null);
}
byte[] imgBits = null;
try {
imgBits = ImageHelper.jpegFromImage(bufferedImage);
} catch (IOException e) {
}
return imgBits;
}
public byte[] getTilesMergedJpeg(List<TileInfo> tileList, int tileWidth, int tileHeight) {
int width = Math.max(tileWidth, tileWidth*tileList.size());
BufferedImage bufferedImage = new BufferedImage(width, tileHeight,
BufferedImage.TYPE_3BYTE_BGR);
Graphics2D g = bufferedImage.createGraphics();
synchronized(offlineImage) {
int i = 0;
for(TileInfo tile : tileList) {
Rectangle rc = tile.getTileRect();
g.drawImage(offlineImage, i*tileWidth, 0, i*tileWidth + rc.width, rc.height,
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
i++;
}
}
byte[] imgBits = null;
try {
imgBits = ImageHelper.jpegFromImage(bufferedImage);
} catch (IOException e) {
}
return imgBits;
}
}

View File

@ -26,6 +26,9 @@ public class VncClient {
private VncClientPacketSender sender;
private VncServerPacketReceiver receiver;
private boolean noUI = false;
private VncClientListener clientListener = null;
public static void main(String args[]) {
if (args.length < 3) {
@ -38,7 +41,7 @@ public class VncClient {
String password = args[2];
try {
new VncClient(host, Integer.parseInt(port), password);
new VncClient(host, Integer.parseInt(port), password, false, null);
} catch (NumberFormatException e) {
SimpleLogger.error("Incorrect VNC server port number: " + port + ".");
System.exit(1);
@ -59,7 +62,11 @@ public class VncClient {
/* LOG */SimpleLogger.info("Usage: HOST PORT PASSWORD.");
}
public VncClient(String host, int port, String password) throws UnknownHostException, IOException {
public VncClient(String host, int port, String password, boolean noUI, VncClientListener clientListener)
throws UnknownHostException, IOException {
this.noUI = noUI;
this.clientListener = clientListener;
connectTo(host, port, password);
}
@ -111,20 +118,23 @@ public class VncClient {
canvas.addMouseMotionListener(sender);
canvas.addKeyListener(sender);
Frame frame = createVncClientMainWindow(canvas, screen.getDesktopName());
Frame frame = null;
if(!noUI)
frame = createVncClientMainWindow(canvas, screen.getDesktopName());
new Thread(sender).start();
// Run server-to-client packet receiver
receiver = new VncServerPacketReceiver(is, canvas, screen, this, sender);
receiver = new VncServerPacketReceiver(is, canvas, screen, this, sender, clientListener);
try {
receiver.run();
} finally {
frame.setVisible(false);
frame.dispose();
if(frame != null) {
frame.setVisible(false);
frame.dispose();
}
this.shutdown();
}
}
private Frame createVncClientMainWindow(BufferedImageCanvas canvas, String title) {

View File

@ -0,0 +1,6 @@
package com.cloud.consoleproxy.vnc;
public interface VncClientListener {
void onFramebufferSizeChange(int w, int h);
void onFramebufferUpdate(int x, int y, int w, int h);
}

View File

@ -17,14 +17,16 @@ public class VncServerPacketReceiver implements Runnable {
private boolean connectionAlive = true;
private VncClient vncConnection;
private final FrameBufferUpdateListener fburListener;
private final VncClientListener clientListener;
public VncServerPacketReceiver(DataInputStream is, BufferedImageCanvas canvas, VncScreenDescription screen, VncClient vncConnection,
FrameBufferUpdateListener fburListener) {
FrameBufferUpdateListener fburListener, VncClientListener clientListener) {
this.screen = screen;
this.canvas = canvas;
this.is = is;
this.vncConnection = vncConnection;
this.fburListener = fburListener;
this.clientListener = clientListener;
}
@Override
@ -43,7 +45,7 @@ public class VncServerPacketReceiver implements Runnable {
// so it can send another frame buffer update request
fburListener.frameBufferPacketReceived();
// Handle frame buffer update
new FramebufferUpdatePacket(canvas, screen, is);
new FramebufferUpdatePacket(canvas, screen, is, clientListener);
break;
}

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import com.cloud.consoleproxy.vnc.BufferedImageCanvas;
import com.cloud.consoleproxy.vnc.RfbConstants;
import com.cloud.consoleproxy.vnc.VncClientListener;
import com.cloud.consoleproxy.vnc.VncScreenDescription;
import com.cloud.consoleproxy.vnc.packet.server.CopyRect;
import com.cloud.consoleproxy.vnc.packet.server.RawRect;
@ -14,10 +15,14 @@ public class FramebufferUpdatePacket {
private final VncScreenDescription screen;
private final BufferedImageCanvas canvas;
private final VncClientListener clientListener;
public FramebufferUpdatePacket(BufferedImageCanvas canvas, VncScreenDescription screen, DataInputStream is) throws IOException {
public FramebufferUpdatePacket(BufferedImageCanvas canvas, VncScreenDescription screen, DataInputStream is,
VncClientListener clientListener) throws IOException {
this.screen = screen;
this.canvas = canvas;
this.clientListener = clientListener;
readPacketData(is);
}
@ -62,6 +67,9 @@ public class FramebufferUpdatePacket {
}
paint(rect, canvas);
if(this.clientListener != null)
this.clientListener.onFramebufferUpdate(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
}
}