
Previously, we sent them strictly upon receipt. However, receiving is fast; by adding in some of the processing time (i.e. queueing and so on), we encourage the adaptive rate control to slow down.
487 lines
16 KiB
JavaScript
487 lines
16 KiB
JavaScript
"use strict";
|
|
/*
|
|
Copyright (C) 2012 by Jeremy P. White <jwhite@codeweavers.com>
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*----------------------------------------------------------------------------
|
|
** 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
|
|
**
|
|
** 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 = [];
|
|
}
|
|
|
|
SpiceMainConn.prototype = Object.create(SpiceConn.prototype);
|
|
SpiceMainConn.prototype.process_channel_message = function(msg)
|
|
{
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_BEGIN)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate Begin");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_CANCEL)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate Cancel");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_INIT)
|
|
{
|
|
this.log_info("Connected to " + this.ws.url);
|
|
this.report_success("Connected")
|
|
this.main_init = new 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 SpiceMiniData;
|
|
attach.type = SPICE_MSGC_MAIN_ATTACH_CHANNELS;
|
|
attach.size = attach.buffer_size();
|
|
this.send_msg(attach);
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MOUSE_MODE)
|
|
{
|
|
var mode = new 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 == SPICE_MSG_MAIN_MULTI_MEDIA_TIME)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Multi Media Time");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_CHANNELS_LIST)
|
|
{
|
|
var i;
|
|
var chans;
|
|
DEBUG > 0 && console.log("channels");
|
|
chans = new 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 == SPICE_CHANNEL_DISPLAY)
|
|
this.display = new SpiceDisplayConn(conn);
|
|
else if (chans.channels[i].type == SPICE_CHANNEL_INPUTS)
|
|
{
|
|
this.inputs = new SpiceInputsConn(conn);
|
|
this.inputs.mouse_mode = this.mouse_mode;
|
|
}
|
|
else if (chans.channels[i].type == SPICE_CHANNEL_CURSOR)
|
|
this.cursor = new SpiceCursorConn(conn);
|
|
else if (chans.channels[i].type == SPICE_CHANNEL_PLAYBACK)
|
|
this.cursor = new SpicePlaybackConn(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 == SPICE_MSG_MAIN_AGENT_CONNECTED)
|
|
{
|
|
this.connect_agent();
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS)
|
|
{
|
|
var connected_tokens = new SpiceMsgMainAgentTokens(msg.data);
|
|
this.agent_tokens = connected_tokens.num_tokens;
|
|
this.connect_agent();
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_AGENT_TOKEN)
|
|
{
|
|
var remaining_tokens, tokens = new 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 == SPICE_MSG_MAIN_AGENT_DISCONNECTED)
|
|
{
|
|
this.agent_connected = false;
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_AGENT_DATA)
|
|
{
|
|
var agent_data = new SpiceMsgMainAgentData(msg.data);
|
|
if (agent_data.type == VD_AGENT_ANNOUNCE_CAPABILITIES)
|
|
{
|
|
var agent_caps = new VDAgentAnnounceCapabilities(agent_data.data);
|
|
if (agent_caps.request)
|
|
this.announce_agent_capabilities(0);
|
|
return true;
|
|
}
|
|
else if (agent_data.type == VD_AGENT_FILE_XFER_STATUS)
|
|
{
|
|
this.handle_file_xfer_status(new VDAgentFileXferStatusMessage(agent_data.data));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_SWITCH_HOST)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate Switch Host");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_END)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate End");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_NAME)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Name");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_UUID)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main UUID");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_BEGIN_SEAMLESS)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate Begin Seamless");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK)
|
|
{
|
|
this.known_unimplemented(msg.type, "Main Migrate Dst Seamless ACK");
|
|
return true;
|
|
}
|
|
|
|
if (msg.type == 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 SpiceMsgcMainAgentData(type, message);
|
|
var sb = 0, maxsize = VD_AGENT_MAX_DATA_SIZE - 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 SpiceMiniData();
|
|
mr.type = 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 VDAgentAnnounceCapabilities(request, (1 << VD_AGENT_CAP_MOUSE_STATE) |
|
|
(1 << VD_AGENT_CAP_MONITORS_CONFIG) |
|
|
(1 << VD_AGENT_CAP_REPLY));
|
|
this.send_agent_message(VD_AGENT_ANNOUNCE_CAPABILITIES, caps);
|
|
}
|
|
|
|
SpiceMainConn.prototype.resize_window = function(flags, width, height, depth, x, y)
|
|
{
|
|
var monitors_config = new VDAgentMonitorsConfig(flags, width, height, depth, x, y);
|
|
this.send_agent_message(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 VDAgentFileXferStartMessage(task_id, file.name, file.size);
|
|
this.send_agent_message(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 VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA:
|
|
this.file_xfer_read(xfer_task);
|
|
return;
|
|
case VD_AGENT_FILE_XFER_STATUS_CANCELLED:
|
|
xfer_error = "transfer is cancelled by spice agent";
|
|
break;
|
|
case VD_AGENT_FILE_XFER_STATUS_ERROR:
|
|
xfer_error = "some errors occurred in the spice agent";
|
|
break;
|
|
case 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 * 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 VDAgentFileXferStatusMessage(file_xfer_task.id,
|
|
VD_AGENT_FILE_XFER_STATUS_CANCELLED);
|
|
this.send_agent_message(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 VDAgentFileXferDataMessage(file_xfer_task.id,
|
|
e.target.result.byteLength,
|
|
e.target.result);
|
|
_this.send_agent_message(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 SpiceMsgcMainAgentStart(~0);
|
|
var mr = new SpiceMiniData();
|
|
mr.build_msg(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 != SPICE_MOUSE_MODE_CLIENT && (supported & SPICE_MOUSE_MODE_CLIENT))
|
|
{
|
|
var mode_request = new SpiceMsgcMainMouseModeRequest(SPICE_MOUSE_MODE_CLIENT);
|
|
var mr = new SpiceMiniData();
|
|
mr.build_msg(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;
|
|
}
|