Mercurial > hg > Members > kono > jpf-core
comparison src/main/gov/nasa/jpf/JPF.java @ 0:61d41facf527
initial v8 import (history reset)
author | Peter Mehlitz <Peter.C.Mehlitz@nasa.gov> |
---|---|
date | Fri, 23 Jan 2015 10:14:01 -0800 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:61d41facf527 |
---|---|
1 /* | |
2 * Copyright (C) 2014, United States Government, as represented by the | |
3 * Administrator of the National Aeronautics and Space Administration. | |
4 * All rights reserved. | |
5 * | |
6 * The Java Pathfinder core (jpf-core) platform is licensed under the | |
7 * Apache License, Version 2.0 (the "License"); you may not use this file except | |
8 * in compliance with the License. You may obtain a copy of the License at | |
9 * | |
10 * http://www.apache.org/licenses/LICENSE-2.0. | |
11 * | |
12 * Unless required by applicable law or agreed to in writing, software | |
13 * distributed under the License is distributed on an "AS IS" BASIS, | |
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
15 * See the License for the specific language governing permissions and | |
16 * limitations under the License. | |
17 */ | |
18 package gov.nasa.jpf; | |
19 | |
20 import gov.nasa.jpf.report.Publisher; | |
21 import gov.nasa.jpf.report.PublisherExtension; | |
22 import gov.nasa.jpf.report.Reporter; | |
23 import gov.nasa.jpf.search.Search; | |
24 import gov.nasa.jpf.search.SearchListener; | |
25 import gov.nasa.jpf.tool.RunJPF; | |
26 import gov.nasa.jpf.util.JPFLogger; | |
27 import gov.nasa.jpf.util.LogManager; | |
28 import gov.nasa.jpf.util.Misc; | |
29 import gov.nasa.jpf.util.RunRegistry; | |
30 import gov.nasa.jpf.vm.VM; | |
31 import gov.nasa.jpf.vm.NoOutOfMemoryErrorProperty; | |
32 import gov.nasa.jpf.vm.VMListener; | |
33 | |
34 import java.io.File; | |
35 import java.util.ArrayList; | |
36 import java.util.List; | |
37 import java.util.logging.Logger; | |
38 | |
39 | |
40 /** | |
41 * main class of the JPF verification framework. This reads the configuration, | |
42 * instantiates the Search and VM objects, and kicks off the Search | |
43 */ | |
44 public class JPF implements Runnable { | |
45 | |
46 public static String VERSION = "8.0"; // the major version number | |
47 | |
48 static Logger logger = null; // initially | |
49 | |
50 public enum Status { NEW, RUNNING, DONE }; | |
51 | |
52 class ConfigListener implements ConfigChangeListener { | |
53 | |
54 /** | |
55 * check for new listeners that are dynamically configured | |
56 */ | |
57 @Override | |
58 public void propertyChanged(Config config, String key, String oldValue, String newValue) { | |
59 if ("listener".equals(key)) { | |
60 if (oldValue == null) | |
61 oldValue = ""; | |
62 | |
63 String[] nv = config.asStringArray(newValue); | |
64 String[] ov = config.asStringArray(oldValue); | |
65 String[] newListeners = Misc.getAddedElements(ov, nv); | |
66 Class<?>[] argTypes = { Config.class, JPF.class }; // Many listeners have 2 parameter constructors | |
67 Object[] args = {config, JPF.this }; | |
68 | |
69 if (newListeners != null) { | |
70 for (String clsName : newListeners) { | |
71 try { | |
72 JPFListener newListener = config.getInstance("listener", clsName, JPFListener.class, argTypes, args); | |
73 addListener(newListener); | |
74 logger.info("config changed, added listener " + clsName); | |
75 | |
76 } catch (JPFConfigException cfx) { | |
77 logger.warning("listener change failed: " + cfx.getMessage()); | |
78 } | |
79 } | |
80 } | |
81 } | |
82 } | |
83 | |
84 /** | |
85 * clean up to avoid a sublte but serious memory leak when using the | |
86 * same config for multiple JPF objects/runs - this listener is an inner | |
87 * class object that keeps its encapsulating JPF instance alive | |
88 */ | |
89 @Override | |
90 public void jpfRunTerminated(Config config){ | |
91 config.removeChangeListener(this); | |
92 } | |
93 } | |
94 | |
95 /** this is the backbone of all JPF configuration */ | |
96 Config config; | |
97 | |
98 /** The search policy used to explore the state space */ | |
99 Search search; | |
100 | |
101 /** Reference to the virtual machine used by the search */ | |
102 VM vm; | |
103 | |
104 /** the report generator */ | |
105 Reporter reporter; | |
106 | |
107 Status status = Status.NEW; | |
108 | |
109 /** a list of listeners that get automatically added from VM, Search or Reporter initialization */ | |
110 List<VMListener> pendingVMListeners; | |
111 List<SearchListener> pendingSearchListeners; | |
112 | |
113 | |
114 /** we use this as safety margin, to be released upon OutOfMemoryErrors */ | |
115 byte[] memoryReserve; | |
116 | |
117 private static Logger initLogging(Config conf) { | |
118 LogManager.init(conf); | |
119 return getLogger("gov.nasa.jpf"); | |
120 } | |
121 | |
122 /** | |
123 * use this one to get a Logger that is initialized via our Config mechanism. Note that | |
124 * our own Loggers do NOT pass | |
125 */ | |
126 public static JPFLogger getLogger (String name) { | |
127 return LogManager.getLogger( name); | |
128 } | |
129 | |
130 public static void main(String[] args){ | |
131 int options = RunJPF.getOptions(args); | |
132 | |
133 if (args.length == 0 || RunJPF.isOptionEnabled( RunJPF.HELP,options)) { | |
134 RunJPF.showUsage(); | |
135 return; | |
136 } | |
137 if (RunJPF.isOptionEnabled( RunJPF.ADD_PROJECT,options)){ | |
138 RunJPF.addProject(args); | |
139 return; | |
140 } | |
141 | |
142 if (RunJPF.isOptionEnabled( RunJPF.BUILD_INFO,options)){ | |
143 RunJPF.showBuild(RunJPF.class.getClassLoader()); | |
144 } | |
145 | |
146 if (RunJPF.isOptionEnabled( RunJPF.LOG,options)){ | |
147 Config.enableLogging(true); | |
148 } | |
149 | |
150 Config conf = createConfig(args); | |
151 | |
152 if (RunJPF.isOptionEnabled( RunJPF.SHOW, options)) { | |
153 conf.printEntries(); | |
154 } | |
155 | |
156 start(conf, args); | |
157 } | |
158 | |
159 public static void start(Config conf, String[] args){ | |
160 // this is redundant to jpf.report.<publisher>.start=..config.. | |
161 // but nobody can remember this (it's only used to produce complete reports) | |
162 | |
163 if (logger == null) { | |
164 logger = initLogging(conf); | |
165 } | |
166 | |
167 if (!checkArgs(args)){ | |
168 return; | |
169 } | |
170 | |
171 setNativeClassPath(conf); // in case we have to find a shell | |
172 | |
173 // check if there is a shell class specification, in which case we just delegate | |
174 JPFShell shell = conf.getInstance("shell", JPFShell.class); | |
175 if (shell != null) { | |
176 shell.start(args); // responsible for exception handling itself | |
177 | |
178 } else { | |
179 // no shell, we start JPF directly | |
180 LogManager.printStatus(logger); | |
181 conf.printStatus(logger); | |
182 | |
183 // this has to be done after we checked&consumed all "-.." arguments | |
184 // this is NOT about checking properties! | |
185 checkUnknownArgs(args); | |
186 | |
187 try { | |
188 JPF jpf = new JPF(conf); | |
189 jpf.run(); | |
190 | |
191 } catch (ExitException x) { | |
192 logger.severe( "JPF terminated"); | |
193 | |
194 // this is how we get most runtime config exceptions | |
195 if (x.shouldReport()) { | |
196 x.printStackTrace(); | |
197 } | |
198 | |
199 /** | |
200 Throwable cause = x.getCause(); | |
201 if ((cause != null) && conf.getBoolean("pass_exceptions", false)) { | |
202 cause.fillInStackTrace(); | |
203 throw cause; | |
204 } | |
205 **/ | |
206 | |
207 } catch (JPFException jx) { | |
208 logger.severe( "JPF exception, terminating: " + jx.getMessage()); | |
209 if (conf.getBoolean("jpf.print_exception_stack")) { | |
210 | |
211 Throwable cause = jx.getCause(); | |
212 if (cause != null && cause != jx){ | |
213 cause.printStackTrace(); | |
214 } else { | |
215 jx.printStackTrace(); | |
216 } | |
217 } | |
218 // pass this on, caller has to handle | |
219 throw jx; | |
220 } | |
221 } | |
222 } | |
223 | |
224 | |
225 static void setNativeClassPath(Config config) { | |
226 if (!config.hasSetClassLoader()) { | |
227 config.initClassLoader( JPF.class.getClassLoader()); | |
228 } | |
229 } | |
230 | |
231 | |
232 protected JPF (){ | |
233 // just to support unit test mockups | |
234 } | |
235 | |
236 /** | |
237 * create new JPF object. Note this is always guaranteed to return, but the | |
238 * Search and/or VM object instantiation might have failed (i.e. the JPF | |
239 * object might not be really usable). If you directly use getSearch() / getVM(), | |
240 * check for null | |
241 */ | |
242 public JPF(Config conf) { | |
243 config = conf; | |
244 | |
245 setNativeClassPath(config); // before we do anything else | |
246 | |
247 if (logger == null) { // maybe somebody created a JPF object explicitly | |
248 logger = initLogging(config); | |
249 } | |
250 | |
251 initialize(); | |
252 } | |
253 | |
254 /** | |
255 * convenience method if caller doesn't care about Config | |
256 */ | |
257 public JPF (String ... args) { | |
258 this( createConfig(args)); | |
259 } | |
260 | |
261 private void initialize() { | |
262 VERSION = config.getString("jpf.version", VERSION); | |
263 memoryReserve = new byte[config.getInt("jpf.memory_reserve", 64 * 1024)]; // in bytes | |
264 | |
265 try { | |
266 | |
267 Class<?>[] vmArgTypes = { JPF.class, Config.class }; | |
268 Object[] vmArgs = { this, config }; | |
269 vm = config.getEssentialInstance("vm.class", VM.class, vmArgTypes, vmArgs); | |
270 | |
271 Class<?>[] searchArgTypes = { Config.class, VM.class }; | |
272 Object[] searchArgs = { config, vm }; | |
273 search = config.getEssentialInstance("search.class", Search.class, | |
274 searchArgTypes, searchArgs); | |
275 | |
276 // although the Reporter will always be notified last, this has to be set | |
277 // first so that it can register utility listeners like Statistics that | |
278 // can be used by configured listeners | |
279 Class<?>[] reporterArgTypes = { Config.class, JPF.class }; | |
280 Object[] reporterArgs = { config, this }; | |
281 reporter = config.getInstance("report.class", Reporter.class, reporterArgTypes, reporterArgs); | |
282 if (reporter != null){ | |
283 search.setReporter(reporter); | |
284 } | |
285 | |
286 addListeners(); | |
287 | |
288 config.addChangeListener(new ConfigListener()); | |
289 | |
290 } catch (JPFConfigException cx) { | |
291 logger.severe(cx.toString()); | |
292 //cx.getCause().printStackTrace(); | |
293 throw new ExitException(false, cx); | |
294 } | |
295 } | |
296 | |
297 | |
298 public Status getStatus() { | |
299 return status; | |
300 } | |
301 | |
302 public boolean isRunnable () { | |
303 return ((vm != null) && (search != null)); | |
304 } | |
305 | |
306 public void addPropertyListener (PropertyListenerAdapter pl) { | |
307 if (vm != null) { | |
308 vm.addListener( pl); | |
309 } | |
310 if (search != null) { | |
311 search.addListener( pl); | |
312 search.addProperty(pl); | |
313 } | |
314 } | |
315 | |
316 public void addSearchListener (SearchListener l) { | |
317 if (search != null) { | |
318 search.addListener(l); | |
319 } | |
320 } | |
321 | |
322 protected void addPendingVMListener (VMListener l){ | |
323 if (pendingVMListeners == null){ | |
324 pendingVMListeners = new ArrayList<VMListener>(); | |
325 } | |
326 pendingVMListeners.add(l); | |
327 } | |
328 | |
329 protected void addPendingSearchListener (SearchListener l){ | |
330 if (pendingSearchListeners == null){ | |
331 pendingSearchListeners = new ArrayList<SearchListener>(); | |
332 } | |
333 pendingSearchListeners.add(l); | |
334 } | |
335 | |
336 public void addListener (JPFListener l) { | |
337 // the VM is instantiated first | |
338 if (l instanceof VMListener) { | |
339 if (vm != null) { | |
340 vm.addListener( (VMListener) l); | |
341 | |
342 } else { // no VM yet, we are in VM initialization | |
343 addPendingVMListener((VMListener)l); | |
344 } | |
345 } | |
346 | |
347 if (l instanceof SearchListener) { | |
348 if (search != null) { | |
349 search.addListener( (SearchListener) l); | |
350 | |
351 } else { // no search yet, we are in Search initialization | |
352 addPendingSearchListener((SearchListener)l); | |
353 } | |
354 } | |
355 } | |
356 | |
357 public <T> T getListenerOfType( Class<T> type){ | |
358 if (search != null){ | |
359 T listener = search.getNextListenerOfType(type, null); | |
360 if (listener != null){ | |
361 return listener; | |
362 } | |
363 } | |
364 | |
365 if (vm != null){ | |
366 T listener = vm.getNextListenerOfType(type, null); | |
367 if (listener != null){ | |
368 return listener; | |
369 } | |
370 } | |
371 | |
372 return null; | |
373 } | |
374 | |
375 public boolean addUniqueTypeListener (JPFListener l) { | |
376 boolean addedListener = false; | |
377 Class<?> cls = l.getClass(); | |
378 | |
379 if (l instanceof VMListener) { | |
380 if (vm != null) { | |
381 if (!vm.hasListenerOfType(cls)) { | |
382 vm.addListener( (VMListener) l); | |
383 addedListener = true; | |
384 } | |
385 } | |
386 } | |
387 if (l instanceof SearchListener) { | |
388 if (search != null) { | |
389 if (!search.hasListenerOfType(cls)) { | |
390 search.addListener( (SearchListener) l); | |
391 addedListener = true; | |
392 } | |
393 } | |
394 } | |
395 | |
396 return addedListener; | |
397 } | |
398 | |
399 | |
400 public void removeListener (JPFListener l){ | |
401 if (l instanceof VMListener) { | |
402 if (vm != null) { | |
403 vm.removeListener( (VMListener) l); | |
404 } | |
405 } | |
406 if (l instanceof SearchListener) { | |
407 if (search != null) { | |
408 search.removeListener( (SearchListener) l); | |
409 } | |
410 } | |
411 } | |
412 | |
413 public void addVMListener (VMListener l) { | |
414 if (vm != null) { | |
415 vm.addListener(l); | |
416 } | |
417 } | |
418 | |
419 public void addSearchProperty (Property p) { | |
420 if (search != null) { | |
421 search.addProperty(p); | |
422 } | |
423 } | |
424 | |
425 /** | |
426 * this is called after vm, search and reporter got instantiated | |
427 */ | |
428 void addListeners () { | |
429 Class<?>[] argTypes = { Config.class, JPF.class }; | |
430 Object[] args = { config, this }; | |
431 | |
432 // first listeners that were automatically added from VM, Search and Reporter initialization | |
433 if (pendingVMListeners != null){ | |
434 for (VMListener l : pendingVMListeners) { | |
435 vm.addListener(l); | |
436 } | |
437 pendingVMListeners = null; | |
438 } | |
439 | |
440 if (pendingSearchListeners != null){ | |
441 for (SearchListener l : pendingSearchListeners) { | |
442 search.addListener(l); | |
443 } | |
444 pendingSearchListeners = null; | |
445 } | |
446 | |
447 // and finally everything that's user configured | |
448 List<JPFListener> listeners = config.getInstances("listener", JPFListener.class, argTypes, args); | |
449 if (listeners != null) { | |
450 for (JPFListener l : listeners) { | |
451 addListener(l); | |
452 } | |
453 } | |
454 } | |
455 | |
456 public Reporter getReporter () { | |
457 return reporter; | |
458 } | |
459 | |
460 public <T extends Publisher> boolean addPublisherExtension (Class<T> pCls, PublisherExtension e) { | |
461 if (reporter != null) { | |
462 return reporter.addPublisherExtension(pCls, e); | |
463 } | |
464 return false; | |
465 } | |
466 | |
467 public <T extends Publisher> void setPublisherItems (Class<T> pCls, | |
468 int category, String[] topics) { | |
469 if (reporter != null) { | |
470 reporter.setPublisherItems(pCls, category, topics); | |
471 } | |
472 } | |
473 | |
474 | |
475 public Config getConfig() { | |
476 return config; | |
477 } | |
478 | |
479 /** | |
480 * return the search object. This can be null if the initialization has failed | |
481 */ | |
482 public Search getSearch() { | |
483 return search; | |
484 } | |
485 | |
486 /** | |
487 * return the VM object. This can be null if the initialization has failed | |
488 */ | |
489 public VM getVM() { | |
490 return vm; | |
491 } | |
492 | |
493 public static void exit() { | |
494 // Hmm, exception as non local return. But we might be called from a | |
495 // context we don't want to kill | |
496 throw new ExitException(); | |
497 } | |
498 | |
499 public boolean foundErrors() { | |
500 return !(search.getErrors().isEmpty()); | |
501 } | |
502 | |
503 /** | |
504 * this assumes that we have checked and 'consumed' (nullified) all known | |
505 * options, so we just have to check for any '-' option prior to the | |
506 * target class name | |
507 */ | |
508 static void checkUnknownArgs (String[] args) { | |
509 for ( int i=0; i<args.length; i++) { | |
510 if (args[i] != null) { | |
511 if (args[i].charAt(0) == '-') { | |
512 logger.warning("unknown command line option: " + args[i]); | |
513 } | |
514 else { | |
515 // this is supposed to be the target class name - everything that follows | |
516 // is supposed to be processed by the program under test | |
517 break; | |
518 } | |
519 } | |
520 } | |
521 } | |
522 | |
523 public static void printBanner (Config config) { | |
524 System.out.println("Java Pathfinder core system v" + | |
525 config.getString("jpf.version", VERSION) + | |
526 " - (C) 2005-2014 United States Government. All rights reserved."); | |
527 } | |
528 | |
529 | |
530 /** | |
531 * find the value of an arg that is either specific as | |
532 * "-key=value" or as "-key value". If not found, the supplied | |
533 * defValue is returned | |
534 */ | |
535 static String getArg(String[] args, String pattern, String defValue, boolean consume) { | |
536 String s = defValue; | |
537 | |
538 if (args != null){ | |
539 for (int i = 0; i < args.length; i++) { | |
540 String arg = args[i]; | |
541 | |
542 if (arg != null) { | |
543 if (arg.matches(pattern)) { | |
544 int idx=arg.indexOf('='); | |
545 if (idx > 0) { | |
546 s = arg.substring(idx+1); | |
547 if (consume) { | |
548 args[i]=null; | |
549 } | |
550 } else if (i < args.length-1) { | |
551 s = args[i+1]; | |
552 if (consume) { | |
553 args[i] = null; | |
554 args[i+1] = null; | |
555 } | |
556 } | |
557 break; | |
558 } | |
559 } | |
560 } | |
561 } | |
562 | |
563 return s; | |
564 } | |
565 | |
566 /** | |
567 * what property file to look for | |
568 */ | |
569 static String getConfigFileName (String[] args) { | |
570 if (args.length > 0) { | |
571 // check if the last arg is a mode property file | |
572 // if yes, it has to include a 'target' spec | |
573 int idx = args.length-1; | |
574 String lastArg = args[idx]; | |
575 if (lastArg.endsWith(".jpf") || lastArg.endsWith(".properties")) { | |
576 if (lastArg.startsWith("-c")) { | |
577 int i = lastArg.indexOf('='); | |
578 if (i > 0) { | |
579 lastArg = lastArg.substring(i+1); | |
580 } | |
581 } | |
582 args[idx] = null; | |
583 return lastArg; | |
584 } | |
585 } | |
586 | |
587 return getArg(args, "-c(onfig)?(=.+)?", "jpf.properties", true); | |
588 } | |
589 | |
590 /** | |
591 * return a Config object that holds the JPF options. This first | |
592 * loads the properties from a (potentially configured) properties file, and | |
593 * then overlays all command line arguments that are key/value pairs | |
594 */ | |
595 public static Config createConfig (String[] args) { | |
596 return new Config(args); | |
597 } | |
598 | |
599 /** | |
600 * runs the verification. | |
601 */ | |
602 @Override | |
603 public void run() { | |
604 Runtime rt = Runtime.getRuntime(); | |
605 | |
606 // this might be executed consecutively, so notify everybody | |
607 RunRegistry.getDefaultRegistry().reset(); | |
608 | |
609 if (isRunnable()) { | |
610 try { | |
611 if (vm.initialize()) { | |
612 status = Status.RUNNING; | |
613 search.search(); | |
614 } | |
615 } catch (OutOfMemoryError oom) { | |
616 | |
617 // try to get memory back before we do anything that makes it worse | |
618 // (note that we even try to avoid calls here, we are on thin ice) | |
619 | |
620 // NOTE - we don't try to recover at this point (that is what we do | |
621 // if we fall below search.min_free within search()), we only want to | |
622 // terminate gracefully (incl. report) | |
623 | |
624 memoryReserve = null; // release something | |
625 long m0 = rt.freeMemory(); | |
626 long d = 10000; | |
627 | |
628 // see if we can reclaim some memory before logging or printing statistics | |
629 for (int i=0; i<10; i++) { | |
630 rt.gc(); | |
631 long m1 = rt.freeMemory(); | |
632 if ((m1 <= m0) || ((m1 - m0) < d)) { | |
633 break; | |
634 } | |
635 m0 = m1; | |
636 } | |
637 | |
638 logger.severe("JPF out of memory"); | |
639 | |
640 // that's questionable, but we might want to see statistics / coverage | |
641 // to know how far we got. We don't inform any other listeners though | |
642 // if it throws an exception we bail - we can't handle it w/o memory | |
643 try { | |
644 search.notifySearchConstraintHit("JPF out of memory"); | |
645 search.error(new NoOutOfMemoryErrorProperty()); // JUnit tests will succeed if OOM isn't flagged. | |
646 reporter.searchFinished(search); | |
647 } catch (Throwable t){ | |
648 throw new JPFListenerException("exception during out-of-memory termination", t); | |
649 } | |
650 | |
651 // NOTE - this is not an exception firewall anymore | |
652 | |
653 } finally { | |
654 status = Status.DONE; | |
655 | |
656 config.jpfRunTerminated(); | |
657 cleanUp(); | |
658 } | |
659 } | |
660 } | |
661 | |
662 protected void cleanUp(){ | |
663 search.cleanUp(); | |
664 vm.cleanUp(); | |
665 reporter.cleanUp(); | |
666 } | |
667 | |
668 public List<Error> getSearchErrors () { | |
669 if (search != null) { | |
670 return search.getErrors(); | |
671 } | |
672 | |
673 return null; | |
674 } | |
675 | |
676 public Error getLastError () { | |
677 if (search != null) { | |
678 return search.getLastError(); | |
679 } | |
680 | |
681 return null; | |
682 } | |
683 | |
684 | |
685 // some minimal sanity checks | |
686 static boolean checkArgs (String[] args){ | |
687 String lastArg = args[args.length-1]; | |
688 if (lastArg != null && lastArg.endsWith(".jpf")){ | |
689 if (!new File(lastArg).isFile()){ | |
690 logger.severe("application property file not found: " + lastArg); | |
691 return false; | |
692 } | |
693 } | |
694 | |
695 return true; | |
696 } | |
697 | |
698 public static void handleException(JPFException e) { | |
699 logger.severe(e.getMessage()); | |
700 exit(); | |
701 } | |
702 | |
703 /** | |
704 * private helper class for local termination of JPF (without killing the | |
705 * whole Java process via System.exit). | |
706 * While this is basically a bad non-local goto exception, it seems to be the | |
707 * least of evils given the current JPF structure, and the need to terminate | |
708 * w/o exiting the whole Java process. If we just do a System.exit(), we couldn't | |
709 * use JPF in an embedded context | |
710 */ | |
711 @SuppressWarnings("serial") | |
712 public static class ExitException extends RuntimeException { | |
713 boolean report = true; | |
714 | |
715 ExitException() {} | |
716 | |
717 ExitException (boolean report, Throwable cause){ | |
718 super(cause); | |
719 | |
720 this.report = report; | |
721 } | |
722 | |
723 ExitException(String msg) { | |
724 super(msg); | |
725 } | |
726 | |
727 public boolean shouldReport() { | |
728 return report; | |
729 } | |
730 } | |
731 } |