summaryrefslogtreecommitdiff
path: root/web/share/js/kvm
diff options
context:
space:
mode:
authorDevaev Maxim <[email protected]>2018-12-15 04:29:40 +0300
committerDevaev Maxim <[email protected]>2018-12-15 04:29:40 +0300
commit3c33bd37190772a783369894e209bcfe0858177a (patch)
treee095f08f37371a3182f6ced0b280c4bcaa06983b /web/share/js/kvm
parent3445766a50eab16a96d969397a6fe0422f7cfcd2 (diff)
own auth
Diffstat (limited to 'web/share/js/kvm')
-rw-r--r--web/share/js/kvm/atx.js43
-rw-r--r--web/share/js/kvm/hid.js198
-rw-r--r--web/share/js/kvm/keyboard.js190
-rw-r--r--web/share/js/kvm/main.js16
-rw-r--r--web/share/js/kvm/mouse.js160
-rw-r--r--web/share/js/kvm/msd.js193
-rw-r--r--web/share/js/kvm/session.js133
-rw-r--r--web/share/js/kvm/stream.js221
8 files changed, 1154 insertions, 0 deletions
diff --git a/web/share/js/kvm/atx.js b/web/share/js/kvm/atx.js
new file mode 100644
index 00000000..ed5f045e
--- /dev/null
+++ b/web/share/js/kvm/atx.js
@@ -0,0 +1,43 @@
+function Atx() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __init__ = function() {
+ $("atx-power-led").title = "Power Led";
+ $("atx-hdd-led").title = "Disk Activity Led";
+
+ tools.setOnClick($("atx-power-button"), () => __clickButton("power", "Are you sure to click the power button?"));
+ tools.setOnClick($("atx-power-button-long"), () => __clickButton("power_long", "Are you sure to perform the long press of the power button?"));
+ tools.setOnClick($("atx-reset-button"), () => __clickButton("reset", "Are you sure to reboot the server?"));
+ };
+
+ /********************************************************************************/
+
+ self.setState = function(state) {
+ $("atx-power-led").className = ((state && state.leds.power) ? "led-green" : "led-gray");
+ $("atx-hdd-led").className = ((state && state.leds.hdd) ? "led-red" : "led-gray");
+
+ wm.switchDisabled($("atx-power-button"), (!state || state.busy));
+ wm.switchDisabled($("atx-power-button-long"), (!state || state.busy));
+ wm.switchDisabled($("atx-reset-button"), (!state || state.busy));
+ };
+
+ var __clickButton = function(button, confirm_msg) {
+ wm.confirm(confirm_msg).then(function(ok) {
+ if (ok) {
+ var http = tools.makeRequest("POST", "/kvmd/atx/click?button=" + button, function() {
+ if (http.readyState === 4) {
+ if (http.status === 409) {
+ wm.error("Performing another ATX operation for other client.<br>Please try again later");
+ } else if (http.status !== 200) {
+ wm.error("Click error:<br>", http.responseText);
+ }
+ }
+ });
+ }
+ });
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js
new file mode 100644
index 00000000..49d659ab
--- /dev/null
+++ b/web/share/js/kvm/hid.js
@@ -0,0 +1,198 @@
+function Hid() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __ws = null;
+
+ var __chars_to_codes = {};
+ var __codes_delay = 50;
+
+ var __keyboard = new Keyboard();
+ var __mouse = new Mouse();
+
+ var __init__ = function() {
+ var __hidden_attr = null;
+ var __visibility_change_attr = null;
+
+ if (typeof document.hidden !== "undefined") {
+ __hidden_attr = "hidden";
+ __visibility_change_attr = "visibilitychange";
+ } else if (typeof document.webkitHidden !== "undefined") {
+ __hidden_attr = "webkitHidden";
+ __visibility_change_attr = "webkitvisibilitychange";
+ } else if (typeof document.mozHidden !== "undefined") {
+ __hidden_attr = "mozHidden";
+ __visibility_change_attr = "mozvisibilitychange";
+ }
+
+ if (__visibility_change_attr) {
+ document.addEventListener(
+ __visibility_change_attr,
+ function() {
+ if (document[__hidden_attr]) {
+ __releaseAll();
+ }
+ },
+ false
+ );
+ }
+
+ window.addEventListener("pagehide", __releaseAll);
+ window.addEventListener("blur", __releaseAll);
+
+ __chars_to_codes = __buildCharsToCodes();
+
+ tools.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton);
+ tools.setOnClick($("hid-reset-button"), __clickResetButton);
+
+ Array.prototype.forEach.call(document.querySelectorAll("[data-shortcut]"), function(el_shortcut) {
+ tools.setOnClick(el_shortcut, () => __emitShortcut(el_shortcut.getAttribute("data-shortcut").split(" ")));
+ });
+ };
+
+ /********************************************************************************/
+
+ self.setSocket = function(ws) {
+ wm.switchDisabled($("hid-pak-text"), !ws);
+ wm.switchDisabled($("hid-pak-button"), !ws);
+ wm.switchDisabled($("hid-reset-button"), !ws);
+ __ws = ws;
+ __keyboard.setSocket(ws);
+ __mouse.setSocket(ws);
+ };
+
+ var __releaseAll = function() {
+ __keyboard.releaseAll();
+ };
+
+ var __emitShortcut = function(codes) {
+ return new Promise(function(resolve) {
+ tools.debug("HID: emitting keys:", codes);
+
+ var raw_events = [];
+ [[codes, true], [codes.slice().reverse(), false]].forEach(function(op) {
+ var [op_codes, state] = op;
+ op_codes.forEach(function(code) {
+ raw_events.push({code: code, state: state});
+ });
+ });
+
+ var index = 0;
+ var iterate = () => setTimeout(function() {
+ __keyboard.fireEvent(raw_events[index].code, raw_events[index].state);
+ ++index;
+ if (index < raw_events.length) {
+ iterate();
+ } else {
+ resolve(null);
+ }
+ }, __codes_delay);
+ iterate();
+ });
+ };
+
+ var __buildCharsToCodes = function() {
+ var chars_to_codes = {
+ "\n": ["Enter"],
+ "\t": ["Tab"],
+ " ": ["Space"],
+ "`": ["Backquote"], "~": ["ShiftLeft", "Backquote"],
+ "\\": ["Backslash"], "|": ["ShiftLeft", "Backslash"],
+ "[": ["BracketLeft"], "{": ["ShiftLeft", "BracketLeft"],
+ "]": ["BracketLeft"], "}": ["ShiftLeft", "BracketRight"],
+ ",": ["Comma"], "<": ["ShiftLeft", "Comma"],
+ ".": ["Period"], ">": ["ShiftLeft", "Period"],
+ "1": ["Digit1"], "!": ["ShiftLeft", "Digit1"],
+ "2": ["Digit2"], "@": ["ShiftLeft", "Digit2"],
+ "3": ["Digit3"], "#": ["ShiftLeft", "Digit3"],
+ "4": ["Digit4"], "$": ["ShiftLeft", "Digit4"],
+ "5": ["Digit5"], "%": ["ShiftLeft", "Digit5"],
+ "6": ["Digit6"], "^": ["ShiftLeft", "Digit6"],
+ "7": ["Digit7"], "&": ["ShiftLeft", "Digit7"],
+ "8": ["Digit8"], "*": ["ShiftLeft", "Digit8"],
+ "9": ["Digit9"], "(": ["ShiftLeft", "Digit9"],
+ "0": ["Digit0"], ")": ["ShiftLeft", "Digit0"],
+ "-": ["Minus"], "_": ["ShiftLeft", "Minus"],
+ "'": ["Quote"], "\"": ["ShiftLeft", "Quote"],
+ ";": ["Semicolon"], ":": ["ShiftLeft", "Semicolon"],
+ "/": ["Slash"], "?": ["ShiftLeft", "Slash"],
+ "=": ["Equal"], "+": ["ShiftLeft", "Equal"],
+ };
+
+ for (var ch = "a".charCodeAt(0); ch <= "z".charCodeAt(0); ++ch) {
+ var low = String.fromCharCode(ch);
+ var up = low.toUpperCase();
+ var code = "Key" + up;
+ chars_to_codes[low] = [code];
+ chars_to_codes[up] = ["ShiftLeft", code];
+ }
+
+ return chars_to_codes;
+ };
+
+ var __clickPasteAsKeysButton = function() {
+ var text = $("hid-pak-text").value.replace(/[^\x00-\x7F]/g, ""); // eslint-disable-line no-control-regex
+ if (text) {
+ var clipboard_codes = [];
+ var codes_count = 0;
+ [...text].forEach(function(ch) {
+ var codes = __chars_to_codes[ch];
+ if (codes) {
+ codes_count += codes.length;
+ clipboard_codes.push(codes);
+ }
+ });
+ var time = __codes_delay * codes_count * 2 / 1000;
+
+ var confirm_msg = `
+ You are going to automatically type ${codes_count} characters from the system clipboard.
+ It will take ${time} seconds.<br>
+ <br>
+ Are you sure you want to continue?
+ `;
+
+ wm.confirm(confirm_msg).then(function(ok) {
+ if (ok) {
+ wm.switchDisabled($("hid-pak-text"), true);
+ wm.switchDisabled($("hid-pak-button"), true);
+ $("hid-pak-led").className = "led-yellow-rotating-fast";
+ $("hid-pak-led").title = "Autotyping...";
+
+ tools.debug("HID: paste-as-keys:", text);
+
+ var index = 0;
+ var iterate = function() {
+ __emitShortcut(clipboard_codes[index]).then(function() {
+ ++index;
+ if (index < clipboard_codes.length && __ws) {
+ iterate();
+ } else {
+ $("hid-pak-text").value = "";
+ wm.switchDisabled($("hid-pak-text"), false);
+ wm.switchDisabled($("hid-pak-button"), false);
+ $("hid-pak-led").className = "led-gray";
+ $("hid-pak-led").title = "";
+ }
+ });
+ };
+ iterate();
+ } else {
+ $("hid-pak-text").value = "";
+ }
+ });
+ }
+ };
+
+ var __clickResetButton = function() {
+ var http = tools.makeRequest("POST", "/kvmd/hid/reset", function() {
+ if (http.readyState === 4) {
+ if (http.status !== 200) {
+ wm.error("HID reset error:<br>", http.responseText);
+ }
+ }
+ });
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js
new file mode 100644
index 00000000..3c821406
--- /dev/null
+++ b/web/share/js/kvm/keyboard.js
@@ -0,0 +1,190 @@
+function Keyboard() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __ws = null;
+
+ var __keys = [].slice.call(document.querySelectorAll("div#keyboard-desktop div.keyboard-block div.keyboard-row div.key"));
+ var __modifiers = [].slice.call(document.querySelectorAll("div#keyboard-desktop div.keyboard-block div.keyboard-row div.modifier"));
+
+ var __init__ = function() {
+ $("hid-keyboard-led").title = "Keyboard free";
+
+ $("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true);
+ $("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false);
+ $("keyboard-window").onfocus = __updateLeds;
+ $("keyboard-window").onblur = __updateLeds;
+
+ $("stream-window").onkeydown = (event) => __keyboardHandler(event, true);
+ $("stream-window").onkeyup = (event) => __keyboardHandler(event, false);
+ $("stream-window").onfocus = __updateLeds;
+ $("stream-window").onblur = __updateLeds;
+
+ window.addEventListener("focusin", __updateLeds);
+ window.addEventListener("focusout", __updateLeds);
+
+ Array.prototype.forEach.call($$("key"), function(el_key) {
+ tools.setOnDown(el_key, () => __clickHandler(el_key, true));
+ tools.setOnUp(el_key, () => __clickHandler(el_key, false));
+ el_key.onmouseout = function() {
+ if (__isPressed(el_key)) {
+ __clickHandler(el_key, false);
+ }
+ };
+ });
+
+ Array.prototype.forEach.call($$("modifier"), function(el_key) {
+ tools.setOnDown(el_key, () => __toggleModifierHandler(el_key));
+ });
+
+ if (tools.browser.is_mac) {
+ tools.info("Keyboard: enabled Mac-CMD-Hook");
+ }
+ };
+
+ /********************************************************************************/
+
+ self.setSocket = function(ws) {
+ if (ws !== __ws) {
+ self.releaseAll();
+ __ws = ws;
+ }
+ __updateLeds();
+ };
+
+ self.releaseAll = function() {
+ __keys.concat(__modifiers).forEach(function(el_key) {
+ if (__isActive(el_key)) {
+ self.fireEvent(el_key.getAttribute("data-key"), false);
+ }
+ });
+ };
+
+ self.fireEvent = function(code, state) {
+ __keyboardHandler({code: code}, state);
+ };
+
+ var __updateLeds = function() {
+ tools.debug("Keyboard: update leds");
+ if (
+ __ws && (
+ $("stream-window").classList.contains("window-active")
+ || $("keyboard-window").classList.contains("window-active")
+ )
+ ) {
+ $("hid-keyboard-led").className = "led-green";
+ $("hid-keyboard-led").title = "Keyboard captured";
+ } else {
+ $("hid-keyboard-led").className = "led-gray";
+ $("hid-keyboard-led").title = "Keyboard free";
+ }
+ };
+
+ var __keyboardHandler = function(event, state) {
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
+ var el_key = document.querySelector(`[data-key='${event.code}']`);
+ if (el_key && !event.repeat) {
+ __commonHandler(el_key, state, "pressed");
+ if (tools.browser.is_mac) {
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=28089
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1299553
+ if ((event.code === "MetaLeft" || event.code === "MetaRight") && !state) {
+ __keys.forEach(function(el_key) {
+ if (__isActive(el_key)) {
+ self.fireEvent(el_key.getAttribute("data-key"), false);
+ }
+ });
+ }
+ }
+ __unholdModifiers();
+ }
+ };
+
+ var __clickHandler = function(el_key, state) {
+ __commonHandler(el_key, state, "pressed");
+ __unholdModifiers();
+ };
+
+ var __toggleModifierHandler = function(el_key) {
+ __commonHandler(el_key, !__isActive(el_key), "holded");
+ };
+
+ var __unholdModifiers = function() {
+ __modifiers.forEach(function(el_key) {
+ if (__isHolded(el_key)) {
+ __deactivate(el_key);
+ __sendKey(el_key, false);
+ }
+ });
+ };
+
+ var __commonHandler = function(el_key, state, cls) {
+ if (state && !__isActive(el_key)) {
+ __deactivate(el_key);
+ __activate(el_key, cls);
+ __sendKey(el_key, true);
+ } else {
+ __deactivate(el_key);
+ __sendKey(el_key, false);
+ }
+ };
+
+ var __isPressed = function(el_key) {
+ var is_pressed = false;
+ Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) {
+ is_pressed = (is_pressed || el_key.classList.contains("pressed"));
+ });
+ return is_pressed;
+ };
+
+ var __isHolded = function(el_key) {
+ var is_holded = false;
+ Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) {
+ is_holded = (is_holded || el_key.classList.contains("holded"));
+ });
+ return is_holded;
+ };
+
+ var __isActive = function(el_key) {
+ var is_active = false;
+ Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) {
+ is_active = (is_active || el_key.classList.contains("pressed") || el_key.classList.contains("holded"));
+ });
+ return is_active;
+ };
+
+ var __activate = function(el_key, cls) {
+ Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) {
+ el_key.classList.add(cls);
+ });
+ };
+
+ var __deactivate = function(el_key) {
+ Array.prototype.forEach.call(__resolveKeys(el_key), function(el_key) {
+ el_key.classList.remove("pressed");
+ el_key.classList.remove("holded");
+ });
+ };
+
+ var __resolveKeys = function(el_key) {
+ var code = el_key.getAttribute("data-key");
+ return document.querySelectorAll(`[data-key='${code}']`);
+ };
+
+ var __sendKey = function(el_key, state) {
+ var code = el_key.getAttribute("data-key");
+ tools.debug("Keyboard: key", (state ? "pressed:" : "released:"), code);
+ if (__ws) {
+ __ws.send(JSON.stringify({
+ event_type: "key",
+ key: code,
+ state: state,
+ }));
+ }
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/main.js b/web/share/js/kvm/main.js
new file mode 100644
index 00000000..5c6d775a
--- /dev/null
+++ b/web/share/js/kvm/main.js
@@ -0,0 +1,16 @@
+var wm;
+
+function main() {
+ if (checkBrowser()) {
+ wm = new WindowManager();
+
+ tools.setOnClick($("show-about-button"), () => wm.showWindow($("about-window")));
+ tools.setOnClick($("show-keyboard-button"), () => wm.showWindow($("keyboard-window")));
+ tools.setOnClick($("show-stream-button"), () => wm.showWindow($("stream-window")));
+ tools.setOnClick($("open-log-button"), () => window.open("/kvmd/log?seek=3600&follow=1", "_blank"));
+
+ wm.showWindow($("stream-window"));
+
+ new Session();
+ }
+}
diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js
new file mode 100644
index 00000000..58e46d15
--- /dev/null
+++ b/web/share/js/kvm/mouse.js
@@ -0,0 +1,160 @@
+function Mouse() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __ws = null;
+
+ var __current_pos = {x: 0, y:0};
+ var __sent_pos = {x: 0, y:0};
+ var __wheel_delta = {x: 0, y: 0};
+
+ var __stream_hovered = false;
+
+ var __init__ = function() {
+ $("hid-mouse-led").title = "Mouse free";
+
+ $("stream-box").onmouseenter = __hoverStream;
+ $("stream-box").onmouseleave = __leaveStream;
+ $("stream-box").onmousedown = (event) => __buttonHandler(event, true);
+ $("stream-box").onmouseup = (event) => __buttonHandler(event, false);
+ $("stream-box").oncontextmenu = (event) => event.preventDefault();
+ $("stream-box").onmousemove = __moveHandler;
+ $("stream-box").onwheel = __wheelHandler;
+ $("stream-box").ontouchstart = (event) => __touchMoveHandler(event);
+
+ Array.prototype.forEach.call(document.querySelectorAll("[data-mouse-button]"), function(el_button) {
+ var button = el_button.getAttribute("data-mouse-button");
+ tools.setOnDown(el_button, () => __sendButton(button, true));
+ tools.setOnUp(el_button, () => __sendButton(button, false));
+ });
+
+ setInterval(__sendMove, 100);
+ };
+
+ /********************************************************************************/
+
+ self.setSocket = function(ws) {
+ __ws = ws;
+ if (ws) {
+ $("stream-box").classList.add("stream-box-mouse-enabled");
+ } else {
+ $("stream-box").classList.remove("stream-box-mouse-enabled");
+ }
+ __updateLeds();
+ };
+
+ var __hoverStream = function() {
+ __stream_hovered = true;
+ __updateLeds();
+ };
+
+ var __leaveStream = function() {
+ __stream_hovered = false;
+ __updateLeds();
+ };
+
+ var __updateLeds = function() {
+ if (__ws && (__stream_hovered || tools.browser.is_ios)) {
+ // Mouse is always available on iOS via touchscreen
+ $("hid-mouse-led").className = "led-green";
+ $("hid-mouse-led").title = "Mouse tracked";
+ } else {
+ $("hid-mouse-led").className = "led-gray";
+ $("hid-mouse-led").title = "Mouse free";
+ }
+ };
+
+ var __buttonHandler = function(event, state) {
+ // https://www.w3schools.com/jsref/event_button.asp
+ event.preventDefault();
+ switch (event.button) {
+ case 0: __sendButton("left", state); break;
+ case 2: __sendButton("right", state); break;
+ }
+ };
+
+ var __touchMoveHandler = function(event) {
+ event.preventDefault();
+ if (event.touches[0].target && event.touches[0].target.getBoundingClientRect) {
+ var rect = event.touches[0].target.getBoundingClientRect();
+ __current_pos = {
+ x: Math.round(event.touches[0].clientX - rect.left),
+ y: Math.round(event.touches[0].clientY - rect.top),
+ };
+ __sendMove();
+ }
+ };
+
+ var __moveHandler = function(event) {
+ var rect = event.target.getBoundingClientRect();
+ __current_pos = {
+ x: Math.round(event.clientX - rect.left),
+ y: Math.round(event.clientY - rect.top),
+ };
+ };
+
+
+ var __sendButton = function(button, state) {
+ tools.debug("Mouse: button", (state ? "pressed:" : "released:"), button);
+ __sendMove();
+ if (__ws) {
+ __ws.send(JSON.stringify({
+ event_type: "mouse_button",
+ button: button,
+ state: state,
+ }));
+ }
+ };
+
+ var __sendMove = function() {
+ var pos = __current_pos;
+ if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
+ var el_stream_image = $("stream-image");
+ var to = {
+ x: __translate(pos.x, 0, el_stream_image.clientWidth, -32768, 32767),
+ y: __translate(pos.y, 0, el_stream_image.clientHeight, -32768, 32767),
+ };
+
+ tools.debug("Mouse: moved:", to);
+ if (__ws) {
+ __ws.send(JSON.stringify({
+ event_type: "mouse_move",
+ to: to,
+ }));
+ }
+ __sent_pos = pos;
+ }
+ };
+
+ var __translate = function(x, a, b, c, d) {
+ return Math.round((x - a) / (b - a) * (d - c) + c);
+ };
+
+ var __wheelHandler = function(event) {
+ // https://learn.javascript.ru/mousewheel
+ if (event.preventDefault) {
+ event.preventDefault();
+ }
+
+ var delta = {x: 0, y: 0};
+
+ __wheel_delta.y += event.deltaY;
+ if (Math.abs(__wheel_delta.y) >= 100) {
+ delta.y = __wheel_delta.y / Math.abs(__wheel_delta.y) * (-5);
+ __wheel_delta.y = 0;
+ }
+
+ if (delta.y) {
+ tools.debug("Mouse: scrolled:", delta);
+ if (__ws) {
+ __ws.send(JSON.stringify({
+ event_type: "mouse_wheel",
+ delta: delta,
+ }));
+ }
+ }
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/msd.js b/web/share/js/kvm/msd.js
new file mode 100644
index 00000000..c23de99d
--- /dev/null
+++ b/web/share/js/kvm/msd.js
@@ -0,0 +1,193 @@
+function Msd() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __state = null;
+ var __upload_http = null;
+ var __image_file = null;
+
+ var __init__ = function() {
+ $("msd-led").title = "Unknown state";
+
+ $("msd-select-new-image-file").onchange = __selectNewImageFile;
+ tools.setOnClick($("msd-select-new-image-button"), () => $("msd-select-new-image-file").click());
+
+ tools.setOnClick($("msd-upload-new-image-button"), __clickUploadNewImageButton);
+ tools.setOnClick($("msd-abort-uploading-button"), __clickAbortUploadingButton);
+
+ tools.setOnClick($("msd-switch-to-kvm-button"), () => __clickSwitchButton("kvm"));
+ tools.setOnClick($("msd-switch-to-server-button"), () => __clickSwitchButton("server"));
+
+ tools.setOnClick($("msd-reset-button"), __clickResetButton);
+ };
+
+ /********************************************************************************/
+
+ self.setState = function(state) {
+ __state = state;
+ __applyState();
+ };
+
+ var __clickUploadNewImageButton = function() {
+ var form_data = new FormData();
+ form_data.append("image_name", __image_file.name);
+ form_data.append("image_data", __image_file);
+
+ __upload_http = new XMLHttpRequest();
+ __upload_http.open("POST", "/kvmd/msd/write", true);
+ __upload_http.upload.timeout = 5000;
+ __upload_http.onreadystatechange = __uploadStateChange;
+ __upload_http.upload.onprogress = __uploadProgress;
+ __upload_http.send(form_data);
+ };
+
+ var __clickAbortUploadingButton = function() {
+ __upload_http.onreadystatechange = null;
+ __upload_http.upload.onprogress = null;
+ __upload_http.abort();
+ __upload_http = null;
+ $("msd-progress").setAttribute("data-label", "Aborted");
+ $("msd-progress-value").style.width = "0%";
+ };
+
+ var __clickSwitchButton = function(to) {
+ var http = tools.makeRequest("POST", "/kvmd/msd/connect?to=" + to, function() {
+ if (http.readyState === 4) {
+ if (http.status !== 200) {
+ wm.error("Switch error:<br>", http.responseText);
+ }
+ }
+ __applyState();
+ });
+ __applyState();
+ wm.switchDisabled($(`msd-switch-to-${to}-button`), true);
+ };
+
+ var __selectNewImageFile = function() {
+ var el_input = $("msd-select-new-image-file");
+ var image_file = (el_input.files.length ? el_input.files[0] : null);
+ if (image_file && image_file.size > __state.info.size) {
+ wm.error("New image is too big for your Mass Storage Device.<br>Maximum:", __formatSize(__state.info.size));
+ el_input.value = "";
+ image_file = null;
+ }
+ __image_file = image_file;
+ __applyState();
+ };
+
+ var __clickResetButton = function() {
+ var http = tools.makeRequest("POST", "/kvmd/msd/reset", function() {
+ if (http.readyState === 4) {
+ if (http.status !== 200) {
+ wm.error("MSD reset error:<br>", http.responseText);
+ }
+ }
+ __applyState();
+ });
+ __applyState();
+ };
+
+ var __applyState = function() {
+ if (__state) {
+ if (__state.connected_to === "server") {
+ $("msd-another-another-user-uploads").style.display = "none";
+ $("msd-led").className = "led-green";
+ $("msd-status").innerHTML = $("msd-led").title = "Connected to Server";
+ } else if (__state.busy) {
+ if (!__upload_http) {
+ $("msd-another-another-user-uploads").style.display = "block";
+ }
+ $("msd-led").className = "led-yellow-rotating-fast";
+ $("msd-status").innerHTML = $("msd-led").title = "Uploading new image";
+ } else {
+ $("msd-another-another-user-uploads").style.display = "none";
+ $("msd-led").className = "led-gray";
+ if (__state.in_operate) {
+ $("msd-status").innerHTML = $("msd-led").title = "Connected to KVM";
+ } else {
+ $("msd-status").innerHTML = $("msd-led").title = "Unavailable";
+ }
+ }
+
+ $("msd-not-in-operate").style.display = (__state.in_operate ? "none" : "block");
+ $("msd-current-image-broken").style.display = (
+ __state.in_operate && __state.info.image &&
+ !__state.info.image.complete && !__state.busy ? "block" : "none"
+ );
+
+ $("msd-current-image-name").innerHTML = (__state.in_operate && __state.info.image ? __state.info.image.name : "None");
+ $("msd-current-image-size").innerHTML = (__state.in_operate && __state.info.image ? __formatSize(__state.info.image.size) : "None");
+ $("msd-storage-size").innerHTML = (__state.in_operate ? __formatSize(__state.info.size) : "Unavailable");
+
+ wm.switchDisabled($("msd-switch-to-kvm-button"), (!__state.in_operate || __state.connected_to === "kvm" || __state.busy));
+ wm.switchDisabled($("msd-switch-to-server-button"), (!__state.in_operate || __state.connected_to === "server" || __state.busy));
+ wm.switchDisabled($("msd-select-new-image-button"), (!__state.in_operate || __state.connected_to !== "kvm" || __state.busy || __upload_http));
+ wm.switchDisabled($("msd-upload-new-image-button"), (!__state.in_operate || __state.connected_to !== "kvm" || __state.busy || !__image_file));
+ wm.switchDisabled($("msd-abort-uploading-button"), (!__state.in_operate || !__upload_http));
+ wm.switchDisabled($("msd-reset-button"), (!__state.in_operate || __upload_http));
+
+ $("msd-new-image").style.display = (__image_file ? "block" : "none");
+ $("msd-progress").setAttribute("data-label", "Waiting for upload ...");
+ $("msd-progress-value").style.width = "0%";
+ $("msd-new-image-name").innerHTML = (__image_file ? __image_file.name : "");
+ $("msd-new-image-size").innerHTML = (__image_file ? __formatSize(__image_file.size) : "");
+
+ } else {
+ $("msd-another-another-user-uploads").style.display = "none";
+ $("msd-led").className = "led-gray";
+ $("msd-status").innerHTML = "";
+ $("msd-led").title = "";
+ $("msd-not-in-operate").style.display = "none";
+ $("msd-current-image-broken").style.display = "none";
+ $("msd-current-image-name").innerHTML = "";
+ $("msd-current-image-size").innerHTML = "";
+ $("msd-storage-size").innerHTML = "";
+
+ wm.switchDisabled($("msd-switch-to-kvm-button"), true);
+ wm.switchDisabled($("msd-switch-to-server-button"), true);
+ wm.switchDisabled($("msd-select-new-image-button"), true);
+ wm.switchDisabled($("msd-upload-new-image-button"), true);
+ wm.switchDisabled($("msd-abort-uploading-button"), true);
+ wm.switchDisabled($("msd-reset-button"), true);
+
+ $("msd-select-new-image-file").value = "";
+ $("msd-new-image").style.display = "none";
+ $("msd-progress").setAttribute("data-label", "");
+ $("msd-progress-value").style.width = "0%";
+ $("msd-new-image-name").innerHTML = "";
+ $("msd-new-image-size").innerHTML = "";
+ }
+ };
+
+ var __formatSize = function(size) {
+ if (size > 0) {
+ var index = Math.floor( Math.log(size) / Math.log(1024) );
+ return (size / Math.pow(1024, index)).toFixed(2) * 1 + " " + ["B", "kB", "MB", "GB", "TB"][index];
+ } else {
+ return 0;
+ }
+ };
+
+ var __uploadStateChange = function() {
+ if (__upload_http.readyState === 4) {
+ if (__upload_http.status !== 200) {
+ wm.error("Can't upload image to the Mass Storage Device:<br>", __upload_http.responseText);
+ }
+ $("msd-select-new-image-file").value = "";
+ __image_file = null;
+ __upload_http = null;
+ __applyState();
+ }
+ };
+
+ var __uploadProgress = function(event) {
+ if(event.lengthComputable) {
+ var percent = Math.round((event.loaded * 100) / event.total);
+ $("msd-progress").setAttribute("data-label", percent + "%");
+ $("msd-progress-value").style.width = percent + "%";
+ }
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/session.js b/web/share/js/kvm/session.js
new file mode 100644
index 00000000..5ba43511
--- /dev/null
+++ b/web/share/js/kvm/session.js
@@ -0,0 +1,133 @@
+function Session() {
+ // var self = this;
+
+ /********************************************************************************/
+
+ var __ws = null;
+
+ var __ping_timer = null;
+ var __missed_heartbeats = 0;
+
+ var __hid = new Hid();
+ var __atx = new Atx();
+ var __msd = new Msd();
+ var __streamer = new Streamer();
+
+ var __init__ = function() {
+ __startSession();
+ };
+
+ /********************************************************************************/
+
+ var __setKvmdInfo = function(state) {
+ if (state.meta) {
+ var text = JSON.stringify(state.meta, undefined, 4).replace(/ /g, "&nbsp;").replace(/\n/g, "<br>");
+ $("about-meta").innerHTML = `
+ <span class="code-comment">// The Pi-KVM metadata.<br>
+ // You can get this json using handle <a target="_blank" href="/kvmd/info">/kvmd/info</a>.<br>
+ // In the standard configuration this data<br>
+ // is specified in the file /etc/kvmd/meta.yaml.</span><br>
+ <br>
+ ${text}
+ `;
+ if (state.meta.server && state.meta.server.host) {
+ $("kvmd-meta-server-host").innerHTML = "Server: " + state.meta.server.host;
+ document.title = "Pi-KVM Session: " + state.meta.server.host;
+ } else {
+ $("kvmd-meta-server-host").innerHTML = "";
+ document.title = "Pi-KVM Session";
+ }
+ }
+
+ $("about-version-kvmd").innerHTML = state.version.kvmd;
+ $("about-version-streamer").innerHTML = `${state.version.streamer} (${state.streamer})`;
+ };
+
+ var __startSession = function() {
+ $("link-led").className = "led-yellow";
+ $("link-led").title = "Connecting...";
+ var proto = (location.protocol === "https:" ? "wss" : "ws");
+ __ws = new WebSocket(`${proto}://${location.host}/kvmd/ws`);
+ __ws.onopen = __wsOpenHandler;
+ __ws.onmessage = __wsMessageHandler;
+ __ws.onerror = __wsErrorHandler;
+ __ws.onclose = __wsCloseHandler;
+ };
+
+ var __wsOpenHandler = function(event) {
+ tools.debug("Session: socket opened:", event);
+ $("link-led").className = "led-green";
+ $("link-led").title = "Connected";
+ __hid.setSocket(__ws);
+ __missed_heartbeats = 0;
+ __ping_timer = setInterval(__pingServer, 1000);
+ };
+
+ var __wsMessageHandler = function(event) {
+ // tools.debug("Session: received socket data:", event.data);
+ event = JSON.parse(event.data);
+ if (event.msg_type === "pong") {
+ __missed_heartbeats = 0;
+ } else if (event.msg_type === "event") {
+ if (event.msg.event === "info_state") {
+ __setKvmdInfo(event.msg.event_attrs);
+ } else if (event.msg.event === "atx_state") {
+ __atx.setState(event.msg.event_attrs);
+ } else if (event.msg.event === "msd_state") {
+ __msd.setState(event.msg.event_attrs);
+ } else if (event.msg.event === "streamer_state") {
+ __streamer.setState(event.msg.event_attrs);
+ }
+ }
+ };
+
+ var __wsErrorHandler = function(event) {
+ tools.error("Session: socket error:", event);
+ if (__ws) {
+ __ws.onclose = null;
+ __ws.close();
+ __wsCloseHandler(null);
+ }
+ };
+
+ var __wsCloseHandler = function(event) {
+ tools.debug("Session: socket closed:", event);
+
+ $("link-led").className = "led-gray";
+
+ if (__ping_timer) {
+ clearInterval(__ping_timer);
+ __ping_timer = null;
+ }
+
+ __hid.setSocket(null);
+ __atx.setState(null);
+ __msd.setState(null);
+ __streamer.setState(null);
+ __ws = null;
+
+ setTimeout(function() {
+ $("link-led").className = "led-yellow";
+ setTimeout(__startSession, 500);
+ }, 500);
+ };
+
+ var __pingServer = function() {
+ try {
+ __missed_heartbeats += 1;
+ if (__missed_heartbeats >= 5) {
+ throw new Error("Too many missed heartbeats");
+ }
+ __ws.send(JSON.stringify({"event_type": "ping"}));
+ } catch (err) {
+ tools.error("Session: ping error:", err.message);
+ if (__ws) {
+ __ws.onclose = null;
+ __ws.close();
+ __wsCloseHandler(null);
+ }
+ }
+ };
+
+ __init__();
+}
diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js
new file mode 100644
index 00000000..01ded5bc
--- /dev/null
+++ b/web/share/js/kvm/stream.js
@@ -0,0 +1,221 @@
+function Streamer() {
+ var self = this;
+
+ /********************************************************************************/
+
+ var __resolution = {width: 640, height: 480};
+ var __size_factor = 1;
+ var __client_key = tools.makeId();
+ var __client_id = "";
+ var __client_fps = -1;
+ var __prev = false;
+
+ var __init__ = function() {
+ $("stream-led").title = "Stream inactive";
+
+ $("stream-quality-slider").min = 5;
+ $("stream-quality-slider").max = 100;
+ $("stream-quality-slider").step = 5;
+ $("stream-quality-slider").value = 80;
+ tools.setOnUpSlider($("stream-quality-slider"), 1000, __updateQualityValue, (value) => __sendParam("quality", value));
+
+ $("stream-desired-fps-slider").min = 0;
+ $("stream-desired-fps-slider").max = 30;
+ $("stream-desired-fps-slider").step = 1;
+ $("stream-desired-fps-slider").value = 0;
+ tools.setOnUpSlider($("stream-desired-fps-slider"), 1000, __updateDesiredFpsValue, (value) => __sendParam("desired_fps", value));
+
+ $("stream-size-slider").min = 20;
+ $("stream-size-slider").max = 200;
+ $("stream-size-slider").step = 5;
+ $("stream-size-slider").value = 100;
+ $("stream-size-slider").oninput = () => __resize();
+ $("stream-size-slider").onchange = () => __resize();
+
+ tools.setOnClick($("stream-screenshot-button"), __clickScreenshotButton);
+ tools.setOnClick($("stream-reset-button"), __clickResetButton);
+ };
+
+ /********************************************************************************/
+
+ self.setState = function(state) {
+ if (state && state.state) {
+ var source = state.state.source;
+ var stream = state.state.stream;
+
+ if (!__prev) {
+ $("stream-quality-slider").activated = false;
+ $("stream-desired-fps-slider").activated = false;
+ }
+
+ if (!$("stream-quality-slider").activated) {
+ wm.switchDisabled($("stream-quality-slider"), false);
+ if ($("stream-quality-slider").value !== source.quality) {
+ $("stream-quality-slider").value = source.quality;
+ __updateQualityValue(source.quality);
+ }
+ }
+
+ if (!$("stream-desired-fps-slider").activated) {
+ wm.switchDisabled($("stream-desired-fps-slider"), false);
+ if ($("stream-desired-fps-slider").value !== source.desired_fps) {
+ $("stream-desired-fps-slider").value = source.desired_fps;
+ __updateDesiredFpsValue(source.desired_fps);
+ }
+ }
+
+ if (__resolution.width !== source.resolution.width || __resolution.height !== source.resolution.height) {
+ __resolution = source.resolution;
+ if ($("stream-auto-resize-checkbox").checked) {
+ __adjustSizeFactor();
+ } else {
+ __applySizeFactor();
+ }
+ }
+
+ var stream_client = tools.getCookie("stream_client");
+ if (!__client_id && stream_client && stream_client.startsWith(__client_key + "/")) {
+ tools.info("Stream: found acceptable stream_client cookie:", stream_client);
+ __client_id = stream_client.slice(stream_client.indexOf("/") + 1);
+ }
+
+ if (stream.clients_stat.hasOwnProperty(__client_id)) {
+ __client_fps = stream.clients_stat[__client_id].fps;
+ } else {
+ __clearState();
+ }
+
+ if (!__prev) {
+ var path = "/streamer/stream?key=" + __client_key;
+ if (tools.browser.is_chrome || tools.browser.is_blink) {
+ // uStreamer fix for Blink https://bugs.chromium.org/p/chromium/issues/detail?id=527446
+ tools.info("Stream: using advance_headers=1 to fix Blink MJPG bugs");
+ path += "&advance_headers=1";
+ } else if (tools.browser.is_safari || tools.browser.is_ios) {
+ // uStreamer fix for WebKit
+ tools.info("Stream: using dual_final_frames=1 to fix WebKit MJPG bugs");
+ path += "&dual_final_frames=1";
+ }
+ $("stream-image").src = path;
+ $("stream-image").className = "stream-image-active";
+ $("stream-box").classList.remove("stream-box-inactive");
+ $("stream-led").className = "led-green";
+ $("stream-led").title = "Stream is active";
+ wm.switchDisabled($("stream-screenshot-button"), false);
+ wm.switchDisabled($("stream-reset-button"), false);
+ tools.info("Stream: acquired");
+ __prev = true;
+ }
+
+ __updateStreamHeader(true);
+
+ } else {
+ __clearState();
+ }
+ };
+
+ var __clearState = function() {
+ tools.info("Stream: refreshing ...");
+
+ $("stream-image").className = "stream-image-inactive";
+ $("stream-box").classList.add("stream-box-inactive");
+ $("stream-led").className = "led-gray";
+ $("stream-led").title = "Stream inactive";
+ wm.switchDisabled($("stream-screenshot-button"), true);
+ wm.switchDisabled($("stream-reset-button"), true);
+ wm.switchDisabled($("stream-quality-slider"), true);
+ wm.switchDisabled($("stream-desired-fps-slider"), true);
+
+ __client_key = tools.makeId();
+ __client_id = "";
+ __client_fps = -1;
+ __prev = false;
+ __updateStreamHeader(false);
+ };
+
+ var __updateQualityValue = function(value) {
+ $("stream-quality-value").innerHTML = value + "%";
+ };
+
+ var __updateDesiredFpsValue = function(value) {
+ $("stream-desired-fps-value").innerHTML = (value === 0 ? "Unlimited" : value);
+ };
+
+ var __updateStreamHeader = function(online) {
+ var el_grab = document.querySelector("#stream-window-header .window-grab");
+ var el_info = $("stream-info");
+ if (online) {
+ var fps_suffix = (__client_fps >= 0 ? ` / ${__client_fps} fps` : "");
+ el_grab.innerHTML = el_info.innerHTML = `Stream &ndash; ${__resolution.width}x${__resolution.height}${fps_suffix}`;
+ } else {
+ el_grab.innerHTML = el_info.innerHTML = "Stream &ndash; offline";
+ }
+ };
+
+ var __clickScreenshotButton = function() {
+ var el_a = document.createElement("a");
+ el_a.href = "/streamer/snapshot";
+ el_a.target = "_blank";
+ document.body.appendChild(el_a);
+ el_a.click();
+ setTimeout(() => document.body.removeChild(el_a), 0);
+ };
+
+ var __clickResetButton = function() {
+ var http = tools.makeRequest("POST", "/kvmd/streamer/reset", function() {
+ if (http.readyState === 4) {
+ if (http.status !== 200) {
+ wm.error("Can't reset stream:<br>", http.responseText);
+ }
+ }
+ });
+ };
+
+ var __sendParam = function(name, value) {
+ var http = tools.makeRequest("POST", `/kvmd/streamer/set_params?${name}=${value}`, function() {
+ if (http.readyState === 4) {
+ if (http.status !== 200) {
+ wm.error("Can't configure stream:<br>", http.responseText);
+ }
+ }
+ });
+ };
+
+ var __resize = function(center=false) {
+ var size = $("stream-size-slider").value;
+ $("stream-size-value").innerHTML = size + "%";
+ __size_factor = size / 100;
+ __applySizeFactor(center);
+ };
+
+ var __adjustSizeFactor = function() {
+ var el_window = $("stream-window");
+ var el_slider = $("stream-size-slider");
+ var view = wm.getViewGeometry();
+
+ for (var size = 100; size >= el_slider.min; size -= el_slider.step) {
+ tools.info("Stream: adjusting size:", size);
+ $("stream-size-slider").value = size;
+ __resize(true);
+
+ var rect = el_window.getBoundingClientRect();
+ if (
+ rect.bottom <= view.bottom
+ && rect.top >= view.top
+ && rect.left >= view.left
+ && rect.right <= view.right
+ ) {
+ break;
+ }
+ }
+ };
+
+ var __applySizeFactor = function(center=false) {
+ var el_stream_image = $("stream-image");
+ el_stream_image.style.width = __resolution.width * __size_factor + "px";
+ el_stream_image.style.height = __resolution.height * __size_factor + "px";
+ wm.showWindow($("stream-window"), false, center);
+ };
+
+ __init__();
+}