File Coverage

blib/lib/Circle/RootObj.pm
Criterion Covered Total %
statement 159 295 53.9
branch 17 66 25.7
condition 5 11 45.4
subroutine 34 52 65.3
pod 0 21 0.0
total 215 445 48.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2008-2014 -- leonerd@leonerd.org.uk
4              
5             package Circle::RootObj;
6              
7 4     4   25 use strict;
  4         9  
  4         113  
8 4     4   18 use warnings;
  4         7  
  4         97  
9 4     4   16 use base qw( Tangence::Object Circle::WindowItem );
  4         7  
  4         1808  
10              
11             our $VERSION = '0.173320';
12              
13 4     4   1844 use Class::Method::Modifiers;
  4         5713  
  4         214  
14              
15 4     4   24 use Carp;
  4         6  
  4         283  
16 4     4   1547 use YAML (); # 'Dump' and 'Load' are a bit generic; we'll call by FQN
  4         24212  
  4         84  
17              
18 4     4   1662 use Circle::Rule::Store;
  4         10  
  4         134  
19             require Circle::GlobalRules;
20              
21 4     4   25 use Circle::CommandInvocation;
  4         6  
  4         172  
22              
23 4         32 use Module::Pluggable sub_name => "net_types",
24             search_path => [ "Circle::Net" ],
25             only => qr/^Circle::Net::\w+$/, # Not inner ones
26 4     4   1889 force_search_all_paths => 1;
  4         31747  
27              
28             {
29             foreach my $class ( net_types ) {
30             ( my $file = "$class.pm" ) =~ s{::}{/}g;
31             require $file;
32             }
33             }
34              
35 4     4   461 use Data::Dump;
  4         8  
  4         280  
36              
37 4   33 4   23 use constant CIRCLERC => $ENV{CIRCLERC} || "$ENV{HOME}/.circlerc";
  4         58  
  4         3253  
38              
39             sub _nettype2class
40             {
41 2     2   6 my ( $type ) = @_;
42              
43 2         11 foreach ( __PACKAGE__->net_types ) {
44 4         11334 my $thistype = eval { $_->NETTYPE };
  4         20  
45 4 100 66     22 if( defined $thistype and $thistype eq $type ) {
46 2         7 return $_;
47             }
48             }
49              
50 0         0 return undef;
51             }
52              
53             sub new
54             {
55 3     3 0 145 my $class = shift;
56 3         13 my %args = @_;
57              
58 3 50       17 my $loop = delete $args{loop} or croak "Need a loop";
59              
60 3         44 my $self = $class->SUPER::new( %args );
61              
62 3         128 $self->{loop} = $loop;
63              
64 3         30 my $rulestore = $self->{rulestore} = Circle::Rule::Store->new();
65 3         21 Circle::GlobalRules::register( $rulestore );
66              
67 3   50     23 my $file = $args{config} // CIRCLERC;
68 3 50       82 if( -r $file ) {
69 0         0 my $config = YAML::LoadFile( $file );
70 0         0 $self->load_configuration( $config );
71             }
72              
73 3         16 return $self;
74             }
75              
76             sub add_network
77             {
78 2     2 0 4 my $self = shift;
79 2         5 my ( $class, $name ) = @_;
80              
81 2         6 my $loop = $self->{loop};
82              
83             # Late-loading to support out-of-tree classes so they don't have to declare
84             # in the .tan file
85 2         15 eval { Tangence::Class->for_perlname( $class ) } or
86 2 50 33     4 eval { $class->DECLARE_TANGENCE } or
  0         0  
87             croak "Unknown Tangence::Class for '$class' and can't lazy-load it";
88              
89 2         31 my $registry = $self->{registry};
90 2         12 my $newnet = $registry->construct(
91             $class,
92             tag => $name,
93             root => $self,
94             loop => $loop,
95             );
96              
97             $newnet->subscribe_event( destroy => sub {
98 0     0   0 my ( $newnet ) = @_;
99 0         0 $self->broadcast_sessions( "delete_item", $newnet );
100 0         0 $self->del_prop_networks( $name );
101 2         17 } );
102              
103 2         284 $self->fire_event( "network_added", $newnet );
104 2         238 $self->add_prop_networks( $name => $newnet );
105              
106 2         37 $self->broadcast_sessions( "new_item", $newnet );
107              
108 2         3014 return $newnet;
109             }
110              
111             sub del_network
112             {
113 0     0 0 0 my $self = shift;
114 0         0 my ( $network ) = @_;
115              
116 0         0 $network->destroy;
117             }
118              
119             use Circle::Collection
120             name => 'networks',
121             storage => {
122             list => sub {
123 0         0 my $self = shift;
124 0         0 my $networks = $self->get_prop_networks;
125 0         0 return map { { name => $_, type => $networks->{$_}->NETTYPE } } sort keys %$networks;
  0         0  
126             },
127              
128             get => sub {
129 2         4 my $self = shift;
130 2         4 my ( $name ) = @_;
131 2 50       11 my $network = $self->get_prop_networks->{$name} or return undef;
132 0         0 return { name => $name, type => $network->NETTYPE };
133             },
134              
135             add => sub {
136 2         5 my $self = shift;
137 2         5 my ( $name, $item ) = @_;
138              
139 2         15 my $class = _nettype2class( $item->{type} );
140              
141 2 50       6 defined $class or die "unrecognised network type '$item->{type}'\n";
142              
143 2         10 $self->add_network( $class, $name );
144             },
145              
146             del => sub {
147 0         0 my $self = shift;
148 0         0 my ( $name ) = @_;
149 0 0       0 my $network = $self->get_prop_networks->{$name} or return;
150              
151 0 0       0 $network->connected and die "still connected\n";
152              
153 0         0 $self->del_network( $network );
154             },
155             },
156             attrs => [
157             name => {},
158             type => { nomod => 1, default => "irc" },
159             ],
160             config => {
161             type => "hash",
162             load => sub {
163 0         0 my $self = shift;
164 0         0 my ( $name, $ynode ) = @_;
165 0         0 $self->get_prop_networks->{$name}->load_configuration( $ynode );
166             },
167             store => sub {
168 0         0 my $self = shift;
169 0         0 my ( $name, $ynode ) = @_;
170 0         0 $self->get_prop_networks->{$name}->store_configuration( $ynode );
171             },
172             },
173 4     4   1800 ;
  4         9  
  4         87  
