The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Net::TCP::PtyServer - Serves pseudo-terminals. Opens a listening connection on a port, waits for network connections on that port, and serves each one in a seperate PTY.

HACKING

ALGORITHM

The actual algorithm is simple, although the implementation looks a bit ickey.

1 Create a listening socket
2 Wait for the next connection on the socket (by calling accept).
3 Fork.
3.1 Parent process closes its copy of the handle (by calling stopio) then goes back to 1.
3.2 In the child process, we create a pseudo-TTY and fork
3.2.1 The child process runs the command by re-opening STDOUT, STDERR and STDIN to the pseudo-TTY's slave terminal and then calling exec; this does not return

This is necessary because the filehandles need to be exactly the same, and we get buffering/crashing issues if we try an open3()

3.2.2 The parent process closes its copy of the pseudo-TTY's slave terminal (using close).
3.2.3 The parent then repeatedly pipes the data between the pseudo-TTY and the networked filehandle until the exec()ed process completes.
3.2.4 The parent process then closes the pseudo-TTY (by implicit destruction) and the networked filehandle (by close), and exits.

Coping with terminal size changes

To set the size of a terminal, you need to call ioctl(), and pass the pseudo-TTY handle, the constant TIOCSWINSZ (defined in termio.h or termios.h - or on my system, defined in the asm includes and imported by one of them), and a winsize{} C-structure.

The TIOCGWINSZ (G instead of S) can also be used to get the size of a terminal. This is used to generate the structure passed to ioctl in the case of the pseudo-TTY running on a real terminal; see this code from IOS::TTY (referenced by IOS::PTY):

   sub clone_winsize_from {
     my ($self, $fh) = @_;
     my $winsize = "";
     croak "Given filehandle is not a tty in clone_winsize_from, called"
       if not POSIX::isatty($fh);  
     return 1 if not POSIX::isatty($self);  # ignored for master ptys
     ioctl($fh, &IO::Tty::Constant::TIOCGWINSZ, $winsize)
       and ioctl($self, &IO::Tty::Constant::TIOCSWINSZ, $winsize)
         and return 1;
     warn "clone_winsize_from: error: $!" if $^W;
     return undef;
   }

The structrure of winsize is defined in termios.h as follows:

   struct winsize {
           unsigned short ws_row;
           unsigned short ws_col;
           unsigned short ws_xpixel;
           unsigned short ws_ypixel;
   };

And the Internet tells me that ws_row is the number of rows, ws_col the number of columns, ws_xpixel the number of horizontal pixels across the terminal, and ws_ypixel the number of vertical pixels across the terminal.

After a little experiementing, this seems to work to create the struct, although it should be noted that this assumes that the struct has the same memory alignment as an array of unsigned shorts:

    my $winsize = pack("S*",$ws_row,$ws_col,$ws_xpixel,$ws_ypixel);

So that's what I'm trying to use (thus saving an XS C function)

BUGS

The module still has to handle the TELNET protocol properly. In particular, the remapping of IAC and handling of TELNET escapes.

For now, we just send the command to turn off echo and linemode, which otherwise interferes with the UI (we also ignore the response, but this seems to have no ill effects so far).

Control characters (ctrl+q, ctrl+x) are coming in as 0x11 (17) and 0x18 (24); these seem to need translating into \C and the keycode for some reason; the translation is not being picked up through the pseudo-TTY. (For now I'll just use character codes in the code that uses this; they seem simpler to me anyway).

When the TCP connection is dropped, we don't currently SIGHUP. We may be able to do this by close()ing the master terminal, but it's probably better to send an explicit HUP signal as well.

METHODS

# Don't make zombies when we don't wait for forks (see perlipc): $SIG{CHLD} = 'IGNORE';

setTerminalSize

Used internally in response to an incoming NAWS command

Takes the terminal as the first argument, followed by the number of rows, then the number of columns. The number of horizontal and vertical pixels can also be specified, but the default is to assume an 8x8 pixel character.

run

Takes a port number as the first argument, followed by a command and its arguments.

Listens for connections on the given port. exec()s the given command on a pseudo-terminal on the given port in a child process for each connection.

Does not return (but it could die if something really goes wrong)