File Coverage

blib/lib/Pb/Command/Context.pm
Criterion Covered Total %
statement 34 171 19.8
branch 1 60 1.6
condition 0 12 0.0
subroutine 12 37 32.4
pod 15 15 100.0
total 62 295 21.0


line stmt bran cond sub pod time code
1             package Pb::Command::Context;
2              
3             our $VERSION = '0.02'; # VERSION
4              
5 1     1   7 use Moo;
  1         2  
  1         11  
6 1     1   435 use 5.14.0;
  1         3  
7 1     1   8 use autodie ':all';
  1         3  
  1         7  
8 1     1   6660 use MooX::HandlesVia;
  1         1019  
  1         9  
9 1     1   570 use namespace::autoclean;
  1         11772  
  1         4  
10              
11             extends 'Clone'; # so we have our own `clone` method
12              
13 1     1   104 use Fcntl qw< :flock >;
  1         2  
  1         132  
14 1     1   7 use File::Path qw< make_path >;
  1         2  
  1         55  
15 1     1   431 use Const::Fast;
  1         1017  
  1         5  
16 1     1   589 use Time::Piece;
  1         8895  
  1         5  
17 1     1   93 use File::Basename;
  1         10  
  1         273  
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   8 use overload '%{}' => sub { (grep { caller =~ /^$_\b/ } @RAW_ACCESS) ? $_[0] : $_[0]->_vars }, fallback => 1;
  1     3   2  
  1         6  
  3         47  
  6         87  
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 0         return 'ASKACTION' if $self->_opts->{interactive};
98 0           return 'ACTION';
99             }
100              
101             sub _safe_file_rw
102             {
103 0     0     my ($self, $file, $line) = @_;
104 0 0         my ($open_mode, $lock_mode) = defined $line ? ('>', LOCK_EX) : ('<', LOCK_SH);
105              
106             # This is essentially the same amount of paranoia that Proc::Pidfile undergoes. I just don't
107             # have to catch all the errors because I have `autodie` turned on.
108             eval
109 0           {
110 0           local *FILE;
111 0           open FILE, $open_mode, $file;
112 0           flock FILE, $lock_mode;
113 0 0         if ($open_mode eq '<')
114             {
115 0           $line = ;
116 0           chomp $line;
117             }
118             else
119             {
120 0           say FILE $line;
121             }
122 0           flock FILE, LOCK_UN;
123 0           close(FILE);
124             };
125 0 0         if ($@)
126             {
127 0 0         $self->raise_error("file read/write failure [" . $@ =~ s/ at .*? line \d+.*\n//sr . "]")
128             unless $@ =~ /^Can't open '$file' for reading:/;
129             }
130 0           return $line;
131             }
132              
133              
134              
135             ##################
136             # PUBLIC METHODS #
137             ##################
138              
139              
140 0     0 1   sub set_debug { my ($self, $level) = @_; $self->_vars->{DEBUG} = $level }
  0            
141              
142 0     0 1   sub set_var { my ($self, $var, $val) = @_; $self->_vars->{$var} = $val }
  0            
143              
144              
145              
146             # Currently, this just sets the `ERR` context var, but in the future it may do more.
147             sub raise_error
148             {
149 0     0 1   my ($self, $err) = @_;
150 0           $self->_vars->{ERR} = $err;
151             }
152              
153             # These are more specialized versions of `raise_error`; again, very simple to start with, but
154             # there's room for expansion.
155             sub syntax_error
156             {
157 0     0 1   my ($self, $err) = @_;
158 0           $self->_dont_update_statfile; # syntax errors shouldn't fire the `unless_clean_exit` condition
159 0           $self->raise_error($err);
160             }
161             sub usage_error
162             {
163 0     0 1   my ($self, $err) = @_;
164 0           $self->_dont_update_statfile; # usage errors shouldn't fire the `unless_clean_exit` condition
165 0           $self->raise_error($err);
166             }
167             sub start_conditions_not_met
168             {
169 0     0 1   my ($self, $err) = @_;
170             # Again, since the command is not going to get run, these errors can't fire off the
171             # `unless_clean_exit` condition.
172 0           $self->_dont_update_statfile;
173 0           $self->raise_error($err);
174             }
175              
176              
177             ######################
178             # STRUCTURE BUILDERS #
179             ######################
180              
181              
182             sub setup_context
183             {
184 0     0 1   my ($inv, $vars, $optdefs, $control) = @_;
185 0 0         my $self = ref $inv ? $inv->clone : $inv->new;
186              
187             # set whatever vars weren't already set
188 0           $self->set_var($_ => $vars->{$_}) foreach keys %$vars;
189              
190             # validate opts (this might also set some vars)
191 0           $self->validate_opts($optdefs);
192 0 0         return $self if $self->error; # no point in continuing if an opt was bobbled
193              
194             # have to do this at runtime so that we only create a logfile for the running command
195 0           $self->prep_logfile;
196             # have to this at run time so we have parsed options to work with
197 0           $self->_set_runmode( $self->_extrapolate_run_mode );
198              
199             # process control stuff; some of this might mean we have to bail out
200 0 0         unless ( $self->_process_control_structure($control) )
201             {
202             # this should never be necessary; `error` should always be set by `_process_control_structure`
203 0 0         $self->syntax_error('Unknown error processing control structure') unless $self->error;
204             }
205              
206 0           return $self;
207             }
208              
209             # This deals with all the stuff you can put in the "control structure (i.e. the hashref that follows
210             # the `control_via` keyword).
211             sub _process_control_structure
212             {
213 0     0     my ($self, $control) = @_;
214              
215 0           foreach (grep { exists $control->{$_} } qw< pidfile statusfile unless_clean_exit >)
  0            
216             {
217 0           my $value = delete $control->{$_};
218 0 0         if ($_ eq 'pidfile')
    0          
    0          
219             {
220 0 0         return undef unless $self->prep_pidfile($value);
221             }
222             elsif ($_ eq 'statusfile')
223             {
224 0           $self->_set_statfile($self->_prep_filename($value));
225             my $statfile = sub
226             {
227 0 0   0     if ($self->update_statfile)
228             {
229 0   0       my $exit_status = $self->error // $CLEAN_EXIT;
230 0           $self->_safe_file_rw($self->statfile, "last run: $exit_status at " . scalar localtime);
231             }
232 0           };
233             # have to use string `eval` here, otherwise the `END` will always fire
234 0           eval 'END { $statfile->() }';
235             }
236             elsif ($_ eq 'unless_clean_exit')
237             {
238 0 0         unless (defined $self->statfile)
239             {
240 0           $self->syntax_error("cannot specify `unless_clean_exit' without `statusfile'");
241 0           return undef;
242             }
243 0           my $lastrun = $self->_safe_file_rw($self->statfile);
244 0 0         return undef if $self->error;
245 0 0         if ($lastrun) # if not, probably means this is the first run
246             {
247 0           my ($last_exit) = $lastrun =~ /: (.*?) at /;
248 0 0         unless ($last_exit eq $CLEAN_EXIT)
249             {
250 0           $self->raise_error($last_exit); # in case our message wants to access %ERR
251 0           my $msg = $self->_expand_vars($value);
252 0           $self->start_conditions_not_met($msg); # this is the real (user-supplied) error message
253 0           return undef;
254             }
255             }
256             }
257             }
258 0 0         if ( %$control )
259             {
260 0           $self->syntax_error("unknown parameter(s) in control structure [" . join(',', sort keys %$control) . "]");
261 0           return undef;
262             }
263             else
264             {
265 0           return 1;
266             }
267             }
268              
269              
270              
271             sub connect_to
272             {
273 0     0 1   my ($self, $command) = @_;
274              
275             # This little dance is to find the ultimate parent command in case we end up with an inline
276             # subcommand or somesuch (viz. CLI::Osprey::InlineSubcommand).
277 0           my $top_level = $command;
278 0   0       $top_level = $top_level->parent_command while $top_level->can('parent_command') and $top_level->parent_command;
279              
280 0           $self->_set_toplevel_command($top_level);
281 0           $self->_vars->{ME} = $top_level->invoked_as;
282 0           $self->_parse_opts($command);
283             }
284              
285             # Build the options hash. Merges both local and global opts.
286             sub _parse_opts
287             {
288 0     0     my ($self, $command) = @_;
289 0           my $optobj_method = '_osprey_options';
290 0 0         my %opt_objects = $command->can($optobj_method) ? $command->$optobj_method : ();
291 0           $self->_opts->{$_} = $command->$_ foreach keys %opt_objects;
292             # get options from top-level command as well (these are the global opts)
293             {
294             # I'm pretty sure the top-level command will always have an options method.
295 0           my %opt_objects = $self->toplevel_command->$optobj_method;
  0            
296 0   0       $self->_opts->{$_} //= $self->toplevel_command->$_ foreach keys %opt_objects;
297             }
298             }
299              
300              
301              
302             sub validate_args
303             {
304 0     0 1   my $defs = pop;
305 0           my ($self, @args) = @_;
306              
307 0           foreach my $def (@$defs)
308             {
309 0           my $arg = shift @args;
310 0 0         return undef unless $self->_validate_value(arg => $def->{type}, $def->{name} => $arg);
311 0           $self->set_var($def->{name}, $arg);
312             }
313 0           return 1;
314             }
315              
316              
317             sub validate_opts
318             {
319 0     0 1   my ($self, $defs) = @_;
320              
321 0           foreach my $def (@$defs)
322             {
323 0           my $opt = $self->_opts->{ $def->{name} };
324 0 0         return undef unless $self->_validate_value(opt => $def->{type}, $def->{name} => $opt);
325             # unlike args, opts only get saved as context vars upon request
326 0 0         $self->set_var($def->{name}, $opt) if $def->{access_as_var};
327             }
328 0           return 1;
329             }
330              
331             # Consolidate error message into a private method for consistency.
332             sub _validate_value
333             {
334 0     0     my ($self, $thing, $type, $name, $value) = @_;
335              
336 0 0         if ($type->check($value))
337             {
338 0           return 1;
339             }
340             else
341             {
342 0           $self->usage_error("$thing $name fails validation [" . $type->validate($value) . "]");
343 0           return undef;
344             }
345             }
346              
347              
348              
349             sub prep_logfile
350             {
351 0     0 1   my $self = shift;
352 0 0         return unless $self->has_var('LOGFILE');
353 0           $self->_set_logfile( $self->_prep_filename($self->_vars->{LOGFILE}) );
354 0           return 1;
355             }
356              
357              
358              
359             sub prep_pidfile
360             {
361 0     0 1   my ($self, $filename) = @_;
362 0           require Proc::Pidfile;
363 0           my $pidfile = eval { Proc::Pidfile->new( pidfile => $self->_prep_filename($filename) ) };
  0            
364 0 0         if ($pidfile)
365             {
366 0           $self->_set_proc_pidfile($pidfile);
367             }
368             else
369             {
370 0 0         if ( $@ =~ /already running: (\d+)/ )
371             {
372 0           $self->start_conditions_not_met("previous instance already running [$1]");
373 0           return undef;
374             }
375             else
376             {
377 0           die; # rethrow
378             }
379             }
380 0           return 1;
381             }
382              
383              
384             1;
385              
386              
387              
388             # ABSTRACT: context object for a Pb command
389             # COPYRIGHT
390              
391             __END__