File Coverage

blib/lib/DBIx/QuickDB/Driver.pm
Criterion Covered Total %
statement 33 199 16.5
branch 0 72 0.0
condition 0 36 0.0
subroutine 11 42 26.1
pod 25 27 92.5
total 69 376 18.3


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