File Coverage

blib/lib/POE/Component/PluginManager.pm
Criterion Covered Total %
statement 18 205 8.7
branch 0 58 0.0
condition 0 12 0.0
subroutine 6 28 21.4
pod 0 17 0.0
total 24 320 7.5


line stmt bran cond sub pod time code
1             package POE::Component::PluginManager::PluginAPI;
2              
3             # this is the plugin API.
4             # i decided to put it in its own class
5             # so that nobody can mess up with calling
6             # the wrong methods.
7             # it also made the design somewhat clearer.
8             sub new {
9 0     0     my ( $class, $kernel, $alias ) = @_;
10 0           my $self = {};
11 0           $self->{kernel} = $kernel;
12 0           $self->{alias} = $alias;
13 0           bless $self, $class;
14             }
15              
16             sub warning {
17 0     0     my $self = shift;
18 0           my $string = shift;
19 0           $self->{kernel}->post( $self->{alias}, 'plugin_warning', $string );
20              
21             # maybe i should rather use poe callbacks here?
22             }
23              
24             sub error {
25 0     0     my $self = shift;
26 0           my $ex = shift;
27 0           $self->{kernel}->post( $self->{alias}, 'plugin_error', $ex );
28             }
29              
30             package POE::Component::PluginManager;
31             our $VERSION = "0.67";
32              
33 1     1   29612 use strict;
  1         3  
  1         38  
34 1     1   6 use warnings;
  1         2  
  1         41  
35 1     1   842 use POE;
  1         65211  
  1         5  
36 1     1   119527 use Carp;
  1         2  
  1         87  
37 1     1   1252 use Data::Dumper;
  1         12181  
  1         89  
38 1     1   1096 use Class::Unload;
  1         5930  
  1         3138  
