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 * as fit from 'xterm/lib/addons/fit/fit'; 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(); next(this, 'exec'); }, willDestroyElement() { this.disconnect(); this._super(); }, 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('state', 'running'); }), 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) { term.fit() socket.send(`4${ AWS.util.base64.encode(JSON.stringify({ Width: term.cols, Height: term.rows })) }`); } }, onResize() { 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.destroy(); this.sendAction('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.destroy(); this.sendAction('dismiss'); } } catch (e) { } } socket.onopen = () => { set(this, 'status', statusMap.init); Terminal.applyAddon(fit) var term = new Terminal({ cursorBlink: true, useStyle: true, fontSize: 12, }); set(this, 'term', term); term.on('data', (data) => { socket.send(`0${ AWS.util.base64.encode(data) }`); }); term.open(this.$('.shell-body')[0], true); this.fit(); term.focus(); socket.onmessage = (message) => { set(this, 'status', statusMap.connected); this.sendAction('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() { setProperties(this, { status: statusMap.closed, userClosed: true, }) const term = get(this, 'term'); if (term) { term.destroy(); set(this, 'term', null); } const socket = get(this, 'socket'); if (socket) { socket.close(); set(this, 'socket', null); } this.sendAction('disconnected'); }, });