File Coverage

blib/lib/POE/Component/AssaultCube/ServerQuery.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             # Declare our package
2             package POE::Component::AssaultCube::ServerQuery;
3              
4             # import the Moose stuff
5 1     1   2508 use MooseX::POE::SweetArgs;
  0            
  0            
6             use MooseX::StrictConstructor;
7              
8             # Initialize our version
9             use vars qw( $VERSION );
10             $VERSION = '0.04';
11              
12             # get some utility stuff
13             use Games::AssaultCube::ServerQuery::Response;
14             use POE::Component::AssaultCube::ServerQuery::Server;
15             use Games::AssaultCube::Utils qw( tostr get_ac_pingport );
16              
17             # We need some POE stuff
18             use POE::Wheel::UDP;
19             use POE::Filter::Stream;
20             use Socket qw( INADDR_ANY );
21             use Time::HiRes qw( time );
22              
23             # TODO improve validation for everything here, ha!
24              
25             has 'alias' => (
26             isa => 'Str',
27             is => 'ro',
28             lazy => 1,
29             default => sub {
30             my $self = shift;
31             return 'ServerQuery-' . $self->get_session_id;
32             },
33             );
34              
35             has 'wheel' => (
36             isa => 'Maybe[POE::Wheel::UDP]',
37             is => 'rw',
38             default => undef,
39             );
40              
41             has 'watchers' => (
42             isa => 'HashRef',
43             is => 'ro',
44             default => sub { {} },
45             );
46              
47             has 'servers' => (
48             isa => 'HashRef',
49             is => 'ro',
50             default => sub { {} },
51             );
52              
53             has 'throttle' => (
54             isa => 'Num',
55             is => 'rw',
56             default => 0.25,
57             );
58              
59             # add this session to the watchers
60             sub register {
61             my( $self, $session, $event ) = @_;
62              
63             if ( ! defined $session ) {
64             # take from current session ID?
65             $session = $poe_kernel->get_active_session;
66             if ( defined $session ) {
67             if ( $session->isa( 'POE::Kernel' ) ) {
68             # no session running
69             return;
70             }
71             } else {
72             return;
73             }
74             }
75              
76             # set the default
77             $event = 'ac_ping' if ! defined $event;
78             $session = $session->ID if ref $session;
79              
80             #warn "$session registered as a watcher\n";
81              
82             $self->watchers->{ $session } = $event;
83             return 1;
84             }
85              
86             sub unregister {
87             my( $self, $session ) = @_;
88              
89             if ( ! defined $session ) {
90             # take from current session ID?
91             $session = $poe_kernel->get_active_session;
92             if ( defined $session ) {
93             if ( $session->isa( 'POE::Kernel' ) ) {
94             # no session running
95             return;
96             }
97             } else {
98             return;
99             }
100             }
101              
102             $session = $session->ID if ref $session;
103              
104             #warn "$session unregistered as a watcher\n";
105              
106             if ( exists $self->watchers->{ $session } ) {
107             delete $self->watchers->{ $session };
108             return 1;
109             } else {
110             return;
111             }
112             }
113              
114             sub addserver {
115             my $self = shift;
116              
117             # sanity
118             my $server;
119             if ( defined $_[0] ) {
120             if ( ref $_[0] and ref( $_[0] ) eq 'POE::Component::AssaultCube::ServerQuery::Server' ) {
121             $server = $_[0];
122             } else {
123             # convert it into an object
124             eval {
125             $server = POE::Component::AssaultCube::ServerQuery::Server->new( @_ );
126             };
127             if ( $@ ) {
128             die "invalid server data: $@";
129             }
130             if ( ! defined $server ) {
131             die "unable to parse server data";
132             }
133             }
134             } else {
135             return;
136             }
137              
138             if ( exists $self->servers->{ $server->ID } ) {
139             return;
140             } else {
141             #warn "added server " . $server->ID;
142              
143             # start pinging this server
144             $self->servers->{ $server->ID } = $server;
145             $poe_kernel->call( $self->get_session_id, 'start_pinger_delay' );
146             return $server;
147             }
148             }
149              
150             sub delserver {
151             my $self = shift;
152              
153             # sanity
154             my $server;
155             if ( defined $_[0] ) {
156             if ( ref $_[0] and ref( $_[0] ) eq 'POE::Component::AssaultCube::ServerQuery::Server' ) {
157             $server = $_[0];
158             } else {
159             # convert it into an object
160             eval {
161             $server = POE::Component::AssaultCube::ServerQuery::Server->new( @_ );
162             };
163             if ( $@ ) {
164             die "invalid server data: $@";
165             }
166             if ( ! defined $server ) {
167             die "unable to parse server data";
168             }
169             }
170             } else {
171             return;
172             }
173              
174             if ( exists $self->servers->{ $server->ID } ) {
175             #warn "deleted server " . $server->ID;
176              
177             delete $self->servers->{ $server->ID };
178             $poe_kernel->call( $self->get_session_id, 'start_pinger_delay' );
179             return 1;
180             } else {
181             return;
182             }
183             }
184              
185             sub STARTALL {
186             my $self = shift;
187              
188             #warn "in STARTALL";
189              
190             $poe_kernel->alias_set( $self->alias );
191              
192             # should we fire up the pinger?
193             if ( keys %{ $self->servers } ) {
194             $poe_kernel->post( $self->get_session_id, 'start_pinger_delay' );
195             }
196              
197             return;
198             }
199              
200             sub STOPALL {
201             my $self = shift;
202              
203             #warn "in STOPALL";
204              
205             return;
206             }
207              
208             sub make_wheel {
209             my $self = shift;
210              
211             # sanity
212             return if defined $self->wheel;
213              
214             #warn "creating POE::Wheel::UDP";
215              
216             $self->wheel( POE::Wheel::UDP->new(
217             LocalAddr => '0.0.0.0',
218             LocalPort => INADDR_ANY,
219             InputEvent => 'wheel_input',
220             Filter => POE::Filter::Stream->new,
221             ) );
222              
223             # be evil but we need to do this...
224             binmode $self->wheel->{sock}, ":utf8" or die "Unable to set binmode: $!";
225              
226             return;
227             }
228              
229             event start_pinger_delay => sub {
230             my $self = shift;
231              
232             $poe_kernel->delay( 'start_pinger' => 0.1 );
233             return;
234             };
235              
236             event start_pinger => sub {
237             my $self = shift;
238              
239             # okay, get the next server timeout
240             my( $server, $nexttime ) = $self->get_next_server;
241             if ( defined $server ) {
242             # do we have a wheel?
243             if ( ! defined $self->wheel ) {
244             $self->make_wheel;
245             $poe_kernel->delay( 'start_pinger' => 1 );
246             return;
247             }
248              
249             if ( $nexttime != 0 and $nexttime < $self->throttle ) {
250             $nexttime = $self->throttle;
251              
252             #warn "THROTTLE HIT!";
253              
254             }
255             $nexttime = 0 if $nexttime < 0;
256              
257             #warn "server(" . $server->ID . ") selected with $nexttime";
258              
259             if ( $nexttime == 0 or $self->throttle == 0 ) {
260             $self->ping_server( $server );
261              
262             # ping the next available server
263             $poe_kernel->delay( 'start_pinger' => 0 );
264             } else {
265             #warn "sleeping for $nexttime secs";
266             $poe_kernel->delay( 'start_pinger' => $nexttime );
267             }
268             } else {
269             # no server, wait until we add a server
270             $poe_kernel->alarm_remove_all;
271             $self->wheel( undef );
272              
273             #warn "no server, waiting until addserver";
274              
275             }
276              
277             return;
278             };
279              
280             sub get_next_server {
281             my $self = shift;
282              
283             # shortcut
284             if ( keys %{ $self->servers } == 0 ) {
285             return;
286             }
287              
288             # okay, we order servers by last_pingtime and in respect to their pingfreq
289             my @servers = sort { $a->[1] <=> $b->[1] }
290             map { [ $_, $_->nextping() ] } values %{ $self->servers };
291              
292             #use Data::Dumper;
293             #print Dumper( \@servers );
294              
295             # return the first server
296             return( $servers[0]->[0], $servers[0]->[1] );
297             }
298              
299             sub ping_server {
300             my( $self, $server ) = @_;
301              
302             # actually ping it!
303             my $datagram;
304             if ( $server->get_players ) {
305             $datagram = tostr('1') . tostr('1');
306             } else {
307             $datagram = tostr('1') . tostr('0');
308             }
309              
310             #warn "pinging " . $server->ID;
311              
312             # send it!
313             eval {
314             $self->wheel->put( {
315             payload => [ $datagram ],
316             addr => $server->server,
317             port => get_ac_pingport( $server->port ),
318             } );
319             };
320              
321             # set the lastpingtime
322             $server->last_pingtime( time() );
323              
324             return;
325             }
326              
327             event 'wheel_input' => sub {
328             my( $self, $input, $wheel_id ) = @_;
329              
330             return if ! length $input;
331              
332             # make the server ID
333             # TODO we hardcode the "$port - 1" behavior...
334             $input->{ID} = $input->{addr} . ':' . ( $input->{port} - 1 );
335              
336             # do we know this server?
337             if ( exists $self->servers->{ $input->{ID} } ) {
338             # yay, got a ping back!
339             $self->process_ping( $input );
340             } else {
341             # hm, unknown ping...
342             warn "unknown DATA from $input->{ID}";
343             }
344              
345             return;
346             };
347              
348             sub shutdown {
349             my $self = shift;
350             $poe_kernel->post( $self->get_session_id, 'do_shutdown' );
351              
352             return;
353             }
354              
355             event 'do_shutdown' => sub {
356             my $self = shift;
357              
358             # cleanup
359             $poe_kernel->alias_remove( $self->alias );
360             $self->wheel( undef );
361             $poe_kernel->alarm_remove_all;
362              
363             return;
364             };
365              
366             sub process_ping {
367             my( $self, $input ) = @_;
368              
369             #use Data::Dumper;
370             #warn "got ping reply: " . Dumper( $input );
371             #
372             #use Data::HexDump;
373             #print HexDump( $input->{payload}->[0] );
374              
375             # okay, convert it into a response object
376             my $response;
377             eval {
378             $response = Games::AssaultCube::ServerQuery::Response->new( $self, $input );
379             };
380             if ( $@ ) {
381             warn "unable to parse DATA from $input->{ID}: $@";
382             };
383              
384             # pass it on to the watchers
385             foreach my $w ( keys %{ $self->watchers } ) {
386             #warn "informing watcher $w of ping";
387              
388             $poe_kernel->post( $w, $self->watchers->{ $w }, $self->servers->{ $input->{ID} }, $response );
389             }
390              
391             return;
392             }
393              
394             sub clearservers {
395             my $self = shift;
396              
397             # get rid of all servers and reset the timer
398             %{ $self->servers } = ();
399             $poe_kernel->call( $self->get_session_id, 'start_pinger_delay' );
400              
401             return;
402             }
403              
404             # from Moose::Manual::BestPractices
405             no MooseX::POE;
406             __PACKAGE__->meta->make_immutable;
407              
408             1;
409             __END__
410              
411             =for stopwords addserver clearservers delserver serverlist
412              
413             =head1 NAME
414              
415             POE::Component::AssaultCube::ServerQuery - Queries a running AssaultCube server for information
416              
417             =head1 SYNOPSIS
418              
419             use POE qw( Component::AssaultCube::ServerQuery );
420              
421             sub _start {
422             $_[HEAP]->{query} = POE::Component::AssaultCube::ServerQuery->new;
423             $_[HEAP]->{query}->register( $_[SESSION], 'got_ping_data' );
424             $_[HEAP]->{query}->addserver( '123.123.123.123' );
425             }
426              
427             sub got_ping_data {
428             my( $server, $response ) = @_[ARG0, ARG1];
429             if ( defined $response ) {
430             print "response from(" . $server->ID . "): " $response->desc_nocolor .
431             " - " . $response->players . " players running\n";
432             } else {
433             print "server " . $server->ID . " is not responding\n";
434             }
435             }
436              
437             =head1 ABSTRACT
438              
439             This module queries a running AssaultCube server for information.
440              
441             =head1 DESCRIPTION
442              
443             This module is a wrapper around the L<Games::AssaultCube::ServerQuery> logic and encapsulates the
444             raw POE details. Furthermore, this module can ping many servers in parallel.
445              
446             This module gives you full control of throttling, and per-server ping frequency ( how often to ping
447             the server ) plus a nice object front-end!
448              
449             Normal usage of this component is to create an object, then add your serverlist to the object. Then
450             you would have to register your session to receive responses. During run-time you can add/remove servers
451             from the list, and finally shutdown the object/session.
452              
453             NOTE: While you can create several ServerQuery objects and use them, it is more optimal to create only
454             one object and put all servers there. ( This theory is unbenchmarked, ha! )
455              
456             This module does not enforce timeouts per server, it gives you a "raw" feed of pings every $frequency
457             seconds. It is up to the application logic to see if a ping failed or not. This is trivial with the
458             appropriate use of timers :) However, patches welcome if you want the server to have individual timeouts.
459             It will not change the logic in the application event "ac_ping" because it already checks for a defined
460             value.
461              
462             This module sets an alias to be "long-lived" and creates/destroys the L<POE::Wheel::UDP> object only
463             when necessary.
464              
465             =head2 Constructor
466              
467             This module uses Moose, so you can pass either a hash or hashref to the constructor.
468              
469             The attributes are:
470              
471             =head3 throttle
472              
473             A number in seconds ( can be floating-point )
474              
475             How long we should wait before sending the next ping. Useful for flood-control!
476              
477             Default: 0.25
478              
479             NOTE: You can set it to 0 to disable this feature
480              
481             =head3 alias
482              
483             The POE session alias we will use
484              
485             Default: 'ServerQuery-' . $_[SESSION]->ID
486              
487             =head2 Methods
488              
489             Once instantiated, you can do various operations on the object.
490              
491             =head3 addserver
492              
493             Adds a server to be monitored to the list. Arguments are passed on to the
494             L<POE::Component::AssaultCube::ServerQuery::Server> constructor. Returns the server
495             object or undef if it was already in the list. Will die if it encounters errors.
496              
497             $query->addserver( "123.123.123.123" );
498             $query->addserver( "123.123.123.123", 12345 );
499             $query->addserver({ server => "123.123.123.123", port => 12345, frequency => 60 });
500              
501             Adding a server automatically sends a ping, then waits for $frequency seconds before sending the next
502             one.
503              
504             =head3 delserver
505              
506             Deletes a server from the monitoring list. You can either pass in a
507             L<POE::Component::AssaultCube::ServerQuery::Server> object ref or the arguments will be converted
508             internally into the object. From there we will see if the server is in the list, returns 1 if it is;
509             returns undef otherwise.
510              
511             =head3 register
512              
513             Adds a "watcher" session that will receive ping replies. Accepts a session ID/alias/reference, and the
514             event name.
515              
516             The session defaults to the running POE session, if there is one
517              
518             The event name defaults to "ac_ping"
519              
520             =head3 unregister
521              
522             Removes a "watcher" session from the list. Accepts a session ID/alias/reference.
523              
524             =head3 shutdown
525              
526             Initiates shutdown procedures and destroys the associated session
527              
528             =head3 clearservers
529              
530             Removes all servers from the list and ceases pinging servers
531              
532             =head2 POE events
533              
534             You can post events to this component too. You can get the session id:
535              
536             $poe_kernel->post( $query->get_session_id, ... );
537              
538             =head3 do_shutdown
539              
540             Initiates shutdown procedures and destroys the associated session
541              
542             =head2 PING data
543              
544             You receive the ping replies via the session/event you registered. There is no "filtering" capability
545             and you get replies for all servers.
546              
547             The event handler gets 2 arguments: the server object, and the response. The server object is the
548             L<POE::Component::AssaultCube::ServerQuery::Server> module. The response can either be
549             undef or a L<Games::AssaultCube::ServerQuery::Response> object.
550              
551             Here's a sample ping handler:
552              
553             sub got_ping_data {
554             my( $server, $response ) = @_[ARG0, ARG1];
555             if ( defined $response ) {
556             print "response from(" . $server->ID . "): " $response->desc_nocolor .
557             " - " . $response->players . " players running\n";
558             } else {
559             print "server " . $server->ID . " is not responding\n";
560             }
561             }
562              
563             =head1 AUTHOR
564              
565             Apocalypse E<lt>apocal@cpan.orgE<gt>
566              
567             Props goes to Getty and the BS clan for the support!
568              
569             This project is sponsored by L<http://cubestats.net>
570              
571             =head1 COPYRIGHT AND LICENSE
572              
573             Copyright 2009 by Apocalypse
574              
575             This library is free software; you can redistribute it and/or modify
576             it under the same terms as Perl itself.
577              
578             =cut