"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = exports.terminalMode = exports.Terminal = void 0;

var React = _interopRequireWildcard(require("react"));

var _uuid = require("uuid");

var _core = require("@kui-shell/core");

var _pluginClientCommon = require("@kui-shell/plugin-client-common");

var _xterm = require("xterm");

var _xtermAddonFit = require("xterm-addon-fit");

var _util = require("../../util/util");

var _resource = require("../../model/resource");

var _options = require("../../../controller/kubectl/options");

var _ContainerCommon = require("./ContainerCommon");

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

/*
 * Copyright 2020 IBM Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) {
  function adopt(value) {
    return value instanceof P ? value : new P(function (resolve) {
      resolve(value);
    });
  }

  return new (P || (P = Promise))(function (resolve, reject) {
    function fulfilled(value) {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    }

    function rejected(value) {
      try {
        step(generator["throw"](value));
      } catch (e) {
        reject(e);
      }
    }

    function step(result) {
      result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
    }

    step((generator = generator.apply(thisArg, _arguments || [])).next());
  });
};

const strings = (0, _core.i18n)('plugin-kubectl', 'exec');
/**
 * Interval in milliseconds before we warn the user that we are about
 * to abort the PTY.
 */

const MINUTES = 60 * 1000;
const INTERVAL_TILL_IDLE_COUNTDOWN = 15 * MINUTES;
/**
 * Interval in milliseconds after the first countdown after which we
 * actually abort the PTY.
 */

const INTERVAL_OF_IDLE_COUNTDOWN = 5 * MINUTES;
/**
 * Take a hex color string and return the corresponding RGBA with the given alpha
 *
 */

function alpha(hex, alpha) {
  if (/^#[0-9a-fA-F]{6}$/.test(hex)) {
    const red = parseInt(hex.slice(1, 3), 16);
    const green = parseInt(hex.slice(3, 5), 16);
    const blue = parseInt(hex.slice(5, 7), 16);
    return `rgba(${red},${green},${blue},${alpha})`;
  } else {
    return hex;
  }
}

class Terminal extends _ContainerCommon.ContainerComponent {
  constructor(props) {
    super(props);
    this.cleaners = [];
    /** After some period of receiving no data, auto-abort the PTY */

    this.isIdle = true;
    this._unmounted = false;
    this._initInProgressForContainer = false;
    this._initCount = 0;
    this.state = {
      container: this.defaultContainer(),
      dom: undefined,
      xterm: undefined,
      doResize: undefined,
      perTerminalCleaners: [],
      isLive: 'Paused',
      isTerminated: false,
      waitingForHysteresis: false,
      gotSomeData: false,
      job: undefined,
      streamUUID: undefined
    };
    this.updateToolbar(this.state.isLive);

    const focus = () => {
      this.doFocus();
      this.doXon();
    };

    const focusOnEvent = `/mode/focus/on/tab/${(0, _core.getPrimaryTabId)(this.props.tab)}/mode/terminal`;

    _core.eventChannelUnsafe.on(focusOnEvent, focus);

    this.cleaners.push(() => _core.eventChannelUnsafe.off(focusOnEvent, focus));
    const xoff = this.doXoff.bind(this);
    const focusOffEvent = `/mode/focus/off/tab/${(0, _core.getPrimaryTabId)(this.props.tab)}/mode/terminal`;

    _core.eventChannelUnsafe.on(focusOffEvent, xoff);

    this.cleaners.push(() => _core.eventChannelUnsafe.off(focusOffEvent, xoff));
    const resizeListener = this.onResize.bind(this);
    window.addEventListener('resize', resizeListener);
    this.cleaners.push(() => window.removeEventListener('resize', resizeListener));
  }
  /** Which container should we focus on by default? */


  defaultContainer() {
    if (this.props.args.argsForMode) {
      const container = (0, _options.getContainer)(this.props.args.argsForMode, 'exec');

      if (container) {
        // TODO MAYBE? validate container name?
        return container;
      }
    }

    return this.props.pod.spec.containers[0].name;
  }

