File Coverage

blib/lib/POE/Component/Metabase/Relay/Server.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package POE::Component::Metabase::Relay::Server;
2             $POE::Component::Metabase::Relay::Server::VERSION = '0.34';
3             # ABSTRACT: A Metabase relay server component
4              
5 1     1   108315 use strict;
  1         1  
  1         28  
6 1     1   5 use warnings;
  1         1  
  1         25  
7 1     1   415 use CPAN::Testers::Report;
  1         37037  
  1         31  
8 1     1   5 use POE qw[Filter::Stream];
  1         2  
  1         9  
9 1     1   2269 use POE::Component::Metabase::Relay::Server::Queue;
  0            
  0            
10             use Test::POE::Server::TCP;
11             use Carp ();
12             use Storable ();
13             use Socket ();
14             use JSON ();
15             use Metabase::User::Profile ();
16             use Metabase::User::Secret ();
17              
18             my @fields = qw(
19             osversion
20             distfile
21             archname
22             textreport
23             osname
24             perl_version
25             grade
26             );
27              
28             use MooseX::POE;
29             use MooseX::Types::Path::Class qw[File];
30             use MooseX::Types::URI qw[Uri];
31              
32             {
33             use Moose::Util::TypeConstraints;
34             my $tc = subtype as 'ArrayRef[Str]';
35             coerce $tc, from 'Str', via { [$_] };
36              
37             has 'address' => (
38             is => 'ro',
39             isa => $tc,
40             default => 0,
41             coerce => 1,
42             );
43              
44             my $ps = subtype as 'Str', where { $poe_kernel->alias_resolve( $_ ) };
45             coerce $ps, from 'Str', via { $poe_kernel->alias_resolve( $_ )->ID };
46              
47             has 'session' => (
48             is => 'ro',
49             isa => $ps,
50             coerce => 1,
51             writer => '_set_session',
52             );
53              
54             no Moose::Util::TypeConstraints;
55             }
56              
57             has 'port' => (
58             is => 'ro',
59             default => sub { 0 },
60             writer => '_set_port',
61             );
62              
63             has 'id_file' => (
64             is => 'ro',
65             required => 1,
66             isa => File,
67             coerce => 1,
68             );
69              
70             has 'dsn' => (
71             is => 'ro',
72             isa => 'Str',
73             required => 1,
74             );
75              
76             has 'uri' => (
77             is => 'ro',
78             isa => Uri,
79             coerce => 1,
80             required => 1,
81             );
82              
83             has 'username' => (
84             is => 'ro',
85             isa => 'Str',
86             default => '',
87             );
88              
89             has 'password' => (
90             is => 'ro',
91             isa => 'Str',
92             default => '',
93             );
94              
95             has 'db_opts' => (
96             is => 'ro',
97             isa => 'HashRef',
98             default => sub {{}},
99             );
100              
101             has 'debug' => (
102             is => 'rw',
103             isa => 'Bool',
104             default => 0,
105             );
106              
107             has 'multiple' => (
108             is => 'ro',
109             isa => 'Bool',
110             default => 0,
111             );
112              
113             has 'recv_event' => (
114             is => 'ro',
115             isa => 'Str',
116             );
117              
118             has 'no_relay' => (
119             is => 'rw',
120             isa => 'Bool',
121             default => 0,
122             trigger => sub {
123             my( $self, $new, $old ) = @_;
124             return if ! $self->_has_queue;
125             $self->queue->no_relay( $new );
126             },
127             );
128              
129             has 'no_curl' => (
130             is => 'ro',
131             isa => 'Bool',
132             default => 0,
133             );
134              
135             has 'submissions' => (
136             is => 'rw',
137             isa => 'Int',
138             default => 10,
139             trigger => sub {
140             my( $self, $new, $old ) = @_;
141             return if ! $self->_has_queue;
142             $self->queue->submissions( $new );
143             },
144             );
145              
146             has '_profile' => (
147             is => 'ro',
148             isa => 'Metabase::User::Profile',
149             init_arg => undef,
150             writer => '_set_profile',
151             );
152              
153             has '_secret' => (
154             is => 'ro',
155             isa => 'Metabase::User::Secret',
156             init_arg => undef,
157             writer => '_set_secret',
158             );
159              
160             has '_relayd' => (
161             accessor => 'relayd',
162             isa => 'ArrayRef[Test::POE::Server::TCP]',
163             lazy_build => 1,
164             auto_deref => 1,
165             init_arg => undef,
166             );
167              
168             has '_queue' => (
169             accessor => 'queue',
170             isa => 'POE::Component::Metabase::Relay::Server::Queue',
171             lazy_build => 1,
172             init_arg => undef,
173             );
174              
175             has '_requests' => (
176             is => 'ro',
177             isa => 'HashRef',
178             default => sub {{}},
179             init_arg => undef,
180             );
181              
182             sub _build__relayd {
183             my $self = shift;
184             return [map {
185             Test::POE::Server::TCP->spawn(
186             address => $_,
187             port => $self->port,
188             prefix => 'relayd',
189             filter => POE::Filter::Stream->new(),
190             )
191             } @{ $self->address }]
192             }
193              
194             sub _build__queue {
195             my $self = shift;
196             POE::Component::Metabase::Relay::Server::Queue->spawn(
197             dsn => $self->dsn,
198             username => $self->username,
199             password => $self->password,
200             db_opts => $self->db_opts,
201             uri => $self->uri->as_string,
202             profile => $self->_profile,
203             secret => $self->_secret,
204             debug => $self->debug,
205             multiple => $self->multiple,
206             no_relay => $self->no_relay,
207             no_curl => $self->no_curl,
208             submissions => $self->submissions,
209             );
210             }
211              
212             sub spawn {
213             shift->new(@_);
214             }
215              
216             sub START {
217             my ($kernel,$self,$sender) = @_[KERNEL,OBJECT,SENDER];
218             if ( $kernel == $sender and $self->recv_event and !$self->session ) {
219             Carp::croak "Not called from another POE session and 'session' wasn't set\n";
220             }
221             if ( $self->recv_event ) {
222             $self->_set_session( $sender->ID ) unless $self->session;
223             }
224             $self->_load_id_file;
225             $self->relayd;
226             $self->queue;
227             return;
228             }
229              
230              
231             event 'shutdown' => sub {
232             my ($kernel,$self) = @_[KERNEL,OBJECT];
233             $_->shutdown for $self->relayd;
234             $poe_kernel->post(
235             $self->queue->get_session_id,
236             'shutdown',
237             );
238             return;
239             };
240              
241             event 'relayd_registered' => sub {
242             my ($kernel,$self,$relayd) = @_[KERNEL,OBJECT,ARG0];
243             my ($port, $addr) = Socket::sockaddr_in($relayd->getsockname);
244             warn "Listening on '", join(q{:} => scalar gethostbyaddr($addr, Socket::AF_INET), $port), "'\n"
245             if $self->debug;
246             $self->_set_port( $relayd->port );
247             return;
248             };
249              
250             event 'relayd_connected' => sub {
251             my ($kernel,$self,$id,$ip) = @_[KERNEL,OBJECT,ARG0,ARG1];
252             return;
253             };
254              
255             event 'relayd_disconnected' => sub {
256             my ($kernel,$self,$id,$ip) = @_[KERNEL,OBJECT,ARG0,ARG1];
257             my $data = delete $self->_requests->{$id};
258             my $report = eval { Storable::thaw($data); };
259             if ( defined $report and ref $report and ref $report eq 'HASH' ) {
260             $kernel->yield( 'process_report', $report, $ip );
261             }
262             else {
263             return unless $self->debug;
264             warn "Client '$id' failed to send parsable data!\n";
265             warn "The error from Storable::thaw was '$@'\n";
266             }
267             return;
268             };
269              
270             event 'relayd_client_input' => sub {
271             my ($kernel,$self,$id,$data) = @_[KERNEL,OBJECT,ARG0,ARG1];
272             $self->_requests->{$id} .= $data;
273             return;
274             };
275              
276             event 'process_report' => sub {
277             my ($kernel,$self,$data,$ip) = @_[KERNEL,OBJECT,ARG0,ARG1];
278             my @present = grep { defined $data->{$_} } @fields;
279             return unless scalar @present == scalar @fields;
280             # Build CPAN::Testers::Report with its various component facts.
281             my $metabase_report = eval { CPAN::Testers::Report->open(
282             resource => 'cpan:///distfile/' . $data->{distfile}
283             ); };
284              
285             return unless $metabase_report;
286              
287             $kernel->post( $self->session, $self->recv_event, $data, $ip )
288             if $self->recv_event;
289              
290             $metabase_report->add( 'CPAN::Testers::Fact::LegacyReport' => {
291             map { ( $_ => $data->{$_} ) } qw(grade osname osversion archname perl_version textreport)
292             });
293              
294             # TestSummary happens to be the same as content metadata
295             # of LegacyReport for now
296             $metabase_report->add( 'CPAN::Testers::Fact::TestSummary' =>
297             [$metabase_report->facts]->[0]->content_metadata()
298             );
299              
300             $metabase_report->close();
301              
302             $kernel->yield( 'submit_report', $metabase_report );
303             return;
304             };
305              
306             event 'submit_report' => sub {
307             my ($kernel,$self,$report) = @_[KERNEL,OBJECT,ARG0];
308             $kernel->post(
309             $self->queue->get_session_id,
310             'submit',
311             $report,
312             );
313             return;
314             };
315              
316             sub _load_id_file {
317             my $self = shift;
318              
319             open my $fh, '<', $self->id_file
320             or Carp::confess __PACKAGE__. ": could not read ID file '" . $self->id_file
321             . "'\n$!";
322              
323             my $data = JSON->new->decode( do { local $/; <$fh> } );
324              
325             my $profile = eval { Metabase::User::Profile->from_struct($data->[0]) }
326             or Carp::confess __PACKAGE__ . ": could not load Metabase profile\n"
327             . "from '" . $self->id_file . "':\n$@";
328              
329             my $secret = eval { Metabase::User::Secret->from_struct($data->[1]) }
330             or Carp::confess __PACKAGE__ . ": could not load Metabase secret\n"
331             . "from '" . $self->id_file . "':\n $@";
332              
333             $self->_set_profile( $profile );
334             $self->_set_secret( $secret );
335             return 1;
336             }
337              
338             no MooseX::POE;
339              
340             __PACKAGE__->meta->make_immutable;
341              
342             1;
343              
344             __END__
345              
346             =pod
347              
348             =encoding UTF-8
349              
350             =head1 NAME
351              
352             POE::Component::Metabase::Relay::Server - A Metabase relay server component
353              
354             =head1 VERSION
355              
356             version 0.34
357              
358             =head1 SYNOPSIS
359              
360             use strict;
361             use warnings;
362              
363             use POE qw[Component::Metabase::Relay::Server];
364              
365             my $test_httpd = POE::Component::Metabase::Relay::Server->spawn(
366             port => 8080,
367             id_file => shift,
368             dsn => 'dbi:SQLite:dbname=dbfile',
369             uri => 'https://metabase.example.foo/',
370             debug => 1,
371             );
372              
373             $poe_kernel->run();
374             exit 0;
375              
376             =head1 DESCRIPTION
377              
378             POE::Component::Metabase::Relay::Server is a relay server for L<Metabase>. It provides a listener
379             that accepts connections from L<Test::Reporter::Transport::Socket> based CPAN Testers and
380             relays the L<Storable> serialised data to L<Metabase> using L<POE::Component::Metabase::Client::Submit>.
381              
382             L<POE::Component::Client::HTTP> is used to submit reports usually, but if version C<0.06> of
383             L<POE::Component::Curl::Multi> is found to be installed, this will be used in preference. You can
384             disable this usage using the C<no_curl> option to C<spawn>.
385              
386             =for Pod::Coverage START
387              
388             =head1 CONSTRUCTOR
389              
390             =over
391              
392             =item C<spawn>
393              
394             Spawns a new component session and creates a SQLite database if it doesn't already exist.
395              
396             Takes a number of mandatory parameters:
397              
398             'id_file', the file path of a Metabase ID file;
399             'dsn', a DBI DSN to use to store the submission queue;
400             'uri', the uri of metabase server to submit to;
401              
402             and a number of optional parameters:
403              
404             'address', the address to bind the listener to, defaults to INADDR_ANY;
405             'port', the port to listen on, defaults to 0, which picks a random port;
406             'username', a DSN username if required;
407             'password', a DSN password if required;
408             'db_opts', a hashref of DBD options that is passed to POE::Component::EasyDBI;
409             'debug', enable debugging information;
410             'multiple', set to true to enable the Queue to use multiple PoCo-Client-HTTPs, default 0;
411             'no_relay', set to true to disable report submissions to the Metabase, default 0;
412             'no_curl', set to true to disable automatic usage of POE::Component::Curl::Multi, default 0;
413             'submissions', an int to control the number of parallel http clients ( used only if multiple == 1 ), default 10;
414             'session', a POE::Session alias or session ID to send events to;
415             'recv_event', an event to be triggered when reports are received by the relay;
416              
417             C<address> may be either an simple scalar value or an arrayref of addresses to bind to.
418              
419             If C<recv_event> is specified an event will be sent for every report received by the relay server.
420             Unless C<session> is specified this event will be sent to the parent session of the component.
421              
422             =back
423              
424             =head1 OUTPUT EVENTS
425              
426             If C<recv_event> is specified to C<spawn>, an event will be sent with the following:
427              
428             C<ARG0> will be a C<HASHREF> with the following keys:
429              
430             osversion
431             distfile
432             archname
433             textreport
434             osname
435             perl_version
436             grade
437              
438             C<ARG1> will be the IP address of the client that sent the report.
439              
440             If C<queue_event> is specified to C<spawn>, an event will be sent for particular changes in queue status
441              
442             =head1 AUTHOR
443              
444             Chris Williams <chris@bingosnet.co.uk>
445              
446             =head1 COPYRIGHT AND LICENSE
447              
448             This software is copyright (c) 2014 by Chris Williams.
449              
450             This is free software; you can redistribute it and/or modify it under
451             the same terms as the Perl 5 programming language system itself.
452              
453             =cut