cronvel / terminal-kit

Terminal utilities for node.js
MIT License
3.08k stars 198 forks source link

Issue: Terminal stuck in while-loop when initating another terminal with stdin/stdout #230

Open Gandalf1783 opened 1 year ago

Gandalf1783 commented 1 year ago

Hello!

I have an issue in regards to the createTerminal() function of termkit.

I have the following code:

const newPty = nativePty.native.open(
        this.ptyInfo.cols,
        this.ptyInfo.rows
      );

      this.pseudoTerminal = {
        stdin_fd: newPty.master,
        stdout_fd: newPty.slave,
        stdin: new tty.WriteStream(newPty.master),
        stdout: new tty.ReadStream(newPty.slave),
      };

      Object.defineProperty(this.pseudoTerminal.stdout, "columns", {
        enumerable: true,
        get: () => this.ptyInfo.cols,
      });

      Object.defineProperty(this.pseudoTerminal.stdout, "rows", {
        enumerable: true,
        get: () => this.ptyInfo.rows,
      });

This is essentially creating a ptty. It provides Write- and ReadStreams. I would like to create a new termkit terminal on this pseudo-tty:

    const sshTerm = (this.term = termkit.createTerminal({
      stdin: this.pseudoTerminal.stdout,
      stdout: this.pseudoTerminal.stdin,
      stderr: undefined,
      generic: this.ptyInfo.term,
      appName: this.title,
      isSSH: true,
      isTTY: true,
    }));

This is the code I use to create a new Terminal.

The regular "console" Terminal which runs upon executing node is created by the following. It just creates a new Terminal and applies the layout. The document in which the layout lies is created using this.term.createDocument() inside the Interface class. The terminal is configured as fullscreen inside the Interface class.

  var consoleTerminal = termkit.createTerminal({
    appName: "BALLON via Console Host",
    isSSH: false,
    isTTY: true
  })
  var consoleInterface = new Interface(consoleTerminal);
  consoleInterface.setupLayout();
  logInfo("Console", "Created console interface!");

Using the pseudo-terminal, I cannot generate a new terminal that wont crash (when supplying stdio options).

My code is similar/identical to most parts to this: https://github.com/jwarkentin/node-monkey/blob/256e7b37746b030965b49a2ccbb435ed29f7128a/src/server/ssh-manager.js I do not have the prompt functions, and dont have a write class since my SSH-terminal is supposed to have the same view as the consoleTerminal with the layout.

To be precise, this is the SSH-Console module:

const {
  logInfo,
  logWarn,
  logError,
  log,
  logCommand,
  logSuccess,
  setAppState,
} = require("../../modules/cli/CLI");

const {
  utils: { parseKey },
  Server,
} = require("ssh2");

const { readFileSync } = require("fs");
const { inspect } = require("util");
const userManager = require("../userManager");
const { Interface } = require("../cli/Interface");
const nativePty = require("node-pty");
const tty = require("tty");
const termkit = require("terminal-kit");

logInfo("MODULE", "SSH Module is being loaded.");

var serverOptions = {
  host: "0.0.0.0",
  port: 23,
  title: "BALLON",
  silent: false,
};

var server;

var clientList = new Set();

function initServer() {
  server = new Server(
    {
      hostKeys: [readFileSync("./certs/ssh_key.pem")],
    },
    (client) => {
      const { title } = serverOptions;
      logInfo("SSH", "Client connected!");

      clientList.add(
        new SSHClient({
          client,
          title,
          userManager,
          onClose: () => clientList.delete(client),
        })
      );
    }
  )
    .on("close", () => {
      logWarn("SSH", "Client disconnected");
    })
    .on("error", (error) => {
      logError("SSH", "Console Host error: ^B" + error);
    })
    .listen(serverOptions.port, serverOptions.host, () => {
      if (!serverOptions.silent)
        logInfo(
          "SSH",
          "SSH-Console-Host is listening on port ^:^B" + server.address().port
        );
    });
}

class SSHClient {
  constructor(options) {
    this.options = options;
    this.client = options.client;
    this.session = undefined;
    this.sshStream = undefined;
    this.pseudoTerminal = undefined;
    this.term = undefined;
    this.ptyInfo = undefined;
    this.userManager = options.userManager;

    this.title = options.title;
    this.username = undefined;

    this.client.on("authentication", this.onAuth.bind(this));
    this.client.on("ready", this.onReady.bind(this));
    this.client.on("end", this.onClose.bind(this));
    this.client.on("error", this.onError.bind(this));
  }