174              
175             our %sessions;
176              
177             sub add_session
178             {
179 2     2 0 4 my $self = shift;
180 2         6 my ( $identity, $type ) = @_;
181              
182 2         77 eval "require $type";
183 2 50       9 die $@ if $@;
184              
185 2         6 my $registry = $self->{registry};
186              
187 2         10 my $session = $registry->construct(
188             $type,
189             root => $self,
190             identity => $identity,
191             );
192              
193 2         9 return $sessions{$identity} = $session;
194             }
195              
196             sub method_get_session
197             {
198 2     2 0 42179 my $self = shift;
199 2         5 my ( $ctx, $opts ) = @_;
200              
201 2         9 my $identity = $ctx->stream->identity;
202              
203 2 50       23 return $sessions{$identity} if exists $sessions{$identity};
204            
205 2         7 my $type = _session_type( $opts );
206              
207 2 50       8 defined $type or die "Cannot identify a session type\n";
208              
209 2         8 return $self->add_session( $identity, $type );
210             }
211              
212             sub broadcast_sessions
213             {
214 2     2 0 4 my $self = shift;
215 2         6 my ( $method, @args ) = @_;
216              
217 2         8 foreach my $session ( values %sessions ) {
218 2 50       15 $session->$method( @args ) if $session->can( $method );
219             }
220             }
221              
222             sub invoke_session
223             {
224 0     0 0 0 my $self = shift;
225 0         0 my ( $conn, $method, @args ) = @_;
226              
227 0         0 my $session = $sessions{$conn->identity};
228 0 0       0 return unless $session;
229              
230 0 0       0 $session->$method( @args ) if $session->can( $method );
231             }
232              
233             sub _session_type
234             {
235 2     2   5 my ( $opts ) = @_;
236 2         5 my %opts = map { $_ => 1 } @$opts;
  2         8  
237              
238 2 50       8 if( $opts{tabs} ) {
239 2         5 delete $opts{tabs};
240 2         981 require Circle::Session::Tabbed;
241 2         9 return Circle::Session::Tabbed::_session_type( \%opts );
242             }
243              
244 0         0 print STDERR "Need Session for options\n";
245 0         0 print STDERR " ".join( "|", sort keys %opts )."\n";
246              
247 0         0 return undef;
248             }
249              
250             use Circle::Collection
251             name => 'sessions',
252             storage => {
253             list => sub {
254 0         0 map { my $class = ref $sessions{$_}; $class =~ s/^Circle::Session:://;
  0         0  
  0         0  
255 0         0 { name => $_, type => $class } } sort keys %sessions;
256             },
257             },
258 4         44 attrs => [
259             name => {},
260             type => { nomod => 1 },
261             ],
262             commands => {
263             # Disable add modify del
264             add => undef, mod => undef, del => undef,
265             },
266             config => 0,
267 4     4   3128 ;
  4         8  
