1 package OpenILS::Utils::Cronscript;
3 # ---------------------------------------------------------------
4 # Copyright (C) 2010 Equinox Software, Inc
5 # Author: Joe Atzberger <jatzberger@esilibrary.com>
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.
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 # ---------------------------------------------------------------
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
27 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
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/;
36 use File::Basename qw/fileparse/;
41 our @extra_opts = ( # additional keys are stored here
51 # default_opts_clean => {},
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@@
58 'internal_var' => 'XYZ',
64 auto_get_options_4_bootstrap => 1,
69 my $key = shift or return 1;
70 $key =~ /[=:].*$/ and return 0;
71 $key =~ /[+!]$/ and return 0;
76 my $key = shift or return;
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}->{$_};
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
97 # TODO: allow more options to be passed here, maybe mimic Getopt::Long::GetOptions style
99 # If an arrayref argument is passed, then @ARGV will NOT be touched.
100 # Instead, the array will be passed to GetOptionsFromArray.
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";
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";
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 {
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
125 $arrayref ? GetOptionsFromArray($arrayref, $self->{opts}, @keys)
126 : GetOptions( $self->{opts}, @keys) ;
129 delete $self->{opts}->{$_}; # now remove the mappings from (1) so we just have (2)
131 $self->clean_mirror('opts'); # populate clean_opts w/ cleaned versions of (2), plus everything else
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};
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;
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};
159 my $dirty = @_ ? shift : 'default_opts';
160 foreach (keys %{$self->{$dirty}}) {
161 defined $self->{$dirty}->{$_} or next;
162 $self->{$dirty . '_clean'}->{clean($_)} = $self->{$dirty}->{$_};
168 my $self = _default_self;
169 bless ($self, $class);
171 $debug and print "new ", __PACKAGE__, " obj: ", Dumper($self);
179 my $clean = clean($key);
180 my @others = grep {/$clean/ and $_ ne $key} keys %{$self->{default_opts}};
182 $debug and print "unique key $key => $val\n";
183 $self->{default_opts}->{$key} = $val; # no purge, just add
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;
197 sub init { # not INIT
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->{$_});
212 return "\nUSAGE: $0 [OPTIONS]";
217 my $chunk = @_ ? shift : '';
221 --osrf-config </path/to/config_file> Default: $self->{default_opts_clean}->{'osrf-config'}
222 Specify OpenSRF core config file.
224 --lock-file </path/to/file_name> Default: $self->{default_opts_clean}->{'lock-file'}
229 --debug Print server responses to STDOUT for debugging
230 --verbose Set verbosity
231 --help Show this help message
237 return $self->usage() . "\n" . $self->options_help(@_) . $self->example();
241 return "\n\nEXAMPLES:\n\n $0 --osrf-config /my/other/opensrf_core.xml\n";
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).
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(@_));
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();
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;
267 $self->{bootstrapped} = 0;
273 my $self = shift or return;
274 OpenILS::Utils::CStoreEditor::init(); # no return value to check
275 $self->{editor_inited} = 1;
279 my $self = shift or return;
280 $self->{bootstrapped} or $self->bootstrap();
281 $self->{editor_inited} or $self->editor_init();
282 return new_editor(@_);
292 OpenILS::Utils::Cronscript - Consolidated options handling for any script (not just cron, really)
296 use OpenILS::Utils::Cronscript;
299 'min=i' => 0, # keys are Getopt::Long style options
300 'max=i' => 999, # values are default values
306 my $core = OpenILS::Utils::Cronscript->new(\%defaults);
307 my $opts = $core->MyGetOptions(); # options now in, e.g.: $opts->{max}
310 Or if you don't need any additional options and just want to get a session going:
312 use OpenILS::Utils::Cronscript;
313 my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
317 There are a few main problems when writing a new script for Evergreen.
319 =head2 Initialization
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.
326 =head2 Common Options, Consistent Options
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).
335 This suggests broader problem of UI consistency and uniformity, also partially addressed by this module.
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.
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.
348 The common options (and default values) are:
350 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
351 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
363 OpenILS::Utils::Lockfile
368 Joe Atzberger <jatzberger@esilibrary.com>