  _initCmdMan() {}

  close() {
    if (this.sshStream) {
      this.sshStream.end();
    }
    this.onClose();
  }

  onError(error) {
    logError("SSH", "Instance emitted ^M" + error);
    console.error(error);
  }

  onAuth(ctx) {
    if (ctx.method == "password") {
      this.userManager
        .verifyUser(ctx.username, ctx.password)
        .then((result) => {
          if (result) {
            this.username = ctx.username;
            ctx.accept();
          } else {
            ctx.reject();
          }
        })
        .catch((err) => {
          ctx.reject();
        });
    } else if (ctx.method == "publickey") {
      ctx.reject();
    } else {
      ctx.reject();
    }
  }

  onReady() {
    this.client.on("session", (accept, reject) => {
      this.session = accept();

      this.session
        .once("pty", (accept, reject, info) => {
          this.ptyInfo = info;
          logInfo(
            "PTY",
            "SSH PTY reports as a " +
              info.term +
              ". It has a size of " +
              info.rows +
              " times " +
              info.cols +
              ". The total width/heigh is " +
              info.width +
              "/" +
              info.height
          );
          accept && accept();
        })
        .on("window-change", (accept, reject, info) => {
          this.ptyInfo = info;
          this._resize();
          accept && accept();
        })
        .once("shell", (accept, reject) => {
          this.sshStream = accept();
          this._initCmdMan();
          this._initStream();
          this._initPty();
          this._initTerm();
        });
    });
  }

  onClose() {
    let onClose = this.options.onClose;
    onClose && onClose();
  }

  onKey(name, matches, data) {}

  _resize({ term } = this) {
    if (term) {
      term.stdout.emit("resize");
    }
  }

  _initStream() {
    const sshStream = this.sshStream;
    sshStream.name = this.title;
    sshStream.isTTY = true;
    sshStream.setRawMode = () => {};
    sshStream.on("error", (error) => {
      console.error("SSH stream error:", error.message);
    });
  }

  _initPty() {
    try {
      const newPty = nativePty.native.open(
        this.ptyInfo.cols,
        this.ptyInfo.rows
      );

      this.pseudoTerminal = {
        stdin_fd: newPty.master,
        stdout_fd: newPty.slave,
        stdin: new tty.WriteStream(newPty.master),
        stdout: new tty.ReadStream(newPty.slave),
      };

      Object.defineProperty(this.pseudoTerminal.stdout, "columns", {
        enumerable: true,
        get: () => this.ptyInfo.cols,
      });

      Object.defineProperty(this.pseudoTerminal.stdout, "rows", {
        enumerable: true,
        get: () => this.ptyInfo.rows,
      });

      console.log(
        "ARE THEY THE SAME:" + (this.sshStream.stdin === this.sshStream.stdout)
      );
      logSuccess("PTY", "Setup PTY!");
    } catch (err) {
      logError("SSH", "Could not setup TTY for client.");
      console.log(err);
    }
  }

  _initTerm() {
    const sshTerm = (this.term = termkit.createTerminal({
      stdin: this.pseudoTerminal.stdout,
      stdout: this.pseudoTerminal.stdin,
      stderr: undefined,
      generic: this.ptyInfo.term,
      appName: this.title,
      isSSH: true,
      isTTY: true,
    }));
    sshTerm.windowTitle(this.title + " via SSH-Console Host");
    this.interface = new Interface(sshTerm, true, this.username);
    this.interface.setupLayout();
    this.interface.setAppState("SSH - READY");
    this.interface.redraw();
  }
}

module.exports = {
  initServer: initServer,
};

I have a Interface-Class which essentialy provides my document, layout and textboxes / input. This works "fine" on the regular console where I start the application. It does not correctly size itself on start, I have to resize it one time manually to get it displayed correctly.

Further, the Pseudo-Terminal is supposed to be connected to a SSH2 session, which uses "streams2" (?), which looks like a duplex-stream if I understood it correctly. I havent connected/piped this part yet, therefore nothing should be on the pseudoterminal.

Apparently, it generates some kind of issues. I assume that some kind of while-loop does not exit and therefore the code wont continue.

It actually also breaks the debugger. I cannot finish a CPU-Profile, nor a heap-snapshot which i find interesting. I dont know how i can help you with debugging further. If you have ideas or if you need further information, please dont hesitate to contanct me :)