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

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

runLocal() method now uniformly executes a process with arguments instead of a single string composed of the multiple arguments. Spaces in filepaths are preserved. But this required a change to how mkcol.pl was executed from CollectionManager.java

  • Property svn:keywords set to Author Date Id Revision
File size: 21.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 /** 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 try {
230 String command = "";
231 for(int i = 0; i < args.length; i++) {
232 command = command + args[i] + " ";
233 }
234
235 ///ystem.err.println("Command: " + command);
236 fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
237
238 Runtime rt = Runtime.getRuntime();
239 Process prcs = null;
240
241 prcs = rt.exec(args);
242 // If we used single argument exec, spaces in filenames or paths cause problems
243 // I.e. better to avoid: prcs = rt.exec(command);
244
245 InputStreamReader eisr = new InputStreamReader( prcs.getErrorStream(), "UTF-8" );
246 InputStreamReader stdisr = new InputStreamReader( prcs.getInputStream(), "UTF-8" );
247
248 StringBuffer eline_buffer = new StringBuffer();
249 StringBuffer stdline_buffer = new StringBuffer();
250
251 while(/*type != GShell.NEW &&*/ processRunning(prcs) && !hasSignalledStop()) {
252 // Hopefully this doesn't block if the process is trying to write to STDOUT.
253 if((eisr!=null) && eisr.ready()) {
254 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
255 }
256 // Hopefully this won't block if the process is trying to write to STDERR
257 else if(stdisr.ready()) {
258 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
259 }
260 else {
261 try {
262 sleep(100);
263 }
264 catch(Exception exception) {
265 }
266 }
267 }
268
269 if(!hasSignalledStop()) {
270 // Of course, just because the process is finished doesn't
271 // mean the incoming streams are empty. Unfortunately I've
272 // got no chance of preserving order, so I'll process the
273 // error stream first, then the out stream
274 while(eisr.ready()) {
275 eline_buffer = get_stream_char(eisr,eline_buffer,bos);
276 }
277
278 while(stdisr.ready()) {
279 stdline_buffer = get_stream_char(stdisr,stdline_buffer,bos);
280 }
281
282 // Ensure that any messages still remaining in the string buffers are fired off.
283 if(eline_buffer.length() > 0) {
284 String eline = eline_buffer.toString();
285 //DebugStream.println("Last bit of eline: " + eline);
286 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
287 eline = null;
288 }
289
290 if(stdline_buffer.length() > 0) {
291 String stdline = stdline_buffer.toString();
292 //DebugStream.println("Last bit of stdline: " + stdline);
293 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
294 stdline = null;
295 }
296 }
297 else {
298 DebugStream.println("We've been asked to stop.");
299 }
300
301
302 if(!hasSignalledStop()) {
303 // Now display final message based on exit value
304
305 prcs.waitFor();
306
307 if(prcs.exitValue() == 0) {
308 status = OK;
309 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Success"), status, null);
310 }
311 else {
312 status = ERROR;
313 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Failure"), status, null);
314 }
315
316 eisr.close();
317 stdisr.close();
318 }
319 else {
320 // I need to somehow kill the child process. Unfortunately
321 // Thread.stop() and Process.destroy() both fail to do
322 // this. But now, thankx to the magic of Michaels 'close the
323 // stream suggestion', it works fine (no it doesn't!)
324 prcs.getInputStream().close();
325 prcs.getErrorStream().close();
326 prcs.getOutputStream().close();
327 prcs.destroy();
328 status = CANCELLED;
329 }
330 }
331 // Exception
332 catch (Exception exception) {
333 DebugStream.println("Exception in GShell.runLocal() - unexpected");
334 DebugStream.printStackTrace(exception);
335 status = ERROR;
336 }
337 }
338
339
340
341 /** Any threaded class must include this method to allow the thread body to be run. */
342 public void run() {
343 String col_name = args[args.length-1];
344
345 // Determine if the user has asked for an outfile.
346 String out_name = null;
347 BufferedOutputStream bos = null;
348 if(type == IMPORT || type == BUILD) {
349 if(type == IMPORT) {
350 out_name = (String) Gatherer.c_man.getCollection().import_options.getValue("out");
351 }
352 else {
353 out_name = (String) Gatherer.c_man.getCollection().build_options.getValue("out");
354 }
355 if(out_name != null && out_name.length() > 0) {
356 try {
357 bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
358 }
359 catch (Exception error) {
360 DebugStream.printStackTrace(error);
361 }
362 }
363 }
364
365 // Issue a processBegun event
366 //ystem.err.println("\nFiring process begun for " + type + "...");
367 fireProcessBegun(type, status);
368 //ystem.err.println("Done process begun.");
369 if (Gatherer.isGsdlRemote) {
370 runRemote(args,bos);
371 }
372 else {
373 runLocal(args,bos);
374 }
375 //ystem.err.println("Done runLocal().");
376
377 if(status == OK) {
378 if (type == NEW) {
379 if (Gatherer.isGsdlRemote) {
380 RemoteGreenstoneServer.downloadCollection(col_name);
381 }
382 }
383 else if(type == IMPORT) {
384
385 // download the archives directory (if gsdl server is remote)
386 if (Gatherer.isGsdlRemote) {
387
388 if (progress!=null) {
389 progress.messageOnProgressBar("Downloading archive data from server");
390 }
391
392 RemoteGreenstoneServer.downloadCollectionArchives(col_name);
393
394 if (progress!=null) {
395 progress.messageOnProgressBar("");
396 }
397 }
398
399 // Refresh the DocXMLFileManager
400 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
401 DocXMLFileManager.clearDocXMLFiles();
402 DocXMLFileManager.loadDocXMLFiles(new File(CollectionManager.getLoadedCollectionArchivesDirectoryPath()));
403 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
404 }
405
406 else if(type == BUILD) {
407 // download the building directory (if gsdl server is remote)
408 if ((Gatherer.isGsdlRemote) && (!Configuration.fedora_info.isActive())) {
409 if (progress!=null) {
410 progress.messageOnProgressBar("Downloading index data from server");
411 }
412
413 if (!Gatherer.GS3){
414 // Only need to download build.cfg file
415 File build_cfg_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "build.cfg");
416 RemoteGreenstoneServer.downloadCollectionFile(col_name, build_cfg_file);
417 }else{
418 // Only need to download buildConfig.xml file
419 File buildConfig_xml_file = new File(CollectionManager.getLoadedCollectionBuildingDirectoryPath(), "buildConfig.xml");
420 RemoteGreenstoneServer.downloadCollectionFile(col_name, buildConfig_xml_file);
421 }
422
423 if (progress!=null) {
424 progress.messageOnProgressBar("");
425 }
426 }
427 }
428 else if(type == CDIMAGE) {
429 // download exported files from tmp folder (if gsdl server is remote)
430 if (Gatherer.isGsdlRemote) {
431 if (progress!=null) {
432 progress.messageOnProgressBar("Downloading CD-ROM data from server");
433 }
434
435 // !! TO DO
436
437 if (progress!=null) {
438 progress.messageOnProgressBar("");
439 }
440 }
441 }
442 }
443
444 // We're done.
445 //ystem.err.println("Firing process complete for " + type + "...");
446 fireProcessComplete(type, status);
447 // Close bos
448 if(bos != null) {
449 try {
450 bos.close();
451 bos = null;
452 }
453 catch(Exception error) {
454 DebugStream.printStackTrace(error);
455 }
456 }
457 }
458
459
460 public void fireMessage(String message)
461 {
462 fireMessage(type, typeAsString(type) + "> " + message, status, buffered_output_stream);
463 }
464
465
466 /** Method for firing a message to all interested listeners.
467 * @param type An <strong>int</strong> indicating the process type.
468 * @param message The message as a <strong>String</strong>.
469 * @param status An <strong>int</strong> specifying the current status of the process.
470 */
471 public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
472 GShellEvent event = new GShellEvent(this, 0, type, message, status);
473 // 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.
474 ArrayList message_queue = new ArrayList();
475 message_queue.add(event);
476 if(progress != null) {
477 progress.process(message_queue);
478 }
479 for(int j = 0; j < message_queue.size(); j++) {
480 GShellEvent current_event = (GShellEvent) message_queue.get(j);
481 // If the event hasn't been vetoed, pass it on to other listeners
482 if(!current_event.isVetoed()) {
483 Object[] concerned = listeners.getListenerList();
484 for(int i = 0; i < concerned.length ; i++) {
485 if(concerned[i] == GShellListener.class) {
486 ((GShellListener)concerned[i+1]).message(current_event);
487 }
488 }
489 concerned = null;
490 }
491 }
492 // And if we have a buffered output stream from error messages, send the message there
493 if(bos != null) {
494 try {
495 bos.write(message.getBytes(), 0, message.length());
496 }
497 catch(Exception exception) {
498 DebugStream.println("Exception in GShell.fireMessage() - unexpected");
499 DebugStream.printStackTrace(exception);
500 }
501 }
502 message_queue = null;
503 event = null;
504 }
505
506 /** Method for firing a process begun event which is called, strangly enough, when the process begins.
507 * @param type An <strong>int</strong> indicating the process type.
508 * @param status An <strong>int</strong> specifying the current status of the process.
509 */
510 protected void fireProcessBegun(int type, int status) {
511 // Start the progres monitor if available
512 if(progress != null) {
513 //ystem.err.println("About to call progress.start().");
514 progress.start();
515 //ystem.err.println("Called progress.start().");
516 }
517 // Fire an event
518 GShellEvent event = new GShellEvent(this, 0, type, "", status);
519 Object[] concerned = listeners.getListenerList();
520 for(int i = 0; i < concerned.length ; i++) {
521 if(concerned[i] == GShellListener.class) {
522 ((GShellListener)concerned[i+1]).processBegun(event);
523 }
524 }
525 }
526
527
528 /** Method for firing a process complete event which is called, no surprise here, when the process ends.
529 * @param type An <strong>int</strong> indicating the process type.
530 * @param status An <strong>int</strong> specifying the current status of the process.
531 */
532 protected void fireProcessComplete(int type, int status) {
533 // 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).
534 if(progress != null && status != CANCELLED) {
535 progress.stop();
536 }
537
538 // If we were cancelled, and we are lower details modes, fire off one last message.
539 if(status == CANCELLED && Configuration.getMode() <= Configuration.SYSTEMS_MODE) {
540 GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
541 Object[] concerned = listeners.getListenerList();
542 for(int i = 0; i < concerned.length ; i++) {
543 if(concerned[i] == GShellListener.class) {
544 ((GShellListener)concerned[i+1]).message(current_event);
545 }
546 }
547 concerned = null;
548 }
549
550 String msg = "";
551 // If we are creating collection and have trouble with permissions, we need more messages
552 if(status == ERROR && type == GShell.NEW){
553 msg = args[args.length-1];
554 }
555 // And firing off an event
556 GShellEvent event = new GShellEvent(this, 0, type, msg, status);
557 Object[] concerned = listeners.getListenerList();
558 for(int i = 0; i < concerned.length ; i++) {
559 if(concerned[i] == GShellListener.class) {
560 ((GShellListener)concerned[i+1]).processComplete(event);
561 }
562 }
563 }
564
565 /** Method to determine if the user, via the progress monitor, has signalled stop.
566 * @return A <strong>boolean</strong> indicating if the user wanted to stop.
567 */
568 public boolean hasSignalledStop() {
569 boolean has_signalled_stop = false;
570 if(progress != null) {
571 has_signalled_stop = progress.hasSignalledStop();
572 }
573 if(has_signalled_stop) {
574 status = CANCELLED;
575 }
576 return has_signalled_stop;
577 }
578
579 /** Converts a type into a text representation.
580 * @param type An <strong>int</strong> which maps to a shell process type.
581 * @return A <strong>String</strong> which is the thread process's text name.
582 */
583 public String typeAsString(int type) {
584 String name = null;
585 switch(type) {
586 case BUILD:
587 name = "buildcol.pl";
588 break;
589 case IMPORT:
590 name = "import.pl";
591 break;
592 case NEW:
593 name = "mkcol.pl";
594 break;
595 case EXPORTAS:
596 name = "export.pl";
597 break;
598 case CDIMAGE:
599 name = "exportcol.pl";
600 break;
601 case CONVERT:
602 name = "convert_coll_from_gs2.pl";
603 break;
604 case EXPLODE:
605 name = "explode_metadata_database.pl";
606 break;
607 case SRCREPLACE: // To work with replace_srcdoc_with_html.pl
608 name = "replace_srcdoc_with_html.pl";
609 break;
610 default:
611 name = "";
612 }
613 return name;
614 }
615}
Note: See TracBrowser for help on using the repository browser.