409 lines
9.9 KiB
JavaScript
409 lines
9.9 KiB
JavaScript
|
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();
|
||
|
|
||
|
let uridata = getURIData();
|
||
|
|
||
|
var type = uridata.type;
|
||
|
var vmid = uridata.vmid;
|
||
|
var vmname = uridata.name;
|
||
|
var nodename = uridata.node;
|
||
|
|
||
|
if (typeof(PVE) === 'undefined') PVE = {};
|
||
|
PVE.UserName = uridata.user;
|
||
|
PVE.CSRFPreventionToken = getCookie("CSRFPreventionToken");
|
||
|
PVE.url = uridata.url;
|
||
|
|
||
|
document.title = document.title.replace("{{ VMName }}", vmid).replace("{{ NodeName }}", nodename);
|
||
|
|
||
|
function getCookie(cname) {
|
||
|
let name = cname + "=";
|
||
|
let decodedCookie = decodeURIComponent(document.cookie);
|
||
|
let ca = decodedCookie.split(";");
|
||
|
for(let i = 0; i < ca.length; i++) {
|
||
|
let c = ca[i];
|
||
|
while (c.charAt(0) === " ") {
|
||
|
c = c.substring(1);
|
||
|
}
|
||
|
if (c.indexOf(name) === 0) {
|
||
|
return c.substring(name.length, c.length);
|
||
|
}
|
||
|
}
|
||
|
return "";
|
||
|
}
|
||
|
|
||
|
function getURIData () {
|
||
|
let url = new URL(window.location.href);
|
||
|
return Object.fromEntries(url.searchParams);
|
||
|
}
|
||
|
|
||
|
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();
|
||
|
|
||
|
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 + "pve.tronnet.net" + ((location.port) ? (':' + location.port) : '') + '/api2/json' + url + '/vncwebsocket?port=' + port + '&vncticket=' + encodeURIComponent(ticket);
|
||
|
|
||
|
term.open(terminalContainer, true);
|
||
|
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.loadAddon(fitAddon);
|
||
|
|
||
|
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) {
|
||
|
even = event || {};
|
||
|
clearEvents();
|
||
|
clearInterval(ping);
|
||
|
socket.close();
|
||
|
term.dispose();
|
||
|
updateState(states.disconnected, event.msg, event.code);
|
||
|
}
|