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.021003';
3 3     3   13675 use v5.10;
  3         6  
4 3     3   375 use strictures 2;
  3         1110  
  3         109  
5 3     3   464 use Carp;
  3         7  
  3         152  
6              
7 3     3   1346 use Time::HiRes 'sleep';
  3         2893  
  3         10  
8 3     3   1569 use IO::File ();
  3         2058  
  3         63  
9 3     3   13 use Fcntl qw/:DEFAULT :flock/;
  3         3  
  3         2899  
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 907 my $class = shift;
24              
25 4         8 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         5 bless $self, $class;
35            
36 4         9 my %args = @_;
37 4         16 $args{lc $_} = delete $args{$_} for keys %args;
38              
39             confess "new() requires a 'file' argument"
40 4 100       184 unless defined $args{file};
41              
42 3         7 $self->file( $args{file} );
43              
44             $self->mode( $args{mode} )
45 3 50       8 if defined $args{mode};
46              
47             $self->perms( $args{perms} )
48 3 50       6 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       6 $self->_open or croak "Could not open specified file ".$args{file};
56 3 50       28 $self->_close if $self->[RUNNING_IN_HELL];
57              
58 3         10 $self
59             }
60              
61             sub file {
62 31     31 1 380 my ($self, $file) = @_;
63              
64 31 100       47 if (defined $file) {
65 3 50       10 $self->_close if $self->_is_open;
66            
67             # stringify a Path::Tiny ->
68 3         71 $self->[PATH] = $file . '';
69            
70 3 50       41 $self->_open unless $self->[RUNNING_IN_HELL];
71             }
72              
73 31         241 $self->[PATH]
74             }
75              
76             sub mode {
77 7     7 1 7 my ($self, $mode) = @_;
78            
79 7 50       13 return $self->[MODE] = $mode if defined $mode;
80            
81 7   100     25 $self->[MODE] //= O_WRONLY | O_APPEND | O_CREAT
82             }
83              
84             sub perms {
85 8     8 1 10 my ($self, $perms) = @_;
86            
87 8 50       12 return $self->[PERMS] = $perms if defined $perms;
88            
89 8   100     212 $self->[PERMS] //= 0666
90             }
91              
92             sub _open {
93 7     7   9 my ($self) = @_;
94              
95 7         4 my $fh;
96 7 50       24 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         26 binmode $fh, ':utf8';
105 7         30 $fh->autoflush;
106              
107 7 50       233 $self->[INODE] = ( stat $self->file )[1]
108             unless $self->[RUNNING_IN_HELL];
109              
110 7         20 $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         8 close $self->[HANDLE];
119 1         1 $self->[HANDLE] = undef;
120              
121 1         4 1
122             }
123              
124             sub _is_open {
125 11     11   10 my ($self) = @_;
126 11         90 $self->[HANDLE]
127             }
128              
129             sub _do_reopen {
130 7     7   18 my ($self) = @_;
131              
132             ## Are we on a stupid system or dealing with a not-open file?
133 7 50       11 return 1 unless $self->_is_open;
134              
135 7 50       14 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   736 my ($self, $str) = @_;
146              
147 7 100       12 if ($self->_do_reopen) {
148 1         2 $self->_close;
149 1 50 0     1 $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         7 my $timer = 0;
154 7         39 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         11 print { $self->[HANDLE] } $str;
  7         204  
164              
165 7         23 flock($self->[HANDLE], LOCK_UN);
166            
167 7 50       15 $self->_close if $self->[RUNNING_IN_HELL];
168              
169 7         29 1
170             }
171              
172              
173             1;
174             __END__