Viewing File: /usr/local/cpanel/share/libraries/cjt2/src/directives/terminal.js

/*
# cjt/directives/terminal.js                      Copyright(c) 2020 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/

/* ----------------------------------------------------------------------
INSTRUCTIONS FOR THIS DIRECTIVE:

You must independently load xterm.js’s CSS file, e.g.:

    /libraries/xtermjs/xterm.min.css
    /frontend/<theme-name>/libraries/xtermjs/xterm.min.css

The following goes in your AngularJS template:

    <cp-terminal></cp-terminal>

This reports alerts to the “myalerts” alert group (cf. the cp-alert-list
directive).

For now there’s not much flexibility because it’s only being used in
contexts where the directive basically *is* the application.
---------------------------------------------------------------------- */

/* global define: false */

define(
    [
        "lodash",
        "angular",
        "cjt/core",
        "cjt/util/locale",
        "cjt/util/uaDetect",
        "cjt/util/query",
        "cjt/io/websocket",
        "cjt/io/appstream",
        "xterm",
        "xterm/addons/fit/fit",
        "cjt/modules",
        "cjt/services/alertService",
        "cjt/directives/alert",
        "cjt/directives/alertList",
        "cjt/services/onBeforeUnload",
        "uiBootstrap",
    ],
    function(_, angular, CJT, LOCALE, UA, QUERY, WEBSOCKET, APPSTREAM, Terminal, Fit) {
        "use strict";

        Terminal.applyAddon(Fit);

        var module = angular.module("cjt2.directives.terminal", []);
        var TEMPLATE_PATH = "libraries/cjt2/directives/terminal.phtml";

        var NBSP = "\u00a0";

        // A resize is handled by cpsrvd itself rather than the shell,
        // so we send this information in an AppStream control message.
        function _sendResize(ws, term) {
            var rows = term.rows;
            var cols = term.cols;

            var msg = "resize:" + rows + "," + cols;

            ws.send( APPSTREAM.encodeControlPayload(msg) );
        }

        var shellUrl = WEBSOCKET.getUrlBase() + "/websocket/Shell";

        module.directive("cpTerminal", [ "alertService", "$log", "onBeforeUnload",
            function(alertService, $log, onBeforeUnload) {
                function _reportError(str) {
                    alertService.add({
                        type: "danger",
                        message: str,
                        closeable: true,
                        autoClose: false,
                        replace: false,
                        group: "myalerts"
                    });
                }

                return {
                    restrict: "E",
                    replace: true,
                    templateUrl: CJT.config.debug ? CJT.buildFullPath(TEMPLATE_PATH) : TEMPLATE_PATH,
                    controller: ["$scope", "$element", function($scope, $element) {
                        var terminalIsOpen;

                        var term = new Terminal( {
                            macOptionIsMeta: UA.isMacintosh(),

                            // These are defaults that we update
                            // via fit() prior to opening the WebSocket
                            // connection. We send the resulting dimensions
                            // as query parameters; this way the pty
                            // on the backend has the correct dimensions
                            // from the get-go.
                            cols: $scope._DEFAULT_COLS,
                            rows: $scope._DEFAULT_ROWS,
                        } );

                        // Use a closure rather than bind() here so that it’s always
                        // the same function regardless of the ws instance.
                        function _sendData(d) {
                            $scope._ws.send( APPSTREAM.encodeDataPayload(d) );
                        }
                        term.on("data", _sendData);

                        // It’s possible to resize the window between the
                        // time when we send off the WebSocket handshake and
                        // when we get the response. When that happens we need
                        // to mark the need to send off a window resize once
                        // the connection is open.
                        var pendingResize;

                        function fitAndSendGeometry() {
                            term.fit();

                            var ws = $scope._ws;
                            if (ws) {
                                if (ws.readyState === WebSocket.OPEN) {
                                    _sendResize(ws, term);
                                } else {
                                    pendingResize = true;
                                }
                            }
                        }

                        window.addEventListener("resize", fitAndSendGeometry);
                        if (CJT.isWhm()) {
                            window.addEventListener("toggle-navigation", fitAndSendGeometry);
                            window.addEventListener("toggle-navigation", fitAndSendGeometry);
                        }

                        term.on( "title", function _setTitle(title) {
                            $scope.title = title || NBSP;
                            $scope.$apply();
                        } );

                        // ------------------------------------------------
                        function _wsOnError(e) {
                            $log.error(e.toString());
                        }
                        function _wsOnOpen(e) {
                            $scope.opening = false;
                            $scope.$apply();

                            if (pendingResize) {
                                _sendResize( e.target, term );
                                pendingResize = false;
                            }
                        }
                        function _wsOnFirstMessage(e) {
                            var ws = e.target;
                            ws.removeEventListener("message", _wsOnFirstMessage);
                            $scope.loading = false;
                            $scope.$apply();

                            term.textarea.disabled = false;
                            term.textarea.focus();
                        }
                        function _wsOnClose(evt) {
                            $scope._ws = null;

                            if (!onBeforeUnload.windowIsUnloading() && (evt.code !== WEBSOCKET.STATUS.SUCCESS)) {
                                var errStr;
                                if ($scope.opening && (evt.code === WEBSOCKET.STATUS.ABORTED)) {
                                    errStr = LOCALE.maketext("The [asis,WebSocket] handshake failed at [local_datetime,_1,time_format_medium].", new Date());
                                } else {
                                    var why, errDetail;
                                    try {
                                        why = JSON.parse( evt.reason );
                                        if (why.got_signal) {
                                            errDetail = "SIG" + why.result;
                                            if (why.dumped_core) {
                                                errDetail += ", +core";
                                            }
                                        } else {
                                            $scope.exitCode = why.result;
                                        }
                                    } catch (err) {

                                        // Don’t warn if the server went away
                                        // suddenly.
                                        if (evt.reason) {
                                            $log.warn("JSON parse: " + err);
                                        }

                                        errDetail = WEBSOCKET.getErrorString(evt);
                                    }

                                    if (errDetail) {
                                        errStr = LOCALE.maketext("The connection to the server ended in failure at [local_datetime,_1,time_format_medium]. ([_2])", new Date(), errDetail);
                                    }
                                }

                                if (errStr) {
                                    _reportError(errStr);
                                }
                            }

                            $scope.closed = true;
                            $scope.opening = false;
                            $scope.loading = false;
                            $scope.$apply();

                            if (term.textarea) {
                                term.textarea.disabled = true;
                            }
                        }

                        // ------------------------------------------------

                        // Edge & IE11 lack TextDecoder support,
                        // so we have to do it manually.
                        // We could send text messages rather than binary,
                        // but it’ll be nice to be able to add ZMODEM support
                        // later on, and for that we’ll need binary.
                        function _setupBinaryParser(ws, term) {
                            var blobQueue = [];

                            var fileReader = new FileReader();

                            // NB: As of March 2018, our PhantomJS version
                            // doesn’t recognize addEventListener on
                            // FileReader instances.
                            fileReader.onload = function(e) {
                                term.write( e.target.result );

                                if (blobQueue.length) {
                                    e.target.readAsText( blobQueue.shift() );
                                }
                            };
                            fileReader.onerror = function(e) {
                                _reportError("UTF-8 decode error: " + e.target.error.toString());
                            };

                            $scope._wsOnMessage = function(evt) {
                                if (fileReader.readyState === FileReader.LOADING) {
                                    blobQueue.push(evt.data);
                                } else {
                                    fileReader.readAsText(evt.data);
                                }
                            };
                            ws.addEventListener("message", $scope._wsOnMessage);
                        }

                        // ------------------------------------------------

                        function _connect() {
                            if ($scope._ws) {
                                throw new Error("WebSocket already open!");
                            }

                            $scope.exitCode = null;

                            if (!terminalIsOpen) {
                                var parentNode = $element.find(".terminal-xterm").get(0);
                                if (!parentNode) {
                                    throw new Error("_connect() with no parent node!");
                                }

                                term.open( parentNode, true );
                                term.textarea.disabled = true;
                                term.fit();
                                terminalIsOpen = true;
                            }

                            var queryStr = QUERY.make_query_string( {
                                rows: term.rows,
                                cols: term.cols,
                            } );
                            var fullUrl = shellUrl + "?" + queryStr;

                            var WSConstructor = $scope._WebSocket;
                            $scope._ws = new WSConstructor(fullUrl);

                            $scope.opening = true;
                            $scope.loading = true;
                            $scope.closed = false;

                            var ws = $scope._ws;
                            ws.addEventListener( "error", _wsOnError );

                            ws.addEventListener( "open", _wsOnOpen );

                            // We wait until the first message to display
                            // the terminal because we want there to be a
                            // “waiting for terminal …” state to report.
                            ws.addEventListener( "message", _wsOnFirstMessage );

                            _setupBinaryParser( ws, term );

                            ws.addEventListener( "close", _wsOnClose );
                        }

                        _.assign(
                            $scope,
                            {
                                title: NBSP,

                                connect: _connect,

                                // while opening the WebSocket connection:
                                opening: true,

                                // while we’ve not received a message yet
                                // (implies “opening”)
                                loading: true,

                                // after a connection closes
                                closed: false,

                                exitCode: null,

                                // Strings
                                openingString: LOCALE.maketext("Opening a connection …"),
                                waitingString: LOCALE.maketext("Waiting for the terminal …"),
                                reconnectString: LOCALE.maketext("Reconnect"),
                                exitCodeString: LOCALE.maketext("Exit Code"),

                                // ----------------------------------------
                                // Testing interface
                                _alertService: alertService,
                                _DEFAULT_COLS: 80,
                                _DEFAULT_ROWS: 24,
                                _window: window,
                                _WebSocket: $scope._WebSocket || window.WebSocket,
                                _ws: null,
                                _wsOnError: _wsOnError,
                                _wsOnOpen: _wsOnOpen,
                                _wsOnFirstMessage: _wsOnFirstMessage,
                                _wsOnMessage: null,
                                _wsOnClose: _wsOnClose,
                            }
                        );

                        _connect();
                    } ],
                };
            }
        ] );
    }
);
Back to Directory File Manager