Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Gandalf1783 opened this issue Apr 9, 2023 · 0 comments

Comments

@Gandalf1783
Copy link

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant