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

Last change on this file since 14116 was 14116, checked in by qq6, 17 years ago

set 'prcs = rt.exec(args)' in runLocal() for escaping spaces in the name of the directory on windows, and set 'prcs = rt.exec(command)' otherwise;

  • Property svn:keywords set to Author Date Id Revision
File size: 20.5 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
92 /** Elements in status type enumeration. */
93 static final public int ERROR = 0;
94 static final public int OK = 1;
95 static final public int CANCELLED = 2;
96
97 /** Elements in process type name enumeration. */
98 static public String GSHELL_BUILD = "gshell_build";
99 static public String GSHELL_IMPORT = "gshell_import";
100 static public String GSHELL_NEW = "gshell_new";
101 static public String GSHELL_EXPORTAS = "gshell_exportas";
102 static public String GSHELL_CDIMAGE = "gshell_cdimage";
103 static public String GSHELL_CONVERT = "gshell_convert";
104 static public String GSHELL_EXPLODE = "gshell_explode";
105
106 /** 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
107 * @param process the Process to test
108 * @return true if it is still executing, false otherwise
109 */
110 static public boolean processRunning(Process process) {
111 boolean process_running = false;
112
113 try {
114 process.exitValue(); // This will throw an exception if the process hasn't ended yet.
115 }
116 catch(IllegalThreadStateException itse) {
117 process_running = true;
118 }
119 catch(Exception exception) {
120 DebugStream.printStackTrace(exception);
121 }
122 return process_running;
123 }
124
125 /** Constructor gatherer all the data required to create a new process, and emit meaningfull messages.
126 * @param args A <strong>String[]</strong> containing the arguments to the process thread, including the name of the executable.
127 * @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).
128 * @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>.
129 * @param caller The default <i>GShellListener</i> that is interested in the progress of this process.
130 * @param progress The <i>GShellProgressMonitor</i> associated with this process.
131 * @param name A <strong>String</strong> identifier given to the process, for convience and debug reasons.
132 */
133 public GShell(String args[], int type, int msg_type, GShellListener caller, GShellProgressMonitor progress, String name) {
134 super(name);
135 this.args = args;
136 this.msg_type = msg_type;
137 this.type = type;
138 this.caller = caller;
139 this.progress = progress;
140 this.status = 0;
141 // Lower this jobs priority
142 this.setPriority(Thread.MIN_PRIORITY);
143 listeners = new EventListenerList();
144 listeners.add(GShellListener.class, caller);
145 }
146 /** This method adds another shell listener to this process.
147 * @param listener The new <i>GShellListener</i>.
148 */
149 public void addGShellListener(GShellListener listener) {
150 listeners.add(GShellListener.class, listener);
151 }
152 /** This method removes a certain shell listener from this process.
153 * @param listener The <i>GShellListener</i> to be removed.
154 */
155 /* private void removeGShellListener(GShellListener listener) {
156 listeners.remove(GShellListener.class, listener);
157 } */
158
159 protected StringBuffer get_stream_char(InputStreamReader isr, StringBuffer line_buffer,
160 BufferedOutputStream bos) throws IOException
161 {
162 int c = isr.read();
163 ///atherer.println("isr: '" + (char) c + "'");
164 if(c == '\n' || c == '\r') {
165 if(line_buffer.length() > 0) {
166 String line = line_buffer.toString();
167 // DebugStream.println("* " + line + " *");
168 fireMessage(type, typeAsString(type) + "> " + line, status, bos);
169 line_buffer = new StringBuffer();
170 }
171 }
172 else {
173 line_buffer.append((char)c);
174 }
175
176 return line_buffer;
177 }
178
179
180 private void runRemote(String[] args, BufferedOutputStream bos)
181 {
182 // Make sure the process hasn't been cancelled
183 if (hasSignalledStop()) {
184 return;
185 }
186
187 try {
188 int directory_name_end = args[0].lastIndexOf(File.separator);
189 String script_name = ((directory_name_end != -1) ? args[0].substring(directory_name_end + 1) : args[0]);
190 System.err.println("Script name: " + script_name);
191 String collection_name = args[args.length - 1];
192 System.err.println("Collection name: " + collection_name);
193
194 String script_args = "";
195 for (int i = 1; i < (args.length - 1); i++) {
196 // Skip arguments that won't make sense on the server
197 if (args[i].equals("-collectdir") || args[i].equals("-importdir") || args[i].equals("-builddir")) {
198 i++;
199 continue;
200 }
201
202 // Script arguments get changed to CGI arguments
203 if (args[i].startsWith("-")) {
204 script_args += "&" + args[i].substring(1) + "=";
205 if ((i + 1) < (args.length - 1) && !args[i + 1].startsWith("-")) {
206 script_args += URLEncoder.encode(args[i + 1], "UTF-8");
207 i++;
208 }
209 }
210 }
211
212 System.err.println("Script args: " + script_args);
213 buffered_output_stream = bos;
214 String command_output = RemoteGreenstoneServer.runScript(collection_name, script_name, script_args, this);
215 status = (command_output.equals("") ? CANCELLED : OK);
216 }
217 catch (Exception exception) {
218 DebugStream.printStackTrace(exception);
219 status = ERROR;
220 }
221 }
222
223
224 private void runLocal(String[] args, BufferedOutputStream bos)
225 {
226 try {
227 String command = "";
228 for(int i = 0; i < args.length; i++) {
229 command = command + args[i] + " ";
230 }
231
232 ///ystem.err.println("Command: " + command);
233 fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
234
235 Runtime rt = Runtime.getRuntime();
236 Process prcs = null;
237 if (Utility.isWindows()){
238 prcs = rt.exec(args);
239 }else{
240 prcs = rt.exec(command);
241 }
242
243 InputStreamReader eisr = new InputStreamReader( prcs.getErrorStream(), "UTF-8" );
244 InputStreamReader stdisr = new InputStreamReader( prcs.getInputStream(), "UTF-8" );
245
246 StringBuffer eline_buffer = new StringBuffer();
247 StringBuffer stdline_buffer = new StringBuffer();
248
249 while(/*type != GShell.NEW &&*/ processRunning(prcs) && !hasSignalledStop()) {
250 // Hopefully this doesn't block if the process is trying to write to STDOUT.
251 if((eisr!=null) && eisr.ready()) {
252 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
253 }
254 // Hopefully this won't block if the process is trying to write to STDERR
255 else if(stdisr.ready()) {
256 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
257 }
258 else {
259 try {
260 sleep(100);
261 }
262 catch(Exception exception) {
263 }
264 }
265 }
266
267 if(!hasSignalledStop()) {
268 // Of course, just because the process is finished doesn't
269 // mean the incoming streams are empty. Unfortunately I've
270 // got no chance of preserving order, so I'll process the
271 // error stream first, then the out stream
272 while(eisr.ready()) {
273 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
274 }
275
276 while(stdisr.ready()) {
277 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
278 }
279
280 // Ensure that any messages still remaining in the string buffers are fired off.
281 if(eline_buffer.length() > 0) {
282 String eline = eline_buffer.toString();
283 //DebugStream.println("Last bit of eline: " + eline);
284 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
285 eline = null;
286 }
287
288 if(stdline_buffer.length() > 0) {
289 String stdline = stdline_buffer.toString();
290 //DebugStream.println("Last bit of stdline: " + stdline);
291 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
292 stdline = null;
293 }
294 }
295 else {
296 DebugStream.println("We've been asked to stop.");
297 }
298
299
300 if(!hasSignalledStop()) {
301 // Now display final message based on exit value
302
303 prcs.waitFor();
304
305 if(prcs.exitValue() == 0) {
306 status = OK;
307 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Success"), status, null);
308 }
309 else {
310 status = ERROR;
311 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Failure"), status, null);
312 }
313
314 eisr.close();
315 stdisr.close();
316 }
317 else {
318 // I need to somehow kill the child process. Unfortunately
319 // Thread.stop() and Process.destroy() both fail to do
320 // this. But now, thankx to the magic of Michaels 'close the
321 // stream suggestion', it works fine (no it doesn't!)
322 prcs.getInputStream().close();
323 prcs.getErrorStream().close();
324 prcs.getOutputStream().close();
325 prcs.destroy();
326 status = CANCELLED;
327 }
328 }
329 // Exception
330 catch (Exception exception) {
331 DebugStream.println("Exception in GShell.runLocal() - unexpected");
332 DebugStream.printStackTrace(exception);
333 status = ERROR;
334 }
335 }
336
337
338
339 /** Any threaded class must include this method to allow the thread body to be run. */
340 public void run() {
341 String col_name = args[args.length-1];
342
343 // Determine if the user has asked for an outfile.
344 String out_name = null;
345 BufferedOutputStream bos = null;
346 if(type == IMPORT || type == BUILD) {
347 if(type == IMPORT) {
348 out_name = (String) Gatherer.c_man.getCollection().import_options.getValue("out");
349 }
350 else {
351 out_name = (String) Gatherer.c_man.getCollection().build_options.getValue("out");
352 }
353 if(out_name != null && out_name.length() > 0) {
354 try {
355 bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
356 }
357 catch (Exception error) {
358 DebugStream.printStackTrace(error);
359 }
360 }
361 }
362
363 // Issue a processBegun event
364 //ystem.err.println("\nFiring process begun for " + type + "...");
365 fireProcessBegun(type, status);
366 //ystem.err.println("Done process begun.");
367 if (Gatherer.isGsdlRemote) {
368 runRemote(args,bos);
369 }
370 else {
371 runLocal(args,bos);
372 }
373 //ystem.err.println("Done runLocal().");
374
375 if(status == OK) {
376 if (type == NEW) {
377 if (Gatherer.isGsdlRemote) {
378 RemoteGreenstoneServer.downloadCollection(col_name);
379 }
380 }
381 else if(type == IMPORT) {
382
383 // download the archives directory (if gsdl server is remote)
384 if (Gatherer.isGsdlRemote) {
385
386 if (progress!=null) {
387 progress.messageOnProgressBar("Downloading archive data from server");
388 }
389
390 RemoteGreenstoneServer.downloadCollectionArchives(col_name);
391
392 if (progress!=null) {
393 progress.messageOnProgressBar("");
394 }
395 }
396
397 // Refresh the DocXMLFileManager
398 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
399 DocXMLFileManager.clearDocXMLFiles();
400 DocXMLFileManager.loadDocXMLFiles(new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath()));
401 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
402 }
403
404 else if(type == BUILD) {
405 // download the building directory (if gsdl server is remote)
406 if (Gatherer.isGsdlRemote) {
407 if (progress!=null) {
408 progress.messageOnProgressBar("Downloading index data from server");
409 }
410
411 // Only need to download build.cfg file
412 File build_cfg_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "build.cfg");
413 RemoteGreenstoneServer.downloadCollectionFile(col_name, build_cfg_file);
414
415 if (progress!=null) {
416 progress.messageOnProgressBar("");
417 }
418 }
419 }
420 else if(type == CDIMAGE) {
421 // download exported files from tmp folder (if gsdl server is remote)
422 if (Gatherer.isGsdlRemote) {
423 if (progress!=null) {
424 progress.messageOnProgressBar("Downloading CD-ROM data from server");
425 }
426
427 // !! TO DO
428
429 if (progress!=null) {
430 progress.messageOnProgressBar("");
431 }
432 }
433 }
434 }
435
436 // We're done.
437 //ystem.err.println("Firing process complete for " + type + "...");
438 fireProcessComplete(type, status);
439 // Close bos
440 if(bos != null) {
441 try {
442 bos.close();
443 bos = null;
444 }
445 catch(Exception error) {
446 DebugStream.printStackTrace(error);
447 }
448 }
449 }
450
451
452 public void fireMessage(String message)
453 {
454 fireMessage(type, typeAsString(type) + "> " + message, status, buffered_output_stream);
455 }
456
457
458 /** Method for firing a message to all interested listeners.
459 * @param type An <strong>int</strong> indicating the process type.
460 * @param message The message as a <strong>String</strong>.
461 * @param status An <strong>int</strong> specifying the current status of the process.
462 */
463 public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
464 GShellEvent event = new GShellEvent(this, 0, type, message, status);
465 // 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.
466 ArrayList message_queue = new ArrayList();
467 message_queue.add(event);
468 if(progress != null) {
469 progress.process(message_queue);
470 }
471 for(int j = 0; j < message_queue.size(); j++) {
472 GShellEvent current_event = (GShellEvent) message_queue.get(j);
473 // If the event hasn't been vetoed, pass it on to other listeners
474 if(!current_event.isVetoed()) {
475 Object[] concerned = listeners.getListenerList();
476 for(int i = 0; i < concerned.length ; i++) {
477 if(concerned[i] == GShellListener.class) {
478 ((GShellListener)concerned[i+1]).message(current_event);
479 }
480 }
481 concerned = null;
482 }
483 }
484 // And if we have a buffered output stream from error messages, send the message there
485 if(bos != null) {
486 try {
487 bos.write(message.getBytes(), 0, message.length());
488 }
489 catch(Exception exception) {
490 DebugStream.println("Exception in GShell.fireMessage() - unexpected");
491 DebugStream.printStackTrace(exception);
492 }
493 }
494 message_queue = null;
495 event = null;
496 }
497
498 /** Method for firing a process begun event which is called, strangly enough, when the process begins.
499 * @param type An <strong>int</strong> indicating the process type.
500 * @param status An <strong>int</strong> specifying the current status of the process.
501 */
502 protected void fireProcessBegun(int type, int status) {
503 // Start the progres monitor if available
504 if(progress != null) {
505 //ystem.err.println("About to call progress.start().");
506 progress.start();
507 //ystem.err.println("Called progress.start().");
508 }
509 // Fire an event
510 GShellEvent event = new GShellEvent(this, 0, type, "", status);
511 Object[] concerned = listeners.getListenerList();
512 for(int i = 0; i < concerned.length ; i++) {
513 if(concerned[i] == GShellListener.class) {
514 ((GShellListener)concerned[i+1]).processBegun(event);
515 }
516 }
517 }
518
519
520 /** Method for firing a process complete event which is called, no surprise here, when the process ends.
521 * @param type An <strong>int</strong> indicating the process type.
522 * @param status An <strong>int</strong> specifying the current status of the process.
523 */
524 protected void fireProcessComplete(int type, int status) {
525 // 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).
526 if(progress != null && status != CANCELLED) {
527 progress.stop();
528 }
529
530 // If we were cancelled, and we are lower details modes, fire off one last message.
531 if(status == CANCELLED && Configuration.getMode() <= Configuration.SYSTEMS_MODE) {
532 GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
533 Object[] concerned = listeners.getListenerList();
534 for(int i = 0; i < concerned.length ; i++) {
535 if(concerned[i] == GShellListener.class) {
536 ((GShellListener)concerned[i+1]).message(current_event);
537 }
538 }
539 concerned = null;
540 }
541 // And firing off an event
542 GShellEvent event = new GShellEvent(this, 0, type, "", status);
543 Object[] concerned = listeners.getListenerList();
544 for(int i = 0; i < concerned.length ; i++) {
545 if(concerned[i] == GShellListener.class) {
546 ((GShellListener)concerned[i+1]).processComplete(event);
547 }
548 }
549 }
550
551 /** Method to determine if the user, via the progress monitor, has signalled stop.
552 * @return A <strong>boolean</strong> indicating if the user wanted to stop.
553 */
554 public boolean hasSignalledStop() {
555 boolean has_signalled_stop = false;
556 if(progress != null) {
557 has_signalled_stop = progress.hasSignalledStop();
558 }
559 if(has_signalled_stop) {
560 status = CANCELLED;
561 }
562 return has_signalled_stop;
563 }
564
565 /** Converts a type into a text representation.
566 * @param type An <strong>int</strong> which maps to a shell process type.
567 * @return A <strong>String</strong> which is the thread process's text name.
568 */
569 public String typeAsString(int type) {
570 String name = null;
571 switch(type) {
572 case BUILD:
573 name = "buildcol.pl";
574 break;
575 case IMPORT:
576 name = "import.pl";
577 break;
578 case NEW:
579 name = "mkcol.pl";
580 break;
581 case EXPORTAS:
582 name = "export.pl";
583 break;
584 case CDIMAGE:
585 name = "exportcol.pl";
586 break;
587 case CONVERT:
588 name = "convert_coll_from_gs2.pl";
589 break;
590 case EXPLODE:
591 name = "explode_metadata_database.pl";
592 break;
593 default:
594 name = "";
595 }
596 return name;
597 }
598}
Note: See TracBrowser for help on using the repository browser.