source: main/trunk/greenstone3/src/java/org/greenstone/gsdl3/build/GS2PerlConstructor.java@ 36794

Last change on this file since 36794 was 36794, checked in by anupama, 19 months ago

Dr Bainbridge fixed a diffcol classifier's subsidiary documents ordering issue: for identical titles under an authorr bookshelf for AZCompactList classifier on Creators in Word-PDF-Basic tutorial model collection, a different order of the documents would appear each time. The solution was 2-fold: Besides the PERL_PERTURB_KEYS environment variable, which we set to 0, there is also the PERL_HASH_SEED (see https://www.perlmonks.org/?node_id=1167787 ), and they both need to be set to 0 to get consistent ordering when calling perl's 'keys' command on a hashmap. The other part of the solution is to initialise AZCompactList's sort property to 'nosort' which then uses an array (thus, having a sense of ordering) instead of AZCompactList's default behaviour of using a hashmap (which does not enforce a sense of ordering). Setting the sort property to nosort had the effect of a consistent order of the same identically Titled documents upon a single build, but no consistent ordering between builds which is what PERL_PERTURB_KEYS in conjunction with PERL_HASH_SEED ensure.

  • Property svn:keywords set to Author Date Id Revision
File size: 30.9 KB
Line 
1package org.greenstone.gsdl3.build;
2
3// greenstome classes
4import org.greenstone.gsdl3.util.*;
5import org.greenstone.util.Misc;
6import org.greenstone.util.GlobalProperties;
7import org.greenstone.util.SafeProcess;
8
9// xml classes
10import org.w3c.dom.Element;
11import org.w3c.dom.NodeList;
12
13//general java classes
14import java.io.BufferedReader;
15import java.io.BufferedWriter;
16import java.io.Closeable;
17import java.io.FileWriter;
18import java.io.InputStreamReader;
19import java.io.File;
20import java.io.InputStream;
21import java.io.IOException;
22import java.util.ArrayList;
23import java.util.Vector;
24
25
26import org.apache.log4j.*;
27
28/**
29 * CollectionConstructor class for greenstone 2 compatible building it uses the
30 * perl scripts to do the building stuff
31 */
32public class GS2PerlConstructor extends CollectionConstructor implements SafeProcess.ExceptionHandler
33{
34 static Logger logger = Logger.getLogger(org.greenstone.gsdl3.build.GS2PerlConstructor.class.getName());
35
36 public static final int NEW = 0;
37 public static final int IMPORT = 1;
38 public static final int BUILD = 2;
39 public static final int ACTIVATE = 3;
40 public static final int MODIFY_METADATA_SERVER = 4;
41
42 /**
43 * gsdlhome for greenstone 2 - we use the perl modules and building scripts
44 * from there
45 */
46 protected String gsdl2home = null;
47 /** gsdlhome for gsdl3 - shouldn't need this eventually ?? */
48 protected String gsdl3home = null;
49 /** gsdlos for greenstone 2 */
50 protected String gsdlos = null;
51 /** the path environment variable */
52 protected String path = null;
53
54
55 public GS2PerlConstructor(String name)
56 {
57 super(name);
58 }
59
60 /** retrieves the necessary environment variables */
61 public boolean configure()
62 {
63 // try to get the environment variables
64 this.gsdl3home = GlobalProperties.getGSDL3Home();
65 this.gsdl2home = this.gsdl3home + File.separator + ".." + File.separator + "gs2build";
66 this.gsdlos = Misc.getGsdlOS();
67
68 this.path = System.getenv("PATH");
69
70 if (this.gsdl2home == null)
71 {
72 System.err.println("You must have gs2build installed, and GSDLHOME set for GS2Perl building to work!!");
73 return false;
74 }
75 if (this.gsdl3home == null || this.gsdlos == null)
76 {
77 System.err.println("You must have GSDL3HOME and GSDLOS set for GS2Perl building to work!!");
78 return false;
79 }
80 if (this.path == null)
81 {
82 System.err.println("You must have the PATH set for GS2Perl building to work!!");
83 return false;
84 }
85 return true;
86 }
87
88 public void run()
89 {
90 String msg;
91 ConstructionEvent evt;
92 if (this.process_type == -1)
93 {
94 msg = "Error: you must set the action type";
95 evt = new ConstructionEvent(this, GSStatus.ERROR, msg);
96 sendMessage(evt);
97 return;
98 }
99 if (this.site_home == null)
100 {
101 msg = "Error: you must set site_home";
102 evt = new ConstructionEvent(this, GSStatus.ERROR, msg);
103 sendMessage(evt);
104 return;
105 }
106 if (this.process_type != NEW && this.collection_name == null)
107 {
108 msg = "Error: you must set collection_name";
109 evt = new ConstructionEvent(this, GSStatus.ERROR, msg);
110 sendMessage(evt);
111 return;
112 }
113
114 switch (this.process_type)
115 {
116 case NEW:
117 newCollection();
118 break;
119 case IMPORT:
120 importCollection();
121 break;
122 case BUILD:
123 buildCollection();
124 break;
125 case ACTIVATE:
126 activateCollection();
127 break;
128 case MODIFY_METADATA_SERVER:
129 modifyMetadataForCollection();
130 break;
131 default:
132 msg = "wrong type of action specified!";
133 evt = new ConstructionEvent(this, GSStatus.ERROR, msg);
134 sendMessage(evt);
135 break;
136 }
137 }
138
139 protected void newCollection()
140 {
141 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "Collection construction: new collection."));
142 Vector<String> command = new Vector<String>();
143 command.add("gs2_mkcol.pl");
144 command.add("-site");
145 command.add(this.site_home);
146 command.add("-collectdir");
147 command.add(GSFile.collectDir(this.site_home));
148 command.addAll(extractParameters(this.process_params));
149 command.add(this.collection_name);
150 String[] command_str = {};
151 command_str = command.toArray(command_str);
152 if (runPerlCommand(command_str))
153 {
154 // success!! - need to send the final completed message
155 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
156 } // else an error message has already been sent, do nothing
157
158 }
159
160 protected void importCollection()
161 {
162 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "Collection construction: import collection."));
163 Vector<String> command = new Vector<String>();
164
165 String perlPath = GlobalProperties.getProperty("perl.path", "perl");
166 if (perlPath.charAt(perlPath.length() - 1) != File.separatorChar)
167 {
168 perlPath = perlPath + File.separator;
169 }
170
171 command.add(perlPath + "perl");
172 command.add("-S");
173 command.add(GlobalProperties.getGS2Build() + File.separator + "bin" + File.separator + "script" + File.separator + "import.pl");
174 if (this.manifest_file != null)
175 {
176 command.add("-keepold");
177 command.add("-manifest");
178 command.add(this.manifest_file);
179 }
180 command.add("-site");
181 command.add(this.site_name);
182 command.add("-collectdir");
183 command.add(GSFile.collectDir(this.site_home));
184 command.addAll(extractParameters(this.process_params));
185 command.add(this.collection_name);
186 String[] command_str = {};
187 command_str = command.toArray(command_str);
188
189 if (runPerlCommand(command_str))
190 {
191 // success!! - need to send the final completed message
192 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
193 } // else an error message has already been sent, do nothing
194 }
195
196 protected void buildCollection()
197 {
198 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "Collection construction: build collection."));
199 Vector<String> command = new Vector<String>();
200
201 String perlPath = GlobalProperties.getProperty("perl.path", "perl");
202 if (perlPath.charAt(perlPath.length() - 1) != File.separatorChar)
203 {
204 perlPath = perlPath + File.separator;
205 }
206
207 command.add(perlPath + "perl");
208 command.add("-S");
209 command.add(GlobalProperties.getGS2Build() + File.separator + "bin" + File.separator + "script" + File.separator + "incremental-buildcol.pl");
210 command.add("-incremental");
211 command.add("-builddir");
212 command.add(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator +"index");
213 command.add("-site");
214 command.add(this.site_name);
215 command.add("-collectdir");
216 command.add(GSFile.collectDir(this.site_home));
217 command.add("-library_name");
218 command.add(this.library_name);
219// command.add("-removeold"); // saves some seconds processing time when this flag's added in explicitly
220 command.addAll(extractParameters(this.process_params));
221 command.add(this.collection_name);
222
223 String[] command_str = {};
224 command_str = command.toArray(command_str);
225
226 if (runPerlCommand(command_str))
227 {
228 // success!! - need to send the final completed message
229 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
230 }// else an error message has already been sent, do nothing
231 }
232
233 protected void activateCollection()
234 {
235 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "Collection construction: activate collection."));
236
237 // first check that we have a building directory
238 // (don't want to bother running activate.pl otherwise)
239 File build_dir = new File(GSFile.collectionIndexDir(this.site_home, this.collection_name));
240 if (!build_dir.exists())
241 {
242 sendMessage(new ConstructionEvent(this, GSStatus.ERROR, "build dir doesn't exist!"));
243 return;
244 }
245
246 /*
247
248 // move building to index
249 File index_dir = new File(GSFile.collectionIndexDir(this.site_home, this.collection_name));
250 if (index_dir.exists())
251 {
252 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "deleting index directory"));
253 GSFile.deleteFile(index_dir);
254 if (index_dir.exists())
255 {
256 sendMessage(new ConstructionEvent(this, GSStatus.ERROR, "index directory still exists!"));
257 return;
258 }
259 }
260
261 GSFile.moveDirectory(build_dir, index_dir);
262 if (!index_dir.exists())
263 {
264 sendMessage(new ConstructionEvent(this, GSStatus.ERROR, "index dir wasn't created!"));
265 }
266
267 // success!! - need to send the final completed message
268 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
269 */
270
271 // Running activate.pl instead of making java move building to index as above
272 // circumvents the issue of the jdbm .lg log file (managed by TransactionManager)
273 // in index dir not getting deleted at times. The perl code is able to delete this
274 // sucessfully consistently during testing, whereas java at times is unable to delete it.
275 Vector<String> command = new Vector<String>();
276
277 String perlPath = GlobalProperties.getProperty("perl.path", "perl");
278 if (perlPath.charAt(perlPath.length() - 1) != File.separatorChar)
279 {
280 perlPath = perlPath + File.separator;
281 }
282
283 command.add(perlPath + "perl");
284 command.add("-S");
285 command.add(GlobalProperties.getGS2Build() + File.separator + "bin" + File.separator + "script" + File.separator + "activate.pl");
286 command.add("-incremental");
287 command.add("-builddir");
288 command.add(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator +"index");
289 command.add("-site");
290 command.add(this.site_name);
291 command.add("-collectdir");
292 command.add(GSFile.collectDir(this.site_home));
293// command.add("-removeold"); // saves some seconds processing time when this flag's added in explicitly. Shouldn't be added for incremental building
294 command.add("-skipactivation"); // gsdl3/service/GS2Construct does the activation and reactivation
295 command.addAll(extractParameters(this.process_params));
296 command.add(this.collection_name);
297
298 String[] command_str = {};
299 command_str = command.toArray(command_str);
300
301 if (runPerlCommand(command_str))
302 {
303 // success!! - need to send the final completed message
304 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
305 }// else an error message has already been sent, do nothing
306
307 }
308
309
310 protected void modifyMetadataForCollection()
311 {
312 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "Collection metadata: modifyMetadata (set or remove meta) for collection."));
313
314 Vector<String> command = new Vector<String>();
315
316 String perlPath = GlobalProperties.getProperty("perl.path", "perl");
317 if (perlPath.charAt(perlPath.length() - 1) != File.separatorChar)
318 {
319 perlPath = perlPath + File.separator;
320 }
321
322 String cgi_directory = GlobalProperties.getGSDL3Home() + File.separator + "WEB-INF" + File.separator + "cgi";
323 command.add(perlPath + "perl");
324 command.add("-S");
325 //command.add(GlobalProperties.getGSDL3Home() + File.separator + "WEB-INF" + File.separator + "cgi" + File.separator + "metadata-server.pl");
326 command.add(cgi_directory + File.separator + "metadata-server.pl");
327
328
329 // Need to set QUERY_STRING and REQUEST_METHOD=GET in environment
330 // Also set GS3_AUTHENTICATED, to allow running metadata-server.pl with mod (set and remove) commands
331 // http://www.cgi101.com/class/ch3/text.html
332
333 // And since we need to ensure that special characters won't get clobbered on Windows by perl/CGI.pm
334 // (see https://www.nntp.perl.org/group/perl.perl5.porters/2016/10/msg240120.html),
335 // each param *value* in QUERY_STRING, not QUERY_STRING in entirety, is further URL encoded,
336 // as at https://stackoverflow.com/questions/10786042/java-url-encoding-of-query-string-parameters
337 // This URL encoding of paramValues already happened in GS2Construct.java::runCommand(), so QUERY_STRING is ready now.
338
339 String[] envvars = {
340 "QUERY_STRING=" + this.query_string,
341 "REQUEST_METHOD=GET",
342 "GS3_AUTHENTICATED=true" // how do we set the env var without having to assign it a value?
343 };
344
345 String[] command_str = {};
346 command_str = command.toArray(command_str);
347
348 // http://www.cgi101.com/class/ch3/text.html
349 // setenv QUERY_STRING and REQUEST_METHOD = GET.
350 if (runPerlCommand(command_str, envvars, new File(cgi_directory)))
351 //new File(GlobalProperties.getGSDL3Home() + File.separator + "WEB-INF" + File.separator + "cgi")))
352 {
353 // success!! - need to send the final completed message
354 sendProcessComplete(new ConstructionEvent(this, GSStatus.COMPLETED, ""));
355 }// else an error message has already been sent, do nothing
356
357 }
358
359 /** extracts all the args from the xml and returns them in a Vector */
360 protected Vector<String> extractParameters(Element param_list)
361 {
362
363 Vector<String> args = new Vector<String>();
364 if (param_list == null)
365 {
366 return args; // return an empty vector
367 }
368 NodeList params = param_list.getElementsByTagName(GSXML.PARAM_ELEM);
369
370 for (int i = 0; i < params.getLength(); i++)
371 {
372 Element p = (Element) params.item(i);
373 String name = p.getAttribute(GSXML.NAME_ATT);
374 String value = p.getAttribute(GSXML.VALUE_ATT);
375 if (!name.equals(""))
376 {
377 args.add("-" + name);
378 if (!value.equals(""))
379 {
380 args.add(value);
381 }
382 }
383 }
384
385 return args;
386 }
387
388 protected SafeProcess createPerlProcess(String[] command, String[] envvars, File dir) {
389 int sepIndex = this.gsdl3home.lastIndexOf(File.separator);
390 String srcHome = this.gsdl3home.substring(0, sepIndex);
391
392 ArrayList<String> args = new ArrayList<String>();
393 args.add("GSDLHOME=" + this.gsdl2home);
394 args.add("GSDL3HOME=" + this.gsdl3home);
395 args.add("GSDL3SRCHOME=" + srcHome);
396 args.add("GSDLOS=" + this.gsdlos);
397 args.add("GSDL-RUN-SETUP=true");
398 args.add("PERL_PERTURB_KEYS=0");
399 args.add("PERL_HASH_SEED=0");
400
401 if(envvars != null) {
402 for(int i = 0; i < envvars.length; i++) {
403 args.add(envvars[i]);
404 }
405 }
406
407 for (String a : System.getenv().keySet()) {
408 args.add(a + "=" + System.getenv(a));
409 }
410
411 SafeProcess perlProcess
412 = new SafeProcess(command, args.toArray(new String[args.size()]), dir); // dir can be null
413
414 return perlProcess;
415 }
416
417 // If you want to run a Perl command without doing GS2PerlConstructor's custom logging in the build log
418 // Then use the runSimplePerlCommand() versions, which use the default behaviour of running a SafeProcess
419 protected boolean runSimplePerlCommand(String[] command) {
420 return runSimplePerlCommand(command, null, null);
421 }
422
423 protected boolean runSimplePerlCommand(String[] command, String[] envvars, File dir) {
424 boolean success = false;
425
426 String command_str = "";
427 for (int i = 0; i < command.length; i++) {
428 command_str = command_str + command[i] + " ";
429 }
430
431 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "command = " + command_str));
432
433 ///logger.info("### Running simple command = " + command_str);
434
435 // This is where we create and run our perl process safely
436 SafeProcess perlProcess = createPerlProcess(command, envvars, dir); // dir can be null
437
438 perlProcess.setExceptionHandler(this);
439
440 sendProcessBegun(new ConstructionEvent(this, GSStatus.ACCEPTED, "starting"));
441
442 int exitVal = perlProcess.runProcess(); // uses default processing of the perl process' iostreams provided by SafeProcess
443
444 if (exitVal == 0) {
445 success = true;
446 sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, "Success"));
447 } else {
448 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Failure"));
449 success = false; // explicit
450 }
451
452 return success;
453 }
454
455
456 /** returns true if completed correctly, false otherwise
457 * Building operations call runPerlCommand which sends build output to collect/log/build_log.#*.txt
458 */
459 protected boolean runPerlCommand(String[] command) {
460 return runPerlCommand(command, null, null);
461 }
462
463
464 protected boolean runPerlCommand(String[] command, String[] envvars, File dir)
465 {
466 boolean success = true;
467
468 String command_str = "";
469 for (int i = 0; i < command.length; i++) {
470 command_str = command_str + command[i] + " ";
471 }
472
473 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "command = " + command_str));
474
475 ///logger.info("### Running logged command = " + command_str);
476 /*
477 // USEFUL DEBUGGING WHEN USING DOC EDITOR TO MODIFY ex.Title META THAT CONTAINS NON-BASIC ASCII CHARS
478 logger.error("### Running logged command = " + command_str);
479 logger.error("### DEBUG Running logged command = " + Misc.debugUnicodeString(command_str));
480 if(envvars != null) {
481 for(int i = 0; i < envvars.length; i++) {
482 logger.error("### envvar = " + envvars[i]);
483 logger.error("### DEBUG envvar = " + Misc.debugUnicodeString(envvars[i]));
484 }
485 }
486 */
487
488 // This is where we create and run our perl process safely
489 SafeProcess perlProcess = createPerlProcess(command, envvars, dir); // dir can be null
490
491 sendProcessBegun(new ConstructionEvent(this, GSStatus.ACCEPTED, "starting"));
492
493 File logDir = new File(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator + "log");
494 if (!logDir.exists()) {
495 logDir.mkdir();
496 }
497
498 // Only from Java 7+: Try-with-Resources block will safely close the BufferedWriter
499 // https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
500 BufferedWriter bw = null;
501 try {
502
503 bw = new BufferedWriter(new FileWriter(new File(logDir, "build_log." + (System.currentTimeMillis()) + ".txt")));
504
505 bw.write("Document Editor Build \n");
506 bw.write("Command = " + command_str + "\n");
507
508 // handle each incoming line from stdout and stderr streams, and any exceptions that occur then
509 SafeProcess.CustomProcessHandler processOutHandler
510 = new SynchronizedProcessHandler(bw, SafeProcess.STDOUT);
511 SafeProcess.CustomProcessHandler processErrHandler
512 = new SynchronizedProcessHandler(bw, SafeProcess.STDERR);
513
514 // GS2PerlConstructor will do further handling of exceptions that may occur during the perl
515 // process (including if writing something to the process' inputstream, not that we're doing that for this perlProcess)
516 perlProcess.setExceptionHandler(this);
517
518 // finally, execute the process
519
520 // Captures the std err of a program and pipes it into
521 // std in of java, as before.
522
523 perlProcess.runProcess(null, processOutHandler, processErrHandler); // use default procIn handling
524
525 // The original runPerlCommand() code had an ineffective check for whether the cmd had been cancelled
526 // midway through executing the perl, as condition of a while loop reading from stderr and stdout.
527 // We don't include the cancel check here, as superclass CollectionConstructor.stopAction(), which set
528 // this.cancel to true, never got called anywhere.
529 // But a proper cancel of our perl process launched by this GS2PerlConstructor Thread object
530 // and of the worker threads it launches is now implemented in SafeProcess.cancelRunningProcess()
531 // with interrupts. See:
532 // http://stackoverflow.com/questions/6859681/better-way-to-signal-other-thread-to-stop
533 // https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html
534 // https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#interrupted()
535 // https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/
536 // If cancel is to be implemented for GS2PerlConstructor, then the code that calls
537 // GS2PerlConstructor.stopAction() should also call or result in a call to the SafeProcess instance's
538 // cancelRunningProcess() method.
539 // For a simple example of the use of SafeProcess' cancel feature, see GLI's GShell. For a more
540 // complicated example, see GLI's DownloadJob.java, which implements SafeProcess.MainHandler to do
541 // additional work when a process is cancelled while still running (and therefore has to be prematurely
542 // terminated) a.o.t. a process that's cancelled when it's already terminated naturally.
543
544 } catch(IOException e) {
545 e.printStackTrace();
546 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + e.toString()));
547 } finally {
548 SafeProcess.closeResource(bw);
549 }
550
551 if (!this.cancel) {
552 // Now display final message based on exit value
553
554 if (perlProcess.getExitValue() == 0) { //status = OK;
555 //logger.info("@@@@@@@@ runPerlCommand success - exitvalue = " + perlProcess.getExitValue());
556 sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, "Success"));
557 success = true;
558
559 } else { //status = ERROR;
560 logger.debug("@@@@@@@@ runPerlCommand failed, exitvalue = " + perlProcess.getExitValue());
561 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Failure"));
562 success = false;
563
564 }
565 } else { // cancelled. The code would never come here, including in the old version of runPerlCommand
566 // but leaving this here for an exact port of the old runPerlCommand to the new one which
567 // uses SafeProcess. Also to allow the cancel functionality in future
568
569 // 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.
570 sendProcessStatus(new ConstructionEvent(this, GSStatus.HALTED, "killing the process"));
571 success = false;
572 }
573 // we're done, but we don't send a process complete message here cos there might be stuff to do after this has finished.
574 //return true;
575 return success;
576 }
577
578
579 // this method is blocking again, on Windows when adding user comments
580 protected boolean old_runPerlCommand(String[] command, String[] envvars, File dir)
581 {
582 boolean success = true;
583
584 int sepIndex = this.gsdl3home.lastIndexOf(File.separator);
585 String srcHome = this.gsdl3home.substring(0, sepIndex);
586
587 ArrayList<String> args = new ArrayList<String>();
588 args.add("GSDLHOME=" + this.gsdl2home);
589 args.add("GSDL3HOME=" + this.gsdl3home);
590 args.add("GSDL3SRCHOME=" + srcHome);
591 args.add("GSDLOS=" + this.gsdlos);
592 args.add("GSDL-RUN-SETUP=true");
593 args.add("PERL_PERTURB_KEYS=0");
594
595 if(envvars != null) {
596 for(int i = 0; i < envvars.length; i++) {
597 args.add(envvars[i]);
598 }
599 }
600
601 for (String a : System.getenv().keySet())
602 {
603 args.add(a + "=" + System.getenv(a));
604 }
605
606 String command_str = "";
607 for (int i = 0; i < command.length; i++)
608 {
609 command_str = command_str + command[i] + " ";
610 }
611
612 // logger.info("### old runPerlCmd, command = " + command_str);
613
614 sendMessage(new ConstructionEvent(this, GSStatus.INFO, "command = " + command_str));
615 Process prcs = null;
616 BufferedReader ebr = null;
617 BufferedReader stdinbr = null;
618 try
619 {
620 Runtime rt = Runtime.getRuntime();
621 sendProcessBegun(new ConstructionEvent(this, GSStatus.ACCEPTED, "starting"));
622 prcs = (dir == null)
623 ? rt.exec(command, args.toArray(new String[args.size()]))
624 : rt.exec(command, args.toArray(new String[args.size()]), dir);
625
626 InputStreamReader eisr = new InputStreamReader(prcs.getErrorStream());
627 InputStreamReader stdinisr = new InputStreamReader(prcs.getInputStream());
628 ebr = new BufferedReader(eisr);
629 stdinbr = new BufferedReader(stdinisr);
630 // Captures the std err of a program and pipes it into
631 // std in of java
632
633 File logDir = new File(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator + "log");
634 if (!logDir.exists())
635 {
636 logDir.mkdir();
637 }
638
639 BufferedWriter bw = new BufferedWriter(new FileWriter(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator + "log" + File.separator + "build_log." + (System.currentTimeMillis()) + ".txt"));
640 bw.write("Document Editor Build \n");
641
642 bw.write("Command = " + command_str + "\n");
643
644 String eline = null;
645 String stdinline = null;
646 while (((eline = ebr.readLine()) != null || (stdinline = stdinbr.readLine()) != null) && !this.cancel)
647 {
648 if (eline != null)
649 {
650 //System.err.println("ERROR: " + eline);
651 bw.write(eline + "\n");
652 sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, eline));
653 }
654 if (stdinline != null)
655 {
656 //System.err.println("OUT: " + stdinline);
657 bw.write(stdinline + "\n");
658 sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, stdinline));
659 }
660 }
661 SafeProcess.closeResource(bw);
662
663 if (!this.cancel)
664 {
665 // Now display final message based on exit value
666 prcs.waitFor();
667
668 if (prcs.exitValue() == 0)
669 {
670 //status = OK;
671 sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, "Success"));
672
673 success = true;
674 }
675 else
676 {
677 //status = ERROR;
678 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Failure"));
679
680 //return false;
681 success = false;
682
683 }
684 }
685 else
686 {
687 // 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.
688 sendProcessStatus(new ConstructionEvent(this, GSStatus.HALTED, "killing the process"));
689 //prcs.getOutputStream().close();
690 //prcs.destroy();
691 ////status = ERROR;
692
693 //return false;
694 success = false;
695 }
696 }
697 catch (Exception e)
698 {
699 e.printStackTrace();
700 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + e.toString()));
701 } finally {
702 // http://steveliles.github.io/invoking_processes_from_java.html
703 // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
704 // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec
705
706 if( prcs != null ) {
707 SafeProcess.closeResource(prcs.getErrorStream());
708 SafeProcess.closeResource(prcs.getOutputStream());
709 SafeProcess.closeResource(prcs.getInputStream());
710 prcs.destroy();
711 }
712
713 SafeProcess.closeResource(ebr);
714 SafeProcess.closeResource(stdinbr);
715 }
716
717 // we're done, but we don't send a process complete message here cos there might be stuff to do after this has finished.
718 //return true;
719 return success;
720 }
721
722
723 // From interface SafeProcess.ExceptionHandler
724 // Called when an exception happens during the running of our perl process. However,
725 // exceptions when reading from our perl process' stderr and stdout streams are handled by
726 // SynchronizedProcessHandler.gotException() below, since they happen in separate threads
727 // from this one (the one from which the perl process is run). So this method's operations
728 // have been made thread-safe, rather than synchronizing on the method itself which locks
729 // *this* object and which isn't actually useful for what I want to do.
730 public void gotException(Exception e) {
731
732 // do what original runPerlCommand() code always did when an exception occurred
733 // when running the perl process:
734 e.printStackTrace();
735 sendProcessStatus(new ConstructionEvent(this,GSStatus.ERROR,
736 "Exception occurred " + e.toString()));
737 }
738
739 // Each instance of this class is run in its own thread by class SafeProcess.InputGobbler.
740 // This class deals with each incoming line from the perl process' stderr or stdout streams. One
741 // instance of this class for each stream. However, since multiple instances of this CustomProcessHandler
742 // could be (and in fact, are) writing to the same file in their own threads, several objects, not just
743 // the bufferedwriter object, needed to be made threadsafe.
744 // This class also handles exceptions during the running of the perl process.
745 // The runPerlCommand code originally would do a sendProcessStatus on each exception, so we ensure
746 // we do that here too, to continue original behaviour. These calls are also synchronized to make their
747 // use of the EventListeners threadsafe.
748 protected class SynchronizedProcessHandler extends SafeProcess.CustomProcessHandler
749 {
750 private final BufferedWriter bwHandle; // needs to be final to synchronize on the object
751
752 public SynchronizedProcessHandler(BufferedWriter bw, int src) {
753 super(src); // will set this.source to STDERR or STDOUT
754 this.bwHandle = bw; // caller will close bw, since many more than one
755 // SynchronizedProcessHandlers are using it
756 }
757
758 public void run(Closeable inputStream) {
759 InputStream is = (InputStream) inputStream;
760
761 BufferedReader br = null;
762 try {
763 br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
764 String line=null;
765 while ( (line = br.readLine()) != null ) {
766
767 // check if we got cancelled
768 if(Thread.currentThread().isInterrupted()) { // Did the SafeProcess thread interrupt us? Then break out of loop pre-maturely and finish up
769 System.err.println("Got interrupted when reading lines from process err/out stream.");
770 break; // will go to finally block
771 }
772
773 ///System.out.println("@@@ GOT LINE: " + line);
774
775
776 if(this.source == SafeProcess.STDERR) {
777 System.err.println("STDERR: " + line);
778 logger.info("@@@@@@@@ STDERR: " + line );
779 } else {
780 System.err.println("STDOUT: " + line);
781 logger.info("@@@@@@@@ STDOUT: " + line );
782 }
783
784 this.gotLine(line); // behaves threadsafe
785 }
786 } catch (IOException ioe) { // problem with reading in from process with BufferedReader br
787
788 String msg = (this.source == SafeProcess.STDERR) ? "stderr" : "stdout";
789 msg = "Got exception when processing the perl process' " + msg + " stream.";
790 GS2PerlConstructor.logger.error(msg, ioe);
791 // now do what the original runPerlCommand() code always did:
792 ioe.printStackTrace();
793 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + ioe.toString())); // now synchronized
794
795 } catch (Exception e) { // problem with BufferedWriter bwHandle on processing each line
796 e.printStackTrace();
797 sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + e.toString())); // now synchronized
798 } finally {
799 SafeProcess.closeResource(br);
800 }
801 }
802
803 // helper method, deals with things in a thread-safe manner
804 // When to use synchronized blocks vs methods
805 // http://stackoverflow.com/questions/574240/is-there-an-advantage-to-use-a-synchronized-method-instead-of-a-synchronized-blo
806 private void gotLine(String line) throws Exception {
807
808 // BufferedWriter writes may not be atomic
809 // http://stackoverflow.com/questions/9512433/is-writer-an-atomic-method
810 // Choosing to put try-catch outside of sync block, since it's okay to give up lock on exception
811 // http://stackoverflow.com/questions/14944551/it-is-better-to-have-a-synchronized-block-inside-a-try-block-or-a-try-block-insi
812 try {
813
814 // try to keep synchronized methods and synchronized blocks as short as possible
815 synchronized(bwHandle) { // get a lock on the writer handle, then write
816 bwHandle.write(line + "\n"); // can throw IOException
817 }
818
819/// GS2PerlConstructor.logger.info("@@@ WROTE LINE: " + line);
820
821 // this next method is thread safe since only synchronized methods are invoked.
822 // and only immutable (final) vars are used. And the listeners of this class have
823 // now been made threadsafe by using CopyOnWriteArrayList in place of EventListenerList.
824 sendProcessStatus(new ConstructionEvent(GS2PerlConstructor.this, GSStatus.CONTINUING, line));
825
826 } catch(IOException ioe) {
827 // "All methods on Logger are multi-thread safe", see
828 // http://stackoverflow.com/questions/14211629/java-util-logger-write-synchronization
829
830 String msg = (this.source == SafeProcess.STDERR) ? "stderr" : "stdout";
831 msg = "IOException when writing out a line read from perl process' " + msg + " stream.";
832 msg += "\nGot line: " + line + "\n";
833 throw new Exception(msg, ioe);
834 }
835 }
836
837 } // end inner class SynchronizedProcessHandler
838
839}
Note: See TracBrowser for help on using the repository browser.