1# -----------------------------------------------------------------------------
2# $Id: Raw.pm 11365 2008-05-10 14:58:28Z topia $
3# -----------------------------------------------------------------------------
4package Log::Raw;
5use strict;
6use warnings;
7use IO::File;
8use File::Spec;
9use Tiarra::Encoding;
10use base qw(Module);
11use Module::Use qw(Tools::DateConvert Log::Writer);
12use Tools::DateConvert;
13use Log::Writer;
14use ControlPort;
15use Mask;
16
17sub new {
18    my $class = shift;
19    my $this = $class->SUPER::new(@_);
20    $this->{matching_cache} = {}; # <servername,fname>
21    $this->{writer_cache} = {}; # <server,Log::Writer>
22    $this->{sync_command} = do {
23	my $sync = $this->config->sync;
24	if (defined $sync) {
25	    uc $sync;
26	}
27	else {
28	    undef;
29	}
30    };
31    $this;
32}
33
34sub sync {
35    my $this = shift;
36    $this->flush_all_file_handles;
37    RunLoop->shared->notify_msg("Raw logs synchronized.");
38}
39
40sub control_requested {
41    my ($this,$request) = @_;
42    if ($request->ID eq 'synchronize') {
43	$this->sync;
44	ControlPort::Reply->new(204,'No Content');
45    }
46    else {
47	die ref($this)." received control request of unsupported ID ".$request->ID."\n";
48    }
49}
50
51sub message_arrived {
52    my ($this,$message,$sender) = @_;
53
54    # syncは有効で、クライアントから受け取ったメッセージであり、かつ今回のコマンドがsyncに一致しているか?
55    if (defined $this->{sync_command} &&
56	$sender->isa('IrcIO::Client') &&
57	$message->command eq $this->{sync_command}) {
58	# 開いているファイルを全てflush。
59	# 他のモジュールも同じコマンドでsyncするかも知れないので、
60	# do-not-send-to-servers => 1は設定するが
61	# メッセージ自体は破棄してしまわない。
62	$this->sync;
63	$message->remark('do-not-send-to-servers',1);
64	return $message;
65    }
66    $message;
67}
68
69sub message_io_hook {
70    my ($this,$message,$io,$type) = @_;
71
72    # break with last
73    while (1) {
74	last unless $io->server_p;
75	last unless Mask::match_deep([Mask::array_or_all(
76	    $this->config->command('all'))], $message->command);
77	my $msg = $message->clone;
78	if ($this->config->resolve_numeric && $message->command =~ /^\d{3}$/) {
79	    $msg->command(
80		(NumericReply::fetch_name($message->command)||'undef').
81		    '('.$message->command.')');
82	}
83	my $server = $io->network_name;
84	my $dirname = $this->_server_match($server);
85	if (defined $dirname) {
86	    my $prefix  = sprintf '(%s/%s) ', $server, do {
87		if ($type eq 'in') {
88		    'recv';
89		} elsif ($type eq 'out') {
90		    'send';
91		} else {
92		    '----';
93		}
94	    };
95
96	    my $charset = do {
97		if ($msg->have_raw_params) {
98		    $msg->encoding_params('binary');
99		    'binary';
100		} elsif ($io->can('out_encoding')) {
101		    $io->out_encoding;
102		} else {
103		    $this->config->charset;
104		}
105	    };
106	    $this->_write($server, $dirname, $msg->time, $prefix .
107			      $msg->serialize($charset));
108	}
109	last;
110    }
111
112    return $message;
113}
114
115sub _server_match {
116    my ($this,$server) = @_;
117
118    my $cached = $this->{matching_cache}->{$server};
119    if (defined $cached) {
120	if ($cached eq '') {
121	    # cache of not found
122	    return undef;
123	}
124	else {
125	    return $cached;
126	}
127    }
128
129    foreach my $line ($this->config->server('all')) {
130	my ($name, $mask) = split /\s+/, $line, 2;
131	if (Mask::match($mask,$server)) {
132	    # マッチした。
133	    my $fname_format = $this->config->filename || '%Y.%m.%d.txt';
134	    my $fpath_format = $name."/$fname_format";
135
136	    $this->{matching_cache}->{$server} = $fpath_format;
137	    return $fpath_format;
138	}
139    }
140    $this->{matching_cache}->{$server} = '';
141    undef;
142}
143
144sub _write {
145    # 指定されたログファイルにヘッダ付きで追記する。
146    # ディレクトリ名の日付のマクロは置換される。
147    my ($this,$channel,$abstract_fpath,$time,$line) = @_;
148    my $concrete_fpath = do {
149	my $basedir = $this->config->directory;
150	if (defined $basedir) {
151	    Tools::DateConvert::replace("$basedir/$abstract_fpath", $time);
152	}
153	else {
154	    Tools::DateConvert::replace($abstract_fpath, $time);
155	}
156    };
157    my $header = Tools::DateConvert::replace(
158	$this->config->header || '%H:%M',
159	$time,
160       );
161    my $always_flush = do {
162	if ($this->config->keep_file_open) {
163	    if ($this->config->always_flush) {
164		1;
165	    } else {
166		0;
167	    }
168	} else {
169	    1;
170	}
171    };
172    # ファイルに追記
173    my $make_writer = sub {
174	Log::Writer->shared_writer->find_object(
175	    $concrete_fpath,
176	    always_flush => $always_flush,
177	    file_mode_oct => $this->config->mode,
178	    dir_mode_oct => $this->config->dir_mode,
179	   );
180    };
181    my $writer = sub {
182	# キャッシュは有効か?
183	if ($this->config->keep_file_open) {
184	    # このチャンネルはキャッシュされているか?
185	    my $cached_elem = $this->{writer_cache}->{$channel};
186	    if (defined $cached_elem) {
187		# キャッシュされたファイルパスは今回のファイルと一致するか?
188		if ($cached_elem->uri eq $concrete_fpath) {
189		    # このファイルハンドルを再利用して良い。
190		    #print "$concrete_fpath: RECYCLED\n";
191		    return $cached_elem;
192		}
193		else {
194		    # ファイル名が違う。日付が変わった等の場合。
195		    # 古いファイルハンドルを閉じる。
196		    #print "$concrete_fpath: recached\n";
197		    eval {
198			$cached_elem->flush;
199			$cached_elem->unregister;
200		    };
201		    # 新たなファイルハンドルを生成。
202		    $cached_elem = $make_writer->();
203		    if (defined $cached_elem) {
204			$cached_elem->register;
205		    }
206		    return $cached_elem;
207		}
208	    }
209	    else {
210		# キャッシュされていないので、ファイルハンドルを作ってキャッシュ。
211		#print "$concrete_fpath: *cached*\n";
212		my $cached_elem =
213		    $this->{writer_cache}->{$channel} =
214			$make_writer->();
215		if (defined $cached_elem) {
216		    $cached_elem->register;
217		}
218		return $cached_elem;
219	    }
220	}
221	else {
222	    # キャッシュ無効。
223	    return $make_writer->();
224	}
225    }->();
226    if (defined $writer) {
227	$writer->reserve("$header $line\n");
228    } else {
229	# XXX: do warn with properly frequency
230	#RunLoop->shared_loop->notify_warn("can't write to $concrete_fpath: ".
231	#				      "$header $line");
232    }
233}
234
235sub flush_all_file_handles {
236    my $this = shift;
237    foreach my $cached_elem (values %{$this->{writer_cache}}) {
238	eval {
239	    $cached_elem->flush;
240	};
241    }
242}
243
244sub destruct {
245    my $this = shift;
246    # 開いている全てのLog::Writerを閉じて、キャッシュを空にする。
247    foreach my $cached_elem (values %{$this->{writer_cache}}) {
248	eval {
249	    $cached_elem->flush;
250	    $cached_elem->unregister;
251	};
252    }
253    %{$this->{writer_cache}} = ();
254}
255
2561;
257
258=pod
259info: サーバとの生の通信を保存する
260default: off
261
262# Log系のモジュールでは、以下のように日付や時刻の置換が行なわれる。
263# %% : %
264# %Y : 年(4桁)
265# %m : 月(2桁)
266# %d : 日(2桁)
267# %H : 時間(2桁)
268# %M : 分(2桁)
269# %S : 秒(2桁)
270
271# ログを保存するディレクトリ。Tiarraが起動した位置からの相対パス。~指定は使えない。
272directory: rawlog
273
274# 各行のヘッダのフォーマット。省略されたら'%H:%M'。
275header: %H:%M:%S
276
277# ファイル名のフォーマット。省略されたら'%Y.%m.%d.txt'
278filename: %Y-%m-%d.txt
279
280# ログファイルのモード(8進数)。省略されたら600
281mode: 600
282
283# ログディレクトリのモード(8進数)。省略されたら700
284dir-mode: 700
285
286# 使っている文字コードがよくわからなかったときの文字コード。省略されたらutf8。
287# たぶんこの指定が生きることはないと思いますが……。
288charset: jis
289
290# NumericReply の名前を解決して表示する(ちゃんとした dump では無くなります)
291resolve-numeric: 1
292
293# ログを取るコマンドを表すマスク。省略されたら記録出来るだけのコマンドを記録する。
294command: *,-ping,-pong
295
296# 各ログファイルを開きっぱなしにするかどうか。
297# このオプションは多くの場合、ディスクアクセスを抑えて効率良くログを保存しますが
298# ログを記録すべき全てのファイルを開いたままにするので、50や100のチャンネルを
299# 別々のファイルにログを取るような場合には使うべきではありません。
300# 万一 fd があふれた場合、クライアントから(またはサーバへ)接続できない・
301# 新たなモジュールをロードできない・ログが全然できないなどの症状が起こる可能性が
302# あります。limit の詳細については OS 等のドキュメントを参照してください。
303-keep-file-open: 1
304
305# keep-file-open 時に各行ごとに flush するかどうか。
306# open/close の負荷は気になるが、ログは失いたくない人向け。
307# keep-file-open が有効でないなら無視され(1になり)ます。
308-always-flush: 0
309
310# keep-file-openを有効にした場合、発言の度にログファイルに追記するのではなく
311# 一定の分量が溜まってから書き込まれる。そのため、ファイルを開いても
312# 最近の発言はまだ書き込まれていない可能性がある。
313# syncを設定すると、即座にログをディスクに書き込むためのコマンドが追加される。
314# 省略された場合はコマンドを追加しない。
315sync: sync
316
317# 各サーバの設定。サーバ名の部分はマスクである。
318# 記述された順序で検索されるので、全てのサーバにマッチする"*"などは最後に書かなければならない。
319# 指定されたディレクトリが存在しなかったら、勝手に作られる。
320# フォーマットは次の通り。
321# channel: <ディレクトリ名> <サーバ名マスク>
322# 例:
323# filename: %Y-%m-%d.txt
324# server: ircnet ircnet
325# server: others *
326# この例では、ircnetのログはircnet/%Y.%m.%d.txtに、
327# それ以外のログはothers/%Y.%m.%d.txtに保存される。
328server: ircnet ircnet
329server: others *
330=cut
331