39              
40             # we need this to unload classes, otherwise
41             # plugins can't be "reloaded". Maybe in the
42             # future there should be an option to disable
43             # this feature, so that PluginManager doesn't
44             # necesarily depend on this module.
45             # However, its just working fine for me, so far.
46              
47             my $pluginmanager_shutdown = 0;
48             my $DEBUG = 0;
49             my $alias;
50              
51             sub debug {
52 0 0   0 0   print @_ if $DEBUG;
53             }
54              
55             sub new {
56 0     0 0   my $type = shift;
57 0 0         if ( @_ & 1 ) {
58 0           carp('PluginManager->new needs even number of options');
59             }
60 0           my %options = @_;
61              
62 0 0         if ( exists $options{'Alias'} ) { # the alias
63 0           $alias = $options{'Alias'};
64 0           delete $options{'Alias'};
65             }
66             else {
67 0           carp '[pluginmanager] Using default Alias \'pluginmanager\'';
68 0           $alias = 'pluginmanager';
69             }
70 0 0         if ( exists $options{'Debug'} ) { # debugging on/off
71 0           $DEBUG = $options{'Debug'};
72 0           delete $options{'Debug'};
73             }
74 0 0         if ( keys %options > 0 ) {
75 0           carp '[$alias]: Unrecognized options in new(): ' . join( ', ', keys %options );
76             }
77              
78             # Create a new session for ourself
79             POE::Session->create(
80              
81             # Our subroutines
82 0 0         'inline_states' => {
83              
84             # Maintenance events
85             '_start' => \&start, # initial startup
86             '_stop' => \&stop, # cleanup, if needed
87             '_dump' => \&_dump, # for debugging purposes
88             '_generate_event' => \&_generate_event, # for broadcasting events
89             '_child' => \&child, # to catch _child events
90             'shutdown' => \&component_shutdown, # shuts down the component
91             'unload' => \&unload_plugin, # unload a plugin
92             'load' => \&load_plugin, # load a plugin.
93             'show_plugin_table' => \&show_plugin_table, # sends a hash
94             'register' => \®ister, # registers your session
95             'unregister' => \&unregister, # unregisters your session
96             'unregister_all' => \&unregister_all, # unregisters all sessions.
97              
98             'add_plugin' => \&add_plugin, # internally used
99             'remove_plugin' => \&remove_plugin, # internally used
100             'plugin_error' => \&plugin_error, # internally used
101             'plugin_warning' => \&plugin_warning, # internally used
102             'relayed_warning'=> \&relayed_warning, # to delay warnings.
103             },
104             ) or die 'Unable to create a new session!';
105              
106             # Return success
107 0           return 1;
108             }
109              
110             sub child {
111 0     0 0   my ( $reason, $child, $return ) = @_[ ARG0 .. ARG2 ];
112 0           my $plugin_id = $child->ID();
113              
114 0 0         if ( $reason eq 'create' ) {
    0          
115              
116             # a new plugin session has been started.
117 0           POE::Kernel->yield( 'add_plugin', $plugin_id, $return );
118             }
119             elsif ( $reason eq 'lose' ) {
120              
121             # a plugin session has stopped
122 0           POE::Kernel->yield( 'remove_plugin', $plugin_id, $return );
123             }
124             }
125              
126             sub start {
127 0     0 0   debug( "[$alias] Plugin manager session has started. Alias: " . $alias . "\n" );
128 0           POE::Kernel->alias_set($alias);
129              
130             # make an alias, so that this session won't go away.
131             # maybe a feature for later versions: if no alias is specified, increase
132             # refcount and return session object.
133 0           my $plugin_api = POE::Component::PluginManager::PluginAPI->new( $_[KERNEL], $alias );
134              
135             # create the plugin API (seperate class)
136 0           $_[HEAP]->{plugin_api} = $plugin_api;
137              
138             }
139              
140             sub stop {
141 0     0 0   debug "[$alias] plugin manager session stopped.\n";
142             }
143              
144             sub add_plugin {
145 0     0 0   my $plugin_id = $_[ARG0];
146 0           my $arguments = $_[ARG1];
147              
148             # a new plugin has come to life. register it to the plugin table.
149 0 0         my ( $name, $longname, $license, $version, $author ) = @{$arguments} unless ref($arguments) ne 'ARRAY';
  0            
150 0 0 0       unless ( $name && $longname && $license && $version && $author ) {
      0        
      0        
      0        
151 0           warn "[$alias] $name did not correctly set all values (name, longname, license and version)";
152 0           POE::Kernel->yield( '_generate_event', 'plugin_invalid_values', $name );
153              
154             # if a plugin didn't specify at least the name correctly, we will not be able
155             # to unload it.
156             }
157 0           $_[HEAP]->{plugins}->{$name}->{id} = $plugin_id;
158 0           $_[HEAP]->{plugins}->{$name}->{longname} = $longname;
159 0           $_[HEAP]->{plugins}->{$name}->{license} = $license;
160 0           $_[HEAP]->{plugins}->{$name}->{version} = $version;
161 0           $_[HEAP]->{plugins}->{$name}->{author} = $author;
162 0           $_[HEAP]->{lookup}->{$plugin_id} = $name;
163              
164             # $_[HEAP]->{lookup} is a reverse lookup table for looking up plugin names by their ID
165 0           POE::Kernel->yield( '_generate_event', 'plugin_started', $name );
166             }
167              
168             sub remove_plugin {
169 0     0 0   my $plugin_id = $_[ARG0];
170 0           my $quit_message = $_[ARG1];
171 0           my $name = $_[HEAP]->{lookup}->{$plugin_id};
172              
173             # a plugin has died away.
174             # $quit_message is the returned value, if any.
175 0 0         $quit_message = "quit" unless $quit_message;
176 0 0         unless ($name) {
177 0           warn "[$alias] WARNING: plugin $plugin_id unregistered, but the pluginmanager didn't knew about it.";
178 0           warn "[$alias] WARNING: this might have been caused by a plugin not registering correctly.";
179             }
180 0           delete $_[HEAP]->{plugins}->{$name};
181 0           delete $_[HEAP]->{lookup}->{$plugin_id};
182 0           Class::Unload->unload($name);
183              
184             # all of this pretty printing is a bit superfluous.
185             # remove it.
186 0           my $spaces = 34 - length($name);
187 0           my $spacer = " " x $spaces;
188 0           debug "[$alias] Plugin unloaded: $name$spacer Quitmsg: $quit_message\n";
189 0           POE::Kernel->yield( '_generate_event', 'plugin_unloaded', $name, $quit_message );
190              
191             #check if we want to shutdown
192 0           my $plugin_count = 0;
193 0 0         if ($pluginmanager_shutdown) {
194 0           foreach ( keys %{ $_[HEAP]->{plugins} } ) {
  0            
195 0           $plugin_count++;
196             }
197 0 0         if ( $plugin_count == 0 ) {
198              
199             # there are no plugins left, shutdown
200             # shut down component here: remove alias
201 0           POE::Kernel->alias_remove($alias);
202 0           POE::Kernel->yield( '_generate_event', 'plugin_manager_shutdown', $plugin_count );
203 0           POE::Kernel->yield("unregister_all");
204             }
205             else {
206 0           debug "[$alias] waiting for plugins to shut down. Remaining: $plugin_count\n";
207 0           POE::Kernel->yield( '_generate_event', 'plugin_waiting', $plugin_count );
208             }
209             }
210             }
211              
212             sub load_plugin {
213              
214             # loads a plugin, parameter: Classname (f.ex YourProgram::Plugins::Foobarplugin)
215 0     0 0   my $plugin = $_[ARG0];
216 0           my $data = $_[ARG1];
217 0 0         warn "[$alias] warning: no plugin name specified" unless $plugin;
218 0           my $classname = $plugin; # the classname equals the plugin name supplied
219 0           my $filename = $classname;
220 0           $filename =~ s#::#/#g;
221 0           $filename .= ".pm";
222 0 0         if ( $_[HEAP]->{plugins}->{$classname} ) {
223 0           warn "[$alias] warning: plugin $classname already loaded!\n";
224 0           POE::Kernel->yield( '_generate_event', 'plugin_compile_failed', $classname, "plugin already loaded" );
225 0           return;
226             }
227              
228             #my $classname = ( split( /\./, $filename ) )[0];
229              
230 0           my $spaces = 40 - length($classname);
231 0           my $spacer = " " x $spaces;
232 0           my ( $name, $longname, $license, $version ); #name, longname, license, version (all strings)
233 0           debug "[$alias] loading $classname...$spacer compile: ";
234             # this makes the load_plugin signal re-entrant. Thanks to Tim Esselens for the patch.
235 0 0         if ($INC{$filename}) { delete $INC{$filename};
  0            
236 0           debug "[$alias] class was still in \@INC, removing...\n";
237             };
238 0           eval { require $filename; };
  0            
239 0 0         if ($@) {
240 0           debug "FAIL\n";
241 0           debug "[$alias] Error: $@\n";
242 0           POE::Kernel->yield( '_generate_event', 'plugin_compile_failed', $classname, $@ );
243 0           Class::Unload->unload($classname); # fixes the "attempt to reload $plugin" warnings
244             }
245             else {
246 0           debug "OK ";
247 0           debug "run: ";
248 0           eval { $classname->new( $_[HEAP]->{plugin_api}, $data ); };
  0            
249 0 0         if ($@) {
250 0           debug "FAIL\n";
251 0           debug "[$alias] Error: $@\n";
252 0           POE::Kernel->yield( '_generate_event', 'plugin_init_failed', $classname, $@ );
253 0           Class::Unload->unload($classname);
254             }
255             else {
256 0           debug "OK\n";
257             }
258             }
259             }
260              
261             sub unload_plugin {
262 0     0 0   my $plugin = $_[ARG0];
263 0           my $mode = $_[ARG1];
264 0           my $reason = $_[ARG2];
265              
266             # checking
267 0 0         warn "[$alias] warning: no plugin name specified" unless $plugin;
268 0 0         $mode = 'smart' unless $mode; # fallback to "smart" if no mode is specified
269 0 0         if ( exists $_[HEAP]->{plugins}->{$plugin} ) {
270 0           my $sid = $_[HEAP]->{plugins}->{$plugin}->{id};
271 0           POE::Kernel->post( $sid, 'shutdown', $mode, $reason );
272             }
273             else {
274 0           warn "[$alias] the plugin $plugin wasn't registered to the pluginmanager";
275 0           POE::Kernel->yield( '_generate_event', 'plugin_unload_failed', $plugin, "no such plugin" );
276             }
277             }
278              
279             sub plugin_error {
280              
281             # a plugin reported an error.
282 0     0 0   my $id = $_[SENDER]->ID();
283 0           my $name = $_[HEAP]->{lookup}->{$id};
284 0 0         $name = $id unless $name;
285              
286             # $name may be unspecified, if a plugin reports an error
287             # in _start, because the pluginmanager didn't have a chance
288             # to register the plugin yet. In this case, we simply return
289             # the session ID. Anyone interested could look up the corresponding
290             # plugin name in the plugin list later on.
291 0           my $error_hashref = $_[ARG0];
292 0           POE::Kernel->yield( '_generate_event', 'plugin_error', $name, $error_hashref );
293             }
294              
295             sub plugin_warning {
296 0     0 0   my $id = $_[SENDER]->ID();
297 0           my $name = $_[HEAP]->{lookup}->{$id};
298 0 0         if(!$name){ # were delaying a bit
299 0           print "unresolved warning!\n";
300 0           $_[KERNEL]->yield('relayed_warning', $id, $_[ARG0]);
301 0           return 1;
302             }
303              
304             # see above, the same goes for warnings.
305 0           my $string = $_[ARG0];
306 0           POE::Kernel->yield( '_generate_event', 'plugin_warning', $name, $string );
307             }
308              
309             sub relayed_warning { # a little helper function, that delays warnings until the _child
310 0     0 0   my $id = $_[ARG0]; # event had time to be dispatched
311 0           my $string = $_[ARG1];
312 0           my $name = $_[HEAP]->{lookup}->{$id};
313 0 0         $name = 'unresolved' unless $name;
314 0           POE::Kernel->yield( '_generate_event', 'plugin_warning', $name, $string );
315             }
316              
317             sub _dump {
318              
319             # for debugging purposes, outputting the plugin and the lookup table.
320 0     0     debug "[$alias] Dumping plugin table to STDOUT...\n";
321 0           debug Dumper $_[HEAP]->{plugins};
322 0           debug "[$alias] Dumping lookup table to STDOUT...\n";
323 0           debug Dumper $_[HEAP]->{lookup};
324             }
325              
326             sub component_shutdown {
327 0     0 0   debug "[$alias] received shutdown signal, shutting down all plugins...\n";
328 0           my $mode = $_[ARG0];
329 0 0         $mode = 'smart' unless $mode; # fallback
330 0           my $plugins_pending = 0;
331 0           $pluginmanager_shutdown = 1; # setting the global shutdown flag
332             # sending the shutdown signal to all plugins.
333             # then, when all plugins are shut down, the plugin manager will go away.
334 0           foreach my $plugin ( keys %{ $_[HEAP]->{plugins} } ) {
  0            
335 0           debug "shutting down $plugin, id: " . $_[HEAP]->{plugins}->{$plugin}->{id} . "\n";
336 0           POE::Kernel->post( $_[HEAP]->{plugins}->{$plugin}->{id}, 'shutdown', $mode, 'pluginmanager shutdown' );
337 0           $plugins_pending++;
338             }
339 0 0         if ( $plugins_pending == 0 ) {
340              
341             # no plugins pending, shut down immediately
342 0           POE::Kernel->alias_remove($alias);
343 0           POE::Kernel->yield( '_generate_event', 'plugin_manager_shutdown', 0 );
344             #POE::Kernel->delay("unregister_all", 1);
345 0           POE::Kernel->yield("unregister_all");
346             }
347             #$_[KERNEL]->delay("unregister_all", 1);
348             }
349              
350             sub show_plugin_table {
351              
352             # renamed from "show_plugin_list" to "show_plugin_table", since
353             # "list" implies we would send back a list, or an arrayref.
354 0     0 0   my $plugins = {};
355 0           foreach my $plugin ( keys %{ $_[HEAP]->{plugins} } ) {
  0            
356 0           $plugins->{$plugin}->{id} = $_[HEAP]->{plugins}->{$plugin}->{id};
357 0           $plugins->{$plugin}->{longname} = $_[HEAP]->{plugins}->{$plugin}->{longname};
358 0           $plugins->{$plugin}->{license} = $_[HEAP]->{plugins}->{$plugin}->{license};
359 0           $plugins->{$plugin}->{version} = $_[HEAP]->{plugins}->{$plugin}->{version};
360 0           $plugins->{$plugin}->{author} = $_[HEAP]->{plugins}->{$plugin}->{author};
361             }
362 0           POE::Kernel->yield( '_generate_event', 'plugin_table', $plugins );
363 0           return $plugins;
364             }
365              
366             sub register {
367 0     0 0   my $session = $_[ARG0];
368 0 0         if ( !$session ) {
369 0           $session = $_[SENDER]->ID();
370             }
371 0           $_[HEAP]->{RecvSessions}->{$session} = 1;
372 0           POE::Kernel->refcount_increment($session);
373              
374             # incrementing the refcount, so that the session can't go away
375             # as long as we are sending events.
376              
377             }
378              
379             sub unregister {
380 0     0 0   my $session = $_[ARG0];
381 0 0         if ( !$session ) {
382 0           $session = $_[SENDER]->ID();
383             }
384 0           delete $_[HEAP]->{RecvSessions}->{$session};
385 0           POE::Kernel->refcount_decrement($session);
386              
387             # now you can go away.
388             }
389             sub unregister_all {
390 0     0 0   foreach(keys %{$_[HEAP]->{RecvSessions}}){
  0            
391 0           $_[KERNEL]->refcount_decrement($_);
392             }
393 0           delete $_[HEAP]->{RecvSessions};
394             }
395              
396             sub _generate_event {
397              
398             #function to generate events. this events receives the event that is to be broad
399             #casted via ARG0 and ARG1 - $#_ are the arguments. then it iterates through
400             #the hash of registered sessions and sends the event to all sessions.
401 0     0     my $event = $_[ARG0];
402 0           my @arguments = @_[ ARG1 .. $#_ ];
403 0           while ( my ( $key, $value ) = each %{ $_[HEAP]->{RecvSessions} } ) {
  0            
404 0           POE::Kernel->post( $key, $event, @arguments );
405             }
406              
407             }
408             return 1;
409              
410             __END__