"use strict"; /* Copyright (C) 2012 by Jeremy P. White This file is part of spice-html5. spice-html5 is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. spice-html5 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with spice-html5. If not, see . */ import * as Messages from './spicemsg.js'; import { Constants } from './enums.js'; import { SpiceCursorConn } from './cursor.js'; import { SpiceConn } from './spiceconn.js'; import { DEBUG } from './utils.js'; import { SpiceFileXferTask } from './filexfer.js'; import { SpiceInputsConn, sendCtrlAltDel, sendCtrlAltFN } from './inputs.js'; import { SpiceDisplayConn } from './display.js'; import { SpicePlaybackConn } from './playback.js'; import { SpicePortConn } from './port.js'; import { handle_file_dragover, handle_file_drop } from './filexfer.js'; import { resize_helper, handle_resize } from './resize.js'; /*---------------------------------------------------------------------------- ** SpiceMainConn ** This is the master Javascript class for establishing and ** managing a connection to a Spice Server. ** ** Invocation: You must pass an object with properties as follows: ** uri (required) Uri of a WebSocket listener that is ** connected to a spice server. ** password (required) Password to send to the spice server ** message_id (optional) Identifier of an element in the DOM ** where SpiceConn will write messages. ** It will use classes spice-messages-x, ** where x is one of info, warning, or error. ** screen_id (optional) Identifier of an element in the DOM ** where SpiceConn will create any new ** client screens. This is the main UI. ** dump_id (optional) If given, an element to use for ** dumping every single image + canvas drawn. ** Sometimes useful for debugging. ** onerror (optional) If given, a function to receive async ** errors. Note that you should also catch ** errors for ones that occur inline ** onagent (optional) If given, a function to be called when ** a VD agent is connected; a good opportunity ** to request a resize ** onsuccess (optional) If given, a function to be called when the ** session is successfully connected ** ** Throws error if there are troubles. Requires a modern (by 2012 standards) ** browser, including WebSocket and WebSocket.binaryType == arraybuffer ** **--------------------------------------------------------------------------*/ function SpiceMainConn() { if (typeof WebSocket === "undefined") throw new Error("WebSocket unavailable. You need to use a different browser."); SpiceConn.apply(this, arguments); this.agent_msg_queue = []; this.file_xfer_tasks = {}; this.file_xfer_task_id = 0; this.file_xfer_read_queue = []; this.ports = []; } SpiceMainConn.prototype = Object.create(SpiceConn.prototype); SpiceMainConn.prototype.process_channel_message = function (msg) { if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_BEGIN) { this.known_unimplemented(msg.type, "Main Migrate Begin"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_CANCEL) { this.known_unimplemented(msg.type, "Main Migrate Cancel"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_INIT) { this.log_info("Connected to " + this.ws.url); this.report_success("Connected") this.main_init = new Messages.SpiceMsgMainInit(msg.data); this.connection_id = this.main_init.session_id; this.agent_tokens = this.main_init.agent_tokens; if (DEBUG > 0) { // FIXME - there is a lot here we don't handle; mouse modes, agent, // ram_hint, multi_media_time this.log_info("session id " + this.main_init.session_id + " ; display_channels_hint " + this.main_init.display_channels_hint + " ; supported_mouse_modes " + this.main_init.supported_mouse_modes + " ; current_mouse_mode " + this.main_init.current_mouse_mode + " ; agent_connected " + this.main_init.agent_connected + " ; agent_tokens " + this.main_init.agent_tokens + " ; multi_media_time " + this.main_init.multi_media_time + " ; ram_hint " + this.main_init.ram_hint); } this.our_mm_time = Date.now(); this.mm_time = this.main_init.multi_media_time; this.handle_mouse_mode(this.main_init.current_mouse_mode, this.main_init.supported_mouse_modes); if (this.main_init.agent_connected) this.connect_agent(); var attach = new Messages.SpiceMiniData; attach.type = Constants.SPICE_MSGC_MAIN_ATTACH_CHANNELS; attach.size = attach.buffer_size(); this.send_msg(attach); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MOUSE_MODE) { var mode = new Messages.SpiceMsgMainMouseMode(msg.data); DEBUG > 0 && this.log_info("Mouse supported modes " + mode.supported_modes + "; current " + mode.current_mode); this.handle_mouse_mode(mode.current_mode, mode.supported_modes); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MULTI_MEDIA_TIME) { this.known_unimplemented(msg.type, "Main Multi Media Time"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_CHANNELS_LIST) { var i; var chans; DEBUG > 0 && console.log("channels"); chans = new Messages.SpiceMsgChannels(msg.data); for (i = 0; i < chans.channels.length; i++) { var conn = { uri: this.ws.url, parent: this, connection_id: this.connection_id, type: chans.channels[i].type, chan_id: chans.channels[i].id }; if (chans.channels[i].type == Constants.SPICE_CHANNEL_DISPLAY) { if (chans.channels[i].id == 0) { this.display = new SpiceDisplayConn(conn); } else { this.log_warn("The spice-html5 client does not handle multiple heads."); } } else if (chans.channels[i].type == Constants.SPICE_CHANNEL_INPUTS) { this.inputs = new SpiceInputsConn(conn); this.inputs.mouse_mode = this.mouse_mode; } else if (chans.channels[i].type == Constants.SPICE_CHANNEL_CURSOR) this.cursor = new SpiceCursorConn(conn); else if (chans.channels[i].type == Constants.SPICE_CHANNEL_PLAYBACK) this.cursor = new SpicePlaybackConn(conn); else if (chans.channels[i].type == Constants.SPICE_CHANNEL_PORT) this.ports.push(new SpicePortConn(conn)); else { if (!("extra_channels" in this)) this.extra_channels = []; this.extra_channels[i] = new SpiceConn(conn); this.log_err("Channel type " + this.extra_channels[i].channel_type() + " not implemented"); } } return true; } if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_CONNECTED) { this.connect_agent(); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS) { var connected_tokens = new Messages.SpiceMsgMainAgentTokens(msg.data); this.agent_tokens = connected_tokens.num_tokens; this.connect_agent(); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_TOKEN) { var remaining_tokens, tokens = new Messages.SpiceMsgMainAgentTokens(msg.data); this.agent_tokens += tokens.num_tokens; this.send_agent_message_queue(); remaining_tokens = this.agent_tokens; while (remaining_tokens > 0 && this.file_xfer_read_queue.length > 0) { var xfer_task = this.file_xfer_read_queue.shift(); this.file_xfer_read(xfer_task, xfer_task.read_bytes); remaining_tokens--; } return true; } if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_DISCONNECTED) { this.agent_connected = false; return true; } if (msg.type == Constants.SPICE_MSG_MAIN_AGENT_DATA) { var agent_data = new Messages.SpiceMsgMainAgentData(msg.data); if (agent_data.type == Constants.VD_AGENT_ANNOUNCE_CAPABILITIES) { var agent_caps = new Messages.VDAgentAnnounceCapabilities(agent_data.data); if (agent_caps.request) this.announce_agent_capabilities(0); return true; } else if (agent_data.type == Constants.VD_AGENT_FILE_XFER_STATUS) { this.handle_file_xfer_status(new Messages.VDAgentFileXferStatusMessage(agent_data.data)); return true; } return false; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_SWITCH_HOST) { this.known_unimplemented(msg.type, "Main Migrate Switch Host"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_END) { this.known_unimplemented(msg.type, "Main Migrate End"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_NAME) { this.known_unimplemented(msg.type, "Main Name"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_UUID) { this.known_unimplemented(msg.type, "Main UUID"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_BEGIN_SEAMLESS) { this.known_unimplemented(msg.type, "Main Migrate Begin Seamless"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK) { this.known_unimplemented(msg.type, "Main Migrate Dst Seamless ACK"); return true; } if (msg.type == Constants.SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_NACK) { this.known_unimplemented(msg.type, "Main Migrate Dst Seamless NACK"); return true; } return false; } SpiceMainConn.prototype.stop = function (msg) { this.state = "closing"; if (this.inputs) { this.inputs.cleanup(); this.inputs = undefined; } if (this.cursor) { this.cursor.cleanup(); this.cursor = undefined; } if (this.display) { this.display.cleanup(); this.display.destroy_surfaces(); this.display = undefined; } this.cleanup(); if ("extra_channels" in this) for (var e in this.extra_channels) this.extra_channels[e].cleanup(); this.extra_channels = undefined; } SpiceMainConn.prototype.send_agent_message_queue = function (message) { if (!this.agent_connected) return; if (message) this.agent_msg_queue.push(message); while (this.agent_tokens > 0 && this.agent_msg_queue.length > 0) { var mr = this.agent_msg_queue.shift(); this.send_msg(mr); this.agent_tokens--; } } SpiceMainConn.prototype.send_agent_message = function (type, message) { var agent_data = new Messages.SpiceMsgcMainAgentData(type, message); var sb = 0, maxsize = Constants.VD_AGENT_MAX_DATA_SIZE - Messages.SpiceMiniData.prototype.buffer_size(); var data = new ArrayBuffer(agent_data.buffer_size()); agent_data.to_buffer(data); while (sb < agent_data.buffer_size()) { var eb = Math.min(sb + maxsize, agent_data.buffer_size()); var mr = new Messages.SpiceMiniData(); mr.type = Constants.SPICE_MSGC_MAIN_AGENT_DATA; mr.size = eb - sb; mr.data = data.slice(sb, eb); this.send_agent_message_queue(mr); sb = eb; } } SpiceMainConn.prototype.announce_agent_capabilities = function (request) { var caps = new Messages.VDAgentAnnounceCapabilities(request, (1 << Constants.VD_AGENT_CAP_MOUSE_STATE) | (1 << Constants.VD_AGENT_CAP_MONITORS_CONFIG) | (1 << Constants.VD_AGENT_CAP_REPLY)); this.send_agent_message(Constants.VD_AGENT_ANNOUNCE_CAPABILITIES, caps); } SpiceMainConn.prototype.resize_window = function (flags, width, height, depth, x, y) { var monitors_config = new Messages.VDAgentMonitorsConfig(flags, width, height, depth, x, y); this.send_agent_message(Constants.VD_AGENT_MONITORS_CONFIG, monitors_config); } SpiceMainConn.prototype.file_xfer_start = function (file) { var task_id, xfer_start, task; task_id = this.file_xfer_task_id++; task = new SpiceFileXferTask(task_id, file); task.create_progressbar(); this.file_xfer_tasks[task_id] = task; xfer_start = new Messages.VDAgentFileXferStartMessage(task_id, file.name, file.size); this.send_agent_message(Constants.VD_AGENT_FILE_XFER_START, xfer_start); } SpiceMainConn.prototype.handle_file_xfer_status = function (file_xfer_status) { var xfer_error, xfer_task; if (!this.file_xfer_tasks[file_xfer_status.id]) { return; } xfer_task = this.file_xfer_tasks[file_xfer_status.id]; switch (file_xfer_status.result) { case Constants.VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA: this.file_xfer_read(xfer_task); return; case Constants.VD_AGENT_FILE_XFER_STATUS_CANCELLED: xfer_error = "transfer is cancelled by spice agent"; break; case Constants.VD_AGENT_FILE_XFER_STATUS_ERROR: xfer_error = "some errors occurred in the spice agent"; break; case Constants.VD_AGENT_FILE_XFER_STATUS_SUCCESS: break; default: xfer_error = "unhandled status type: " + file_xfer_status.result; break; } this.file_xfer_completed(xfer_task, xfer_error) } SpiceMainConn.prototype.file_xfer_read = function (file_xfer_task, start_byte) { var FILE_XFER_CHUNK_SIZE = 32 * Constants.VD_AGENT_MAX_DATA_SIZE; var _this = this; var sb, eb; var slice, reader; if (!file_xfer_task || !this.file_xfer_tasks[file_xfer_task.id] || (start_byte > 0 && start_byte == file_xfer_task.file.size)) { return; } if (file_xfer_task.cancelled) { var xfer_status = new Messages.VDAgentFileXferStatusMessage(file_xfer_task.id, Constants.VD_AGENT_FILE_XFER_STATUS_CANCELLED); this.send_agent_message(Constants.VD_AGENT_FILE_XFER_STATUS, xfer_status); delete this.file_xfer_tasks[file_xfer_task.id]; return; } sb = start_byte || 0, eb = Math.min(sb + FILE_XFER_CHUNK_SIZE, file_xfer_task.file.size); if (!this.agent_tokens) { file_xfer_task.read_bytes = sb; this.file_xfer_read_queue.push(file_xfer_task); return; } reader = new FileReader(); reader.onload = function (e) { var xfer_data = new Messages.VDAgentFileXferDataMessage(file_xfer_task.id, e.target.result.byteLength, e.target.result); _this.send_agent_message(Constants.VD_AGENT_FILE_XFER_DATA, xfer_data); _this.file_xfer_read(file_xfer_task, eb); file_xfer_task.update_progressbar(eb); }; slice = file_xfer_task.file.slice(sb, eb); reader.readAsArrayBuffer(slice); } SpiceMainConn.prototype.file_xfer_completed = function (file_xfer_task, error) { if (error) this.log_err(error); else this.log_info("transfer of '" + file_xfer_task.file.name + "' was successful"); file_xfer_task.remove_progressbar(); delete this.file_xfer_tasks[file_xfer_task.id]; } SpiceMainConn.prototype.connect_agent = function () { this.agent_connected = true; var agent_start = new Messages.SpiceMsgcMainAgentStart(~0); var mr = new Messages.SpiceMiniData(); mr.build_msg(Constants.SPICE_MSGC_MAIN_AGENT_START, agent_start); this.send_msg(mr); this.announce_agent_capabilities(1); if (this.onagent !== undefined) this.onagent(this); } SpiceMainConn.prototype.handle_mouse_mode = function (current, supported) { this.mouse_mode = current; if (current != Constants.SPICE_MOUSE_MODE_CLIENT && (supported & Constants.SPICE_MOUSE_MODE_CLIENT)) { var mode_request = new Messages.SpiceMsgcMainMouseModeRequest(Constants.SPICE_MOUSE_MODE_CLIENT); var mr = new Messages.SpiceMiniData(); mr.build_msg(Constants.SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST, mode_request); this.send_msg(mr); } if (this.inputs) this.inputs.mouse_mode = current; } /* Shift current time to attempt to get a time matching that of the server */ SpiceMainConn.prototype.relative_now = function () { var ret = (Date.now() - this.our_mm_time) + this.mm_time; return ret; } export { SpiceMainConn, handle_file_dragover, handle_file_drop, resize_helper, handle_resize, sendCtrlAltDel, sendCtrlAltFN, };