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