File Coverage

blib/lib/MooX/Async/Console/TCP.pm
Criterion Covered Total %
statement 30 84 35.7
branch 0 20 0.0
condition 0 12 0.0
subroutine 10 20 50.0
pod 1 1 100.0
total 41 137 29.9


line stmt bran cond sub pod time code
1             package MooX::Async::Console::TCP;
2              
3             =head1 NAME
4              
5             MooX::Async::Console::TCP - A TCP framing module for MooX::Async::Console
6              
7             =head1 SYNOPSIS
8              
9             See L
10              
11             =head1 DESCRIPTION
12              
13             A L subclass which listens on a TCP port. Each
14             connection is created as a L module
15             which interpets the byte stream and invokes L when a
16             command is ready to be executed.
17              
18             =head1 BUGS
19              
20             Certainly.
21              
22             =cut
23              
24 1     1   567 use Modern::Perl '2017';
  1         2  
  1         8  
25 1     1   161 use strictures 2;
  1         8  
  1         41  
26              
27 1     1   187 use Moo;
  1         1  
  1         6  
28 1     1   299 use Future;
  1         2  
  1         31  
29 1     1   6 use MooX::Async;
  1         2  
  1         6  
30 1     1   452 use MooX::Async::Console::TCPClient;
  1         4  
  1         38  
31 1     1   8 use Scalar::Util qw(blessed);
  1         2  
  1         60  
32 1     1   5 use namespace::clean;
  1         2  
  1         9  
