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