File Coverage

blib/lib/Tangence/Server.pm
Criterion Covered Total %
statement 327 336 97.3
branch 33 58 56.9
condition 10 16 62.5
subroutine 40 40 100.0
pod 4 21 19.0
total 414 471 87.9


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2011-2021 -- leonerd@leonerd.org.uk
5              
6             package Tangence::Server 0.33;
7              
8 9     9   44350 use v5.26;
  9         40  
9 9     9   67 use warnings;
  9         20  
  9         623  
10 9     9   60 use experimental 'signatures';
  9         21  
  9         98  
11              
12 9     9   1780 use base qw( Tangence::Stream );
  9         23  
  9         5511  
13              
14 9     9   102 use Carp;
  9         18  
  9         934  
15              
16 9     9   70 use Scalar::Util qw( weaken );
  9         22  
  9         685  
17             # TODO: since we're using meta 0.007 we could use set_subname from there but
18             # it's more awkward to use it this way
19 9     9   6323 use Sub::Util 1.40 qw( set_subname );
  9         4360  
  9         945  
20 9     9   70 use Feature::Compat::Try;
  9         22  
  9         92  
21              
22 9     9   1199 use Tangence::Constants;
  9         23  
  9         2738  
23 9     9   77 use Tangence::Types;
  9         21  
  9         833  
24 9     9   5573 use Tangence::Server::Context;
  9         33  
  9         647  
25              
26 9     9   82 use Struct::Dumb;
  9         18  
  9         105  
27             struct CursorObject => [qw( cursor obj )];
28              
29             # We will accept any version back to 3
30 9     9   1098 use constant VERSION_MINOR_MIN => 3;
  9         22  
  9         79290  
31              
32             =head1 NAME
33              
34             C - mixin class for building a C server
35              
36             =head1 SYNOPSIS
37              
38             This class is a mixin, it cannot be directly constructed
39              
40             package Example::Server;
41             use base qw( Base::Server Tangence::Server );
42              
43             sub new
44             {
45             my $class = shift;
46             my %args = @_;
47              
48             my $registry = delete $args{registry};
49              
50             my $self = $class->SUPER::new( %args );
51              
52             $self->registry( $registry );
53              
54             return $self;
55             }
56              
57             sub tangence_write
58             {
59             my $self = shift;
60             $self->write( $_[0] );
61             }
62              
63             sub on_read
64             {
65             my $self = shift;
66             $self->tangence_readfrom( $_[0] );
67             }
68              
69             =head1 DESCRIPTION
70              
71             This module provides mixin to implement a C server connection. It
72             should be mixed in to an object used to represent a single connection from a
73             client. It provides a location for the objects in server to store information
74             about the client connection, and coordinates passing messages between the
75             client and the objects in the server.
76              
77             This is a subclass of L which provides implementations of
78             the required C methods. A class mixing in C
79             must still provide the C method required for sending data to the
80             client.
81              
82             For an example of a class that uses this mixin, see
83             L.
84              
85             =cut
86              
87             =head1 PROVIDED METHODS
88              
89             The following methods are provided by this mixin.
90              
91             =cut
92              
93 9   100 9 0 68 sub subscriptions { shift->{subscriptions} ||= [] }
94 49   100 49 0 476 sub watches { shift->{watches} ||= [] }
95              
96             =head2 registry
97              
98             $server->registry( $registry )
99              
100             $registry = $server->registry
101              
102             Accessor to set or obtain the L object for the server.
103              
104             =cut
105              
106             sub registry
107             {
108 75     75 1 1016 my $self = shift;
109 75 100       252 $self->{registry} = shift if @_;
110 75         458 return $self->{registry};
111             }
112              
113             sub tangence_closed
114             {
115 1     1 1 12419 my $self = shift;
116 1         12 $self->SUPER::tangence_closed;
117              
118 1 50       8 if( my $subscriptions = $self->subscriptions ) {
119 1         3 foreach my $s ( @$subscriptions ) {
120 0         0 my ( $object, $event, $id ) = @$s;
121 0         0 $object->unsubscribe_event( $event, $id );
122             }
123              
124 1         3 undef @$subscriptions;
125             }
126              
127 1 50       6 if( my $watches = $self->watches ) {
128 1         23 foreach my $w ( @$watches ) {
129 4         14 my ( $object, $prop, $id ) = @$w;
130 4         17 $object->unwatch_property( $prop, $id );
131             }
132              
133 1         7 undef @$watches;
134             }
135              
136 1 50       5 if( my $cursors = $self->peer_hascursor ) {
137 1         5 foreach my $cursorobj ( values %$cursors ) {
138 0         0 $self->drop_cursorobj( $cursorobj );
139             }
140             }
141             }
142              
143 54         115 sub get_by_id ( $self, $id )
144 54     54 0 101 {
  54         121  
  54         78  
145             # Only permit the client to interact with objects they've already been
146             # sent, so they cannot gain access by inventing object IDs
147 54 100       256 $self->peer_hasobj->{$id} or
148             die "Access not allowed to object with id $id\n";
149              
150 48 50       172 my $obj = $self->registry->get_by_id( $id ) or
151             die "No such object with id $id\n";
152              
153 48         135 return $obj;
154             }
155              
156 4         10 sub handle_request_CALL ( $self, $token, $message )
  4         11  
