File Coverage

blib/lib/App/Manager.pm
Criterion Covered Total %
statement 45 220 20.4
branch 0 102 0.0
condition 0 30 0.0
subroutine 15 40 37.5
pod 0 11 0.0
total 60 403 14.8


line stmt bran cond sub pod time code
1             package App::Manager;
2              
3 1     1   687 use strict 'subs';
  1         1  
  1         30  
4 1     1   5 use Carp;
  1         2  
  1         90  
5 1     1   8301 use subs qw(LIBTRACER_SO LIBDIR S_ISLNK S_ISREG S_ISDIR S_IFMT);
  1         42  
  1         5  
6              
7             require Exporter;
8             require DynaLoader;
9              
10 1     1   1990 use IO::Socket::UNIX;
  1         441290  
  1         9  
11 1     1   851 use File::Spec 0.7;
  1         50  
  1         33  
12 1     1   6 use File::Basename;
  1         2  
  1         128  
13 1     1   8 use Cwd;
  1         1  
  1         87  
14 1     1   8 use Fcntl;
  1         2  
  1         346  
15 1     1   1151 use POSIX qw(EAGAIN);
  1         9138  
  1         6  
16              
17             BEGIN {
18 1     1   1058 $^W=0; # I'm fed up with bogus and unnecessary warnings nobody can turn off.
19              
20 1         16 @ISA = qw(Exporter DynaLoader);
21              
22 1         2 @EXPORT = qw(trace_program);
23 1         2 @EXPORT_OK = (@EXPORT,qw(slog S_ISLNK S_ISREG S_ISDIR S_IFMT));
24 1         895 $VERSION = '0.03';
25             }
26              
27             bootstrap App::Manager $VERSION;
28              
29             $verbose=0;
30              
31             $unix_path = (eval { File::Spec->tmpdir } || "/tmp")."/installtracer_socket$$~";
32              
33             sub slog($@) {
34 0 0   0 0   (print STDERR "APPMAN: ",@_,"\n") if $verbose => shift;
35             }
36              
37             my $sizeof_int = length pack "i",0;
38              
39             my $unix; # unix listening socket
40             my $fh; # the filehandle
41             my $change_cb; # call before every change
42              
43             sub xread($) {
44 0     0 0   my $len=shift;
45 0           my $buff;
46 0           while ($len) {
47 0           my $read = sysread $fh,$buff,$len,length($buff);
48 0 0 0       redo if !$read && $! == EAGAIN;
49 0 0         $read>0 or die "\n";
50 0           $len -= $read;
51             }
52 0           $buff;
53             }
54              
55 0     0 0   sub get_char() { xread 1 }
56 0     0 0   sub get_int() { unpack "i", xread $sizeof_int }
57 0     0 0   sub get_str() { xread get_int }
58              
59             # read cwd, pathname and canonicalize it
60             sub get_abspath() {
61 0     0 0   my $path = File::Spec->catdir(get_str,get_str);
62 0           my($base,$dir)=fileparse($path);
63            
64 0 0         $abspath{$dir} = Cwd::abs_path($dir) unless defined $abspath{$dir};
65 0           File::Spec->canonpath("$abspath{$dir}/$base$suffix");
66             }
67              
68             sub handle_msg {
69 0     0 0   my $type = get_char;
70              
71 0 0         if ($type eq "S") {
    0          
    0          
72 0           syswrite $fh,"s",1;
73             } elsif ($type eq "C") {
74 0           $change_cb->(get_abspath);
75             } elsif ($type eq "I") {
76 0           my $pid = get_int;
77             # process $pid just connected.. fine
78             } else {
79 0           die "illegal message received: MSG $type, pid $pid\n";
80             }
81 0           1;
82             }
83              
84 1     1   259 END { unlink $unix_path }
85              
86             sub init_tracer() {
87 0     0 0   $unix = new IO::Socket::UNIX Local => $unix_path, Listen => 1;
88 0 0         $unix or die "Unable to create unix domain socket '$unix_path' for listening: $!\n";
89              
90 0 0         -x LIBTRACER_SO
91             or die "FATAL: tracer helper object '".LIBTRACER_SO."' not executable!\n";
92             }
93              
94             sub stop_tracer() {
95 0     0 0   unlink $unix_path; undef $unix_path;
  0            
96             }
97              
98             sub run_tracer() {
99 0     0 0   my($rm,$r,$handles);
100              
101 0           vec($rm,fileno($unix),1)=1;
102              
103 0           while(!$server_quit) {
104 0 0         if(select($r=$rm,undef,undef,undef)>0) {
105 0 0 0       if ($unix && vec($r,fileno($unix),1)) {
106 0           $fh = $unix->accept;
107 0           $handles{fileno $fh} = $fh;
108 0           vec($rm,fileno($fh),1)=1;
109             }
110 0           for $f (keys(%handles)) {
111 0 0         if(vec($r,$f,1)) {
112 0           $fh=$handles{$f};
113 0 0         if(!eval { handle_msg }) {
  0            
114 0           vec($rm,$f,1)=0;
115 0           delete $handles{$f};
116 0           undef $fh;
117 0 0 0       die $@ if $@ && $@ ne "\n";
118             }
119             }
120             }
121             }
122             }
123             }
124              
125             # launch a single program and update %before hashes.
126             sub trace_program($@) {
127 0     0 0   $change_cb = shift;
128            
129 0           init_tracer;
130            
131 0           $server_quit = 0;
132 0     0     local $SIG{CHLD} = sub { $server_quit = 1 };
  0            
133              
134 0 0         if (fork == 0) {
135 0           $ENV{LD_PRELOAD}=join(":",LIBTRACER_SO,split /:/,$ENV{LD_PRELOAD});
136 0           $ENV{INSTALLTRACER_SOCKET}=$unix_path;
137 0           exec @_;
138 0           die "Unable to exec @_: $!\n";
139             }
140              
141 0           local $SIG{PIPE} = 'IGNORE';
142 0           local $SIG{QUIT} = 'IGNORE';
143 0           local $SIG{INT} = 'IGNORE';
144 0           local $SIG{HUP} = 'IGNORE';
145              
146 0           run_tracer;
147 0           stop_tracer;
148             }
149              
150             package App::Manager::DB;
151              
152 1     1   894 use Storable qw(nstore retrieve);
  1         3087  
  1         60  
