Search code examples
node.jssocket.ioxtermjs

Stream interactive shell session with socket.io


I have 3 components device, server and frontend (admin).

Server

Starts socket.io server with 2 namespaces /admin and /client.
If socket from /admin namespace sends data, server passes it along to /client namespace. If socket from /client namespace sends data, server passes it along to /admin namespace.


const io = require('socket.io');
const device = io.of('/device');
const admin = io.of('/admin');

device.on('connection', (socket) => {

  socket.on('data', (data) => {
    console.log("PASSING DATA FROM [DEVICE] TO [ADMIN]")
    admin.emit('data', data);
  })

});

admin.on('connection', (socket) => {

  socket.on('data', (data) => {
    console.log("PASSING DATA FROM [ADMIN] TO [DEVICE]")
    device.emit('data', data);
  });

});

io.listen(80);

Device

Uses socket.io-client to connect to socket.io server.
Starts interactive shell session using node-pty.

const io = require('socket.io-client');
const socket = io('http://localhost:80/client');
const os = require('os');
const pty = require('node-pty');
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';

const ptyProcess = pty.spawn(shell, [], {
  name: 'xterm-color',
  cols: 80,
  rows: 30
});

socket.on('connect', () => {

});

// INPUT DATA
socket.on('data', (data) => {
  ptyProcess.write(data);
});

// OUTPUTING DATA
ptyProcess.onData = (data) => {
  socket.emit('data', data)
}

Frontend

Finally I have the frontend which uses xterm.js to create a terminal inside the browser. I am using vue. The browser client as well connects to socket.io server on the /admin namespace. Basically I have this :


<template>
  <div id="app">
    <div id="terminal" ref="terminal"></div>
  </div>
</template>

<script>

import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { io } from 'socket.io-client';

export default {
  mounted() {

    const term = new Terminal({ cursorBlink : true });
    term.open(this.$refs.terminal);

    const socket = io('http://localhost:80/admin');

    socket.on('connect', () => {

      term.write('\r\n*** Connected to backend***\r\n');
      term.onData((data) => {
        socket.emit('data', data);
      })

      socket.on('data', (data) => {
        term.write(data);
      });

      socket.on('disconnect', () => {
        term.write('\r\n*** Disconnected from backend***\r\n');
      });

    });

  }
}
</script>

Problem

❌ Starting the pty session seems to work, at least there are now errors reported. However it seems the onData listener callback is never fired, even when I ptyProcess.write() something.

❌ Getting input from xterm all the way to the device ptyProcess.write does not seem to work. I can see the data passed along through the socket.io sockets all the way to the device. But from there nothing happens. What do I miss ? Also I don't see my input in the xterm window as well.


Solution

  • After switching from child_process to using node-pty to create an interactive shell session I almost had it right. Following the node-pty documentation it marked the on('data') eventhandler as deprecated. Instead I should use .onData property of the process to register a callback. Like this:

    ptyProcess.onData =  function(data) {
      socket.emit('data', data);
    };
    

    But that didn't do anything. So I switched back to the depracated way of adding an event listener:

    ptyProcess.on('data', function(data) {
      socket.emit('data', data);
    });
    

    Now I have a working interactive shell session forwarded from a remote device through websocket inside my browser ✅.

    UPDATE
    Did more digging for onData property. Realized it's not a property but a method so I used it wrong. This would be the prefered way :

    ptyProcess.onData(function(data) {
      socket.emit('data', data);
    });
    

    Which also works as expected 👍