File Coverage

blib/lib/Argon/Server.pm
Criterion Covered Total %
statement 78 95 82.1
branch 3 8 37.5
condition 1 2 50.0
subroutine 25 29 86.2
pod 5 8 62.5
total 112 142 78.8


line stmt bran cond sub pod time code
1             package Argon::Server;
2             # ABSTRACT: Base class for Argon server objects
3             $Argon::Server::VERSION = '0.18';
4              
5 1     1   570 use strict;
  1         2  
  1         28  
6 1     1   5 use warnings;
  1         1  
  1         29  
7 1     1   4 use Carp;
  1         1  
  1         45  
8 1     1   5 use Moose;
  1         1  
  1         7  
9 1     1   5953 use Try::Catch;
  1         623  
  1         46  
10 1     1   7 use AnyEvent;
  1         2  
  1         21  
11 1     1   327 use AnyEvent::Socket qw(tcp_server);
  1         10329  
  1         89  
12 1     1   10 use Path::Tiny 'path';
  1         2  
  1         39  
13 1     1   5 use Argon::SecureChannel;
  1         2  
  1         24  
14 1     1   6 use Argon::Constants qw(:commands);
  1         2  
  1         149  
15 1     1   8 use Argon::Log;
  1         2  
  1         55  
16 1     1   5 use Argon::Message;
  1         2  
  1         21  
17 1     1   4 use Argon::Util qw(K param);
  1         3  
  1         738  
