1 /* 2 * Copyright (c) 2002, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.tools.javah; 27 28 import java.io.File; 29 import java.io.FileNotFoundException; 30 import java.io.IOException; 31 import java.io.OutputStream; 32 import java.io.PrintWriter; 33 import java.io.Writer; 34 import java.nio.file.NoSuchFileException; 35 import java.text.MessageFormat; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Collections; 39 import java.util.HashMap; 40 import java.util.Iterator; 41 import java.util.LinkedHashSet; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.MissingResourceException; 46 import java.util.Objects; 47 import java.util.ResourceBundle; 48 import java.util.Set; 49 50 import javax.annotation.processing.AbstractProcessor; 51 import javax.annotation.processing.Messager; 52 import javax.annotation.processing.ProcessingEnvironment; 53 import javax.annotation.processing.RoundEnvironment; 54 import javax.annotation.processing.SupportedAnnotationTypes; 55 import javax.lang.model.SourceVersion; 56 import javax.lang.model.element.ExecutableElement; 57 import javax.lang.model.element.TypeElement; 58 import javax.lang.model.element.VariableElement; 59 import javax.lang.model.type.ArrayType; 60 import javax.lang.model.type.DeclaredType; 61 import javax.lang.model.type.TypeMirror; 62 import javax.lang.model.type.TypeVisitor; 63 import javax.lang.model.util.ElementFilter; 64 import javax.lang.model.util.SimpleTypeVisitor9; 65 import javax.lang.model.util.Types; 66 import javax.tools.Diagnostic; 67 import javax.tools.DiagnosticListener; 68 import javax.tools.JavaCompiler; 69 import javax.tools.JavaCompiler.CompilationTask; 70 import javax.tools.JavaFileManager; 71 import javax.tools.JavaFileObject; 72 import javax.tools.StandardJavaFileManager; 73 import javax.tools.StandardLocation; 74 import javax.tools.ToolProvider; 75 76 import com.sun.tools.javac.code.Symbol.CompletionFailure; 77 import com.sun.tools.javac.main.CommandLine; 78 import com.sun.tools.javac.util.DefinedBy; 79 import com.sun.tools.javac.util.DefinedBy.Api; 80 81 import static javax.tools.Diagnostic.Kind.*; 82 83 84 /** 85 * Javah generates support files for native methods. 86 * Parse commandline options and invokes javadoc to execute those commands. 87 * 88 * <p><b>This is NOT part of any supported API. 89 * If you write code that depends on this, you do so at your own 90 * risk. This code and its internal interfaces are subject to change 91 * or deletion without notice.</b></p> 92 * 93 * @author Sucheta Dambalkar 94 * @author Jonathan Gibbons 95 */ 96 public class JavahTask implements NativeHeaderTool.NativeHeaderTask { 97 public class BadArgs extends Exception { 98 private static final long serialVersionUID = 1479361270874789045L; 99 BadArgs(String key, Object... args) { 100 super(JavahTask.this.getMessage(key, args)); 101 this.key = key; 102 this.args = args; 103 } 104 105 BadArgs showUsage(boolean b) { 106 showUsage = b; 107 return this; 108 } 109 110 final String key; 111 final Object[] args; 112 boolean showUsage; 113 } 114 115 static abstract class Option { 116 Option(boolean hasArg, String... aliases) { 117 this.hasArg = hasArg; 118 this.aliases = aliases; 119 } 120 121 boolean isHidden() { 122 return false; 123 } 124 125 boolean matches(String opt) { 126 for (String a: aliases) { 127 if (a.equals(opt)) 128 return true; 129 } 130 return false; 131 } 132 133 boolean ignoreRest() { 134 return false; 135 } 136 137 abstract void process(JavahTask task, String opt, String arg) throws BadArgs; 138 139 final boolean hasArg; 140 final String[] aliases; 141 } 142 143 static abstract class HiddenOption extends Option { 144 HiddenOption(boolean hasArg, String... aliases) { 145 super(hasArg, aliases); 146 } 147 148 @Override 149 boolean isHidden() { 150 return true; 151 } 152 } 153 154 static final Option[] recognizedOptions = { 155 new Option(true, "-o") { 156 void process(JavahTask task, String opt, String arg) { 157 task.ofile = new File(arg); 158 } 159 }, 160 161 new Option(true, "-d") { 162 void process(JavahTask task, String opt, String arg) { 163 task.odir = new File(arg); 164 } 165 }, 166 167 new HiddenOption(true, "-td") { 168 void process(JavahTask task, String opt, String arg) { 169 // ignored; for backwards compatibility 170 } 171 }, 172 173 new Option(false, "-v", "-verbose") { 174 void process(JavahTask task, String opt, String arg) { 175 task.verbose = true; 176 } 177 }, 178 179 new Option(false, "-h", "-help", "--help", "-?") { 180 void process(JavahTask task, String opt, String arg) { 181 task.help = true; 182 } 183 }, 184 185 new HiddenOption(false, "-trace") { 186 void process(JavahTask task, String opt, String arg) { 187 task.trace = true; 188 } 189 }, 190 191 new Option(false, "-version") { 192 void process(JavahTask task, String opt, String arg) { 193 task.version = true; 194 } 195 }, 196 197 new HiddenOption(false, "-fullversion") { 198 void process(JavahTask task, String opt, String arg) { 199 task.fullVersion = true; 200 } 201 }, 202 203 new Option(false, "-jni") { 204 void process(JavahTask task, String opt, String arg) { 205 task.jni = true; 206 } 207 }, 208 209 new Option(false, "-force") { 210 void process(JavahTask task, String opt, String arg) { 211 task.force = true; 212 } 213 }, 214 215 new HiddenOption(false, "-Xnew") { 216 void process(JavahTask task, String opt, String arg) { 217 // we're already using the new javah 218 } 219 }, 220 221 new HiddenOption(false, "-llni", "-Xllni") { 222 void process(JavahTask task, String opt, String arg) { 223 task.llni = true; 224 } 225 }, 226 227 new HiddenOption(false, "-llnidouble") { 228 void process(JavahTask task, String opt, String arg) { 229 task.llni = true; 230 task.doubleAlign = true; 231 } 232 }, 233 234 new HiddenOption(false) { 235 boolean matches(String opt) { 236 return opt.startsWith("-XD"); 237 } 238 void process(JavahTask task, String opt, String arg) { 239 task.javac_extras.add(opt); 240 } 241 }, 242 }; 243 244 JavahTask() { 245 } 246 247 JavahTask(Writer out, 248 JavaFileManager fileManager, 249 DiagnosticListener<? super JavaFileObject> diagnosticListener, 250 Iterable<String> options, 251 Iterable<String> classes) { 252 this(); 253 this.log = getPrintWriterForWriter(out); 254 this.fileManager = fileManager; 255 this.diagnosticListener = diagnosticListener; 256 257 try { 258 handleOptions(options, false); 259 } catch (BadArgs e) { 260 throw new IllegalArgumentException(e.getMessage()); 261 } 262 263 this.classes = new ArrayList<>(); 264 if (classes != null) { 265 for (String classname: classes) { 266 Objects.requireNonNull(classname); 267 this.classes.add(classname); 268 } 269 } 270 } 271 272 public void setLocale(Locale locale) { 273 if (locale == null) 274 locale = Locale.getDefault(); 275 task_locale = locale; 276 } 277 278 public void setLog(PrintWriter log) { 279 this.log = log; 280 } 281 282 public void setLog(OutputStream s) { 283 setLog(getPrintWriterForStream(s)); 284 } 285 286 static PrintWriter getPrintWriterForStream(OutputStream s) { 287 return new PrintWriter(s, true); 288 } 289 290 static PrintWriter getPrintWriterForWriter(Writer w) { 291 if (w == null) 292 return getPrintWriterForStream(null); 293 else if (w instanceof PrintWriter) 294 return (PrintWriter) w; 295 else 296 return new PrintWriter(w, true); 297 } 298 299 public void setDiagnosticListener(DiagnosticListener<? super JavaFileObject> dl) { 300 diagnosticListener = dl; 301 } 302 303 public void setDiagnosticListener(OutputStream s) { 304 setDiagnosticListener(getDiagnosticListenerForStream(s)); 305 } 306 307 private DiagnosticListener<JavaFileObject> getDiagnosticListenerForStream(OutputStream s) { 308 return getDiagnosticListenerForWriter(getPrintWriterForStream(s)); 309 } 310 311 private DiagnosticListener<JavaFileObject> getDiagnosticListenerForWriter(Writer w) { 312 final PrintWriter pw = getPrintWriterForWriter(w); 313 return diagnostic -> { 314 if (diagnostic.getKind() == Diagnostic.Kind.ERROR) { 315 pw.print(getMessage("err.prefix")); 316 pw.print(" "); 317 } 318 pw.println(diagnostic.getMessage(null)); 319 }; 320 } 321 322 int run(String[] args) { 323 try { 324 handleOptions(args); 325 boolean ok = run(); 326 return ok ? 0 : 1; 327 } catch (BadArgs e) { 328 diagnosticListener.report(createDiagnostic(e.key, e.args)); 329 return 1; 330 } catch (InternalError e) { 331 diagnosticListener.report(createDiagnostic("err.internal.error", e.getMessage())); 332 return 1; 333 } catch (Util.Exit e) { 334 return e.exitValue; 335 } finally { 336 log.flush(); 337 } 338 } 339 340 public void handleOptions(String[] args) throws BadArgs { 341 handleOptions(Arrays.asList(args), true); 342 } 343 344 private void handleOptions(Iterable<String> args, boolean allowClasses) throws BadArgs { 345 if (log == null) { 346 log = getPrintWriterForStream(System.out); 347 if (diagnosticListener == null) 348 diagnosticListener = getDiagnosticListenerForStream(System.err); 349 } else { 350 if (diagnosticListener == null) 351 diagnosticListener = getDiagnosticListenerForWriter(log); 352 } 353 354 if (fileManager == null) 355 fileManager = getDefaultFileManager(diagnosticListener, log); 356 357 Iterator<String> iter = expandAtArgs(args).iterator(); 358 noArgs = !iter.hasNext(); 359 360 while (iter.hasNext()) { 361 String arg = iter.next(); 362 if (arg.startsWith("-")) 363 handleOption(arg, iter); 364 else if (allowClasses) { 365 if (classes == null) 366 classes = new ArrayList<>(); 367 classes.add(arg); 368 while (iter.hasNext()) 369 classes.add(iter.next()); 370 } else 371 throw new BadArgs("err.unknown.option", arg).showUsage(true); 372 } 373 374 if ((classes == null || classes.size() == 0) && 375 !(noArgs || help || version || fullVersion)) { 376 throw new BadArgs("err.no.classes.specified"); 377 } 378 379 if (jni && llni) 380 throw new BadArgs("jni.llni.mixed"); 381 382 if (odir != null && ofile != null) 383 throw new BadArgs("dir.file.mixed"); 384 } 385 386 private void handleOption(String name, Iterator<String> rest) throws BadArgs { 387 for (Option o: recognizedOptions) { 388 if (o.matches(name)) { 389 if (o.hasArg) { 390 if (rest.hasNext()) 391 o.process(this, name, rest.next()); 392 else 393 throw new BadArgs("err.missing.arg", name).showUsage(true); 394 } else 395 o.process(this, name, null); 396 397 if (o.ignoreRest()) { 398 while (rest.hasNext()) 399 rest.next(); 400 } 401 return; 402 } 403 } 404 405 if (fileManager.handleOption(name, rest)) 406 return; 407 408 throw new BadArgs("err.unknown.option", name).showUsage(true); 409 } 410 411 private Iterable<String> expandAtArgs(Iterable<String> args) throws BadArgs { 412 try { 413 List<String> l = new ArrayList<>(); 414 for (String arg: args) l.add(arg); 415 return Arrays.asList(CommandLine.parse(l.toArray(new String[l.size()]))); 416 } catch (FileNotFoundException | NoSuchFileException e) { 417 throw new BadArgs("at.args.file.not.found", e.getLocalizedMessage()); 418 } catch (IOException e) { 419 throw new BadArgs("at.args.io.exception", e.getLocalizedMessage()); 420 } 421 } 422 423 public Boolean call() { 424 return run(); 425 } 426 427 public boolean run() throws Util.Exit { 428 429 if (!javac_extras.contains("-XDsuppress-tool-removal-message")) { 430 log.println(getMessage("javah.misc.Deprecation")); 431 } 432 433 Util util = new Util(log, diagnosticListener); 434 435 if (noArgs || help) { 436 showHelp(); 437 return help; // treat noArgs as an error for purposes of exit code 438 } 439 440 if (version || fullVersion) { 441 showVersion(fullVersion); 442 return true; 443 } 444 445 util.verbose = verbose; 446 447 Gen g; 448 449 if (llni) 450 g = new LLNI(doubleAlign, util); 451 else { 452 g = new JNI(util); 453 } 454 455 if (ofile != null) { 456 if (!(fileManager instanceof StandardJavaFileManager)) { 457 diagnosticListener.report(createDiagnostic("err.cant.use.option.for.fm", "-o")); 458 return false; 459 } 460 Iterable<? extends JavaFileObject> iter = 461 ((StandardJavaFileManager) fileManager).getJavaFileObjectsFromFiles(Collections.singleton(ofile)); 462 JavaFileObject fo = iter.iterator().next(); 463 g.setOutFile(fo); 464 } else { 465 if (odir != null) { 466 if (!(fileManager instanceof StandardJavaFileManager)) { 467 diagnosticListener.report(createDiagnostic("err.cant.use.option.for.fm", "-d")); 468 return false; 469 } 470 471 if (!odir.exists()) 472 if (!odir.mkdirs()) 473 util.error("cant.create.dir", odir.toString()); 474 try { 475 ((StandardJavaFileManager) fileManager).setLocation(StandardLocation.CLASS_OUTPUT, Collections.singleton(odir)); 476 } catch (IOException e) { 477 Object msg = e.getLocalizedMessage(); 478 if (msg == null) { 479 msg = e; 480 } 481 diagnosticListener.report(createDiagnostic("err.ioerror", odir, msg)); 482 return false; 483 } 484 } 485 g.setFileManager(fileManager); 486 } 487 488 /* 489 * Force set to false will turn off smarts about checking file 490 * content before writing. 491 */ 492 g.setForce(force); 493 494 if (fileManager instanceof JavahFileManager) 495 ((JavahFileManager) fileManager).setSymbolFileEnabled(false); 496 497 JavaCompiler c = ToolProvider.getSystemJavaCompiler(); 498 List<String> opts = new ArrayList<>(); 499 opts.add("-proc:only"); 500 opts.addAll(javac_extras); 501 502 CompilationTask t; 503 try { 504 t = c.getTask(log, fileManager, diagnosticListener, opts, classes, null); 505 } catch (IllegalArgumentException e) { 506 util.error("bad.arg", e.getMessage()); 507 return false; 508 } 509 510 JavahProcessor p = new JavahProcessor(g); 511 t.setProcessors(Collections.singleton(p)); 512 513 boolean ok = t.call(); 514 if (p.exit != null) 515 throw new Util.Exit(p.exit); 516 return ok; 517 518 } 519 520 static StandardJavaFileManager getDefaultFileManager(final DiagnosticListener<? super JavaFileObject> dl, PrintWriter log) { 521 return JavahFileManager.create(dl, log); 522 } 523 524 private void showHelp() { 525 log.println(getMessage("main.usage", progname)); 526 527 for (Option o: recognizedOptions) { 528 if (o.isHidden()) 529 continue; 530 String name = o.aliases[0].substring(1); // there must always be at least one name 531 log.println(getMessage("main.opt." + name)); 532 } 533 534 String[] fmOptions = { 535 "--module-path", "--system", 536 "--class-path", "-classpath", "-cp", 537 "-bootclasspath" 538 }; 539 540 for (String o: fmOptions) { 541 if (fileManager.isSupportedOption(o) == -1) 542 continue; 543 String name = o.replaceAll("^-+", "").replaceAll("-+", "_"); 544 log.println(getMessage("main.opt." + name)); 545 } 546 547 log.println(getMessage("main.usage.foot")); 548 } 549 550 private void showVersion(boolean full) { 551 log.println(version(full)); 552 } 553 554 private static final String versionRBName = "com.sun.tools.javah.resources.version"; 555 private static ResourceBundle versionRB; 556 557 private String version(boolean full) { 558 String msgKey = (full ? "javah.fullVersion" : "javah.version"); 559 String versionKey = (full ? "full" : "release"); 560 // versionKey=product: mm.nn.oo[-milestone] 561 // versionKey=full: mm.mm.oo[-milestone]-build 562 if (versionRB == null) { 563 try { 564 versionRB = ResourceBundle.getBundle(versionRBName); 565 } catch (MissingResourceException e) { 566 return getMessage("version.resource.missing", System.getProperty("java.version")); 567 } 568 } 569 try { 570 return getMessage(msgKey, "javah", versionRB.getString(versionKey)); 571 } 572 catch (MissingResourceException e) { 573 return getMessage("version.unknown", System.getProperty("java.version")); 574 } 575 } 576 577 private Diagnostic<JavaFileObject> createDiagnostic(final String key, final Object... args) { 578 return new Diagnostic<JavaFileObject>() { 579 @DefinedBy(Api.COMPILER) 580 public Kind getKind() { 581 return Diagnostic.Kind.ERROR; 582 } 583 584 @DefinedBy(Api.COMPILER) 585 public JavaFileObject getSource() { 586 return null; 587 } 588 589 @DefinedBy(Api.COMPILER) 590 public long getPosition() { 591 return Diagnostic.NOPOS; 592 } 593 594 @DefinedBy(Api.COMPILER) 595 public long getStartPosition() { 596 return Diagnostic.NOPOS; 597 } 598 599 @DefinedBy(Api.COMPILER) 600 public long getEndPosition() { 601 return Diagnostic.NOPOS; 602 } 603 604 @DefinedBy(Api.COMPILER) 605 public long getLineNumber() { 606 return Diagnostic.NOPOS; 607 } 608 609 @DefinedBy(Api.COMPILER) 610 public long getColumnNumber() { 611 return Diagnostic.NOPOS; 612 } 613 614 @DefinedBy(Api.COMPILER) 615 public String getCode() { 616 return key; 617 } 618 619 @DefinedBy(Api.COMPILER) 620 public String getMessage(Locale locale) { 621 return JavahTask.this.getMessage(locale, key, args); 622 } 623 624 }; 625 } 626 627 private String getMessage(String key, Object... args) { 628 return getMessage(task_locale, key, args); 629 } 630 631 private String getMessage(Locale locale, String key, Object... args) { 632 if (bundles == null) { 633 // could make this a HashMap<Locale,SoftReference<ResourceBundle>> 634 // and for efficiency, keep a hard reference to the bundle for the task 635 // locale 636 bundles = new HashMap<>(); 637 } 638 639 if (locale == null) 640 locale = Locale.getDefault(); 641 642 ResourceBundle b = bundles.get(locale); 643 if (b == null) { 644 try { 645 b = ResourceBundle.getBundle("com.sun.tools.javah.resources.l10n", locale); 646 bundles.put(locale, b); 647 } catch (MissingResourceException e) { 648 throw new InternalError("Cannot find javah resource bundle for locale " + locale, e); 649 } 650 } 651 652 try { 653 return MessageFormat.format(b.getString(key), args); 654 } catch (MissingResourceException e) { 655 return key; 656 //throw new InternalError(e, key); 657 } 658 } 659 660 File ofile; 661 File odir; 662 String bootcp; 663 String usercp; 664 List<String> classes; 665 boolean verbose; 666 boolean noArgs; 667 boolean help; 668 boolean trace; 669 boolean version; 670 boolean fullVersion; 671 boolean jni; 672 boolean llni; 673 boolean doubleAlign; 674 boolean force; 675 Set<String> javac_extras = new LinkedHashSet<>(); 676 677 PrintWriter log; 678 JavaFileManager fileManager; 679 DiagnosticListener<? super JavaFileObject> diagnosticListener; 680 Locale task_locale; 681 Map<Locale, ResourceBundle> bundles; 682 683 private static final String progname = "javah"; 684 685 @SupportedAnnotationTypes("*") 686 class JavahProcessor extends AbstractProcessor { 687 private Messager messager; 688 689 JavahProcessor(Gen g) { 690 this.g = g; 691 } 692 693 @Override @DefinedBy(Api.ANNOTATION_PROCESSING) 694 public SourceVersion getSupportedSourceVersion() { 695 // since this is co-bundled with javac, we can assume it supports 696 // the latest source version 697 return SourceVersion.latest(); 698 } 699 700 @Override @DefinedBy(Api.ANNOTATION_PROCESSING) 701 public void init(ProcessingEnvironment pEnv) { 702 super.init(pEnv); 703 messager = processingEnv.getMessager(); 704 } 705 706 @DefinedBy(Api.ANNOTATION_PROCESSING) 707 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { 708 try { 709 Set<TypeElement> classes = getAllClasses(ElementFilter.typesIn(roundEnv.getRootElements())); 710 if (classes.size() > 0) { 711 checkMethodParameters(classes); 712 g.setProcessingEnvironment(processingEnv); 713 g.setClasses(classes); 714 g.run(); 715 } 716 } catch (CompletionFailure cf) { 717 messager.printMessage(ERROR, getMessage("class.not.found", cf.sym.getQualifiedName().toString())); 718 } catch (ClassNotFoundException cnfe) { 719 messager.printMessage(ERROR, getMessage("class.not.found", cnfe.getMessage())); 720 } catch (IOException ioe) { 721 messager.printMessage(ERROR, getMessage("io.exception", ioe.getMessage())); 722 } catch (Util.Exit e) { 723 exit = e; 724 } 725 726 return true; 727 } 728 729 private Set<TypeElement> getAllClasses(Set<? extends TypeElement> classes) { 730 Set<TypeElement> allClasses = new LinkedHashSet<>(); 731 getAllClasses0(classes, allClasses); 732 return allClasses; 733 } 734 735 private void getAllClasses0(Iterable<? extends TypeElement> classes, Set<TypeElement> allClasses) { 736 for (TypeElement c: classes) { 737 allClasses.add(c); 738 getAllClasses0(ElementFilter.typesIn(c.getEnclosedElements()), allClasses); 739 } 740 } 741 742 // 4942232: 743 // check that classes exist for all the parameters of native methods 744 private void checkMethodParameters(Set<TypeElement> classes) { 745 Types types = processingEnv.getTypeUtils(); 746 for (TypeElement te: classes) { 747 for (ExecutableElement ee: ElementFilter.methodsIn(te.getEnclosedElements())) { 748 for (VariableElement ve: ee.getParameters()) { 749 TypeMirror tm = ve.asType(); 750 checkMethodParametersVisitor.visit(tm, types); 751 } 752 } 753 } 754 } 755 756 private TypeVisitor<Void,Types> checkMethodParametersVisitor = 757 new SimpleTypeVisitor9<Void,Types>() { 758 @Override @DefinedBy(Api.LANGUAGE_MODEL) 759 public Void visitArray(ArrayType t, Types types) { 760 visit(t.getComponentType(), types); 761 return null; 762 } 763 @Override @DefinedBy(Api.LANGUAGE_MODEL) 764 public Void visitDeclared(DeclaredType t, Types types) { 765 t.asElement().getKind(); // ensure class exists 766 for (TypeMirror st: types.directSupertypes(t)) 767 visit(st, types); 768 return null; 769 } 770 }; 771 772 private Gen g; 773 private Util.Exit exit; 774 } 775 }