1# ----------------------------------------------------------------------------- 2# $Id: Channel.pm 36686 2010-02-10 18:47:59Z topia $ 3# ----------------------------------------------------------------------------- 4package Log::Channel; 5use strict; 6use warnings; 7use IO::File; 8use File::Spec; 9use Tiarra::Encoding; 10use base qw(Module); 11use Module::Use qw(Tools::DateConvert Log::Logger Log::Writer); 12use Tools::DateConvert; 13use Log::Logger; 14use Log::Writer; 15use Module::Use qw(Tools::HashTools); 16use Tools::HashTools; 17use ControlPort; 18use Mask; 19use Multicast; 20 21our $DEFAULT_FILENAME_ENCODING = $^O eq 'MSWin32' ? 'sjis' : 'utf8'; 22 23sub new { 24 my $class = shift; 25 my $this = $class->SUPER::new(@_); 26 $this->{channels} = []; # 要素は[ディレクトリ名,マスク] 27 $this->{matching_cache} = {}; # <チャンネル名,ファイル名> 28 $this->{writer_cache} = {}; # <チャンネル名,Log::Writer> 29 $this->{sync_command} = do { 30 my $sync = $this->config->sync; 31 if (defined $sync) { 32 uc $sync; 33 } 34 else { 35 undef; 36 } 37 }; 38 $this->{distinguish_myself} = do { 39 my $conf_val = $this->config->distinguish_myself; 40 if (defined $conf_val) { 41 $conf_val; 42 } 43 else { 44 1; 45 } 46 }; 47 $this->{logger} = 48 Log::Logger->new( 49 sub { 50 $this->_search_and_write(@_); 51 }, 52 $this, 53 'S_PRIVMSG','C_PRIVMSG','S_NOTICE','C_NOTICE'); 54 55 $this->_init; 56} 57 58sub _init { 59 my $this = shift; 60 foreach ($this->config->channel('all')) { 61 my ($dirname,$mask) = split /\s+/; 62 if (!defined($dirname) || $dirname eq '' || 63 !defined($mask) || $mask eq '') { 64 die 'Illegal definition in '.__PACKAGE__."/channel : $_\n"; 65 } 66 push @{$this->{channels}},[$dirname,$mask]; 67 } 68 69 $this; 70} 71 72sub sync { 73 my $this = shift; 74 $this->flush_all_file_handles; 75 RunLoop->shared->notify_msg("Channel logs synchronized."); 76} 77 78sub control_requested { 79 my ($this,$request) = @_; 80 if ($request->ID eq 'synchronize') { 81 $this->sync; 82 ControlPort::Reply->new(204,'No Content'); 83 } 84 else { 85 die "Log::Channel received control request of unsupported ID ".$request->ID."\n"; 86 } 87} 88 89sub message_arrived { 90 my ($this,$message,$sender) = @_; 91 92 # syncは有効で、クライアントから受け取ったメッセージであり、かつ今回のコマンドがsyncに一致しているか? 93 if (defined $this->{sync_command} && 94 $sender->isa('IrcIO::Client') && 95 $message->command eq $this->{sync_command}) { 96 # 開いているファイルを全てflush。 97 # 他のモジュールも同じコマンドでsyncするかも知れないので、 98 # do-not-send-to-servers => 1は設定するが 99 # メッセージ自体は破棄してしまわない。 100 $this->sync; 101 $message->remark('do-not-send-to-servers',1); 102 return $message; 103 } 104 105 # __PACKAGE__/commandにマッチするか? 106 if (Mask::match(lc($this->config->command || '*'),lc($message->command))) { 107 $this->{logger}->log($message,$sender); 108 } 109 110 $message; 111} 112 113{ 114 no warnings qw(once); 115 *S_PRIVMSG = \&PRIVMSG_or_NOTICE; 116 *S_NOTICE = \&PRIVMSG_or_NOTICE; 117 *C_PRIVMSG = \&PRIVMSG_or_NOTICE; 118 *C_NOTICE = \&PRIVMSG_or_NOTICE; 119} 120 121sub PRIVMSG_or_NOTICE { 122 my ($this,$msg,$sender) = @_; 123 my $target = Multicast::detatch($msg->param(0)); 124 my $is_priv = Multicast::nick_p($target); 125 my $cmd = $msg->command; 126 127 my $line = do { 128 if ($is_priv) { 129 # privの時は自分と相手を必ず区別する。 130 if ($sender->isa('IrcIO::Client')) { 131 sprintf( 132 $cmd eq 'PRIVMSG' ? '>%s< %s' : ')%s( %s', 133 $msg->param(0), 134 $msg->param(1)); 135 } 136 else { 137 sprintf( 138 $cmd eq 'PRIVMSG' ? '-%s- %s' : '=%s= %s', 139 $msg->nick || $sender->current_nick, 140 $msg->param(1)); 141 } 142 } 143 else { 144 my $format = do { 145 if ($this->{distinguish_myself} && $sender->isa('IrcIO::Client')) { 146 $cmd eq 'PRIVMSG' ? '>%s:%s< %s' : ')%s:%s( %s'; 147 } 148 else { 149 $cmd eq 'PRIVMSG' ? '<%s:%s> %s' : '(%s:%s) %s'; 150 } 151 }; 152 my $nick = do { 153 if ($sender->isa('IrcIO::Client')) { 154 RunLoop->shared_loop->network( 155 (Multicast::detatch($msg->param(0)))[1]) 156 ->current_nick; 157 } 158 else { 159 $msg->nick || $sender->current_nick; 160 } 161 }; 162 sprintf $format,$msg->param(0),$nick,$msg->param(1); 163 } 164 }; 165 166 [$is_priv ? 'priv' : $msg->param(0),$line]; 167} 168 169sub _channel_match { 170 # 指定されたチャンネル名にマッチするログ保存ファイルのパターンを定義から探す。 171 # 一つもマッチしなければundefを返す。 172 # このメソッドは検索結果を$this->{matching_cache}に保存して、後に再利用する。 173 my ($this,$channel) = @_; 174 175 my $cached = $this->{matching_cache}->{$channel}; 176 if (defined $cached) { 177 if ($cached eq '') { 178 # マッチするエントリは存在しない、という結果がキャッシュされている。 179 return undef; 180 } 181 else { 182 return $cached; 183 } 184 } 185 186 foreach my $ch (@{$this->{channels}}) { 187 if (Mask::match($ch->[1],$channel)) { 188 # マッチした。 189 my $fname_format = $this->config->filename || '%Y.%m.%d.txt'; 190 # あまり好ましくなさそうな文字はあらかじめエスケープ. 191 my $chan_filename = $channel; 192 $chan_filename =~ s/![0-9A-Z]{5}/!/; 193 $chan_filename =~ s{([^-\w@#%!+&.\x80-\xff])}{ 194 sprintf('=%02x', unpack("C", $1)); 195 }ge; 196 my $chan_dir = Tools::HashTools::replace_recursive( 197 $ch->[0], [{channel => $chan_filename, lc_channel => lc $chan_filename}]); 198 my $fpath_format = "$chan_dir/$fname_format"; 199 200 $this->{matching_cache}->{$channel} = $fpath_format; 201 return $fpath_format; 202 } 203 } 204 $this->{matching_cache}->{$channel} = ''; 205 undef; 206} 207 208sub _search_and_write { 209 my ($this,$channel,$line) = @_; 210 my $dirname = $this->_channel_match($channel); 211 if (defined $dirname) { 212 $this->_write($channel,$dirname,$line); 213 } 214} 215 216sub _write { 217 # 指定されたログファイルにヘッダ付きで追記する。 218 # ディレクトリ名の日付のマクロは置換される。 219 my ($this,$channel,$abstract_fpath,$line) = @_; 220 my $concrete_fpath = do { 221 my $basedir = $this->config->directory; 222 if (defined $basedir) { 223 Tools::DateConvert::replace("$basedir/$abstract_fpath"); 224 } 225 else { 226 Tools::DateConvert::replace($abstract_fpath); 227 } 228 }; 229 my $filename_encoding = $this->config->filename_encoding || $DEFAULT_FILENAME_ENCODING; 230 if( $filename_encoding ne 'ascii' ) 231 { 232 $concrete_fpath = Tiarra::Encoding->new($concrete_fpath)->conv($filename_encoding); 233 }else 234 { 235 $concrete_fpath =~ s/([^ -~])/sprintf('=%02x', unpack("C", $1))/ge; 236 } 237 my $header = Tools::DateConvert::replace( 238 $this->config->header || '%H:%M' 239 ); 240 my $always_flush = do { 241 if ($this->config->keep_file_open) { 242 if ($this->config->always_flush) { 243 1; 244 } else { 245 0; 246 } 247 } else { 248 1; 249 } 250 }; 251 # ファイルに追記 252 my $make_writer = sub { 253 Log::Writer->shared_writer->find_object( 254 $concrete_fpath, 255 always_flush => $always_flush, 256 file_mode_oct => $this->config->mode, 257 dir_mode_oct => $this->config->dir_mode, 258 ); 259 }; 260 my $writer = sub { 261 # キャッシュは有効か? 262 if ($this->config->keep_file_open) { 263 # このチャンネルはキャッシュされているか? 264 my $cached_elem = $this->{writer_cache}->{$channel}; 265 if (defined $cached_elem) { 266 # キャッシュされたファイルパスは今回のファイルと一致するか? 267 if ($cached_elem->uri eq $concrete_fpath) { 268 # このファイルハンドルを再利用して良い。 269 #print "$concrete_fpath: RECYCLED\n"; 270 return $cached_elem; 271 } 272 else { 273 # ファイル名が違う。日付が変わった等の場合。 274 # 古いファイルハンドルを閉じる。 275 #print "$concrete_fpath: recached\n"; 276 eval { 277 $cached_elem->flush; 278 $cached_elem->unregister; 279 }; 280 # 新たなファイルハンドルを生成。 281 $cached_elem = $make_writer->(); 282 if (defined $cached_elem) { 283 $cached_elem->register; 284 } 285 return $cached_elem; 286 } 287 } 288 else { 289 # キャッシュされていないので、ファイルハンドルを作ってキャッシュ。 290 #print "$concrete_fpath: *cached*\n"; 291 my $cached_elem = 292 $this->{writer_cache}->{$channel} = 293 $make_writer->(); 294 if (defined $cached_elem) { 295 $cached_elem->register; 296 } 297 return $cached_elem; 298 } 299 } 300 else { 301 # キャッシュ無効。 302 return $make_writer->(); 303 } 304 }->(); 305 if (defined $writer) { 306 $writer->reserve( 307 Tiarra::Encoding->new("$header $line\n",'utf8')->conv( 308 $this->config->charset || 'jis')); 309 } else { 310 # XXX: do warn with properly frequency 311 #RunLoop->shared_loop->notify_warn("can't write to $concrete_fpath: ". 312 # "$header $line"); 313 } 314} 315 316sub flush_all_file_handles { 317 my $this = shift; 318 foreach my $cached_elem (values %{$this->{writer_cache}}) { 319 eval { 320 $cached_elem->flush; 321 }; 322 } 323} 324 325sub destruct { 326 my $this = shift; 327 # 開いている全てのLog::Writerを閉じて、キャッシュを空にする。 328 foreach my $cached_elem (values %{$this->{writer_cache}}) { 329 eval { 330 $cached_elem->flush; 331 $cached_elem->unregister; 332 }; 333 } 334 %{$this->{writer_cache}} = (); 335} 336 3371; 338 339=pod 340info: チャンネルやprivのログを取るモジュール。 341default: off 342section: important 343 344# Log系のモジュールでは、以下のように日付や時刻の置換が行なわれる。 345# %% : % 346# %Y : 年(4桁) 347# %m : 月(2桁) 348# %d : 日(2桁) 349# %H : 時間(2桁) 350# %M : 分(2桁) 351# %S : 秒(2桁) 352 353# ログを保存するディレクトリ。Tiarraが起動した位置からの相対パス。~指定は使えない。 354directory: log 355 356# ログファイルの文字コード。省略されたらjis。 357charset: utf8 358 359# 各行のヘッダのフォーマット。省略されたら'%H:%M'。 360header: %H:%M:%S 361 362# ファイル名のフォーマット。省略されたら'%Y.%m.%d.txt' 363filename: %Y.%m.%d.txt 364 365# ログファイルのモード(8進数)。省略されたら600 366mode: 600 367 368# ログディレクトリのモード(8進数)。省略されたら700 369dir-mode: 700 370 371# ログを取るコマンドを表すマスク。省略されたら記録出来るだけのコマンドを記録する。 372command: privmsg,join,part,kick,invite,mode,nick,quit,kill,topic,notice 373 374# PRIVMSGとNOTICEを記録する際に、自分の発言と他人の発言でフォーマットを変えるかどうか。1/0。デフォルトで1。 375distinguish-myself: 1 376 377# 各ログファイルを開きっぱなしにするかどうか。 378# このオプションは多くの場合、ディスクアクセスを抑えて効率良くログを保存しますが 379# ログを記録すべき全てのファイルを開いたままにするので、50や100のチャンネルを 380# 別々のファイルにログを取るような場合には使うべきではありません。 381# 万一 fd があふれた場合、クライアントから(またはサーバへ)接続できない・ 382# 新たなモジュールをロードできない・ログが全然できないなどの症状が起こる可能性が 383# あります。limit の詳細については OS 等のドキュメントを参照してください。 384-keep-file-open: 1 385 386# keep-file-open 時に各行ごとに flush するかどうか。 387# open/close の負荷は気になるが、ログは失いたくない人向け。 388# keep-file-open が有効でないなら無視され(1になり)ます。 389-always-flush: 0 390 391# keep-file-openを有効にした場合、発言の度にログファイルに追記するのではなく 392# 一定の分量が溜まってから書き込まれる。そのため、ファイルを開いても 393# 最近の発言はまだ書き込まれていない可能性がある。 394# syncを設定すると、即座にログをディスクに書き込むためのコマンドが追加される。 395# 省略された場合はコマンドを追加しない。 396sync: sync 397 398# 各チャンネルの設定。チャンネル名の部分はマスクである。 399# 個人宛てに送られたPRIVMSGやNOTICEはチャンネル名"priv"として検索される。 400# 記述された順序で検索されるので、全てのチャンネルにマッチする"*"などは最後に書かなければならない。 401# 指定されたディレクトリが存在しなかったら、Log::Channelはそれを勝手に作る。 402# フォーマットは次の通り。 403# channel: <ディレクトリ名> (<チャンネル名> / 'priv') 404# 例: 405# filename: %Y.%m.%d.txt 406# channel: IRCDanwasitu #IRC談話室@ircnet 407# channel: others * 408# この例では、#IRC談話室@ircnetのログはIRCDanwasitu/%Y.%m.%d.txtに、 409# それ以外(privも含む)のログはothers/%Y.%m.%d.txtに保存される。 410# #(channel) はチャンネル名に展開される。 411# (古いバージョンだと展開されずにそのままディレクトリ名になってしまいます。) 412# IRCのチャンネル名は大文字小文字が区別されず、サーバからは各送信者が指定した通りの 413# チャンネル名が送られてきます。そのため、大文字小文字が区別されるファイルシステムでは 414# 同じチャンネルが別々のディレクトリに作られることになります。 415# この問題を回避するため、チャンネル名を小文字に統一した #(lc_channel) が利用できます。 416channel: priv priv 417channel: #(lc_channel) * 418-channel: others * 419 420# ファイル名のエンコーディング. 421# 指定可能な値は, utf8, sjis, euc, jis, ascii. 422# ascii は実際には utf8 と同等で8bit部分が全てquoted-printableされる. 423# デフォルトはWindowsではsjis, それ以外では utf8. 424-filename-encoding: utf8 425 426=cut 427