157 4     4 0 11 {
  4         7  
  4         9  
158 4         34 my $ctx = Tangence::Server::Context->new( $self, $token );
159              
160 4         13 my $response;
161 4         48 try {
162 4         28 my $objid = $message->unpack_int();
163              
164 4         55 my $object = $self->get_by_id( $objid );
165              
166 3         28 $response = $object->handle_request_CALL( $ctx, $message );
167             }
168             catch ( $e ) {
169 1         5 return $ctx->responderr( $e );
170             }
171              
172 3         17 $ctx->respond( $response );
173             }
174              
175 3         8 sub handle_request_SUBSCRIBE ( $self, $token, $message )
  3         6  
176 3     3 0 9 {
  3         7  
  3         7  
177 3         24 my $ctx = Tangence::Server::Context->new( $self, $token );
178              
179 3         9 my $response;
180 3         9 try {
181 3         18 my $objid = $message->unpack_int();
182 3         16 my $event = $message->unpack_str();
183              
184 3         38 my $object = $self->get_by_id( $objid );
185              
186 2         11 weaken( my $weakself = $self );
187              
188             my $id = $object->subscribe_event( $event,
189             set_subname "__SUBSCRIBE($event)__" => sub {
190 2 50   2   11 $weakself or return;
191 2         5 my $object = shift;
192              
193 2         22 my $message = $object->generate_message_EVENT( $weakself, $event, @_ );
194             $weakself->request(
195             request => $message,
196 2         15 on_response => sub { "IGNORE" },
197 2         37 );
198             }
199 2         49 );
200              
201 2         7 push @{ $self->subscriptions }, [ $object, $event, $id ];
  2         23  
202              
203 2         17 $response = Tangence::Message->new( $self, MSG_SUBSCRIBED )
204             }
205             catch ( $e ) {
206 1         5 return $ctx->responderr( $e );
207             }
208              
209 2         14 $ctx->respond( $response );
210             }
211              
212 2         5 sub handle_request_UNSUBSCRIBE ( $self, $token, $message )
  2         5  
213 2     2 0 6 {
  2         4  
  2         4  
214 2         16 my $ctx = Tangence::Server::Context->new( $self, $token );
215              
216 2         7 my $response;
217 2         6 try {
218 2         13 my $objid = $message->unpack_int();
219 2         12 my $event = $message->unpack_str();
220              
221 2         27 my $object = $self->get_by_id( $objid );
222              
223 2 50       13 my $edef = $object->can_event( $event ) or
224             die "Object cannot respond to event $event\n";
225              
226             # Delete from subscriptions and obtain id
227 2         8 my $id;
228 2 50 33     8 @{ $self->subscriptions } = grep { $_->[0] == $object and $_->[1] eq $event and ( $id = $_->[2], 0 ) or 1 }
  2   33     34  
229 2         5 @{ $self->subscriptions };
  2         98  
230 2 50       8 defined $id or
231             die "Not subscribed to $event\n";
232              
233 2         20 $object->unsubscribe_event( $event, $id );
234              
235 2         34 $response = Tangence::Message->new( $self, MSG_OK )
236             }
237             catch ( $e ) {
238 0         0 return $ctx->responderr( $e );
239             }
240              
241 2         11 $ctx->respond( $response );
242             }
243              
244 5         14 sub handle_request_GETPROP ( $self, $token, $message )
  5         10  
245 5     5 0 13 {
  5         11  
  5         12  
246 5         41 my $ctx = Tangence::Server::Context->new( $self, $token );
247              
248 5         13 my $response;
249 5         14 try {
250 5         31 my $objid = $message->unpack_int();
251              
252 5         28 my $object = $self->get_by_id( $objid );
253              
254 4         47 $response = $object->handle_request_GETPROP( $ctx, $message )
255             }
256             catch ( $e ) {
257 1         6 return $ctx->responderr( $e );
258             }
259              
260 4         21 $ctx->respond( $response );
261             }
262              
263 4         8 sub handle_request_GETPROPELEM ( $self, $token, $message )
  4         9  
