File Coverage

lib/Log/Unrotate/Cursor/File.pm
Criterion Covered Total %
statement 135 140 96.4
branch 65 84 77.3
condition 14 20 70.0
subroutine 18 18 100.0
pod 5 5 100.0
total 237 267 88.7


line stmt bran cond sub pod time code
1             package Log::Unrotate::Cursor::File;
2             {
3             $Log::Unrotate::Cursor::File::VERSION = '1.32';
4             }
5              
6 2     2   76929 use strict;
  2         6  
  2         93  
7 2     2   10 use warnings;
  2         4  
  2         66  
8              
9 2     2   10 use base qw(Log::Unrotate::Cursor);
  2         4  
  2         1581  
10              
11 2     2   18 use overload '""' => sub { shift()->{file} };
  2     12   4  
  2         26  
  12         307  
12              
13             =head1 NAME
14              
15             Log::Unrotate::Cursor::File - file keeping unrotate position
16              
17             =head1 VERSION
18              
19             version 1.32
20              
21             =head1 SYNOPSIS
22              
23             use Log::Unrotate::Cursor::File;
24             $cursor = Log::Unrotate::Cursor::File->new($file, { lock => "blocking" });
25              
26             =head1 METHODS
27              
28             =cut
29              
30 2     2   198 use Fcntl qw(:flock);
  2         4  
  2         347  
31 2     2   12 use Carp;
  2         5  
  2         343  
32 2     2   6602 use File::Temp 0.15;
  2         122302  
  2         481  
33 2     2   24 use File::Basename;
  2         4  
  2         14594  
