$Bot::Cobalt::Logger::Output::File::VERSION = '0.021003';
use v5.10;
use strictures 2;
use Carp;
use Time::HiRes 'sleep';
use IO::File ();
use Fcntl qw/:DEFAULT :flock/;
# ... after which we fall back to warning on stderr with message included:
sub FLOCK_TIMEOUT () { 0.5 }
sub PATH () { 0 }
sub HANDLE () { 1 }
sub MODE () { 2 }
sub PERMS () { 3 }
sub INODE () { 4 }
sub RUNNING_IN_HELL () { 5 }
sub new {
my $class = shift;
my $self = [
'', ## PATH
undef, ## HANDLE
undef, ## MODE
undef, ## PERMS
undef, ## INODE
0, ## RUNNING_IN_HELL
];
bless $self, $class;
my %args = @_;
$args{lc $_} = delete $args{$_} for keys %args;
confess "new() requires a 'file' argument"
unless defined $args{file};
$self->file( $args{file} );
$self->mode( $args{mode} )
if defined $args{mode};
$self->perms( $args{perms} )
if defined $args{perms};
if ($^O eq 'MSWin32' or $^O eq 'VMS') {
$self->[RUNNING_IN_HELL] = 1
}
## Try to open/create file when object is constructed
$self->_open or croak "Could not open specified file ".$args{file};
$self->_close if $self->[RUNNING_IN_HELL];
$self
}
sub file {
my ($self, $file) = @_;
if (defined $file) {
$self->_close if $self->_is_open;
# stringify a Path::Tiny ->
$self->[PATH] = $file . '';
$self->_open unless $self->[RUNNING_IN_HELL];
}
$self->[PATH]
}
sub mode {
my ($self, $mode) = @_;
return $self->[MODE] = $mode if defined $mode;
$self->[MODE] //= O_WRONLY | O_APPEND | O_CREAT
}
sub perms {
my ($self, $perms) = @_;
return $self->[PERMS] = $perms if defined $perms;
$self->[PERMS] //= 0666
}
sub _open {
my ($self) = @_;
my $fh;
unless (sysopen($fh, $self->file, $self->mode, $self->perms) ) {
warn(
"Log file could not be opened: ",
join ' ', $self->file, $!
);
return
}
binmode $fh, ':utf8';
$fh->autoflush;
$self->[INODE] = ( stat $self->file )[1]
unless $self->[RUNNING_IN_HELL];
$self->[HANDLE] = $fh
}
sub _close {
my ($self) = @_;
return 1 unless $self->_is_open;
close $self->[HANDLE];
$self->[HANDLE] = undef;
1
}
sub _is_open {
my ($self) = @_;
$self->[HANDLE]
}
sub _do_reopen {
my ($self) = @_;
## Are we on a stupid system or dealing with a not-open file?
return 1 unless $self->_is_open;
unless ( $self->[RUNNING_IN_HELL] ) {
## Do the inodes match?
return if -e $self->file
and $self->[INODE] == ( stat $self->file )[1];
}
1
}
sub _write {
my ($self, $str) = @_;
if ($self->_do_reopen) {
$self->_close;
$self->_open or warn "_open failure" and return;
}
## FIXME if flock fails, buffer and try next _write up to X items ?
my $timer = 0;
until ( flock($self->[HANDLE], LOCK_EX | LOCK_NB) ) {
if ($timer > FLOCK_TIMEOUT) {
warn "flock failure for '@{[$self->file]}' ('$str')";
return
}
sleep 0.01;
$timer += 0.01;
}
print { $self->[HANDLE] } $str;
flock($self->[HANDLE], LOCK_UN);
$self->_close if $self->[RUNNING_IN_HELL];
1
}
1;
__END__
=pod
=head1 NAME
Bot::Cobalt::Logger::Output::File - Bot::Cobalt::Logger file output
=head1 SYNOPSIS
$output_obj->add(
'MyFile' => {
type => 'File',
## Required:
file => $path_to_log,
## Optional:
# perms() defaults to 0666 and is modified by umask:
perms => 0666,
# mode() should be Fcntl constants suitable for sysopen()
# defaults to O_WRONLY | O_APPEND | O_CREAT
mode => O_WRONLY | O_APPEND | O_CREAT,
},
);
See L<Bot::Cobalt::Logger::Output>.
=head1 DESCRIPTION
This is a L<Bot::Cobalt::Logger::Output> writer for logging messages to a
file.
The constructor requires a L</file> specification (the path to the actual
file to write). L</perms> or </mode> can also be set at construction
time but are optional.
The log file is kept open persistently, but closed and reopened if the
file's inode has changed or the file has disappeared. This doesn't apply
on Windows, which has no concept of inodes; an open-write-close cycle
will be executed for each logged message on systems without useful inode
details, in order to ensure messages are going to the expected file.
Attempts to lock the file for every write; if a lock cannot be obtained after
half a second, falls back to C<warn>ing with the log message included.
Expects UTF-8.
=head2 file
Retrieve or set the current file path.
=head2 perms
Retrieve or set the permissions passed to C<sysopen()>.
This should be an octal mode and will be modified by the current
C<umask>.
Defaults to 0666
=head2 mode
Retrieve or set the open mode passed to C<sysopen()>.
See L<Fcntl>.
Defaults to:
O_WRONLY | O_APPEND | O_CREAT
=head1 AUTHOR
Jon Portnoy <avenj@cobaltirc.org>
=cut