File Coverage

blib/lib/DBIx/QuickDB/Driver.pm
Criterion Covered Total %
statement 33 206 16.0
branch 0 82 0.0
condition 0 38 0.0
subroutine 11 43 25.5
pod 26 28 92.8
total 70 397 17.6


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