File Coverage

blib/lib/App/Pocosi.pm
Criterion Covered Total %
statement 42 336 12.5
branch 0 140 0.0
condition 0 12 0.0
subroutine 16 46 34.7
pod 1 14 7.1
total 59 548 10.7


line stmt bran cond sub pod time code
1             package App::Pocosi;
2             BEGIN {
3 1     1   1029 $App::Pocosi::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 1     1   13 $App::Pocosi::VERSION = '0.03';
7             }
8              
9 1     1   5 use strict;
  1         2  
  1         25  
10 1     1   5 use warnings FATAL => 'all';
  1         2  
  1         68  
11              
12             # we want instant child process reaping
13 4     4 1 124620 sub POE::Kernel::USE_SIGCHLD () { return 1 }
14              
15 1     1   5 use App::Pocosi::Status;
  1         2  
  1         24  
16 1     1   869 use Class::Load qw(try_load_class);
  1         29187  
  1         55  
17 1     1   7 use Fcntl qw(O_CREAT O_EXCL O_WRONLY);
  1         2  
  1         49  
18 1     1   5 use File::Glob ':glob';
  1         3  
  1         219  
19 1     1   5 use File::Spec::Functions 'rel2abs';
  1         2  
  1         37  
20 1     1   5 use IO::Handle;
  1         1  
  1         37  
21 1     1   5 use IRC::Utils qw(decode_irc);
  1         1  
  1         31  
22 1     1   953 use Net::Netmask;
  1         5465  
  1         87  
23 1     1   760 use POE;
  1         32709  
  1         7  
24 1     1   8392 use POSIX 'strftime';
  1         2  
  1         10  
25 1     1   64 use Scalar::Util 'looks_like_number';
  1         2  
  1         4365  
