]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Utils/Cronscript.pm
ab2152b39b8523f1fe5c7b78264f29b8d16a3870
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Utils / Cronscript.pm
1 package OpenILS::Utils::Cronscript;
2
3 # ---------------------------------------------------------------
4 # Copyright (C) 2010 Equinox Software, Inc
5 # Author: Joe Atzberger <jatzberger@esilibrary.com>
6 #
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License
9 # as published by the Free Software Foundation; either version 2
10 # of the License, or (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 # ---------------------------------------------------------------
17
18 # The purpose of this module is to consolidate the common aspects
19 # of various cron tasks that all need the same things:
20 #    ~ non-duplicative processing, i.e. lockfiles and lockfile checking
21 #    ~ opensrf_core.xml file location 
22 #    ~ common options like help and debug
23
24 use strict;
25 use warnings;
26
27 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
28 use OpenSRF::System;
29 use OpenSRF::AppSession;
30 use OpenSRF::Utils::JSON;
31 use OpenSRF::EX qw(:try);
32 use OpenILS::Utils::Fieldmapper;
33 use OpenILS::Utils::Lockfile;
34 use OpenILS::Utils::CStoreEditor q/:funcs/;
35
36 use File::Basename qw/fileparse/;
37
38 use Data::Dumper;
39 use Carp;
40
41 our @extra_opts = (     # additional keys are stored here
42     # 'addlopt'
43 );
44
45 our $debug = 0;
46
47 sub _default_self {
48     return {
49     #   opts       => {},
50     #   opts_clean => {},
51     #   default_opts_clean => {},
52         default_opts       => {
53             'lock-file=s'   => OpenILS::Utils::Lockfile::default_filename,
54             'osrf-config=s' => '/openils/conf/opensrf_core.xml',   # TODO: packaging needs a make variable like @@EG_CONF_DIR@@
55             'debug'         => 0,
56             'verbose+'      => 0,
57             'help'          => 0,
58           # 'internal_var'  => 'XYZ',
59         },
60     #   lockfile => undef,
61     #   session => undef,
62     #   bootstrapped => 0,
63     #   got_options => 0,
64         auto_get_options_4_bootstrap => 1,
65     };
66 }
67
68 sub is_clean {
69     my $key = shift   or  return 1;
70     $key =~ /[=:].*$/ and return 0;
71     $key =~ /[+!]$/   and return 0;
72     return 1;
73 }
74
75 sub clean {
76     my $key = shift or return;
77     $key =~ s/[=:].*$//;
78     $key =~ s/[+!]$//;
79     return $key;
80 }
81
82 sub fuzzykey {                      # when you know the hash you want from, but not the exact key
83     my $self = shift or return;
84     my $key  = shift or return;
85     my $target = @_ ? shift : 'opts_clean';
86     foreach (map {clean($_)} keys %{$self->{default_opts}}) {  # TODO: cache
87         $key eq $_ and return $self->{$target}->{$_};
88     }
89 }
90
91 # MyGetOptions
92 # A wrapper around GetOptions
93 # {opts} does two things for GetOptions (see Getopt::Long)
94 #  (1) maps command-line options to the *other* variables where values are stored (in opts_clean)
95 #  (2) provides hashspace for the rest of the arbitrary options from the command-line
96 #
97 # TODO: allow more options to be passed here, maybe mimic Getopt::Long::GetOptions style
98 #
99 # If an arrayref argument is passed, then @ARGV will NOT be touched.
100 # Instead, the array will be passed to GetOptionsFromArray.
101 #
102
103 sub MyGetOptions {
104     my $self = shift;
105     my $arrayref = @_ ? shift : undef;
106     if ($arrayref and ref($arrayref) ne 'ARRAY') {
107         carp "MyGetOptions argument is not an array ref.  Expect GetOptionsFromArray to explode";
108     }
109     $self->{got_options} and carp "MyGetOptions called after options were already retrieved previously";
110     my @keys = sort {is_clean($b) <=> is_clean($a)} keys %{$self->{default_opts}};
111     $debug and print "KEYS: ", join(", ", @keys), "\n";
112     foreach (@keys) {
113         my $clean = clean($_);
114         my $place = $self->{default_opts_clean}->{$clean};
115         $self->{opts_clean}->{$clean} = $place;  # prepopulate default
116         # $self->{opts}->{$_} = $self->{opts_clean}->{$clean};                 # pointer for GetOptions
117         $self->{opts}->{$_} = sub {
118             my $opt = shift;
119             my $val = shift;
120             ref ( $self->{opts_clean}->{$opt} ) and ref($self->{opts_clean}->{$opt}) eq 'SCALAR'
121             and ${$self->{opts_clean}->{$opt}} = $val;  # set the referent's value
122             $self->{opts_clean}->{$opt} = $val;     # burn the map, stick the value there
123         };                 # pointer for GetOptions
124     }
125     $arrayref  ? GetOptionsFromArray($arrayref, $self->{opts}, @keys)
126                : GetOptions(                    $self->{opts}, @keys) ;
127    
128     foreach (@keys) {
129         delete $self->{opts}->{$_};     # now remove the mappings from (1) so we just have (2)
130     }
131     $self->clean_mirror('opts');        # populate clean_opts w/ cleaned versions of (2), plus everything else
132
133     print $self->help() and exit if $self->{opts_clean}->{help};
134     $self->new_lockfile();
135     $self->{got_options}++;
136     return wantarray ? %{$self->{opts_clean}} : $self->{opts_clean};
137 }
138
139 sub new_lockfile {
140     my $self = shift;
141     $debug and $OpenILS::Utils::Lockfile::debug = $debug;
142     unless ($self->{opts_clean}->{nolockfile} || $self->{default_opts_clean}->{nolockfile}) {
143         $self->{lockfile_obj} = OpenILS::Utils::Lockfile->new($self->first_defined('lock-file'));
144         $self->{lockfile}     = $self->{lockfile_obj}->filename;
145     }
146 }
147
148 sub first_defined {
149     my $self = shift;
150     my $key  = shift or return;
151     foreach (qw(opts_clean opts default_opts_clean default_opts)) {
152         defined $self->{$_}->{$key} and return $self->{$_}->{$key};
153     }
154     return;
155 }
156
157 sub clean_mirror {
158     my $self  = shift;
159     my $dirty = @_ ? shift : 'default_opts';
160     foreach (keys %{$self->{$dirty}}) {
161         defined $self->{$dirty}->{$_} or next;
162         $self->{$dirty . '_clean'}->{clean($_)} = $self->{$dirty}->{$_};
163     }
164 }
165
166 sub new {
167     my $class = shift;
168     my $self  = _default_self;
169     bless ($self, $class);
170     $self->init(@_);
171     $debug and print "new ",  __PACKAGE__, " obj: ", Dumper($self);
172     return $self;
173 }
174
175 sub add_and_purge {
176     my $self = shift;
177     my $key  = shift;
178     my $val  = shift;
179     my $clean = clean($key);
180     my @others = grep {/$clean/ and $_ ne $key} keys %{$self->{default_opts}};
181     unless (@others) {
182         $debug and print "unique key $key => $val\n";
183         $self->{default_opts}->{$key} = $val;   # no purge, just add
184         return;
185     }
186     foreach (@others) {
187         $debug and print "variant of $key => $_\n";
188         if ($key ne $clean) {    # if it is a dirtier key, delete the clean one
189             delete $self->{default_opts}->{$_};
190             $self->{default_opts}->{$key} = $val;
191         } else {                 # else update the dirty one
192             $self->{default_opts}->{$_} = $val;
193         }
194     }
195 }
196
197 sub init {      # not INIT
198     my $self = shift;
199     my $opts  = @_ ? shift : {};    # user can specify more default options to constructor
200 # TODO: check $opts is hashref; then check verbose/debug first.  maybe check negations e.g. "no-verbose" ?
201     @extra_opts = keys %$opts;
202     foreach (@extra_opts) {        # add any other keys w/ default values
203         $debug and print "init() adding option $_, default value: $opts->{$_}\n";
204         $self->add_and_purge($_, $opts->{$_});
205     }
206     $self->clean_mirror;
207     return $self;
208 }
209
210 sub usage {
211     # my $self = shift;
212     return "\nUSAGE: $0 [OPTIONS]";
213 }
214
215 sub options_help {
216     my $self = shift;
217     my $chunk = @_ ? shift : '';
218     return <<HELP
219
220 COMMON OPTIONS:
221     --osrf-config </path/to/config_file>  Default: $self->{default_opts_clean}->{'osrf-config'}
222                  Specify OpenSRF core config file.
223
224     --lock-file </path/to/file_name>      Default: $self->{default_opts_clean}->{'lock-file'}
225                  Specify lock file.     
226
227 HELP
228     . $chunk . <<HELP;
229     --debug      Print server responses to STDOUT for debugging
230     --verbose    Set verbosity
231     --help       Show this help message
232 HELP
233 }
234
235 sub help {
236     my $self = shift;
237     return $self->usage() . "\n" . $self->options_help(@_) . $self->example();
238 }
239
240 sub example {
241     return "\n\nEXAMPLES:\n\n    $0 --osrf-config /my/other/opensrf_core.xml\n";
242 }
243
244 # the proper order is: MyGetOptions, bootstrap, session.
245 # But the latter subs will check to see if they need to call the preceeding one(s).  
246
247 sub session {
248     my $self = shift or return;
249     $self->{bootstrapped} or $self->bootstrap();
250     @_ or croak "session() called without required argument (app_name, e.g. 'open-ils.acq')";
251     return ($self->{session} ||= OpenSRF::AppSession->create(@_));
252 }
253
254 sub bootstrap {
255     my $self = shift or return;
256     if ($self->{auto_get_options_4_bootstrap} and not $self->{got_options}) {
257         $debug and print "Automatically calling MyGetOptions before bootstrap\n";
258         $self->MyGetOptions();
259     }
260     try {
261         $debug and print "bootstrap lock-file  : ", $self->first_defined('lock-file'), "\n";
262         $debug and print "bootstrap osrf-config: ", $self->first_defined('osrf-config'), "\n";
263         OpenSRF::System->bootstrap_client(config_file => $self->first_defined('osrf-config'));
264         Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
265         $self->{bootstrapped} = 1;
266     } otherwise {
267         $self->{bootstrapped} = 0;
268         warn shift;
269     };
270 }
271
272 sub editor_init {
273     my $self = shift or return;
274     OpenILS::Utils::CStoreEditor::init();   # no return value to check
275     $self->{editor_inited} = 1;
276 }
277
278 sub editor {
279     my $self = shift or return;
280     $self->{bootstrapped}  or $self->bootstrap();
281     $self->{editor_inited} or $self->editor_init();
282     return new_editor(@_);
283 }
284
285 1;
286 __END__
287
288 =pod
289
290 =head1 NAME
291
292 OpenILS::Utils::Cronscript - Consolidated options handling for any script (not just cron, really)
293
294 =head1 SYNOPSIS
295
296     use OpenILS::Utils::Cronscript;
297
298     my %defaults = (
299         'min=i'      => 0,          # keys are Getopt::Long style options
300         'max=i'      => 999,        # values are default values
301         'user=s'     => 'admin',
302         'password=s' => '',
303         'nolockfile' => 1,
304     };
305
306     my $core = OpenILS::Utils::Cronscript->new(\%defaults);
307     my $opts = $core->MyGetOptions();   # options now in, e.g.: $opts->{max}
308     $core->bootstrap;
309
310 Or if you don't need any additional options and just want to get a session going:
311     
312     use OpenILS::Utils::Cronscript;
313     my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
314
315 =head1 DESCRIPTION
316
317 There are a few main problems when writing a new script for Evergreen. 
318
319 =head2 Initialization
320
321 The runtime 
322 environment for the application requires a lot of initialization, but during normal operation it
323 has already occured (when Evergreen was started).  So most of the EG code never has to deal with 
324 this problem, but standalone scripts do.  The timing and sequence of requisite events is important and not obvious.
325
326 =head2 Common Options, Consistent Options
327
328 We need several common options for each script that accesses the database or
329 uses EG data objects and methods.  Logically, these options often deal with initialization.  They
330 should take the B<exact> same form(s) for each script and should not be 
331 dependent on the local author to copy and paste them from some reference source.  We really don't want to encourage (let alone force)
332 admins to use C<--config>, C<--osrf-confg>, C<-c>, and C<@ARGV[2]> for the same purpose in different scripts, with different
333 default handling, help descriptions and error messages (or lack thereof).
334
335 This suggests broader problem of UI consistency and uniformity, also partially addressed by this module.
336
337 =head2 Lockfiles
338
339 A lockfile is necessary for a script that wants to prevent possible simultaneous execution.  For example, consider a script 
340 that is scheduled to run frequently, but that experiences occasional high load: you wouldn't want crontab to start running
341 it again if the first instance had not yet finished.  
342
343 But the code for creating, writing to, checking for, reading and cleaning up a lockfile for the script bloats what might otherwise be a terse 
344 method call.  Conscript handles lockfile generation and removal automatically.
345
346 =head1 OPTIONS
347
348 The common options (and default values) are:
349
350     'lock-file=s'   => OpenILS::Utils::Lockfile::default_filename,
351     'osrf-config=s' => '/openils/conf/opensrf_core.xml',
352     'debug'         => 0,
353     'verbose+'      => 0,
354     'help'          => 0,
355
356 =head1 TODO 
357
358 More docs here.
359
360 =head1 SEE ALSO
361
362     Getopt::Long
363     OpenILS::Utils::Lockfile
364     oils_header.pl
365
366 =head1 AUTHOR
367
368 Joe Atzberger <jatzberger@esilibrary.com>
369
370 =cut
371