  doXon() {
    if (this.state && this.state.job) {
      setTimeout(() => this.state.job.xon());
    }
  }

  doXoff() {
    if (this.state && this.state.job) {
      setTimeout(() => this.state.job.xoff());
    }
  }

  doFocus() {
    if (this.state && this.state.xterm) {
      setTimeout(() => this.state.xterm.focus());
    }
  }
  /**
   * Convert the current theme to an xterm.js ITheme
   *
   */


  static injectTheme(xterm, dom) {
    const theme = getComputedStyle(dom); // debug('kui theme for xterm', theme)

    /** helper to extract a kui theme color */

    const val = (key, kind = 'color') => theme.getPropertyValue(`--${kind}-${key}`).trim();

    const itheme = {
      foreground: val('text-01'),
      background: val('base01'),
      cursor: val('support-01'),
      selection: alpha(val('selection-background'), 0.3),
      black: val('black'),
      red: val('red'),
      green: val('green'),
      yellow: val('yellow'),
      blue: val('blue'),
      magenta: val('magenta'),
      cyan: val('cyan'),
      white: val('white'),
      brightBlack: val('black'),
      brightRed: val('red'),
      brightGreen: val('green'),
      brightYellow: val('yellow'),
      brightBlue: val('blue'),
      brightMagenta: val('magenta'),
      brightCyan: val('cyan'),
      brightWhite: val('white')
    }; // debug('itheme for xterm', itheme)

    xterm.setOption('theme', itheme);
    xterm.setOption('fontFamily', val('monospace', 'font'));

    try {
      const fontTheme = getComputedStyle(document.querySelector('body .repl .repl-input input'));
      xterm.setOption('fontSize', parseInt(fontTheme.fontSize.replace(/px$/, ''), 10)); // terminal.setOption('lineHeight', )//parseInt(fontTheme.lineHeight.replace(/px$/, ''), 10))
      // FIXME. not tied to theme

      xterm.setOption('fontWeight', 400);
      xterm.setOption('fontWeightBold', 600);
    } catch (err) {
      console.error('Error setting terminal font size', err);
    }
  }

  onResize() {
    if (this.state.xterm) {
      this.state.doResize();
    }
  }
  /** When we are going away, make sure to abort the streaming job. */


  componentWillUnmount() {
    this._unmounted = true;
    super.componentWillUnmount();
    this.disposeTerminal();
    this.cleaners.forEach(cleaner => cleaner());
  }

  disposeTerminal() {
    this.state.xterm.dispose();
    this.state.perTerminalCleaners.forEach(cleaner => cleaner());
    this.setState({
      xterm: undefined,
      perTerminalCleaners: []
    });
  }
  /** Finish up the initialization of the stream */


  componentDidUpdate() {
    this.updateToolbar(this.state.isLive);

    if (!this.state.job && !this.state.isTerminated) {
      this.initStream();
    }
  }

  abortPriorJob() {
    if (this.state.job) {
      const abortThisJob = this.state.job; // the setTimeout helps us avoid exit-after-spawn races

      setTimeout(() => abortThisJob.abort(), 5000);
    }
  }

  showContainer(container, extraState) {
    this.setState(curState => {
      this.abortPriorJob();
      return Object.assign({
        container,
        job: undefined,
        streamUUID: undefined,
        isTerminated: false
      }, extraState ? extraState(curState) : {});
    });
  }

  static getDerivedStateFromProps(props, state) {
    if (state.dom && !state.xterm) {
      return Terminal.initTerminal(state.dom);
    } else {
      return state;
    }
  } // eslint-disable-next-line @typescript-eslint/no-unused-vars


