X-Git-Url: https://git.proxmox.com/?p=pve-xtermjs.git;a=blobdiff_plain;f=xterm.js%2Fsrc%2Fmain.js;fp=xterm.js%2Fsrc%2Fmain.js;h=85cc39953c928bc2003323777069a49fca5078ab;hp=0000000000000000000000000000000000000000;hb=145da0bf450e9f6447c9dad2d6d967ea7f0ca011;hpb=13e2e50222d588fc9105276193cdbdc596b9feb9 diff --git a/xterm.js/src/main.js b/xterm.js/src/main.js new file mode 100644 index 0000000..85cc399 --- /dev/null +++ b/xterm.js/src/main.js @@ -0,0 +1,386 @@ +console.log('xtermjs: starting'); + +var states = { + start: 1, + connecting: 2, + connected: 3, + disconnecting: 4, + disconnected: 5, + reconnecting: 6, +}; + +var term, + protocol, + socketURL, + socket, + ticket, + resize, + ping, + state = states.start, + starttime = new Date(); + +var type = getQueryParameter('console'); +var vmid = getQueryParameter('vmid'); +var vmname = getQueryParameter('vmname'); +var nodename = getQueryParameter('node'); +var cmd = getQueryParameter('cmd'); +var cmdOpts = getQueryParameter('cmd-opts'); + +function updateState(newState, msg, code) { + var timeout, severity, message; + switch (newState) { + case states.connecting: + message = "Connecting..."; + timeout = 0; + severity = severities.warning; + break; + case states.connected: + window.onbeforeunload = windowUnload; + message = "Connected"; + break; + case states.disconnecting: + window.onbeforeunload = undefined; + message = "Disconnecting..."; + timeout = 0; + severity = severities.warning; + break; + case states.reconnecting: + window.onbeforeunload = undefined; + message = "Reconnecting..."; + timeout = 0; + severity = severities.warning; + break; + case states.disconnected: + window.onbeforeunload = undefined; + switch (state) { + case states.start: + case states.connecting: + case states.reconnecting: + message = "Connection failed"; + timeout = 0; + severity = severities.error; + break; + case states.connected: + case states.disconnecting: + var time_since_started = new Date() - starttime; + timeout = 5000; + if (time_since_started > 5*1000 || type === 'shell') { + message = "Connection closed"; + } else { + message = "Connection failed"; + severity = severities.error; + } + break; + case states.disconnected: + // no state change + break; + default: + throw "unknown state"; + } + break; + default: + throw "unknown state"; + } + let msgArr = []; + if (msg) { + msgArr.push(msg); + } + if (code !== undefined) { + msgArr.push(`Code: ${code}`); + } + if (msgArr.length > 0) { + message += ` (${msgArr.join(', ')})`; + } + state = newState; + showMsg(message, timeout, severity); +} + +var terminalContainer = document.getElementById('terminal-container'); +document.getElementById('status_bar').addEventListener('click', hideMsg); +document.getElementById('connect_btn').addEventListener('click', startGuest); +const fitAddon = new FitAddon.FitAddon(); +const webglAddon = new WebglAddon.WebglAddon(); + +createTerminal(); + +function startConnection(url, params, term) { + API2Request({ + method: 'POST', + params: params, + url: url + '/termproxy', + success: function(result) { + var port = encodeURIComponent(result.data.port); + ticket = result.data.ticket; + socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/api2/json' + url + '/vncwebsocket?port=' + port + '&vncticket=' + encodeURIComponent(ticket); + + socket = new WebSocket(socketURL, 'binary'); + socket.binaryType = 'arraybuffer'; + socket.onopen = runTerminal; + socket.onclose = tryReconnect; + socket.onerror = tryReconnect; + updateState(states.connecting); + }, + failure: function(msg) { + updateState(states.disconnected,msg); + } + }); +} + +function startGuest() { + let api_type = type === 'kvm' ? 'qemu' : 'lxc'; + API2Request({ + method: 'POST', + url: `/nodes/${nodename}/${api_type}/${vmid}/status/start`, + success: function(result) { + showMsg('Guest started successfully', 0); + setTimeout(function() { + location.reload(); + }, 1000); + }, + failure: function(msg) { + if (msg.match(/already running/)) { + showMsg('Guest started successfully', 0); + setTimeout(function() { + location.reload(); + }, 1000); + } else { + updateState(states.disconnected,msg); + } + } + }); +} + +function createTerminal() { + term = new Terminal(getTerminalSettings()); + term.open(terminalContainer); + term.loadAddon(fitAddon); + try { + term.loadAddon(webglAddon); + } catch (_e) { + console.warn("webgl-addon loading failed, falling back to regular dom renderer"); + } + + term.onResize(function (size) { + if (state === states.connected) { + socket.send("1:" + size.cols + ":" + size.rows + ":"); + } + }); + + protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; + + var params = {}; + var url = '/nodes/' + nodename; + switch (type) { + case 'kvm': + url += '/qemu/' + vmid; + break; + case 'lxc': + url += '/lxc/' + vmid; + break; + case 'upgrade': + params.cmd = 'upgrade'; + break; + case 'cmd': + params.cmd = decodeURI(cmd); + if (cmdOpts !== undefined && cmdOpts !== null && cmdOpts !== "") { + params['cmd-opts'] = decodeURI(cmdOpts); + } + break; + } + if (type === 'kvm' || type === 'lxc') { + API2Request({ + method: 'GET', + url: `${url}/status/current`, + success: function(result) { + if (result.data.status === 'running') { + startConnection(url, params, term); + } else { + document.getElementById('connect_dlg').classList.add('pve_open'); + } + }, + failure: function(msg) { + updateState(states.disconnected, msg); + }, + }); + } else { + startConnection(url, params, term); + } +} + +function runTerminal() { + socket.onmessage = function(event) { + var answer = new Uint8Array(event.data); + if (state === states.connected) { + term.write(answer); + } else if(state === states.connecting) { + if (answer[0] === 79 && answer[1] === 75) { // "OK" + updateState(states.connected); + term.write(answer.slice(2)); + } else { + socket.close(); + } + } + }; + + term.onData(function(data) { + if (state === states.connected) { + socket.send("0:" + unescape(encodeURIComponent(data)).length.toString() + ":" + data); + } + }); + + ping = setInterval(function() { + socket.send("2"); + }, 30*1000); + + window.addEventListener('resize', function() { + clearTimeout(resize); + resize = setTimeout(function() { + // done resizing + fitAddon.fit(); + }, 250); + }); + + socket.send(PVE.UserName + ':' + ticket + "\n"); + + // initial focus and resize + setTimeout(function() { + term.focus(); + fitAddon.fit(); + }, 250); +} + +function getLxcStatus(callback) { + API2Request({ + method: 'GET', + url: '/nodes/' + nodename + '/lxc/' + vmid + '/status/current', + success: function(result) { + if (typeof callback === 'function') { + callback(true, result); + } + }, + failure: function(msg) { + if (typeof callback === 'function') { + callback(false, msg); + } + } + }); +} + +function checkMigration() { + var apitype = type; + if (apitype === 'kvm') { + apitype = 'qemu'; + } + API2Request({ + method: 'GET', + params: { + type: 'vm' + }, + url: '/cluster/resources', + success: function(result) { + // if not yet migrated , wait and try again + // if not migrating and stopped, cancel + // if started, connect + result.data.forEach(function(entity) { + if (entity.id === (apitype + '/' + vmid)) { + var started = entity.status === 'running'; + var migrated = entity.node !== nodename; + if (migrated) { + if (started) { + // goto different node + location.href = '?console=' + type + + '&xtermjs=1&vmid=' + vmid + '&vmname=' + + vmname + '&node=' + entity.node; + } else { + // wait again + updateState(states.reconnecting, 'waiting for migration to finish...'); + setTimeout(checkMigration, 5000); + } + } else { + if (type === 'lxc') { + // we have to check the status of the + // container to know if it has the + // migration lock + getLxcStatus(function(success, result) { + if (success) { + if (result.data.lock === 'migrate') { + // still waiting + updateState(states.reconnecting, 'waiting for migration to finish...'); + setTimeout(checkMigration, 5000); + } else if (started) { + // container was rebooted + location.reload(); + } else { + stopTerminal(); + } + } else { + // probably the status call failed because + // the ct is already somewhere else, so retry + setTimeout(checkMigration, 1000); + } + }); + } else if (started) { + // this happens if we have old data in + // /cluster/resources, or the connection + // disconnected, so simply try to reload here + location.reload(); + } else if (type === 'kvm') { + // it seems the guest simply stopped + stopTerminal(); + } + } + + return; + } + }); + }, + failure: function(msg) { + errorTerminal({msg: msg}); + } + }); +} + +function tryReconnect(event) { + var time_since_started = new Date() - starttime; + var type = getQueryParameter('console'); + if (time_since_started < 5*1000 || type === 'shell' || type === 'cmd') { // 5 seconds + stopTerminal(event); + return; + } + + updateState(states.disconnecting, 'Detecting migration...'); + setTimeout(checkMigration, 5000); +} + +function clearEvents() { + term.onResize(() => {}); + term.onData(() => {}); +} + +function windowUnload(e) { + let message = "Are you sure you want to leave this page?"; + + e = e || window.event; + if (e) { + e.returnValue = message; + } + + return message; +} + +function stopTerminal(event) { + event = event || {}; + clearEvents(); + clearInterval(ping); + socket.close(); + updateState(states.disconnected, event.reason, event.code); +} + +function errorTerminal(event) { + event = event || {}; + clearEvents(); + clearInterval(ping); + socket.close(); + term.dispose(); + updateState(states.disconnected, event.msg, event.code); +}