268              
269             sub command_session
270             : Command_description("Manage the current session")
271       0 0   {
272 4     4   21 }
  4         6  
  4         13  
273              
274             sub command_session_info
275             : Command_description("Show information about the session")
276             : Command_subof('session')
277             : Command_default()
278             {
279 0     0 0 0 my $self = shift;
280 0         0 my ( $cinv ) = @_;
281              
282 0         0 my $identity = $cinv->connection->identity;
283 0 0       0 my $session = defined $identity ? $sessions{$identity} : undef;
284              
285 0 0       0 unless( defined $session ) {
286 0         0 $cinv->responderr( "Cannot find a session for this identity" );
287 0         0 return;
288             }
289              
290 0         0 ( my $type = ref $session ) =~ s/^Circle::Session:://;
291              
292 0         0 $cinv->respond_table(
293             [
294             [ Type => $type ],
295             [ Identity => $identity ],
296             [ Items => scalar $session->items ],
297             ],
298             colsep => ": ",
299             );
300              
301 0         0 return;
302 4     4   1143 }
  4         9  
  4         21  
303              
304             sub command_session_clonefrom
305             : Command_description("Clone items from another session")
306             : Command_subof('session')
307             : Command_arg('name')
308             {
309 0     0 0 0 my $self = shift;
310 0         0 my ( $name, $cinv ) = @_;
311              
312 0         0 my $identity = $cinv->connection->identity;
313              
314 0 0       0 my $destsession = defined $identity ? $sessions{$identity} : undef or
    0          
315             return $cinv->responderr( "Cannot find a session for this identity" );
316              
317 0 0       0 my $srcsession = $sessions{$name} or
318             return $cinv->responderr( "Cannot find a session called '$name'" );
319              
320 0 0       0 eval { $destsession->clonefrom( $srcsession ); 1 } or
  0         0  
  0         0  
321             return $cinv->responderr( "Cannot clone $name into $identity - $@" );
322              
323 0         0 return;
324 4     4   1082 }
  4         6  
  4         20  
325              
326             sub command_eval
327             : Command_description("Evaluate a perl expression")
328             : Command_arg('expr', eatall => 1)
329             {
330 2     2 0 4 my $self = shift;
331 2         4 my ( $expr, $cinv ) = @_;
332              
333 2         4 my $connection = $cinv->connection;
334              
335 2         12 my $identity = $connection->identity;
336 2 50       12 my $session = defined $identity ? $sessions{$identity} : undef;
337              
338             my %pad = (
339             ROOT => $self,
340             LOOP => $self->{loop},
341 2         6 CONN => $connection,
342             ITEM => $cinv->invocant,
343             SESSION => $session,
344             );
345              
346 2         8 my $result = do {
347             local $SIG{__WARN__} = sub {
348 0     0   0 my $msg = $_[0];
349 0         0 $msg =~ s/ at \(eval \d+\) line \d+\.$//;
350 0         0 chomp $msg;
351 0         0 $cinv->respondwarn( $msg, level => 2 );
352 2         14 };
353              
354 2         13 eval join( "", map { "my \$$_ = \$pad{$_}; " } keys %pad ) . "$expr";
  10         197  
355             };
356              
357 2 50       7 if( $@ ) {
358 0         0 my $err = $@; chomp $err;
  0         0  
359 0         0 $cinv->responderr( "Died: $err" );
360             }
361             else {
362 2         5 my @lines;
363              
364             my $timedout;
365 2     0   36 local $SIG{ALRM} = sub { $timedout = 1; die };
  0         0  
  0         0  
366 2         7 eval {
367 2         17 alarm(5);
368 2         10 @lines = split m/\n/, Data::Dump::dump($result);
369 2         208 alarm(0);
370             };
371              
372 2 50       7 if( $timedout ) {
373 0         0 $cinv->responderr( "Failed - took too long to render results. Try something more specific" );
374 0         0 return;
375             }
376              
377 2 50       5 if( @lines > 20 ) {
378 0         0 @lines = ( @lines[0..18], "...", $lines[-1] );
379             }
380              
381 2 50       5 if( @lines == 1 ) {
382 2         11 $cinv->respond( "Result: $lines[0]" );
383             }
384             else {
385 0         0 $cinv->respond( "Result:" );
386 0         0 $cinv->respond( " $_" ) for @lines;
387             }
388             }
389              
390 2         12 return;
391 4     4   2393 }
  4         8  
  4         14  
