File Coverage

blib/lib/Pb/Command/Context.pm
Criterion Covered Total %
statement 34 170 20.0
branch 1 58 1.7
condition 0 12 0.0
subroutine 12 37 32.4
pod 15 15 100.0
total 62 292 21.2


line stmt bran cond sub pod time code
1             package Pb::Command::Context;
2              
3             our $VERSION = '0.01_03'; # TRIAL VERSION
4              
5 1     1   8 use Moo;
  1         2  
  1         6  
6 1     1   351 use 5.14.0;
  1         3  
7 1     1   6 use autodie ':all';
  1         2  
  1         7  
8 1     1   5222 use MooX::HandlesVia;
  1         626  
  1         6  
9 1     1   503 use namespace::autoclean;
  1         9608  
  1         5  
10              
11             extends 'Clone'; # so we have our own `clone` method
12              
13 1     1   74 use Fcntl qw< :flock >;
  1         2  
  1         97  
14 1     1   5 use File::Path qw< make_path >;
  1         2  
  1         41  
15 1     1   363 use Const::Fast;
  1         832  
  1         5  
16 1     1   483 use Time::Piece;
  1         7127  
  1         4  
17 1     1   75 use File::Basename;
  1         7  
  1         246  
18              
19              
20             # Default values for vars here; most values are set as we go along.
21             my %DEFAULT_CONTEXT =
22             (
23             DEBUG => 0,
24             TIME => localtime($^T)->strftime("%Y%m%d%H%M%S"),
25             DATE => localtime($^T)->strftime("%Y%m%d"),
26             );
27              
28             # This is how we tell if we didn't have an error on the last run.
29             my $CLEAN_EXIT = 'exited cleanly';
30              
31              
32             ##############
33             # ATTRIBUTES #
34             ##############
35              
36              
37              
38             # These are the actual context vars that flows can access via hash deferencing.
39             has _vars => ( is => 'ro', default => sub { +{%DEFAULT_CONTEXT} }, handles_via => 'Hash',
40             handles => { var => 'get', has_var => 'exists', }, );
41             # These are the options (both command-specific and global) for the running command.
42             has _opts => ( is => 'ro', default => sub { +{ } }, handles_via => 'Hash',
43             handles => { opt => 'get', opts => 'elements', }, );
44              
45             # The `@RAW_ACCESS` lists packages that are allowed to access our internals directly. Everyone else
46             # who treats us like a hash reference gets the hash of context vars instead. This is how we get
47             # around the infinite dereferencing loop we would otherwise engender for being a blessed hash that
48             # defines an overloaded hash dereference operator. See `perldoc overload` for more details.
49             # (This method is a bit hacky, but effective, and fairly quick.)
50             my @RAW_ACCESS = qw< Method::Generate::Accessor Pb::Command::Context >;
51 1 50   1   7 use overload '%{}' => sub { (grep { caller =~ /^$_\b/ } @RAW_ACCESS) ? $_[0] : $_[0]->_vars }, fallback => 1;
  1     3   1  
  1         5  
  3         42  
  6         71  
52              
53             # Simple attributes; free to read, but only certain people can write to them.
54             has runmode => ( is => 'rwp', );
55             has statfile => ( is => 'rwp', );
56             has proc_pidfile => ( is => 'rwp', );
57             has toplevel_command => ( is => 'rwp', );
58              
59             # Do we, or do we not, update the statfile (if any) when we exit?
60             has update_statfile => ( is => 'rwp', default => 1, );
61 0     0     sub _dont_update_statfile { my $self = shift; $self->_set_update_statfile(0); }
  0            
62              
63             # pseudo-attributes
64             # (Mostly context vars posing as attributes, but also some attributes' attributes.)
65              
66              
67 0     0 1   sub error { my $self = shift; $self->_vars->{ERR} }
  0            
68 0     0 1   sub logfile { my $self = shift; $self->_vars->{LOGFILE} }
  0            
69 0 0   0 1   sub pidfile { my $self = shift; my $ppf = $self->proc_pidfile; $ppf ? $ppf->pidfile : undef }
  0            
  0            
70              
71 0     0     sub _set_logfile { my ($self, $file) = @_; $self->_vars->{LOGFILE} = $file; }
  0            
