File Coverage

blib/lib/DBIx/QuickDB/Driver.pm
Criterion Covered Total %
statement 33 215 15.3
branch 0 90 0.0
condition 0 38 0.0
subroutine 11 45 24.4
pod 26 30 86.6
total 70 418 16.7


line stmt bran cond sub pod time code
1             package DBIx::QuickDB::Driver;
2 19     19   10551 use strict;
  19         60  
  19         633  
3 19     19   84 use warnings;
  19         49  
  19         1270  
4              
5             our $VERSION = '0.000040';
6              
7 19     19   93 use Carp qw/croak confess/;
  19         28  
  19         1059  
8 19     19   108 use File::Path qw/remove_tree/;
  19         54  
  19         909  
9 19     19   1727 use File::Temp qw/tempdir/;
  19         20535  
  19         904  
10 19     19   90 use POSIX ":sys_wait_h";
  19         42  
  19         161  
11 19     19   11816 use Scalar::Util qw/blessed/;
  19         41  
  19         872  
12 19     19   151 use Time::HiRes qw/sleep time/;
  19         25  
  19         178  
13              
14 19     19   2661 use DBIx::QuickDB::Util qw/clone_dir/;
  19         46  
  19         169  
15              
16 19     19   9041 use DBIx::QuickDB::Watcher;
  19         47  
  19         792  
17              
18 19         89 use DBIx::QuickDB::Util::HashBase qw{
19             -root_pid
20             -dir
21             -_cleanup
22             -autostop -autostart
23             verbose
24             -_log_id
25             username
26             password
27             env_vars
28            
29            
30 19     19   109 };
  19         30  
