import { get, set, observer, computed, setProperties } from '@ember/object'; import { inject as service } from '@ember/service'; import { alternateLabel } from 'ui/utils/platform'; import ThrottledResize from 'shared/mixins/throttled-resize'; import { next } from '@ember/runloop'; import layout from './template'; import Component from '@ember/component'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import $ from 'jquery'; const DEFAULT_COMMAND = ['/bin/sh', '-c', 'TERM=xterm-256color; export TERM; [ -x /bin/bash ] && ([ -x /usr/bin/script ] && /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) || exec /bin/sh']; const statusMap = { closed: 'closed', connected: 'connected', connecting: 'connecting', init: 'initializing' }; export default Component.extend(ThrottledResize, { scope: service(), growl: service(), layout, // URL or instance+cmd url: null, instance: null, command: null, alternateLabel, showProtip: true, contenteditable: false, error: null, socket: null, term: null, containerName: null, init() { this._super(...arguments); this._bootstrap(); }, didInsertElement() { this._super(...arguments); next(this, 'exec'); }, willDestroyElement() { this.disconnect(true); this._super(...arguments); }, actions: { contextMenuHandler() { // fix for no paste button in firefox context menu on Windows set(this, 'contenteditable', true); setTimeout(() => { set(this, 'contenteditable', false); }, 20); } }, containerDidChange: observer('containerName', function() { this.disconnect(); this.exec(); }), runningContainers: computed('instance.containers', function() { return (get(this, 'instance.containers') || []).filterBy('canShell', true); }), status: statusMap.connecting, _bootstrap() { set(this, 'containerName', get(this, 'containerName') || get(this, 'instance.containers.firstObject.name')); }, fit() { const term = get(this, 'term'); const socket = get(this, 'socket'); if (term && socket) { try { this.fitAddon.fit(); } catch (error) { } socket.send(`4${ AWS.util.base64.encode(JSON.stringify({ Width: term.cols, Height: term.rows })) }`); } }, onResize() { next(() => { this.fit(); }); }, exec() { let url = get(this, 'url'); if ( !url ) { const instance = get(this, 'instance'); const clusterId = get(this, 'scope.currentCluster.id'); const namespaceId = get(instance, 'namespaceId'); const podName = get(instance, 'name'); const containerName = get(this, 'containerName'); const scheme = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; let command = get(instance, 'command') || DEFAULT_COMMAND; url = `${ scheme }${ window.location.host }/k8s/clusters/${ clusterId }/api/v1/namespaces/${ namespaceId }/pods/${ podName }/exec`; url += `?container=${ encodeURIComponent(containerName) }&stdout=1&stdin=1&stderr=1&tty=1`; if ( this.isWindows() ) { command = ['cmd'] } command.forEach((c) => { url += `&command=${ encodeURIComponent(c) }`; }); } this.connect(url); }, isWindows() { if ( get(this, 'windows') === undefined ) { const system = get(this, 'instance.node.info.os.operatingSystem') || ''; return system.startsWith('Windows'); } else { return get(this, 'windows'); } }, connect(url) { const socket = new WebSocket(url, 'base64.channel.k8s.io'); set(this, 'socket', socket); socket.onclose = (err = {}) => { const term = get(this, 'term') if (!get(this, 'userClosed')) { set(this, 'statusCode', err.code) } try { if ( !get(this, 'userClosed') && get(this, 'instance.containers.length') === 1 ) { term.dispose(); if (this.dismiss) { this.dismiss(); } } set(this, 'status', statusMap.closed); } catch (e) { } }; socket.onerror = () => { const term = get(this, 'term') set(this, 'status', statusMap.closed); try { if ( !get(this, 'userClosed') && get(this, 'instance.containers.length') === 1 ) { term.dispose(); if (this.dismiss) { this.dismiss(); } } } catch (e) { } } socket.onopen = () => { set(this, 'status', statusMap.init); const fitAddon = new FitAddon(); var term = new Terminal({ cursorBlink: true, useStyle: true, fontSize: 12, cols: 180, }); term.loadAddon(fitAddon) set(this, 'term', term); set(this, 'fitAddon', fitAddon); term.onData((data) => { socket.send(`0${ AWS.util.base64.encode(data) }`); }); term.open($('.shell-body')[0]); next(() => { term.focus(); this.fit(); }); socket.onmessage = (message) => { set(this, 'status', statusMap.connected); if (this.connected) { this.connected(); } const data = message.data.slice(1); switch (message.data[0]) { case '1': case '2': case '3': term.write(AWS.util.base64.decode(data).toString()); break; } }; }; }, disconnect(destroying = false) { setProperties(this, { status: statusMap.closed, userClosed: true, }) const term = get(this, 'term'); if (term) { term.dispose(); if (!destroying) { set(this, 'term', null); } } const socket = get(this, 'socket'); if (socket) { socket.send(`0${ AWS.util.base64.encode('exit\r\n') }`); socket.close(); if (!destroying) { set(this, 'socket', null); } } if (this.disconnected) { this.disconnected(); } }, });