72              
73              
74             ##################
75             # HELPER METHODS #
76             ##################
77              
78             sub _expand_vars
79             {
80 0     0     my ($self, $string) = @_;
81 0   0       $string =~ s{%(\w+)}{ $self->_vars->{$1} // $self->raise_error("variable $1 used in expansion but never defined") }ge;
  0            
82 0           return $string;
83             }
84              
85             sub _prep_filename
86             {
87 0     0     my ($self, $file) = @_;
88 0           $file = $self->_expand_vars($file);
89 0           make_path(dirname($file));
90 0           return $file;
91             }
92              
93             sub _extrapolate_run_mode
94             {
95 0     0     my ($self) = @_;
96 0 0         return 'NOACTION' if $self->_opts->{pretend};
97 0           return 'ACTION';
98             }
99              
100             sub _safe_file_rw
101             {
102 0     0     my ($self, $file, $line) = @_;
103 0 0         my ($open_mode, $lock_mode) = defined $line ? ('>', LOCK_EX) : ('<', LOCK_SH);
104              
105             # This is essentially the same amount of paranoia that Proc::Pidfile undergoes. I just don't
106             # have to catch all the errors because I have `autodie` turned on.
107             eval
108 0           {
109 0           local *FILE;
110 0           open FILE, $open_mode, $file;
111 0           flock FILE, $lock_mode;
112 0 0         if ($open_mode eq '<')
113             {
114 0           $line = ;
115 0           chomp $line;
116             }
117             else
118             {
119 0           say FILE $line;
120             }
121 0           flock FILE, LOCK_UN;
122 0           close(FILE);
123             };
124 0 0         if ($@)
125             {
126 0 0         $self->raise_error("file read/write failure [" . $@ =~ s/ at .*? line \d+.*\n//sr . "]")
127             unless $@ =~ /^Can't open '$file' for reading:/;
128             }
129 0           return $line;
130             }
131              
132              
133              
134             ##################
135             # PUBLIC METHODS #
136             ##################
137              
138              
139 0     0 1   sub set_debug { my ($self, $level) = @_; $self->_vars->{DEBUG} = $level }
  0            
140              
141 0     0 1   sub set_var { my ($self, $var, $val) = @_; $self->_vars->{$var} = $val }
  0            
142              
143              
144              
145             # Currently, this just sets the `ERR` context var, but in the future it may do more.
146             sub raise_error
147             {
148 0     0 1   my ($self, $err) = @_;
149 0           $self->_vars->{ERR} = $err;
150             }
151              
152             # These are more specialized versions of `raise_error`; again, very simple to start with, but
153             # there's room for expansion.
154             sub syntax_error
155             {
156 0     0 1   my ($self, $err) = @_;
157 0           $self->_dont_update_statfile; # syntax errors shouldn't fire the `unless_clean_exit` condition
158 0           $self->raise_error($err);
159             }
160             sub usage_error
161             {
162 0     0 1   my ($self, $err) = @_;
163 0           $self->_dont_update_statfile; # usage errors shouldn't fire the `unless_clean_exit` condition
164 0           $self->raise_error($err);
165             }
166             sub start_conditions_not_met
167             {
168 0     0 1   my ($self, $err) = @_;
169             # Again, since the command is not going to get run, these errors can't fire off the
170             # `unless_clean_exit` condition.
171 0           $self->_dont_update_statfile;
172 0           $self->raise_error($err);
173             }
174              
175              
176             ######################
177             # STRUCTURE BUILDERS #
178             ######################
179              
180              
181             sub setup_context
182             {
183 0     0 1   my ($inv, $vars, $optdefs, $control) = @_;
184 0 0         my $self = ref $inv ? $inv->clone : $inv->new;
185              
186             # set whatever vars weren't already set
187 0           $self->set_var($_ => $vars->{$_}) foreach keys %$vars;
188              
189             # validate opts (this might also set some vars)
190 0           $self->validate_opts($optdefs);
191 0 0         return $self if $self->error; # no point in continuing if an opt was bobbled
192              
193             # have to do this at runtime so that we only create a logfile for the running command
194 0           $self->prep_logfile;
195             # have to this at run time so we have parsed options to work with
196 0           $self->_set_runmode( $self->_extrapolate_run_mode );
197              
198             # process control stuff; some of this might mean we have to bail out
199 0 0         unless ( $self->_process_control_structure($control) )
200             {
201             # this should never be necessary; `error` should always be set by `_process_control_structure`
202 0 0         $self->syntax_error('Unknown error processing control structure') unless $self->error;
203             }
204              
205 0           return $self;
206             }
207              
208             # This deals with all the stuff you can put in the "control structure (i.e. the hashref that follows
209             # the `control_via` keyword).
210             sub _process_control_structure
211             {
212 0     0     my ($self, $control) = @_;
213              
214 0           foreach (grep { exists $control->{$_} } qw< pidfile statusfile unless_clean_exit >)
  0            
215             {
216 0           my $value = delete $control->{$_};
217 0 0         if ($_ eq 'pidfile')
    0          
    0          
218             {
219 0 0         return undef unless $self->prep_pidfile($value);
220             }
221             elsif ($_ eq 'statusfile')
222             {
223 0           $self->_set_statfile($self->_prep_filename($value));
224             my $statfile = sub
225             {
226 0 0   0     if ($self->update_statfile)
227             {
228 0   0       my $exit_status = $self->error // $CLEAN_EXIT;
229 0           $self->_safe_file_rw($self->statfile, "last run: $exit_status at " . scalar localtime);
230             }
231 0           };
232             # have to use string `eval` here, otherwise the `END` will always fire
233 0           eval 'END { $statfile->() }';
234             }
235             elsif ($_ eq 'unless_clean_exit')
236             {
237 0 0         unless (defined $self->statfile)
238             {
239 0           $self->syntax_error("cannot specify `unless_clean_exit' without `statusfile'");
240 0           return undef;
241             }
242 0           my $lastrun = $self->_safe_file_rw($self->statfile);
243 0 0         return undef if $self->error;
244 0 0         if ($lastrun) # if not, probably means this is the first run
245             {
246 0           my ($last_exit) = $lastrun =~ /: (.*?) at /;
247 0 0         unless ($last_exit eq $CLEAN_EXIT)
248             {
249 0           $self->raise_error($last_exit); # in case our message wants to access %ERR
250 0           my $msg = $self->_expand_vars($value);
251 0           $self->start_conditions_not_met($msg); # this is the real (user-supplied) error message
252 0           return undef;
253             }
254             }
255             }
256             }
257 0 0         if ( %$control )
258             {
259 0           $self->syntax_error("unknown parameter(s) in control structure [" . join(',', sort keys %$control) . "]");
260 0           return undef;
261             }
262             else
263             {
264 0           return 1;
265             }
266             }
267              
268              
269              
270             sub connect_to
271             {
272 0     0 1   my ($self, $command) = @_;
273              
274             # This little dance is to find the ultimate parent command in case we end up with an inline
275             # subcommand or somesuch (viz. CLI::Osprey::InlineSubcommand).
276 0           my $top_level = $command;
277 0   0       $top_level = $top_level->parent_command while $top_level->can('parent_command') and $top_level->parent_command;
278              
279 0           $self->_set_toplevel_command($top_level);
280 0           $self->_vars->{ME} = $top_level->invoked_as;
281 0           $self->_parse_opts($command);
282             }
283              
284             # Build the options hash. Merges both local and global opts.
285             sub _parse_opts
286             {
287 0     0     my ($self, $command) = @_;
288 0           my $optobj_method = '_osprey_options';
289 0 0         my %opt_objects = $command->can($optobj_method) ? $command->$optobj_method : ();
290 0           $self->_opts->{$_} = $command->$_ foreach keys %opt_objects;
291             # get options from top-level command as well (these are the global opts)
292             {
293             # I'm pretty sure the top-level command will always have an options method.
294 0           my %opt_objects = $self->toplevel_command->$optobj_method;
  0            
295 0   0       $self->_opts->{$_} //= $self->toplevel_command->$_ foreach keys %opt_objects;
296             }
297             }
298              
299              
300              
301             sub validate_args
302             {
303 0     0 1   my $defs = pop;
304 0           my ($self, @args) = @_;
305              
306 0           foreach my $def (@$defs)
307             {
308 0           my $arg = shift @args;
309 0 0         return undef unless $self->_validate_value(arg => $def->{type}, $def->{name} => $arg);
310 0           $self->set_var($def->{name}, $arg);
311             }
312 0           return 1;
313             }
314              
315              
316             sub validate_opts
317             {
318 0     0 1   my ($self, $defs) = @_;
319              
320 0           foreach my $def (@$defs)
321             {
322 0           my $opt = $self->_opts->{ $def->{name} };
323 0 0         return undef unless $self->_validate_value(opt => $def->{type}, $def->{name} => $opt);
324             # unlike args, opts only get saved as context vars upon request
325 0 0         $self->set_var($def->{name}, $opt) if $def->{access_as_var};
326             }
327 0           return 1;
328             }
329              
330             # Consolidate error message into a private method for consistency.
331             sub _validate_value
332             {
333 0     0     my ($self, $thing, $type, $name, $value) = @_;
334              
335 0 0         if ($type->check($value))
336             {
337 0           return 1;
338             }
339             else
340             {
341 0           $self->usage_error("$thing $name fails validation [" . $type->validate($value) . "]");
342 0           return undef;
343             }
344             }
345              
346              
347              
348             sub prep_logfile
349             {
350 0     0 1   my $self = shift;
351 0 0         return unless $self->has_var('LOGFILE');
352 0           $self->_set_logfile( $self->_prep_filename($self->_vars->{LOGFILE}) );
353 0           return 1;
354             }
355              
356              
357              
358             sub prep_pidfile
359             {
360 0     0 1   my ($self, $filename) = @_;
361 0           require Proc::Pidfile;
362 0           my $pidfile = eval { Proc::Pidfile->new( pidfile => $self->_prep_filename($filename) ) };
  0            
363 0 0         if ($pidfile)
364             {
365 0           $self->_set_proc_pidfile($pidfile);
366             }
367             else
368             {
369 0 0         if ( $@ =~ /already running: (\d+)/ )
370             {
371 0           $self->start_conditions_not_met("previous instance already running [$1]");
372 0           return undef;
373             }
374             else
375             {
376 0           die; # rethrow
377             }
378             }
379 0           return 1;
380             }
381              
382              
383             1;
384              
385              
386              
387             # ABSTRACT: context object for a Pb command
388             # COPYRIGHT
389              
390             __END__