From bb5b51dfbe3a0281c2ff1875c0300e00dff9df49 Mon Sep 17 00:00:00 2001 From: Paul Tinsley Date: Fri, 20 Oct 2006 06:20:39 +0000 Subject: [PATCH] Initial checkin of the POE::Filter::FSSocket module and fsconsole.pl which is a curses, multi-window console for freeswitch mod_event_socket. See http://search.cpan.org/~ptinsley/POE-Filter-FSSocket-0.04/lib/POE/Filter/FSSocket.pm for more info on the perl module. git-svn-id: http://svn.freeswitch.org/svn/freeswitch/trunk@3116 d0543943-73ff-0310-b7d9-9358b9ac24b2 --- scripts/POE-Filter-FSSocket/CHANGES | 50 +++ scripts/POE-Filter-FSSocket/INSTALL | 4 + scripts/POE-Filter-FSSocket/LICENSE | 18 + scripts/POE-Filter-FSSocket/Makefile.PL | 22 ++ scripts/POE-Filter-FSSocket/README | 3 + scripts/POE-Filter-FSSocket/TODO | 5 + .../POE-Filter-FSSocket/examples/fsconsole.pl | 361 ++++++++++++++++++ .../POE-Filter-FSSocket/examples/poetest.pl | 42 ++ .../lib/POE/Filter/FSSocket.pm | 343 +++++++++++++++++ scripts/POE-Filter-FSSocket/t/01_basic.t | 10 + scripts/socket/fsconsole.pl | 361 ++++++++++++++++++ 11 files changed, 1219 insertions(+) create mode 100644 scripts/POE-Filter-FSSocket/CHANGES create mode 100644 scripts/POE-Filter-FSSocket/INSTALL create mode 100644 scripts/POE-Filter-FSSocket/LICENSE create mode 100644 scripts/POE-Filter-FSSocket/Makefile.PL create mode 100644 scripts/POE-Filter-FSSocket/README create mode 100644 scripts/POE-Filter-FSSocket/TODO create mode 100644 scripts/POE-Filter-FSSocket/examples/fsconsole.pl create mode 100644 scripts/POE-Filter-FSSocket/examples/poetest.pl create mode 100755 scripts/POE-Filter-FSSocket/lib/POE/Filter/FSSocket.pm create mode 100644 scripts/POE-Filter-FSSocket/t/01_basic.t create mode 100644 scripts/socket/fsconsole.pl diff --git a/scripts/POE-Filter-FSSocket/CHANGES b/scripts/POE-Filter-FSSocket/CHANGES new file mode 100644 index 0000000000..64bedb1c13 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/CHANGES @@ -0,0 +1,50 @@ +========================= +2006-10-14 00:08:00 v0_05 +========================= + + 2006-10-14 00:08:00 (r5) by ptinsley + + Added some "bullet proofing" in the module to handle bad/unknown input + Updated the curses example (fsconsole.pl) + Added debug and strict parsing as part of the bullet proofing + to enable debug add debug => 1 in your new() and if you want the module + to croak when it doesn't properly parse something use strict => 1 in the + new(). Example Poe::Filter::FSSocket->new(debug => 1, strict => 1) + The default for debug and strict is 0 + +========================= +2006-10-14 00:08:00 v0_04 +========================= + + 2006-10-14 00:08:00 (r4) by ptinsley + + Fixed support for log/data + Added an example that is a quick curses console for freeswitch + +========================= +2006-09-18 21:08:00 v0_03 +========================= + + 2006-09-18 21:08:00 (r3) by ptinsley + + Added support for log/data + +========================= +2006-09-17 23:57:00 v0_02 +========================= + + 2006-09-17 23:57:00 (r2) by ptinsley + + Added support for api/response type, data ends up in api-response variable. + +========================= +2006-09-17 22:19:20 v0_01 +========================= + + 2006-09-17 22:19:20 (r1) by ptinsley + + Initial package of the module. + +============== +End of Excerpt +============== diff --git a/scripts/POE-Filter-FSSocket/INSTALL b/scripts/POE-Filter-FSSocket/INSTALL new file mode 100644 index 0000000000..a22bcab192 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/INSTALL @@ -0,0 +1,4 @@ +perl Makefile.PL +make +make test +make install diff --git a/scripts/POE-Filter-FSSocket/LICENSE b/scripts/POE-Filter-FSSocket/LICENSE new file mode 100644 index 0000000000..0a02964d52 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/LICENSE @@ -0,0 +1,18 @@ +The contents of this file are subject to the Mozilla Public License +Version 1.1 (the "License"); you may not use this file except in +compliance with the License. You may obtain a copy of the License at +http://www.mozilla.org/MPL/ + +Software distributed under the License is distributed on an "AS IS" +basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the +License for the specific language governing rights and limitations +under the License. + +The Original Code is POE::Filter::FSSocket. + +The Initial Developer of the Original Code is Paul Tinsley . +Portions created by the Initial Developer are Copyright (C) 2006 +the Initial Developer. All Rights Reserved. + +Contributor(s): + Paul Tinsley diff --git a/scripts/POE-Filter-FSSocket/Makefile.PL b/scripts/POE-Filter-FSSocket/Makefile.PL new file mode 100644 index 0000000000..9c27738d36 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/Makefile.PL @@ -0,0 +1,22 @@ +#!/usr/bin/perl + +use ExtUtils::MakeMaker; + +WriteMakefile( + NAME => 'POE::Filter::FSSocket', + AUTHOR => 'Paul Tinsley ', + ABSTRACT => 'POE filter for getting events out of FreeSWITCH', + VERSION_FROM => 'lib/POE/Filter/FSSocket.pm', + + PM => { 'lib/POE/Filter/FSSocket.pm' => '$(INST_LIBDIR)/FSSocket.pm' }, + PREREQ_PM => { + POE => 0.3101, + Test::More => 0, + POE::Component::Client::TCP => 0, + POE::Filter::Line => 0, + }, + dist => { + COMPRESS => 'gzip -9f', + SUFFIX => 'gz', + }, +); diff --git a/scripts/POE-Filter-FSSocket/README b/scripts/POE-Filter-FSSocket/README new file mode 100644 index 0000000000..ca22c8946f --- /dev/null +++ b/scripts/POE-Filter-FSSocket/README @@ -0,0 +1,3 @@ +A POE filter for FreeSWITCH (http://www.freeswitch.org) that parses event/log/etc... messages for you. You must ask for events in plain mode. + +perldoc POE::Filter::FSSocket for more info and an example. diff --git a/scripts/POE-Filter-FSSocket/TODO b/scripts/POE-Filter-FSSocket/TODO new file mode 100644 index 0000000000..81fac1f948 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/TODO @@ -0,0 +1,5 @@ +- Support bgapi output +- more example scripts +- more documentation +- sister component that uses this filter +- reconnect handling diff --git a/scripts/POE-Filter-FSSocket/examples/fsconsole.pl b/scripts/POE-Filter-FSSocket/examples/fsconsole.pl new file mode 100644 index 0000000000..db43e319fb --- /dev/null +++ b/scripts/POE-Filter-FSSocket/examples/fsconsole.pl @@ -0,0 +1,361 @@ +#!/usr/bin/perl +use strict; +use warnings; + +sub POE::Kernel::ASSERT_DEFAULT () { 1 }; +sub Term::Visual::DEBUG () { 1 } +sub Term::Visual::DEBUG_FILE () { 'test.log' } +use IO::Socket; +use POE qw/Filter::FSSocket Component::Client::TCP/; +use Data::Dumper; +use Term::Visual; + + +local *D; +if (Term::Visual::DEBUG) { + *D = *Term::Visual::ERRS; +} + +#local *ERROR = *STDERR; + + +$SIG{__DIE__} = sub { + if (Term::Visual::DEBUG) { + print Term::Visual::ERRS "Died: @_\n"; + } +}; + +############################################################################### +## BEGIN Globals ############################################################## +############################################################################### +our $server_address = "127.0.0.1"; +our $server_port = "8021"; +our $server_secret = "ClueCon"; + +#this is where you can customize the color scheme +our %Pallet = ( + 'warn_bullet' => 'bold yellow', + 'err_bullet' => 'bold red', + 'out_bullet' => 'bold green', + 'access' => 'bright red on blue', + 'current' => 'bright yellow on blue', +); + +our $terminal; +my %sockets; +my %windows; +my %unread_count; +my %commands = ( + 'window' => 1, + 'w' => 1, + 'win' => 1, +); +############################################################################### +## END Globals ############################################################## +############################################################################### + +#setup our session +POE::Session->create( + 'inline_states' => { + '_start' => \&handle_start, #session start + '_stop' => \&handle_stop, #session stop + 'curses_input' => \&handle_curses_input, #input from the keyboard + 'update_time' => \&handle_update_time, #update the status line clock + 'quit' => \&handle_quit, #handler to do any cleanup + 'server_input' => \&handle_server_input, + '_default' => \&handle_default, + }, + 'heap' => { + 'terminal' => undef, + 'freeswitch' => undef, + }, +); + +#start the kernel a chugging along +$poe_kernel->run; + +############################################################################### +## BEGIN Handlers ############################################################# +############################################################################### +#handles any startup functions for our session +sub handle_default { +} + +sub handle_start { + my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP]; + + #setup our terminal + $heap->{'terminal'} = Term::Visual->new( + 'Alias' => 'terminal', #poe alias for this + 'History_Size' => 300, #number of things to keep in history + 'Common_Input' => 1, #all windows share input and history + 'Tab_Complete' => \&tab_complete, + ); + + $terminal = $heap->{'terminal'}; + + #setup the color palette + $terminal->set_palette(%Pallet); + + #create a base window + my $window_id = $terminal->create_window( + 'Window_Name' => 'console', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Console', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'console'} = $window_id; + + $window_id = $terminal->create_window( + 'Window_Name' => 'log', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Logs', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'log'} = $window_id; + + $window_id = $terminal->create_window( + 'Window_Name' => 'event', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Event', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'event'} = $window_id; + + #tell the terminal what to call when there is input from the keyboard + $kernel->post('terminal' => 'send_me_input' => 'curses_input'); + + $terminal->change_window(0); + $kernel->delay_set('update_time' => 1); + $terminal->set_status_field(0, 'time' => scalar(localtime)); + new_message('destination_window' => 0, 'message' => " +Welcome to the FreeSWITCH POE Curses Console! + The console is split into three windows: + - 'console' for api response messages + - 'log' for freeswitch log output (simply send the log level you want + to start seeing events eg: 'log all') + - 'event' for freeswitch event output (must subscribe in plain format + eg: 'event plain all') + +To switch between windows type 'w set_status_field($terminal->current_window, 'window_status' => format_window_status()); + + #connect to freeswitch + $heap->{'freeswitch'} = POE::Component::Client::TCP->new( + 'RemoteAddress' => $server_address, + 'RemotePort' => $server_port, + 'ServerInput' => \&handle_server_input, + 'Connected' => \&handle_fs_connected, + 'ServerError' => \&handle_server_error, + 'Disconnected' => \&handle_server_disconnect, + 'Domain' => AF_INET, + 'Filter' => POE::Filter::FSSocket->new(), + ); + +} + +#called when users enter commands in a window +sub handle_curses_input { + my ($kernel, $heap, $input, $context) = @_[KERNEL, HEAP, ARG0, ARG1]; + + #get the id of the window that is responsible for the input + my $window = $heap->{'terminal'}->current_window; + + open(ERROR, ">>error.log"); + + if($input eq "quit") { + $kernel->yield('quit'); + } elsif ($input =~ /^w\ (.*)$/) { + #get the id of the requested window + eval { + my $window_id = $windows{$1}; + + #see if it's real + if(defined($window_id)) { + $unread_count{$window_id} = 0; + $terminal->change_window($window_id); + $terminal->set_status_field($window_id, 'window_status' => &format_window_status()); + } + }; + if($@) { + print ERROR "put error: $@\n"; + } + } else { + #see if we got connected at some point + if(defined($sockets{'localhost'})) { + #send the command + $sockets{'localhost'}->put($input); + } + } +} + +sub handle_fs_connected { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + + eval { + $sockets{'localhost'} = $heap->{'server'}; + } +} + +#this is responsible for doing any cleanup and returning the terminal to the previous +#state before we mucked with it +sub handle_quit { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + + #tell curses to clean up it's crap + $kernel->post('terminal' => 'shutdown'); + + #there is probably a more elegant way, but this works for now + exit; +} + +#data from freeswitch +sub handle_server_input { + my ($kernel,$heap,$input) = @_[KERNEL,HEAP,ARG0]; + + eval { + #terminal HATES null + if(defined($input->{'__DATA__'})) { + $input->{'__DATA__'} =~ s/[\x00]//g; + } + + #handle the login + if($input->{'Content-Type'} eq "auth/request") { + $heap->{'server'}->put("auth $server_secret"); + } elsif ($input->{'Content-Type'} eq "api/response") { + new_message('destination_window' => 0, 'message' => 'Response: '); + new_message('destination_window' => 0, 'message' => $input->{'__DATA__'}); + } elsif ($input->{'Content-Type'} eq "log/data") { + new_message('destination_window' => 1, 'message' => $input->{'__DATA__'}); + } elsif ($input->{'Content-Type'} eq "text/event-plain") { + new_message('destination_window' => 2, 'message' => Dumper $input); + } + }; + + if($@) { + open(ERROR, ">>error.log"); + print ERROR "died: $@\n"; + print ERROR Dumper $heap; + close(ERROR); + } +} + +sub handle_server_error { +} + +sub handle_server_disconnect { +} + +sub tab_complete { + my $left = shift; + + my @return; + + if(defined($commands{$left})) { + return [$left . " "]; + #} elsif () { + } + +} + +sub handle_update_time { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + $terminal->set_status_field($terminal->current_window, 'time' => scalar(localtime)); + $kernel->delay_set('update_time' => 1); +} +############################################################################### +## END Handlers ############################################################# +############################################################################### + +sub new_message { + my %args = @_; + + my $message = $args{'message'}; + my $destination_window = $args{'destination_window'}; + + my $status_field; + + #see if we are on the window being updated + if($terminal->current_window != $destination_window) { + #increment the unread count for the window + #FIXME, should we count messages or lines? + $unread_count{$destination_window}++; + + + #update the status bar + eval { + $terminal->set_status_field($terminal->current_window, 'window_status' => &format_window_status()); + }; + + if($@) { + print $@; + } + } + + + #deliver the message + $terminal->print($destination_window, $message); +} + +sub format_window_status { + my $status_field; + + #put all the windows in the bar with their current unread count + foreach my $window (sort {$windows{$a} <=> $windows{$b}} keys %windows) { + #see if we are printing the current window + if($terminal->current_window == $windows{$window}) { + $status_field .= "[\0(current)$window\0(st_frames)"; + } else { + $status_field .= "[$window"; + } + + if($unread_count{$windows{$window}}) { + $status_field .= " (" . $unread_count{$windows{$window}} . ")"; + } + + $status_field .= "] "; + } + + return $status_field; +} diff --git a/scripts/POE-Filter-FSSocket/examples/poetest.pl b/scripts/POE-Filter-FSSocket/examples/poetest.pl new file mode 100644 index 0000000000..bbb434828b --- /dev/null +++ b/scripts/POE-Filter-FSSocket/examples/poetest.pl @@ -0,0 +1,42 @@ +#!/usr/bin/perl + +use warnings; +use strict; + +use POE qw(Component::Client::TCP Filter::FSSocket); +use Data::Dumper; + +my $auth_sent = 0; +my $password = "ClueCon"; + +POE::Component::Client::TCP->new( + 'RemoteAddress' => '127.0.0.1', + 'RemotePort' => '8021', + 'ServerInput' => \&handle_server_input, + 'Filter' => POE::Filter::FSSocket->new(), +); + +POE::Kernel->run(); +exit; + + +sub handle_server_input { + my ($heap,$input) = @_[HEAP,ARG0]; + + print Dumper $input; + + + if($input->{'Content-Type'} eq "auth/request") { + $auth_sent = 1; + $heap->{'server'}->put("auth $password"); + } elsif ($input->{'Content-Type'} eq "command/reply") { + if($auth_sent == 1) { + $auth_sent = -1; + + #do post auth stuff + $heap->{'server'}->put("events plain all"); + $heap->{'server'}->put("log"); + $heap->{'server'}->put("api show channels"); + } + } +} diff --git a/scripts/POE-Filter-FSSocket/lib/POE/Filter/FSSocket.pm b/scripts/POE-Filter-FSSocket/lib/POE/Filter/FSSocket.pm new file mode 100755 index 0000000000..4e1e67cbc4 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/lib/POE/Filter/FSSocket.pm @@ -0,0 +1,343 @@ +=head1 NAME + +POE::Filter::FSSocket - a POE filter that parses FreeSWITCH events into hashes + +=head1 SYNOPSIS + + #!/usr/bin/perl + + use warnings; + use strict; + + use POE qw(Component::Client::TCP Filter::FSSocket); + use Data::Dumper; + + my $auth_sent = 0; + my $password = "ClueCon"; + + POE::Component::Client::TCP->new( + 'RemoteAddress' => '127.0.0.1', + 'RemotePort' => '8021', + 'ServerInput' => \&handle_server_input, + 'Filter' => 'POE::Filter::FSSocket', + ); + + POE::Kernel->run(); + exit; + + sub handle_server_input { + my ($heap,$input) = @_[HEAP,ARG0]; + + print Dumper $input; + + + if($input->{'Content-Type'} eq "auth/request") { + $auth_sent = 1; + $heap->{'server'}->put("auth $password"); + } elsif ($input->{'Content-Type'} eq "command/reply") { + if($auth_sent == 1) { + $auth_sent = -1; + + #do post auth stuff + $heap->{'server'}->put("events plain all"); + } + } + } + +=head1 EXAMPLES + +See examples in the examples directory of the distribution. + +=head1 DESCRIPTION + +POE::Filter::FSSocket parses output from FreeSWITCH into hashes. FreeSWITCH +events have a very wide range of keys, the only consistant one being +Content-Type. The keys are dependant on the type of events. You must use the +plain event type as that is what the filter knows how to parse. You can ask for +as many event types as you like or all for everything. You specify a list of +event types by putting spaces between them ex: "events plain api log talk" + +Currently known event types (Event-Name): + + CUSTOM + CHANNEL_CREATE + CHANNEL_DESTROY + CHANNEL_STATE + CHANNEL_ANSWER + CHANNEL_HANGUP + CHANNEL_EXECUTE + CHANNEL_BRIDGE + CHANNEL_UNBRIDGE + CHANNEL_PROGRESS + CHANNEL_OUTGOING + CHANNEL_PARK + CHANNEL_UNPARK + API + LOG + INBOUND_CHAN + OUTBOUND_CHAN + STARTUP + SHUTDOWN + PUBLISH + UNPUBLISH + TALK + NOTALK + SESSION_CRASH + MODULE_LOAD + DTMF + MESSAGE + CODEC + BACKGROUND_JOB + ALL + +Currently handled FreeSWITCH messages (Content-Type): + + auth/request + command/response + text/event-plain + api/response (data in __DATA__ variable) + log/data (data in __DATA__ variable) + +=cut + + +package POE::Filter::FSSocket; + +use warnings; +use strict; + +use Carp qw(carp croak); +use vars qw($VERSION); +use base qw(POE::Filter); + +$VERSION = '0.05'; + +use POE::Filter::Line; +use Data::Dumper; + +#self array +sub LINE_FILTER() {1} +sub PARSER_STATE() {2} +sub PARSER_STATENEXT() {3} +sub PARSED_RECORD() {4} +sub CURRENT_LENGTH() {5} +sub STRICT_PARSE() {6} +sub DEBUG_LEVEL() {7} + +#states of the parser +sub STATE_WAITING() {1} #looking for new input +sub STATE_CLEANUP() {2} #wipe out record separators +sub STATE_GETDATA() {3} #have header, get data +sub STATE_FLUSH() {4} #puts us back in wait state and tells us to kill the parsed_record +sub STATE_TEXTRESPONSE() {5} #used for api output + +sub new { + my $class = shift; + my %args = @_; + + my $strict = 0; + my $debug = 0; + + if(defined($args{'debug'})) { + $debug = $args{'debug'}; + } + + if(defined($args{'strict'}) && $args{'strict'} == 1) { + $strict = $args{'strict'}; + } + + #our filter is line based, don't reinvent the wheel + my $line_filter = POE::Filter::Line->new(); + + my $self = bless [ + "", #not used by me but the baseclass clone wants it here + $line_filter, #LINE_FILTER + STATE_WAITING, #PARSER_STATE + undef, #PARSER_STATE + {}, #PARSED_RECORD + 0, #length tracking (for Content-Length when needed) + $strict, #whether we should bail on a bad parse or try and save the session + $debug, #debug level + ], $class; + + return $self; +} + + +sub get_one_start { + my ($self, $stream) = @_; + $self->[LINE_FILTER]->get_one_start($stream); +} + +sub get_one { + my $self = shift; + + while(1) { + #grab a line from the filter + my $line = $self->[LINE_FILTER]->get_one(); + + #quit if we can't get any lines + return [] unless @$line; + + #get the actual line + $line = $line->[0]; + + if(($self->[PARSER_STATE] == STATE_WAITING) || ($self->[PARSER_STATE] == STATE_FLUSH)) { + #see if we need to wipe out the parsed_record info + if($self->[PARSER_STATE] == STATE_FLUSH) { + delete $self->[PARSED_RECORD]; + $self->[CURRENT_LENGTH] = 0; + + $self->[PARSER_STATE] = STATE_WAITING; + } + + if($line =~ /Content-Length:\ (\d+)$/) { + #store the length + $self->[PARSED_RECORD]{'Content-Length'} = $1; + + #see if we had a place to go from here (we should) + if(defined($self->[PARSER_STATENEXT])) { + $self->[PARSER_STATE] = $self->[PARSER_STATENEXT]; + $self->[PARSER_STATENEXT] = undef; + } + } elsif($line =~ /Content-Type:\ (.*)$/) { + #store the type of request + $self->[PARSED_RECORD]{'Content-Type'} = $1; + + if($1 eq "auth/request") { + $self->[PARSER_STATE] = STATE_CLEANUP; + $self->[PARSER_STATENEXT] = STATE_FLUSH; + return [ $self->[PARSED_RECORD] ]; + } elsif ($1 eq "command/reply") { #do something with this later + $self->[PARSER_STATE] = STATE_GETDATA; + } elsif ($1 eq "text/event-plain") { + $self->[PARSER_STATE] = STATE_CLEANUP; + $self->[PARSER_STATENEXT] = STATE_GETDATA; + } elsif ($1 eq "api/response") { + $self->[PARSER_STATENEXT] = STATE_TEXTRESPONSE; + } elsif ($1 eq "log/data") { + $self->[PARSER_STATENEXT] = STATE_TEXTRESPONSE; + } else { #unexpected input + croak ref($self) . " unknown input [" . $self->[PARSER_STATE] . "] (" . $line . ")"; + } + } else { + #already in wait state, if we are not in strict, keep going + if($self->[STRICT_PARSE]) { + croak ref($self) . " unknown input [STATE_WAITING] (" . $line . ")"; + } + } + } elsif ($self->[PARSER_STATE] == STATE_CLEANUP) { + if($line eq "") { + if(defined($self->[PARSER_STATENEXT])) { + $self->[PARSER_STATE] = $self->[PARSER_STATENEXT]; + $self->[PARSER_STATENEXT] = undef; + } else { + $self->[PARSER_STATE] = STATE_WAITING; + } + } else { + #see if we should bail + if($self->[STRICT_PARSE]) { + croak ref($self) . " unknown input [STATE_CLEANUP] (" . $line . ")"; + } else { + #we are not supposed to bail so try and save our session... + #since we are think we should be cleaning up, flush it all away + $self->[PARSER_STATE] = STATE_FLUSH; + + #parser fail should be considered critical, if any debug at all, print dump + if($self->[DEBUG_LEVEL]) { + print STDERR "Parse failed on ($line) in STATE_CLEANUP:\n"; + print STDERR Dumper $self->[PARSED_RECORD]; + } + } + } + } elsif ($self->[PARSER_STATE] == STATE_GETDATA) { + if($line =~ /^([^:]+):\ (.*)$/) { + $self->[PARSED_RECORD]{$1} = $2; + } elsif ($line eq "") { #end of event + $self->[PARSER_STATE] = STATE_FLUSH; + + return [ $self->[PARSED_RECORD] ]; + } else { + if($self->[STRICT_PARSE]) { + croak ref($self) . " unknown input [STATE_GETDATA] (" . $line . ")"; + } else { + #flush and run + $self->[PARSER_STATE] = STATE_FLUSH; + + #parser fail should be considered critical, if any debug at all, print dump + if($self->[DEBUG_LEVEL]) { + print STDERR "Parse failed on ($line) in STATE_GETDATA:\n"; + print STDERR Dumper $self->[PARSED_RECORD]; + } + } + } + } elsif ($self->[PARSER_STATE] == STATE_TEXTRESPONSE) { + if($self->[CURRENT_LENGTH] == -1) { + $self->[CURRENT_LENGTH] = 0; + next; + } + + $self->[CURRENT_LENGTH] += (length($line) + 1); + + if(($self->[CURRENT_LENGTH] - 1) == $self->[PARSED_RECORD]{'Content-Length'}) { + $self->[PARSER_STATE] = STATE_FLUSH; + $self->[PARSED_RECORD]{'__DATA__'} .= $line; + + return [$self->[PARSED_RECORD]]; + } else { + $self->[PARSED_RECORD]{'__DATA__'} .= $line . "\n"; + } + } + } +} + +sub put { + my ($self, $lines) = @_; + + my @row; + foreach my $line (@$lines) { + push @row, $line . "\n\n"; + } + + return \@row; + +} + +sub get_pending { + my $self = shift; + return $self->[LINE_FILTER]->get_pending(); +} + +sub get { + my ($self, $stream) = @_; + my @return; + + $self->get_one_start($stream); + while(1) { + my $next = $self->get_one(); + last unless @$next; + push @return, @$next; + } + + return \@return; +} + +1; + +=head1 SEE ALSO + +FreeSWITCH - http://www.freeswitch.org/ + +=head1 AUTHORS + +POE::Filter::FSSocket is written by Paul Tinsley. You can reach him by e-mail +at pdt@jackhammer.org. + +=head1 COPYRIGHT + +Copyright 2006, Paul Tinsley. All rights are reserved. + +POE::Filter::FSSocket is free software; it is currently licensed under the MPL +license version 1.1. + +=cut diff --git a/scripts/POE-Filter-FSSocket/t/01_basic.t b/scripts/POE-Filter-FSSocket/t/01_basic.t new file mode 100644 index 0000000000..ed57b71aa0 --- /dev/null +++ b/scripts/POE-Filter-FSSocket/t/01_basic.t @@ -0,0 +1,10 @@ +#!/usr/bin/env perl + +use warnings; +use strict; + +use Test::More tests => 1; + +use_ok("POE::Filter::FSSocket"); + +exit; diff --git a/scripts/socket/fsconsole.pl b/scripts/socket/fsconsole.pl new file mode 100644 index 0000000000..db43e319fb --- /dev/null +++ b/scripts/socket/fsconsole.pl @@ -0,0 +1,361 @@ +#!/usr/bin/perl +use strict; +use warnings; + +sub POE::Kernel::ASSERT_DEFAULT () { 1 }; +sub Term::Visual::DEBUG () { 1 } +sub Term::Visual::DEBUG_FILE () { 'test.log' } +use IO::Socket; +use POE qw/Filter::FSSocket Component::Client::TCP/; +use Data::Dumper; +use Term::Visual; + + +local *D; +if (Term::Visual::DEBUG) { + *D = *Term::Visual::ERRS; +} + +#local *ERROR = *STDERR; + + +$SIG{__DIE__} = sub { + if (Term::Visual::DEBUG) { + print Term::Visual::ERRS "Died: @_\n"; + } +}; + +############################################################################### +## BEGIN Globals ############################################################## +############################################################################### +our $server_address = "127.0.0.1"; +our $server_port = "8021"; +our $server_secret = "ClueCon"; + +#this is where you can customize the color scheme +our %Pallet = ( + 'warn_bullet' => 'bold yellow', + 'err_bullet' => 'bold red', + 'out_bullet' => 'bold green', + 'access' => 'bright red on blue', + 'current' => 'bright yellow on blue', +); + +our $terminal; +my %sockets; +my %windows; +my %unread_count; +my %commands = ( + 'window' => 1, + 'w' => 1, + 'win' => 1, +); +############################################################################### +## END Globals ############################################################## +############################################################################### + +#setup our session +POE::Session->create( + 'inline_states' => { + '_start' => \&handle_start, #session start + '_stop' => \&handle_stop, #session stop + 'curses_input' => \&handle_curses_input, #input from the keyboard + 'update_time' => \&handle_update_time, #update the status line clock + 'quit' => \&handle_quit, #handler to do any cleanup + 'server_input' => \&handle_server_input, + '_default' => \&handle_default, + }, + 'heap' => { + 'terminal' => undef, + 'freeswitch' => undef, + }, +); + +#start the kernel a chugging along +$poe_kernel->run; + +############################################################################### +## BEGIN Handlers ############################################################# +############################################################################### +#handles any startup functions for our session +sub handle_default { +} + +sub handle_start { + my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP]; + + #setup our terminal + $heap->{'terminal'} = Term::Visual->new( + 'Alias' => 'terminal', #poe alias for this + 'History_Size' => 300, #number of things to keep in history + 'Common_Input' => 1, #all windows share input and history + 'Tab_Complete' => \&tab_complete, + ); + + $terminal = $heap->{'terminal'}; + + #setup the color palette + $terminal->set_palette(%Pallet); + + #create a base window + my $window_id = $terminal->create_window( + 'Window_Name' => 'console', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Console', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'console'} = $window_id; + + $window_id = $terminal->create_window( + 'Window_Name' => 'log', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Logs', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'log'} = $window_id; + + $window_id = $terminal->create_window( + 'Window_Name' => 'event', + 'Buffer_Size' => 3000, + 'Title' => 'FreeSWITCH Event', + 'Status' => { + '0' => { + 'format' => '%s', + 'fields' => ['time'], + }, + '1' => { + 'format' => '%s', + 'fields' => ['window_status'], + }, + }, + ); + + $windows{'event'} = $window_id; + + #tell the terminal what to call when there is input from the keyboard + $kernel->post('terminal' => 'send_me_input' => 'curses_input'); + + $terminal->change_window(0); + $kernel->delay_set('update_time' => 1); + $terminal->set_status_field(0, 'time' => scalar(localtime)); + new_message('destination_window' => 0, 'message' => " +Welcome to the FreeSWITCH POE Curses Console! + The console is split into three windows: + - 'console' for api response messages + - 'log' for freeswitch log output (simply send the log level you want + to start seeing events eg: 'log all') + - 'event' for freeswitch event output (must subscribe in plain format + eg: 'event plain all') + +To switch between windows type 'w set_status_field($terminal->current_window, 'window_status' => format_window_status()); + + #connect to freeswitch + $heap->{'freeswitch'} = POE::Component::Client::TCP->new( + 'RemoteAddress' => $server_address, + 'RemotePort' => $server_port, + 'ServerInput' => \&handle_server_input, + 'Connected' => \&handle_fs_connected, + 'ServerError' => \&handle_server_error, + 'Disconnected' => \&handle_server_disconnect, + 'Domain' => AF_INET, + 'Filter' => POE::Filter::FSSocket->new(), + ); + +} + +#called when users enter commands in a window +sub handle_curses_input { + my ($kernel, $heap, $input, $context) = @_[KERNEL, HEAP, ARG0, ARG1]; + + #get the id of the window that is responsible for the input + my $window = $heap->{'terminal'}->current_window; + + open(ERROR, ">>error.log"); + + if($input eq "quit") { + $kernel->yield('quit'); + } elsif ($input =~ /^w\ (.*)$/) { + #get the id of the requested window + eval { + my $window_id = $windows{$1}; + + #see if it's real + if(defined($window_id)) { + $unread_count{$window_id} = 0; + $terminal->change_window($window_id); + $terminal->set_status_field($window_id, 'window_status' => &format_window_status()); + } + }; + if($@) { + print ERROR "put error: $@\n"; + } + } else { + #see if we got connected at some point + if(defined($sockets{'localhost'})) { + #send the command + $sockets{'localhost'}->put($input); + } + } +} + +sub handle_fs_connected { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + + eval { + $sockets{'localhost'} = $heap->{'server'}; + } +} + +#this is responsible for doing any cleanup and returning the terminal to the previous +#state before we mucked with it +sub handle_quit { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + + #tell curses to clean up it's crap + $kernel->post('terminal' => 'shutdown'); + + #there is probably a more elegant way, but this works for now + exit; +} + +#data from freeswitch +sub handle_server_input { + my ($kernel,$heap,$input) = @_[KERNEL,HEAP,ARG0]; + + eval { + #terminal HATES null + if(defined($input->{'__DATA__'})) { + $input->{'__DATA__'} =~ s/[\x00]//g; + } + + #handle the login + if($input->{'Content-Type'} eq "auth/request") { + $heap->{'server'}->put("auth $server_secret"); + } elsif ($input->{'Content-Type'} eq "api/response") { + new_message('destination_window' => 0, 'message' => 'Response: '); + new_message('destination_window' => 0, 'message' => $input->{'__DATA__'}); + } elsif ($input->{'Content-Type'} eq "log/data") { + new_message('destination_window' => 1, 'message' => $input->{'__DATA__'}); + } elsif ($input->{'Content-Type'} eq "text/event-plain") { + new_message('destination_window' => 2, 'message' => Dumper $input); + } + }; + + if($@) { + open(ERROR, ">>error.log"); + print ERROR "died: $@\n"; + print ERROR Dumper $heap; + close(ERROR); + } +} + +sub handle_server_error { +} + +sub handle_server_disconnect { +} + +sub tab_complete { + my $left = shift; + + my @return; + + if(defined($commands{$left})) { + return [$left . " "]; + #} elsif () { + } + +} + +sub handle_update_time { + my ($kernel, $heap) = @_[KERNEL, HEAP]; + $terminal->set_status_field($terminal->current_window, 'time' => scalar(localtime)); + $kernel->delay_set('update_time' => 1); +} +############################################################################### +## END Handlers ############################################################# +############################################################################### + +sub new_message { + my %args = @_; + + my $message = $args{'message'}; + my $destination_window = $args{'destination_window'}; + + my $status_field; + + #see if we are on the window being updated + if($terminal->current_window != $destination_window) { + #increment the unread count for the window + #FIXME, should we count messages or lines? + $unread_count{$destination_window}++; + + + #update the status bar + eval { + $terminal->set_status_field($terminal->current_window, 'window_status' => &format_window_status()); + }; + + if($@) { + print $@; + } + } + + + #deliver the message + $terminal->print($destination_window, $message); +} + +sub format_window_status { + my $status_field; + + #put all the windows in the bar with their current unread count + foreach my $window (sort {$windows{$a} <=> $windows{$b}} keys %windows) { + #see if we are printing the current window + if($terminal->current_window == $windows{$window}) { + $status_field .= "[\0(current)$window\0(st_frames)"; + } else { + $status_field .= "[$window"; + } + + if($unread_count{$windows{$window}}) { + $status_field .= " (" . $unread_count{$windows{$window}} . ")"; + } + + $status_field .= "] "; + } + + return $status_field; +}