31              
32 0     0 1   sub viable { (0, "viable() is not implemented for the " . $_[0]->name . " driver") }
33              
34 0     0 1   sub socket { confess "socket() is not implemented for the " . $_[0]->name . " driver" }
35 0     0 1   sub load_sql { confess "load_sql() is not implemented for the " . $_[0]->name . " driver" }
36 0     0     sub bootstrap { confess "bootstrap() is not implemented for the " . $_[0]->name . " driver" }
37 0     0 1   sub connect_string { confess "connect_string() is not implemented for the " . $_[0]->name . " driver" }
38 0     0 1   sub start_command { confess "start_command() is not implemented for the " . $_[0]->name . " driver" }
39 0     0 1   sub shell_command { confess "shell_command() is not implemented for the " . $_[0]->name . " driver" }
40              
41 0     0 0   sub error_log { undef }
42              
43             sub read_error_log {
44 0     0 0   my $self = shift;
45 0 0         my $log = $self->error_log or return "";
46 0 0         return "" unless -f $log;
47 0 0         open(my $fh, '<', $log) or return "Could not open error log '$log': $!";
48 0           return join "" => <$fh>;
49             }
50              
51 0     0 1   sub list_env_vars { qw/DBI_USER DBI_PASS DBI_DSN/ }
52              
53 0     0 1   sub version_string { 'unknown' }
54              
55 0     0 1   sub stop_sig { 'TERM' }
56              
57       0 1   sub write_config {}
58              
59             sub do_in_env {
60 0     0 1   my $self = shift;
61 0           my ($code) = @_;
62              
63 0           my $old = $self->mask_env_vars;
64              
65 0           my $ok = eval { $code->(); 1 };
  0            
  0            
66 0           my $err = $@;
67              
68 0           $self->unmask_env_vars($old);
69              
70 0 0         die $err unless $ok;
71              
72 0           return;
73             }
74              
75             sub mask_env_vars {
76 0     0 1   my $self = shift;
77              
78 0           my %old;
79              
80 0           for my $var ($self->list_env_vars) {
81 0 0         next unless defined $ENV{$var};
82 0           $old{$var} = delete $ENV{$var};
83             }
84              
85 0   0       my $env_vars = $self->env_vars || {};
86 0           for my $var (keys %$env_vars) {
87 0 0         $old{$var} = delete $ENV{$var} unless defined $old{$var};
88 0           $ENV{$var} = $env_vars->{$var};
89             }
90              
91 0           return \%old;
92             }
93              
94             sub unmask_env_vars {
95 0     0 1   my $self = shift;
96 0           my ($old) = @_;
97              
98 0           for my $var (keys %$old) {
99 0           my $val = $old->{$var};
100              
101 0 0         if (defined $val) {
102 0           $ENV{$var} = $val;
103             }
104             else {
105 0           delete $ENV{$var};
106             }
107             }
108              
109 0           return;
110             }
111              
112             sub name {
113 0     0 1   my $in = shift;
114 0   0       my $type = blessed($in) || $in;
115              
116 0           $type =~ s/^DBIx::QuickDB::Driver:://;
117              
118 0           return $type;
119             }
120              
121             sub init {
122 0     0 1   my $self = shift;
123              
124 0 0         confess "'dir' is a required attribute" unless $self->{+DIR};
125              
126 0           $self->{+ROOT_PID} = $$;
127 0           $self->{+_CLEANUP} = delete $self->{cleanup};
128              
129 0 0         $self->{+USERNAME} = '' unless defined $self->{+USERNAME};
130 0 0         $self->{+PASSWORD} = '' unless defined $self->{+PASSWORD};
131              
132 0   0       $self->{+ENV_VARS} ||= {};
133              
134 0           return;
135             }
136              
137             sub clone_data {
138 0     0 1   my $self = shift;
139              
140             return (
141             USERNAME() => $self->{+USERNAME},
142             PASSWORD() => $self->{+PASSWORD},
143             VERBOSE() => $self->{+VERBOSE},
144             AUTOSTOP() => $self->{+AUTOSTOP},
145             AUTOSTART() => $self->{+AUTOSTART},
146              
147             cleanup => $self->{+_CLEANUP},
148              
149 0           ENV_VARS() => {%{$self->{+ENV_VARS}}},
  0            
150             );
151             }
152              
153             sub resync {
154 0     0 1   my $self = shift;
155              
156 0 0         my $from = $self->{+CLONED_FROM} or croak "No original source to sync from";
157              
158 0           my $started = $self->started;
159 0 0         $self->stop if $started;
160              
161 0 0 0       clone_dir($from, $self->{+DIR}, verbose => (($self->{+VERBOSE} // 0) > 2) ? 1 : 0, checksum => 1);
162              
163 0           $self->write_config();
164              
165 0 0         $self->start if $started;
166             }
167              
168             sub clone {
169 0     0 1   my $self = shift;
170 0           my %params = @_;
171              
172 0 0         confess "Cannot clone a started database, please stop it first."
173             if $self->started;
174              
175 0           my $orig_dir = $self->{+DIR};
176 0 0 0       my $new_dir = delete $params{dir} // tempdir('DB-QUICK-CLONE-XXXXXX', CLEANUP => 0, $ENV{QDB_TMPDIR} ? (DIR => $ENV{QDB_TMPDIR}) : (TMPDIR => 1));
177              
178 0 0 0       clone_dir($orig_dir, $new_dir, verbose => (($self->{+VERBOSE} // 0) > 2) ? 1 : 0);
179              
180 0           my $class = ref($self);
181             my %ok = (
182             cleanup => 1,
183 0           map {$_ => 1} DBIx::QuickDB::Util::HashBase::attr_list($class),
  0            
184             );
185 0           my @bad = grep { !$ok{$_} } keys %params;
  0            
186              
187 0 0         confess "Invalid options to clone(): " . join(', ' => @bad)
188             if @bad;
189              
190 0           my $clone = $class->new(
191             $self->clone_data,
192              
193             %params,
194              
195             DIR() => $new_dir,
196              
197             WATCHER() => undef,
198              
199             CLONED_FROM() => $orig_dir,
200             );
201              
202 0           $clone->write_config();
203 0 0         $clone->start if $clone->{+AUTOSTART};
204              
205 0           return $clone;
206             }
207              
208             sub gen_log {
209 0     0 0   my $self = shift;
210 0 0         return if $self->no_log(@_);
211 0           return $self->{+DIR} . "/cmd-log-$$-" . $self->{+_LOG_ID}++;
212             }
213              
214             sub no_log {
215 0     0 1   my $self = shift;
216 0           my ($params) = @_;
217 0   0       return $self->{+VERBOSE} || $params->{no_log} || $ENV{DB_VERBOSE};
218             }
219              
220             sub run_command {
221 0     0 1   my $self = shift;
222 0           my ($cmd, $params) = @_;
223              
224 0           my $no_log = $self->no_log($params);
225 0   0       my $log_file = $params->{log_file} || ($no_log ? undef : $self->gen_log);
226              
227 0           my $pid = fork();
228 0 0         croak "Could not fork" unless defined $pid;
229              
230 0 0         if ($pid) {
231 0           local $?;
232 0 0         return ($pid, $log_file) if $params->{no_wait};
233 0           my $ret = waitpid($pid, 0);
234 0           my $exit = $?;
235 0 0         die "waitpid returned $ret" unless $ret == $pid;
236              
237 0 0         return unless $exit;
238              
239 0           my $log = "";
240 0 0         unless ($no_log) {
241 0 0         open(my $fh, '<', $log_file) or warn "Failed to open log: $!";
242 0           $log = eval { join "" => <$fh> };
  0            
243             }
244 0           my $error_log = $self->read_error_log;
245 0 0         $log .= "\n=== error log ===\n$error_log" if length $error_log;
246 0           croak "Failed to run command '" . join(' ' => @$cmd) . "' ($exit)\n$log";
247             }
248              
249 0           $self->mask_env_vars;
250              
251 0 0         unless ($no_log) {
252 0 0         open(my $log, '>', $log_file) or die "Could not open log file ($log_file): $!";
253 0           close(STDOUT);
254 0           open(STDOUT, '>&', $log);
255 0           close(STDERR);
256 0           open(STDERR, '>&', $log);
257             }
258              
259 0 0         if (my $file = $params->{stdin}) {
260 0           close(STDIN);
261 0 0         open(STDIN, '<', $file) or die "Could not open new STDIN ($file): $!";
262             }
263              
264 0           exec(@$cmd);
265             }
266              
267 0     0 1   sub should_cleanup { shift->{+_CLEANUP} }
268              
269             sub cleanup {
270 0     0 1   my $self = shift;
271              
272             # Ignore errors here.
273 0           my $err = [];
274 0 0         remove_tree($self->{+DIR}, {safe => 1, error => \$err}) if -d $self->{+DIR};
275 0           return;
276             }
277              
278             sub connect {
279 0     0 1   my $self = shift;
280 0           my ($db_name, %params) = @_;
281              
282 0 0         %params = (AutoCommit => 1, RaiseError => 1) unless @_ > 1;
283              
284 0           my $dbh;
285             $self->do_in_env(
286             sub {
287 0     0     my $cstring = $self->connect_string($db_name);
288 0           require DBI;
289 0           $dbh = DBI->connect($cstring, $self->username, $self->password, \%params);
290             }
291 0           );
292              
293 0           return $dbh;
294             }
295              
296             sub started {
297 0     0 0   my $self = shift;
298              
299 0           my $socket = $self->socket;
300 0 0 0       return 1 if $self->{+WATCHER} || -S $socket;
301 0           return 0;
302             }
303              
304             sub start {
305 0     0 1   my $self = shift;
306 0           my @args = @_;
307              
308 0           my $dir = $self->{+DIR};
309 0           my $socket = $self->socket;
310              
311 0 0 0       return if $self->{+WATCHER} || -S $socket;
312              
313 0           my $watcher = $self->{+WATCHER} = DBIx::QuickDB::Watcher->new(db => $self, args => \@args);
314              
315 0           my $start = time;
316 0           until (-S $socket) {
317 0           my $waited = time - $start;
318              
319 0 0         if ($waited > 10) {
320 0           my $error_log = $self->read_error_log;
321 0           $watcher->eliminate();
322 0           confess "Timed out waiting for server to start\n$error_log";
323 0           last;
324             }
325              
326 0           sleep 0.01;
327             }
328              
329 0           return;
330             }
331              
332             sub stop {
333 0     0 1   my $self = shift;
334 0           my %params = @_;
335              
336 0 0         my $watcher = delete $self->{+WATCHER} or return;
337              
338             DBI->visit_handles(
339             sub {
340 0     0     my ($driver_handle) = @_;
341              
342             $driver_handle->disconnect
343             if $driver_handle->{Type} && $driver_handle->{Type} eq 'db'
344 0 0 0       && $driver_handle->{Name} && index($driver_handle->{Name}, $self->{+DIR}) >= 0;
      0        
      0        
345              
346 0           return 1;
347             }
348 0           );
349              
350 0           $watcher->stop();
351              
352 0           my $start = time;
353 0 0         unless ($params{no_wait}) {
354 0           $watcher->wait();
355              
356 0           while (-S $self->socket) {
357 0           my $waited = time - $start;
358              
359 0 0         if ($waited > 10) {
360 0           confess "Timed out waiting for server to stop";
361 0           last;
362             }
363              
364 0           sleep 0.01;
365             }
366             }
367              
368 0           return;
369             }
370              
371             sub shell {
372 0     0 1   my $self = shift;
373 0           my ($db_name) = @_;
374 0 0         $db_name = 'quickdb' unless defined $db_name;
375              
376 0           system($self->shell_command($db_name));
377             }
378              
379             sub DESTROY {
380 0     0     my $self = shift;
381 0 0 0       return unless $self->{+ROOT_PID} && $self->{+ROOT_PID} == $$;
382              
383 0 0         if (my $watcher = delete $self->{+WATCHER}) {
    0          
384 0           $watcher->eliminate();
385             }
386             elsif ($self->should_cleanup) {
387 0           $self->cleanup();
388             }
389              
390 0           return;
391             }
392              
393             1;
394              
395             __END__