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

Last change on this file since 15125 was 15125, checked in by ak19, 16 years ago

runLocal executes a Process with single string command in general, but when there are spaces in the path it executes with arguments

  • Property svn:keywords set to Author Date Id Revision
File size: 22.0 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 /** Elements in process type enumeration. */
84 static final public int BUILD = 0;
85 static final public int IMPORT = 1;
86 static final public int NEW = 2;
87 static final public int EXPORTAS = 3;
88 static final public int CDIMAGE = 4;
89 static final public int CONVERT = 5;
90 static final public int EXPLODE = 6;
91 static final public int SRCREPLACE = 7; // for replacing source docs with their html
92
93 /** Elements in status type enumeration. */
94 static final public int ERROR = 0;
95 static final public int OK = 1;
96 static final public int CANCELLED = 2;
97
98 /** Elements in process type name enumeration. */
99 static public String GSHELL_BUILD = "gshell_build";
100 static public String GSHELL_IMPORT = "gshell_import";
101 static public String GSHELL_NEW = "gshell_new";
102 static public String GSHELL_EXPORTAS = "gshell_exportas";
103 static public String GSHELL_CDIMAGE = "gshell_cdimage";
104 static public String GSHELL_CONVERT = "gshell_convert";
105 static public String GSHELL_EXPLODE = "gshell_explode";
106 static public String GSHELL_SRCREPLACE = "gshell_srcreplace"; // for replacing source docs with their html versions
107
108 /** 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
109 * @param process the Process to test
110 * @return true if it is still executing, false otherwise
111 */
112 static public boolean processRunning(Process process) {
113 boolean process_running = false;
114
115 try {
116 process.exitValue(); // This will throw an exception if the process hasn't ended yet.
117 }
118 catch(IllegalThreadStateException itse) {
119 process_running = true;
120 }
121 catch(Exception exception) {
122 DebugStream.printStackTrace(exception);
123 }
124 return process_running;
125 }
126
127 /** Constructor gatherer all the data required to create a new process, and emit meaningfull messages.
128 * @param args A <strong>String[]</strong> containing the arguments to the process thread, including the name of the executable.
129 * @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).
130 * @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>.
131 * @param caller The default <i>GShellListener</i> that is interested in the progress of this process.
132 * @param progress The <i>GShellProgressMonitor</i> associated with this process.
133 * @param name A <strong>String</strong> identifier given to the process, for convience and debug reasons.
134 */
135 public GShell(String args[], int type, int msg_type, GShellListener caller, GShellProgressMonitor progress, String name) {
136 super(name);
137 this.args = args;
138 this.msg_type = msg_type;
139 this.type = type;
140 this.caller = caller;
141 this.progress = progress;
142 this.status = 0;
143 // Lower this jobs priority
144 this.setPriority(Thread.MIN_PRIORITY);
145 listeners = new EventListenerList();
146 listeners.add(GShellListener.class, caller);
147 }
148 /** This method adds another shell listener to this process.
149 * @param listener The new <i>GShellListener</i>.
150 */
151 public void addGShellListener(GShellListener listener) {
152 listeners.add(GShellListener.class, listener);
153 }
154 /** This method removes a certain shell listener from this process.
155 * @param listener The <i>GShellListener</i> to be removed.
156 */
157 /* private void removeGShellListener(GShellListener listener) {
158 listeners.remove(GShellListener.class, listener);
159 } */
160
161 protected StringBuffer get_stream_char(InputStreamReader isr, StringBuffer line_buffer,
162 BufferedOutputStream bos) throws IOException
163 {
164 int c = isr.read();
165 ///atherer.println("isr: '" + (char) c + "'");
166 if(c == '\n' || c == '\r') {
167 if(line_buffer.length() > 0) {
168 String line = line_buffer.toString();
169 // DebugStream.println("* " + line + " *");
170 fireMessage(type, typeAsString(type) + "> " + line, status, bos);
171 line_buffer = new StringBuffer();
172 }
173 }
174 else {
175 line_buffer.append((char)c);
176 }
177
178 return line_buffer;
179 }
180
181
182 private void runRemote(String[] args, BufferedOutputStream bos)
183 {
184 // Make sure the process hasn't been cancelled
185 if (hasSignalledStop()) {
186 return;
187 }
188
189 try {
190 int directory_name_end = args[0].lastIndexOf(File.separator);
191 String script_name = ((directory_name_end != -1) ? args[0].substring(directory_name_end + 1) : args[0]);
192 System.err.println("Script name: " + script_name);
193 String collection_name = args[args.length - 1];
194 System.err.println("Collection name: " + collection_name);
195
196
197 String script_args = "";
198 for (int i = 1; i < (args.length - 1); i++) {
199 // Skip arguments that won't make sense on the server
200 if (args[i].equals("-collectdir") || args[i].equals("-importdir") || args[i].equals("-builddir")) {
201 i++;
202 continue;
203 }
204
205 // Script arguments get changed to CGI arguments
206 if (args[i].startsWith("-")) {
207 script_args += "&" + args[i].substring(1) + "=";
208 if ((i + 1) < (args.length - 1) && !args[i + 1].startsWith("-")) {
209 script_args += URLEncoder.encode(args[i + 1], "UTF-8");
210 i++;
211 }
212 }
213 }
214
215 System.err.println("Script args: " + script_args);
216 buffered_output_stream = bos;
217 String command_output = RemoteGreenstoneServer.runScript(collection_name, script_name, script_args, this);
218 status = (command_output.equals("") ? CANCELLED : OK);
219 }
220 catch (Exception exception) {
221 DebugStream.printStackTrace(exception);
222 status = ERROR;
223 }
224 }
225
226
227 private void runLocal(String[] args, BufferedOutputStream bos)
228 {
229 boolean execArgs = false;
230 try {
231 String command = "";
232 for(int i = 0; i < args.length && !execArgs; i++) {
233 if(args[i].indexOf(' ') != -1) { // if any argument contains spaces, then we'll have to
234 execArgs = true; // execute it with argument array rather than command item
235 }
236 command = command + args[i] + " ";
237 }
238
239 ///ystem.err.println("Command: " + command);
240 fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
241
242 Runtime rt = Runtime.getRuntime();
243 Process prcs = null;
244 if (Utility.isWindows() || execArgs){ // if execArgs=true, then we're on Linux and one or more arguments
245 // contained spaces, meaning we'll have to execute it with an argument array rather than command item.
246 // This is especially necessary if one of the args is a filepath containing spaces in the filename or path
247 prcs = rt.exec(args);
248 } else { // we're on Linux and none of the arguments contains any spaces,
249 // so just execute with single string argument (command)
250 prcs = rt.exec(command);
251 }
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) {
357 if(type == IMPORT) {
358 out_name = (String) Gatherer.c_man.getCollection().import_options.getValue("out");
359 }
360 else {
361 out_name = (String) Gatherer.c_man.getCollection().build_options.getValue("out");
362 }
363 if(out_name != null && out_name.length() > 0) {
364 try {
365 bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
366 }
367 catch (Exception error) {
368 DebugStream.printStackTrace(error);
369 }
370 }
371 }
372
373 // Issue a processBegun event
374 //ystem.err.println("\nFiring process begun for " + type + "...");
375 fireProcessBegun(type, status);
376 //ystem.err.println("Done process begun.");
377 if (Gatherer.isGsdlRemote) {
378 runRemote(args,bos);
379 }
380 else {
381 runLocal(args,bos);
382 }
383 //ystem.err.println("Done runLocal().");
384
385 if(status == OK) {
386 if (type == NEW) {
387 if (Gatherer.isGsdlRemote) {
388 RemoteGreenstoneServer.downloadCollection(col_name);
389 }
390 }
391 else if(type == IMPORT) {
392
393 // download the archives directory (if gsdl server is remote)
394 if (Gatherer.isGsdlRemote) {
395
396 if (progress!=null) {
397 progress.messageOnProgressBar("Downloading archive data from server");
398 }
399
400 RemoteGreenstoneServer.downloadCollectionArchives(col_name);
401
402 if (progress!=null) {
403 progress.messageOnProgressBar("");
404 }
405 }
406
407 // Refresh the DocXMLFileManager
408 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
409 DocXMLFileManager.clearDocXMLFiles();
410 DocXMLFileManager.loadDocXMLFiles(new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath()));
411 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
412 }
413
414 else if(type == BUILD) {
415 // download the building directory (if gsdl server is remote)
416 if ((Gatherer.isGsdlRemote) && (!Configuration.fedora_info.isActive())) {
417 if (progress!=null) {
418 progress.messageOnProgressBar("Downloading index data from server");
419 }
420
421 if (!Gatherer.GS3){
422 // Only need to download build.cfg file
423 File build_cfg_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "build.cfg");
424 RemoteGreenstoneServer.downloadCollectionFile(col_name, build_cfg_file);
425 }else{
426 // Only need to download buildConfig.xml file
427 File buildConfig_xml_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "buildConfig.xml");
428 RemoteGreenstoneServer.downloadCollectionFile(col_name, buildConfig_xml_file);
429 }
430
431 if (progress!=null) {
432 progress.messageOnProgressBar("");
433 }
434 }
435 }
436 else if(type == CDIMAGE) {
437 // download exported files from tmp folder (if gsdl server is remote)
438 if (Gatherer.isGsdlRemote) {
439 if (progress!=null) {
440 progress.messageOnProgressBar("Downloading CD-ROM data from server");
441 }
442
443 // !! TO DO
444
445 if (progress!=null) {
446 progress.messageOnProgressBar("");
447 }
448 }
449 }
450 }
451
452 // We're done.
453 //ystem.err.println("Firing process complete for " + type + "...");
454 fireProcessComplete(type, status);
455 // Close bos
456 if(bos != null) {
457 try {
458 bos.close();
459 bos = null;
460 }
461 catch(Exception error) {
462 DebugStream.printStackTrace(error);
463 }
464 }
465 }
466
467
468 public void fireMessage(String message)
469 {
470 fireMessage(type, typeAsString(type) + "> " + message, status, buffered_output_stream);
471 }
472
473
474 /** Method for firing a message to all interested listeners.
475 * @param type An <strong>int</strong> indicating the process type.
476 * @param message The message as a <strong>String</strong>.
477 * @param status An <strong>int</strong> specifying the current status of the process.
478 */
479 public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
480 GShellEvent event = new GShellEvent(this, 0, type, message, status);
481 // 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.
482 ArrayList message_queue = new ArrayList();
483 message_queue.add(event);
484 if(progress != null) {
485 progress.process(message_queue);
486 }
487 for(int j = 0; j < message_queue.size(); j++) {
488 GShellEvent current_event = (GShellEvent) message_queue.get(j);
489 // If the event hasn't been vetoed, pass it on to other listeners
490 if(!current_event.isVetoed()) {
491 Object[] concerned = listeners.getListenerList();
492 for(int i = 0; i < concerned.length ; i++) {
493 if(concerned[i] == GShellListener.class) {
494 ((GShellListener)concerned[i+1]).message(current_event);
495 }
496 }
497 concerned = null;
498 }
499 }
500 // And if we have a buffered output stream from error messages, send the message there
501 if(bos != null) {
502 try {
503 bos.write(message.getBytes(), 0, message.length());
504 }
505 catch(Exception exception) {
506 DebugStream.println("Exception in GShell.fireMessage() - unexpected");
507 DebugStream.printStackTrace(exception);
508 }
509 }
510 message_queue = null;
511 event = null;
512 }
513
514 /** Method for firing a process begun event which is called, strangly enough, when the process begins.
515 * @param type An <strong>int</strong> indicating the process type.
516 * @param status An <strong>int</strong> specifying the current status of the process.
517 */
518 protected void fireProcessBegun(int type, int status) {
519 // Start the progres monitor if available
520 if(progress != null) {
521 //ystem.err.println("About to call progress.start().");
522 progress.start();
523 //ystem.err.println("Called progress.start().");
524 }
525 // Fire an event
526 GShellEvent event = new GShellEvent(this, 0, type, "", status);
527 Object[] concerned = listeners.getListenerList();
528 for(int i = 0; i < concerned.length ; i++) {
529 if(concerned[i] == GShellListener.class) {
530 ((GShellListener)concerned[i+1]).processBegun(event);
531 }
532 }
533 }
534
535
536 /** Method for firing a process complete event which is called, no surprise here, when the process ends.
537 * @param type An <strong>int</strong> indicating the process type.
538 * @param status An <strong>int</strong> specifying the current status of the process.
539 */
540 protected void fireProcessComplete(int type, int status) {
541 // 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).
542 if(progress != null && status != CANCELLED) {
543 progress.stop();
544 }
545
546 // If we were cancelled, and we are lower details modes, fire off one last message.
547 if(status == CANCELLED && Configuration.getMode() <= Configuration.SYSTEMS_MODE) {
548 GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
549 Object[] concerned = listeners.getListenerList();
550 for(int i = 0; i < concerned.length ; i++) {
551 if(concerned[i] == GShellListener.class) {
552 ((GShellListener)concerned[i+1]).message(current_event);
553 }
554 }
555 concerned = null;
556 }
557
558 String msg = "";
559 // If we are creating collection and have trouble with permissions, we need more messages
560 if(status == ERROR && type == GShell.NEW){
561 msg = args[args.length-1];
562 }
563 // And firing off an event
564 GShellEvent event = new GShellEvent(this, 0, type, msg, status);
565 Object[] concerned = listeners.getListenerList();
566 for(int i = 0; i < concerned.length ; i++) {
567 if(concerned[i] == GShellListener.class) {
568 ((GShellListener)concerned[i+1]).processComplete(event);
569 }
570 }
571 }
572
573 /** Method to determine if the user, via the progress monitor, has signalled stop.
574 * @return A <strong>boolean</strong> indicating if the user wanted to stop.
575 */
576 public boolean hasSignalledStop() {
577 boolean has_signalled_stop = false;
578 if(progress != null) {
579 has_signalled_stop = progress.hasSignalledStop();
580 }
581 if(has_signalled_stop) {
582 status = CANCELLED;
583 }
584 return has_signalled_stop;
585 }
586
587 /** Converts a type into a text representation.
588 * @param type An <strong>int</strong> which maps to a shell process type.
589 * @return A <strong>String</strong> which is the thread process's text name.
590 */
591 public String typeAsString(int type) {
592 String name = null;
593 switch(type) {
594 case BUILD:
595 name = "buildcol.pl";
596 break;
597 case IMPORT:
598 name = "import.pl";
599 break;
600 case NEW:
601 name = "mkcol.pl";
602 break;
603 case EXPORTAS:
604 name = "export.pl";
605 break;
606 case CDIMAGE:
607 name = "exportcol.pl";
608 break;
609 case CONVERT:
610 name = "convert_coll_from_gs2.pl";
611 break;
612 case EXPLODE:
613 name = "explode_metadata_database.pl";
614 break;
615 case SRCREPLACE: // To work with replace_srcdoc_with_html.pl
616 name = "replace_srcdoc_with_html.pl";
617 break;
618 default:
619 name = "";
620 }
621 return name;
622 }
623}
Note: See TracBrowser for help on using the repository browser.