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

Last change on this file since 7102 was 7102, checked in by kjdon, 20 years ago

now reads in output and error streams in UTF-8

  • Property svn:keywords set to Author Date Id Revision
File size: 17.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.util.ArrayList;
41import javax.swing.*;
42import javax.swing.event.*;
43import javax.swing.tree.*;
44import org.greenstone.gatherer.Configuration;
45import org.greenstone.gatherer.Dictionary;
46import org.greenstone.gatherer.Gatherer;
47import org.greenstone.gatherer.msm.GreenstoneArchiveParser;
48import org.greenstone.gatherer.shell.GShellListener;
49import org.greenstone.gatherer.shell.GShellProgressMonitor;
50
51/** 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.
52 */
53public class GShell
54 extends Thread {
55 /** A flag used to determine if this process has been asked to cancel. */
56 private boolean cancel = false;
57 /** The list of listeners associated with this class. */
58 private EventListenerList listeners = null;
59 /** The current status of this shell process. */
60 private int status = -1;
61 /** The type of message being sent. */
62 private int msg_type = -1;
63 /** The type of shell process. */
64 private int type = -1;
65 /** The caller of this process, and thus the class most interested in messages. */
66 private GShellListener caller = null;
67 /** The progress monitor associated with this process. */
68 private GShellProgressMonitor progress = null;
69 /** Arguments to be given to the process (including the executable you are calling. */
70 private String args[] = null;
71 /** Element in process type enumeration. */
72 static final public int BUILD = 0;
73 /** Element in process type enumeration. */
74 static final public int IMPORT = 1;
75 /** Element in process type enumeration. */
76 static final public int NEW = 2;
77 /** Element in process type enumeration. */
78 static final public int EXPORT = 3;
79 /** Element in process type enumeration. */
80 static final public int OTHER = 4;
81 /** Element in status type enumeration. */
82 static final public int ERROR = 0;
83 /** Element in status type enumeration. */
84 static final public int OK = 1;
85 static final public int CANCELLED = 2;
86 /** Element in process type name enumeration. */
87 static public String GSHELL_BUILD = "gshell_build";
88 /** Element in process type name enumeration. */
89 static public String GSHELL_IMPORT = "gshell_import";
90 /** Element in process type name enumeration. */
91 static public String GSHELL_NEW = "gshell_new";
92 /** Element in process type name enumeration */
93 static public String GSHELL_EXPORT = "gshell_export";
94
95 /** 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
96 * @param process the Process to test
97 * @return true if it is still executing, false otherwise
98 */
99 static public boolean processRunning(Process process) {
100 boolean process_running = false;
101 try {
102 process.exitValue(); // This will throw an exception if the process hasn't ended yet.
103 }
104 catch(IllegalThreadStateException itse) {
105 process_running = true;
106 }
107 catch(Exception exception) {
108 Gatherer.printStackTrace(exception);
109 }
110 return process_running;
111 }
112
113 /** Constructor gatherer all the data required to create a new process, and emit meaningfull messages.
114 * @param args A <strong>String[]</strong> containing the arguments to the process thread, including the name of the executable.
115 * @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).
116 * @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>.
117 * @param caller The default <i>GShellListener</i> that is interested in the progress of this process.
118 * @param progress The <i>GShellProgressMonitor</i> associated with this process.
119 * @param name A <strong>String</strong> identifier given to the process, for convience and debug reasons.
120 */
121 public GShell(String args[], int type, int msg_type, GShellListener caller, GShellProgressMonitor progress, String name) {
122 super(name);
123 this.args = args;
124 this.msg_type = msg_type;
125 this.type = type;
126 this.caller = caller;
127 this.progress = progress;
128 this.status = 0;
129 // Lower this jobs priority
130 this.setPriority(Thread.MIN_PRIORITY);
131 listeners = new EventListenerList();
132 listeners.add(GShellListener.class, caller);
133 }
134 /** This method adds another shell listener to this process.
135 * @param listener The new <i>GShellListener</i>.
136 */
137 public void addGShellListener(GShellListener listener) {
138 listeners.add(GShellListener.class, listener);
139 }
140 /** This method removes a certain shell listener from this process.
141 * @param listener The <i>GShellListener</i> to be removed.
142 */
143 /* private void removeGShellListener(GShellListener listener) {
144 listeners.remove(GShellListener.class, listener);
145 } */
146
147 /** Any threaded class must include this method to allow the thread body to be run. */
148 public void run() {
149 // Determine if the user has asked for an outfile.
150 String out_name = null;
151 BufferedOutputStream bos = null;
152 if(type == IMPORT || type == BUILD) {
153 if(type == IMPORT) {
154 out_name = (String) Gatherer.c_man.getCollection().build_options.getImportValue("out");
155 }
156 else {
157 out_name = (String) Gatherer.c_man.getCollection().build_options.getBuildValue("out");
158 }
159 if(out_name != null && out_name.length() > 0) {
160 try {
161 bos = new BufferedOutputStream(new FileOutputStream(new File(out_name), true));
162 }
163 catch (Exception error) {
164 Gatherer.printStackTrace(error);
165 }
166 }
167 }
168 // Issue a processBegun event
169 fireProcessBegun(type, status);
170 try {
171 String command = "";
172 for(int i = 0; i < args.length; i++) {
173 command = command + args[i] + " ";
174 }
175 ///ystem.err.println("Command: " + command);
176 fireMessage(type, Dictionary.get("GShell.Command") + ": " + command, status, null);
177
178 Runtime rt = Runtime.getRuntime();
179 Process prcs = rt.exec(args);
180 InputStreamReader eisr = new InputStreamReader( prcs.getErrorStream(), "UTF-8" );
181 InputStreamReader stdisr = new InputStreamReader( prcs.getInputStream(), "UTF-8" );
182 //BufferedReader ebr = new BufferedReader( eisr );
183 //BufferedReader stdinbr = new BufferedReader( stdinisr );
184 // Captures the std err of a program and pipes it into std in of java
185
186 StringBuffer eline_buffer = new StringBuffer();
187 StringBuffer stdline_buffer = new StringBuffer();
188 while(type != GShell.NEW && processRunning(prcs) && !hasSignalledStop()) {
189 // Hopefully this doesn't block if the process is trying to write to STDOUT.
190 if(eisr.ready()) {
191 int c = eisr.read();
192 ///atherer.println("eisr: '" + (char) c + "'");
193 if(c == '\n' || c == '\r') {
194 if(eline_buffer.length() > 0) {
195 String eline = eline_buffer.toString();
196 ///atherer.println("* " + eline + " *");
197 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
198 eline = null;
199 eline_buffer = new StringBuffer();
200 }
201 }
202 else {
203 eline_buffer.append((char)c);
204 }
205 }
206 // Hopefully this won't block if the process is trying to write to STDERR
207 else if(stdisr.ready()) {
208 int c = stdisr.read();
209 ///atherer.println("eisr: '" + (char) c + "'");
210 if(c == '\n' || c == '\r') {
211 if(stdline_buffer.length() > 0) {
212 String stdline = stdline_buffer.toString();
213 ///atherer.println("+ " + stdline + " +");
214 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
215 stdline = null;
216 stdline_buffer = new StringBuffer();
217 }
218 }
219 else {
220 stdline_buffer.append((char)c);
221 }
222 }
223 else {
224 try {
225 sleep(100);
226 }
227 catch(Exception exception) {
228 }
229 }
230 }
231 if(!hasSignalledStop()) {
232 // Of course, just because the process is finished doesn't mean the incoming streams are empty. Unfortunately I've got no chance of preserving order, so I'll process the error stream first, then the out stream
233 while(eisr.ready()) {
234 int c = eisr.read();
235 ///atherer.println("eisr: '" + (char) c + "'");
236 if(c == '\n' || c == '\r') {
237 if(eline_buffer.length() > 0) {
238 String eline = eline_buffer.toString();
239 ///atherer.println("* " + eline + " *");
240 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
241 eline = null;
242 eline_buffer = new StringBuffer();
243 }
244 }
245 else {
246 eline_buffer.append((char)c);
247 }
248 }
249 while(stdisr.ready()) {
250 int c = stdisr.read();
251 ///atherer.println("eisr: '" + (char) c + "'");
252 if(c == '\n' || c == '\r') {
253 if(stdline_buffer.length() > 0) {
254 String stdline = stdline_buffer.toString();
255 ///atherer.println("+ " + stdline + " +");
256 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
257 stdline = null;
258 stdline_buffer = new StringBuffer();
259 }
260 }
261 else {
262 stdline_buffer.append((char)c);
263 }
264 }
265
266 // Ensure that any messages still remaining in the string buffers are fired off.
267 if(eline_buffer.length() > 0) {
268 String eline = eline_buffer.toString();
269 ///atherer.println("Last bit of eline: " + eline);
270 fireMessage(type, typeAsString(type) + "> " + eline, status, bos);
271 eline = null;
272 }
273
274 if(stdline_buffer.length() > 0) {
275 String stdline = stdline_buffer.toString();
276 ///atherer.println("Last bit of stdline: " + stdline);
277 fireMessage(type, typeAsString(type) + "> " + stdline, status, null);
278 stdline = null;
279 }
280 }
281 else {
282 System.err.println("We've been asked to stop.");
283 }
284
285 if(!hasSignalledStop()) {
286 // Now display final message based on exit value
287 prcs.waitFor();
288
289 if(prcs.exitValue() == 0) {
290 status = OK;
291 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Success"), status, null);
292 }
293 else {
294 status = ERROR;
295 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Failure"), status, null);
296 }
297 }
298 else {
299 // I need to somehow kill the child process. Unfortunately Thread.stop() and Process.destroy() both fail to do this. But now, thankx to the magic of Michaels 'close the stream suggestion', it works fine (no it doesn't!)
300 prcs.getInputStream().close();
301 prcs.getErrorStream().close();
302 prcs.getOutputStream().close();
303 prcs.destroy();
304 status = CANCELLED;
305 }
306 }
307 // Exception
308 catch (Exception exception) {
309 Gatherer.println("Exception in GShell.run() - unexpected");
310 Gatherer.printStackTrace(exception);
311 status = ERROR;
312 }
313 // If no error occured, and this was an import process we now extract any new metadata from the archive directory.
314 if(status == OK && type == IMPORT) {
315 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Start"), status, null);
316 new GreenstoneArchiveParser(progress, this);
317 fireMessage(type, typeAsString(type) + "> " + Dictionary.get("GShell.Parsing_Metadata_Complete"), status, null);
318 }
319 if(hasSignalledStop()) {
320 status = CANCELLED;
321 }
322 // We're done.
323 fireProcessComplete(type, status);
324 // Close bos
325 if(bos != null) {
326 try {
327 bos.close();
328 bos = null;
329 }
330 catch(Exception error) {
331 Gatherer.printStackTrace(error);
332 }
333 }
334 }
335 /** Method for firing a message to all interested listeners.
336 * @param type An <strong>int</strong> indicating the process type.
337 * @param message The message as a <strong>String</strong>.
338 * @param status An <strong>int</strong> specifying the current status of the process.
339 */
340 public void fireMessage(int type, String message, int status, BufferedOutputStream bos) {
341 GShellEvent event = new GShellEvent(this, 0, type, message, status);
342 // 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.
343 ArrayList message_queue = new ArrayList();
344 message_queue.add(event);
345 if(progress != null) {
346 progress.process(message_queue);
347 }
348 for(int j = 0; j < message_queue.size(); j++) {
349 GShellEvent current_event = (GShellEvent) message_queue.get(j);
350 // If the event hasn't been vetoed, pass it on to other listeners
351 if(!current_event.isVetoed()) {
352 Object[] concerned = listeners.getListenerList();
353 for(int i = 0; i < concerned.length ; i++) {
354 if(concerned[i] == GShellListener.class) {
355 ((GShellListener)concerned[i+1]).message(current_event);
356 }
357 }
358 concerned = null;
359 }
360 }
361 // And if we have a buffered output stream from error messages, send the message there
362 if(bos != null) {
363 try {
364 bos.write(message.getBytes(), 0, message.length());
365 }
366 catch(Exception exception) {
367 Gatherer.println("Exception in GShell.fireMessage() - unexpected");
368 Gatherer.printStackTrace(exception);
369 }
370 }
371 message_queue = null;
372 event = null;
373 }
374
375 /** Method for firing a process begun event which is called, strangly enough, when the process begins.
376 * @param type An <strong>int</strong> indicating the process type.
377 * @param status An <strong>int</strong> specifying the current status of the process.
378 */
379 protected void fireProcessBegun(int type, int status) {
380 // Start the progres monitor if available
381 if(progress != null) {
382 progress.start();
383 }
384 // Fire an event
385 GShellEvent event = new GShellEvent(this, 0, type, "", status);
386 Object[] concerned = listeners.getListenerList();
387 for(int i = 0; i < concerned.length ; i++) {
388 if(concerned[i] == GShellListener.class) {
389 ((GShellListener)concerned[i+1]).processBegun(event);
390 }
391 }
392 }
393 /** Method for firing a process complete event which is called, no surprise here, when the process ends.
394 * @param type An <strong>int</strong> indicating the process type.
395 * @param status An <strong>int</strong> specifying the current status of the process.
396 */
397 protected void fireProcessComplete(int type, int status) {
398 // 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).
399 if(progress != null && status != CANCELLED) {
400 progress.stop();
401 }
402
403 // If we were cancelled, and we are lower details modes, fire off one last message.
404 if(status == CANCELLED && Gatherer.config.getMode() <= Configuration.SYSTEMS_MODE) {
405 GShellEvent current_event = new GShellEvent(this, 0, type, Dictionary.get("GShell.Build.BuildCancelled"), status);
406 Object[] concerned = listeners.getListenerList();
407 for(int i = 0; i < concerned.length ; i++) {
408 if(concerned[i] == GShellListener.class) {
409 ((GShellListener)concerned[i+1]).message(current_event);
410 }
411 }
412 concerned = null;
413 }
414 // And firing off an event
415 GShellEvent event = new GShellEvent(this, 0, type, "", status);
416 Object[] concerned = listeners.getListenerList();
417 for(int i = 0; i < concerned.length ; i++) {
418 if(concerned[i] == GShellListener.class) {
419 ((GShellListener)concerned[i+1]).processComplete(event);
420 }
421 }
422 }
423
424 /** Method to determine if the user, via the progress monitor, has signalled stop.
425 * @return A <strong>boolean</strong> indicating if the user wanted to stop.
426 */
427 private boolean hasSignalledStop() {
428 boolean has_signalled_stop = false;
429 if(progress != null) {
430 has_signalled_stop = progress.hasSignalledStop();
431 }
432 return has_signalled_stop;
433 }
434
435 /** Converts a type into a text representation.
436 * @param type An <strong>int</strong> which maps to a shell process type.
437 * @return A <strong>String</strong> which is the thread process's text name.
438 */
439 public String typeAsString(int type) {
440 String name = null;
441 switch(type) {
442 case BUILD:
443 name = Dictionary.get("GShell.Build");
444 break;
445 case IMPORT:
446 name = Dictionary.get("GShell.Import");
447 break;
448 case NEW:
449 name = Dictionary.get("GShell.New");
450 break;
451 case EXPORT:
452 name= Dictionary.get("GShell.Export");
453 break;
454 default:
455 name = Dictionary.get("GShell.Other");
456 }
457 return name;
458 }
459}
Note: See TracBrowser for help on using the repository browser.