]> git.proxmox.com Git - novnc-pve.git/blame - debian/patches/0001-add-pve-specific-js-code.patch
update noVNC to v1.2.0
[novnc-pve.git] / debian / patches / 0001-add-pve-specific-js-code.patch
CommitLineData
2b8dde16 1From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
fe91e9e1
DC
2From: Dominik Csapak <d.csapak@proxmox.com>
3Date: Tue, 13 Dec 2016 16:11:35 +0100
2b8dde16 4Subject: [PATCH 01/12] add pve specific js code
fe91e9e1
DC
5
6this adds a es6 module 'PVEUI' which we use for defining the pve related
7methods (API2Request, etc.)
8
9we also modify ui.js so that it uses this module and sets up our
10autoresizing, commandstoggle, etc.
11
12Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
13---
2b8dde16
DC
14 app/pve.js | 418 +++++++++++++++++++++++++++++++++++++++++++++++++++++
15 app/ui.js | 66 +++++++--
4cc9ed08 16 vnc.html | 10 +-
2b8dde16 17 3 files changed, 480 insertions(+), 14 deletions(-)
fe91e9e1
DC
18 create mode 100644 app/pve.js
19
20diff --git a/app/pve.js b/app/pve.js
21new file mode 100644
4cc9ed08 22index 0000000..e2c37fb
fe91e9e1
DC
23--- /dev/null
24+++ b/app/pve.js
4cc9ed08 25@@ -0,0 +1,418 @@
fe91e9e1
DC
26+/*
27+ * PVE Utility functions for noVNC
28+ * Copyright (C) 2017 Proxmox GmbH
29+ */
30+
31+import * as WebUtil from "./webutil.js";
32+
33+export default function PVEUI(UI){
34+ this.consoletype = WebUtil.getQueryVar('console');
35+ this.vmid = WebUtil.getQueryVar('vmid');
36+ this.vmname = WebUtil.getQueryVar('vmname');
37+ this.nodename = WebUtil.getQueryVar('node');
38+ this.resize = WebUtil.getQueryVar('resize');
39+ this.lastFBWidth = undefined;
40+ this.lastFBHeight = undefined;
41+ this.sizeUpdateTimer = undefined;
42+ this.UI = UI;
43+
44+ var baseUrl = '/nodes/' + this.nodename;
45+ var url;
46+ var params = { websocket: 1 };
47+ var title;
48+
49+ switch (this.consoletype) {
50+ case 'kvm':
51+ baseUrl += '/qemu/' + this.vmid;
52+ url = baseUrl + '/vncproxy';
53+ title = "VM " + this.vmid;
54+ if (this.vmname) {
55+ title += " ('" + this.vmname + "')";
56+ }
57+ break;
58+ case 'lxc':
59+ baseUrl += '/lxc/' + this.vmid;
60+ url = baseUrl + '/vncproxy';
61+ title = "CT " + this.vmid;
62+ if (this.vmname) {
63+ title += " ('" + this.vmname + "')";
64+ }
65+ break;
66+ case 'shell':
67+ url = baseUrl + '/vncshell';
68+ title = "node '" + this.nodename + "'";
69+ break;
70+ case 'upgrade':
71+ url = baseUrl + '/vncshell';
72+ params.upgrade = 1;
73+ title = 'System upgrade on node ' + this.nodename;
74+ break;
75+ default:
76+ throw 'implement me';
77+ break;
78+ }
79+
a375b7e5
DC
80+ if (this.resize == 'scale' &&
81+ (this.consoletype === 'lxc' || this.consoletype === 'shell')) {
82+ var size = this.getFBSize();
83+ params.width = size.width;
84+ params.height = size.height;
85+ }
86+
fe91e9e1
DC
87+ this.baseUrl = baseUrl;
88+ this.url = url;
89+ this.params = params;
90+ document.title = title;
91+};
92+
93+PVEUI.prototype = {
94+ urlEncode: function(object) {
95+ var i,value, params = [];
96+
97+ for (i in object) {
98+ if (object.hasOwnProperty(i)) {
99+ value = object[i];
100+ if (value === undefined) value = '';
101+ params.push(encodeURIComponent(i) + '=' + encodeURIComponent(String(value)));
102+ }
103+ }
104+
105+ return params.join('&');
106+ },
107+
108+ API2Request: function(reqOpts) {
109+ var me = this;
110+
111+ reqOpts.method = reqOpts.method || 'GET';
112+
113+ var xhr = new XMLHttpRequest();
114+
115+ xhr.onload = function() {
116+ var scope = reqOpts.scope || this;
117+ var result;
118+ var errmsg;
119+
120+ if (xhr.readyState === 4) {
121+ var ctype = xhr.getResponseHeader('Content-Type');
122+ if (xhr.status === 200) {
123+ if (ctype.match(/application\/json;/)) {
124+ result = JSON.parse(xhr.responseText);
125+ } else {
126+ errmsg = 'got unexpected content type ' + ctype;
127+ }
128+ } else {
129+ errmsg = 'Error ' + xhr.status + ': ' + xhr.statusText;
130+ }
131+ } else {
132+ errmsg = 'Connection error - server offline?';
133+ }
134+
135+ if (errmsg !== undefined) {
136+ if (reqOpts.failure) {
137+ reqOpts.failure.call(scope, errmsg);
138+ }
139+ } else {
140+ if (reqOpts.success) {
141+ reqOpts.success.call(scope, result);
142+ }
143+ }
144+ if (reqOpts.callback) {
145+ reqOpts.callback.call(scope, errmsg === undefined);
146+ }
147+ }
148+
149+ var data = me.urlEncode(reqOpts.params || {});
150+
151+ if (reqOpts.method === 'GET') {
152+ xhr.open(reqOpts.method, "/api2/json" + reqOpts.url + '?' + data);
153+ } else {
154+ xhr.open(reqOpts.method, "/api2/json" + reqOpts.url);
155+ }
156+ xhr.setRequestHeader('Cache-Control', 'no-cache');
157+ if (reqOpts.method === 'POST' || reqOpts.method === 'PUT') {
158+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
159+ xhr.setRequestHeader('CSRFPreventionToken', PVE.CSRFPreventionToken);
160+ xhr.send(data);
161+ } else if (reqOpts.method === 'GET') {
162+ xhr.send();
163+ } else {
164+ throw "unknown method";
165+ }
166+ },
167+
168+ pve_detect_migrated_vm: function() {
169+ var me = this;
170+ if (me.consoletype === 'kvm') {
171+ // try to detect migrated VM
172+ me.API2Request({
173+ url: '/cluster/resources',
174+ method: 'GET',
175+ success: function(result) {
176+ var list = result.data;
177+ list.every(function(item) {
178+ if (item.type === 'qemu' && item.vmid == me.vmid) {
179+ var url = "?" + me.urlEncode({
180+ console: me.consoletype,
181+ novnc: 1,
182+ vmid: me.vmid,
183+ vmname: me.vmname,
184+ node: item.node,
185+ resize: me.resize
186+ });
187+ location.href = url;
188+ return false; // break
189+ }
190+ return true;
191+ });
192+ }
193+ });
194+ } else if(me.consoletype === 'lxc') {
195+ // lxc restart migration can take a while,
196+ // so we need to find out if we are really migrating
197+ var migrating;
198+ var check = setInterval(function() {
199+ if (migrating === undefined ||
200+ migrating === true) {
201+ // check (again) if migrating
f4aecc66 202+ me.UI.showStatus('Waiting for connection...', 'warning', 5000);
fe91e9e1
DC
203+ me.API2Request({
204+ url: me.baseUrl + '/config',
205+ method: 'GET',
206+ success: function(result) {
207+ var lock = result.data.lock;
208+ if (lock == 'migrate') {
209+ migrating = true;
210+ me.UI.showStatus('Migration detected, waiting...', 'warning', 5000);
211+ } else {
212+ migrating = false;
213+ }
214+ },
215+ failure: function() {
216+ migrating = false;
217+ }
218+ });
219+ } else {
220+ // not migrating any more
f4aecc66 221+ me.UI.showStatus('Connection resumed', 'warning');
fe91e9e1
DC
222+ clearInterval(check);
223+ me.API2Request({
224+ url: '/cluster/resources',
225+ method: 'GET',
226+ success: function(result) {
227+ var list = result.data;
228+ list.every(function(item) {
229+ if (item.type === 'lxc' && item.vmid == me.vmid) {
230+ var url = "?" + me.urlEncode({
231+ console: me.consoletype,
232+ novnc: 1,
233+ vmid: me.vmid,
234+ vmname: me.vmname,
235+ node: item.node,
236+ resize: me.resize
237+ });
238+ location.href = url;
239+ return false; // break
240+ }
241+ return true;
242+ });
243+ }
244+ });
245+ }
246+ }, 5000);
247+ }
248+
249+ },
250+
251+ pve_vm_command: function(cmd, params, reload) {
252+ var me = this;
253+ var baseUrl;
254+ var confirmMsg = "";
255+
256+ switch(cmd) {
257+ case "start":
258+ reload = 1;
259+ case "shutdown":
260+ case "stop":
261+ case "reset":
262+ case "suspend":
263+ case "resume":
264+ confirmMsg = "Do you really want to " + cmd + " VM/CT {0}?";
265+ break;
266+ case "reload":
267+ location.reload();
268+ break;
269+ default:
270+ throw "implement me " + cmd;
271+ }
272+
273+ confirmMsg = confirmMsg.replace('{0}', me.vmid);
274+
275+ if (confirmMsg !== "" && confirm(confirmMsg) !== true) {
276+ return;
277+ }
278+
279+ me.UI.closePVECommandPanel();
280+
281+ if (me.consoletype === 'kvm') {
282+ baseUrl = '/nodes/' + me.nodename + '/qemu/' + me.vmid;
283+ } else if (me.consoletype === 'lxc') {
284+ baseUrl = '/nodes/' + me.nodename + '/lxc/' + me.vmid;
285+ } else {
286+ throw "unknown VM type";
287+ }
288+
289+ me.API2Request({
290+ params: params,
291+ url: baseUrl + "/status/" + cmd,
292+ method: 'POST',
293+ failure: function(msg) {
294+ me.UI.showStatus(msg, 'warning');
295+ },
296+ success: function() {
297+ me.UI.showStatus("VM command '" + cmd +"' successful", 'normal');
298+ if (reload) {
299+ setTimeout(function() {
300+ location.reload();
301+ }, 1000);
302+ };
303+ }
304+ });
305+ },
306+
307+ addPVEHandlers: function() {
308+ var me = this;
309+ document.getElementById('pve_commands_button')
310+ .addEventListener('click', me.UI.togglePVECommandPanel);
311+
312+ // show/hide the buttons
fe91e9e1
DC
313+ document.getElementById('noVNC_disconnect_button')
314+ .classList.add('noVNC_hidden');
315+ if (me.consoletype === 'kvm') {
316+ document.getElementById('noVNC_clipboard_button')
317+ .classList.add('noVNC_hidden');
318+ }
319+
320+ if (me.consoletype === 'shell' || me.consoletype === 'upgrade') {
321+ document.getElementById('pve_commands_button')
322+ .classList.add('noVNC_hidden');
323+ }
324+
325+ // add command logic
326+ var commandArray = [
327+ { cmd: 'start', kvm: 1, lxc: 1},
328+ { cmd: 'stop', kvm: 1, lxc: 1},
329+ { cmd: 'shutdown', kvm: 1, lxc: 1},
330+ { cmd: 'suspend', kvm: 1},
331+ { cmd: 'resume', kvm: 1},
332+ { cmd: 'reset', kvm: 1},
333+ { cmd: 'reload', kvm: 1, lxc: 1, shell: 1},
334+ ];
335+
336+ commandArray.forEach(function(item) {
337+ var el = document.getElementById('pve_command_'+item.cmd);
338+ if (!el) {
339+ return;
340+ }
341+
342+ if (item[me.consoletype] === 1) {
343+ el.onclick = function() {
344+ me.pve_vm_command(item.cmd);
345+ };
346+ } else {
347+ el.classList.add('noVNC_hidden');
348+ }
349+ });
fe91e9e1
DC
350+ },
351+
352+ getFBSize: function() {
353+ var oh;
354+ var ow;
355+
356+ if (window.innerHeight) {
357+ oh = window.innerHeight;
358+ ow = window.innerWidth;
359+ } else if (document.documentElement &&
360+ document.documentElement.clientHeight) {
361+ oh = document.documentElement.clientHeight;
362+ ow = document.documentElement.clientWidth;
363+ } else if (document.body) {
364+ oh = document.body.clientHeight;
365+ ow = document.body.clientWidth;
366+ } else {
367+ throw "can't get window size";
368+ }
369+
370+ return { width: ow, height: oh };
371+ },
372+
373+ pveStart: function(callback) {
374+ var me = this;
375+ me.API2Request({
376+ url: me.url,
377+ method: 'POST',
378+ params: me.params,
379+ success: function(result) {
380+ var wsparams = me.urlEncode({
381+ port: result.data.port,
382+ vncticket: result.data.ticket
383+ });
384+
385+ document.getElementById('noVNC_password_input').value = result.data.ticket;
4cc9ed08 386+ me.UI.forceSetting('path', 'api2/json' + me.baseUrl + '/vncwebsocket' + "?" + wsparams);
fe91e9e1
DC
387+
388+ callback();
389+ },
390+ failure: function(msg) {
391+ me.UI.showStatus(msg, 'error');
392+ }
393+ });
394+ },
395+
4cc9ed08 396+ updateFBSize: function(rfb, width, height) {
fe91e9e1 397+ var me = this;
fe91e9e1
DC
398+ try {
399+ // Note: window size must be even number for firefox
400+ me.lastFBWidth = Math.floor((width + 1)/2)*2;
401+ me.lastFBHeight = Math.floor((height + 1)/2)*2;
402+
403+ if (me.sizeUpdateTimer !== undefined) {
404+ clearInterval(me.sizeUpdateTimer);
405+ }
fe91e9e1
DC
406+
407+ var update_size = function() {
4cc9ed08
DC
408+ var clip = me.UI.getSetting('view_clip');
409+ var resize = me.UI.getSetting('resize');
410+ var autoresize = me.UI.getSetting('autoresize');
411+ if (clip || resize === 'scale' || !autoresize) {
412+ return;
413+ }
414+
fe91e9e1
DC
415+ // we do not want to resize if we are in fullscreen
416+ if (document.fullscreenElement || // alternative standard method
417+ document.mozFullScreenElement || // currently working methods
418+ document.webkitFullscreenElement ||
419+ document.msFullscreenElement) {
420+ return;
421+ }
422+
423+ var oldsize = me.getFBSize();
424+ var offsetw = me.lastFBWidth - oldsize.width;
425+ var offseth = me.lastFBHeight - oldsize.height;
426+ if (offsetw !== 0 || offseth !== 0) {
427+ //console.log("try resize by " + offsetw + " " + offseth);
428+ try {
429+ window.resizeBy(offsetw, offseth);
fe91e9e1
DC
430+ } catch (e) {
431+ console.log('resizing did not work', e);
432+ }
433+ }
434+ };
435+
436+ update_size();
437+ me.sizeUpdateTimer = setInterval(update_size, 1000);
438+
439+ } catch(e) {
440+ console.log(e);
441+ }
442+ },
443+};
444diff --git a/app/ui.js b/app/ui.js
2b8dde16 445index 17ec48d..4683c02 100644
fe91e9e1
DC
446--- a/app/ui.js
447+++ b/app/ui.js
2b8dde16 448@@ -16,6 +16,7 @@ import keysyms from "../core/input/keysymdef.js";
4cc9ed08 449 import Keyboard from "../core/input/keyboard.js";
fe91e9e1 450 import RFB from "../core/rfb.js";
fe91e9e1
DC
451 import * as WebUtil from "./webutil.js";
452+import PVEUI from "./pve.js";
453
4cc9ed08 454 const UI = {
fe91e9e1 455
2b8dde16 456@@ -54,6 +55,8 @@ const UI = {
fe91e9e1 457 // Render default UI and initialize settings menu
2b8dde16 458 start() {
fe91e9e1 459
a375b7e5 460+ UI.PVE = new PVEUI(UI);
2b8dde16
DC
461+
462 UI.initSettings();
463
464 // Translate the DOM
465@@ -81,6 +84,9 @@ const UI = {
fe91e9e1
DC
466 UI.addConnectionControlHandlers();
467 UI.addClipboardHandlers();
468 UI.addSettingsHandlers();
469+
a375b7e5
DC
470+ // add pve specific event handlers
471+ UI.PVE.addPVEHandlers();
fe91e9e1
DC
472 document.getElementById("noVNC_status")
473 .addEventListener('click', UI.hideStatus);
474
2b8dde16 475@@ -89,19 +95,15 @@ const UI = {
fe91e9e1
DC
476
477 UI.openControlbar();
478
a375b7e5
DC
479+ UI.updateViewClip();
480+
481 UI.updateVisualState('init');
fe91e9e1 482
fe91e9e1
DC
483 document.documentElement.classList.remove("noVNC_loading");
484
4cc9ed08 485- let autoconnect = WebUtil.getConfigVar('autoconnect', false);
fe91e9e1
DC
486- if (autoconnect === 'true' || autoconnect == '1') {
487- autoconnect = true;
a375b7e5
DC
488+ UI.PVE.pveStart(function() {
489 UI.connect();
fe91e9e1
DC
490- } else {
491- autoconnect = false;
a375b7e5
DC
492- // Show the connect panel on first load unless autoconnecting
493- UI.openConnectPanel();
fe91e9e1 494- }
a375b7e5 495+ });
fe91e9e1 496
2b8dde16
DC
497 return Promise.resolve(UI.rfb);
498 },
499@@ -145,9 +147,10 @@ const UI = {
fe91e9e1
DC
500 /* Populate the controls if defaults are provided in the URL */
501 UI.initSetting('host', window.location.hostname);
502 UI.initSetting('port', port);
503- UI.initSetting('encrypt', (window.location.protocol === "https:"));
504+ UI.initSetting('encrypt', true);
a375b7e5 505 UI.initSetting('view_clip', false);
4cc9ed08
DC
506 UI.initSetting('resize', 'off');
507+ UI.initSetting('autoresize', true);
fe91e9e1
DC
508 UI.initSetting('shared', true);
509 UI.initSetting('view_only', false);
2b8dde16
DC
510 UI.initSetting('show_dot', false);
511@@ -334,6 +337,7 @@ const UI = {
4cc9ed08 512 UI.addSettingChangeHandler('resize');
4cc9ed08 513 UI.addSettingChangeHandler('resize', UI.applyResizeMode);
2b8dde16 514 UI.addSettingChangeHandler('resize', UI.updateViewClip);
4cc9ed08
DC
515+ UI.addSettingChangeHandler('autoresize');
516 UI.addSettingChangeHandler('view_clip');
517 UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
518 UI.addSettingChangeHandler('shared');
2b8dde16 519@@ -384,6 +388,9 @@ const UI = {
a375b7e5
DC
520 document.documentElement.classList.add("noVNC_connecting");
521 break;
fe91e9e1 522 case 'connected':
a375b7e5
DC
523+ UI.connected = true;
524+ UI.inhibit_reconnect = false;
525+ UI.pveAllowMigratedTest = true;
fe91e9e1 526 document.documentElement.classList.add("noVNC_connected");
a375b7e5
DC
527 break;
528 case 'disconnecting':
2b8dde16 529@@ -391,6 +398,11 @@ const UI = {
a375b7e5 530 document.documentElement.classList.add("noVNC_disconnecting");
fe91e9e1
DC
531 break;
532 case 'disconnected':
a375b7e5
DC
533+ UI.showStatus(_("Disconnected"));
534+ if (UI.pveAllowMigratedTest === true) {
535+ UI.pveAllowMigratedTest = false;
536+ UI.PVE.pve_detect_migrated_vm();
537+ }
fe91e9e1 538 break;
a375b7e5
DC
539 case 'reconnecting':
540 transition_elem.textContent = _("Reconnecting...");
2b8dde16 541@@ -805,6 +817,7 @@ const UI = {
a375b7e5 542 UI.closePowerPanel();
fe91e9e1
DC
543 UI.closeClipboardPanel();
544 UI.closeExtraKeys();
a375b7e5 545+ UI.closePVECommandPanel();
fe91e9e1
DC
546 },
547
548 /* ------^-------
2b8dde16 549@@ -980,6 +993,12 @@ const UI = {
a375b7e5 550 UI.reconnect_password = password;
fe91e9e1
DC
551 }
552
a375b7e5 553+ var password = document.getElementById('noVNC_password_input').value;
fe91e9e1 554+
a375b7e5
DC
555+ if (!password) {
556+ password = WebUtil.getConfigVar('password');
557+ }
fe91e9e1 558+
a375b7e5
DC
559 if (password === null) {
560 password = undefined;
561 }
2b8dde16 562@@ -1549,9 +1568,36 @@ const UI = {
fe91e9e1
DC
563 /* ------^-------
564 * /EXTRA KEYS
565 * ==============
566- * MISC
567+ * PVE
568 * ------v------*/
569
570+ togglePVECommandPanel: function() {
a375b7e5
DC
571+ if (document.getElementById('pve_commands').classList.contains("noVNC_open")) {
572+ UI.closePVECommandPanel();
573+ } else {
574+ UI.openPVECommandPanel();
575+ }
fe91e9e1
DC
576+ },
577+
578+ openPVECommandPanel: function() {
a375b7e5
DC
579+ var me = this;
580+ UI.closeAllPanels();
581+ UI.openControlbar();
fe91e9e1 582+
a375b7e5
DC
583+ document.getElementById('pve_commands').classList.add("noVNC_open");
584+ document.getElementById('pve_commands_button').classList.add("noVNC_selected");
fe91e9e1
DC
585+ },
586+
587+ closePVECommandPanel: function() {
a375b7e5
DC
588+ document.getElementById('pve_commands').classList.remove("noVNC_open");
589+ document.getElementById('pve_commands_button').classList.remove("noVNC_selected");
fe91e9e1
DC
590+ },
591+
592+/* ------^-------
a375b7e5 593+ * /PVE
fe91e9e1
DC
594+ * ==============
595+ * MISC
596+ * ------v------*/
4cc9ed08
DC
597 setMouseButton(num) {
598 const view_only = UI.rfb.viewOnly;
fe91e9e1 599 if (UI.rfb && !view_only) {
4cc9ed08 600diff --git a/vnc.html b/vnc.html
2b8dde16 601index 212321b..072d86b 100644
4cc9ed08
DC
602--- a/vnc.html
603+++ b/vnc.html
2b8dde16 604@@ -191,7 +191,7 @@
4cc9ed08 605 <li class="noVNC_heading">
2b8dde16 606 <img alt="" src="app/images/settings.svg"> Settings
4cc9ed08
DC
607 </li>
608- <li>
609+ <li style="display:none;">
2b8dde16 610 <label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
4cc9ed08
DC
611 </li>
612 <li>
2b8dde16 613@@ -201,16 +201,18 @@
4cc9ed08 614 <li>
2b8dde16
DC
615 <label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
616 </li>
617+ <li>
4cc9ed08
DC
618+ <label><input id="noVNC_setting_autoresize" type="checkbox" /> Autoresize Window</label>
619+ </li>
2b8dde16 620 <li>
4cc9ed08
DC
621 <label for="noVNC_setting_resize">Scaling Mode:</label>
622 <select id="noVNC_setting_resize" name="vncResize">
623- <option value="off">None</option>
624+ <option value="off">Off</option>
625 <option value="scale">Local Scaling</option>
626- <option value="remote">Remote Resizing</option>
627 </select>
628 </li>
629 <li><hr></li>
630- <li>
631+ <li style="display:none;">
632 <div class="noVNC_expander">Advanced</div>
633 <div><ul>
634 <li>