264 4     4 0 11 {
  4         7  
  4         9  
265 4         25 my $ctx = Tangence::Server::Context->new( $self, $token );
266              
267 4         10 my $response;
268 4         11 try {
269 4         23 my $objid = $message->unpack_int();
270              
271 4         18 my $object = $self->get_by_id( $objid );
272              
273 4         30 $response = $object->handle_request_GETPROPELEM( $ctx, $message )
274             }
275             catch ( $e ) {
276 0         0 return $ctx->responderr( $e );
277             }
278              
279 4         21 $ctx->respond( $response );
280             }
281              
282 9         17 sub handle_request_SETPROP ( $self, $token, $message )
  9         17  
283 9     9 0 16 {
  9         17  
  9         14  
284 9         59 my $ctx = Tangence::Server::Context->new( $self, $token );
285              
286 9         16 my $response;
287 9         21 try {
288 9         38 my $objid = $message->unpack_int();
289              
290 9         39 my $object = $self->get_by_id( $objid );
291              
292 8         63 $response = $object->handle_request_SETPROP( $ctx, $message )
293             }
294             catch ( $e ) {
295 2         10 return $ctx->responderr( $e );
296             }
297              
298 7         34 $ctx->respond( $response );
299             }
300              
301             *handle_request_WATCH = \&_handle_request_WATCHany;
302             *handle_request_WATCH_CUSR = \&_handle_request_WATCHany;
303 19         36 sub _handle_request_WATCHany ( $self, $token, $message )
  19         39  
