diff --git a/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java new file mode 100644 index 00000000000..2c66ed091e2 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/ConsoleProxyVncClient.java @@ -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 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 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 "

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

"; + } + + public String onAjaxClientStart(String title, List 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 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); + } + } + + boolean linuxGuest = true; + if(guest != null && guest.equalsIgnoreCase("windows")) + linuxGuest = false; + + 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();"; + } + + 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 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; + } + } +*/ +} diff --git a/console-proxy/src/com/cloud/consoleproxy/util/ImageHelper.java b/console-proxy/src/com/cloud/consoleproxy/util/ImageHelper.java new file mode 100644 index 00000000000..7e4d7b22458 --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/util/ImageHelper.java @@ -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; + } +} diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/BufferedImageCanvas.java b/console-proxy/src/com/cloud/consoleproxy/vnc/BufferedImageCanvas.java index cd9a8378ba4..c21c422eb16 100644 --- a/console-proxy/src/com/cloud/consoleproxy/vnc/BufferedImageCanvas.java +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/BufferedImageCanvas.java @@ -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 BuffereImageCanvas 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 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; + } } \ No newline at end of file diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java index 25035bda0ec..1bdc114daea 100644 --- a/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClient.java @@ -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) { diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/VncClientListener.java b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClientListener.java new file mode 100644 index 00000000000..f3934f10d7b --- /dev/null +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/VncClientListener.java @@ -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); +} diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/VncServerPacketReceiver.java b/console-proxy/src/com/cloud/consoleproxy/vnc/VncServerPacketReceiver.java index 01d9cf887e6..669fb895c8a 100644 --- a/console-proxy/src/com/cloud/consoleproxy/vnc/VncServerPacketReceiver.java +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/VncServerPacketReceiver.java @@ -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; } diff --git a/console-proxy/src/com/cloud/consoleproxy/vnc/packet/server/FramebufferUpdatePacket.java b/console-proxy/src/com/cloud/consoleproxy/vnc/packet/server/FramebufferUpdatePacket.java index aa2c2b19e90..64c184c8e72 100644 --- a/console-proxy/src/com/cloud/consoleproxy/vnc/packet/server/FramebufferUpdatePacket.java +++ b/console-proxy/src/com/cloud/consoleproxy/vnc/packet/server/FramebufferUpdatePacket.java @@ -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()); } }