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.021002';
3 3     3   14156 use v5.10;
  3         6  
4 3     3   383 use strictures 2;
  3         1352  
  3         104  
5 3     3   526 use Carp;
  3         5  
  3         179  
6              
7 3     3   1586 use Time::HiRes 'sleep';
  3         3243  
  3         12  
8 3     3   2039 use IO::File ();
  3         2482  
  3         74  
9 3     3   14 use Fcntl qw/:DEFAULT :flock/;
  3         2  
  3         3316  
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 879 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         8 my %args = @_;
37 4         14 $args{lc $_} = delete $args{$_} for keys %args;
38              
39             confess "new() requires a 'file' argument"
40 4 100       183 unless defined $args{file};
41              
42 3         6 $self->file( $args{file} );
43              
44             $self->mode( $args{mode} )
45 3 50       6 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       5 $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         11 $self
59             }
60              
61             sub file {
62 31     31 1 377 my ($self, $file) = @_;
63              
64 31 100       46 if (defined $file) {
65 3 50       12 $self->_close if $self->_is_open;
66            
67             # stringify a Path::Tiny ->
68 3         76 $self->[PATH] = $file . '';
69            
70 3 50       42 $self->_open unless $self->[RUNNING_IN_HELL];
71             }
72              
73 31         238 $self->[PATH]
74             }
75              
76             sub mode {
77 7     7 1 5 my ($self, $mode) = @_;
78            
79 7 50       11 return $self->[MODE] = $mode if defined $mode;
80            
81 7   100     28 $self->[MODE] //= O_WRONLY | O_APPEND | O_CREAT
82             }
83              
84             sub perms {
85 8     8 1 5 my ($self, $perms) = @_;
86            
87 8 50       20 return $self->[PERMS] = $perms if defined $perms;
88            
89 8   100     201 $self->[PERMS] //= 0666
90             }
91              
92             sub _open {
93 7     7   7 my ($self) = @_;
94              
95 7         6 my $fh;
96 7 50       22 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         24 binmode $fh, ':utf8';
105 7         31 $fh->autoflush;
106              
107 7 50       216 $self->[INODE] = ( stat $self->file )[1]
108             unless $self->[RUNNING_IN_HELL];
109              
110 7         18 $self->[HANDLE] = $fh
111             }
112              
113             sub _close {
114 1     1   1 my ($self) = @_;
115            
116 1 50       1 return 1 unless $self->_is_open;
117            
118 1         6 close $self->[HANDLE];
119 1         2 $self->[HANDLE] = undef;
120              
121 1         3 1
122             }
123              
124             sub _is_open {
125 11     11   11 my ($self) = @_;
126 11         38 $self->[HANDLE]
127             }
128              
129             sub _do_reopen {
130 7     7   17 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     9 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   724 my ($self, $str) = @_;
146              
147 7 100       11 if ($self->_do_reopen) {
148 1         3 $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         11 my $timer = 0;
154 7         38 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         181  
164              
165 7         22 flock($self->[HANDLE], LOCK_UN);
166            
167 7 50       16 $self->_close if $self->[RUNNING_IN_HELL];
168              
169 7         29 1
170             }
171              
172              
173             1;
174             __END__