import Ember from 'ember'; import { get, set, observer } from '@ember/object'; import { inject as service } from '@ember/service'; import { alternateLabel } from 'ui/utils/platform'; import ThrottledResize from 'shared/mixins/throttled-resize'; import Terminal from 'npm:xterm'; import { proposeGeometry } from 'ui/utils/xterm-fit-addon'; import { next } from '@ember/runloop'; import layout from './template'; 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']; export default Ember.Component.extend(ThrottledResize, { scope: service(), layout, // URL or instance+cmd url: null, instance: null, command: null, alternateLabel: alternateLabel, showProtip: true, contenteditable: false, status: 'connecting', error: null, socket: null, term: null, containerName: null, actions: { contextMenuHandler() { // fix for no paste button in firefox context menu on Windows set(this,'contenteditable', true); setTimeout(() => { set(this,'contenteditable', false); }, 20); } }, fit() { var term = get(this,'term'); var socket = get(this,'socket'); if (term && socket) { var geometry = proposeGeometry(term); socket.send("4" + AWS.util.base64.encode(`{"Width":${geometry.cols},"Height":${geometry.rows}}`)); term.resize(geometry.cols, geometry.rows); } }, onResize: function () { this.fit(); }, didInsertElement: function () { this._super(); next(this, 'exec'); }, init() { this._super(...arguments); const containerName = get(this, 'instance.containers.firstObject.name'); set(this, 'containerName', containerName); }, exec: function () { let url = get(this, 'url'); if ( !url ) { var 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://'; 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`; const command = get(instance, 'command') || DEFAULT_COMMAND; command.forEach(c => { url += "&command=" + encodeURIComponent(c); }); } this.connect(url); }, connect: function (url) { var socket = new WebSocket(url, 'base64.channel.k8s.io'); set(this,'socket', socket); socket.onopen = () => { set(this,'status', 'initializing'); var term = new Terminal({ useStyle: true, screenKeys: true, cursorBlink: false }); set(this,'term', term); term.on('data', function (data) { socket.send(`0${AWS.util.base64.encode(window.unescape(encodeURIComponent(data)))}`); }); term.open(this.$('.shell-body')[0], true); this.fit(); socket.onmessage = (message) => { set(this,'status', 'connected'); this.sendAction('connected'); const data = message.data.slice(1); switch (message.data[0]) { case '1': case '2': case '3': term.write(decodeURIComponent(window.escape(AWS.util.base64.decode(data)))); break; } }; socket.onclose = () => { try { if (!get(this,'userClosed')) { set(this,'status', 'closed'); term.destroy(); this.sendAction('dismiss'); } } catch (e) { } }; }; }, disconnect: function () { set(this,'status', 'closed'); set(this,'userClosed', true); var term = get(this,'term'); if (term) { term.destroy(); set(this,'term', null); } var socket = get(this,'socket'); if (socket) { socket.close(); set(this,'socket', null); } this.sendAction('disconnected'); }, willDestroyElement: function () { this.disconnect(); this._super(); }, containerDidChange: observer('containerName', function() { this.disconnect(); this.exec(); }), });