#!/usr/bin/perl ####################################################################### # ttyd.pl # # This program is a perl implementation of a program we used to use # at Computone called Iservd. It maps pseudo ttys on a UNIX system # to a remote serial ports on terminal servers that can support the # simple system of TCP -> Serial. This program can not send # serial settings such as speed and parity to the remote. There is no # way in UNIX that I know of to do that via a pseudo without using # a real character driver and implementing RFC2217 in the driver or # some user space program the driver uses. It might be possible # to create a protocol that the application uses with this program # and then this program could implement RFC2217. The problem with # that is that this program should be used when you can not modify your # serial application. In my test I had a terminal server in Alpharetta # attached to a serial console of a Linux server. I then pointed this # progarm to it and use Minicom on my laptop to log into the console # of the Linux server. Works great. # # Many things you may want to do are not possible with this method. # Many people who use Iservd wanted a terminal server to act like # a real serial port on their UNIX box. Since hardware settings can # not be sent this does help their cause. Also beware of DTR/DCD behavior. # In some terminal server implementations if the remote serial device drops # DTR and the TS see's loss of carrier they will also drop the socket to # signal to us that the remote has dropped carrier. Our reaction should # be to close the pseudo tty. If your program works correctly and you # are using -clocal then you should see a hangup on the pseudo. This simulates # loss of carrier in this implemenation. You'll want to read the # manual of your terminal server to see how it behaves. # Likewise there should be a method where when your app closes the pseudo # we detect it and close the socket to the TS. The TS should see that and # toggle DTR on the serial port. I do not think I've programmed ttyd.pl to # do this but you can test and make modifications as you see fit. # In iservd these features were part of the config for each tty. # # YMMV! # Questions? cfowler@outpostsentinel.com ####################################################################### use IO::Socket::INET; use IO::Pty; use IO::Select; use XML::Simple; use Getopt::Std; use vars qw/$opt_c $opt_d $opt_k/; use strict; #######################################3 # Constants #######################################3 # The following are states that are not # currently used but could be used in the future use constant DISCONNECTED => 0; use constant CONNECT_IN_PROGRESS => 1; use constant CONNECTED => 2; #######################################3 # Globals #######################################3 my %remotes = (); my $select_timeout = 30; $opt_c = "config.xml"; # Config File $opt_d = 0; # run as a daemon $opt_k = 0; # kill running ttyd process # Function will read config data. # File should be in XML format and look like this: # # #
# # # #
#
# # Now I'm using XML::Simple and it does provide # an easy way to parse XML data but you should now # a bit of how it creates data. In the function I # assume that you will have at least two elements # in the file. If you do then XML::Simple places those # elements in an array. If not it places them in a hash. Maybe # there is a universal way to deal wiht either a hash or # an array in this case but I simply wrote the code to # work when XML::Simple provides me with an array of # objects. # If you want to support 1 or many you'll need to modify # this function to be smarter. sub config { my $data = ""; my $ready = undef; # Yea I know that XML::Simple will handle # file. There may be a case where we want to # do some pre-parsing of the data to verify before # even giving it to the XML parser. open D, "< $opt_c" or return undef; while() { $data .= "$_"; } close D; # Like here return undef unless $data =~ m//; my $xml = XMLin($data); foreach my $ref (@{$xml->{'remote'}}) { # Check for required values # Hard to make a connection if we do not know the # address and port. We need a device name to create the # symbolic link. next unless defined $ref->{'address'}; next unless defined $ref->{'port'}; next unless defined $ref->{'device_name'}; $ready = 1; # We have at least one remote in our config $remotes{$ref->{'device_name'}}{'device_name'} = $ref->{'device_name'}; $remotes{$ref->{'device_name'}}{'port'} = $ref->{'port'}; $remotes{$ref->{'device_name'}}{'address'} = $ref->{'address'}; if(defined $ref->{'log_file'}) { $remotes{$ref->{'device_name'}}{'log_file'} = $ref->{'log_file'}; } } return $ready; } # Function will setup a socket and start the connect # process. # Caller should place the returned file handle and place # in a select writers list. If the write can happen then # it means that something has changed in regards to the # connecting process. Maybe we were able to connect maybe # not. Caller needs to validate that we are connected # before placing the file handle in the readers select # list sub setup { my ($addr, $port) = @_; my $sock = IO::Socket::INET->new(Proto => 'tcp', Blocking => 0); my $iaddr = inet_aton($addr) or return undef; my $paddr = sockaddr_in($port, $iaddr); my $ret = connect($sock, $paddr); if (!$ret && ! $!{EINPROGRESS}) { close $sock; return undef; } return $sock; } sub main { my $readers = new IO::Select; my $writers = new IO::Select; $| = 1; # Process Command line arguments getopts("c:dk"); # simple way to look for a program and kill it. if($opt_k) { open PID_FILE, "< /var/run/ttyd.pid" or die "No ttyd process running?\n"; my $pid = ; print "Killing ttyd process at $pid\n"; kill 15,($pid); exit 0; } # Become a daemon and do "the right thing"(tm) if($opt_d) { my $pid = fork(); exit 0 if $pid; # Parent chdir "/tmp"; # We are now in the child # Redirect these so there are no lingering # ttys. open STDOUT, "> /dev/null"; open STDERR, "> /dev/null"; open STDIN, " /var/run/ttyd.pid"; print PID_FILE "$$"; close PID_FILE; $SIG{'TERM'} = sub { unlink "/var/run/ttyd.pid"; exit 0; }; } # Load up the config config() or die "Failed to parse config!\n"; foreach my $ref (keys %remotes) { $ref = $remotes{$ref}; my $sock = setup($ref->{'address'}, $ref->{'port'}); if($sock) { $ref->{'socket'} = $sock; $writers->add($sock); $ref->{'state'} = CONNECT_IN_PROGRESS; } } while(1) { my $buffer = ""; my $n_read = 0; my ($rr, $rw, $er) = IO::Select->select($readers, $writers, $readers, $select_timeout) ; # Check our readers foreach my $fh (@$rr) { foreach my $ref (keys %remotes) { $ref = $remotes{$ref}; if(defined $ref->{'pty'} and $fh == $ref->{'pty'}) { if(($n_read = sysread($fh, $buffer, 1024)) > 0) { syswrite($ref->{'socket'}, $buffer, $n_read); } else { $readers->remove($ref->{'pty'}); $readers->remove($ref->{'socket'}); close $ref->{'pty'}; close $ref->{'slave'}; close $ref->{'socket'}; delete $ref->{'pty'}; delete $ref->{'slave'}; delete $ref->{'socket'}; $ref->{'state'} = DISCONNECTED; } } if(defined $ref->{'socket'} and $fh == $ref->{'socket'}) { if(($n_read = sysread($fh, $buffer, 1024)) > 0) { syswrite($ref->{'pty'}, $buffer, $n_read); if(defined $ref->{'log_file'}) { open F, ">> $ref->{'log_file'}"; syswrite(F, $buffer, $n_read); close F; } } else { print "Lost Connection: $ref->{'address'}:$ref->{'port'}\n"; $readers->remove($ref->{'pty'}); $readers->remove($ref->{'socket'}); close $ref->{'pty'}; close $ref->{'slave'}; close $ref->{'socket'}; delete $ref->{'pty'}; delete $ref->{'slave'}; delete $ref->{'socket'}; $ref->{'state'} = DISCONNECTED; } } } } # check our writers foreach my $fh (@$rw) { foreach my $ref (keys %remotes) { $ref = $remotes{$ref}; if(defined $ref->{'socket'} and ($fh == $ref->{'socket'})) { my $peername = $ref->{'socket'}->peername(); if($peername) { print "Connection to $ref->{'address'}:$ref->{'port'} "; $ref->{'socket'}->blocking(1); my $pty = new IO::Pty; my $slave = $pty->slave(); $pty->autoflush(); $slave->set_raw(); $pty->set_raw(); print "success -> /dev/$ref->{'device_name'} (".$pty->ttyname(),")\n"; unlink "/dev/$ref->{'device_name'}"; symlink $pty->ttyname(), "/dev/$ref->{'device_name'}"; $ref->{'pty'} = $pty; $ref->{'slave'} = $slave; # Add it to the selector; $readers->add($pty); $readers->add($ref->{'socket'}); # No need to block on write capability anymore $writers->remove($ref->{'socket'}); $ref->{'state'} = CONNECTED; } else { $writers->remove($ref->{'socket'}); close $ref->{'socket'}; delete $ref->{'socket'}; $ref->{'state'} = DISCONNECTED; } } } } # Now that we have reading out of the way # we need to go through the list of all sockets # that are not connected. Allow setup() to do # the connect() on a non-blocking socket and # add it to the writers list. foreach my $ref (keys %remotes) { $ref = $remotes{$ref}; next if defined $ref->{'socket'}; # If out OS gives us a EHOSTUNREACH each time we # try to connect to a host that is unreachable then it # is possible that we could consume many cycles. We setup # the connect() here and then the minute we get back to select() # we can read and we have a EHOSTUNREACH. We then come back # here an repeat the process. This could keep this program # going and provide no sleep time. This value could be # customized but what it does is look at the last time # we did a connect() for the remote and if it is >= 30s then # we try again. Equivalent of init throttling runway getty processes. next if (time() - $ref->{'last_connect_attempt'} < 30); $ref->{'last_connect_attempt'} = time(); my $sock = setup($ref->{'address'}, $ref->{'port'}); # We have a connect() in progress. Add it to the writers if($sock) { $ref->{'socket'} = $sock; $writers->add($sock); $ref->{'state'} = CONNECT_IN_PROGRESS; } else { # We might want to log why we were unable to get a socket # to a log file to fix in the future. } } } return 0; } exit main; # vi: set ts=2 sw=2 #