source: gli/trunk/src/org/greenstone/gatherer/shell/GShell.java@ 18396

Last change on this file since 18396 was 18396, checked in by ak19, 15 years ago

Storing the commandOutput, in case any data is returned from runninig the script on the server which then needs to be used by the client.

  • Property svn:keywords set to Author Date Id Revision
File size: 22.4 KB
Line 
1/**
2 *#########################################################################
3 *
4 * A component of the Gatherer application, part of the Greenstone digital
5 * library suite from the New Zealand Digital Library Project at the
6 * University of Waikato, New Zealand.
7 *
8 * <BR><BR>
9 *
10 * Author: John Thompson, Greenstone Digital Library, University of Waikato
11 *
12 * <BR><BR>
13 *
14 * Copyright (C) 1999 New Zealand Digital Library Project
15 *
16 * <BR><BR>
17 *
18 * This program is free software; you can redistribute it and/or modify
19 * it under the terms of the GNU General Public License as published by
20 * the Free Software Foundation; either version 2 of the License, or
21 * (at your option) any later version.
22 *
23 * <BR><BR>
24 *
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
29 *
30 * <BR><BR>
31 *
32 * You should have received a copy of the GNU General Public License
33 * along with this program; if not, write to the Free Software
34 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
35 *########################################################################
36 */
37package org.greenstone.gatherer.shell;
38
39import java.io.*;
40import java.net.*;
41import java.util.ArrayList;
42import java.util.Enumeration;
43import java.util.regex.*;
44import javax.swing.*;
45import javax.swing.event.*;
46import javax.swing.tree.*;
47import org.greenstone.gatherer.Configuration;
48import org.greenstone.gatherer.DebugStream;
49import org.greenstone.gatherer.Dictionary;
50import org.greenstone.gatherer.Gatherer;
51import org.greenstone.gatherer.cdm.CollectionConfiguration;
52import org.greenstone.gatherer.cdm.CollectionDesignManager;
53import org.greenstone.gatherer.cdm.CollectionMetaManager;
54import org.greenstone.gatherer.cdm.CollectionMeta;
55import org.greenstone.gatherer.collection.CollectionManager;
56import org.greenstone.gatherer.metadata.DocXMLFileManager;
57import org.greenstone.gatherer.remote.RemoteGreenstoneServer;
58import org.greenstone.gatherer.util.StaticStrings;
59import org.greenstone.gatherer.util.Utility;
60
61
62/** The <strong>GShell</strong> is reponsible for running a separately threaded process in the command shell. This is necessary for executing the Perl Scripts and also for other system related funcitonality.
63 */
64public class GShell
65 extends Thread {
66 /** A flag used to determine if this process has been asked to cancel. */
67 private boolean cancel = false;
68 private BufferedOutputStream buffered_output_stream = null;
69 /** The list of listeners associated with this class. */
70 private EventListenerList listeners = null;
71 /** The current status of this shell process. */
72 private int status = -1;
73 /** The type of message being sent. */
74 private int msg_type = -1;
75 /** The type of shell process. */
76 private int type = -1;
77 /** The caller of this process, and thus the class most interested in messages. */
78 private GShellListener caller = null;
79 /** The progress monitor associated with this process. */
80 private GShellProgressMonitor progress = null;
81 /** Arguments to be given to the process (including the executable you are calling. */
82 private String args[] = null;
83 /** The command_output returned from executing a process */
84 private String commandOutput = null;
85
86 /** Elements in process type enumeration. */
87 static final public int BUILD = 0;
88 static final public int IMPORT = 1;
89 static final public int NEW = 2;
90 static final public int EXPORTAS = 3;
91 static final public int CDIMAGE = 4;
92 static final public int CONVERT = 5;
93 static final public int EXPLODE = 6;
94 static final public int SRCREPLACE = 7; // for replacing source docs with their html
95 static final public int SCHEDULE = 8;
96
97 /** Elements in status type enumeration. */
98 static final public int ERROR = 0;
99 static final public int OK = 1;
100 static final public int CANCELLED = 2;
101
102 /** Elements in process type name enumeration. */
103 static public String GSHELL_BUILD = "gshell_build";
104 static public String GSHELL_IMPORT = "gshell_import";
105 static public String GSHELL_NEW = "gshell_new";
106 static public String GSHELL_EXPORTAS = "gshell_exportas";
107 static public String GSHELL_CDIMAGE = "gshell_cdimage";
108 static public String GSHELL_CONVERT = "gshell_convert";
109 static public String GSHELL_EXPLODE = "gshell_explode";
110 static public String GSHELL_SRCREPLACE = "gshell_srcreplace"; // for replacing source docs with their html versions
111 static public String GSHELL_SCHEDULE = "gshell_schedule";
112
113 /** Determine if the given process is still executing. It does this by attempting to throw an exception - not the most efficient way, but the only one as far as I know
114 * @param process the Process to test
115 * @return true if it is still executing, false otherwise
116 */
117 static public boolean processRunning(Process process) {
118 boolean process_running = false;
119
120 try {
121 process.exitValue(); // This will throw an exception if the process hasn't ended yet.
122 }
123 catch(IllegalThreadStateException itse) {
124 process_running = true;
125 }
126 catch(Exception exception) {
127 DebugStream.printStackTrace(exception);
128 }
129 return process_running;
130 }
131
132 /** Constructor gatherer all the data required to create a new process, and emit meaningfull messages.
133 * @param args A <strong>String[]</strong> containing the arguments to the process thread, including the name of the executable.
134 * @param type An <strong>int</strong> that indicates what group of processes this process belongs to, as some are treated slightly differently (i.e an IMPORT type process is always followed by a BUILD one).
135 * @param msg_type As process threads may be background (like a makecol.pl call) or important processes in their own right (such as an IMPORT-BUILD) we must be able to set what level messages posted by this class will have by usings this <strong>int</strong>.
136 * @param caller The default <i>GShellListener</i> that is interested in the progress of this process.
137 * @param progress The <i>GShellProgressMonitor</i> associated with this process.
138 * @param name A <strong>String</strong> identifier given to the process, for convience and debug reasons.
139 */
140 public GShell(String args[], int type, int msg_type, GShellListener caller, GShellProgressMonitor progress, String name) {
141 super(name);
142 this.args = args;
143 this.msg_type = msg_type;
144 this.type = type;
145 this.caller = caller;
146 this.progress = progress;
147 this.status = 0;
148 // Lower this jobs priority
149 this.setPriority(Thread.MIN_PRIORITY);
150 listeners = new EventListenerList();
151 listeners.add(GShellListener.class, caller);
152 }
153 /** This method adds another shell listener to this process.
154 * @param listener The new <i>GShellListener</i>.
155 */
156 public void addGShellListener(GShellListener listener) {
157 listeners.add(GShellListener.class, listener);
158 }
159 /** This method removes a certain shell listener from this process.
160 * @param listener The <i>GShellListener</i> to be removed.
161 */
162 /* private void removeGShellListener(GShellListener listener) {
163 listeners.remove(GShellListener.class, listener);
164 } */
165
166 protected StringBuffer get_stream_char(InputStreamReader isr, StringBuffer line_buffer,
167 BufferedOutputStream bos) throws IOException
168 {
169 int c = isr.read();
170 ///atherer.println("isr: '" + (char) c + "'");
171 if(c == '\n' || c == '\r') {
172 if(line_buffer.length() > 0) {
173 String line = line_buffer.toString();
174 // DebugStream.println("* " + line + " *");
175 fireMessage(type, typeAsString(type) + "> " + line, status, bos);
176 line_buffer = new StringBuffer();
177 }
178 }
179 else {
180 line_buffer.append((char)c);
181 }
182
183 return line_buffer;
184 }
185
186 public String getCommandOutput() { return commandOutput; }
187 public void resetCommandOutput() { commandOutput = null; }
188
189 private void runRemote(String[] args, BufferedOutputStream bos)
190 {
191 // Make sure the process hasn't been cancelled
192 if (hasSignalledStop()) {
193 return;
194 }
195
196 try {
197 int directory_name_end = args[0].lastIndexOf(File.separator);
198 String script_name = ((directory_name_end != -1) ? args[0].substring(directory_name_end + 1) : args[0]);
199 System.err.println("Script name: " + script_name);
200 String collection_name = args[args.length - 1];
201 System.err.println("Collection name: " + collection_name);
202
203
204 String script_args = "";
205 for (int i = 1; i < (args.length - 1); i++) {
206 // Skip arguments that won't make sense on the server
207 if (args[i].equals("-collectdir") || args[i].equals("-importdir") || args[i].equals("-builddir")) {
208 i++;
209 continue;
210 }
211
212 // Script arguments get changed to CGI arguments
213 if (args[i].startsWith("-")) {
214 script_args += "&" + args[i].substring(1) + "=";
215 if ((i + 1) < (args.length - 1) && !args[i + 1].startsWith("-")) {
216 script_args += URLEncoder.encode(args[i + 1], "UTF-8");
217 i++;
218 }
219 }
220 }
221
222 System.err.println("Script args: " + script_args);
223 buffered_output_stream = bos;
224 String command_output = Gatherer.remoteGreenstoneServer.runScript(collection_name, script_name, script_args, this);
225 this.commandOutput = command_output;
226 status = (command_output.equals("") ? CANCELLED : OK);
227 }
228 catch (Exception exception) {
229 DebugStream.printStackTrace(exception);
230 status = ERROR;
231 }
232 }
233
234
235 private void runLocal(String[] args, BufferedOutputStream bos)
236 {
237 try {
238 String command = "";
239 for(int i = 0; i < args.length; i++) {
240 command = command + args[i] + " ";
241 }
242
243 ///ystem.err.println("Command: " + command);
244 fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
245
246 Runtime rt = Runtime.getRuntime();
247 Process prcs = null;
248
249 prcs = rt.exec(args);
250 // If we used single argument exec, spaces in filenames or paths cause problems
251 // I.e. better to avoid: prcs = rt.exec(command);
252
253 InputStreamReader eisr = new InputStreamReader( prcs.getErrorStream(), "UTF-8" );
254 InputStreamReader stdisr = new InputStreamReader( prcs.getInputStream(), "UTF-8" );
255
256 StringBuffer eline_buffer = new StringBuffer();
257 StringBuffer stdline_buffer = new StringBuffer();
258
259 while(/*type != GShell.NEW &&*/ processRunning(prcs) && !hasSignalledStop()) {
260 // Hopefully this doesn't block if the process is trying to write to STDOUT.
261 if((eisr!=null) && eisr.ready()) {
262 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
263 }
264 // Hopefully this won't block if the process is trying to write to STDERR
265 else if(stdisr.ready()) {
266 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
267 }
268 else {
269 try {
270 sleep(100);
271 }
272 catch(Exception exception) {
273 }
274 }
275 }
276
277 if(!hasSignalledStop()) {
278 // Of course, just because the process is finished doesn't
279 // mean the incoming streams are empty. Unfortunately I've
280 // got no chance of preserving order, so I'll process the
281 // error stream first, then the out stream
282 while(eisr.ready()) {
283 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
284 }
285
286 while(stdisr.ready()) {
287 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
288 }
289
290 // Ensure that any messages still remaining in the string buffers are fired off.
291 if(eline_buffer.length() > 0) {
292 String eline = eline_buffer.toString();
293 //DebugStream.println("Last bit of eline: " + eline);
294 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
295 eline = null;
296 }
297
298 if(stdline_buffer.length() > 0) {
299 String stdline = stdline_buffer.toString();
300 //DebugStream.println("Last bit of stdline: " + stdline);
301 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
302 stdline = null;
303 }
304 }
305 else {
306 DebugStream.println("We've been asked to stop.");
307 }
308
309
310 if(!hasSignalledStop()) {
311 // Now display final message based on exit value
312
313 prcs.waitFor();
314
315 if(prcs.exitValue() == 0) {
316 status = OK;
317 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Success"), status, null);
318 }
319 else {
320 status = ERROR;
321 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Failure"), status, null);
322 }
323
324 eisr.close();
325 stdisr.close();
326 }
327 else {
328 // I need to somehow kill the child process. Unfortunately
329 // Thread.stop() and Process.destroy() both fail to do
330 // this. But now, thankx to the magic of Michaels 'close the
331 // stream suggestion', it works fine (no it doesn't!)
332 prcs.getInputStream().close();
333 prcs.getErrorStream().close();
334 prcs.getOutputStream().close();
335 prcs.destroy();
336 status = CANCELLED;
337 }
338 }
339 // Exception
340 catch (Exception exception) {
341 DebugStream.println("Exception in GShell.runLocal() - unexpected");
342 DebugStream.printStackTrace(exception);
343 status = ERROR;
344 }
345 }
346
347
348
349 /** Any threaded class must include this method to allow the thread body to be run. */
350 public void run() {
351 String col_name = args[args.length-1];
352
353 // Determine if the user has asked for an outfile.
354 String out_name = null;
355 BufferedOutputStream bos = null;
356 if(type == IMPORT || type == BUILD || type == SCHEDULE) {
357 if(type == IMPORT) {
358 out_name = (String) Gatherer.c_man.getCollection().import_options.getValue("out");
359 }
360 else if(type == BUILD) {
361 out_name = (String) Gatherer.c_man.getCollection().build_options.getValue("out");
362 }
363 else { // SCHEDULE
364 out_name = (String) Gatherer.c_man.getCollection().schedule_options.getValue("out");
365 }
366 if(out_name != null && out_name.length() > 0) {
367 try {
368 bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
369 }
370 catch (Exception error) {
371 DebugStream.printStackTrace(error);
372 }
373 }
374 }
375
376 // Issue a processBegun event
377 //ystem.err.println("\nFiring process begun for " + type + "...");
378 fireProcessBegun(type, status);
379 //ystem.err.println("Done process begun.");
380 if (Gatherer.isGsdlRemote) {
381 runRemote(args,bos);
382 }
383 else {
384 runLocal(args,bos);
385 }
386 //ystem.err.println("Done runLocal().");
387
388 if(status == OK) {
389 if (type == NEW) {
390 if (Gatherer.isGsdlRemote) {
391 Gatherer.remoteGreenstoneServer.downloadCollection(col_name);
392 }
393 }
394 else if(type == IMPORT) {
395
396 // download the archives directory (if gsdl server is remote)
397 if (Gatherer.isGsdlRemote) {
398
399 if (progress!=null) {
400 progress.messageOnProgressBar("Downloading archive data from server");
401 }
402
403 Gatherer.remoteGreenstoneServer.downloadCollectionArchives(col_name);
404
405 if (progress!=null) {
406 progress.messageOnProgressBar("");
407 }
408 }
409
410 // Refresh the DocXMLFileManager
411 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
412 DocXMLFileManager.clearDocXMLFiles();
413 if (Configuration.fedora_info.isActive()) { // FLI case
414 File collection_export_directory = new File(CollectionManager.getLoadedCollectionExportDirectoryPath());
415 DocXMLFileManager.loadDocXMLFiles(collection_export_directory,"docmets.xml");
416 }
417 else {
418 File collection_archives_directory = new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath());
419 DocXMLFileManager.loadDocXMLFiles(collection_archives_directory,"doc.xml");
420 }
421
422
423 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
424 }
425
426 else if(type == BUILD) {
427 // download the building directory (if gsdl server is remote)
428 if ((Gatherer.isGsdlRemote) && (!Configuration.fedora_info.isActive())) {
429 if (progress!=null) {
430 progress.messageOnProgressBar("Downloading index data from server");
431 }
432
433 if (!Gatherer.GS3){
434 // Only need to download build.cfg file
435 File build_cfg_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "build.cfg");
436 Gatherer.remoteGreenstoneServer.downloadCollectionFile(col_name, build_cfg_file);
437 }else{
438 // Only need to download buildConfig.xml file
439 File buildConfig_xml_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "buildConfig.xml");
440 Gatherer.remoteGreenstoneServer.downloadCollectionFile(col_name, buildConfig_xml_file);
441 }
442
443 if (progress!=null) {
444 progress.messageOnProgressBar("");
445 }
446 }
447 }
448 else if(type == CDIMAGE) {
449 // download exported files from tmp folder (if gsdl server is remote)
450 if (Gatherer.isGsdlRemote) {
451 if (progress!=null) {
452 progress.messageOnProgressBar("Downloading CD-ROM data from server");
453 }
454
455 // !! TO DO
456
457 if (progress!=null) {
458 progress.messageOnProgressBar("");
459 }
460 }
461 }
462 }
463
464 // We're done.
465 //ystem.err.println("Firing process complete for " + type + "...");
466 fireProcessComplete(type, status);
467 // Close bos
468 if(bos != null) {
469 try {
470 bos.close();
471 bos = null;
472 }
473 catch(Exception error) {
474 DebugStream.printStackTrace(error);
475 }
476 }
477 }
478
479
480 public void fireMessage(String message)
481 {
482 fireMessage(type, typeAsString(type) + "> " + message, status, buffered_output_stream);
483 }
484
485
486 /** Method for firing a message to all interested listeners.
487 * @param type An <strong>int</strong> indicating the process type.
488 * @param message The message as a <strong>String</strong>.
489 * @param status An <strong>int</strong> specifying the current status of the process.
490 */
491 public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
492 GShellEvent event = new GShellEvent(this, 0, type, message, status);
493 // If there is a progress monitor attached, pass the event to it first. Note that we pass a queue of messages as the processing may cause one message to be split into several.
494 ArrayList message_queue = new ArrayList();
495 message_queue.add(event);
496 if(progress != null) {
497 progress.process(message_queue);
498 }
499 for(int j = 0; j < message_queue.size(); j++) {
500 GShellEvent current_event = (GShellEvent) message_queue.get(j);
501 // If the event hasn't been vetoed, pass it on to other listeners
502 if(!current_event.isVetoed()) {
503 Object[] concerned = listeners.getListenerList();
504 for(int i = 0; i < concerned.length ; i++) {
505 if(concerned[i] == GShellListener.class) {
506 ((GShellListener)concerned[i+1]).message(current_event);
507 }
508 }
509 concerned = null;
510 }
511 }
512 // And if we have a buffered output stream from error messages, send the message there
513 if(bos != null) {
514 try {
515 bos.write(message.getBytes(), 0, message.length());
516 }
517 catch(Exception exception) {
518 DebugStream.println("Exception in GShell.fireMessage() - unexpected");
519 DebugStream.printStackTrace(exception);
520 }
521 }
522 message_queue = null;
523 event = null;
524 }
525
526 /** Method for firing a process begun event which is called, strangly enough, when the process begins.
527 * @param type An <strong>int</strong> indicating the process type.
528 * @param status An <strong>int</strong> specifying the current status of the process.
529 */
530 protected void fireProcessBegun(int type, int status) {
531 // Start the progres monitor if available
532 if(progress != null) {
533 //ystem.err.println("About to call progress.start().");
534 progress.start();
535 //ystem.err.println("Called progress.start().");
536 }
537 // Fire an event
538 GShellEvent event = new GShellEvent(this, 0, type, "", status);
539 Object[] concerned = listeners.getListenerList();
540 for(int i = 0; i < concerned.length ; i++) {
541 if(concerned[i] == GShellListener.class) {
542 ((GShellListener)concerned[i+1]).processBegun(event);
543 }
544 }
545 }
546
547
548 /** Method for firing a process complete event which is called, no surprise here, when the process ends.
549 * @param type An <strong>int</strong> indicating the process type.
550 * @param status An <strong>int</strong> specifying the current status of the process.
551 */
552 protected void fireProcessComplete(int type, int status) {
553 // Tidy up by stopping the progress bar. If it was cancelled then the cancel command has arrived via the progress bars and they don't need to be told again (it actually causes problems).
554 if(progress != null && status != CANCELLED) {
555 progress.stop();
556 }
557
558 // If we were cancelled, and we are lower details modes, fire off one last message.
559 if(status == CANCELLED && Configuration.getMode() <= Configuration.SYSTEMS_MODE) {
560 GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
561 Object[] concerned = listeners.getListenerList();
562 for(int i = 0; i < concerned.length ; i++) {
563 if(concerned[i] == GShellListener.class) {
564 ((GShellListener)concerned[i+1]).message(current_event);
565 }
566 }
567 concerned = null;
568 }
569
570 String msg = "";
571 // If we are creating collection and have trouble with permissions, we need more messages
572 if(status == ERROR && type == GShell.NEW){
573 msg = args[args.length-1];
574 }
575 // And firing off an event
576 GShellEvent event = new GShellEvent(this, 0, type, msg, status);
577 Object[] concerned = listeners.getListenerList();
578 for(int i = 0; i < concerned.length ; i++) {
579 if(concerned[i] == GShellListener.class) {
580 ((GShellListener)concerned[i+1]).processComplete(event);
581 }
582 }
583 }
584
585 /** Method to determine if the user, via the progress monitor, has signalled stop.
586 * @return A <strong>boolean</strong> indicating if the user wanted to stop.
587 */
588 public boolean hasSignalledStop() {
589 boolean has_signalled_stop = false;
590 if(progress != null) {
591 has_signalled_stop = progress.hasSignalledStop();
592 }
593 if(has_signalled_stop) {
594 status = CANCELLED;
595 }
596 return has_signalled_stop;
597 }
598
599 /** Converts a type into a text representation.
600 * @param type An <strong>int</strong> which maps to a shell process type.
601 * @return A <strong>String</strong> which is the thread process's text name.
602 */
603 public String typeAsString(int type) {
604 String name = null;
605 switch(type) {
606 case BUILD:
607 name = "buildcol.pl";
608 break;
609 case IMPORT:
610 name = "import.pl";
611 break;
612 case NEW:
613 name = "mkcol.pl";
614 break;
615 case EXPORTAS:
616 name = "export.pl";
617 break;
618 case CDIMAGE:
619 name = "exportcol.pl";
620 break;
621 case CONVERT:
622 name = "convert_coll_from_gs2.pl";
623 break;
624 case EXPLODE:
625 name = "explode_metadata_database.pl";
626 break;
627 case SRCREPLACE: // To work with replace_srcdoc_with_html.pl
628 name = "replace_srcdoc_with_html.pl";
629 break;
630 case SCHEDULE:
631 name = "schedule.pl";
632 break;
633 default:
634 name = "";
635 }
636 return name;
637 }
638}
Note: See TracBrowser for help on using the repository browser.