virt.virt_test_utils: run_autotest - 'tar' needs relative paths to strip the leading '/'
[autotest-zwu.git] / conmux / conmux
blob12bc871b4184dade0b63f03139cfba4587df8a98
1 #!/usr/bin/perl
3 # conmux -- the main console multiplexor daemon
4 #
5 # Main console multiplexor daemon. There is one of these daemons for
6 # each open console supported in the system. Clients are directed to
7 # this daemon via the conmux-registry deamon.
9 # (C) Copyright IBM Corp. 2004, 2005, 2006
10 # Author: Andy Whitcroft <andyw@uk.ibm.com>
12 # The Console Multiplexor is released under the GNU Public License V2
14 use strict;
16 use FindBin;
17 use Symbol qw(gensym);
18 use IO::Socket;
19 use IO::Multiplex;
20 use IPC::Open3;
21 use URI::Escape;
22 use Net::Domain;
24 # Find our internal libraries.
25 use lib $FindBin::Bin;
26 use lib "$FindBin::Bin/../lib/";
27 use lib "$FindBin::Bin/lib/";
28 use Conmux;
30 our $P = 'conmux';
31 our $debug = 0;
33 $SIG{'CHLD'} = "IGNORE";
35 $| = 1;
38 # CALLBACK: this class is used to provide a timed callback. The multiplexor
39 # libarary allows us to set a timeout on any open file we have registered.
40 # So, we open a new file descriptor to /dev/null and set a timeout on that.
42 package Callback;
44 sub new {
45 my ($class, $mux, $who, $time) = @_;
46 my $self = bless { 'who' => $who }, $class;
48 my ($fh);
50 print "Callback::new [$self] mux<$mux> who<$who> time<$time>\n"
51 if ($main::debug);
53 # Open a file handle to nothing, we need this to hang the timeout
54 # on in the multiplexor. It will fail with a mux_eof, which we ignore.
55 open($fh, "</dev/null") || die "$P: /dev/null: open failed - $!\n";
57 $mux->add($fh);
58 $mux->set_callback_object($self, $fh);
60 $mux->set_timeout($fh, $time);
62 $self;
64 sub mux_timeout {
65 my ($self, $mux, $fh) = @_;
67 print "Callback::mux_timeout [$self] mux<$mux> fh<$fh>\n"
68 if ($main::debug);
70 $self->{'who'}->callback_timeout();
72 $mux->close($fh);
74 sub DESTROY {
75 my ($self) = @_;
76 print "Callback::DESTROY [$self]\n" if ($main::debug);
80 # LISTENER SOCKET: creates an intenet listener for new clients and
81 # connects them to the junction provided.
83 package ListenerSocket;
85 sub new {
86 my ($class, $mux, $port) = @_;
87 my $self = bless { 'mux' => $mux }, $class;
89 print "ListenerSocket::new [$self] mux<$mux> port<$port>\n"
90 if ($main::debug);
92 $self->initialise($port);
94 $self;
97 sub initialise {
98 my ($self, $port) = @_;
99 my ($sock);
101 print "ListenerSocket::initialise [$self] port<$port> "
102 if ($main::debug);
104 # Create a listening socket and add it to the multiplexor.
105 my $sock = new IO::Socket::INET(Proto => 'tcp',
106 LocalPort => $port,
107 Listen => 4,
108 ReuseAddr => 1)
109 or die "socket: $@";
111 print " adding $self $sock\n" if ($main::debug);
112 $self->mux->listen($sock);
113 $self->mux->set_callback_object($self, $sock);
114 $self->listener($sock);
117 # DATA accessors.
118 sub mux {
119 my $self = shift;
120 if (@_) { $self->{'mux'} = shift }
121 return $self->{'mux'};
123 sub listener {
124 my $self = shift;
125 if (@_) { $self->{'listener'} = shift }
126 return $self->{'listener'};
129 sub address {
130 my ($self) = @_;
131 Net::Domain::hostfqdn() . ':' . $self->{'listener'}->sockport();
134 # JUNCTION: callbacks.
135 ##sub junctionInput {
137 ##sub junctionEOF {
138 ## my ($self) = @_;
140 ## $self->{'junction'}->junctionRemove($self, 'console-client');
141 ## $self->{'mux'}->close($self->{'listener'});
144 # Handle new connections by instantiating a new client class.
145 sub mux_connection {
146 my ($self, $mux, $fh) = @_;
147 my ($client);
149 print "ListenerSocket::mux_connection [$self] mux<$mux> fh<$fh>\n"
150 if ($main::debug);
152 # Make a new client connection.
153 $client = ClientCmd->new($mux, $fh);
154 print " new connection $self $client\n" if ($main::debug);
157 sub DESTROY {
158 my ($self) = @_;
160 print "ListenerSocket::DESTROY [$self]\n" if ($main::debug);
162 close($self->listener);
166 # JUNCTION: generic junction box object, connects names groups of objects
167 # to other named groups.
169 # Expects the following callbacks to be defined on each object registered:
170 # junctionInput($from, $data)
171 # junctionEOF($from, $to)
173 package Junction;
175 sub new {
176 my ($class) = @_;
177 my $self = bless { }, $class;
179 print "Junction::new [$self]\n" if ($main::debug);
181 $self;
184 sub junctionAdd {
185 my ($self, $client) = @_;
187 print "Junction::junctionAdd [$self] client<$client>\n"
188 if ($main::debug);
190 # Add ourselves to the list of recipients.
191 $self->{$client} = $client;
194 sub junctionInput {
195 my ($self, $client, $data) = @_;
196 my ($c);
198 print "Junction::junctionInput [$self] client<$client> " .
199 "data<$data>\n" if ($main::debug);
201 # Send this data on to the clients listed in the output list.
202 for $c (values %{$self}) {
203 print " sending to $c\n" if ($main::debug);
204 $c->junctionInput($client, $data);
207 sub junctionEOF {
208 my ($self, $client) = @_;
209 my ($c);
211 print "Junction::junctionEOF [$self] client<$client>\n"
212 if ($main::debug);
214 # Send this eof on to the clients listed in the output list.
215 for $c (values %{$self}) {
216 print " sending to $c\n" if ($main::debug);
217 $c->junctionEOF($client);
220 sub junctionRemove {
221 my ($self, $client) = @_;
223 print "Junction::junctionRemove [$self] client<$client>\n"
224 if ($main::debug);
226 # Drop this client from our lists.
227 delete $self->{$client};
231 # PAYLOAD: generic payload object, connects itself to the requisite junction.
233 package Payload;
235 my %payloads = ();
236 my $payloads = 0;
238 sub lookup {
239 my ($class, $name) = @_;
241 $payloads{$name};
244 sub found {
245 my ($class, $name, $self) = @_;
247 print "Payloads::found name<$name> self<$self>\n" if ($main::debug);
249 $payloads{$name} = $self;
250 $payloads++;
252 sub lost {
253 my ($class, $name, $self) = @_;
255 print "Payloads::lost name<$name> self<$self>\n" if ($main::debug);
257 undef $payloads{$name};
258 if (--$payloads == 0) {
259 exit(0);
263 sub new {
264 my ($class, $name, $title, $mux, @a) = @_;
265 my $self = bless { }, $class;
267 print "Payload::new [$self] name<$name> title<$title> mux<$mux>\n"
268 if ($main::debug);
270 Payload->found($name, $self);
272 $self->name($name);
273 $self->title($title);
274 $self->mux($mux);
275 $self->enabled(1);
277 $self->cin(Junction->new);
278 $self->cout(Junction->new);
280 $self->initialise(@a);
282 $self;
285 # Data accessors.
286 sub name {
287 my $self = shift;
288 if (@_) { $self->{'name'} = shift }
289 return $self->{'name'};
291 sub title {
292 my $self = shift;
293 if (@_) { $self->{'title'} = shift }
294 return $self->{'title'};
296 sub mux {
297 my $self = shift;
298 if (@_) { $self->{'mux'} = shift }
299 return $self->{'mux'};
301 sub cin {
302 my $self = shift;
303 if (@_) { $self->{'cin'} = shift }
304 return $self->{'cin'};
306 sub cout {
307 my $self = shift;
308 if (@_) { $self->{'cout'} = shift }
309 return $self->{'cout'};
311 sub enabled {
312 my $self = shift;
313 if (@_) { $self->{'enabled'} = shift }
314 return $self->{'enabled'};
316 sub connected {
317 my $self = shift;
318 if (@_) { $self->{'connected'} = shift }
319 $self->transition();
320 return $self->{'connected'};
322 sub transition {
323 my $self = shift;
324 my $time = time;
325 if (($time - $self->{'trans_minor'}) > 30) {
326 $self->{'trans_major'} = $time;
328 $self->{'trans_minor'} = $time;
330 sub retry_timeout {
331 my $self = shift;
332 my $time = time - $self->{'trans_major'};
334 if ($time < 60) {
335 return 1;
336 } elsif ($time < 120) {
337 return 10;
338 } else {
339 return 30;
342 sub state {
343 my $self = shift;
344 my $ctime = $self->{'connected'};
345 my $ttime = $self->{'trans_major'};
346 my $time = time;
348 if ($ctime && ($time - $ctime) > 30) {
349 "connected";
350 } elsif ($ttime && ($time - $ttime) < 60) {
351 "transition";
352 } else {
353 "disconnected";
357 sub initialise {
358 my ($self) = @_;
359 my ($sock);
361 print "Payload::initialise [$self]\n" if ($main::debug);
363 # Ensure we recieve client input.
364 $self->cin->junctionAdd($self);
366 $self->connected(time);
369 # Telnet constants.
370 my $TN_IAC = sprintf("%c", 255);
371 my $TN_DONT = sprintf("%c", 254);
372 my $TN_DO = sprintf("%c", 253);
373 my $TN_WONT = sprintf("%c", 252);
374 my $TN_WILL = sprintf("%c", 251);
375 my $TN_SB = sprintf("%c", 250);
376 my $TN_SE = sprintf("%c", 240);
377 my $TN_BREAK = sprintf("%c", 243);
379 my $TNOPT_ECHO = sprintf("%c", 1);
380 my $TNOPT_SGA = sprintf("%c", 3);
383 # If we get here then we have accumulated a complete telnet
384 # negotiation string.
386 # Telnet negotiation protocol - RFC#854:
388 # DO We are being asked to DO an option
389 # DONT We are being asked to NOT DO an option
390 # WILL We are being told they will DO an option
391 # WONT We are being told they will NOT DO an option
393 # DO/DONT requests indicate we should {en,dis}able a mode.
394 # We are expected to respond with WILL or WONT. To prevent
395 # loops, we should not respond if the request matches our
396 # current mode.
398 # WILL/WONT requests indicate the other end would like to
399 # {en,dis}able a mode. We are expected to respond with
400 # DO/DONT.
402 # If we want a particular mode {en,dis}abled then we may start
403 # negotiation of that mode with a WILL/WONT.
405 # We want the other end to perform echo by default so we will
406 # DO any request for ECHO and DONT all other requests.
409 sub mux_input {
410 my ($self, $mux, $fh, $input) = @_;
411 my ($client);
413 print "Payload::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n"
414 if ($main::debug);
416 while ($$input ne "") {
417 # Ordinary text.
418 if ($$input =~ s/^([^$TN_IAC]+)//) {
419 # Data coming in from the payload, this needs to go to
420 # all of the clients.
421 $self->cout->junctionInput($self, $1);
422 next;
425 # IAC,SB,...,SE
426 if ($$input =~ s/^$TN_IAC$TN_SB([^$TN_SE]+)$TN_SE//) {
427 print "SB\n" if ($main::debug);
428 next;
430 # IAC,[DO|DONT|WILL|WONT],<what>
431 if ($$input =~ s/^$TN_IAC$TN_DO(.)//) {
432 my $c = unpack("C", $1);
433 print "DO<$c:$1>\n" if ($main::debug);
434 # We are DONT on all options so WONT all requests.
435 $self->junctionInput($self, "$TN_IAC$TN_WONT$1");
436 next;
438 if ($$input =~ s/^$TN_IAC$TN_DONT(.)//) {
439 my $c = unpack("C", $1);
440 print "DONT<$c:$1>\n" if ($main::debug);
441 # We are already DONT on all options, no reply.
442 next;
444 if ($$input =~ s/^$TN_IAC$TN_WILL(.)//) {
445 my $c = unpack("C", $1);
446 print "WILL<$c:$1>\n" if ($main::debug);
448 my $reply = $TN_DONT;
449 if ($1 == $TNOPT_ECHO || $1 == $TNOPT_SGA) {
450 $reply = $TN_DO;
452 $self->junctionInput($self, "$TN_IAC$reply$1");
453 next;
455 if ($$input =~ s/^$TN_IAC$TN_WONT(.)//) {
456 my $c = unpack("C", $1);
457 print "WONT<$c:$1>\n" if ($main::debug);
458 $self->junctionInput($self, "$TN_IAC$TN_DONT$1");
459 next;
461 # IAC,<option>
462 if ($$input =~ s/^$TN_IAC([^$TN_SB$TN_DO$TN_DONT$TN_WILL$TN_WONT])//) {
463 print "OPTION<$1>\n" if ($main::debug);
464 next;
467 # Incomplete ...
468 if ($$input =~ /^$TN_IAC/) {
469 return;
473 sub junctionInput {
474 my ($self, $from, $data) = @_;
475 my ($fh);
477 print "Payload::junctionInput [$self] from<$from> data<$data>\n"
478 if ($main::debug);
480 ##$self->{'mux'}->write($self->{'wfh'}, $data);
481 # If we are connected ...
482 if ($self->{'wfh'}) {
483 $fh = $self->{'wfh'};
484 print $fh $data;
485 } else {
486 $from->junctionInput($self, "<<<NOT CONNECTED>>>\n");
490 sub mux_eof {
491 my ($self, $mux, $fh) = @_;
492 my ($client);
494 print "Payload::mux_eof [$self] mux<$mux> fh<$fh>\n" if ($main::debug);
496 # Check for a restartable connection.
497 if ($self->can("restart")) {
498 my ($timeout) = $self->retry_timeout();
500 $self->cout->junctionInput($self,
501 "<<<PAYLOAD LOST ... retrying in $timeout secs>>>\n");
503 # Schedule a timeout to trigger a reconnect.
504 Callback->new($mux, $self, $timeout);
506 } else {
507 $self->cout->junctionEOF($self);
508 $self->cin->junctionRemove($self);
510 Payload->lost($self->name, $self);
513 # Close down the payload ...
514 $mux->close($self->{'rfh'});
515 ##$mux->remove($self->{'wfh'});
518 sub mux_close {
519 my ($self, $mux, $fh) = @_;
521 $self->connected(0);
523 #close($self->{'rfh'});
524 close($self->{'wfh'});
525 undef $self->{'rfh'};
526 undef $self->{'wfh'};
528 if ($self->{'pid'}) {
529 # Kill the process group for this pid.
530 kill 1, 0 - $self->{'pid'};
531 undef $self->{'pid'};
535 sub callback_timeout {
536 my ($self) = @_;
538 print "Payload::callback_timeout [$self]\n" if ($main::debug);
540 if ($self->enabled) {
541 $self->cout->junctionInput($self, "<<<PAYLOAD RESTART>>>\n");
542 $self->openPayload();
543 } else {
544 $self->cout->junctionInput($self, "<<<PAYLOAD DISABLED>>>\n");
548 sub closePayload {
549 my ($self) = @_;
551 if ($self->connected) {
552 $self->cout->junctionInput($self, "<<<PAYLOAD CLOSED>>>\n");
554 # Close down the payload ...
555 $self->mux->close($self->{'rfh'});
557 if ($self->enabled) {
558 $self->enabled(0);
559 return 1;
560 } else {
561 return 0;
564 sub openPayload {
565 my ($self) = @_;
567 $self->enabled(1);
568 if (!$self->connected) {
569 if ($self->can("restart")) {
570 $self->restart();
572 return 1;
575 return 0;
578 sub helpAdd {
579 my ($self, $cmd, $msg) = @_;
581 push(@{$self->{'help'}}, [ $cmd, $msg ]);
584 sub commandHelp {
585 my ($self) = @_;
586 my @entries = (
587 [ 'break', 'send a break sequence' ]
590 if (defined $self->{'help'}) {
591 ( @entries, @{$self->{'help'}} );
593 } else {
594 @entries;
597 sub commandAdd {
598 my ($self, $cmd, @a) = @_;
600 $self->{'cmd'}->{$cmd} = [ @a ];
602 sub commandExec {
603 my ($self, $client, $cmd, $a) = @_;
604 my ($exe);
606 print "Payload::commandExec [$self] client<$client> cmd<$cmd> a<$a>\n"
607 if ($main::debug);
609 $exe = $self->{'cmd'}->{$cmd};
611 if ($cmd eq "break") {
612 # Send a telnet break ...
613 $self->junctionInput($self, "$TN_IAC$TN_BREAK");
614 return;
616 } elsif ($cmd eq "close") {
617 if (!$self->enabled && !$self->connected) {
618 $client->junctionInput($self,
619 "console already closed\n");
621 } elsif ($self->closePayload()) {
622 $self->cout->junctionInput($self, "(" . $client->id .
623 ") triggered a console close\n");
625 } else {
626 $client->junctionInput($self, "ERROR: close failed\n");
628 return;
630 } elsif ($cmd eq "open") {
631 if ($self->connected) {
632 $client->junctionInput($self, "console already open\n");
634 } elsif ($self->openPayload()) {
635 $self->cout->junctionInput($self, "(" . $client->id .
636 ") triggered a console open\n");
638 } else {
639 $client->junctionInput($self, "open failed\n");
641 return;
644 # Ensure we error if we have no command.
645 if (!$exe) {
646 $client->junctionInput($self, "Command not recognised\n");
647 return;
650 my ($msg, $run) = @{$exe};
651 if ($msg ne '') {
652 $self->cout->junctionInput($self, "(" . $client->id .
653 ") $msg\n");
656 local(*IN, *OUT, *ERR);
657 my ($cmd, @args) = split(m/'(.*?)'|"(.*?)"|\s(.*?)\s/g, $run . " $a");
658 my @opts;
659 for (my $i=0; $i < @args; $i++) {
660 next if not $args[$i];
661 push(@opts, $args[$i]);
663 my $pid = IPC::Open3::open3(*IN, *OUT, *ERR, $cmd, @opts);
664 close(*IN{IO});
666 # XXX: this should not be happening here.
667 $self->mux->add(*OUT{IO});
668 my $data = ClientData->new($self->mux, *OUT{IO});
669 $data->{'id'} = "cmd:$cmd stdout";
671 $data->payload($self);
672 $data->cout($self->cout);
674 # XXX: this should not be happening here.
675 $self->mux->add(*ERR{IO});
676 my $data = ClientData->new($self->mux, *ERR{IO});
677 $data->{'id'} = "cmd:$cmd stderr";
679 $data->payload($self);
680 $data->cout($client);
683 sub DESTROY {
684 my ($self) = @_;
686 print "Payload::DESTROY [$self]\n" if ($main::debug);
690 # PAYLOAD APPLICATION: handles forking off a command as a payload.
692 package PayloadApplication;
693 use base 'Payload';
695 sub initialise {
696 my ($self, $cmd) = @_;
697 my ($pid, $wfh, $rfh);
699 print "PayloadApplication::initialise [$self] cmd<$cmd>"
700 if ($main::debug);
702 $self->SUPER::initialise();
704 # XXX: we cannot use the write buffering offered by the mux package
705 # without suffering a read error from the PWR file handle, there
706 # is no a way to add a write-only channel.
708 $self->{'args'} = $cmd;
710 # Start the payload ...
711 $pid = IPC::Open3::open3($wfh, $rfh, 0, "setsid " . $cmd);
713 $self->{'rfh'} = $rfh;
714 $self->{'wfh'} = $wfh;
715 $self->{'pid'} = $pid;
717 $self->mux->add($rfh);
718 ##$mux->add($wfh);
720 $self->mux->set_callback_object($self, $rfh);
721 ##$mux->set_callback_object($self, $wfh);
723 print "SHARE PAYLOAD: $self $wfh/$rfh (to $cmd) [fd=" .
724 fileno($wfh) . "/" . fileno($rfh) . "]\n" if ($main::debug);
725 print "payload '$cmd' on fd=" . fileno($wfh) . "/" .
726 fileno($rfh) . "\n";
728 $self;
731 sub restart {
732 my ($self) = @_;
734 $self->initialise($self->{'args'});
738 # PAYLOAD SOCKET: handles a network socket as payload.
740 package PayloadSocket;
741 use base 'Payload';
743 sub initialise {
744 my ($self, $addr) = @_;
745 my ($payload);
747 print "PayloadSocket::initialise [$self] addr<$addr>\n"
748 if ($main::debug);
750 $self->SUPER::initialise();
752 $self->{'args'} = $addr;
754 # Create a listening socket and add it to the multiplexor.
755 my $payload = new IO::Socket::INET(PeerAddr => $addr);
756 if (!$payload) {
757 $self->connected(0);
758 if ($self->can("restart")) {
759 my ($timeout) = $self->retry_timeout();
761 $self->cout->junctionInput($self,
762 "<<<PAYLOAD ERROR ($!) ... retrying in $timeout secs>>>\n");
763 # Schedule a timeout to trigger a reconnect.
764 Callback->new($self->mux, $self, $timeout);
765 } else {
766 $self->cout->junctionEOF($self);
767 $self->cin->junctionRemove($self);
769 Payload->lost($self->name, $self);
772 } else {
773 $self->{'rfh'} = $payload;
774 $self->{'wfh'} = $payload;
776 print "SHARE PAYLOAD: $self $payload (to $addr) [fd=" .
777 fileno($payload) . "]\n" if ($main::debug);
778 print "payload '$addr' on fd=" . fileno($payload) . "\n";
779 $self->mux->add($payload);
781 $self->mux->set_callback_object($self, $payload);
784 print "SHARE PAYLOAD: $self $payload ... done\n" if ($main::debug);
786 $self;
789 sub restart {
790 my ($self) = @_;
792 $self->initialise($self->{'args'});
796 # CLIENT: general client object, represents a remote client channel
798 package Client;
800 sub new {
801 my ($class, $mux, $fh) = @_;
802 my $self = bless { 'mux' => $mux,
803 'fh' => $fh }, $class;
805 print "Client::new [$self] mux<$mux> fh<$fh>\n"
806 if ($main::debug);
808 $self->initialise();
810 $self;
813 sub clone {
814 my ($class, $from) = @_;
816 my $self = bless { %{$from} }, $class;
818 print "Client::clone [$self] from<$from>\n" if ($main::debug);
820 $self->initialise();
822 $self;
825 # Data accessors.
826 sub mux {
827 my $self = shift;
828 if (@_) { $self->{'mux'} = shift }
829 return $self->{'mux'};
831 sub payload {
832 my $self = shift;
833 if (@_) { $self->{'payload'} = shift }
834 return $self->{'payload'};
836 sub fh {
837 my $self = shift;
838 if (@_) { $self->{'fh'} = shift }
839 return $self->{'fh'};
841 sub id {
842 my $self = shift;
843 if (@_) { $self->{'id'} = shift }
844 return $self->{'id'};
846 sub announce {
847 my $self = shift;
848 if (@_) { $self->{'announce'} = shift }
849 return $self->{'announce'};
851 sub cout {
852 my $self = shift;
853 if (@_) { $self->{'cout'} = shift }
854 return $self->{'cout'};
856 sub cin {
857 my $self = shift;
858 if (@_) {
859 $self->{'cin'}->junctionRemove($self) if ($self->{'cin'});
860 $self->{'cin'} = shift;
861 $self->{'cin'}->junctionAdd($self) if ($self->{'cin'} != undef);
863 return $self->{'cin'};
866 sub initialise {
867 my ($self) = @_;
869 print "Client::initialise [$self]\n" if ($main::debug);
871 $self->mux->set_callback_object($self, $self->fh);
874 sub junctionInput {
875 my ($self, $from, $data) = @_;
877 print "Client::junctionInput [$self] data<$data>\n" if ($main::debug);
879 $self->mux->write($self->fh, $data);
881 sub junctionEOF {
882 my ($self, $from, $data) = @_;
884 print "Client::junctionEOF [$self] data<$data>\n" if ($main::debug);
886 $self->shutdown();
889 sub mux_eof {
890 my ($self, $mux, $fh, $input) = @_;
892 print "Client::mux_eof [$self] mux<$mux> fh<$fh> input<$input>\n"
893 if ($main::debug);
895 # Handle any pending input, then remove myself from the clients list.
896 $self->mux_input($mux, $fh, $input);
897 $self->cin(undef);
898 $self->cout(undef);
900 # Tell the multiplexor we no longer are using this channel.
901 $mux->shutdown($fh, 1);
903 sub mux_close {
904 my ($self, $mux, $fn) = @_;
906 print "Client::close [$self]\n" if ($main::debug);
908 if ($self->announce) {
909 $self->announce->junctionInput($self, "(" . $self->id .
910 ") disconnected\n");
912 print "$self->{'id'} disconnected\n";
915 sub shutdown {
916 my ($self) = @_;
918 print "Client::shutdown [$self]\n" if ($main::debug);
920 # Close myself down and tell the payload.
921 $self->mux->shutdown($self->fh, 2);
923 sub DESTROY {
924 my ($self) = @_;
926 print "Client::DESTROY [$self]\n" if ($main::debug);
930 # CLIENT CMD: represents a client whilst in command mode, when we have commited
931 # to connecting this will pass the client connection off to a ClientData
932 # object.
934 package ClientCmd;
935 use base 'Client';
937 sub mux_input {
938 my ($self, $mux, $fh, $input) = @_;
940 print "Client::shutdown [$self] mux<$mux> fh<$fh> input<$$input>\n"
941 if ($main::debug);
943 while ($$input =~ s/^(.*?)\n//) {
944 my ($cmd, $args) = split(' ', $1, 2);
945 my (%args) = Conmux::decodeArgs($args);
947 my $reply = {
948 'status' => 'ENOSYS unknown command',
951 # XXX: check authentication if required and reject the
952 # command out of hand - leak _nothing_.
953 if (!defined $args{'id'}) {
954 $reply->{'status'} = 'EACCES identifier required';
955 goto reply;
957 # They are who they say they are, record who that is.
958 $self->{'id'} = $args{'id'};
960 if ($cmd eq "CONNECT") {
961 # Switch over to data mode, hand this connection off
962 # to a data client instance, I am done.
963 my ($data, $to, $in, $out);
964 $data = ClientData->clone($self);
966 $to = $args{'to'};
967 if (!$to) {
968 $reply->{'status'} = "EINVAL CONNECT " .
969 " requires 'to' specifier";
970 goto reply;
972 my $payload = Payload->lookup($to);
973 if (!defined $payload) {
974 $reply->{'status'} = "EINVAL '$to' not a " .
975 " valid destination specifier";
976 goto reply;
979 $reply->{'status'} = 'OK';
981 # Get the payload title and pass that back.
982 $reply->{'title'} = $payload->title . ' [channel ' .
983 $payload->state() . ']';
984 $reply->{'state'} = $payload->state();
985 # Get connected clients and pass back as the motd
986 for my $cl (keys(%{$payload->cout})) {
987 $reply->{'motd'} .= '(' . $payload->cout->{$cl}->id;
988 $reply->{'motd'} .= ") is already connected\n";
991 $data->payload($payload);
992 $args{'type'} = 'client' if (!$args{'type'});
993 if ($args{'type'} eq 'status') {
994 $data->cout($payload->cout);
995 } elsif ($args{'type'} eq 'client') {
996 if (!$args{'hide'}) {
997 $data->announce($payload->cout);
998 $payload->cout->junctionInput(
999 $self, "(" . $self->id .
1000 ") connected\n");
1002 $data->cin($payload->cout);
1003 $data->cout($payload->cin);
1004 } else {
1005 $reply->{'status'} = "EINVAL '$args{'type'}' " .
1006 "not a valid destination type";
1007 goto reply;
1010 print "$self->{'id'} connected to $to/$args{'type'}\n";
1012 $self->junctionInput($self,
1013 Conmux::encodeArgs($reply) . "\n");
1015 # Don't handle any more input - its not going to be
1016 # for us.
1017 last;
1020 reply:
1021 # We're done, send back our response to this.
1022 $self->junctionInput($self, Conmux::encodeArgs($reply) . "\n");
1027 # CLIENT DATA: handles a client connection when in data mode, attaches
1028 # the client connection to the relevant junction.
1030 package ClientData;
1031 use base 'Client';
1033 my @help = (
1034 [ 'msg', 'send a message to all connected clients' ],
1035 [ 'quit', 'disconnect from the console' ],
1037 sub mux_input {
1038 my ($self, $mux, $fh, $input) = @_;
1040 print "ClientData::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n"
1041 if ($main::debug);
1043 while ($$input ne "") {
1044 if ($self->{'cmd'} eq '') {
1045 # Check for an incomplete escape ... wait for more.
1046 if ($$input =~ /^~$/s) {
1047 return;
1049 if ($$input =~ s/^~\$//s) {
1050 $self->{'cmd'} = '>';
1051 my $title = $self->payload->title;
1052 $self->junctionInput($self, "\r\nCommand($title)> ");
1053 next;
1055 # Its not an escape ... pass it on.
1056 # Ship anything before that cannot be the escape.
1057 if ($$input =~ s/^(.[^~]*)(~|$)/\2/s) {
1058 # Data coming in from the client, send it to
1059 # the payload.
1060 $self->cout->junctionInput($self, $1);
1062 } else {
1063 # Consume characters upto a newline, echo them back
1064 # to the client as we go.
1065 while ($$input =~ s/^([^\r\n])//) {
1066 my $c = $1;
1067 if ($c eq "\b" || $c eq "\x7f") {
1068 if (length($self->{'cmd'}) > 1) {
1069 $c = "\b \b";
1070 substr($self->{'cmd'},
1071 -1, 1, '');
1072 } else {
1073 $c = '';
1075 } else {
1076 $self->{'cmd'} .= $c;
1078 $self->junctionInput($self, $c);
1080 # If we arn't at a newline, then wait for more input.
1081 if ($$input !~ s/^[\r\n]+//) {
1082 return;
1085 $self->junctionInput($self, "\n");
1087 my ($cmd, $a) = split(' ', substr($self->{'cmd'},
1088 1), 2);
1089 $self->{'cmd'} = '';
1091 if ($cmd eq '') {
1093 } elsif ($cmd eq 'help') {
1094 my @cmds = $self->payload->commandHelp();
1096 my $ent;
1097 my $help = "Conmux commands:\n";
1098 for $ent (@cmds, @help) {
1099 $help .= sprintf(" %-20s %s\n",
1100 $ent->[0], $ent->[1]);
1102 $self->junctionInput($self, $help);
1104 } elsif ($cmd eq 'quit') {
1105 $self->shutdown();
1107 } elsif ($cmd eq 'msg') {
1108 $self->cin->junctionInput($self,
1109 "($self->{'id'}) $a\n");
1111 # Not a client command ... pass it to the payload.
1112 } else {
1113 $self->payload->commandExec($self, $cmd, $a);
1120 # LIBRARY: split a string honouring quoting.
1122 package main;
1123 sub parse($) {
1124 my ($str) = @_;
1126 my ($pos, @args, $argc, $quote, $real, $c, $inc);
1128 $inc = 0;
1129 @args = ();
1130 $argc = 0;
1131 $quote = 0;
1132 $real = 0;
1135 $pos = 0;
1136 while (substr($str, $pos, 1) eq " ") {
1137 $pos++;
1139 for (; $pos < length($str); $pos++) {
1140 $c = substr($str, $pos, 1);
1141 if ($quote != 2 && $c eq '\\') {
1142 $real = 1;
1143 $pos++;
1144 $c = substr($str, $pos, 1);
1145 } else {
1146 $real = 0;
1149 if ($quote != 2 && $c eq '"' && !$real) {
1150 $quote ^= 1;
1151 } elsif ($quote != 1 && $c eq "'" && !$real) {
1152 $quote ^= 2;
1153 } elsif ($c eq " " && $quote == 0 && !$real) {
1154 while (substr($str, $pos, 1) eq " ") {
1155 $pos++;
1157 $pos--;
1158 $argc++;
1159 } else {
1160 if ($inc) {
1161 $inc = 0;
1162 $argc++;
1164 $args[$argc] .= $c;
1168 @args;
1172 # MAIN: makes the IO multiplexor, junction, listener and payload and stitches
1173 # them all together.
1175 package main;
1177 # Usage checks.
1178 if ($#ARGV != 0 && $#ARGV != 3) {
1179 print STDERR "Usage: $P <config file>\n";
1180 print STDERR " $P <local port> <title> socket <host>:<port>\n";
1181 print STDERR " $P <local port> <title> cmd <cmd>\n";
1182 exit 1
1184 my @conf;
1185 if ($#ARGV == 3) {
1186 my ($lport, $title, $what, $arg) = @ARGV;
1187 @conf = (
1188 "listener '$lport'",
1189 "'$what' console '$title' '$arg'"
1191 } else {
1192 my ($cf) = @ARGV;
1193 open(CONF, '<', $cf) || die "$P: $cf: open failed - $!\n";
1194 @conf = <CONF>;
1195 close(CONF);
1198 # Make a new multiplexer.
1199 my $mux = new IO::Multiplex;
1201 my ($line, $seg, $listener, $payload);
1202 $line = '';
1203 for $seg (@conf) {
1204 # Handle comments, blank lines and line continuation.
1205 chomp($seg); $seg =~ s/^\s+//;
1206 next if ($seg =~ /^#/);
1207 $line .= $seg;
1208 if ($line =~ m/\\$/) {
1209 chop($line);
1210 next;
1212 next if (/^\s+$/);
1214 my ($cmd, @a) = parse($line);
1215 $line = '';
1217 if ($cmd eq "listener") {
1218 if ($#a != 0) {
1219 warn "$P: Usage: listener <port>\n" .
1220 "$P: $line\n";
1221 next;
1224 my ($lport) = @a;
1225 my ($rhost, $rname);
1227 # port
1228 if ($lport =~ m@^\d+$@) {
1229 # Already in the right format.
1231 # registry/service
1232 } elsif ($lport =~ m@(.*)/(.*)@) {
1233 ($rhost, $rname, $lport) = ($1, $2, 0);
1235 # service
1236 } else {
1237 ($rhost, $rname, $lport) = ('-', $lport, 0);
1240 # Create the client listener socket.
1241 $listener = ListenerSocket->new($mux, $lport);
1243 # Register us with the registry.
1244 if ($rhost) {
1245 Conmux::Registry::add($rhost, $rname, $listener->address);
1248 } elsif ($cmd eq 'socket') {
1249 if ($#a != 2) {
1250 warn "$P: Usage: socket <name> <title> <host:port>\n" .
1251 "$P: $line\n";
1252 next;
1254 my ($name, $title, $sock) = @a;
1256 # Create the payload.
1257 $payload = PayloadSocket->new($name, $title, $mux, $sock);
1259 } elsif ($cmd eq 'application') {
1260 if ($#a != 2) {
1261 warn "$P: Usage: application <name> <title> <host:port>\n" .
1262 "$P: $line\n";
1263 next;
1265 my ($name, $title, $app) = @a;
1267 $payload = PayloadApplication->new($name, $title, $mux, $app);
1269 } elsif ($cmd eq 'command') {
1270 if ($#a != 2) {
1271 warn "$P: Usage: command <name> <msg> <cmd>\n" .
1272 "$P: $line\n";
1273 next;
1275 my ($name, $msg, $cmd) = @a;
1277 $payload->commandAdd($name, $msg, $cmd);
1279 } elsif ($cmd eq 'help') {
1280 if ($#a != 1) {
1281 warn "$P: Usage: $cmd <name> <msg>\n" .
1282 "$P: $line\n";
1283 next;
1285 my ($name, $msg) = @a;
1287 $payload->helpAdd($name, $msg);
1288 } else {
1289 warn "$P: $cmd: unknown configuration command\n";
1293 # Hand over to the multiplexor.
1294 do {
1295 eval { $mux->loop; };
1296 warn "$@";
1297 } while ($@ =~ /^Use of freed value in iteration/);
1298 die "ERROR: $@\n";