웹 기반 터미널을 만드는 방법

Originally posted on my blog

이 문서에서는 웹 기술을 사용하여 터미널을 구축하고 브라우저에서 사용하는 방법에 대한 기본적인 세부 정보를 제공합니다. 동일한 기술이 VSCode 내장 터미널 및 Hyper과 같은 터미널 앱을 만드는 데 사용됩니다.

우리는 서버와 클라이언트를 모두 만들어야 합니다. 그리고 Socket.IO을 사용하여 데이터를 주고 받을 예정입니다. 전자에 이것이 필요하면 socket.io 필요하지 않습니다. 글 말미에 전자 관련 정보를 확인해주세요.

우리가 사용할 주요 라이브러리:



고객 입장에서

  • Socket.io 클라이언트

  • xterm.js - 터미널용 UI

  • 서버 측

  • Socket.io 서버

  • node-pty - 유사 터미널을 생성합니다. 이에 대한 입력을 보내야 합니다. pseudoterminals에 대한 자세한 정보가 필요하면 this을 확인하십시오.

  • 클라이언트와 서버 모두에 대해 실행 중인 앱은 다음 코드 및 상자 링크에서 사용할 수 있습니다. 작동하지 않는 경우 링크를 열고 앱이 Codesandbox에 의해 최대 절전 모드인 경우 깨우기 위해 빠르게 새로 고침하십시오.
  • Server Codesandbox
  • Client Codesandbox

  • 사용 가능한 코드는 Github에서 사용할 수 있습니다.

    서버 생성



    먼저 기본적인 설정을 해봅시다. NodeJShttp 모듈에서 서버를 생성하여 socket.io 서버에 전달합니다.

    //index.js
    const http = require("http");
    const SocketService = require("./SocketService");
    
    /* 
      Create Server from http module.
      If you use other packages like express, use something like,
    
      const app = require("express")();
      const server = require("http").Server(app);
    
    */
    const server = http.createServer((req, res) => {
      res.write("Terminal Server Running.");
      res.end();
    });
    
    const port = 8080;
    
    server.listen(port, function() {
      console.log("Server listening on : ", port);
      const socketService = new SocketService();
    
     // We are going to pass server to socket.io in SocketService.js
      socketService.attachServer(server);
    });
    


    다음으로 socket.io 이벤트에 대한 이벤트 리스너를 추가할 래퍼 클래스를 만들어야 합니다.

    //SocketService.js
    
    const socketIO = require("socket.io");
    const PTYService = require("./PTYService");
    
    class SocketService {
      constructor() {
        this.socket = null;
        this.pty = null;
      }
    
      attachServer(server) {
        if (!server) {
          throw new Error("Server not found...");
        }
    
        const io = socketIO(server);
        console.log("Created socket server. Waiting for client connection.");
        // "connection" event happens when any client connects to this io instance.
        io.on("connection", socket => {
          console.log("Client connect to socket.", socket.id);
    
          this.socket = socket;
    
          this.socket.on("disconnect", () => {
            console.log("Disconnected Socket: ", socket.id);
          });
    
          // Create a new pty service when client connects.
          this.pty = new PTYService(this.socket);
    
         // Attach event listener for socket.io
          this.socket.on("input", input => {
            // Runs this listener when socket receives "input" events from socket.io client.
                    // input event is emitted on client side when user types in terminal UI
            this.pty.write(input);
          });
        });
      }
    }
    
    module.exports = SocketService;
    


    마지막으로 서버 측에서 node-pty 를 사용하여 의사 터미널 프로세스를 생성해 보겠습니다. 입력한 입력은 node-pty의 인스턴스로 전달되고 출력은 연결된 socket.io 클라이언트로 전송됩니다. 나중에 socket.io 클라이언트를 추가할 예정입니다.

    // PTYService.js
    
    const os = require("os");
    const pty = require("node-pty");
    
    class PTY {
      constructor(socket) {
        // Setting default terminals based on user os
        this.shell = os.platform() === "win32" ? "powershell.exe" : "bash";
        this.ptyProcess = null;
        this.socket = socket;
    
        // Initialize PTY process.
        this.startPtyProcess();
      }
    
      /**
       * Spawn an instance of pty with a selected shell.
       */
      startPtyProcess() {
        this.ptyProcess = pty.spawn(this.shell, [], {
          name: "xterm-color",
          cwd: process.env.HOME, // Which path should terminal start
          env: process.env // Pass environment variables
        });
    
        // Add a "data" event listener.
        this.ptyProcess.on("data", data => {
          // Whenever terminal generates any data, send that output to socket.io client
          this.sendToClient(data);
        });
      }
    
      /**
       * Use this function to send in the input to Pseudo Terminal process.
       * @param {*} data Input from user like a command sent from terminal UI
       */
    
      write(data) {
        this.ptyProcess.write(data);
      }
    
      sendToClient(data) {
        // Emit data to socket.io client in an event "output"
        this.socket.emit("output", data);
      }
    }
    
    module.exports = PTY;
    


    클라이언트 생성



    이제 UI가 나옵니다. 매우 간단합니다. 지금 해야 할 일은 xterm로 터미널을 만들고 이를 dom의 컨테이너에 연결하는 것입니다. 그런 다음 터미널의 입력을 연결된 socket.io의 서버로 전달합니다. 또한 socket.io 서버에서 xtermjs 터미널로 응답을 작성하는 이벤트 리스너를 socket.io-client에 추가할 예정입니다.

    html 페이지에서 xtermjs가 터미널을 연결할 위치div를 만듭니다.

    <!DOCTYPE html>
    <html>
      <head>
        <title>Terminal in Browser</title>
        <meta charset="UTF-8" />
      </head>
    
      <body>
        <div id="terminal-container"></div>
        <script src="src/index.js"></script>
      </body>
    </html>
    


    socket.io 클라이언트를 시작하기 전에 xtermjs 관련 함수와 socket.io-client에 필요한 이벤트 리스너를 포함하는 래퍼 클래스를 생성해 보겠습니다.

    // TerminalUI.js
    
    // You will need a bundler like webpack or parcel to use these imports.
    // The example in codesandboxes and github uses parcel.
    
    import { Terminal } from "xterm";
    import "xterm/css/xterm.css"; // DO NOT forget importing xterm.css
    
    export class TerminalUI {
      constructor(socket) {
        this.terminal = new Terminal();
    
        /* You can make your terminals colorful :) */
        this.terminal.setOption("theme", {
          background: "#202B33",
          foreground: "#F5F8FA"
        });
    
        this.socket = socket;
      }
    
      /**
       * Attach event listeners for terminal UI and socket.io client
       */
      startListening() {
        this.terminal.onData(data => this.sendInput(data));
        this.socket.on("output", data => {
          // When there is data from PTY on server, print that on Terminal.
          this.write(data);
        });
      }
    
      /**
       * Print something to terminal UI.
       */
      write(text) {
        this.terminal.write(text);
      }
    
      /**
       * Utility function to print new line on terminal.
       */
      prompt() {
        this.terminal.write(`\\r\\n$ `);
      }
    
      /**
       * Send whatever you type in Terminal UI to PTY process in server.
       * @param {*} input Input to send to server
       */
      sendInput(input) {
        this.socket.emit("input", input);
      }
    
      /**
       *
       * container is a HTMLElement where xterm can attach terminal ui instance.
       * div#terminal-container in this example.
       */
      attachTo(container) {
        this.terminal.open(container);
        // Default text to display on terminal.
        this.terminal.write("Terminal Connected");
        this.terminal.write("");
        this.prompt();
      }
    
      clear() {
        this.terminal.clear();
      }
    }
    


    xtermjs는 모든 종류의 멋진 기능을 지원합니다. 터미널용 테마를 만들 수 있고 다른 기능에 애드온을 사용할 수 있습니다. 자세한 내용은 xtermjs github repo을 확인하십시오. 이 예에서 더 많은 사용자 지정 권한을 원하는 경우 위의 TerminalUI.js 파일을 업데이트하고 this.terminal 개체를 사용자 지정할 수 있습니다. 예를 들어 기본 어두운 테마 옵션이 여기에 추가되었습니다.

    마지막으로 서버에서 node-pty 프로세스의 이벤트를 송수신하도록 클라이언트socket.io를 초기화해야 합니다.

    // index.js
    
    import { TerminalUI } from "./TerminalUI";
    import io from "socket.io-client";
    
    // IMPORTANT: Make sure you replace this address with your server address.
    
    const serverAddress = "http://localhost:8080";
    
    function connectToSocket(serverAddress) {
      return new Promise(res => {
        const socket = io(serverAddress);
        res(socket);
      });
    }
    
    function startTerminal(container, socket) {
      // Create an xterm.js instance.
      const terminal = new TerminalUI(socket);
    
      // Attach created terminal to a DOM element.
      terminal.attachTo(container);
    
      // When terminal attached to DOM, start listening for input, output events.
      // Check TerminalUI startListening() function for details.
      terminal.startListening();
    }
    
    function start() {
      const container = document.getElementById("terminal-container");
      // Connect to socket and when it is available, start terminal.
      connectToSocket(serverAddress).then(socket => {
        startTerminal(container, socket);
      });
    }
    
    // Better to start on DOMContentLoaded. So, we know terminal-container is loaded
    start();
    


    서버와 클라이언트가 모두 실행 중이면 브라우저에 터미널이 표시됩니다. 높이, 너비와 같은 다른 스타일 사용자 정의에 대해서는 xtermjs 문서를 확인하십시오.



    전자 사용자의 경우



    Electron에서는 xtermjs와 node-pty를 사용하는 것이 훨씬 더 간단합니다. 렌더러 프로세스는 노드 모듈을 실행할 수 있으므로 소켓 라이브러리를 사용하지 않고 xtermjs와 node-pty 간에 직접 데이터를 생성하고 전달할 수 있습니다. 간단한 예는 다음과 같습니다.

    // In electronjs renderer process
    
    // Make sure nodeIntegration is enabled in your BrowserWindow. 
    // Check github repo for full example (link given at the beginning of this article).
    
    // Choose shell based on os
    const shell = os.platform() === "win32" ? "powershell.exe" : "bash";
    
    // Start PTY process
    const ptyProcess = pty.spawn(shell, [], {
      name: "xterm-color",
      cwd: process.env.HOME, // Which path should terminal start
      env: process.env // Pass environment variables
    });
    
    // Create and attach xtermjs terminal on DOM
    const terminal = new Terminal();
    terminal.open(document.getElementById("terminal-container"));
    
    // Add event listeners for pty process and terminal
    // we don't need to use any socket to communicate between xterm/node-pty
    
    ptyProcess.on("data", function(data) {
      terminal.write(data);
    });
    
    terminal.onData(data => ptyProcess.write(data));
    


    작동하는 전자 예제가 Github 저장소에 추가되었습니다.

    기타 정보



    NodeJS의 출력만 인쇄하는 터미널 UI만 필요한 경우child_process node-pty가 필요하지 않습니다. child_process stdout을 xtermjs UI로 직접 보낼 수 있습니다.

    내 오픈 소스 프로젝트https://github.com/saisandeepvaddi/ten-hands 중 하나가 이 방식으로 작동합니다. Ten Hands을 확인하여 xtermjs , socket.ioReactJS 를 함께 사용하여 터미널 기반 앱을 빌드하는 방법을 자세히 알아보세요.

    감사합니다 🙏

    좋은 웹페이지 즐겨찾기