Uses process IO Instead of SSH2 socket IO
Hey,
As of Bun v1.2.5, I noticed that the ssh2 server package should be working correctly. So I decided to try integrating @opentui/react with it. Unfortunately, things didn’t go quite as expected.
When I start the SSH server, the TUI output is written to the server’s process IO instead of the client’s stream when connecting (provided by ssh2). I attempted to debug this myself, but due to my limited understanding of the codebase, I wasn’t able to figure out the issue.
Interestingly, when using nearly identical code with Ink instead of @opentui/react, only replacing the render and elements, everything works, the UI renders to the SSH client stream, with minor issues e.g. the known LF/CRLF issue which can be patched.
To reproduce it:
Create ssh key
Generate key in same folder as the code
ssh-keygen -t rsa -b 4096 -f host.key
Code snippet
// server.ts
import { Server as SSHServer } from "ssh2";
import { render } from "@opentui/react";
import { readFileSync } from "fs";
import { PassThrough } from "stream";
const ssh = new SSHServer({
hostKeys: [readFileSync('host.key')]
});
ssh.on("connection", (client, info) => {
client.on("authentication", (ctx) => {
ctx.accept();
});
client.on("ready", () => {
client.on("session", (accept, reject) => {
const session = accept();
session.on("pty", (acceptPty) => {
acceptPty();
});
session.on("shell", async (acceptShell) => {
const stream = acceptShell();
const sshInput = new PassThrough();
const sshOutput = new PassThrough();
// Pipe streams between SSH and OpenTUI
stream.pipe(sshInput);
sshOutput.pipe(stream);
const App = () => (
<box style={{ flexDirection: "column", padding: 1 }}>
<text>Welcome to my TUI over SSH!</text>
</box>
);
// render with custom stdin/stdout
await render(<App />, {
stdin: sshInput as unknown as NodeJS.ReadStream,
stdout: sshOutput as unknown as NodeJS.WriteStream,
exitOnCtrlC: true,
});
});
});
});
});
ssh.listen(2222, "0.0.0.0", () => {
console.log("SSH server listening on port 2222");
});
Yes, that is a known limitation currently, because the write out happens natively in zig, in a thread even if not on Linux. Native doesn't know about the js streams and I cannot hook into the bun event loop, yet.
I want to keep native working standalone as well, so my thought here was giving the renderer a fd/socket/address that it should write/connect to.
Open to ideas.
Edit: mind that the renderer is stateful, so currently for solid there is only one renderer instance and rendering the app for multiple clients is undefined behavior currently.
Edit2: until there is an optimised way to run multiple stateful renderers in the same process, a working path would be to use a worker thread per session, with an app instance each, forward stdin to the thread and pipe the thread stdout to the socket again.
Edit3: I have been thinking about this before, but didn't come to a conclusion, so collecting my thoughts here. Another option is: optionally prevent the native renderer from writing out, let js via FFI know about the write buffer pointers, read the write buffer in bun and write it out to the socket or any stream.