  toolbarText(status) {
    if (this.state.isTerminated) {
      return {
        type: 'error',
        text: strings('The terminal connection has closed.')
      };
    } else if (!this.state.job) {
      return {
        type: 'warning',
        text: strings('Please wait. Connecting to container X.', this.state.container)
      };
    } else if (this.state.isLive === 'Idle') {
      return {
        type: 'warning',
        text: strings('Connection is idle, and will expire shortly. Connected to container X.', this.state.container)
      };
    } else {
      return {
        type: 'info',
        text: strings('Connected to container X.', this.state.container)
      };
    }
  }
  /** @return the command to issue in order to initialize the pty stream */


  ptyCommand() {
    const {
      args,
      pod
    } = this.props;
    const {
      container
    } = this.state;
    const command = args.argsForMode && args.argsForMode.command || `${(0, _util.getCommandFromArgs)(this.props.args)} exec -it ${pod.metadata.name} -c ${container} -n ${pod.metadata.namespace} -- sh`; // only use argsForMode once

    if (args.argsForMode && args.argsForMode.command) {
      args.argsForMode.command = undefined;
    }

    return {
      command
    };
  }
  /** Indicate that we have received some data */


  gotSomeData(streamUUID) {
    if (!this.state.gotSomeData) {
      this.setState(curState => {
        if (!curState.gotSomeData && curState.streamUUID === streamUUID) {
          return {
            gotSomeData: true
          };
        }
      });
    }
  }
  /** Tell the user whether or not we are preparing to idle the PTY */


  indicate(isLive) {
    this.updateToolbar(isLive);
    this.setState({
      isLive
    });
  }

  initiateIdleCountdown() {
    this.indicate('Idle');
    this.idleTimeout = setTimeout(() => {
      this.abortPriorJob();
    }, INTERVAL_OF_IDLE_COUNTDOWN);
  }

  initiateIdleTimer() {
    return setTimeout(() => this.initiateIdleCountdown(), INTERVAL_TILL_IDLE_COUNTDOWN);
  }
  /** Initialize a PTY stream from the current state's settings */


  initStream() {
    const {
      tab: {
        REPL: repl
      }
    } = this.props;
    const {
      xterm
    } = this.state;
    const streamUUID = (0, _uuid.v4)();

    if (this._initInProgressForContainer === this.state.container) {
      return;
    } else if (this._initCount++ > 0) {
      // Note: reset, not clear. This will fully clear the xterm screen
      // as we prepare for a new connection. clear() alone only does a
      // ctrl+L, and thus the xterm will still show e.g. the CWD part of
      // the old prompt.
      xterm.reset();
    }

    this._initInProgressForContainer = this.state.container; // what command line should we use? and should we default to Live?
    // or Paused?

    const {
      command,
      isLive = 'Live'
    } = this.ptyCommand(); // execute that command, setting up the onInit, onReady, and
    // onExit lifecycle handlers

    repl.qexec(command, undefined, undefined, {
      tab: this.props.tab,
      onInit: () => {
        if (this._unmounted) {
          return;
        }

        this._initInProgressForContainer = false;
        return _ => {
          if (this.idleTimeout) {
            clearTimeout(this.idleTimeout);
            this.indicate(isLive);
            this.idleTimeout = this.initiateIdleTimer();
          }

          if (typeof _ === 'string' && this.state.streamUUID === streamUUID) {
            this.gotSomeData(streamUUID);
            xterm.write(_);
          }
        };
      },
      onReady: job => {
        if (this._unmounted) {
          return;
        }

        xterm.onData(data => {
          if (!this._unmounted && this.state.streamUUID === streamUUID) {
            job.write(data);
          }
        });
        this.doFocus();
        setTimeout(() => {
          if (!this.state.isTerminated) {
            this.setState(curState => {
              if (curState.streamUUID === streamUUID) {
                return {
                  waitingForHysteresis: false
                };
              }
            });
          }
        }, _ContainerCommon.HYSTERESIS);

        if (this.idleTimeout) {
          clearTimeout(this.idleTimeout);
        }

        this.idleTimeout = this.initiateIdleTimer();
        this.setState({
          job,
          streamUUID,
          isLive,
          waitingForHysteresis: true
        });
      },
      onExit: exitCode => {
        if (this._unmounted) {
          return;
        }

        this.setState(curState => {
          if (curState.streamUUID === streamUUID) {
            const isLive = exitCode === 0 ? 'Stopped' : 'Error';
            this.updateToolbar(isLive);
            return {
              job: undefined,
              streamUUID: undefined,
              container: curState.container,
              isLive,
              isTerminated: true
            };
          }
        });
      }
    }).catch(err => {
      if (!this._unmounted && this.state.streamUUID === streamUUID) {
        console.error(err);
        this.updateToolbar('Error');
      }
    });
  }
  /** Are we focusing on all containers? */