304 19     19   44 {
  19         42  
  19         31  
305 19         145 my $ctx = Tangence::Server::Context->new( $self, $token );
306              
307 19         100 my ( $want_initial, $object, $prop );
308              
309 19         0 my $response;
310 19         54 try {
311 19         127 my $objid = $message->unpack_int();
312 19         99 $prop = $message->unpack_str();
313              
314 19         266 $object = $self->get_by_id( $objid );
315              
316 18 50       107 my $pdef = $object->can_property( $prop ) or
317             die "Object does not have property $prop\n";
318              
319 18         107 $self->_install_watch( $object, $prop );
320              
321 18 100       93 if( $message->code == MSG_WATCH ) {
    50          
322 12         61 $want_initial = $message->unpack_bool();
323              
324 12         89 $response = Tangence::Message->new( $self, MSG_WATCHING )
325             }
326             elsif( $message->code == MSG_WATCH_CUSR ) {
327 6         26 my $from = $message->unpack_int();
328              
329 6         20 my $m = "cursor_prop_$prop";
330 6         45 my $cursor = $object->$m( $from );
331 6         32 my $id = $self->message_state->{next_cursorid}++;
332              
333 6         35 $self->peer_hascursor->{$id} = CursorObject( $cursor, $object );
334             $response = Tangence::Message->new( $self, MSG_WATCHING_CUSR )
335             ->pack_int( $id )
336             ->pack_int( 0 ) # first index
337 6         45 ->pack_int( $#{ $object->${\"get_prop_$prop"} } ) # last index
  6         44  
  6         121  
338             }
339             }
340             catch ( $e ) {
341 1         5 return $ctx->responderr( $e );
342             }
343              
344 18         109 $ctx->respond( $response );
345              
346 18 100       144 $self->_send_initial( $object, $prop ) if $want_initial;
347             }
348              
349 8         14 sub _send_initial ( $self, $object, $prop )
  8         13  
350 8     8   15 {
  8         19  
  8         40  
351 8         26 my $m = "get_prop_$prop";
352 8 50       71 return unless( $object->can( $m ) );
353              
354 8         54 try {
355 8         45 my $value = $object->$m();
356 8         137 my $message = $object->generate_message_UPDATE( $self, $prop, CHANGE_SET, $value );
357             $self->request(
358             request => $message,
359 8     8   40 on_response => sub { "IGNORE" },
360 8         114 );
361             }
362             catch ( $e ) {
363 0         0 warn "$e during initial property fetch";
364             }
365             }
366              
367 4         9 sub handle_request_UNWATCH ( $self, $token, $message )
  4         10  
368 4     4 0 11 {
  4         8  
  4         6  
369 4         29 my $ctx = Tangence::Server::Context->new( $self, $token );
370              
371 4         12 my $response;
372 4         11 try {
373 4         22 my $objid = $message->unpack_int();
374 4         22 my $prop = $message->unpack_str();
375              
376 4         50 my $object = $self->get_by_id( $objid );
377              
378 4 50       21 my $pdef = $object->can_property( $prop ) or
379             die "Object does not have property $prop\n";
380              
381             # Delete from watches and obtain id
382 4         13 my $id;
383 4 50 66     14 @{ $self->watches } = grep { $_->[0] == $object and $_->[1] eq $prop and ( $id = $_->[2], 0 ) or 1 }
  13   66     153  
384 4         11 @{ $self->watches };
  4         20  
385 4 50       13 defined $id or
386             die "Not watching $prop\n";
387              
388 4         41 $object->unwatch_property( $prop, $id );
389              
390 4         74 $response = Tangence::Message->new( $self, MSG_OK );
391             }
392             catch ( $e ) {
393 0         0 return $ctx->responderr( $e );
394             }
395              
396 4         22 $ctx->respond( $response );
397             }
398              
399 12         22 sub handle_request_CUSR_NEXT ( $self, $token, $message )
  12         50  
400 12     12 0 22 {
  12         23  
  12         16  
401 12         54 my $cursor_id = $message->unpack_int();
402              
403 12         76 my $ctx = Tangence::Server::Context->new( $self, $token );
404              
405 12 50       49 my $cursorobj = $self->peer_hascursor->{$cursor_id} or
406             return $ctx->responderr( "No such cursor with id $cursor_id" );
407              
408 12         648 $cursorobj->cursor->handle_request_CUSR_NEXT( $ctx, $message );
409             }
410              
411 6         15 sub handle_request_CUSR_DESTROY ( $self, $token, $message )
  6         14  
412 6     6 0 16 {
  6         13  
  6         10  
413 6         48 my $cursor_id = $message->unpack_int();
414              
415 6         41 my $ctx = Tangence::Server::Context->new( $self, $token );
416              
417 6         31 my $cursorobj = delete $self->peer_hascursor->{$cursor_id};
418 6         65 $self->drop_cursorobj( $cursorobj );
419              
420 6         51 $ctx->respond( Tangence::Message->new( $self, MSG_OK ) );
421             }
422              
423 6         13 sub drop_cursorobj ( $self, $cursorobj )
424 6     6 0 13 {
  6         14  
  6         10  
425 6         251 my $m = "uncursor_prop_" . $cursorobj->cursor->prop->name;
426 6         168 $cursorobj->obj->$m( $cursorobj->cursor );
427             }
428              
429 9         20 sub handle_request_INIT ( $self, $token, $message )
  9         19  
430 9     9 0 20 {
  9         17  
  9         19  
431 9         55 my $major = $message->unpack_int();
432 9         40 my $minor_max = $message->unpack_int();
433 9         40 my $minor_min = $message->unpack_int();
434              
435 9         146 my $ctx = Tangence::Server::Context->new( $self, $token );
436              
437 9 50       41 if( $major != VERSION_MAJOR ) {
438 0         0 return $ctx->responderr( "Major version $major not available" );
439             }
440              
441             # Don't accept higher than the minor version we recognise
442 9 50       43 $minor_max = VERSION_MINOR if $minor_max > VERSION_MINOR;
443 9 50       33 $minor_min = VERSION_MINOR_MIN if $minor_min < VERSION_MINOR_MIN;
444              
445 9 50       40 if( $minor_max < $minor_min ) {
446 0         0 return $ctx->responderr( "No suitable minor version available" );
447             }
448              
449             # For unit tests or other synchronous cases, we need to set the version
450             # -before- we send the message. But we'd better construct the response
451             # message before setting the version, in case it makes a difference.
452 9         76 my $response = Tangence::Message->new( $self, MSG_INITED )
453             ->pack_int( $major )
454             ->pack_int( $minor_max );
455              
456 9         50 $self->minor_version( $minor_max );
457              
458 9         45 $ctx->respond( $response );
459             }
460              
461 9         39 sub handle_request_GETROOT ( $self, $token, $message )
  9         22  
462 9     9 0 50 {
  9         21  
  9         19  
463 9         67 my $identity = TYPE_ANY->unpack_value( $message );
464              
465 9         144 my $ctx = Tangence::Server::Context->new( $self, $token );
466              
467 9         81 $self->identity( $identity );
468              
469 9         74 my $root = $self->rootobj( $identity );
470              
471 9         59 my $response = Tangence::Message->new( $self, MSG_RESULT );
472 9         70 TYPE_OBJ->pack_value( $response, $root );
473              
474 9         106 $ctx->respond( $response );
475             }
476              
477 9         21 sub handle_request_GETREGISTRY ( $self, $token, $message )
  9         19  
478 9     9 0 22 {
  9         17  
  9         26  
479 9         66 my $ctx = Tangence::Server::Context->new( $self, $token );
480              
481 9 50       108 $self->permit_registry or
482             return $ctx->responderr( "This client is not permitted access to the registry" );
483              
484 9         68 my $response = Tangence::Message->new( $self, MSG_RESULT );
485 9         46 TYPE_OBJ->pack_value( $response, $self->registry );
486              
487 9         53 $ctx->respond( $response );
488             }
489              
490             my %change_values = (
491             on_set => CHANGE_SET,
492             on_add => CHANGE_ADD,
493             on_del => CHANGE_DEL,
494             on_push => CHANGE_PUSH,
495             on_shift => CHANGE_SHIFT,
496             on_splice => CHANGE_SPLICE,
497             on_move => CHANGE_MOVE,
498             );
499              
500 38         71 sub _install_watch ( $self, $object, $prop )
  38         68  
501 38     38   80 {
  38         78  
  38         65  
502 38         179 my $pdef = $object->can_property( $prop );
503 38         191 my $dim = $pdef->dimension;
504              
505 38         146 weaken( my $weakself = $self );
506              
507 38         115 my %callbacks;
508 38         88 foreach my $name ( @{ CHANGETYPES->{$dim} } ) {
  38         157  
509 106         280 my $how = $change_values{$name};
510             $callbacks{$name} = set_subname "__WATCH($prop:$name)__" => sub {
511 37 50   37   149 $weakself or return;
512 37         79 my $object = shift;
513              
514 37         276 my $message = $object->generate_message_UPDATE( $weakself, $prop, $how, @_ );
515             $weakself->request(
516             request => $message,
517 37         191 on_response => sub { "IGNORE" },
518 37         354 );
519 106         1331 };
520             }
521              
522 38         271 my $id = $object->watch_property( $prop, %callbacks );
523              
524 38         96 push @{ $self->watches }, [ $object, $prop, $id ];
  38         202  
525             }
526              
527 2         4 sub object_destroyed ( $self, $obj, @rest )
  2         5  
528 2     2 0 5 {
  2         5  
  2         4  
529 2 50       12 if( my $subs = $self->subscriptions ) {
530 2         5 my $i = 0;
531 2         45 while( $i < @$subs ) {
532 2         7 my $s = $subs->[$i];
533              
534 2 50       9 $i++, next unless $s->[0] == $obj;
535              
536 2         18 my ( undef, $event, $id ) = @$s;
537 2         16 $obj->unsubscribe_event( $event, $id );
538              
539 2         11 splice @$subs, $i, 1;
540             # No $i++
541             }
542             }
543              
544 2 50       11 if( my $watches = $self->watches ) {
545 2         5 my $i = 0;
546 2         8 while( $i < @$watches ) {
547 8         40 my $w = $watches->[$i];
548              
549 8 50       28 $i++, next unless $w->[0] == $obj;
550              
551 8         23 my ( undef, $prop, $id ) = @$w;
552 8         33 $obj->unwatch_property( $prop, $id );
553              
554 8         111 splice @$watches, $i, 1;
555             # No $i++
556             }
557             }
558              
559 2         28 $self->SUPER::object_destroyed( $obj, @rest );
560             }
561              
562             =head1 OVERRIDEABLE METHODS
563              
564             The following methods are provided but intended to be overridden if the
565             implementing class wishes to provide different behaviour from the default.
566              
567             =cut
568              
569             =head2 rootobj
570              
571             $rootobj = $server->rootobj( $identity )
572              
573             Invoked when a C message is received from the client, this method
574             should return a L as root object for the connection.
575              
576             The default implementation will return the object with ID 1; i.e. the first
577             object created in the registry.
578              
579             =cut
580              
581             sub rootobj
582             {
583 9     9 1 26 my $self = shift;
584              
585 9         37 return $self->registry->get_by_id( 1 );
586             }
587              
588             =head2 permit_registry
589              
590             $allow = $server->permit_registry
591              
592             Invoked when a C message is received from the client, this method
593             should return a boolean to indicate whether the client is allowed to access
594             the object registry.
595              
596             The default implementation always permits this, but an overridden method may
597             decide to disallow it in some situations. When disabled, a client will not be
598             able to gain access to any serverside objects other than the root object, and
599             (recursively) any other objects returned by methods, events or properties on
600             objects already known. This can be used as a security mechanism.
601              
602             =cut
603              
604 9     9 1 35 sub permit_registry { 1; }
605              
606             =head1 AUTHOR
607              
608             Paul Evans
609              
610             =cut
611              
612             0x55AA;