]>
Commit | Line | Data |
---|---|---|
dcf3d43b DC |
1 | console.log('xtermjs: starting'); |
2 | ||
3 | var states = { | |
4 | start: 1, | |
5 | connecting: 2, | |
6 | connected: 3, | |
7 | disconnecting: 4, | |
8 | disconnected: 5, | |
291e1a24 | 9 | reconnecting: 6, |
dcf3d43b DC |
10 | }; |
11 | ||
12 | var term, | |
13 | protocol, | |
14 | socketURL, | |
15 | socket, | |
16 | ticket, | |
dcf3d43b DC |
17 | resize, |
18 | ping, | |
291e1a24 DC |
19 | state = states.start, |
20 | starttime = new Date(); | |
21 | ||
22 | var type = getQueryParameter('console'); | |
23 | var vmid = getQueryParameter('vmid'); | |
24 | var vmname = getQueryParameter('vmname'); | |
25 | var nodename = getQueryParameter('node'); | |
6c7c792d | 26 | var cmd = getQueryParameter('cmd'); |
1d3d2919 | 27 | var cmdOpts = getQueryParameter('cmd-opts'); |
dcf3d43b | 28 | |
93c96ac5 | 29 | function updateState(newState, msg, code) { |
dcf3d43b DC |
30 | var timeout, severity, message; |
31 | switch (newState) { | |
32 | case states.connecting: | |
33 | message = "Connecting..."; | |
34 | timeout = 0; | |
35 | severity = severities.warning; | |
36 | break; | |
37 | case states.connected: | |
e435736a | 38 | window.onbeforeunload = windowUnload; |
dcf3d43b DC |
39 | message = "Connected"; |
40 | break; | |
41 | case states.disconnecting: | |
f7fd260c | 42 | window.onbeforeunload = undefined; |
dcf3d43b DC |
43 | message = "Disconnecting..."; |
44 | timeout = 0; | |
45 | severity = severities.warning; | |
46 | break; | |
291e1a24 | 47 | case states.reconnecting: |
f7fd260c | 48 | window.onbeforeunload = undefined; |
291e1a24 DC |
49 | message = "Reconnecting..."; |
50 | timeout = 0; | |
51 | severity = severities.warning; | |
52 | break; | |
dcf3d43b | 53 | case states.disconnected: |
e435736a | 54 | window.onbeforeunload = undefined; |
dcf3d43b DC |
55 | switch (state) { |
56 | case states.start: | |
57 | case states.connecting: | |
291e1a24 | 58 | case states.reconnecting: |
dcf3d43b DC |
59 | message = "Connection failed"; |
60 | timeout = 0; | |
61 | severity = severities.error; | |
62 | break; | |
63 | case states.connected: | |
64 | case states.disconnecting: | |
291e1a24 DC |
65 | var time_since_started = new Date() - starttime; |
66 | timeout = 5000; | |
3645b3f4 | 67 | if (time_since_started > 5*1000 || type === 'shell') { |
291e1a24 DC |
68 | message = "Connection closed"; |
69 | } else { | |
70 | message = "Connection failed"; | |
71 | severity = severities.error; | |
72 | } | |
dcf3d43b DC |
73 | break; |
74 | case states.disconnected: | |
75 | // no state change | |
76 | break; | |
77 | default: | |
78 | throw "unknown state"; | |
79 | } | |
80 | break; | |
81 | default: | |
82 | throw "unknown state"; | |
83 | } | |
93c96ac5 | 84 | let msgArr = []; |
dcf3d43b | 85 | if (msg) { |
93c96ac5 DC |
86 | msgArr.push(msg); |
87 | } | |
88 | if (code !== undefined) { | |
89 | msgArr.push(`Code: ${code}`); | |
90 | } | |
91 | if (msgArr.length > 0) { | |
92 | message += ` (${msgArr.join(', ')})`; | |
dcf3d43b DC |
93 | } |
94 | state = newState; | |
95 | showMsg(message, timeout, severity); | |
96 | } | |
97 | ||
98 | var terminalContainer = document.getElementById('terminal-container'); | |
99 | document.getElementById('status_bar').addEventListener('click', hideMsg); | |
9871a3b8 | 100 | document.getElementById('connect_btn').addEventListener('click', startGuest); |
b9bbd688 | 101 | const fitAddon = new FitAddon.FitAddon(); |
154b65d5 | 102 | const webglAddon = new WebglAddon.WebglAddon(); |
dcf3d43b DC |
103 | |
104 | createTerminal(); | |
105 | ||
9871a3b8 DC |
106 | function startConnection(url, params, term) { |
107 | API2Request({ | |
108 | method: 'POST', | |
109 | params: params, | |
110 | url: url + '/termproxy', | |
111 | success: function(result) { | |
112 | var port = encodeURIComponent(result.data.port); | |
113 | ticket = result.data.ticket; | |
114 | socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/api2/json' + url + '/vncwebsocket?port=' + port + '&vncticket=' + encodeURIComponent(ticket); | |
115 | ||
9871a3b8 DC |
116 | socket = new WebSocket(socketURL, 'binary'); |
117 | socket.binaryType = 'arraybuffer'; | |
118 | socket.onopen = runTerminal; | |
119 | socket.onclose = tryReconnect; | |
120 | socket.onerror = tryReconnect; | |
121 | updateState(states.connecting); | |
122 | }, | |
123 | failure: function(msg) { | |
124 | updateState(states.disconnected,msg); | |
125 | } | |
126 | }); | |
127 | } | |
128 | ||
129 | function startGuest() { | |
130 | let api_type = type === 'kvm' ? 'qemu' : 'lxc'; | |
131 | API2Request({ | |
132 | method: 'POST', | |
133 | url: `/nodes/${nodename}/${api_type}/${vmid}/status/start`, | |
134 | success: function(result) { | |
135 | showMsg('Guest started successfully', 0); | |
136 | setTimeout(function() { | |
137 | location.reload(); | |
138 | }, 1000); | |
139 | }, | |
140 | failure: function(msg) { | |
141 | if (msg.match(/already running/)) { | |
142 | showMsg('Guest started successfully', 0); | |
143 | setTimeout(function() { | |
144 | location.reload(); | |
145 | }, 1000); | |
146 | } else { | |
147 | updateState(states.disconnected,msg); | |
148 | } | |
149 | } | |
150 | }); | |
151 | } | |
152 | ||
dcf3d43b | 153 | function createTerminal() { |
37445374 | 154 | term = new Terminal(getTerminalSettings()); |
154b65d5 | 155 | term.open(terminalContainer); |
b9bbd688 | 156 | term.loadAddon(fitAddon); |
aa9a479c | 157 | let loadedWebgl = false; |
154b65d5 | 158 | try { |
aa9a479c DC |
159 | if (detectWebgl()) { |
160 | term.loadAddon(webglAddon); | |
161 | loadedWebgl = true; | |
162 | } | |
163 | } catch (_e) { } | |
164 | ||
165 | if (!loadedWebgl) { | |
154b65d5 DC |
166 | console.warn("webgl-addon loading failed, falling back to regular dom renderer"); |
167 | } | |
dcf3d43b | 168 | |
b9bbd688 | 169 | term.onResize(function (size) { |
dcf3d43b DC |
170 | if (state === states.connected) { |
171 | socket.send("1:" + size.cols + ":" + size.rows + ":"); | |
172 | } | |
173 | }); | |
174 | ||
175 | protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; | |
176 | ||
177 | var params = {}; | |
dcf3d43b DC |
178 | var url = '/nodes/' + nodename; |
179 | switch (type) { | |
180 | case 'kvm': | |
181 | url += '/qemu/' + vmid; | |
dcf3d43b DC |
182 | break; |
183 | case 'lxc': | |
184 | url += '/lxc/' + vmid; | |
dcf3d43b DC |
185 | break; |
186 | case 'upgrade': | |
af39a6f0 | 187 | params.cmd = 'upgrade'; |
dcf3d43b | 188 | break; |
6c7c792d TM |
189 | case 'cmd': |
190 | params.cmd = decodeURI(cmd); | |
bc2ce72a | 191 | if (cmdOpts !== undefined && cmdOpts !== null && cmdOpts !== "") { |
1d3d2919 TL |
192 | params['cmd-opts'] = decodeURI(cmdOpts); |
193 | } | |
6c7c792d | 194 | break; |
dcf3d43b | 195 | } |
9871a3b8 DC |
196 | if (type === 'kvm' || type === 'lxc') { |
197 | API2Request({ | |
198 | method: 'GET', | |
199 | url: `${url}/status/current`, | |
200 | success: function(result) { | |
201 | if (result.data.status === 'running') { | |
202 | startConnection(url, params, term); | |
203 | } else { | |
204 | document.getElementById('connect_dlg').classList.add('pve_open'); | |
205 | } | |
206 | }, | |
207 | failure: function(msg) { | |
208 | updateState(states.disconnected, msg); | |
209 | }, | |
210 | }); | |
211 | } else { | |
212 | startConnection(url, params, term); | |
213 | } | |
dcf3d43b DC |
214 | } |
215 | ||
216 | function runTerminal() { | |
217 | socket.onmessage = function(event) { | |
920ae86d | 218 | var answer = new Uint8Array(event.data); |
dcf3d43b DC |
219 | if (state === states.connected) { |
220 | term.write(answer); | |
221 | } else if(state === states.connecting) { | |
920ae86d | 222 | if (answer[0] === 79 && answer[1] === 75) { // "OK" |
dcf3d43b DC |
223 | updateState(states.connected); |
224 | term.write(answer.slice(2)); | |
225 | } else { | |
226 | socket.close(); | |
227 | } | |
228 | } | |
229 | }; | |
230 | ||
b9bbd688 | 231 | term.onData(function(data) { |
dcf3d43b DC |
232 | if (state === states.connected) { |
233 | socket.send("0:" + unescape(encodeURIComponent(data)).length.toString() + ":" + data); | |
234 | } | |
235 | }); | |
236 | ||
237 | ping = setInterval(function() { | |
238 | socket.send("2"); | |
239 | }, 30*1000); | |
240 | ||
241 | window.addEventListener('resize', function() { | |
242 | clearTimeout(resize); | |
243 | resize = setTimeout(function() { | |
244 | // done resizing | |
b9bbd688 | 245 | fitAddon.fit(); |
dcf3d43b DC |
246 | }, 250); |
247 | }); | |
248 | ||
5e91985c | 249 | socket.send(PVE.UserName + ':' + ticket + "\n"); |
dcf3d43b | 250 | |
1553e6ef DC |
251 | // initial focus and resize |
252 | setTimeout(function() { | |
253 | term.focus(); | |
b9bbd688 | 254 | fitAddon.fit(); |
1553e6ef | 255 | }, 250); |
dcf3d43b DC |
256 | } |
257 | ||
291e1a24 DC |
258 | function getLxcStatus(callback) { |
259 | API2Request({ | |
260 | method: 'GET', | |
261 | url: '/nodes/' + nodename + '/lxc/' + vmid + '/status/current', | |
262 | success: function(result) { | |
263 | if (typeof callback === 'function') { | |
264 | callback(true, result); | |
265 | } | |
266 | }, | |
267 | failure: function(msg) { | |
268 | if (typeof callback === 'function') { | |
269 | callback(false, msg); | |
270 | } | |
271 | } | |
272 | }); | |
273 | } | |
274 | ||
275 | function checkMigration() { | |
276 | var apitype = type; | |
277 | if (apitype === 'kvm') { | |
278 | apitype = 'qemu'; | |
279 | } | |
280 | API2Request({ | |
281 | method: 'GET', | |
282 | params: { | |
283 | type: 'vm' | |
284 | }, | |
285 | url: '/cluster/resources', | |
286 | success: function(result) { | |
287 | // if not yet migrated , wait and try again | |
288 | // if not migrating and stopped, cancel | |
289 | // if started, connect | |
290 | result.data.forEach(function(entity) { | |
291 | if (entity.id === (apitype + '/' + vmid)) { | |
292 | var started = entity.status === 'running'; | |
293 | var migrated = entity.node !== nodename; | |
294 | if (migrated) { | |
295 | if (started) { | |
296 | // goto different node | |
297 | location.href = '?console=' + type + | |
298 | '&xtermjs=1&vmid=' + vmid + '&vmname=' + | |
299 | vmname + '&node=' + entity.node; | |
300 | } else { | |
301 | // wait again | |
302 | updateState(states.reconnecting, 'waiting for migration to finish...'); | |
303 | setTimeout(checkMigration, 5000); | |
304 | } | |
305 | } else { | |
306 | if (type === 'lxc') { | |
307 | // we have to check the status of the | |
308 | // container to know if it has the | |
309 | // migration lock | |
310 | getLxcStatus(function(success, result) { | |
311 | if (success) { | |
312 | if (result.data.lock === 'migrate') { | |
313 | // still waiting | |
314 | updateState(states.reconnecting, 'waiting for migration to finish...'); | |
315 | setTimeout(checkMigration, 5000); | |
8f2ac472 DC |
316 | } else if (started) { |
317 | // container was rebooted | |
318 | location.reload(); | |
291e1a24 DC |
319 | } else { |
320 | stopTerminal(); | |
321 | } | |
322 | } else { | |
323 | // probably the status call failed because | |
324 | // the ct is already somewhere else, so retry | |
325 | setTimeout(checkMigration, 1000); | |
326 | } | |
327 | }); | |
328 | } else if (started) { | |
329 | // this happens if we have old data in | |
330 | // /cluster/resources, or the connection | |
331 | // disconnected, so simply try to reload here | |
332 | location.reload(); | |
333 | } else if (type === 'kvm') { | |
334 | // it seems the guest simply stopped | |
335 | stopTerminal(); | |
336 | } | |
337 | } | |
338 | ||
339 | return; | |
340 | } | |
341 | }); | |
342 | }, | |
343 | failure: function(msg) { | |
344 | errorTerminal({msg: msg}); | |
345 | } | |
346 | }); | |
347 | } | |
348 | ||
93c96ac5 | 349 | function tryReconnect(event) { |
291e1a24 DC |
350 | var time_since_started = new Date() - starttime; |
351 | var type = getQueryParameter('console'); | |
6c7c792d | 352 | if (time_since_started < 5*1000 || type === 'shell' || type === 'cmd') { // 5 seconds |
93c96ac5 | 353 | stopTerminal(event); |
291e1a24 | 354 | return; |
291e1a24 DC |
355 | } |
356 | ||
357 | updateState(states.disconnecting, 'Detecting migration...'); | |
358 | setTimeout(checkMigration, 5000); | |
359 | } | |
360 | ||
b9bbd688 DC |
361 | function clearEvents() { |
362 | term.onResize(() => {}); | |
363 | term.onData(() => {}); | |
364 | } | |
365 | ||
e435736a DC |
366 | function windowUnload(e) { |
367 | let message = "Are you sure you want to leave this page?"; | |
368 | ||
369 | e = e || window.event; | |
370 | if (e) { | |
371 | e.returnValue = message; | |
372 | } | |
373 | ||
374 | return message; | |
375 | } | |
376 | ||
dcf3d43b | 377 | function stopTerminal(event) { |
8c502b81 | 378 | event = event || {}; |
b9bbd688 | 379 | clearEvents(); |
dcf3d43b DC |
380 | clearInterval(ping); |
381 | socket.close(); | |
93c96ac5 | 382 | updateState(states.disconnected, event.reason, event.code); |
dcf3d43b DC |
383 | } |
384 | ||
385 | function errorTerminal(event) { | |
34d796a1 | 386 | event = event || {}; |
b9bbd688 | 387 | clearEvents(); |
dcf3d43b DC |
388 | clearInterval(ping); |
389 | socket.close(); | |
f21a83b7 | 390 | term.dispose(); |
93c96ac5 | 391 | updateState(states.disconnected, event.msg, event.code); |
dcf3d43b | 392 | } |
aa9a479c DC |
393 | |
394 | // try to detect hardware acceleration, can throw an exception if there is no webgl support | |
395 | // | |
396 | // for chrome we have to give the parameter, otherwise it'll use its software renderer | |
397 | // which is buggy: https://github.com/xtermjs/xterm.js/issues/4574 | |
398 | // | |
399 | // firefox will fail on the getContext anyway if there is not hardware support | |
400 | function detectWebgl() { | |
401 | const canvas = document.createElement("canvas"); | |
402 | const gl = canvas.getContext("webgl2", { failIfMajorPerformanceCaveat: true }); | |
403 | return !!gl; | |
404 | } |