392              
393             sub command_rerequire
394             : Command_description("Rerequire a perl module")
395             : Command_arg('module')
396             {
397 0     0 0 0 my $self = shift;
398 0         0 my ( $module, $cinv ) = @_;
399              
400             # This might be a module name Foo::Bar or a filename Foo/Bar.pm
401 0         0 my $filename;
402              
403 0 0       0 if( $module =~ m/::/ ) {
    0          
404 0         0 ( $filename = $module ) =~ s{::}{/}g;
405 0         0 $filename .= ".pm";
406             }
407             elsif( $module =~ m/^(.*)\.pm$/ ) {
408 0         0 $filename = $module;
409 0         0 ( $module = $1 ) =~ s{/}{::}g;
410             }
411             else {
412 0         0 return $cinv->responderr( "Unable to recognise if $module is a module name or a file name" );
413             }
414              
415 0 0       0 if( !exists $INC{$filename} ) {
416 0         0 return $cinv->responderr( "Module $module in file $filename isn't loaded" );
417             }
418              
419             {
420 0         0 local $SIG{__WARN__} = sub {
421 0     0   0 my $msg = $_[0];
422 0         0 $msg =~ s/ at \(eval \d+\) line \d+\.$//;
423 0         0 chomp $msg;
424 0         0 $cinv->respondwarn( $msg, level => 2 );
425 0         0 };
426              
427 4     4   1569 no warnings 'redefine';
  4         8  
  4         450  
428              
429 0         0 delete $INC{$filename};
430 0         0 eval { require $filename };
  0         0  
431             }
432              
433 0 0       0 if( $@ ) {
434 0         0 my $err = $@; chomp $err;
  0         0  
435 0         0 $cinv->responderr( "Died: $err" );
436             }
437             else {
438 0         0 $cinv->respond( "Reloaded $module from $filename" );
439             }
440              
441 0         0 return;
442 4     4   24 }
  4         7  
  4         15  
443              
444             sub commandable_parent
445             {
446 12     12 0 24 my $self = shift;
447 12         20 my ( $cinv ) = @_;
448              
449 12         36 return $sessions{$cinv->connection->identity};
450             }
451              
452             sub enumerate_items
453             {
454 0     0 0 0 my $self = shift;
455 0         0 my $networks = $self->get_prop_networks;
456 0         0 return { map { $_->enumerable_name => $_ } values %$networks };
  0         0  
457             }
458              
459             sub enumerable_name
460             {
461 0     0 0 0 return "";
462             }
463              
464             sub parent
465             {
466 8     8 0 30 return undef;
467             }
468              
469             sub command_delay
470             : Command_description("Run command after some delay")
471             : Command_arg('seconds')
472             : Command_arg('command', eatall => 1)
473             {
474 0     0 0 0 my $self = shift;
475 0         0 my ( $seconds, $text, $cinv ) = @_;
476              
477             # TODO: A CommandInvocant subclass that somehow prefixes its output so we
478             # know it's delayed output from earlier, so as not to confuse
479 0         0 my $subinv = $cinv->nest( $text );
480              
481 0 0       0 my $cmdname = $subinv->peek_token or
482             return $cinv->responderr( "No command given" );
483              
484 0         0 my $loop = $self->{loop};
485              
486             my $id = $loop->enqueue_timer(
487             delay => $seconds,
488             code => sub {
489 0     0   0 eval {
490 0         0 $subinv->invocant->do_command( $subinv );
491             };
492 0 0       0 if( $@ ) {
493 0         0 my $err = $@; chomp $err;
  0         0  
494 0         0 $cinv->responderr( "Delayed command $cmdname failed - $err" );
495             }
496             },
497 0         0 );
498              
499             # TODO: Store ID, allow list, cancel, etc...
500              
501 0         0 return;
502 4     4   1725 }
  4         10  
  4         21  
