File Coverage

blib/lib/RPC/EPC/Service.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package RPC::EPC::Service;
2              
3 8     8   389004 use warnings;
  8         23  
  8         339  
4 8     8   45 use strict;
  8         20  
  8         292  
5 8     8   8609 use utf8;
  8         83  
  8         44  
6 8     8   590 use Carp;
  8         16  
  8         634  
7              
8 8     8   6485 use version; our $VERSION = qv('0.0.11');
  8         41259  
  8         62  
9              
10 8     8   1095 use base 'Exporter';
  8         17  
  8         1487  
11              
12             our @EXPORT = qw(
13             to_sexp
14             );
15              
16 8     8   10094 use Encode;
  8         103808  
  8         854  
17 8     8   14679 use AnyEvent;
  0            
  0            
18             use AnyEvent::Socket;
19             use AnyEvent::Handle;
20             use Data::SExpression;
21             use Data::SExpression::Symbol;
22             use B;
23             use Data::Dumper; # for debug
24              
25            
26             ##################################################
27             # sexp encoding
28             # (This code is based on Mojolicious's JSON library.)
29              
30             # Translate an argument object into S-expression text.
31             sub to_sexp {
32             my $arg = shift;
33             if (ref $arg eq 'HASH') {
34             return _to_sexp_hash($arg);
35             } elsif (ref $arg eq 'ARRAY') {
36             return _to_sexp_list($arg);
37             }
38             my $flags = B::svref_2object(\$arg)->FLAGS;
39             if ($flags & (B::SVp_IOK | B::SVp_NOK) && !($flags & B::SVp_POK)) {
40             return _to_sexp_num($arg);
41             } else {
42             return _to_sexp_string($arg);
43             }
44             }
45              
46              
47             # for elisp's prin1 escape
48             my %ESCAPE = (
49             '"' => '"',
50             '\\' => '\\',
51             );
52              
53             my %REVERSE;
54             for my $key (keys %ESCAPE) { $REVERSE{$ESCAPE{$key}} = "\\$key" }
55              
56             sub _to_sexp_string {
57             my $string = shift;
58             return "nil" unless $string;
59              
60             # Escape
61             $string =~ s/([\\\"])/$REVERSE{$1}/gs;
62              
63             $string = Encode::encode_utf8($string) if Encode::is_utf8($string);
64              
65             # Stringify
66             return "\"$string\"";
67             }
68              
69             sub _to_sexp_num {
70             return shift;
71             }
72              
73             sub _to_sexp_list {
74             my $list = shift;
75             my @out = map {to_sexp $_} @$list;
76             return "(" . join(" ", @out) . ")";
77             }
78              
79             sub _to_sexp_hash {
80             my $hash = shift;
81             my $out = [];
82             foreach my $key ( keys %$hash ) {
83             push @$out, "(".to_sexp($key)." . ".to_sexp($hash->{$key}).")";
84             }
85             return "(" . join(" ", @$out) . ")";
86             }
87              
88             ##################################################
89             # nil to undef
90              
91             our $NIL = Data::SExpression::Symbol->new("nil");
92              
93             sub _nil_to_undef {
94             my $arg = shift;
95             if (ref $arg eq 'HASH') {
96             return _nil_to_undef_hash($arg);
97             } elsif (ref $arg eq 'ARRAY') {
98             return _nil_to_undef_list($arg);
99             }
100             return if ($NIL eq $arg);
101             return $arg;
102             }
103              
104             sub _nil_to_undef_list {
105             my $list = shift;
106             for (my $i = 0; $i < @$list; $i++) {
107             $list->[$i] = _nil_to_undef($list->[$i]);
108             }
109             return $list;
110             }
111              
112             sub _nil_to_undef_hash {
113             my $hash = shift;
114             foreach my $key ( keys %$hash ) {
115             $hash->{$key} = _nil_to_undef($hash->{$key});
116             }
117             return $hash;
118             }
119              
120             sub _correct_method_spec {
121             my $a = shift;
122             if (ref($a) eq "CODE") {
123             return {'sub' => $a, 'arg_spec' => undef, 'docstring' => undef};
124             } elsif (ref($a) eq "ARRAY") {
125             my ($sub,$arg,$doc) = @$a;
126             return {'sub' => $sub, 'arg_spec' => $arg, 'docstring' => $doc};
127             } elsif (ref($a) eq "HASH") {
128             return $a;
129             } else {
130             return $a;
131             }
132             }
133              
134             sub _correct_method_specs {
135             my $methods = shift;
136             my $ret = {};
137             foreach my $key ( keys %$methods ) {
138             $ret->{$key} = _correct_method_spec( $methods->{$key} );
139             }
140             return $ret;
141             }
142              
143            
144             ##################################################
145             # Protocol Stacks
146              
147             sub new {
148             my ($class, $port, $methods) = @_;
149             my $cv = AnyEvent->condvar;
150             return bless { 'port' => $port,
151             'count' => 0,
152             'methods' => _correct_method_specs($methods),
153             'sessions' => {},
154             'wait' => $cv }, $class;
155             }
156              
157             sub _register_event_loop {
158             my ($self,$fh) = @_;
159              
160             my $ds = Data::SExpression->new({use_symbol_class=>1,fold_alists=>1});
161              
162             my $hdl; $hdl = new AnyEvent::Handle
163             (fh => $fh,
164             on_error => sub {
165             my ($hdl, $fatal, $msg) = @_;
166             AE::log warn => "got error $msg\n";
167             $hdl->destroy;
168             $self->{wait}->send;
169             });
170             $self->{handle} = $hdl;
171              
172             my $reading_buffer = "";
173             my @reader; @reader =
174             (line => sub {
175             my ($hdl, $incoming) = @_;
176             # print STDERR "INCOMING:$incoming";
177             $reading_buffer .= $incoming . "\n";
178             my $len = substr($reading_buffer, 0, 6);
179             if (!$len =~ /[0-9a-f]{6}/i) {
180             # print STDERR "Wrong length code: $reading_buffer\nAbort\n";
181             AE::log warn => "Wrong length code: $reading_buffer\n";
182             $hdl->destroy;
183             $self->{wait}->send;
184             return;
185             }
186             $len = hex($len);
187             my $body = substr($reading_buffer, 6);
188             if ($len > length $body) {
189             # try next lines
190             # print STDERR "lencode:$len / current:" . (length $body) . "\n";
191             $hdl->push_read(@reader);
192             return;
193             }
194             # print STDERR "Here len:$len / current:" . (length $body) . "\n";
195             if ($len < length $body) {
196             $reading_buffer = substr($body, $len+1);
197             $body = substr($body, 0, $len);
198             } else {
199             $reading_buffer = "";
200             }
201              
202             my ($text, $sexp);
203             eval {
204             ($sexp, $text) = $ds->read(Encode::decode_utf8($body));
205             };
206             # print STDERR "SEXP:".Dumper $sexp;
207             if ($sexp->[0]) {
208             if ($sexp->[0]->name eq "quit") {
209             $hdl->destroy;
210             $self->{wait}->send;
211             return;
212             }
213            
214             my $handler = $self->{handlers}->{shift(@$sexp)->name};
215             if ($handler) {
216             $handler->($sexp);
217             }
218             } else {
219             # print STDERR 'NULL:'.Dumper $sexp;
220             }
221             $hdl->push_read(@reader);
222             });
223              
224             $hdl->push_read(@reader);
225             }
226              
227             sub _handle_connection {
228             my ($self,$fh,$host,$port) = @_;
229            
230             my $handlers = {
231             'call' => sub { $self->_call(@_); },
232             'return' => sub { $self->_return(@_); },
233             'return-error' => sub { $self->_return_error(@_); },
234             'epc-error' => sub { $self->_epc_error(@_); },
235             'methods' => sub { $self->_query_methods(@_); },
236             };
237             $self->{handlers} = $handlers;
238             $self->_register_event_loop($fh);
239             }
240              
241             sub _uid {
242             my $self = shift;
243             return $self->{count}++;
244             }
245              
246             sub _send_message {
247             my ($self, $message) = @_;
248             my $hdl = $self->{handle};
249             my $len = length($message) + 1;
250             # print STDERR ">>> [$message]\n";
251             $hdl->push_write(sprintf("%06x%s\n", $len, $message));
252             }
253              
254             sub _call {
255             my ($self, $sexp) = @_;
256             # print STDERR "CALL:" . Dumper $sexp;
257             my $id = shift(@$sexp);
258             my $name = shift(@$sexp);
259             my $task = $self->{methods}->{$name}->{sub};
260             if ($task) {
261             my $args = _nil_to_undef($sexp->[0]);
262             eval {
263             my $ret = $task->($args);
264             if ((ref $ret) eq "AnyEvent::CondVar") {
265             $ret = $ret->recv;
266             }
267             $self->_send_message("(return $id ".to_sexp($ret).")");
268             };
269             if ($@) {
270             $self->_send_message("(return-error $id ".to_sexp($@).")");
271             }
272             } else {
273             $self->_send_message("(epc-error $id \"Not found the method: $name\")");
274             }
275             }
276              
277             sub _return {
278             my ($self, $sexp) = @_;
279             # print STDERR "RET:" . Dumper $sexp;
280             my $id = shift(@$sexp);
281             my $result = _nil_to_undef($sexp);
282             my $cv = $self->{sessions}->{$id};
283             if ($cv) {
284             delete $self->{sessions}->{$id};
285             $cv->send($result->[0]);
286             } else {
287             print STDERR "Not found ID : $id\n";
288             }
289             }
290              
291             sub _return_error {
292             my ($self, $sexp) = @_;
293             # print STDERR "ERR-RET:" . Dumper $sexp;
294             my $id = shift(@$sexp);
295             my $result = $sexp->[0];
296             my $cv = $self->{sessions}->{$id};
297             if ($cv) {
298             delete $self->{sessions}->{$id};
299             $cv->croak(['ERROR',$result]);
300             } else {
301             print STDERR "Not found ID : $id\n";
302             }
303             }
304              
305             sub _epc_error {
306             my ($self, $sexp) = @_;
307             # print STDERR "EPCERR-RET:" . Dumper $sexp;
308             my $id = shift(@$sexp);
309             my $result = $sexp->[0];
310             my $cv = $self->{sessions}->{$id};
311             if ($cv) {
312             delete $self->{sessions}->{$id};
313             $cv->croak(['EPC_ERROR',$result]);
314             } else {
315             print STDERR "Not found ID : $id\n";
316             }
317             }
318              
319             sub _query_methods {
320             my ($self, $sexp) = @_;
321             # print STDERR "METHODS:" . Dumper $sexp;
322             my $id = shift(@$sexp);
323             eval {
324             my @ret = ();
325             my $hash = $self->{methods};
326             # print STDERR "HASH:" . Dumper $hash;
327             foreach my $key ( keys %$hash ) {
328             my $method = $hash->{$key};
329             push @ret, [$key, $method->{'arg_spec'}, $method->{'docstring'}];
330             }
331              
332             $self->_send_message("(return $id ".to_sexp(\@ret).")");
333             };
334             if ($@) {
335             $self->_send_message("(return-error $id ".to_sexp($@).")");
336             }
337             }
338              
339             sub call_method {
340             my $self = shift;
341             my $name = shift;
342             my $args = shift;
343             my $cv = AnyEvent->condvar;
344             my $id = $self->_uid;
345             $self->{sessions}->{$id} = $cv;
346             $self->_send_message("(call $id $name ".(to_sexp $args).")");
347             return $cv;
348             }
349              
350             sub define_method {
351             my $self = shift;
352             my $name = shift;
353             $self->{methods}->{$name} = _correct_method_spec(\@_);
354             }
355              
356             sub query_methods {
357             my $self = shift;
358             my $cv = AnyEvent->condvar;
359             my $id = $self->_uid;
360             $self->{sessions}->{$id} = $cv;
361             $self->_send_message("(methods $id)");
362             return $cv;
363             }
364              
365             sub start {
366             my $self = shift;
367             my $server = tcp_server undef, $self->{port}, sub {
368             $self->_handle_connection(@_);
369             }, sub {
370             my ($fh, $thishost, $thisport) = @_;
371             binmode( STDOUT, ":unix" ); # immediate flush
372             print "$thisport\n"; # epc protocol
373             };
374             $self->{server} = $server;
375             $self->{wait}->recv;
376             }
377              
378             sub client_start {
379             my $self = shift;
380             my $host = "127.0.0.1";
381             my $cv = AnyEvent->condvar;
382             my $client = tcp_connect $host, $self->{port}, sub {
383             my ($fh) = @_;
384             if ($fh) {
385             $self->_handle_connection($fh, $host, $self->{port});
386             $cv->send;
387             } else {
388             $cv->croak(['Could not connect server.']);
389             }
390             };
391             $self->{client} = $client;
392             $cv->recv;
393             }
394              
395             sub stop {
396             my $self = shift;
397             $self->{handle}->push_shutdown;
398             }
399              
400            
401              
402             1; # Magic true value required at end of module
403             __END__