PageRenderTime 68ms CodeModel.GetById 38ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/Helm/Log/Channel/irc.pm

https://github.com/mpeters/helm
Perl | 272 lines | 235 code | 22 blank | 15 comment | 17 complexity | 119a31598272d84d6cdd8b8906f4d2c4 MD5 | raw file
  1. package Helm::Log::Channel::irc;
  2. use strict;
  3. use warnings;
  4. use Moose;
  5. use namespace::autoclean;
  6. use DateTime;
  7. use AnyEvent;
  8. use IO::Pipe;
  9. BEGIN {
  10. eval { require AnyEvent::IRC::Client };
  11. die "Could not load AnyEvent::IRC::Client. It must be installed to use Helm's irc logging"
  12. if $@;
  13. }
  14. extends 'Helm::Log::Channel';
  15. has irc_pipe => (is => 'ro', writer => '_irc_pipe');
  16. has pipes => (is => 'ro', writer => '_pipes', isa => 'HashRef');
  17. has is_parallel => (is => 'rw', isa => 'Bool', default => 0);
  18. has irc_pause => (is => 'ro', writer => '_irc_pause', isa => 'Int', default => 0);
  19. has prefix => (is => 'rw', isa => 'Str', default => '');
  20. my $DISCONNECT = 'Disconnecting';
  21. # first parse the IRC URI into some parts that we can use to create an IRC connection.
  22. # Then fork off an IRC worker process to go into an event loop that will read input
  23. # from the main process via a pipe and then output that to the IRC server. We need
  24. # to do it in an event loop because it needs to also respond asynchronously to the
  25. # IRC server for pings and such.
  26. sub initialize {
  27. my ($self, $helm) = @_;
  28. my $options = $helm->extra_options;
  29. my $pause = $options->{'irc-pause'} || $options->{'irc_pause'};
  30. $self->_irc_pause($pause) if $pause;
  31. my %irc_info;
  32. # file the file and open it for appending
  33. my $uri = $self->uri;
  34. if ($uri->authority =~ /@/) {
  35. my ($nick, $host) = split(/@/, $uri->authority);
  36. $irc_info{nick} = $nick;
  37. $irc_info{server} = $host;
  38. } else {
  39. $irc_info{nick} = 'helm';
  40. $irc_info{server} = $uri->authority;
  41. }
  42. $helm->die("No IRC server given in URI $uri") unless $irc_info{server};
  43. # get the channel
  44. my $channel = $uri->path;
  45. $helm->die("No IRC channel given in URI $uri") unless $channel;
  46. $channel =~ s/^\///; # remove leading slash
  47. $channel = "#$channel" unless $channel =~ /^#/;
  48. $irc_info{channel} = $channel;
  49. # do we need a password
  50. my $query = $uri->query;
  51. if ($query && $query =~ /(?:^|&|;)(pass|pw|password|passw|passwd)=(.*)(?:$|&|;)/) {
  52. $irc_info{password} = $1;
  53. }
  54. # do we have a port?
  55. if ($irc_info{server} =~ /:(\d+)$/) {
  56. $irc_info{port} = $1;
  57. $irc_info{server} =~ s/:(\d+)$//;
  58. } else {
  59. $irc_info{port} = 6667;
  60. }
  61. # setup a pipe for communicating
  62. my $irc_pipe = IO::Pipe->new();
  63. # fork off a child process
  64. my $pid = fork();
  65. $helm->die("Couldn't fork IRC bot process") if !defined $pid;
  66. if ($pid) {
  67. # parent here
  68. $irc_pipe->writer;
  69. $irc_pipe->autoflush(1);
  70. $self->_irc_pipe($irc_pipe);
  71. Helm->debug("Parent IRC pipe set up");
  72. } else {
  73. Helm->debug("Child IRC worker process");
  74. # child here
  75. $irc_pipe->reader;
  76. $irc_pipe->autoflush(1);
  77. Helm->debug("Child IRC pipe set up");
  78. $self->_irc_events($irc_pipe, %irc_info);
  79. }
  80. }
  81. sub finalize {
  82. my ($self, $helm) = @_;
  83. Helm->debug("IRC channel finalized: Nothing to do");
  84. }
  85. sub start_server {
  86. my ($self, $server) = @_;
  87. $self->SUPER::start_server($server);
  88. $self->_say("BEGIN Helm task \"" . $self->task . "\" on $server");
  89. }
  90. sub end_server {
  91. my ($self, $server) = @_;
  92. $self->SUPER::end_server($server);
  93. $self->_say("END Helm task \"" . $self->task . "\" on $server");
  94. }
  95. sub debug {
  96. my ($self, $msg) = @_;
  97. $self->_say("[debug] $msg");
  98. }
  99. sub info {
  100. my ($self, $msg) = @_;
  101. $self->_say("$msg");
  102. }
  103. sub warn {
  104. my ($self, $msg) = @_;
  105. $self->_say("[warn] $msg");
  106. }
  107. sub error {
  108. my ($self, $msg) = @_;
  109. $self->_say("[error] $msg");
  110. }
  111. sub _say {
  112. my ($self, $msg) = @_;
  113. my $prefix = $self->prefix;
  114. Helm->debug("Sending message to IO worker: $prefix$msg");
  115. $self->irc_pipe->print("MSG: $prefix$msg\n") or CORE::die("Could not print message to IO Worker: $!");
  116. }
  117. sub _irc_events {
  118. my ($self, $irc_pipe, %args) = @_;
  119. my $irc = AnyEvent::IRC::Client->new();
  120. my $done = AnyEvent->condvar;
  121. my $io_watcher;
  122. $irc->reg_cb(
  123. join => sub {
  124. my ($irc, $nick, $channel, $is_myself) = @_;
  125. Helm->debug("IRC worker joined channel $channel");
  126. # send the initial message
  127. if ($is_myself && $channel eq $args{channel}) {
  128. $io_watcher = AnyEvent->io(
  129. fh => $irc_pipe,
  130. poll => 'r',
  131. cb => sub {
  132. my $msg = <$irc_pipe>;
  133. if(!$msg) {
  134. Helm->debug("IRC worker ran out of pipe");
  135. $irc->send_msg(PRIVMSG => $channel, $DISCONNECT);
  136. undef $io_watcher;
  137. } else {
  138. chomp($msg);
  139. if ($msg =~ /^MSG: (.*)/) {
  140. my $content = $1;
  141. chomp($content);
  142. if( my $secs = $self->irc_pause ) {
  143. Helm->debug("IRC worker sleeping for $secs seconds");
  144. sleep($secs);
  145. }
  146. Helm->debug("IRC worker sending message to IRC channel: $content");
  147. $irc->send_msg(PRIVMSG => $channel, $content);
  148. }
  149. }
  150. }
  151. );
  152. }
  153. }
  154. );
  155. # we aren't done until the server acknowledges the send disconnect message
  156. $irc->reg_cb(
  157. sent => sub {
  158. my ($irc, $junk, $type, $channel, $msg) = @_;
  159. if( $type eq 'PRIVMSG' && $msg eq $DISCONNECT ) {
  160. Helm->debug("IRC channel received DISCONNECT message");
  161. $done->send();
  162. }
  163. }
  164. );
  165. Helm->debug("IRC worker connecting to server $args{server}");
  166. $irc->connect($args{server}, $args{port}, {nick => $args{nick}});
  167. Helm->debug("IRC worker trying to join channel $args{channel}");
  168. $irc->send_srv(JOIN => ($args{channel}));
  169. Helm->debug("IRC worker waiting for work");
  170. $done->recv;
  171. Helm->debug("IRC worker done with work, disconnecting");
  172. $irc->disconnect();
  173. exit(0);
  174. }
  175. # we already have an IRC bot forked off which has a pipe to our main process for
  176. # communication. But if we then share that pipe in all our children we'll end up
  177. # with garbled messages. So we need to fork off another worker process which has
  178. # multiple pipes, one for each possible server that we'll be executing tasks on.
  179. # This extra IO worker process will multi-plex the output coming from those pipes
  180. # into something reasonable for the IRC bot to handle.
  181. sub parallelize {
  182. my ($self, $helm) = @_;
  183. $self->is_parallel(1);
  184. # if we're going to do parallel stuff, then create a pipe for each server now
  185. # that we can use to communicate with the child processes later
  186. my %pipes = map { $_->name => IO::Pipe->new } (@{$helm->servers});
  187. $self->_pipes(\%pipes);
  188. # fork off an IO worker process
  189. my $pid = fork();
  190. $helm->die("Couldn't fork IRC IO worker process") if !defined $pid;
  191. if (!$pid) {
  192. Helm->debug("IO worker forked");
  193. # child here
  194. my %pipe_cleaners;
  195. my $all_clean = AnyEvent->condvar;
  196. foreach my $server (keys %pipes) {
  197. my $pipe = $pipes{$server};
  198. $pipe->reader;
  199. # create an IO watcher for this pipe
  200. Helm->debug("IO worker setting up AnyEvent reads on pipe for $server");
  201. $pipe_cleaners{$server} = AnyEvent->io(
  202. fh => $pipe,
  203. poll => 'r',
  204. cb => sub {
  205. my $msg = <$pipe>;
  206. if ($msg) {
  207. Helm->debug("Printing message to IRC PIPE: $msg");
  208. $self->irc_pipe->print($msg) or CORE::die "Could not print message to IRC PIPE: $!";
  209. } else {
  210. delete $pipe_cleaners{$server};
  211. Helm->debug("Removing IO pipe for $server");
  212. # tell the main program we're done if this is the last broom
  213. $all_clean->send unless %pipe_cleaners;
  214. }
  215. },
  216. );
  217. }
  218. Helm->debug("Waiting for IO to send to IRC worker process");
  219. $all_clean->recv;
  220. Helm->debug("All done with IO to send to IRC worker process");
  221. exit(0);
  222. }
  223. }
  224. # we've been forked, and if it's a child we want to initialize the pipe
  225. # for this worker child's server
  226. sub forked {
  227. my ($self, $type) = @_;
  228. if ($type eq 'child') {
  229. Helm->debug("Forked worker process for " . $self->current_server->name);
  230. my $pipes = $self->pipes;
  231. my $pipe = $pipes->{$self->current_server->name};
  232. $pipe->writer();
  233. $pipe->autoflush(1);
  234. $self->_irc_pipe($pipe);
  235. $self->prefix('[' . $self->current_server->name . '] ');
  236. }
  237. }
  238. __PACKAGE__->meta->make_immutable;
  239. 1;