153 1     1   693 use File::Copy;
  1         4514  
  1         57  
154 1     1   10 use App::Manager qw(S_ISDIR S_ISLNK S_ISREG S_IFMT slog);
  1         11  
  1         63  
155 1     1   617 use File::Compare;
  1         891  
  1         1469  
156              
157             sub new {
158 0     0     my $self = bless {},shift;
159 0           $self->{path} = shift;
160 0           $self;
161             }
162              
163             sub sync {
164 0     0     my $self=shift;
165 0 0         if ($self->{dirty}) {
166 0           my @unlink = @{delete $self->{unlink}};
  0            
167 0           $self->{dirty}=0;
168 0 0         nstore $self,$self->{path}."/db"
169             or die "Unable to freeze into $self->{path}/db: $!\n";
170 0           unlink @unlink;
171             }
172             }
173              
174             sub dirty {
175 0     0     my $self=shift;
176 0           $self->{mtime}=time;
177 0           $self->{dirty}++;
178             }
179              
180             sub DESTROY {
181 0     0     my $self=shift;
182 0           $self->sync;
183             }
184              
185             sub open {
186 0     0     shift;
187 0           my $path=App::Manager::LIBDIR."/".shift;
188 0           slog 1,"opening db $path";
189 0           retrieve $path."/db";
190             }
191              
192             sub creat {
193 0     0     my $self = bless {}, shift;
194 0           $self->{path} = App::Manager::LIBDIR."/".shift;
195 0           slog 1,"creating db $self->{path}";
196 0           system 'rm','-rf',$self->{path};
197 0 0         mkdir $self->{path},0777
198             or die "Unable to create database '$self->{path}': $!\n";
199 0           $self->{genfile} = 'Xaaaaaa';
200 0           $self->{ctime} = time; # now this really is the _creation_ time
201 0           $self->{version}=$VERSION;
202 0           $self->dirty;
203 0           $self;
204             }
205              
206             sub xlstat($) {
207 0     0     my @stat = lstat $_[0];
208 0 0         @stat ?
209             {
210             path => $_[0],
211             dev => $stat[ 0],
212             ino => $stat[ 1],
213             mode => $stat[ 2],
214             nlink => $stat[ 3],
215             uid => $stat[ 4],
216             gid => $stat[ 5],
217             rdev => $stat[ 6],
218             size => $stat[ 7],
219             atime => $stat[ 8],
220             mtime => $stat[ 9],
221             ctime => $stat[10],
222             blksize => $stat[11],
223             blocks => $stat[12],
224             }
225             :
226             {
227             path => $_[0],
228             }
229             }
230              
231             sub ci($$) {
232 0     0     my $self=shift;
233 0           my $stat=xlstat shift;
234 0           my $gen = $self->{genfile}++;
235              
236 0           $stat->{id} = $gen;
237              
238 0           $self->{storage}{$gen}=
239             $self->{source}{$stat->{path}}=$stat;
240              
241 0 0         if (defined $stat->{mode}) {
242 0 0         if (S_ISREG $stat->{mode}) {
    0          
    0          
243 0           $stat->{savetype} = 1; # none, stored
244 0           $stat->{savepath} = $self->{path}."/".$gen;
245 0 0         copy $stat->{path},$stat->{savepath}
246             or die "Unable to save away file '$stat->{path}': $!\n";
247             } elsif (App::Manager::S_ISLNK $stat->{mode}) {
248 0 0         $stat->{symlink}=readlink $stat->{path}
249             or die "Unable to read symlink '$stat->{path}': $!\n";
250             } elsif (App::Manager::S_ISDIR $stat->{mode}) {
251             # nothing to do
252             } else {
253 0           die "FATAL: Don't know how to check in $stat->{path}.\n";
254             }
255             }
256 0           $self->dirty;
257             }
258              
259             sub optimize($$) {
260 0     0     my $self=shift;
261 0           my $level=shift;
262 0           slog 1,"checking for differences between database and filesystem";
263 0           for my $stat (values (%{$self->{storage}})) {
  0            
264 0           my $msg;
265 0           my $nstat = xlstat $stat->{path};
266 0 0 0       if (defined $stat->{mode} || defined $nstat->{mode}) {
267 0 0 0       if (($stat->{mode} ^ $nstat->{mode}) & App::Manager::S_IFMT
268             || defined $stat->{mode} ^ defined $nstat->{mode}) {
269 0           $msg = "type changed";
270             } else {
271 0           my $samecontent;
272 0           $msg = "content changed";
273 0 0         if (S_ISREG $stat->{mode}) {
    0          
    0          
274 0           $samecontent = !compare ($stat->{path}, $stat->{savepath});
275 0 0         if ($samecontent) {
276 0           unlink delete $stat->{savepath};
277 0           $stat->{savetype} = 0;
278             }
279             } elsif (S_ISDIR $stat->{mode}) {
280 0           $samecontent = 1;
281             } elsif (S_ISLNK $stat->{mode}) {
282 0           $samecontent = $stat->{symlink} eq readlink $stat->{path};
283             }
284 0 0         $msg = "attributes changed" if $samecontent;
285 0 0 0       if ($samecontent
      0        
      0        
      0        
      0        
286             && $stat->{uid} eq $nstat->{uid}
287             && $stat->{gid} eq $nstat->{gid}
288             && $stat->{size} eq $nstat->{size}
289             && ($level > 0 || $stat->{mtime} eq $nstat->{mtime})) {
290 0           $msg = "no change";
291 0           delete $self->{storage}{$stat->{id}};
292 0           delete $self->{source}{$stat->{path}};
293             }
294             }
295 0           slog 3,"$stat->{path}: $msg";
296 0           $stat->{ctype} = $msg;
297             } else {
298 0           delete $self->{storage}{$stat->{id}};
299 0           delete $self->{source}{$stat->{path}};
300             }
301             }
302 0           $self->dirty;
303             }
304              
305             sub storage {
306 0     0     my $self=shift;
307 0           $self->{storage};
308             }
309              
310             sub remove($) {
311 0     0     my $path=shift;
312 0           lstat $path;
313 0 0         if (-e _) {
314 0 0         if (-d _) {
315 0 0         rmdir $path
316             or warn "WARNING: Unable to remove existing directory '$path': $!\n";
317             } else {
318 0 0         unlink $path
319             or die "Unable to remove existing object '$path': $!\n";
320             }
321             }
322             }
323              
324             sub recreate($) {
325 0     0     my $stat = shift;
326 0 0         if (defined $stat->{mode}) {
327 0 0         if (S_ISREG $stat->{mode}) {
    0          
    0          
328 0 0         if (exists $stat->{savepath}) {
329 0           remove $stat->{path};
330 0 0         $stat->{savetype} == 1
331             or die "Unknown savetype for file\n";
332 0 0         copy $stat->{savepath},$stat->{path}
333             or die "Unable to recreate file '$stat->{path}': $!\n";
334             }
335             } elsif (S_ISLNK $stat->{mode}) {
336 0           remove $stat->{path};
337 0 0         symlink $stat->{symlink},$stat->{path}
338             or die "Unable to recreate symbolic link '$stat->{path}' => '$stat->{symlink}': $!\n";
339             } elsif (S_ISDIR $stat->{mode}) {
340 0           remove $stat->{path};
341 0 0         mkdir $stat->{path},$stat->{mode} & 07777
342             or die "Unable to recreate directory '$stat->{path}': $!\n";
343             } else {
344 0           die "FATAL: don't know how to check in $stat->{path}.\n";
345             }
346 0 0         unless (S_ISLNK $stat->{mode}) {
347 0 0         chmod $stat->{mode} & 07777,$stat->{path}
348             or die "Unable to change mode for '$stat->{path}': $!\n";
349 0 0         chown $stat->{uid},$stat->{gid},$stat->{path}
350             or warn "Unable to change user and group id for '$stat->{path}': $!\n";
351 0 0         utime $stat->{atime},$stat->{mtime},$stat->{path}
352             or warn "Unable to change atime and mtime for '$stat->{path}': $!\n";
353             }
354             }
355             }
356              
357             # this is safe as long as we don't sync too early
358             sub swap {
359 0     0     my $self=shift;
360 0           slog 1,"swapping all changes";
361 0           for my $stat (reverse sort values %{$self->{storage}}) {
  0            
362 0           slog 2,"swapping $stat->{path}";
363             # saving old version
364 0           $self->ci($stat->{path});
365 0           recreate $stat;
366 0 0         push @{$self->{unlink}},$stat->{savepath} if exists $stat->{savepath};
  0            
367 0           delete $self->{storage}{$stat->{id}};
368             }
369 0           slog 2,"syncing database";
370 0           $self->dirty;
371 0           $self->sync;
372 0           slog 2,"optimizing database";
373 0           $App::Manager::verbose=0;
374 0           local $App::Manager::verbose=0;
375 0           $self->optimize(0);
376             }
377              
378             1;
379              
380             __END__