33              
34             extends MooXAsync('Listener');
35              
36             with 'MooX::Role::Logger';
37              
38             =head1 PUBLIC INTERFACE
39              
40             =head2 ATTRIBUTES
41              
42             =over
43              
44             =item address (default: C<127.0.0.1>)
45              
46             The IP address to listen for connections on.
47              
48             =item port (default: C<$ENV{MXACPORT} // 0>)
49              
50             The TCP port to listen for connections on. The default is 0 which lets
51             the kernel select a port. The value in this attribute is updated when
52             the socket is bound.
53              
54             =back
55              
56             =cut
57              
58             has address => is => ro => default => '127.0.0.1';
59             has port => is => rwp => default => ($ENV{MXACPORT} // 0);
60              
61             =head2 EVENTS
62              
63             =over
64              
65             =item on_command
66              
67             Must be included in the constuctor. Invoked by this module to execute
68             a command. This interface is described in
69             L.
70              
71             Arguments:
72              
73             =over
74              
75             =item command
76              
77             Name of the command to execute.
78              
79             =item inform
80              
81             Coderef with which the command can send messages over the connection.
82              
83             =item args
84              
85             Arrayref of arguments to execute the command with.
86              
87             =item then
88              
89             L to complete or fail when the command is finished.
90              
91             =back
92              
93             =item on_terminate
94              
95             Invoked by this module when the connection has terminated.
96              
97             Arguments: none.
98              
99             =item on_success
100              
101             Invoked by this module when a command has completed successfully.
102              
103             Arguments: The result the command's L was completed with.
104              
105             If the implementation of this event returns a L then that is
106             used to provide the result sent to the client.
107              
108             =item on_failure
109              
110             Invoked by this module when a command has failed.
111              
112             Arguments: The L's failure.
113              
114             =back
115              
116             =cut
117              
118             event 'on_command';
119             event $_, sub {} for qw(on_terminate on_success on_failure);
120              
121             =head1 PRIVATE INTERFACE
122              
123             =head2 CONSTRUCTION
124              
125             Begins listening on the port when it's added to the loop.
126              
127             =cut
128              
129             after _add_to_loop => sub {
130             my $self = shift;
131             $self->listen(
132             host => $self->address,
133             service => $self->port,
134             socktype => 'stream',
135             )->then(sub {
136             $self->_set_port($self->read_handle->sockport);
137             $self->_logger->noticef('TCP Console listening on %s:%s', $self->address, $self->port);
138             Future->done();
139             })->get
140             };
141              
142             =pod
143              
144             Detaches all clients when removed from the loop.
145              
146             =cut
147              
148             before _remove_from_loop => sub {
149             if ($_[0]->children) {
150             $_[0]->_logger->warningf('TCP Console closed with %u active client[s]', scalar $_[0]->children);
151             $_[0]->_detach_client($_) for $_[0]->children;
152             }
153             };
154              
155             =pod
156              
157             C<_init>, which is used during the parent class
158             L's own construction, replaces its C<$args> with
159             a single entry of C.
160              
161             =cut
162              
163             around _init => sub {
164             my $orig = shift;
165             my $self = shift;
166              
167             =pod
168              
169             C contains a coderef to attach the client
170             implemented by L and handle its
171             L and L events.
172              
173             =cut
174              
175             my $line = sub { unshift @_, $self; goto \&__on_line };
176             my $close = sub { unshift @_, $self; goto \&__on_close};
177             %{$_[0]} = (handle_constructor => sub {
178             MooX::Async::Console::TCPClient->new(on_close => $close, on_line => $line);
179             });
180             $self->$orig(@_);
181             };
182              
183 1     1   820 use namespace::clean '__close';
  1         2  
  1         8  
184             sub __on_close {
185 0     0     my $self = shift;
186 0           $self->invoke_event(on_terminate =>);
187 0           $self->_logger->informf('Client disconnected from %s:%s', $_[0]->address, $_[0]->port);
188 0           $self->_detach_client($_[0]);
189             }
190              
191             =head3 Client's on_line event handler
192              
193             For the present this is extremely simple. The client types in a line
194             of text and ends it with newline. That line is broken up into a list
195             on whitespace and the first word in the list is the command name, the
196             rest its args.
197              
198             Only one command may be running at a time. This is enforced by the
199             C<$state> variable.
200              
201             =cut
202              
203 1     1   123 use namespace::clean '__line';
  1         2  
  1         4  
204             sub __on_line {
205 0     0     my $self = shift;
206 0           my $client = shift;
207 0           my ($cmd, @args) = split ' ', shift;
208 0           my $state; # for now - false nothing, true busy;
209 0 0         die 'One command at a time for now' if $state;
210 0           $state++;
211 0           my $quit;
212 0           $self->_logger->debugf('Received command %s %s', $cmd, \@args);
213              
214             =pod
215              
216             The L event handler is invoked with a new L.
217              
218             =cut
219              
220 0           my $future = $self->loop->new_future;
221             $self->adopt_future(
222             $future->followed_by(sub {
223             # Why is this useful?
224 0 0   0     return Future->fail($_[0]->failure) if $_[0]->failure;
225 0           my $command_future = $_[0]->get;
226 0 0         return Future->done($command_future->get) if $command_future->is_done;
227              
228             =pod
229              
230             Disconnecting the client is treated specially so that everything is
231             shutdown in an orderly manner.
232              
233             If the L which is given to the command handler is failed with
234             the word C then this is flagged using C<$quit> and the L
235             is replaced with a done L with an appropriate message
236             substituted.
237              
238             =cut
239              
240 0 0         return Future->fail($command_future->failure) if $command_future->failure ne 'quit';
241 0           $self->_logger->debugf('Requested disconnect');
242 0           $quit = 1;
243 0           return Future->done('disconnecting...');
244              
245             =pod
246              
247             After the L completes succesfully a message is returned to the
248             client containing its result.
249              
250             =cut
251              
252             })->then(sub {
253 0     0     my $r = $self->invoke_event(on_success => @_);
254 0 0 0       @_ = $r->get if blessed $r and $r->DOES('Future');
255 0 0         my $extra = @_ ? ' - ' . (join ' ', ('%s')x@_) : '';
256             # TODO: Figure out a better way to do this
257 0           $client->say(sprintf "OK$extra", Log::Any::Proxy::_stringify_params(@_));
258              
259             =pod
260              
261             If the C<$quit> flag is true the client is detached.
262              
263             =cut
264              
265 0 0         if ($quit) {
266 0           $self->_logger->informf('Client disconnecting from %s:%s', $client->address, $client->port);
267 0           $self->_detach_client($client);
268             }
269 0           Future->done(@_);
270              
271             =pod
272              
273             If the command handler's L was failed then a message is logged
274             and sent to the client.
275              
276             =cut
277              
278             })->else(sub {
279             # TODO: @_ may not end with a hash
280 0     0     my ($message, $category, %args) = @_;
281 0           local $self->_logger->context->{ex} = $message;
282 0           $self->invoke_event(on_failure => %args);
283 0 0 0       if ($category and $category eq 'console'
      0        
      0        
284             and $message and $message eq "Unknown command: $cmd") {
285 0           my $max = 1024;
286 0           chomp $message;
287 0           $self->_logger->debug($message);
288 0 0         $message = substr($message, 0, $max-5) . " ..." if length $message > $max;
289 0           $client->say($message);
290             } else {
291 0           $self->_logger->noticef('Command %s failed: %s', $cmd, $args{ex});
292 0           $client->say(sprintf 'Command %s failed: %s', $cmd, $args{ex});
293             }
294 0           Future->done(@_);
295 0     0     })->on_ready(sub { $state-- }));
  0            
296             $self->invoke_event(on_command =>
297             command => $cmd,
298 0     0     inform => sub { $client->say(join "\n", @_) },
299 0           args => \@args,
300             then => $future);
301 0           return;
302             }
303              
304             =head2 METHODS
305              
306             =over
307              
308             =item _attach_client($client)
309              
310             =item _detach_client($client)
311              
312             Add & remove the new client as a child of this notifier.
313              
314             =back
315              
316             =cut
317              
318             sub _attach_client {
319 0     0     $_[0]->_logger->debugf('Attaching TCP client %s', $_[1]);
320 0           $_[0]->add_child($_[1]);
321             }
322              
323             sub _detach_client {
324 0     0     $_[0]->_logger->debugf('Detaching TCP client %s', $_[1]);
325 0 0         $_[1]->flush unless $_[1]->{read_eof};
326 0           $_[0]->remove_child($_[1]);
327             }
328              
329             =head2 EVENTS
330              
331             =over
332              
333             =item on_accept
334              
335             Implemented by this module, attaches the new client which has connected.
336              
337             =back
338              
339             =cut
340              
341             sub on_accept {
342 0     0 1   my $self = shift;
343 0           my ($stream) = @_;
344 0           $self->_logger->informf('New client connected from %s:%s', $stream->address, $stream->port);
345 0           $self->_attach_client($stream);
346             }
347              
348             1;
349              
350             =head1 SEE ALSO
351              
352             L
353              
354             L
355              
356             =head1 AUTHOR
357              
358             Matthew King
359              
360             =cut