34              
35             our %_lock_values = map { $_ => 1 } qw(none blocking nonblocking);
36             our %_text2field = (
37             position => 'Position',
38             logfile => 'LogFile',
39             inode => 'Inode',
40             lastline => 'LastLine',
41             committime => 'CommitTime',
42             );
43              
44             =over
45              
46             =item B
47              
48             =item B
49              
50             Construct a cursor from the file.
51              
52             C<$options> is an optional hashref.
53              
54             I option describes the locking behavior. See C for details.
55              
56             I option defines the target rollback time in seconds. If 0, rollback behaviour will be off.
57              
58             =cut
59             sub new {
60 108     108 1 2029 my ($class, $file, $options) = @_;
61 108 50       406 croak "No file specified" unless defined $file;
62              
63 108         8516 my $lock = 'none';
64 108         171 my $rollback;
65 108 50       302 if ($options) {
66 108         223 $lock = $options->{lock};
67 108         204 $rollback = $options->{rollback_period};
68             }
69 108 100       484 croak "unknown lock value: '$lock'" unless $_lock_values{$lock};
70 107 50 66     1762 croak "wrong rollback_period: '$rollback'" if ($rollback and $rollback !~ /^\d+$/);
71              
72 107         1513 my $self = bless {
73             file => $file,
74             rollback => $rollback,
75             } => $class;
76              
77 107 100       999 unless ($lock eq 'none') {
78             # locks
79 4 50       232 unless (open $self->{lock_fh}, '>>', "$self->{file}.lock") {
80 0         0 delete $self->{lock_fh};
81 0         0 croak "Can't open $self->{file}.lock: $!";
82             }
83 4 100       23 if ($lock eq 'blocking') {
    50          
84 1 50       24 flock $self->{lock_fh}, LOCK_EX or croak "Failed to obtain lock: $!";
85             }
86             elsif ($lock eq 'nonblocking') {
87 3 100       61 flock $self->{lock_fh}, LOCK_EX | LOCK_NB or croak "Failed to obtain lock: $!";
88             }
89             }
90              
91 106         314 $self->{positions} = $self->_read_file_fully();
92              
93 103         827 return $self;
94             }
95              
96             sub _read_file_fully {
97 106     106   976 my ($self) = @_;
98              
99 106         392 my $file = $self->{file};
100 106 100       3178 return unless -e $file;
101              
102 43 50       2293 open my $fh, '<', $file or die "Can't open '$file': $!";
103 43         73 my $content = do {local $/; <$fh>};
  43         208  
  43         2634  
104              
105 43         98 my @poss = ();
106 43         95 my $pos = {};
107 43         275 for my $line (split /\n/, $content) {
108 256 100       5350 if ($line =~ /^\s*(inode|committime|position):\s*(\d+)/) {
    100          
    100          
109 150         347 my $field = $_text2field{$1};
110 150 50       660 if (defined $pos->{$field}) {
111 0         0 die "Some pos-file inconsistency: '$field' defined twice";
112             }
113 150         487 $pos->{$field} = $2;
114             } elsif ($line =~ /^\s*(logfile|lastline):\s(.*)/) {
115 94         278 my $field = $_text2field{$1};
116 94 50       257 if (defined $pos->{$field}) {
117 0         0 die "Some pos-file inconsistency: '$field' defined twice";
118             }
119 94         437 $pos->{$field} = $2;
120             } elsif ($line =~ /^###$/) {
121 10 50       34 die "missing 'position:' in $file" unless defined $pos->{Position};
122 10         20 push @poss, $pos;
123 10         24 $pos = {};
124             }
125             }
126 43 100 50     454 if ($pos && scalar keys %$pos) {
127 40 50       193 die "missing 'position:' in $file" unless defined $pos->{Position};
128 40         82 push @poss, $pos;
129             }
130 43 100       201 die "missing 'position:' in $file" unless scalar @poss;
131              
132 40         785 return \@poss;
133             }
134              
135             sub read {
136 141     141 1 19175 my $self = shift;
137 141 100       1628 return undef unless defined $self->{positions};
138 91         117 return {%{$self->{positions}->[0]}};
  91         1231  
139             }
140              
141             sub _save_positions {
142 89     89   1570 my ($self, $poss) = @_;
143              
144 89         171 $self->{positions} = [ map { {%$_} } @$poss ];
  141         1297  
145              
146 89         21897 my $fh = File::Temp->new(DIR => dirname($self->{file}));
147              
148 89         87841 my $first = 1;
149 89         150 for my $pos (@{$self->{positions}}) {
  89         285  
150 141 100       581 $fh->print("###\n") unless $first;
151 141         378 $first = 0;
152 141         870 $fh->print("logfile: $pos->{LogFile}\n");
153 141         2201 $fh->print("position: $pos->{Position}\n");
154 141 50       887 if ($pos->{Inode}) {
155 141         602 $fh->print("inode: $pos->{Inode}\n");
156             }
157 141 100       1182 if ($pos->{LastLine}) {
158 139         527 $fh->print("lastline: $pos->{LastLine}\n");
159             }
160 141   66     5829 $pos->{CommitTime} ||= time;
161 141         613 $fh->print("committime: $pos->{CommitTime}\n");
162              
163 141         2095 my @to_clean;
164 141         588 for my $field (keys %$pos) {
165 706 100       1193 unless (grep { $_ eq $field } values %_text2field) {
  3530         7826  
166 1         2 push @to_clean, $field;
167             }
168             }
169 141 100       533 delete @{$pos}{@to_clean} if (scalar @to_clean);
  1         6  
170             }
171 89         7526 $fh->flush;
172 89 50       509 if ($fh->error) {
173 0         0 die 'print into '.$fh->filename.' failed';
174             }
175              
176 89 50       412 chmod(0644, $fh->filename) or die "Failed to chmod ".$fh->filename.": $!";
177 89 50       2966 rename($fh->filename, $self->{file}) or die "Failed to commit pos $self->{file}: $!";
178 89         14262 $fh->unlink_on_destroy(0);
179             }
180              
181             sub _commit_with_backups($$) {
182 86     86   174 my ($self, $pos) = @_;
183              
184 86         189 my $time = time;
185              
186 86         265 my $poss = $self->{positions};
187 86 100       290 unless ($poss) {
188 38         202 $self->_save_positions([$pos]);
189 38         4246 return;
190             }
191              
192 48 100 100     298 if ($poss->[0]->{Position} == $pos->{Position} && $poss->[0]->{LastLine} eq $pos->{LastLine} && $poss->[0]->{Inode} == $pos->{Inode}) {
      66        
193 5         24 return; # same position! do not write anything!
194             }
195              
196 43   33     83 my @times = map { $time - ($_->{CommitTime} || $time) } @$poss;
  75         325  
197 43         131 my @new_poss = ();
198 43 100 100     295 if ($times[0] > $self->{rollback} || scalar @times == 1) {
    100          
    50          
199 21         64 @new_poss = ($pos, $poss->[0]);
200             } elsif ($times[1] <= $self->{rollback}) {
201 14         34 @new_poss = @$poss;
202 14         23 $new_poss[0] = $pos;
203             } elsif ($times[1] > $self->{rollback}) {
204 8         27 @new_poss = ($pos, $poss->[0], $poss->[1]);
205             }
206 43         120 $self->_save_positions(\@new_poss);
207             }
208              
209             sub commit($$) {
210 94     94 1 1006828 my ($self, $pos) = @_;
211              
212 94 50       389 return unless defined $pos->{Position}; # pos is missing and log either => do nothing
213 94 100       536 return $self->_commit_with_backups($pos) if ($self->{rollback});
214              
215 8         33 $self->_save_positions([$pos]);
216             }
217              
218             sub rollback {
219 37     37 1 6962 my ($self) = @_;
220              
221 37 100       179 return 0 unless $self->{positions};
222 36 100       50 return 0 unless scalar @{$self->{positions}} > 1;
  36         161  
223              
224 17         33 shift @{$self->{positions}};
  17         42  
225 17         77 return 1;
226             }
227              
228             sub clean($) {
229 3     3 1 172 my ($self) = @_;
230 3 50       54 return unless -e $self->{file};
231 3 50       306 unlink $self->{file} or die "Can't remove $self->{file}: $!";
232 3         12 $self->{positions} = undef;
233             }
234              
235             sub DESTROY {
236 107     107   124295 my ($self) = @_;
237 107 100       2268 if ($self->{lock_fh}) {
238 4         156 flock $self->{lock_fh}, LOCK_UN;
239             }
240             }
241              
242             =back
243              
244             =cut
245              
246             1;