handle command failure; fix write_command_to_file
[function2gene.git] / bin / do_it_all
1 #! /usr/bin/perl
2 # do_it_all, is part of the gene search suite and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
6
7
8 use threads;
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 use Storable;
16
17 =head1 NAME
18
19   do_it_all - Call out to each of the search modules to search for
20   each of the terms
21
22 =head1 SYNOPSIS
23
24  do_it_all --keywords keywords.txt --results gene_search_results
25
26  Options:
27   --keywords newline delineated list of keywords to search for
28   --results directory to store results in
29   --database databases to search
30   --restart-at mode to start searching at
31   --debug, -d debugging level (Default 0)
32   --help, -h display this help
33   --man, -m display manual
34
35 =head1 OPTIONS
36
37 =over
38
39 =item B<--keywords>
40
41 A file which contains a newline delinated list of keywords to search
42 for. Can be specified multiple times. Lines starting with # or ; are
43 ignored.
44
45 =item B<--results>
46
47 Directory in which to store results; also stores the current state of
48 the system
49
50 =item B<--database>
51
52 Databases to search, can be specified multiple times. [Defaults to
53 NCBI, GeneCards and Harvester, the only currently supported
54 databases.]
55
56 =item B<--restart-at>
57
58 If you need to restart the process at a particular state (which has
59 already been completed) specify this option.
60
61 =item B<--debug, -d>
62
63 Debug verbosity. (Default 0)
64
65 =item B<--help, -h>
66
67 Display brief useage information.
68
69 =item B<--man, -m>
70
71 Display this manual.
72
73 =back
74
75 =head1 EXAMPLES
76
77
78 =cut
79
80
81 use vars qw($DEBUG);
82 use Cwd qw(abs_path);
83 use IO::File;
84 use Storable qw(thaw freeze);
85 use File::Basename qw(basename dirname);
86 use Thread::Queue;
87
88 my %options = (databases       => [],
89                keywords        => [],
90                debug           => 0,
91                help            => 0,
92                man             => 0,
93                results         => '',
94                );
95
96 GetOptions(\%options,'keywords=s@','databases=s@',
97            'restart_at|restart-at=s','results=s',
98            'debug|d+','help|h|?','man|m');
99
100 pod2usage() if $options{help};
101 pod2usage({verbose=>2}) if $options{man};
102
103 my $base_dir = dirname(abs_path($0));
104
105 my $ERRORS='';
106
107 $ERRORS.="restart-at must be one of get, parse or combine\n" if
108      exists $options{restart_at} and $options{restart_at} !~ /^(?:get|parse|combine)$/;
109
110 $ERRORS.="unknown database(s)" if
111      @{$options{databases}} and
112      grep {$_ !~ /^(?:ncbi|genecard|harvester)$/i} @{$options{databases}};
113
114 if (not length $options{results}) {
115      $ERRORS.="results directory not specified";
116 }
117 elsif (not -d $options{results} or not -w $options{results}) {
118      $ERRORS.="results directory $options{results} does not exist or is not writeable";
119 }
120
121 pod2usage($ERRORS) if length $ERRORS;
122
123 if (not @{$options{databases}}) {
124      $options{databases} = [qw(ncbi genecard harvester)]
125 }
126
127 $DEBUG = $options{debug};
128
129 # There are three states for our engine
130 # Getting results
131 # Parsing them
132 # Combining results
133
134 # first, check to see if the state in the result directory exists
135
136 my %state;
137
138 $options{keywords} = [map {abs_path($_)} @{$options{keywords}}];
139
140 chdir $options{results} or die "Unable to chdir to $options{results}";
141
142 if (-e "do_it_all_state") {
143      ADVISE("Using existing state information");
144      my $state_fh = IO::File->new("do_it_all_state",'r') or die
145           "Unable to open state file for reading: $!";
146      local $/;
147      my $state_file = <$state_fh>;
148      %state = %{thaw($state_file)} or die "Unable to thaw state file";
149 }
150 else {
151      ADVISE("Starting new run");
152      %state = (keywords => [],
153                databases => [map {lc($_)} @{$options{databases}}],
154                done_keywords => {
155                                  get => {},
156                                  parse => {},
157                                  combine => {},
158                                 },
159               );
160 }
161
162 my @new_keywords;
163 if (@{$options{keywords}}) {
164      # uniqify keywords
165      my %old_keywords;
166      @old_keywords{@{$state{keywords}}} = (1) x @{$state{keywords}};
167      for my $keyword_file (@{$options{keywords}}) {
168           my $keyword_fh = IO::File->new($keyword_file,'r') or die
169                "Unable to open $keyword_file for reading: $!";
170           while (<$keyword_fh>) {
171                next if /^\s*[#;]/;
172                next unless /\w+/;
173                chomp;
174                if (not $old_keywords{$_}) {
175                     DEBUG("Adding new keyword '$_'");
176                     push @new_keywords, $_;
177                }
178                else {
179                     DEBUG("Not adding duplicate keyword '$_'");
180                }
181           }
182      }
183      push @{$state{keywords}},@new_keywords;
184 }
185
186 if (exists $options{restart_at} and length $options{restart_at}) {
187      if (lc($options{restart_at}) eq 'get') {
188           delete $state{done_keywords}{get};
189           delete $state{done_keywords}{parse};
190           delete $state{done_keywords}{combine};
191      }
192      elsif (lc($options{restart_at}) eq 'parse') {
193           delete $state{done_keywords}{parse};
194           delete $state{done_keywords}{combine};
195      }
196      elsif (lc($options{restart_at}) eq 'combine') {
197           delete $state{done_keywords}{combine};
198      }
199 }
200
201 # now we need to figure out what has to happen
202 # for each keyword, we check to see if we've got results, parsed
203 # results, and combined it. If not, we queue up those actions.
204
205 my %actions = (combine => 0,
206                get     => {},
207                parse   => {},
208               );
209
210 if (not @{$state{keywords}}) {
211      ADVISE("There are no keywords specified");
212 }
213
214 for my $keyword (@{$state{keywords}}) {
215      for my $database (@{$state{databases}}) {
216           if (not exists $state{done_keywords}{get}{$database}{$keyword}) {
217                push @{$actions{get}{$database}}, $keyword;
218                delete $state{done_keywords}{parse}{$database}{$keyword} if
219                     exists $state{done_keywords}{parse}{$database}{$keyword};
220                delete $state{done_keywords}{combine}{$database}{$keyword} if
221                     exists $state{done_keywords}{combine}{$database}{$keyword};
222           }
223           if (not exists $state{done_keywords}{parse}{$database}{$keyword}) {
224                push @{$actions{parse}{$database}},$keyword;
225                delete $state{done_keywords}{combine}{$database}{$keyword} if
226                     exists $state{done_keywords}{combine}{$database}{$keyword};
227           }
228           if (not exists $state{done_keywords}{combine}{$database}{$keyword}) {
229               $actions{combine} = 1;
230           }
231      }
232 }
233
234
235 for my $state (qw(get parse)) {
236      my %databases;
237      for my $database (keys %{$actions{$state}}) {
238           next unless @{$actions{$state}{$database}};
239           $databases{$database}{queue} = Thread::Queue->new
240                or die "Unable to create new thread queue";
241           $databases{$database}{thread} = threads->create(\&handle_action,$state,$database,$databases{$database}{queue})
242                or die "Unable to create new thread";
243           $databases{$database}{queue}->enqueue(@{$actions{$state}{$database}});
244           $databases{$database}{queue}->enqueue(undef);
245      }
246      my $ERRORS=0;
247      for my $database (keys %databases) {
248           my ($actioned_keywords,$failed_keywords) = @{$databases{$database}{thread}->join||[]};
249           if (not defined $failed_keywords) {
250                ADVISE("Something bad happened during '$state' of '$database'");
251                $ERRORS = 1;
252           }
253           elsif (@{$failed_keywords}) {
254                ADVISE("These keywords failed during '$state' of '$database':",@{$failed_keywords});
255                $ERRORS=1;
256           }
257           @{$state{done_keywords}{$state}{$database}}{@{$actioned_keywords}} = (1) x @{$actioned_keywords};
258           delete @{$state{done_keywords}{$state}{$database}}{@{$failed_keywords}};
259      }
260      save_state(\%state);
261      if ($ERRORS) {
262           WARN("Stoping, as there are errors");
263           exit 1;
264      }
265 }
266
267 if ($actions{combine}) {
268      save_state(\%state);
269      # deal with combining results
270      my @parsed_results = map { my $db = $_;
271                                 map {
272                                      "parsed_results_${db}_${_}.txt"
273                                 } keys %{$state{done_keywords}{parse}{$db}}
274                            } keys %{$state{done_keywords}{parse}};
275
276      write_command_to_file('combined_results.txt',
277                            "$base_dir/combine_results",
278                            @parsed_results,
279                           );
280      for my $result (@parsed_results) {
281           s/^parsed_results_//;
282           s/\.txt$//;
283           my ($db,$keyword) = split /_/, $_, 2;
284           $state{done_keywords}{combined}{$db}{$keyword} = 1;
285      }
286      save_state(\%state);
287      ADVISE("Finished; results in $options{results}/combined_results");
288 }
289 else {
290      ADVISE('Nothing to do. [Perhaps you wanted --restart-at?]');
291 }
292
293 sub handle_action{
294      my ($state,$database,$queue) = @_;
295      my $keyword;
296      my $actioned_keywords = [];
297      my $failed_keywords = [];
298      DEBUG("Beginning to handle actions for state '$state' database '$database'");
299      while ($keyword = $queue->dequeue) {
300           DEBUG("Handling state '$state' database '$database' keyword '$keyword'");
301           # handle the action, baybee
302           if ($state eq 'get') {
303                my $command_fh;
304                eval {
305                     open($command_fh,'|-',
306                          "$base_dir/get_${database}_results",
307                         ) or die "unable to execute '$base_dir/get_${database}_results'";
308                     print {$command_fh} "$keyword\n" or die "unable to print $keyword to 'get_${database}_results'";
309                     close($command_fh) or die "Unable to close filehandle";
310                     if ($? != 0) {
311                          die "get_${database}_results with keyword $keyword failed with error code ".($?>>8);
312                     }
313                };
314                if ($@) {
315                     WARN($@);
316                     push @{$failed_keywords}, $keyword;
317                     next;
318                }
319           }
320           elsif ($state eq 'parse') {
321                eval {
322                     write_command_to_file("parsed_results_${database}_${keyword}.txt",
323                                           "$base_dir/parse_${database}_results",
324                                           '--keywords',
325                                           $keyword,
326                                          );
327                };
328                if ($@) {
329                     WARN("parse_${database}_results failed with $@");
330                     push @{$failed_keywords}, $keyword;
331                     next;
332                }
333           }
334           else {
335                die "I don't know how to handle state $state";
336           }
337           ADVISE("$state results from '$database' for '$keyword'");
338           push @{$actioned_keywords},$keyword;
339      }
340      return [$actioned_keywords,$failed_keywords];
341 }
342
343 sub save_state{
344      my ($state) = @_;
345      my $state_fh = IO::File->new("do_it_all_state",'w') or die
346           "Unable to open state file for writing: $!";
347      print {$state_fh} freeze($state) or die "Unable to freeze state file";
348      close $state_fh or die "Unable to close state file: $!";
349 }
350
351 sub write_command_to_file{
352      my ($file,@command) = @_;
353      my $fh = IO::File->new($file,'w') or
354           die "Unable to open $file for writing: $!";
355      my $command_fh;
356      open($command_fh,'-|',
357           @command,
358          ) or die "Unable to execute $command[0] $!";
359      print {$fh} <$command_fh>;
360      close $fh;
361      close $command_fh or die "$command[0] failed with ".($?>>8);
362 }
363
364
365 sub ADVISE{
366      print STDOUT map {($_,qq(\n))} @_;
367 }
368
369 sub DEBUG{
370      print STDERR map {($_,qq(\n))} @_;
371 }
372
373
374 sub WARN {
375      print STDERR map {($_,qq(\n))} @_;
376 }
377
378 __END__