26              
27             sub new {
28 0     0 0   my ($package, %args) = @_;
29 0           return bless \%args, $package;
30             }
31              
32             sub run {
33 0     0 0   my ($self) = @_;
34              
35             # we print IRC output, which will be UTF-8
36 0           binmode $_, ':utf8' for (*STDOUT, *STDERR);
37              
38 0 0         if ($self->{list_plugins}) {
39 0           require Module::Pluggable;
40 0           Module::Pluggable->import(
41             sub_name => '_available_plugins',
42             search_path => 'POE::Component::Server::IRC::Plugin',
43             );
44 0           for my $plugin (sort $self->_available_plugins()) {
45 0           $plugin =~ s/^POE::Component::Server::IRC::Plugin:://;
46 0           print $plugin, "\n";
47             }
48 0           return;
49             }
50              
51 0           $self->_setup();
52              
53 0 0         if ($self->{check_cfg}) {
54 0           print "The configuration is valid and all modules could be compiled.\n";
55 0           return;
56             }
57              
58 0 0         if ($self->{daemonize}) {
59 0           require Proc::Daemon;
60 0           eval {
61 0           Proc::Daemon::Init->();
62 0 0         if (defined $self->{log_file}) {
63 0 0         open STDOUT, '>>:encoding(utf8)', $self->{log_file}
64             or die "Can't open $self->{log_file}: $!\n";
65 0 0         open STDERR, '>>&STDOUT' or die "Can't redirect STDERR: $!\n";
66 0           STDOUT->autoflush(1);
67             }
68 0           $poe_kernel->has_forked();
69             };
70 0           chomp $@;
71 0 0         die "Can't daemonize: $@\n" if $@;
72             }
73              
74 0 0         if (defined $self->{pid_file}) {
75 0 0         sysopen my $fh, $self->{pid_file}, O_CREAT|O_EXCL|O_WRONLY
76             or die "Can't create pid file or it already exists. Pocosi already running?\n";
77 0           print $fh "$$\n";
78 0           close $fh;
79             }
80              
81             POE::Session->create(
82 0           object_states => [
83             $self => [qw(
84             _start
85             sig_die
86             sig_int
87             sig_term
88             ircd_plugin_add
89             ircd_plugin_del
90             ircd_plugin_error
91             ircd_plugin_status
92             ircd_shutdown
93             )],
94             ],
95             );
96              
97 0           $poe_kernel->run();
98 0 0         unlink $self->{pid_file} if defined $self->{pid_file};
99 0           return;
100             }
101              
102             sub _setup {
103 0     0     my ($self) = @_;
104              
105 0 0         if (defined $self->{cfg}{pid_file}) {
106 0           $self->{pid_file} = rel2abs(bsd_glob(delete $self->{cfg}{pid_file}));
107             }
108              
109 0 0         if (defined $self->{cfg}{log_file}) {
110 0           my $log = rel2abs(bsd_glob(delete $self->{cfg}{log_file}));
111 0 0         open my $fh, '>>', $log or die "Can't open $log: $!\n";
112 0           close $fh;
113 0           $self->{log_file} = $log;
114             }
115              
116 0 0         if (!$self->{no_color}) {
117 0           require Term::ANSIColor;
118 0           Term::ANSIColor->import();
119             }
120              
121 0 0         if (defined $self->{cfg}{lib}) {
122 0 0 0       if (ref $self->{cfg}{lib} eq 'ARRAY' && @{ $self->{cfg}{lib} }) {
  0            
123 0           unshift @INC, map { rel2abs(bsd_glob($_)) } @{ delete $self->{cfg}{lib} };
  0            
  0            
124             }
125             else {
126 0           unshift @INC, rel2abs(bsd_glob(delete $self->{cfg}{lib}));
127             }
128             }
129              
130 0           $self->_load_classes();
131 0           return;
132             }
133              
134             sub _load_classes {
135 0     0     my ($self) = @_;
136 0           my $cfg = $self->{cfg};
137              
138 0 0         for my $plug_spec (@{ $cfg->{plugins} || [] }) {
  0            
139 0           $self->_load_plugin($plug_spec);
140             }
141              
142 0 0         if (!defined $cfg->{config}) {
143 0           die "No 'config' parameter found in config file\n";
144             }
145              
146 0 0         if (defined $cfg->{class}) {
147 0           $cfg->{class} = _load_either_class(
148             "POE::Component::Server::IRC::$cfg->{class}",
149             $cfg->{class},
150             );
151             }
152             else {
153 0           $cfg->{class} = 'POE::Component::Server::IRC';
154 0           my ($success, $error) = try_load_class($cfg->{class});
155 0 0         chomp $error if defined $error;
156 0 0         die "Can't load class $cfg->{class}: $error\n" if !$success;
157             }
158              
159 0           return;
160             }
161              
162             # find out the canonical class name for the plugin and load it
163             sub _load_plugin {
164 0     0     my ($self, $plug_spec) = @_;
165              
166 0 0         return if defined $plug_spec->[2];
167 0           my ($class, $args) = @$plug_spec;
168 0 0         $args = {} if !defined $args;
169              
170 0           my $canonclass = _load_either_class(
171             "POE::Component::Server::IRC::Plugin::$class",
172             $class,
173             );
174              
175 0           $plug_spec->[1] = $args;
176 0           $plug_spec->[2] = $canonclass;
177 0           return;
178             }
179              
180             # create plugins, spawn components, and connect to IRC
181             sub _start {
182 0     0     my ($kernel, $session, $self) = @_[KERNEL, SESSION, OBJECT];
183              
184 0           $kernel->sig(DIE => 'sig_die');
185 0           $kernel->sig(INT => 'sig_int');
186 0           $kernel->sig(TERM => 'sig_term');
187 0           $self->_status('normal', "Started (pid $$)");
188              
189 0           $self->_status('normal', "Constructing plugins");
190 0           my ($own_plugs, $plugins) = $self->_construct_plugins();
191              
192 0           $self->_status('normal', "Spawning IRCd component ($self->{cfg}{class})");
193 0           my $ircd = $self->_spawn_ircd();
194              
195 0           $self->_status('normal', 'Registering plugins');
196 0           $self->_register_plugins($ircd, $session->ID(), [@$own_plugs, @$plugins]);
197              
198 0           $self->{own_plugins} = $own_plugs;
199 0           $self->{ircd} = $ircd;
200              
201 0           $self->_add_auths();
202 0           $self->_add_operators();
203 0           $self->_add_denials();
204 0           $self->_add_exemptions();
205 0           $self->_add_peers();
206 0           $self->_add_listeners();
207              
208 0           return;
209             }
210              
211             sub _construct_plugins {
212 0     0     my ($self) = @_;
213              
214 0           my $plug_specs = $self->{cfg}{plugins};
215 0           my @plugins;
216 0           for my $plug_spec (@$plug_specs) {
217 0           my ($class, $args, $canonclass) = @$plug_spec;
218 0           my $obj = $canonclass->new(%$args);
219 0           my $isa = eval { $obj->isa($canonclass) };
  0            
220 0 0         die "isa() test failed for plugin of class $canonclass\n" if !$isa;
221 0           push @plugins, [$class, $obj];
222             }
223              
224 0           my @own_plugs = (
225             [
226             'PocosiStatus',
227             App::Pocosi::Status->new(
228             Pocosi => $self,
229             Trace => $self->{trace},
230             Verbose => $self->{verbose},
231             ),
232             ],
233             );
234              
235 0 0         if ($self->{interactive}) {
236 0           require App::Pocosi::ReadLine;
237 0           push @own_plugs, [
238             'PocosiReadLine',
239             App::Pocosi::ReadLine->new(
240             Pocosi => $self,
241             ),
242             ];
243             }
244              
245 0           return \@own_plugs, \@plugins;
246             }
247              
248             sub _spawn_ircd {
249 0     0     my ($self) = @_;
250              
251 0           my $class = $self->{cfg}{class};
252 0 0         my $ircd = $class->spawn(
    0          
253             plugin_debug => 1,
254             config => $self->{cfg}{config},
255             ($self->{cfg}{flood} ? (antiflood => 0) : ()),
256             (defined $self->{cfg}{auth} ? (auth => $self->{cfg}{auth}) : ()),
257             );
258 0           my $isa = eval { $ircd->isa($class) };
  0            
259 0 0         die "isa() test failed for component of class $class\n" if !$isa;
260              
261 0           return $ircd;
262             }
263              
264             sub _load_either_class {
265 0     0     my ($primary, $secondary) = @_;
266              
267 0           my ($success, $error, $errors);
268 0           ($success, $error) = try_load_class($primary);
269 0 0         return $primary if $success;
270              
271 0           $errors .= $error;
272 0           ($success, $error) = try_load_class($secondary);
273 0 0         return $secondary if $success;
274              
275 0 0         chomp $error if defined $error;
276 0           $errors .= $error;
277 0           die "Failed to load class $primary or $secondary: $errors\n";
278             }
279              
280             sub _register_plugins {
281 0     0     my ($self, $ircd, $session_id, $plugins) = @_;
282              
283 0           for my $plugin (@$plugins) {
284 0           my ($name, $object) = @$plugin;
285 0           $ircd->plugin_add("${name}_$session_id", $object);
286             }
287              
288 0           return;
289             }
290              
291             sub _add_denials {
292 0     0     my ($self) = @_;
293 0           my $ircd = $self->{ircd};
294 0           my $denials = $self->{cfg}{denials};
295 0 0         return if !defined $denials;
296              
297 0           for my $denial (@$denials) {
298 0           my ($mask, $reason) = @$denial;
299 0           my $netmask = Net::Netmask->new2($mask);
300 0 0         if (!defined $netmask) {
301 0           die "Invalid denial: $mask\n";
302             }
303 0           $ircd->add_denial($netmask, $reason);
304             }
305 0           return;
306             }
307              
308             sub _add_exemptions {
309 0     0     my ($self) = @_;
310 0           my $ircd = $self->{ircd};
311 0           my $exemptions = $self->{cfg}{exemptions};
312 0 0         return if !defined $exemptions;
313              
314 0           for my $mask (@$exemptions) {
315 0           my $netmask = Net::Netmask->new2($mask);
316 0 0         if (!defined $netmask) {
317 0           die "Invalid exemption: $mask\n";
318             }
319 0           $ircd->add_exemption($netmask);
320             }
321 0           return;
322             }
323              
324             sub _add_operators {
325 0     0     my ($self) = @_;
326 0           my $ircd = $self->{ircd};
327 0           my $opers = $self->{cfg}{operators};
328 0 0         return if !defined $opers;
329              
330 0           for my $oper (@$opers) {
331 0 0         die "No username supplier for operator\n" if !defined $oper->{username};
332 0 0         if (ref $oper->{ipmask} eq 'ARRAY') {
333 0           my @netmasks;
334 0           for my $mask (@{ $oper->{ipmask} }) {
  0            
335 0           my $netmask = Net::Netmask->new2($mask);
336 0 0         if (!defined $netmask) {
337 0           die "Invalid netmask for oper $oper->{username}: $mask\n";
338             }
339 0           push @netmasks, $netmask;
340             }
341 0           $oper->{ipmask} = \@netmasks;
342             }
343              
344 0           $ircd->add_operator(%$oper);
345             }
346 0           return;
347             }
348              
349             sub _add_peers {
350 0     0     my ($self) = @_;
351 0           my $ircd = $self->{ircd};
352 0           my $peers = $self->{cfg}{peers};
353 0 0         return if !defined $peers;
354 0           $ircd->add_peer(%$_) for @$peers;
355 0           return;
356             }
357              
358             sub _add_auths {
359 0     0     my ($self) = @_;
360 0           my $ircd = $self->{ircd};
361 0           my $auths = $self->{cfg}{auths};
362 0 0         return if !defined $auths;
363 0           $ircd->add_auth(%$_) for @$auths;
364 0           return;
365             }
366              
367             sub _add_listeners {
368 0     0     my ($self) = @_;
369 0           my $ircd = $self->{ircd};
370 0           my $listeners = $self->{cfg}{listeners};
371 0 0         return if !defined $listeners;
372 0           $ircd->yield('add_listener', %$_) for @$listeners;
373 0           return;
374             }
375              
376             sub _dump {
377 0     0     my ($arg) = @_;
378              
379 0 0         if (ref $arg eq 'ARRAY') {
    0          
    0          
    0          
380 0           my @elems;
381 0           for my $elem (@$arg) {
382 0           push @elems, _dump($elem);
383             }
384 0           return '['. join(', ', @elems) .']';
385             }
386             elsif (ref $arg eq 'HASH') {
387 0           my @pairs;
388 0           for my $key (keys %$arg) {
389 0           push @pairs, [$key, _dump($arg->{$key})];
390             }
391 0           return '{'. join(', ', map { "$_->[0] => $_->[1]" } @pairs) .'}';
  0            
392             }
393             elsif (ref $arg) {
394 0           require overload;
395 0           return overload::StrVal($arg);
396             }
397             elsif (defined $arg) {
398 0 0         return $arg if looks_like_number($arg);
399 0           return "'".decode_irc($arg)."'";
400             }
401             else {
402 0           return 'undef';
403             }
404             }
405              
406             sub _event_debug {
407 0     0     my ($self, $args, $event) = @_;
408              
409 0 0         if (!defined $event) {
410 0           $event = (caller(1))[3];
411 0           $event =~ s/.*:://;
412             }
413              
414 0           my @output;
415 0           for my $i (0..$#{ $args }) {
  0            
416 0           push @output, "ARG$i: " . _dump($args->[$i]);
417             }
418 0           $self->_status('debug', "$event: ".join(', ', @output));
419 0           return;
420             }
421              
422             # we handle plugin status messages here because the status plugin won't
423             # see these for previously added plugins or plugin_del for itself, etc
424             sub ircd_plugin_add {
425 0     0 0   my ($self, $alias) = @_[OBJECT, ARG0];
426 0 0         $self->_event_debug([@_[ARG0..$#_]], 'IRCD_plugin_add') if $self->{trace};
427 0           $self->_status('normal', "Added plugin $alias");
428 0           return;
429             }
430              
431             sub ircd_plugin_del {
432 0     0 0   my ($self, $alias) = @_[OBJECT, ARG0];
433 0 0         $self->_event_debug([@_[ARG0..$#_]], 'IRCD_plugin_del') if $self->{trace};
434 0           $self->_status('normal', "Deleted plugin $alias");
435 0           return;
436             }
437              
438             sub ircd_plugin_error {
439 0     0 0   my ($self, $error) = @_[OBJECT, ARG0];
440 0 0         $self->_event_debug([@_[ARG0..$#_]], 'IRCD_plugin_error') if $self->{trace};
441 0           $self->_status('error', $error);
442 0           return;
443             }
444              
445             sub ircd_plugin_status {
446 0     0 0   my ($self, $plugin, $type, $status) = @_[OBJECT, ARG0..ARG2];
447 0           my $ircd = $_[SENDER]->get_heap();
448 0           my $plugins = $ircd->plugin_list();
449 0           my %plug2alias = map { $plugins->{$_} => $_ } keys %$plugins;
  0            
450              
451 0 0         if (ref $plugin ne 'App::Pocosi::Status') {
452 0           $status = "[$plug2alias{$plugin}] $status";
453             }
454 0           $self->_status($type, $status);
455 0           return;
456             }
457              
458             sub ircd_shutdown {
459 0     0 0   my ($self) = $_[OBJECT];
460 0 0         $self->_event_debug([@_[ARG0..$#_]], 'IRCD_shutdown') if $self->{trace};
461 0           $self->_status('normal', 'IRCd component shut down');
462 0           return;
463             }
464              
465             sub verbose {
466 0     0 0   my ($self, $value) = @_;
467 0 0         if (defined $value) {
468 0           $self->{verbose} = $value;
469 0           for my $plugin (@{ $self->{own_plugins} }) {
  0            
470 0 0         $plugin->[1]->verbose($value) if $plugin->[1]->can('verbose');
471             }
472             }
473 0           return $self->{verbose};
474             }
475              
476             sub trace {
477 0     0 0   my ($self, $value) = @_;
478 0 0         if (defined $value) {
479 0           $self->{trace} = $value;
480 0           for my $plugin (@{ $self->{own_plugins} }) {
  0            
481 0 0         $plugin->[1]->trace($value) if $plugin->[1]->can('trace');
482             }
483             }
484 0           return $self->{trace};
485             }
486              
487             sub _status {
488 0     0     my ($self, $type, $message) = @_;
489              
490 0           my $stamp = strftime('%Y-%m-%d %H:%M:%S', localtime);
491 0 0 0       if (defined $type && $type eq 'error') {
492 0           $message = "!!! $message";
493             }
494              
495 0           my $log_line = "$stamp $message";
496 0           my $term_line = $log_line;
497              
498 0 0         if (!$self->{no_color}) {
499 0 0 0       if (defined $type && $type eq 'error') {
    0 0        
500 0           $term_line = colored($term_line, 'red');
501             }
502             elsif (defined $type && $type eq 'debug') {
503 0           $term_line = colored($term_line, 'yellow');
504             }
505             else {
506 0           $term_line = colored($term_line, 'green');
507             }
508             }
509              
510 0 0         print $term_line, "\n" if !$self->{daemonize};
511 0 0         if (defined $self->{log_file}) {
512 0 0         if (open my $fh, '>>:encoding(utf8)', $self->{log_file}) {
    0          
513 0           $fh->autoflush(1);
514 0           print $fh $log_line, "\n";
515 0           close $fh;
516             }
517             elsif (!$self->{daemonize}) {
518 0           warn "Can't open $self->{log_file}: $!\n";
519             }
520             }
521 0           return;
522             }
523              
524             sub sig_die {
525 0     0 0   my ($kernel, $self, $ex) = @_[KERNEL, OBJECT, ARG1];
526 0           $kernel->sig_handled();
527              
528 0           chomp $ex->{error_str};
529 0           my $error = "Event $ex->{event} in session ".$ex->{dest_session}->ID
530             ." raised exception:\n $ex->{error_str}";
531              
532 0           $self->_status('error', $error);
533 0 0         $self->shutdown('Exiting due to an exception') if !$self->{shutdown};
534 0           return;
535             }
536              
537             sub sig_int {
538 0     0 0   my ($kernel, $self) = @_[KERNEL, OBJECT];
539 0 0         $self->shutdown('Exiting due to SIGINT') if !$self->{shutdown};
540 0           $kernel->sig_handled();
541 0           return;
542             }
543              
544             sub sig_term {
545 0     0 0   my ($kernel, $self) = @_[KERNEL, OBJECT];
546 0 0         $self->shutdown('Exiting due to SIGTERM') if !$self->{shutdown};
547 0           $kernel->sig_handled();
548 0           return;
549             }
550              
551             sub shutdown {
552 0     0 0   my ($self, $reason) = @_;
553 0 0         return if $self->{shutdown};
554 0           $self->_status('normal', $reason);
555 0 0         $self->{ircd}->shutdown() if $self->{ircd};
556 0           $self->{shutdown} = 1;
557 0           return;
558             }
559              
560             1;
561              
562             =encoding utf8
563              
564             =head1 NAME
565              
566             App::Pocosi - A command line tool for launching a POE::Component::Server::IRC instance
567              
568             =head1 DESCRIPTION
569              
570             This distribution provides a generic way to launch a
571             L instance.
572              
573             =over 4
574              
575             =item * Prints useful status information (to your terminal and/or a log file)
576              
577             =item * Will daemonize if you so wish
578              
579             =item * Supports a configuration file
580              
581             =item * Offers a user friendly way to pass arguments to POE::Component::Server::IRC
582              
583             =item * Has an interactive mode where you can issue issue commands and
584             call methods on the IRCd component.
585              
586             =back
587              
588             =head1 CONFIGURATION
589              
590             class: POE::Component::Server::IRC
591             log_file: /my/log.file
592             pid_file: /my/pid.file
593             lib: /my/modules
594             flood: false
595             auth: true
596              
597             config:
598             servername: myserver.com
599             motd:
600             - "Welcome to this great server"
601             - ""
602             - "Enjoy your stay"
603              
604             plugins:
605             - [OperServ]
606              
607             listeners:
608             - bindaddr: "127.0.0.1"
609             port: 10023
610              
611             denials:
612             - ["12.34.56.0/24", "I don't like this IP block"]
613              
614             exemptions:
615             - "12.34.56.78"
616              
617             operators:
618             - username: jack
619             password: foo
620             ipmask: ["127.0.0.1", "1.2.3.4", "192.168.1.0/24"]
621             - username: locke
622             password: bar
623             ipmask: "10.0.0.*"
624              
625             auths:
626             - mask: "*@example.com"
627             password: hlagh
628             spoof: jacob
629             no_tilde: true
630              
631             peers:
632             - name: otherserver.com
633             rpass: hlaghpass
634             pass: hlaghpass
635             type: r
636             raddress: "127.0.0.1"
637             rport: 12345
638             auto: true
639              
640             The configuration file is in L or L format. It consists
641             of a hash containing the options described in the above code example. Only
642             C is required.
643              
644             =head2 C
645              
646             Either the name of a directory containing Perl modules (e.g. plugins), or an
647             array of such names. Kind of like Perl's I<-I>.
648              
649             =head2 C
650              
651             Path to a pid file, as used by most daemons. If is specified, App::Pocosi
652             will refuse to run if the file already exists.
653              
654             =head2 C
655              
656             Path to a log file to which status messages will be written.
657              
658             =head2 C
659              
660             The IRC server component class. Defaults to
661             L.
662              
663             =head2 C
664              
665             This is a hash of various configuration variables for the IRCd. See
666             PoCo-Server-IRC's L|POE::Component::Server::IRC/configure>
667             for a list of parameters.
668              
669             =head2 C
670              
671             An array of arrays containing a short plugin class name (e.g. 'OperServ')
672             and optionally a hash of arguments to that plugin. When figuring out the
673             correct package name, App::Pocosi will first try to load
674             POE::Component::Server::IRC::Plugin::I before trying to load
675             I.
676              
677             =head2 C
678              
679             An array of hashes. The keys should be any of the options listed in the docs
680             for PoCo-Server-IRC-Backend's
681             L|POE::Component::Server::IRC::Backend/add_listener> method.
682              
683             =head2 C
684              
685             An array of hashes. The keys are described in the docs for PoCo-Server-IRC's
686             L|POE::Component::Server::IRC/add_auth> method.
687              
688             =head2 C
689              
690             An array of hashes. The keys are described in the docs for PoCo-Server-IRC's
691             L|POE::Component::Server::IRC/add_operator> method. You
692             you can supply an array of netmasks (the kind accepted by
693             L's constructor) for the B<'ipmask'> key.
694              
695             =head2 C
696              
697             An array of hashes. The keys should be any of the options listed in the docs
698             for PoCo-Server-IRC's
699             L|POE::Component::Server::IRC/add_listener> method.
700              
701             =head2 C
702              
703             An array of arrays. The first element of the inner array should be a netmask
704             accepted by L's constructor. The second
705             (optional) element should be a reason for the denial.
706              
707             =head2 C
708              
709             An array of netmasks (the kind which L's
710             constructor accepts).
711              
712             =head1 OUTPUT
713              
714             Here is some example output from the program:
715              
716             $ pocosi -f example/config.yml
717             2011-05-22 15:30:02 Started (pid 13191)
718             2011-05-22 15:30:02 Constructing plugins
719             2011-05-22 15:30:02 Spawning IRCd component (POE::Component::Server::IRC)
720             2011-05-22 15:30:02 Registering plugins
721             2011-05-22 15:30:02 Added plugin PocosiStatus_1
722             2011-05-22 15:30:02 Added plugin OperServ_1
723             2011-05-22 15:30:02 Started listening on 127.0.0.1:10023
724             2011-05-22 15:30:02 Connected to peer otherserver.com on 127.0.0.1:12345
725             2011-05-22 15:30:02 Server otherserver.com (hops: 1) introduced to the network by myserver.com
726             ^C2011-05-22 15:30:18 Exiting due to SIGINT
727             2011-05-22 15:30:18 Deleted plugin OperServ_1
728             2011-05-22 15:30:18 Deleted plugin PocosiStatus_1
729             2011-05-22 15:30:18 IRCd component shut down
730              
731             =head1 AUTHOR
732              
733             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
734              
735             =head1 LICENSE AND COPYRIGHT
736              
737             Copyright 2011 Hinrik Ern SigurEsson
738              
739             This program is free software, you can redistribute it and/or modify
740             it under the same terms as Perl itself.
741              
742             =cut