This project has retired. For details please refer to its
Attic page.
001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.apache.oozie.action.hadoop;
019
020 import java.io.BufferedReader;
021 import java.io.File;
022 import java.io.FileReader;
023 import java.io.IOException;
024 import java.io.InputStream;
025 import java.io.InputStreamReader;
026 import java.io.OutputStream;
027 import java.io.OutputStreamWriter;
028 import java.io.PrintWriter;
029 import java.io.StringWriter;
030 import java.io.Writer;
031 import java.lang.reflect.InvocationTargetException;
032 import java.lang.reflect.Method;
033 import java.security.Permission;
034 import java.text.MessageFormat;
035 import java.util.Properties;
036 import java.util.StringTokenizer;
037 import java.util.concurrent.ScheduledThreadPoolExecutor;
038 import java.util.concurrent.TimeUnit;
039
040 import org.apache.hadoop.conf.Configuration;
041 import org.apache.hadoop.fs.FileSystem;
042 import org.apache.hadoop.fs.Path;
043 import org.apache.hadoop.mapred.Counters;
044 import org.apache.hadoop.mapred.JobConf;
045 import org.apache.hadoop.mapred.Mapper;
046 import org.apache.hadoop.mapred.OutputCollector;
047 import org.apache.hadoop.mapred.Reporter;
048 import org.apache.hadoop.mapred.RunningJob;
049 import org.apache.oozie.service.HadoopAccessorException;
050 import org.apache.oozie.service.HadoopAccessorService;
051 import org.apache.oozie.service.Services;
052 import org.apache.oozie.util.XLog;
053
054 public class LauncherMapper<K1, V1, K2, V2> implements Mapper<K1, V1, K2, V2>, Runnable {
055
056 public static final String CONF_OOZIE_ACTION_MAIN_CLASS = "oozie.launcher.action.main.class";
057
058 private static final String CONF_OOZIE_ACTION_MAIN_ARG_COUNT = "oozie.action.main.arg.count";
059 private static final String CONF_OOZIE_ACTION_MAIN_ARG_PREFIX = "oozie.action.main.arg.";
060 private static final String CONF_OOZIE_ACTION_MAX_OUTPUT_DATA = "oozie.action.max.output.data";
061
062 private static final String COUNTER_GROUP = "oozie.launcher";
063 private static final String COUNTER_DO_ID_SWAP = "oozie.do.id.swap";
064 private static final String COUNTER_OUTPUT_DATA = "oozie.output.data";
065 private static final String COUNTER_LAUNCHER_ERROR = "oozie.launcher.error";
066
067 private static final String OOZIE_JOB_ID = "oozie.job.id";
068 private static final String OOZIE_ACTION_ID = "oozie.action.id";
069
070 private static final String OOZIE_ACTION_DIR_PATH = "oozie.action.dir.path";
071 private static final String OOZIE_ACTION_RECOVERY_ID = "oozie.action.recovery.id";
072
073 static final String ACTION_CONF_XML = "action.xml";
074 private static final String ACTION_OUTPUT_PROPS = "output.properties";
075 private static final String ACTION_NEW_ID_PROPS = "newId.properties";
076 private static final String ACTION_ERROR_PROPS = "error.properties";
077
078 private void setRecoveryId(Configuration launcherConf, Path actionDir, String recoveryId) throws LauncherException {
079 try {
080 FileSystem fs = FileSystem.get(launcherConf);
081 String jobId = launcherConf.get("mapred.job.id");
082 Path path = new Path(actionDir, recoveryId);
083 if (!fs.exists(path)) {
084 try {
085 Writer writer = new OutputStreamWriter(fs.create(path));
086 writer.write(jobId);
087 writer.close();
088 }
089 catch (IOException ex) {
090 failLauncher(0, "IO error", ex);
091 }
092 }
093 else {
094 InputStream is = fs.open(path);
095 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
096 String id = reader.readLine();
097 reader.close();
098 if (!jobId.equals(id)) {
099 failLauncher(0, MessageFormat.format(
100 "Hadoop job Id mismatch, action file [{0}] declares Id [{1}] current Id [{2}]", path, id,
101 jobId), null);
102 }
103
104 }
105 }
106 catch (IOException ex) {
107 failLauncher(0, "IO error", ex);
108 }
109 }
110
111 /**
112 * @param launcherConf
113 * @param actionDir
114 * @param recoveryId
115 * @return
116 * @throws HadoopAccessorException
117 * @throws IOException
118 */
119 public static String getRecoveryId(Configuration launcherConf, Path actionDir, String recoveryId)
120 throws HadoopAccessorException, IOException {
121 String jobId = null;
122 Path recoveryFile = new Path(actionDir, recoveryId);
123 //FileSystem fs = FileSystem.get(launcherConf);
124 FileSystem fs = Services.get().get(HadoopAccessorService.class)
125 .createFileSystem(launcherConf.get("user.name"),
126 launcherConf.get("group.name"), launcherConf);
127
128 if (fs.exists(recoveryFile)) {
129 InputStream is = fs.open(recoveryFile);
130 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
131 jobId = reader.readLine();
132 reader.close();
133 }
134 return jobId;
135
136 }
137
138 public static void setupMainClass(Configuration launcherConf, String javaMainClass) {
139 launcherConf.set(CONF_OOZIE_ACTION_MAIN_CLASS, javaMainClass);
140 }
141
142 public static void setupMainArguments(Configuration launcherConf, String[] args) {
143 launcherConf.setInt(CONF_OOZIE_ACTION_MAIN_ARG_COUNT, args.length);
144 for (int i = 0; i < args.length; i++) {
145 launcherConf.set(CONF_OOZIE_ACTION_MAIN_ARG_PREFIX + i, args[i]);
146 }
147 }
148
149 public static void setupMaxOutputData(Configuration launcherConf, int maxOutputData) {
150 launcherConf.setInt(CONF_OOZIE_ACTION_MAX_OUTPUT_DATA, maxOutputData);
151 }
152
153 /**
154 * @param launcherConf
155 * @param jobId
156 * @param actionId
157 * @param actionDir
158 * @param recoveryId
159 * @param actionConf
160 * @throws IOException
161 * @throws HadoopAccessorException
162 */
163 public static void setupLauncherInfo(JobConf launcherConf, String jobId, String actionId, Path actionDir,
164 String recoveryId, Configuration actionConf) throws IOException, HadoopAccessorException {
165
166 launcherConf.setMapperClass(LauncherMapper.class);
167 launcherConf.setSpeculativeExecution(false);
168 launcherConf.setNumMapTasks(1);
169 launcherConf.setNumReduceTasks(0);
170
171 launcherConf.set(OOZIE_JOB_ID, jobId);
172 launcherConf.set(OOZIE_ACTION_ID, actionId);
173 launcherConf.set(OOZIE_ACTION_DIR_PATH, actionDir.toString());
174 launcherConf.set(OOZIE_ACTION_RECOVERY_ID, recoveryId);
175
176 actionConf.set(OOZIE_JOB_ID, jobId);
177 actionConf.set(OOZIE_ACTION_ID, actionId);
178
179 FileSystem fs = Services.get().get(HadoopAccessorService.class).createFileSystem(launcherConf.get("user.name"),
180 launcherConf.get("group.name"), launcherConf);
181 fs.mkdirs(actionDir);
182
183 OutputStream os = fs.create(new Path(actionDir, ACTION_CONF_XML));
184 actionConf.writeXml(os);
185 os.close();
186
187 Path inputDir = new Path(actionDir, "input");
188 fs.mkdirs(inputDir);
189 Writer writer = new OutputStreamWriter(fs.create(new Path(inputDir, "dummy.txt")));
190 writer.write("dummy");
191 writer.close();
192
193 launcherConf.set("mapred.input.dir", inputDir.toString());
194 launcherConf.set("mapred.output.dir", new Path(actionDir, "output").toString());
195 }
196
197 public static boolean isMainDone(RunningJob runningJob) throws IOException {
198 return runningJob.isComplete();
199 }
200
201 public static boolean isMainSuccessful(RunningJob runningJob) throws IOException {
202 boolean succeeded = runningJob.isSuccessful();
203 if (succeeded) {
204 Counters counters = runningJob.getCounters();
205 if (counters != null) {
206 Counters.Group group = counters.getGroup(COUNTER_GROUP);
207 if (group != null) {
208 succeeded = group.getCounter(COUNTER_LAUNCHER_ERROR) == 0;
209 }
210 }
211 }
212 return succeeded;
213 }
214
215 public static boolean hasOutputData(RunningJob runningJob) throws IOException {
216 boolean output = false;
217 Counters counters = runningJob.getCounters();
218 if (counters != null) {
219 Counters.Group group = counters.getGroup(COUNTER_GROUP);
220 if (group != null) {
221 output = group.getCounter(COUNTER_OUTPUT_DATA) == 1;
222 }
223 }
224 return output;
225 }
226
227 /**
228 * @param runningJob
229 * @return
230 * @throws IOException
231 */
232 public static boolean hasIdSwap(RunningJob runningJob) throws IOException {
233 boolean swap = false;
234 Counters counters = runningJob.getCounters();
235 if (counters != null) {
236 Counters.Group group = counters.getGroup(COUNTER_GROUP);
237 if (group != null) {
238 swap = group.getCounter(COUNTER_DO_ID_SWAP) == 1;
239 }
240 }
241 return swap;
242 }
243
244 /**
245 * @param runningJob
246 * @param user
247 * @param group
248 * @param actionDir
249 * @return
250 * @throws IOException
251 * @throws HadoopAccessorException
252 */
253 public static boolean hasIdSwap(RunningJob runningJob, String user, String group, Path actionDir)
254 throws IOException, HadoopAccessorException {
255 boolean swap = false;
256
257 XLog log = XLog.getLog("org.apache.oozie.action.hadoop.LauncherMapper");
258
259 Counters counters = runningJob.getCounters();
260 if (counters != null) {
261 Counters.Group counterGroup = counters.getGroup(COUNTER_GROUP);
262 if (counterGroup != null) {
263 swap = counterGroup.getCounter(COUNTER_DO_ID_SWAP) == 1;
264 }
265 }
266 // additional check for swapped hadoop ID
267 // Can't rely on hadoop counters existing
268 // we'll check for the newID file in hdfs if the hadoop counters is null
269 else {
270
271 Path p = getIdSwapPath(actionDir);
272 // log.debug("Checking for newId file in: [{0}]", p);
273
274 FileSystem fs = Services.get().get(HadoopAccessorService.class).createFileSystem(user, group, p.toUri(),
275 new Configuration());
276 if (fs.exists(p)) {
277 log.debug("Hadoop Counters is null, but found newID file.");
278
279 swap = true;
280 }
281 else {
282 log.debug("Hadoop Counters is null, and newID file doesn't exist at: [{0}]", p);
283 }
284 }
285 return swap;
286 }
287
288 public static Path getOutputDataPath(Path actionDir) {
289 return new Path(actionDir, ACTION_OUTPUT_PROPS);
290 }
291
292 public static Path getErrorPath(Path actionDir) {
293 return new Path(actionDir, ACTION_ERROR_PROPS);
294 }
295
296 public static Path getIdSwapPath(Path actionDir) {
297 return new Path(actionDir, ACTION_NEW_ID_PROPS);
298 }
299
300 private JobConf jobConf;
301 private Path actionDir;
302 private ScheduledThreadPoolExecutor timer;
303
304 private boolean configFailure = false;
305
306 public LauncherMapper() {
307 }
308
309 public void configure(JobConf jobConf) {
310 System.out.println();
311 System.out.println("Oozie Launcher starts");
312 System.out.println();
313 this.jobConf = jobConf;
314 actionDir = new Path(getJobConf().get(OOZIE_ACTION_DIR_PATH));
315 String recoveryId = jobConf.get(OOZIE_ACTION_RECOVERY_ID, null);
316 try {
317 setRecoveryId(jobConf, actionDir, recoveryId);
318 }
319 catch (LauncherException ex) {
320 configFailure = true;
321 }
322 }
323
324 public void map(K1 key, V1 value, OutputCollector<K2, V2> collector, Reporter reporter) throws IOException {
325 try {
326 if (configFailure) {
327 throw new LauncherException();
328 }
329 else {
330 String mainClass = getJobConf().get(CONF_OOZIE_ACTION_MAIN_CLASS);
331 String msgPrefix = "Main class [" + mainClass + "], ";
332 int errorCode = 0;
333 Throwable errorCause = null;
334 String errorMessage = null;
335
336 try {
337 new LauncherSecurityManager();
338 }
339 catch (SecurityException ex) {
340 errorMessage = "Could not set LauncherSecurityManager";
341 errorCause = ex;
342 }
343
344 try {
345 setupHeartBeater(reporter);
346
347 setupMainConfiguration();
348
349 String[] args = getMainArguments(getJobConf());
350
351 printContentsOfCurrentDir();
352
353 System.out.println();
354 System.out.println("Oozie Java/Map-Reduce/Pig action launcher-job configuration");
355 System.out.println("=================================================================");
356 System.out.println("Workflow job id : " + System.getProperty("oozie.job.id"));
357 System.out.println("Workflow action id: " + System.getProperty("oozie.action.id"));
358 System.out.println();
359 System.out.println("Classpath :");
360 System.out.println("------------------------");
361 StringTokenizer st = new StringTokenizer(System.getProperty("java.class.path"), ":");
362 while (st.hasMoreTokens()) {
363 System.out.println(" " + st.nextToken());
364 }
365 System.out.println("------------------------");
366 System.out.println();
367 System.out.println("Main class : " + mainClass);
368 System.out.println();
369 System.out.println("Maximum output : "
370 + getJobConf().getInt(CONF_OOZIE_ACTION_MAX_OUTPUT_DATA, 2 * 1024));
371 System.out.println();
372 System.out.println("Arguments :");
373 for (String arg : args) {
374 System.out.println(" " + arg);
375 }
376
377 System.out.println();
378 System.out.println("Java System Properties:");
379 System.out.println("------------------------");
380 System.getProperties().store(System.out, "");
381 System.out.flush();
382 System.out.println("------------------------");
383 System.out.println();
384
385 System.out.println("=================================================================");
386 System.out.println();
387 System.out.println(">>> Invoking Main class now >>>");
388 System.out.println();
389 System.out.flush();
390
391 try {
392 Class klass = getJobConf().getClass(CONF_OOZIE_ACTION_MAIN_CLASS, Object.class);
393 Method mainMethod = klass.getMethod("main", String[].class);
394 mainMethod.invoke(null, (Object) args);
395 }
396 catch (InvocationTargetException ex) {
397 if (LauncherMainException.class.isInstance(ex.getCause())) {
398 errorMessage = msgPrefix + "exit code [" +((LauncherMainException)ex.getCause()).getErrorCode()
399 + "]";
400 errorCause = null;
401 }
402 else if (SecurityException.class.isInstance(ex.getCause())) {
403 if (LauncherSecurityManager.getExitInvoked()) {
404 System.out.println("Intercepting System.exit(" + LauncherSecurityManager.getExitCode()
405 + ")");
406 System.err.println("Intercepting System.exit(" + LauncherSecurityManager.getExitCode()
407 + ")");
408 // if 0 main() method finished successfully
409 // ignoring
410 errorCode = LauncherSecurityManager.getExitCode();
411 if (errorCode != 0) {
412 errorMessage = msgPrefix + "exit code [" + errorCode + "]";
413 errorCause = null;
414 }
415 }
416 }
417 else {
418 throw ex;
419 }
420 }
421 finally {
422 System.out.println();
423 System.out.println("<<< Invocation of Main class completed <<<");
424 System.out.println();
425 }
426 if (errorMessage == null) {
427 File outputData = new File(System.getProperty("oozie.action.output.properties"));
428 if (outputData.exists()) {
429 FileSystem fs = FileSystem.get(getJobConf());
430 fs.copyFromLocalFile(new Path(outputData.toString()), new Path(actionDir,
431 ACTION_OUTPUT_PROPS));
432 reporter.incrCounter(COUNTER_GROUP, COUNTER_OUTPUT_DATA, 1);
433
434 int maxOutputData = getJobConf().getInt(CONF_OOZIE_ACTION_MAX_OUTPUT_DATA, 2 * 1024);
435 if (outputData.length() > maxOutputData) {
436 String msg = MessageFormat.format("Output data size [{0}] exceeds maximum [{1}]",
437 outputData.length(), maxOutputData);
438 failLauncher(0, msg, null);
439 }
440 System.out.println();
441 System.out.println("Oozie Launcher, capturing output data:");
442 System.out.println("=======================");
443 Properties props = new Properties();
444 props.load(new FileReader(outputData));
445 props.store(System.out, "");
446 System.out.println();
447 System.out.println("=======================");
448 System.out.println();
449 }
450 File newId = new File(System.getProperty("oozie.action.newId.properties"));
451 if (newId.exists()) {
452 Properties props = new Properties();
453 props.load(new FileReader(newId));
454 if (props.getProperty("id") == null) {
455 throw new IllegalStateException("ID swap file does not have [id] property");
456 }
457 FileSystem fs = FileSystem.get(getJobConf());
458 fs.copyFromLocalFile(new Path(newId.toString()), new Path(actionDir, ACTION_NEW_ID_PROPS));
459 reporter.incrCounter(COUNTER_GROUP, COUNTER_DO_ID_SWAP, 1);
460
461 System.out.println("Oozie Launcher, copying new Hadoop job id to file: "
462 + new Path(actionDir, ACTION_NEW_ID_PROPS).toUri());
463
464 System.out.println();
465 System.out.println("Oozie Launcher, propagating new Hadoop job id to Oozie");
466 System.out.println("=======================");
467 System.out.println("id: " + props.getProperty("id"));
468 System.out.println("=======================");
469 System.out.println();
470 }
471 }
472 }
473 catch (NoSuchMethodException ex) {
474 errorMessage = msgPrefix + "main() method not found";
475 errorCause = ex;
476 }
477 catch (InvocationTargetException ex) {
478 errorMessage = msgPrefix + "main() threw exception";
479 errorCause = ex.getTargetException();
480 }
481 catch (Throwable ex) {
482 errorMessage = msgPrefix + "exception invoking main()";
483 errorCause = ex;
484 }
485 finally {
486 destroyHeartBeater();
487 if (errorMessage != null) {
488 failLauncher(errorCode, errorMessage, errorCause);
489 }
490 }
491 }
492 }
493 catch (LauncherException ex) {
494 reporter.incrCounter(COUNTER_GROUP, COUNTER_LAUNCHER_ERROR, 1);
495 System.out.println();
496 System.out.println("Oozie Launcher failed, finishing Hadoop job gracefully");
497 System.out.println();
498 }
499 }
500
501 public void close() throws IOException {
502 System.out.println();
503 System.out.println("Oozie Launcher ends");
504 System.out.println();
505 }
506
507 protected JobConf getJobConf() {
508 return jobConf;
509 }
510
511 private void setupMainConfiguration() throws IOException {
512 FileSystem fs = FileSystem.get(getJobConf());
513 fs.copyToLocalFile(new Path(getJobConf().get(OOZIE_ACTION_DIR_PATH), ACTION_CONF_XML), new Path(new File(
514 ACTION_CONF_XML).getAbsolutePath()));
515
516 System.setProperty("oozie.launcher.job.id", getJobConf().get("mapred.job.id"));
517 System.setProperty("oozie.job.id", getJobConf().get(OOZIE_JOB_ID));
518 System.setProperty("oozie.action.id", getJobConf().get(OOZIE_ACTION_ID));
519 System.setProperty("oozie.action.conf.xml", new File(ACTION_CONF_XML).getAbsolutePath());
520 System.setProperty("oozie.action.output.properties", new File(ACTION_OUTPUT_PROPS).getAbsolutePath());
521 System.setProperty("oozie.action.newId.properties", new File(ACTION_NEW_ID_PROPS).getAbsolutePath());
522 }
523
524 public static String[] getMainArguments(Configuration conf) {
525 String[] args = new String[conf.getInt(CONF_OOZIE_ACTION_MAIN_ARG_COUNT, 0)];
526 for (int i = 0; i < args.length; i++) {
527 args[i] = conf.get(CONF_OOZIE_ACTION_MAIN_ARG_PREFIX + i);
528 }
529 return args;
530 }
531
532 private void setupHeartBeater(Reporter reporter) {
533 timer = new ScheduledThreadPoolExecutor(1);
534 timer.scheduleAtFixedRate(new LauncherMapper(reporter), 0, 30, TimeUnit.SECONDS);
535 }
536
537 private void destroyHeartBeater() {
538 timer.shutdownNow();
539 }
540
541 private Reporter reporter;
542
543 private LauncherMapper(Reporter reporter) {
544 this.reporter = reporter;
545 }
546
547 public void run() {
548 System.out.println("Heart beat");
549 reporter.progress();
550 }
551
552 private void failLauncher(int errorCode, String reason, Throwable ex) throws LauncherException {
553 try {
554 if (ex != null) {
555 reason += ", " + ex.getMessage();
556 }
557 Properties errorProps = new Properties();
558 errorProps.setProperty("error.code", Integer.toString(errorCode));
559 errorProps.setProperty("error.reason", reason);
560 if (ex != null) {
561 if (ex.getMessage() != null) {
562 errorProps.setProperty("exception.message", ex.getMessage());
563 }
564 StringWriter sw = new StringWriter();
565 PrintWriter pw = new PrintWriter(sw);
566 ex.printStackTrace(pw);
567 pw.close();
568 errorProps.setProperty("exception.stacktrace", sw.toString());
569 }
570 FileSystem fs = FileSystem.get(getJobConf());
571 OutputStream os = fs.create(new Path(actionDir, ACTION_ERROR_PROPS));
572 errorProps.store(os, "");
573 os.close();
574
575 System.out.print("Failing Oozie Launcher, " + reason + "\n");
576 System.err.print("Failing Oozie Launcher, " + reason + "\n");
577 if (ex != null) {
578 ex.printStackTrace(System.out);
579 ex.printStackTrace(System.err);
580 }
581 throw new LauncherException();
582 }
583 catch (IOException rex) {
584 throw new RuntimeException("Error while failing launcher, " + rex.getMessage(), rex);
585 }
586 }
587
588 /**
589 * Print files and directories in current directory. Will list files in the sub-directory (only 1 level deep)
590 */
591 protected void printContentsOfCurrentDir() {
592 File folder = new File(".");
593 System.out.println();
594 System.out.println("Files in current dir:" + folder.getAbsolutePath());
595 System.out.println("======================");
596
597 File[] listOfFiles = folder.listFiles();
598 for (File fileName : listOfFiles) {
599 if (fileName.isFile()) {
600 System.out.println("File: " + fileName.getName());
601 }
602 else if (fileName.isDirectory()) {
603 System.out.println("Dir: " + fileName.getName());
604 File subDir = new File(fileName.getName());
605 File[] moreFiles = subDir.listFiles();
606 for (File subFileName : moreFiles) {
607 if (subFileName.isFile()) {
608 System.out.println(" File: " + subFileName.getName());
609 }
610 else if (subFileName.isDirectory()) {
611 System.out.println(" Dir: " + subFileName.getName());
612 }
613 }
614 }
615 }
616 }
617
618 }
619
620 class LauncherSecurityManager extends SecurityManager {
621 private static boolean exitInvoked;
622 private static int exitCode;
623 private SecurityManager securityManager;
624
625 public LauncherSecurityManager() {
626 reset();
627 securityManager = System.getSecurityManager();
628 System.setSecurityManager(this);
629 }
630
631 @Override
632 public void checkPermission(Permission perm, Object context) {
633 if (securityManager != null) {
634 // check everything with the original SecurityManager
635 securityManager.checkPermission(perm, context);
636 }
637 }
638
639 @Override
640 public void checkPermission(Permission perm) {
641 if (securityManager != null) {
642 // check everything with the original SecurityManager
643 securityManager.checkPermission(perm);
644 }
645 }
646
647 @Override
648 public void checkExit(int status) throws SecurityException {
649 exitInvoked = true;
650 exitCode = status;
651 throw new SecurityException("Intercepted System.exit(" + status + ")");
652 }
653
654 public static boolean getExitInvoked() {
655 return exitInvoked;
656 }
657
658 public static int getExitCode() {
659 return exitCode;
660 }
661
662 public static void reset() {
663 exitInvoked = false;
664 exitCode = 0;
665 }
666 }
667
668 class LauncherException extends Exception {
669 }