503              
504             ###
505             # Configuration management
506             ###
507              
508             sub command_config
509             : Command_description("Save configuration or change details about it")
510       0 0   {
511             # The body doesn't matter as it never gets run
512 4     4   528 }
  4         7  
  4         13  
513              
514             sub command_config_show
515             : Command_description("Show the configuration that would be saved")
516             : Command_subof('config')
517             : Command_default()
518             {
519 0     0 0 0 my $self = shift;
520 0         0 my ( $cinv ) = @_;
521              
522             # Since we're only showing config, only fetch it for the invocant
523 0         0 my $obj = $cinv->invocant;
524              
525 0 0       0 unless( $obj->can( "get_configuration" ) ) {
526 0         0 $cinv->respond( "No configuration" );
527 0         0 return;
528             }
529              
530 0         0 my $config = YAML::Dump( $obj->get_configuration );
531              
532 0         0 $cinv->respond( $_ ) for split m/\n/, $config;
533 0         0 return;
534 4     4   1031 }
  4         6  
  4         15  
535              
536             sub command_config_save
537             : Command_description("Save configuration to disk")
538             : Command_subof('config')
539             {
540 0     0 0 0 my $self = shift;
541 0         0 my ( $cinv ) = @_;
542              
543 0         0 my $file = CIRCLERC;
544 0         0 YAML::DumpFile( $file, $self->get_configuration );
545              
546 0         0 $cinv->respond( "Configuration written to $file" );
547 0         0 return;
548 4     4   703 }
  4         7  
  4         13  
549              
550             sub command_config_reload
551             : Command_description("Reload configuration from disk")
552             : Command_subof('config')
553             {
554 0     0 0 0 my $self = shift;
555 0         0 my ( $cinv ) = @_;
556              
557 0         0 my $file = CIRCLERC;
558 0         0 $self->load_configuration( YAML::LoadFile( $file ) );
559              
560 0         0 $cinv->respond( "Configuration loaded from $file" );
561 0         0 return;
562 4     4   659 }
  4         6  
  4         14  
563              
564             # For Configurable role
565             after load_configuration => sub {
566             my $self = shift;
567             my ( $ynode ) = @_;
568              
569             if( my $sessions_ynode = $ynode->{sessions} ) {
570             foreach my $sessionname ( keys %$sessions_ynode ) {
571             my $sessionnode = $sessions_ynode->{$sessionname};
572             my $type = $sessionnode->{type};
573              
574             my $session = $self->add_session( $sessionname, "Circle::Session::$type" );
575             $session->load_configuration( $sessionnode );
576             }
577             }
578             };
579              
580             after store_configuration => sub {
581             my $self = shift;
582             my ( $ynode ) = @_;
583              
584             my $sessions_ynode = $ynode->{sessions} ||= YAML::Node->new({});
585             %$sessions_ynode = ();
586              
587             foreach my $identity ( keys %sessions ) {
588             my $session = $sessions{$identity};
589              
590             my $sessionnode = $session->get_configuration;
591             $sessions_ynode->{$identity} = $sessionnode;
592              
593             unless( $sessionnode->{type} ) { # exists doesn't quite play ball
594             # Ensure it's first
595             unshift @{ tied(%$sessionnode)->keys }, 'type'; # I am going to hell for this
596             ( $sessionnode->{type} ) = (ref $session) =~ m/^Circle::Session::(.*)$/;
597             }
598             }
599             };
600              
601             0x55AA;