/* * JS Linux main * * Copyright (c) 2017 Fabrice Bellard * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ "use strict"; var term, console_write1, console_resize_event; var graphic_display, display_key_event, display_mouse_event; var net_state, net_write_packet, net_set_carrier; var display_wheel_event; var fs_import_file; var Module = {}; var downloading_timer_pending = false; var downloading_timer; function on_update_file(f) { var f, reader; reader = new FileReader(); reader.onload = function (ev) { var buf, buf_addr, buf_len; buf = new Uint8Array(reader.result); buf_len = buf.length; buf_addr = _malloc(buf_len); HEAPU8.set(buf, buf_addr); /* the buffer is freed by the function */ fs_import_file(f.name, buf_addr, buf_len); }; reader.readAsArrayBuffer(f); } function on_update_files(files) { var i, n; n = files.length; for(i = 0; i < n; i++) { on_update_file(files[i]); } } function term_handler(str) { var i; for(i = 0; i < str.length; i++) { console_write1(str.charCodeAt(i)); } } function downloading_timer_cb() { var el = document.getElementById("net_progress"); el.style.visibility = "hidden"; downloading_timer_pending = false; } function update_downloading(flag) { var el; if (flag) { if (downloading_timer_pending) { clearTimeout(downloading_timer); downloading_timer_pending = false; } else { el = document.getElementById("net_progress"); el.style.visibility = "visible"; } } else { downloading_timer_pending = true; downloading_timer = setTimeout(downloading_timer_cb, 500); } } function get_params() { var url, query_str, p, tab, i, params, tab2; query_str = window.location.href; p = query_str.indexOf("?"); if (p < 0) return {}; query_str = query_str.substr(p + 1); tab = query_str.split("&"); params = {}; for(i = 0; i < tab.length; i++) { tab2 = tab[i].split("="); params[decodeURIComponent(tab2[0])] = decodeURIComponent(tab2[1]); } return params; } function get_absolute_url(fname) { var path, p; if (fname.indexOf(":") >= 0) return fname; path = window.location.pathname; p = path.lastIndexOf("/"); if (p < 0) return fname; return window.location.origin + path.slice(0, p + 1) + fname; } function GraphicDisplay(parent_el, width, height) { this.width = width; this.height = height; this.canvas_el = document.createElement("canvas"); this.canvas_el.width = width; /* logical width */ this.canvas_el.height = height; /* logical width */ /* displayed size */ this.canvas_el.style.width = width + "px"; this.canvas_el.style.height = height + "px"; this.canvas_el.style.cursor = "none"; parent_el.appendChild(this.canvas_el); this.ctx = this.canvas_el.getContext("2d"); /* clear the display */ this.ctx.fillStyle = "#000000"; this.ctx.fillRect(0, 0, width, height); this.image = this.ctx.createImageData(width, height); this.key_pressed = new Uint8Array(128); document.addEventListener("keydown", this.keyDownHandler.bind(this), false); document.addEventListener("keyup", this.keyUpHandler.bind(this), false); document.addEventListener("blur", this.blurHandler.bind(this), false); this.canvas_el.onmousedown = this.mouseMoveHandler.bind(this); this.canvas_el.onmouseup = this.mouseMoveHandler.bind(this); this.canvas_el.onmousemove = this.mouseMoveHandler.bind(this); this.canvas_el.oncontextmenu = this.onContextMenuHandler.bind(this); this.canvas_el.onwheel = this.wheelHandler.bind(this); } GraphicDisplay.code_to_input_map = { "Escape": 0x01, "Digit1": 0x02, "Digit2": 0x03, "Digit3": 0x04, "Digit4": 0x05, "Digit5": 0x06, "Digit6": 0x07, "Digit7": 0x08, "Digit8": 0x09, "Digit9": 0x0a, "Digit0": 0x0b, "Minus": 0x0c, "Equal": 0x0d, "Backspace": 0x0e, "Tab": 0x0f, "KeyQ": 0x10, "KeyW": 0x11, "KeyE": 0x12, "KeyR": 0x13, "KeyT": 0x14, "KeyY": 0x15, "KeyU": 0x16, "KeyI": 0x17, "KeyO": 0x18, "KeyP": 0x19, "BracketLeft": 0x1a, "BracketRight": 0x1b, "Enter": 0x1c, "ControlLeft": 0x1d, "KeyA": 0x1e, "KeyS": 0x1f, "KeyD": 0x20, "KeyF": 0x21, "KeyG": 0x22, "KeyH": 0x23, "KeyJ": 0x24, "KeyK": 0x25, "KeyL": 0x26, "Semicolon": 0x27, "Quote": 0x28, "Backquote": 0x29, "ShiftLeft": 0x2a, "Backslash": 0x2b, "KeyZ": 0x2c, "KeyX": 0x2d, "KeyC": 0x2e, "KeyV": 0x2f, "KeyB": 0x30, "KeyN": 0x31, "KeyM": 0x32, "Comma": 0x33, "Period": 0x34, "Slash": 0x35, "ShiftRight": 0x36, "NumpadMultiply": 0x37, "AltLeft": 0x38, "Space": 0x39, "CapsLock": 0x3a, "F1": 0x3b, "F2": 0x3c, "F3": 0x3d, "F4": 0x3e, "F5": 0x3f, "F6": 0x40, "F7": 0x41, "F8": 0x42, "F9": 0x43, "F10": 0x44, "NumLock": 0x45, "ScrollLock": 0x46, "Numpad7": 0x47, "Numpad8": 0x48, "Numpad9": 0x49, "NumpadSubtract": 0x4a, "Numpad4": 0x4b, "Numpad5": 0x4c, "Numpad6": 0x4d, "NumpadAdd": 0x4e, "Numpad1": 0x4f, "Numpad2": 0x50, "Numpad3": 0x51, "Numpad0": 0x52, "NumpadDecimal": 0x53, "IntlBackslash": 0x56, "F11": 0x57, "F12": 0x58, "NumpadEnter": 96, "ControlRight": 97, "NumpadDivide": 98, "AltRight": 100, "Home": 102, "ArrowUp": 103, "PageUp": 104, "ArrowLeft": 105, "ArrowRight": 106, "End": 107, "ArrowDown": 108, "PageDown": 109, "Insert": 110, "Delete": 111, "OSLeft": 125, "OSRight": 126, "ContextMenu": 127, }; GraphicDisplay.key_code_to_input_map = new Uint8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0x0E, 0x0F, 0, 0, 0, 0x1C, 0, 0, 0x2A, 0x1D, 0x38, 0, 0x3A, 0, 0, 0, /* 0x10 */ 0, 0, 0, 0x01, 0, 0, 0, 0, 0x39, 104, 109, 107, 102, 105, 103, 106, /* 0x20 */ 0x50, 0, 0, 0, 0, 0x52, 0x53, 0, 0x0B, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, /* 0x30 */ 0x09, 0x0A, 0, 0x27, 0, 0x0D, 0, 0, 0, 0x1E, 0x30, 0x2E, 0x20, 0x12, 0x21, 0x22, /* 0x40 */ 0x23, 0x17, 0x24, 0x25, 0x26, 0x32, 0x31, 0x18, 0x19, 0x10, 0x13, 0x1F, 0x14, 0x16, 0x2F, 0x11, /* 0x50 */ 0x2D, 0x15, 0x2C, 125, 126, 127, 0, 0, 0x52, 0x4F, 0x50, 0x51, 0x4B, 0x4C, 0x4D, 0x47, /* 0x60 */ 0x48, 0x49, 0x37, 0x4e, 0, 0x4a, 0x53, 98, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, /* 0x70 */ 0x43, 0x44, 0x57, 0x58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x80 */ 0, 0, 0, 0, 0, 0, 0, 0, 0x45, 0, 0, 0, 0, 0, 0, 0, /* 0x90 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xa0 */ 0, 0, 0, 0, 0, 0x0C, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xb0 */ 0, 0, 0x27, 0x0D, 0x33, 0x0C, 0x34, 0x35, 0x29, 0, 0, 0, 0, 0, 0, 0, /* 0xc0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0xd0 */ 0, 0, 0, 0x1A, 0x2B, 0x1B, 0x28, 0, 125, 100, 0, 0, 0, 0, 0, 0, /* 0xe0 */ 0, 0, 0, 0, 0, 0, 0, 0, ]); GraphicDisplay.prototype.keyHandler = function keyHandler(ev, isDown) { var code, input_key_code; /* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are * pressed */ if (ev.ctrlKey) { window.onbeforeunload = function() { window.onbeforeunload = null; return "CTRL-W or Ctrl-Q cannot be sent to the emulator."; }; } else { window.onbeforeunload = null; } if (typeof ev.code != "undefined") { code = ev.code; input_key_code = GraphicDisplay.code_to_input_map[code]; if (typeof input_key_code != "undefined") { // console.log("code=" + code + " isDown=" + isDown + " input_key_code=" + input_key_code); this.key_pressed[input_key_code] = isDown; display_key_event(isDown, input_key_code); if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); return false; } } else { /* fallback using keyCodes. Works only with an US keyboard */ code = ev.keyCode; if (code < 256) { input_key_code = GraphicDisplay.key_code_to_input_map[code]; // console.log("keyCode=" + code + " isDown=" + isDown + " input_key_code=" + input_key_code); if (input_key_code) { this.key_pressed[input_key_code] = isDown; display_key_event(isDown, input_key_code); if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); return false; } } } return true; } GraphicDisplay.prototype.keyDownHandler = function keyDownHandler(ev) { return this.keyHandler(ev, 1); } GraphicDisplay.prototype.keyUpHandler = function keyUpHandler(ev) { return this.keyHandler(ev, 0); } GraphicDisplay.prototype.blurHandler = function blurHandler(ev, isDown) { var i, n, key_pressed; /* allow unloading the page */ window.onbeforeunload = null; /* release all keys */ key_pressed = this.key_pressed; for(i = 0; i < key_pressed.length; i++) { if (key_pressed[i]) { display_key_event(0, i); key_pressed[i] = 0; } } } GraphicDisplay.prototype.mouseMoveHandler = function (ev) { var x, y, rect, buttons; rect = this.canvas_el.getBoundingClientRect(); x = ev.clientX - rect.left; y = ev.clientY - rect.top; buttons = ev.buttons & 7; // console.log("mouse: x=" + x + " y=" + y + " buttons=" + buttons); display_mouse_event(x, y, buttons); if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); return false; } GraphicDisplay.prototype.wheelHandler = function (ev) { if (ev.deltaY < 0) { display_wheel_event(1); } else if (ev.deltaY > 0) { display_wheel_event(-1); } if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); } /* disable contextual menu */ GraphicDisplay.prototype.onContextMenuHandler = function (ev) { if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); return false; } /* Network support */ function Ethernet(url) { try { this.socket = new WebSocket(url); } catch(err) { this.socket = null; console.log("Could not open websocket url=" + url); return; } this.socket.binaryType = 'arraybuffer'; this.socket.onmessage = this.messageHandler.bind(this); this.socket.onclose = this.closeHandler.bind(this); this.socket.onopen = this.openHandler.bind(this); this.socket.onerror = this.errorHandler.bind(this); } Ethernet.prototype.openHandler = function(e) { net_set_carrier(1); } Ethernet.prototype.closeHandler = function(e) { net_set_carrier(0); } Ethernet.prototype.errorHandler = function(e) { console.log("Websocket error=" + e); } Ethernet.prototype.messageHandler = function(e) { var str, buf_len, buf_addr, buf; if (e.data instanceof ArrayBuffer) { buf_len = e.data.byteLength; buf = new Uint8Array(e.data); buf_addr = _malloc(buf_len); HEAPU8.set(buf, buf_addr); net_write_packet(buf_addr, buf_len); _free(buf_addr); } else { str = e.data.toString(); if (str.substring(0, 5) == "ping:") { try { this.socket.send('pong:' + str.substring(5)); } catch (err) { } } } } Ethernet.prototype.recv_packet = function(buf) { if (this.socket) { try { this.socket.send(buf); } catch (err) { } } } function start_vm(user, pwd) { var url, mem_size, cpu, params, vm_url, cmdline, cols, rows, guest_url; var font_size, graphic_enable, width, height, net_url, alloc_size; var drive_url, vm_file; function loadScript(src, f) { var head = document.getElementsByTagName("head")[0]; var script = document.createElement("script"); script.src = src; var done = false; script.onload = script.onreadystatechange = function() { // attach to both events for cross browser finish detection: if ( !done && (!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) { done = true; if (f) { f(); } script.onload = script.onreadystatechange = null; head.removeChild(script); } }; head.appendChild(script); } function start() { /* C functions called from javascript */ console_write1 = Module.cwrap('console_queue_char', null, ['number']); console_resize_event = Module.cwrap('console_resize_event', null, []); fs_import_file = Module.cwrap('fs_import_file', null, ['string', 'number', 'number']); display_key_event = Module.cwrap('display_key_event', null, ['number', 'number']); display_mouse_event = Module.cwrap('display_mouse_event', null, ['number', 'number', 'number']); display_wheel_event = Module.cwrap('display_wheel_event', null, ['number']); net_write_packet = Module.cwrap('net_write_packet', null, ['number', 'number']); net_set_carrier = Module.cwrap('net_set_carrier', null, ['number']); net_state = null; if (net_url != "") { net_state = new Ethernet(net_url); } Module.ccall("vm_start", null, ["string", "number", "string", "string", "number", "number", "number", "string"], [url, mem_size, cmdline, pwd, width, height, (net_state != null) | 0, drive_url]); pwd = null; } function term_wrap_onclick_handler() { var term_wrap_el, w, h, term_bar_el, bar_h; term_wrap_el = document.getElementById("term_wrap"); term_bar_el = document.getElementById("term_bar"); w = term_wrap_el.clientWidth; h = term_wrap_el.clientHeight; bar_h = term_bar_el.clientHeight; if (term.resizePixel(w, h - bar_h)) { console_resize_event(); } } /* read the parameters */ params = get_params(); cpu = params["cpu"] || "x86"; url = params["url"]; url = get_absolute_url(url); mem_size = (params["mem"] | 0) || 128; /* in mb */ cmdline = params["cmdline"] || ""; cols = (params["cols"] | 0) || 80; rows = (params["rows"] | 0) || 30; font_size = (params["font_size"] | 0) || 15; guest_url = params["guest_url"] || ""; width = (params["w"] | 0) || 1024; height = (params["h"] | 0) || 640; graphic_enable = params["graphic"] | 0; net_url = params["net_url"]; /* empty string means no network */ if (typeof net_url == "undefined") net_url = "wss://relay.widgetry.org/"; drive_url = params["drive_url"] || ""; if (user) { cmdline += " LOGIN_USER=" + user; } else if (guest_url) { cmdline += " GUEST_URL=" + guest_url; } if (graphic_enable) { graphic_display = new GraphicDisplay(document.getElementById("term_container"), width, height); } else { var term_wrap_el; width = 0; height = 0; /* start the terminal */ term = new Term({ cols: cols, rows: rows, scrollback: 10000, fontSize: font_size }); term.setKeyHandler(term_handler); term.open(document.getElementById("term_container"), document.getElementById("term_paste")); term_wrap_el = document.getElementById("term_wrap") term_wrap_el.style.width = term.term_el.style.width; term_wrap_el.onclick = term_wrap_onclick_handler; term.write("Loading...\r\n"); } // console.log("cpu=" + cpu + " url=" + url + " mem=" + mem_size); switch(cpu) { case "x86": vm_file = "x86emu"; break; case "riscv64": case "riscv": vm_file = "riscvemu64"; break; case "riscv32": vm_file = "riscvemu32"; break; default: term.writeln("Unknown cpu=" + cpu); return; } if (typeof WebAssembly === "object") { /* wasm support : the memory grows automatically */ vm_url = vm_file + "-wasm.js"; } else { /* set the total memory */ alloc_size = mem_size; if (cpu == "x86") alloc_size += 16; if (graphic_enable) { /* frame buffer memory */ alloc_size += (width * height * 4 + 1048576 - 1) >> 20; } alloc_size += 32; /* extra space (XXX: reduce it ?) */ alloc_size = (alloc_size + 15) & -16; /* align to 16 MB */ Module.TOTAL_MEMORY = alloc_size << 20; vm_url = vm_file + ".js"; } Module.preRun = start; loadScript(vm_url, null); } function on_login() { var login_wrap_el = document.getElementById("wrap"); var term_wrap_el = document.getElementById("term_wrap"); var form = document.getElementById("form"); var status = document.getElementById("status"); var user = form.user.value; var pwd = form.password.value; if (user.length <= 1) { status.innerHTML = "User name must be provided"; return false; } login_wrap_el.style.display = "none"; term_wrap_el.style.display = "block"; form.password.value = ""; form.user.value = ""; start_vm(user, pwd); return false; } (function() { var login, params; params = get_params(); login = params["login"] || 0; if (login) { var login_wrap_el = document.getElementById("wrap"); login_wrap_el.style.display = "block"; } else { var term_wrap_el = document.getElementById("term_wrap"); term_wrap_el.style.display = "block"; start_vm(null, null); } })();