File Coverage

blib/lib/Bot/Cobalt/Logger/Output/File.pm
Criterion Covered Total %
statement 72 81 88.8
branch 24 42 57.1
condition 7 12 58.3
subroutine 15 15 100.0
pod 3 4 75.0
total 121 154 78.5


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Logger::Output::File;
2             $Bot::Cobalt::Logger::Output::File::VERSION = '0.021001';
3 3     3   14906 use v5.10;
  3         7  
4 3     3   451 use strictures 2;
  3         1104  
  3         106  
5 3     3   507 use Carp;
  3         4  
  3         212  
6              
7 3     3   1407 use Time::HiRes 'sleep';
  3         2944  
  3         11  
8 3     3   1573 use IO::File ();
  3         2231  
  3         61  
9 3     3   11 use Fcntl qw/:DEFAULT :flock/;
  3         4  
  3         2837  
10              
11             # ... after which we fall back to warning on stderr with message included:
12             sub FLOCK_TIMEOUT () { 0.5 }
13              
14              
15             sub PATH () { 0 }
16             sub HANDLE () { 1 }
17             sub MODE () { 2 }
18             sub PERMS () { 3 }
19             sub INODE () { 4 }
20             sub RUNNING_IN_HELL () { 5 }
21              
22             sub new {
23 4     4 0 553 my $class = shift;
24              
25 4         7 my $self = [
26             '', ## PATH
27             undef, ## HANDLE
28             undef, ## MODE
29             undef, ## PERMS
30             undef, ## INODE
31             0, ## RUNNING_IN_HELL
32             ];
33              
34 4         6 bless $self, $class;
35            
36 4         9 my %args = @_;
37 4         15 $args{lc $_} = delete $args{$_} for keys %args;
38              
39             confess "new() requires a 'file' argument"
40 4 100       202 unless defined $args{file};
41              
42 3         7 $self->file( $args{file} );
43              
44             $self->mode( $args{mode} )
45 3 50       9 if defined $args{mode};
46              
47             $self->perms( $args{perms} )
48 3 50       7 if defined $args{perms};
49              
50 3 50 33     18 if ($^O eq 'MSWin32' or $^O eq 'VMS') {
51 0         0 $self->[RUNNING_IN_HELL] = 1
52             }
53              
54             ## Try to open/create file when object is constructed
55 3 50       7 $self->_open or croak "Could not open specified file ".$args{file};
56 3 50       30 $self->_close if $self->[RUNNING_IN_HELL];
57              
58 3         9 $self
59             }
60              
61             sub file {
62 31     31 1 279 my ($self, $file) = @_;
63              
64 31 100       50 if (defined $file) {
65 3 50       9 $self->_close if $self->_is_open;
66            
67             # stringify a Path::Tiny ->
68 3         74 $self->[PATH] = $file . '';
69            
70 3 50       44 $self->_open unless $self->[RUNNING_IN_HELL];
71             }
72              
73 31         239 $self->[PATH]
74             }
75              
76             sub mode {
77 7     7 1 8 my ($self, $mode) = @_;
78            
79 7 50       14 return $self->[MODE] = $mode if defined $mode;
80            
81 7   100     69 $self->[MODE] //= O_WRONLY | O_APPEND | O_CREAT
82             }
83              
84             sub perms {
85 8     8 1 8 my ($self, $perms) = @_;
86            
87 8 50       17 return $self->[PERMS] = $perms if defined $perms;
88            
89 8   100     251 $self->[PERMS] //= 0666
90             }
91              
92             sub _open {
93 7     7   7 my ($self) = @_;
94              
95 7         24 my $fh;
96 7 50       30 unless (sysopen($fh, $self->file, $self->mode, $self->perms) ) {
97 0         0 warn(
98             "Log file could not be opened: ",
99             join ' ', $self->file, $!
100             );
101             return
102 0         0 }
103              
104 7         27 binmode $fh, ':utf8';
105 7         33 $fh->autoflush;
106              
107 7 50       217 $self->[INODE] = ( stat $self->file )[1]
108             unless $self->[RUNNING_IN_HELL];
109              
110 7         19 $self->[HANDLE] = $fh
111             }
112              
113             sub _close {
114 1     1   1 my ($self) = @_;
115            
116 1 50       2 return 1 unless $self->_is_open;
117            
118 1         7 close $self->[HANDLE];
119 1         2 $self->[HANDLE] = undef;
120              
121 1         2 1
122             }
123              
124             sub _is_open {
125 11     11   9 my ($self) = @_;
126 11         42 $self->[HANDLE]
127             }
128              
129             sub _do_reopen {
130 7     7   19 my ($self) = @_;
131              
132             ## Are we on a stupid system or dealing with a not-open file?
133 7 50       10 return 1 unless $self->_is_open;
134              
135 7 50       13 unless ( $self->[RUNNING_IN_HELL] ) {
136             ## Do the inodes match?
137 7 100 66     10 return if -e $self->file
138             and $self->[INODE] == ( stat $self->file )[1];
139             }
140            
141             1
142 1         3 }
143              
144             sub _write {
145 7     7   614 my ($self, $str) = @_;
146              
147 7 100       13 if ($self->_do_reopen) {
148 1         3 $self->_close;
149 1 50 0     2 $self->_open or warn "_open failure" and return;
150             }
151              
152             ## FIXME if flock fails, buffer and try next _write up to X items ?
153 7         8 my $timer = 0;
154 7         40 until ( flock($self->[HANDLE], LOCK_EX | LOCK_NB) ) {
155 0 0       0 if ($timer > FLOCK_TIMEOUT) {
156 0         0 warn "flock failure for '@{[$self->file]}' ('$str')";
  0         0  
157             return
158 0         0 }
159 0         0 sleep 0.01;
160 0         0 $timer += 0.01;
161             }
162              
163 7         5 print { $self->[HANDLE] } $str;
  7         225  
164              
165 7         22 flock($self->[HANDLE], LOCK_UN);
166            
167 7 50       17 $self->_close if $self->[RUNNING_IN_HELL];
168              
169 7         31 1
170             }
171              
172              
173             1;
174             __END__