  isAllContainers() {
    return super.isAllContainers() && !this.state.isTerminated;
  }

  static initTerminal(dom) {
    // for tests, we need to extract the text in the Terminal; for
    // now, we facilitate this by using the dom renderer
    const rendererType = (0, _pluginClientCommon.inDebugMode)() || process.env.RUNNING_SHELL_TEST || process.env.RUNNING_KUI_TEST ? 'dom' : 'canvas';
    const xterm = new _xterm.Terminal({
      scrollback: 1000,
      rendererType
    });
    const perTerminalCleaners = [];

    const inject = () => Terminal.injectTheme(xterm, dom);

    inject();

    _core.eventChannelUnsafe.on('/theme/change', inject);

    perTerminalCleaners.push(() => _core.eventChannelUnsafe.on('/theme/change', inject));
    const fitAddon = new _xtermAddonFit.FitAddon();
    xterm.loadAddon(fitAddon);
    xterm.open(dom);

    const doResize = () => {
      fitAddon.fit();
    }; // resize once on init


    doResize();
    return {
      xterm,
      doResize,
      perTerminalCleaners
    };
  }
  /** Should we wait a bit before proclaiming we have no data? */


  needsHysteresis() {
    return false;
  }
  /** Render in the case we have received no information from the PTY */


  nothingToShow() {
    return React.createElement(React.Fragment, null);
  }
  /** If needsHysteresis(), and we haven't yet received any data, render as such */


  maybeNothingToShow() {
    if (this.needsHysteresis()) {
      if (this.state.waitingForHysteresis || !this.state.xterm || !this.state.job) {
        return React.createElement(React.Fragment, null);
      } else if (!this.state.gotSomeData) {
        return this.nothingToShow();
      }
    }
  }

  render() {
    if (this.state.dom && !this.state.xterm) {
      return React.createElement(_pluginClientCommon.Loading, null);
    } else {
      return React.createElement("div", {
        className: "kui--full-height kui--terminal kui--relative-positioning",
        "data-needs-hysteresis": this.needsHysteresis(),
        "data-got-some-data": this.state.gotSomeData,
        ref: dom => this.setState({
          dom
        })
      }, this.maybeNothingToShow());
    }
  }

}
/**
 * The content renderer for the summary tab
 *
 */


exports.Terminal = Terminal;

function content(tab, pod, args) {
  return __awaiter(this, void 0, void 0, function* () {
    return {
      react: function TerminalProvider(toolbarController) {
        return React.createElement(Terminal, {
          tab: tab,
          pod: pod,
          args: args,
          toolbarController: toolbarController
        });
      }
    };
  });
}
/**
 * Add a Pods mode button to the given modes model, if called for by
 * the given resource.
 *
 */


const terminalMode = {
  when: resource => (0, _resource.isPod)(resource) && !resource.isSimulacrum,
  mode: {
    mode: 'terminal',
    label: 'Terminal',
    content
  }
};
exports.terminalMode = terminalMode;
var _default = terminalMode;
exports.default = _default;