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 # Added for authentication
42 use Digest::MD5 qw/md5_hex/;
44 our @extra_opts = ( # additional keys are stored here
54 # default_opts_clean => {},
56 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
57 'osrf-config=s' => '@sysconfdir@/opensrf_core.xml',
61 # 'internal_var' => 'XYZ',
67 auto_get_options_4_bootstrap => 1,
72 my $key = shift or return 1;
73 $key =~ /[=:].*$/ and return 0;
74 $key =~ /[+!]$/ and return 0;
79 my $key = shift or return;
85 sub fuzzykey { # when you know the hash you want from, but not the exact key
86 my $self = shift or return;
87 my $key = shift or return;
88 my $target = @_ ? shift : 'opts_clean';
89 foreach (map {clean($_)} keys %{$self->{default_opts}}) { # TODO: cache
90 $key eq $_ and return $self->{$target}->{$_};
95 # A wrapper around GetOptions
96 # {opts} does two things for GetOptions (see Getopt::Long)
97 # (1) maps command-line options to the *other* variables where values are stored (in opts_clean)
98 # (2) provides hashspace for the rest of the arbitrary options from the command-line
100 # TODO: allow more options to be passed here, maybe mimic Getopt::Long::GetOptions style
102 # If an arrayref argument is passed, then @ARGV will NOT be touched.
103 # Instead, the array will be passed to GetOptionsFromArray.
108 my $arrayref = @_ ? shift : undef;
109 if ($arrayref and ref($arrayref) ne 'ARRAY') {
110 carp "MyGetOptions argument is not an array ref. Expect GetOptionsFromArray to explode";
112 $self->{got_options} and carp "MyGetOptions called after options were already retrieved previously";
113 my @keys = sort {is_clean($b) <=> is_clean($a)} keys %{$self->{default_opts}};
114 $debug and print "KEYS: ", join(", ", @keys), "\n";
116 my $clean = clean($_);
117 my $place = $self->{default_opts_clean}->{$clean};
118 $self->{opts_clean}->{$clean} = $place; # prepopulate default
119 # $self->{opts}->{$_} = $self->{opts_clean}->{$clean}; # pointer for GetOptions
120 $self->{opts}->{$_} = sub {
123 ref ( $self->{opts_clean}->{$opt} ) and ref($self->{opts_clean}->{$opt}) eq 'SCALAR'
124 and ${$self->{opts_clean}->{$opt}} = $val; # set the referent's value
125 $self->{opts_clean}->{$opt} = $val; # burn the map, stick the value there
126 }; # pointer for GetOptions
128 $arrayref ? GetOptionsFromArray($arrayref, $self->{opts}, @keys)
129 : GetOptions( $self->{opts}, @keys) ;
132 delete $self->{opts}->{$_}; # now remove the mappings from (1) so we just have (2)
134 $self->clean_mirror('opts'); # populate clean_opts w/ cleaned versions of (2), plus everything else
136 print $self->help() and exit if $self->{opts_clean}->{help};
137 $self->new_lockfile();
138 $self->{got_options}++;
139 return wantarray ? %{$self->{opts_clean}} : $self->{opts_clean};
144 $debug and $OpenILS::Utils::Lockfile::debug = $debug;
145 unless ($self->{opts_clean}->{nolockfile} || $self->{default_opts_clean}->{nolockfile}) {
146 $self->{lockfile_obj} = OpenILS::Utils::Lockfile->new($self->first_defined('lock-file'));
147 $self->{lockfile} = $self->{lockfile_obj}->filename;
153 my $key = shift or return;
154 foreach (qw(opts_clean opts default_opts_clean default_opts)) {
155 defined $self->{$_}->{$key} and return $self->{$_}->{$key};
162 my $dirty = @_ ? shift : 'default_opts';
163 foreach (keys %{$self->{$dirty}}) {
164 defined $self->{$dirty}->{$_} or next;
165 $self->{$dirty . '_clean'}->{clean($_)} = $self->{$dirty}->{$_};
171 my $self = _default_self;
172 bless ($self, $class);
174 $debug and print "new ", __PACKAGE__, " obj: ", Dumper($self);
182 my $clean = clean($key);
183 my @others = grep {/$clean/ and $_ ne $key} keys %{$self->{default_opts}};
185 $debug and print "unique key $key => $val\n";
186 $self->{default_opts}->{$key} = $val; # no purge, just add
190 $debug and print "variant of $key => $_\n";
191 if ($key ne $clean) { # if it is a dirtier key, delete the clean one
192 delete $self->{default_opts}->{$_};
193 $self->{default_opts}->{$key} = $val;
194 } else { # else update the dirty one
195 $self->{default_opts}->{$_} = $val;
200 sub init { # not INIT
202 my $opts = @_ ? shift : {}; # user can specify more default options to constructor
203 # TODO: check $opts is hashref; then check verbose/debug first. maybe check negations e.g. "no-verbose" ?
204 @extra_opts = keys %$opts;
205 foreach (@extra_opts) { # add any other keys w/ default values
206 $debug and print "init() adding option $_, default value: $opts->{$_}\n";
207 $self->add_and_purge($_, $opts->{$_});
215 return "\nUSAGE: $0 [OPTIONS]";
220 my $chunk = @_ ? shift : '';
224 --osrf-config </path/to/config_file> Default: $self->{default_opts_clean}->{'osrf-config'}
225 Specify OpenSRF core config file.
227 --lock-file </path/to/file_name> Default: $self->{default_opts_clean}->{'lock-file'}
232 --debug Print server responses to STDOUT for debugging
233 --verbose Set verbosity
234 --help Show this help message
240 return $self->usage() . "\n" . $self->options_help(@_) . $self->example();
244 return "\n\nEXAMPLES:\n\n $0 --osrf-config /my/other/opensrf_core.xml\n";
247 # the proper order is: MyGetOptions, bootstrap, session.
248 # But the latter subs will check to see if they need to call the preceeding one(s).
251 my $self = shift or return;
252 $self->{bootstrapped} or $self->bootstrap();
253 @_ or croak "session() called without required argument (app_name, e.g. 'open-ils.acq')";
254 return ($self->{session} ||= OpenSRF::AppSession->create(@_));
258 my $self = shift or return;
259 if ($self->{auto_get_options_4_bootstrap} and not $self->{got_options}) {
260 $debug and print "Automatically calling MyGetOptions before bootstrap\n";
261 $self->MyGetOptions();
264 $debug and print "bootstrap lock-file : ", $self->first_defined('lock-file'), "\n";
265 $debug and print "bootstrap osrf-config: ", $self->first_defined('osrf-config'), "\n";
266 OpenSRF::System->bootstrap_client(config_file => $self->first_defined('osrf-config'));
267 Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
268 $self->{bootstrapped} = 1;
270 $self->{bootstrapped} = 0;
276 my $self = shift or return;
277 OpenILS::Utils::CStoreEditor::init(); # no return value to check
278 $self->{editor_inited} = 1;
282 my $self = shift or return;
283 $self->{bootstrapped} or $self->bootstrap();
284 $self->{editor_inited} or $self->editor_init();
285 return new_editor(@_);
288 # Authenticate with the open-ils.auth module.
289 # Takes a hash ref of arguments:
291 # username => username to authenticate as,
292 # password => the user's password,
293 # workstation => the workstation to use (optional),
294 # type => the type of login (optional, but defaults to staff)
297 # returns the authtoken or undef on failure.
298 # Also stores the authtoken and authtime as fields on the object.
300 my $self = shift or return;
301 my $args = shift or return;
302 if ($args && ref($args) eq 'HASH') {
303 $self->{bootstrapped} or $self->bootstrap();
305 my $session = Opensrf::AppSession->create('open-ils.auth');
306 my $seed = $session->request(
307 'open-ils.auth.authenticate.init', $args->{'username'}
310 $args->{password} = md5_hex($seed . md5_hex($args->{password}));
311 my $req = $session->request(
312 'open-ils.auth.authenticate.complete', $args
315 my $response = $req->gather(1);
316 if ($response && ref($response) eq 'HASH' && $response->{payload}) {
317 $self->{authtoken} = $response->{payload}->{authtoken};
318 $self->{authtime} = $response->{payload}->{authtime};
320 $self->{authtoken} = undef;
321 $self->{authtime} = undef;
323 $session->disconnect();
324 return $self->authtoken;
332 return $self->{authtoken};
337 return $self->{authtime};
347 OpenILS::Utils::Cronscript - Consolidated options handling for any script (not just cron, really)
351 use OpenILS::Utils::Cronscript;
354 'min=i' => 0, # keys are Getopt::Long style options
355 'max=i' => 999, # values are default values
361 my $core = OpenILS::Utils::Cronscript->new(\%defaults);
362 my $opts = $core->MyGetOptions(); # options now in, e.g.: $opts->{max}
365 Or if you don't need any additional options and just want to get a session going:
367 use OpenILS::Utils::Cronscript;
368 my $session = OpenILS::Utils::Cronscript->new()->session('open-ils.acq');
372 There are a few main problems when writing a new script for Evergreen.
374 =head2 Initialization
377 environment for the application requires a lot of initialization, but during normal operation it
378 has already occured (when Evergreen was started). So most of the EG code never has to deal with
379 this problem, but standalone scripts do. The timing and sequence of requisite events is important and not obvious.
381 =head2 Common Options, Consistent Options
383 We need several common options for each script that accesses the database or
384 uses EG data objects and methods. Logically, these options often deal with initialization. They
385 should take the B<exact> same form(s) for each script and should not be
386 dependent on the local author to copy and paste them from some reference source. We really don't want to encourage (let alone force)
387 admins to use C<--config>, C<--osrf-confg>, C<-c>, and C<@ARGV[2]> for the same purpose in different scripts, with different
388 default handling, help descriptions and error messages (or lack thereof).
390 This suggests broader problem of UI consistency and uniformity, also partially addressed by this module.
394 A lockfile is necessary for a script that wants to prevent possible simultaneous execution. For example, consider a script
395 that is scheduled to run frequently, but that experiences occasional high load: you wouldn't want crontab to start running
396 it again if the first instance had not yet finished.
398 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
399 method call. Conscript handles lockfile generation and removal automatically.
403 The common options (and default values) are:
405 'lock-file=s' => OpenILS::Utils::Lockfile::default_filename,
406 'osrf-config=s' => '/openils/conf/opensrf_core.xml',
418 OpenILS::Utils::Lockfile
423 Joe Atzberger <jatzberger@esilibrary.com>