5
0
mirror of git://git.proxmox.com/git/novnc-pve.git synced 2025-01-09 01:18:03 +03:00
novnc-pve/debian/patches/0001-add-pve-specific-js-code.patch
Dominik Csapak fe91e9e1e3 add new rebased patches
adds the new rebased patches, based on current novnc master
notable changes:
* (most of) our code is in an es6 module, opposed to patching the ui.js
* removed our sendkeys menu in favor of the novnc one
* you can now enter fullscreen from a popup console
* when a vm resizes its resolution, the canvas should also rescale in
  the console tab

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2017-05-24 13:00:39 +02:00

640 lines
17 KiB
Diff

From ad6975daaeee0798955f1d8b36d567975f4cbae5 Mon Sep 17 00:00:00 2001
From: Dominik Csapak <d.csapak@proxmox.com>
Date: Tue, 13 Dec 2016 16:11:35 +0100
Subject: [PATCH 1/9] add pve specific js code
this adds a es6 module 'PVEUI' which we use for defining the pve related
methods (API2Request, etc.)
we also modify ui.js so that it uses this module and sets up our
autoresizing, commandstoggle, etc.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
app/pve.js | 417 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
app/ui.js | 95 ++++++++++----
2 files changed, 488 insertions(+), 24 deletions(-)
create mode 100644 app/pve.js
diff --git a/app/pve.js b/app/pve.js
new file mode 100644
index 0000000..668835a
--- /dev/null
+++ b/app/pve.js
@@ -0,0 +1,417 @@
+/*
+ * PVE Utility functions for noVNC
+ * Copyright (C) 2017 Proxmox GmbH
+ */
+
+import * as WebUtil from "./webutil.js";
+
+export default function PVEUI(UI){
+ this.consoletype = WebUtil.getQueryVar('console');
+ this.vmid = WebUtil.getQueryVar('vmid');
+ this.vmname = WebUtil.getQueryVar('vmname');
+ this.nodename = WebUtil.getQueryVar('node');
+ this.resize = WebUtil.getQueryVar('resize');
+ this.lastFBWidth = undefined;
+ this.lastFBHeight = undefined;
+ this.sizeUpdateTimer = undefined;
+ this.UI = UI;
+
+ var baseUrl = '/nodes/' + this.nodename;
+ var url;
+ var params = { websocket: 1 };
+ var title;
+
+ switch (this.consoletype) {
+ case 'kvm':
+ baseUrl += '/qemu/' + this.vmid;
+ url = baseUrl + '/vncproxy';
+ title = "VM " + this.vmid;
+ if (this.vmname) {
+ title += " ('" + this.vmname + "')";
+ }
+ break;
+ case 'lxc':
+ baseUrl += '/lxc/' + this.vmid;
+ url = baseUrl + '/vncproxy';
+ title = "CT " + this.vmid;
+ if (this.vmname) {
+ title += " ('" + this.vmname + "')";
+ }
+ break;
+ case 'shell':
+ url = baseUrl + '/vncshell';
+ title = "node '" + this.nodename + "'";
+ break;
+ case 'upgrade':
+ url = baseUrl + '/vncshell';
+ params.upgrade = 1;
+ title = 'System upgrade on node ' + this.nodename;
+ break;
+ default:
+ throw 'implement me';
+ break;
+ }
+
+ this.baseUrl = baseUrl;
+ this.url = url;
+ this.params = params;
+ document.title = title;
+};
+
+PVEUI.prototype = {
+ urlEncode: function(object) {
+ var i,value, params = [];
+
+ for (i in object) {
+ if (object.hasOwnProperty(i)) {
+ value = object[i];
+ if (value === undefined) value = '';
+ params.push(encodeURIComponent(i) + '=' + encodeURIComponent(String(value)));
+ }
+ }
+
+ return params.join('&');
+ },
+
+ API2Request: function(reqOpts) {
+ var me = this;
+
+ reqOpts.method = reqOpts.method || 'GET';
+
+ var xhr = new XMLHttpRequest();
+
+ xhr.onload = function() {
+ var scope = reqOpts.scope || this;
+ var result;
+ var errmsg;
+
+ if (xhr.readyState === 4) {
+ var ctype = xhr.getResponseHeader('Content-Type');
+ if (xhr.status === 200) {
+ if (ctype.match(/application\/json;/)) {
+ result = JSON.parse(xhr.responseText);
+ } else {
+ errmsg = 'got unexpected content type ' + ctype;
+ }
+ } else {
+ errmsg = 'Error ' + xhr.status + ': ' + xhr.statusText;
+ }
+ } else {
+ errmsg = 'Connection error - server offline?';
+ }
+
+ if (errmsg !== undefined) {
+ if (reqOpts.failure) {
+ reqOpts.failure.call(scope, errmsg);
+ }
+ } else {
+ if (reqOpts.success) {
+ reqOpts.success.call(scope, result);
+ }
+ }
+ if (reqOpts.callback) {
+ reqOpts.callback.call(scope, errmsg === undefined);
+ }
+ }
+
+ var data = me.urlEncode(reqOpts.params || {});
+
+ if (reqOpts.method === 'GET') {
+ xhr.open(reqOpts.method, "/api2/json" + reqOpts.url + '?' + data);
+ } else {
+ xhr.open(reqOpts.method, "/api2/json" + reqOpts.url);
+ }
+ xhr.setRequestHeader('Cache-Control', 'no-cache');
+ if (reqOpts.method === 'POST' || reqOpts.method === 'PUT') {
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.setRequestHeader('CSRFPreventionToken', PVE.CSRFPreventionToken);
+ xhr.send(data);
+ } else if (reqOpts.method === 'GET') {
+ xhr.send();
+ } else {
+ throw "unknown method";
+ }
+ },
+
+ pve_detect_migrated_vm: function() {
+ var me = this;
+ if (me.consoletype === 'kvm') {
+ // try to detect migrated VM
+ me.API2Request({
+ url: '/cluster/resources',
+ method: 'GET',
+ success: function(result) {
+ var list = result.data;
+ list.every(function(item) {
+ if (item.type === 'qemu' && item.vmid == me.vmid) {
+ var url = "?" + me.urlEncode({
+ console: me.consoletype,
+ novnc: 1,
+ vmid: me.vmid,
+ vmname: me.vmname,
+ node: item.node,
+ resize: me.resize
+ });
+ location.href = url;
+ return false; // break
+ }
+ return true;
+ });
+ }
+ });
+ } else if(me.consoletype === 'lxc') {
+ // lxc restart migration can take a while,
+ // so we need to find out if we are really migrating
+ var migrating;
+ var check = setInterval(function() {
+ if (migrating === undefined ||
+ migrating === true) {
+ // check (again) if migrating
+ me.UI.showStatus('Checking for Migration', 'warning', 5000);
+ me.API2Request({
+ url: me.baseUrl + '/config',
+ method: 'GET',
+ success: function(result) {
+ var lock = result.data.lock;
+ if (lock == 'migrate') {
+ migrating = true;
+ me.UI.showStatus('Migration detected, waiting...', 'warning', 5000);
+ } else {
+ migrating = false;
+ }
+ },
+ failure: function() {
+ migrating = false;
+ }
+ });
+ } else {
+ // not migrating any more
+ me.UI.showStatus('Migration finished', 'warning');
+ clearInterval(check);
+ me.API2Request({
+ url: '/cluster/resources',
+ method: 'GET',
+ success: function(result) {
+ var list = result.data;
+ list.every(function(item) {
+ if (item.type === 'lxc' && item.vmid == me.vmid) {
+ var url = "?" + me.urlEncode({
+ console: me.consoletype,
+ novnc: 1,
+ vmid: me.vmid,
+ vmname: me.vmname,
+ node: item.node,
+ resize: me.resize
+ });
+ location.href = url;
+ return false; // break
+ }
+ return true;
+ });
+ }
+ });
+ }
+ }, 5000);
+ }
+
+ },
+
+ pve_vm_command: function(cmd, params, reload) {
+ var me = this;
+ var baseUrl;
+ var confirmMsg = "";
+
+ switch(cmd) {
+ case "start":
+ reload = 1;
+ case "shutdown":
+ case "stop":
+ case "reset":
+ case "suspend":
+ case "resume":
+ confirmMsg = "Do you really want to " + cmd + " VM/CT {0}?";
+ break;
+ case "reload":
+ location.reload();
+ break;
+ default:
+ throw "implement me " + cmd;
+ }
+
+ confirmMsg = confirmMsg.replace('{0}', me.vmid);
+
+ if (confirmMsg !== "" && confirm(confirmMsg) !== true) {
+ return;
+ }
+
+ me.UI.closePVECommandPanel();
+
+ if (me.consoletype === 'kvm') {
+ baseUrl = '/nodes/' + me.nodename + '/qemu/' + me.vmid;
+ } else if (me.consoletype === 'lxc') {
+ baseUrl = '/nodes/' + me.nodename + '/lxc/' + me.vmid;
+ } else {
+ throw "unknown VM type";
+ }
+
+ me.API2Request({
+ params: params,
+ url: baseUrl + "/status/" + cmd,
+ method: 'POST',
+ failure: function(msg) {
+ me.UI.showStatus(msg, 'warning');
+ },
+ success: function() {
+ me.UI.showStatus("VM command '" + cmd +"' successful", 'normal');
+ if (reload) {
+ setTimeout(function() {
+ location.reload();
+ }, 1000);
+ };
+ }
+ });
+ },
+
+ addPVEHandlers: function() {
+ var me = this;
+ document.getElementById('pve_commands_button')
+ .addEventListener('click', me.UI.togglePVECommandPanel);
+
+ // show/hide the buttons
+ document.getElementById('noVNC_settings_button')
+ .classList.add('noVNC_hidden');
+ document.getElementById('noVNC_disconnect_button')
+ .classList.add('noVNC_hidden');
+ if (me.consoletype === 'kvm') {
+ document.getElementById('noVNC_clipboard_button')
+ .classList.add('noVNC_hidden');
+ }
+
+ if (me.consoletype === 'shell' || me.consoletype === 'upgrade') {
+ document.getElementById('pve_commands_button')
+ .classList.add('noVNC_hidden');
+ }
+
+ // add command logic
+ var commandArray = [
+ { cmd: 'start', kvm: 1, lxc: 1},
+ { cmd: 'stop', kvm: 1, lxc: 1},
+ { cmd: 'shutdown', kvm: 1, lxc: 1},
+ { cmd: 'suspend', kvm: 1},
+ { cmd: 'resume', kvm: 1},
+ { cmd: 'reset', kvm: 1},
+ { cmd: 'reload', kvm: 1, lxc: 1, shell: 1},
+ ];
+
+ commandArray.forEach(function(item) {
+ var el = document.getElementById('pve_command_'+item.cmd);
+ if (!el) {
+ return;
+ }
+
+ if (item[me.consoletype] === 1) {
+ el.onclick = function() {
+ me.pve_vm_command(item.cmd);
+ };
+ } else {
+ el.classList.add('noVNC_hidden');
+ }
+ });
+
+ //edge/ie11 quirk
+ var canvas = document.getElementById('noVNC_canvas');
+ canvas.onclick = canvas.focus;
+ },
+
+ getFBSize: function() {
+ var oh;
+ var ow;
+
+ if (window.innerHeight) {
+ oh = window.innerHeight;
+ ow = window.innerWidth;
+ } else if (document.documentElement &&
+ document.documentElement.clientHeight) {
+ oh = document.documentElement.clientHeight;
+ ow = document.documentElement.clientWidth;
+ } else if (document.body) {
+ oh = document.body.clientHeight;
+ ow = document.body.clientWidth;
+ } else {
+ throw "can't get window size";
+ }
+
+ return { width: ow, height: oh };
+ },
+
+ pveStart: function(callback) {
+ var me = this;
+ me.API2Request({
+ url: me.url,
+ method: 'POST',
+ params: me.params,
+ success: function(result) {
+ var wsparams = me.urlEncode({
+ port: result.data.port,
+ vncticket: result.data.ticket
+ });
+
+ document.getElementById('noVNC_password_input').value = result.data.ticket;
+ me.UI.updateSetting('path', 'api2/json' + me.baseUrl + '/vncwebsocket' + "?" + wsparams);
+
+ callback();
+ },
+ failure: function(msg) {
+ me.UI.showStatus(msg, 'error');
+ }
+ });
+ },
+
+ updateFBSize: function(rfb, width, height, clip) {
+ var me = this;
+ console.log(arguments);
+ try {
+ // Note: window size must be even number for firefox
+ me.lastFBWidth = Math.floor((width + 1)/2)*2;
+ me.lastFBHeight = Math.floor((height + 1)/2)*2;
+
+ if (me.sizeUpdateTimer !== undefined) {
+ clearInterval(me.sizeUpdateTimer);
+ }
+ if (clip) return;
+
+ var update_size = function() {
+ // we do not want to resize if we are in fullscreen
+ if (document.fullscreenElement || // alternative standard method
+ document.mozFullScreenElement || // currently working methods
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement) {
+ return;
+ }
+
+ var oldsize = me.getFBSize();
+ var offsetw = me.lastFBWidth - oldsize.width;
+ var offseth = me.lastFBHeight - oldsize.height;
+ if (offsetw !== 0 || offseth !== 0) {
+ //console.log("try resize by " + offsetw + " " + offseth);
+ try {
+ window.resizeBy(offsetw, offseth);
+ // wait a little an then fix the scrollbars
+ // on chrome
+ setTimeout(function() {
+ me.UI.fixScrollbars();
+ }, 100);
+ } catch (e) {
+ console.log('resizing did not work', e);
+ }
+ }
+ };
+
+ update_size();
+ me.sizeUpdateTimer = setInterval(update_size, 1000);
+
+ } catch(e) {
+ console.log(e);
+ }
+ },
+};
diff --git a/app/ui.js b/app/ui.js
index 73ad2b4..def0eda 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -20,6 +20,7 @@ import keysyms from "../core/input/keysymdef.js";
import RFB from "../core/rfb.js";
import Display from "../core/display.js";
import * as WebUtil from "./webutil.js";
+import PVEUI from "./pve.js";
const UI = {
@@ -63,6 +64,7 @@ const UI = {
// Render default UI and initialize settings menu
start: function(callback) {
+ UI.PVE = new PVEUI(UI);
// Setup global variables first
UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1);
@@ -95,6 +97,9 @@ const UI = {
UI.addConnectionControlHandlers();
UI.addClipboardHandlers();
UI.addSettingsHandlers();
+
+ // add pve specific event handlers
+ UI.PVE.addPVEHandlers();
document.getElementById("noVNC_status")
.addEventListener('click', UI.hideStatus);
@@ -103,11 +108,6 @@ const UI = {
UI.openControlbar();
- // Show the connect panel on first load unless autoconnecting
- if (!autoconnect) {
- UI.openConnectPanel();
- }
-
UI.updateViewClip();
UI.updateVisualState();
@@ -115,17 +115,13 @@ const UI = {
document.getElementById('noVNC_setting_host').focus();
document.documentElement.classList.remove("noVNC_loading");
- var autoconnect = WebUtil.getConfigVar('autoconnect', false);
- if (autoconnect === 'true' || autoconnect == '1') {
- autoconnect = true;
- UI.connect();
- } else {
- autoconnect = false;
- }
+ UI.PVE.pveStart(function() {
+ UI.connect();
- if (typeof callback === "function") {
- callback(UI.rfb);
- }
+ if (typeof callback === "function") {
+ callback(UI.rfb);
+ }
+ });
},
initFullscreen: function() {
@@ -170,10 +166,14 @@ const UI = {
/* Populate the controls if defaults are provided in the URL */
UI.initSetting('host', window.location.hostname);
UI.initSetting('port', port);
- UI.initSetting('encrypt', (window.location.protocol === "https:"));
+ UI.initSetting('encrypt', true);
UI.initSetting('cursor', !isTouchDevice);
UI.initSetting('clip', false);
- UI.initSetting('resize', 'off');
+ // we need updateSetting because
+ // otherwise we load from browser storage
+ // we want to overwrite the resize mode from url
+ var resize = WebUtil.getQueryVar('resize');
+ UI.updateSetting('resize', resize);
UI.initSetting('shared', true);
UI.initSetting('view_only', false);
UI.initSetting('path', 'websockify');
@@ -434,6 +434,7 @@ const UI = {
case 'connected':
UI.connected = true;
UI.inhibit_reconnect = false;
+ UI.pveAllowMigratedTest = true;
document.documentElement.classList.add("noVNC_connected");
if (rfb && rfb.get_encrypt()) {
msg = _("Connected (encrypted) to ") + UI.desktopName;
@@ -449,6 +450,10 @@ const UI = {
break;
case 'disconnected':
UI.showStatus(_("Disconnected"));
+ if (UI.pveAllowMigratedTest === true) {
+ UI.pveAllowMigratedTest = false;
+ UI.PVE.pve_detect_migrated_vm();
+ }
break;
default:
msg = "Invalid UI state";
@@ -861,6 +866,7 @@ const UI = {
UI.closeXvpPanel();
UI.closeClipboardPanel();
UI.closeExtraKeys();
+ UI.closePVECommandPanel();
},
/* ------^-------
@@ -1033,9 +1039,15 @@ const UI = {
password = WebUtil.getConfigVar('password');
}
- if (password === null) {
- password = undefined;
- }
+ var password = document.getElementById('noVNC_password_input').value;
+
+ if (!password) {
+ password = WebUtil.getConfigVar('password');
+ }
+
+ if (password === null) {
+ password = undefined;
+ }
if ((!host) || (!port)) {
var msg = _("Must set host and port");
@@ -1608,9 +1620,36 @@ const UI = {
/* ------^-------
* /EXTRA KEYS
* ==============
- * MISC
+ * PVE
* ------v------*/
+ togglePVECommandPanel: function() {
+ if (document.getElementById('pve_commands').classList.contains("noVNC_open")) {
+ UI.closePVECommandPanel();
+ } else {
+ UI.openPVECommandPanel();
+ }
+ },
+
+ openPVECommandPanel: function() {
+ var me = this;
+ UI.closeAllPanels();
+ UI.openControlbar();
+
+ document.getElementById('pve_commands').classList.add("noVNC_open");
+ document.getElementById('pve_commands_button').classList.add("noVNC_selected");
+ },
+
+ closePVECommandPanel: function() {
+ document.getElementById('pve_commands').classList.remove("noVNC_open");
+ document.getElementById('pve_commands_button').classList.remove("noVNC_selected");
+ },
+
+/* ------^-------
+ * /PVE
+ * ==============
+ * MISC
+ * ------v------*/
setMouseButton: function(num) {
var view_only = UI.rfb.get_view_only();
if (UI.rfb && !view_only) {
@@ -1658,8 +1697,16 @@ const UI = {
},
updateSessionSize: function(rfb, width, height) {
- UI.updateViewClip();
- UI.fixScrollbars();
+ var resize = UI.getSetting('resize');
+
+ if (resize == 'null') {
+ var clip = UI.getSetting('clip');
+ UI.PVE.updateFBSize(rfb, width, height, clip);
+ }
+
+ UI.applyResizeMode();
+ UI.updateViewClip();
+ UI.updateViewDrag();
},
fixScrollbars: function() {
@@ -1704,7 +1751,7 @@ const UI = {
},
/* ------^-------
- * /MISC
+ * /MISC
* ==============
*/
};
--
2.11.0