18              
19             with qw(Argon::Encryption);
20              
21              
22             has host => (
23             is => 'rw',
24             isa => 'Maybe[Str]',
25             );
26              
27              
28             has port => (
29             is => 'rw',
30             isa => 'Maybe[Int]',
31             );
32              
33             has fh => (
34             is => 'rw',
35             isa => 'FileHandle',
36             init_arg => undef,
37             );
38              
39             has handler => (
40             is => 'rw',
41             isa => 'HashRef',
42             default => sub {{}},
43             );
44              
45             has client => (
46             is => 'rw',
47             isa => 'HashRef',
48             default => sub {{}},
49             );
50              
51             has addr => (
52             is => 'rw',
53             isa => 'HashRef',
54             default => sub {{}},
55             );
56              
57              
58             sub start {
59 1     1 1 3 my $self = shift;
60 1         5 $self->listen;
61             }
62              
63              
64             sub listen {
65 1     1 1 3 my $self = shift;
66 1         7 $self->configure;
67              
68 1         32 tcp_server $self->host, $self->port,
69             K('_accept', $self),
70             K('_prepare', $self);
71             }
72              
73              
74             sub configure {
75 1     1 1 3 my $self = shift;
76 1         7 $self->handles($PING, K('_ping', $self));
77             }
78              
79              
80             sub handles {
81 1     1 1 4 my ($self, $cmd, $cb) = @_;
82 1   50     40 $self->handler->{$cmd} ||= [];
83 1         3 push @{$self->handler->{$cmd}}, $cb;
  1         30  
84             }
85              
86             sub get_addr {
87 1     1 0 4 my ($self, $msg) = @_;
88             exists $self->addr->{$msg->id}
89 1 50       112 && $self->addr->{$msg->id};
90             }
91              
92              
93             sub send {
94 1     1 1 6 my ($self, $msg) = @_;
95 1         7 my $addr = $self->get_addr($msg);
96              
97 1 50       14 unless ($addr) {
98 0         0 log_debug 'message %s (%s) has no connected client', $msg->id, $msg->cmd;
99 0         0 return;
100             }
101              
102             try {
103 1     1   85 $self->client->{$addr}->send($msg);
104             }
105             catch {
106 0     0   0 log_note 'unable to send message %s (%s) to %s: %s', $msg->id, $msg->cmd, $addr, $_;
107 0         0 $self->unregister_client($addr);
108 1         19 };
109              
110 1         124 delete $self->addr->{$msg->id};
111             }
112              
113             sub register_client {
114 1     1 0 4 my ($self, $addr, $fh) = @_;
115 1         39 $self->client->{$addr} = Argon::SecureChannel->new(
116             fh => $fh,
117             key => $self->key,
118             on_msg => K('_on_client_msg', $self, $addr),
119             on_err => K('_on_client_err', $self, $addr),
120             on_close => K('_on_client_close', $self, $addr),
121             );
122             }
123              
124             sub unregister_client {
125 0     0 0 0 my ($self, $addr) = @_;
126 0         0 delete $self->client->{$addr};
127              
128 0         0 foreach (keys %{$self->addr}) {
  0         0  
129 0 0       0 if ($self->addr->{$_} eq $addr) {
130 0         0 delete $self->addr->{$_};
131             }
132             }
133             }
134              
135             sub _prepare {
136 1     1   5 my ($self, $fh, $host, $port) = @_;
137 1 50       5 if ($fh) {
138 1         7 log_info 'Listening on %s:%d', $host, $port;
139 1         142 $self->host($host);
140 1         31 $self->port($port);
141 1         33 $self->fh($fh);
142             } else {
143 0         0 croak "socket error: $!";
144             }
145              
146 1         4 return;
147             }
148              
149             sub _accept {
150 1     1   4 my ($self, $fh, $host, $port) = @_;
151 1         5 my $addr = "$host:$port";
152 1         6 log_trace 'New connection from %s', $addr;
153 1         81 $self->register_client($addr, $fh);
154 1         9 return;
155             }
156              
157             sub _on_client_msg {
158 1     1   5 my ($self, $addr, $msg) = @_;
159 1         43 $self->addr->{$msg->id} = $addr;
160 1         5 $_->($addr, $msg) foreach @{$self->handler->{$msg->cmd}};
  1         41  
161             }
162              
163             sub _on_client_err {
164 0     0   0 my ($self, $addr, $err) = @_;
165 0         0 log_info '[client %s] error: %s', $addr, $err;
166 0         0 $self->unregister_client($addr);
167             }
168              
169             sub _on_client_close {
170 0     0   0 my ($self, $addr) = @_;
171 0         0 log_debug '[client %s] disconnected', $addr;
172 0         0 $self->unregister_client($addr);
173             }
174              
175             sub _ping {
176 1     1   5 my ($self, $addr, $msg) = @_;
177 1         9 $self->send($msg->reply(cmd => $ACK));
178             }
179              
180             __PACKAGE__->meta->make_immutable;
181              
182             1;
183              
184             __END__
185              
186             =pod
187              
188             =encoding UTF-8
189              
190             =head1 NAME
191              
192             Argon::Server - Base class for Argon server objects
193              
194             =head1 VERSION
195              
196             version 0.18
197              
198             =head1 SYNOPSIS
199              
200             use Moose;
201             use Argon::Constants ':commands';
202             use Argon::Server;
203              
204             extends 'Argon::Server';
205              
206             after configure => sub{
207             my $self = shift;
208             $self->handles($SOME_COMMAND, K('_handler_method_name', $self));
209             };
210              
211             =head1 DESCRIPTION
212              
213             Provides TCP listener services for Ar classes.
214              
215             =head1 ATTRIBUTES
216              
217             =head2 keyfile
218              
219             Path to the file containing the encryption pass phrase. Inherited from
220             L<Argon::Encryption>.
221              
222             =head2 host
223              
224             The hostname or interface on which to listen. Defaults to C<127.0.0.1>.
225              
226             =head2 port
227              
228             The port on which the server should listen. If not specified, an OS-assigned
229             port is used and the attribute is set once the listening socket has been
230             configured.
231              
232             =head1 METHODS
233              
234             =head2 start
235              
236             Starts the manager.
237              
238             =head2 listen
239              
240             Creates the listener socket. Called by L</start>.
241              
242             =head2 configure
243              
244             Classes inheriting C<Argon::Server> register protocol verb handlers with
245             the L<Argon::Server/handles> method. The C<configure> method provides a
246             trigger for registering actions during start up.
247              
248             after configure => sub{
249             my $self = shift;
250             $self->handles($ACTION, K('_handler', $self));
251             };
252              
253             =head2 handles
254              
255             Registers a handler for a protocol command verb.
256              
257             $self->handles($ACTION, K('_handler_method'), $self));
258              
259             See L<Argon::Constants/:commands>.
260              
261             =head2 send
262              
263             Sends a reply L<Argon::Message>. Emits a warning and returns early if the
264             message's id does not match one sent by an existing client.
265              
266             =head1 AUTHOR
267              
268             Jeff Ober <sysread@fastmail.fm>
269              
270             =head1 COPYRIGHT AND LICENSE
271              
272             This software is copyright (c) 2017 by Jeff Ober.
273              
274             This is free software; you can redistribute it and/or modify it under
275             the same terms as the Perl 5 programming language system itself.
276              
277             =cut