1 /* 2 * Copyright (c) 2016, 2018, 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 jdk.jfr.internal; 27 28 import static jdk.jfr.internal.LogLevel.DEBUG; 29 import static jdk.jfr.internal.LogLevel.WARN; 30 import static jdk.jfr.internal.LogTag.JFR; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.nio.channels.FileChannel; 35 import java.nio.file.StandardOpenOption; 36 import java.security.AccessControlContext; 37 import java.security.AccessController; 38 import java.time.Duration; 39 import java.time.Instant; 40 import java.time.LocalDateTime; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Date; 44 import java.util.LinkedHashMap; 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.StringJoiner; 49 import java.util.TimerTask; 50 import java.util.TreeMap; 51 52 import jdk.jfr.Configuration; 53 import jdk.jfr.FlightRecorderListener; 54 import jdk.jfr.Recording; 55 import jdk.jfr.RecordingState; 56 import jdk.jfr.internal.SecuritySupport.SafePath; 57 58 public final class PlatformRecording implements AutoCloseable { 59 60 private final PlatformRecorder recorder; 61 private final long id; 62 // Recording settings 63 private Map<String, String> settings = new LinkedHashMap<>(); 64 private Duration duration; 65 private Duration maxAge; 66 private long maxSize; 67 68 private WriteableUserPath destination; 69 70 private boolean toDisk = true; 71 private String name; 72 private boolean dumpOnExit; 73 private SafePath dumpOnExitDirectory = new SafePath("."); 74 // Timestamp information 75 private Instant stopTime; 76 private Instant startTime; 77 78 // Misc, information 79 private RecordingState state = RecordingState.NEW; 80 private long size; 81 private final LinkedList<RepositoryChunk> chunks = new LinkedList<>(); 82 private volatile Recording recording; 83 private TimerTask stopTask; 84 private TimerTask startTask; 85 private AccessControlContext noDestinationDumpOnExitAccessControlContext; 86 private boolean shuoldWriteActiveRecordingEvent = true; 87 PlatformRecording(PlatformRecorder recorder, long id)88 PlatformRecording(PlatformRecorder recorder, long id) { 89 // Typically the access control context is taken 90 // when you call dump(Path) or setDdestination(Path), 91 // but if no destination is set and dumponexit=true 92 // the control context of the recording is taken when the 93 // Recording object is constructed. This works well for 94 // -XX:StartFlightRecording and JFR.dump 95 this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext(); 96 this.id = id; 97 this.recorder = recorder; 98 this.name = String.valueOf(id); 99 } 100 start()101 public void start() { 102 RecordingState oldState; 103 RecordingState newState; 104 synchronized (recorder) { 105 oldState = getState(); 106 if (!Utils.isBefore(state, RecordingState.RUNNING)) { 107 throw new IllegalStateException("Recording can only be started once."); 108 } 109 if (startTask != null) { 110 startTask.cancel(); 111 startTask = null; 112 startTime = null; 113 } 114 recorder.start(this); 115 Logger.log(LogTag.JFR, LogLevel.INFO, () -> { 116 // Only print non-default values so it easy to see 117 // which options were added 118 StringJoiner options = new StringJoiner(", "); 119 if (!toDisk) { 120 options.add("disk=false"); 121 } 122 if (maxAge != null) { 123 options.add("maxage=" + Utils.formatTimespan(maxAge, "")); 124 } 125 if (maxSize != 0) { 126 options.add("maxsize=" + Utils.formatBytesCompact(maxSize)); 127 } 128 if (dumpOnExit) { 129 options.add("dumponexit=true"); 130 } 131 if (duration != null) { 132 options.add("duration=" + Utils.formatTimespan(duration, "")); 133 } 134 if (destination != null) { 135 options.add("filename=" + destination.getRealPathText()); 136 } 137 String optionText = options.toString(); 138 if (optionText.length() != 0) { 139 optionText = "{" + optionText + "}"; 140 } 141 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText; 142 }); 143 newState = getState(); 144 } 145 notifyIfStateChanged(oldState, newState); 146 } 147 stop(String reason)148 public boolean stop(String reason) { 149 RecordingState oldState; 150 RecordingState newState; 151 synchronized (recorder) { 152 oldState = getState(); 153 if (stopTask != null) { 154 stopTask.cancel(); 155 stopTask = null; 156 } 157 recorder.stop(this); 158 String endText = reason == null ? "" : ". Reason \"" + reason + "\"."; 159 Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId() + ")" + endText); 160 this.stopTime = Instant.now(); 161 newState = getState(); 162 } 163 WriteableUserPath dest = getDestination(); 164 165 if (dest != null) { 166 try { 167 dumpStopped(dest); 168 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getRealPathText()); 169 notifyIfStateChanged(newState, oldState); 170 close(); // remove if copied out 171 } catch(IOException e) { 172 // throw e; // BUG8925030 173 } 174 } else { 175 notifyIfStateChanged(newState, oldState); 176 } 177 return true; 178 } 179 scheduleStart(Duration delay)180 public void scheduleStart(Duration delay) { 181 synchronized (recorder) { 182 ensureOkForSchedule(); 183 184 startTime = Instant.now().plus(delay); 185 LocalDateTime now = LocalDateTime.now().plus(delay); 186 setState(RecordingState.DELAYED); 187 startTask = createStartTask(); 188 recorder.getTimer().schedule(startTask, delay.toMillis()); 189 Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + getName() + "\" (" + getId() + ") to start at " + now); 190 } 191 } 192 ensureOkForSchedule()193 private void ensureOkForSchedule() { 194 if (getState() != RecordingState.NEW) { 195 throw new IllegalStateException("Only a new recoridng can be scheduled for start"); 196 } 197 } 198 createStartTask()199 private TimerTask createStartTask() { 200 // Taking ref. to recording here. 201 // Opens up for memory leaks. 202 return new TimerTask() { 203 @Override 204 public void run() { 205 synchronized (recorder) { 206 if (getState() != RecordingState.DELAYED) { 207 return; 208 } 209 start(); 210 } 211 } 212 }; 213 } 214 215 void scheduleStart(Instant startTime) { 216 synchronized (recorder) { 217 ensureOkForSchedule(); 218 this.startTime = startTime; 219 setState(RecordingState.DELAYED); 220 startTask = createStartTask(); 221 recorder.getTimer().schedule(startTask, startTime.toEpochMilli()); 222 } 223 } 224 225 public Map<String, String> getSettings() { 226 synchronized (recorder) { 227 return settings; 228 } 229 } 230 231 public long getSize() { 232 return size; 233 } 234 235 public Instant getStopTime() { 236 synchronized (recorder) { 237 return stopTime; 238 } 239 } 240 241 public Instant getStartTime() { 242 synchronized (recorder) { 243 return startTime; 244 } 245 } 246 247 public Long getMaxSize() { 248 synchronized (recorder) { 249 return maxSize; 250 } 251 } 252 253 public Duration getMaxAge() { 254 synchronized (recorder) { 255 return maxAge; 256 } 257 } 258 259 public String getName() { 260 synchronized (recorder) { 261 return name; 262 } 263 } 264 265 public RecordingState getState() { 266 synchronized (recorder) { 267 return state; 268 } 269 } 270 271 @Override 272 public void close() { 273 RecordingState oldState; 274 RecordingState newState; 275 276 synchronized (recorder) { 277 oldState = getState(); 278 if (RecordingState.CLOSED != getState()) { 279 if (startTask != null) { 280 startTask.cancel(); 281 startTask = null; 282 } 283 recorder.finish(this); 284 for (RepositoryChunk c : chunks) { 285 removed(c); 286 } 287 chunks.clear(); 288 setState(RecordingState.CLOSED); 289 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId() + ")"); 290 } 291 newState = getState(); 292 } 293 notifyIfStateChanged(newState, oldState); 294 } 295 296 // To be used internally when doing dumps. 297 // Caller must have recorder lock and close recording before releasing lock 298 public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException { 299 if(!Thread.holdsLock(recorder)) { 300 throw new InternalError("Caller must have recorder lock"); 301 } 302 RecordingState state = getState(); 303 if (state == RecordingState.CLOSED) { 304 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write"); 305 } 306 if (state == RecordingState.DELAYED || state == RecordingState.NEW) { 307 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write"); 308 } 309 if (state == RecordingState.STOPPED) { 310 PlatformRecording clone = recorder.newTemporaryRecording(); 311 for (RepositoryChunk r : chunks) { 312 clone.add(r); 313 } 314 return clone; 315 } 316 317 // Recording is RUNNING, create a clone 318 PlatformRecording clone = recorder.newTemporaryRecording(); 319 clone.setShouldWriteActiveRecordingEvent(false); 320 clone.setName(getName()); 321 clone.setToDisk(true); 322 // We purposely don't clone settings here, since 323 // a union a == a 324 if (!isToDisk()) { 325 // force memory contents to disk 326 clone.start(); 327 } else { 328 // using existing chunks on disk 329 for (RepositoryChunk c : chunks) { 330 clone.add(c); 331 } 332 clone.setState(RecordingState.RUNNING); 333 clone.setStartTime(getStartTime()); 334 } 335 if (pathToGcRoots == null) { 336 clone.setSettings(getSettings()); // needed for old object sample 337 clone.stop(reason); // dumps to destination path here 338 } else { 339 // Risk of violating lock order here, since 340 // clone.stop() will take recorder lock inside 341 // metadata lock, but OK if we already 342 // have recorder lock when we entered metadata lock 343 synchronized (MetadataRepository.getInstance()) { 344 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots)); 345 clone.stop(reason); 346 } 347 } 348 return clone; 349 } 350 351 public boolean isToDisk() { 352 synchronized (recorder) { 353 return toDisk; 354 } 355 } 356 357 public void setMaxSize(long maxSize) { 358 synchronized (recorder) { 359 if (getState() == RecordingState.CLOSED) { 360 throw new IllegalStateException("Can't set max age when recording is closed"); 361 } 362 this.maxSize = maxSize; 363 trimToSize(); 364 } 365 } 366 367 public void setDestination(WriteableUserPath userSuppliedPath) throws IOException { 368 synchronized (recorder) { 369 checkSetDestination(userSuppliedPath); 370 this.destination = userSuppliedPath; 371 } 372 } 373 374 public void checkSetDestination(WriteableUserPath userSuppliedPath) throws IOException { 375 synchronized (recorder) { 376 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 377 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed"); 378 } 379 } 380 } 381 382 public WriteableUserPath getDestination() { 383 synchronized (recorder) { 384 return destination; 385 } 386 } 387 388 void setState(RecordingState state) { 389 synchronized (recorder) { 390 this.state = state; 391 } 392 } 393 394 void setStartTime(Instant startTime) { 395 synchronized (recorder) { 396 this.startTime = startTime; 397 } 398 } 399 400 void setStopTime(Instant timeStamp) { 401 synchronized (recorder) { 402 stopTime = timeStamp; 403 } 404 } 405 406 public long getId() { 407 synchronized (recorder) { 408 return id; 409 } 410 } 411 412 public void setName(String name) { 413 synchronized (recorder) { 414 ensureNotClosed(); 415 this.name = name; 416 } 417 } 418 419 private void ensureNotClosed() { 420 if (getState() == RecordingState.CLOSED) { 421 throw new IllegalStateException("Can't change name on a closed recording"); 422 } 423 } 424 425 public void setDumpOnExit(boolean dumpOnExit) { 426 synchronized (recorder) { 427 this.dumpOnExit = dumpOnExit; 428 } 429 } 430 431 public boolean getDumpOnExit() { 432 synchronized (recorder) { 433 return dumpOnExit; 434 } 435 } 436 437 public void setToDisk(boolean toDisk) { 438 synchronized (recorder) { 439 if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) { 440 this.toDisk = toDisk; 441 } else { 442 throw new IllegalStateException("Recording option disk can't be changed after recording has started"); 443 } 444 } 445 } 446 447 public void setSetting(String id, String value) { 448 synchronized (recorder) { 449 this.settings.put(id, value); 450 if (getState() == RecordingState.RUNNING) { 451 recorder.updateSettings(); 452 } 453 } 454 } 455 456 public void setSettings(Map<String, String> settings) { 457 setSettings(settings, true); 458 } 459 460 private void setSettings(Map<String, String> settings, boolean update) { 461 if (Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO) && update) { 462 TreeMap<String, String> ordered = new TreeMap<>(settings); 463 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); 464 for (Map.Entry<String, String> entry : ordered.entrySet()) { 465 String text = entry.getKey() + "=\"" + entry.getValue() + "\""; 466 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); 467 } 468 } 469 synchronized (recorder) { 470 this.settings = new LinkedHashMap<>(settings); 471 if (getState() == RecordingState.RUNNING && update) { 472 recorder.updateSettings(); 473 } 474 } 475 } 476 477 private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { 478 if (oldState == newState) { 479 return; 480 } 481 for (FlightRecorderListener cl : PlatformRecorder.getListeners()) { 482 try { 483 // Skip internal recordings 484 if (recording != null) { 485 cl.recordingStateChanged(recording); 486 } 487 } catch (RuntimeException re) { 488 Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage()); 489 } 490 } 491 } 492 493 public void setRecording(Recording recording) { 494 this.recording = recording; 495 } 496 497 public Recording getRecording() { 498 return recording; 499 } 500 501 @Override 502 public String toString() { 503 return getName() + " (id=" + getId() + ") " + getState(); 504 } 505 506 public void setConfiguration(Configuration c) { 507 setSettings(c.getSettings()); 508 } 509 510 public void setMaxAge(Duration maxAge) { 511 synchronized (recorder) { 512 if (getState() == RecordingState.CLOSED) { 513 throw new IllegalStateException("Can't set max age when recording is closed"); 514 } 515 this.maxAge = maxAge; 516 if (maxAge != null) { 517 trimToAge(Instant.now().minus(maxAge)); 518 } 519 } 520 } 521 522 void appendChunk(RepositoryChunk chunk) { 523 if (!chunk.isFinished()) { 524 throw new Error("not finished chunk " + chunk.getStartTime()); 525 } 526 synchronized (recorder) { 527 if (!toDisk) { 528 return; 529 } 530 if (maxAge != null) { 531 trimToAge(chunk.getEndTime().minus(maxAge)); 532 } 533 chunks.addLast(chunk); 534 added(chunk); 535 trimToSize(); 536 } 537 } 538 539 private void trimToSize() { 540 if (maxSize == 0) { 541 return; 542 } 543 while (size > maxSize && chunks.size() > 1) { 544 RepositoryChunk c = chunks.removeFirst(); 545 removed(c); 546 } 547 } 548 549 private void trimToAge(Instant oldest) { 550 while (!chunks.isEmpty()) { 551 RepositoryChunk oldestChunk = chunks.peek(); 552 if (oldestChunk.getEndTime().isAfter(oldest)) { 553 return; 554 } 555 chunks.removeFirst(); 556 removed(oldestChunk); 557 } 558 } 559 560 void add(RepositoryChunk c) { 561 chunks.add(c); 562 added(c); 563 } 564 565 private void added(RepositoryChunk c) { 566 c.use(); 567 size += c.getSize(); 568 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size); 569 } 570 571 private void removed(RepositoryChunk c) { 572 size -= c.getSize(); 573 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size); 574 c.release(); 575 } 576 577 public List<RepositoryChunk> getChunks() { 578 return chunks; 579 } 580 581 public InputStream open(Instant start, Instant end) throws IOException { 582 synchronized (recorder) { 583 if (getState() != RecordingState.STOPPED) { 584 throw new IOException("Recording must be stopped before it can be read."); 585 } 586 List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>(); 587 for (RepositoryChunk chunk : chunks) { 588 if (chunk.isFinished()) { 589 Instant chunkStart = chunk.getStartTime(); 590 Instant chunkEnd = chunk.getEndTime(); 591 if (start == null || !chunkEnd.isBefore(start)) { 592 if (end == null || !chunkStart.isAfter(end)) { 593 chunksToUse.add(chunk); 594 } 595 } 596 } 597 } 598 if (chunksToUse.isEmpty()) { 599 return null; 600 } 601 return new ChunkInputStream(chunksToUse); 602 } 603 } 604 605 public Duration getDuration() { 606 synchronized (recorder) { 607 return duration; 608 } 609 } 610 611 void setInternalDuration(Duration duration) { 612 this.duration = duration; 613 } 614 615 public void setDuration(Duration duration) { 616 synchronized (recorder) { 617 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 618 throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed"); 619 } 620 setInternalDuration(duration); 621 if (getState() != RecordingState.NEW) { 622 updateTimer(); 623 } 624 } 625 } 626 627 void updateTimer() { 628 if (stopTask != null) { 629 stopTask.cancel(); 630 stopTask = null; 631 } 632 if (getState() == RecordingState.CLOSED) { 633 return; 634 } 635 if (duration != null) { 636 stopTask = createStopTask(); 637 recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli())); 638 } 639 } 640 641 TimerTask createStopTask() { 642 return new TimerTask() { 643 @Override 644 public void run() { 645 try { 646 stop("End of duration reached"); 647 } catch (Throwable t) { 648 // Prevent malicious user to propagate exception callback in the wrong context 649 Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording."); 650 } 651 } 652 }; 653 } 654 655 public Recording newCopy(boolean stop) { 656 return recorder.newCopy(this, stop); 657 } 658 659 void setStopTask(TimerTask stopTask) { 660 synchronized (recorder) { 661 this.stopTask = stopTask; 662 } 663 } 664 665 void clearDestination() { 666 destination = null; 667 } 668 669 public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { 670 return noDestinationDumpOnExitAccessControlContext; 671 } 672 673 void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { 674 this.shuoldWriteActiveRecordingEvent = shouldWrite; 675 } 676 677 boolean shouldWriteMetadataEvent() { 678 return shuoldWriteActiveRecordingEvent; 679 } 680 681 // Dump running and stopped recordings 682 public void dump(WriteableUserPath writeableUserPath) throws IOException { 683 synchronized (recorder) { 684 try(PlatformRecording p = newSnapshotClone("Dumped by user", null)) { 685 p.dumpStopped(writeableUserPath); 686 } 687 } 688 } 689 690 public void dumpStopped(WriteableUserPath userPath) throws IOException { 691 synchronized (recorder) { 692 userPath.doPriviligedIO(() -> { 693 try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { 694 cc.transferTo(fc); 695 fc.force(true); 696 } 697 return null; 698 }); 699 } 700 } 701 702 public void filter(Instant begin, Instant end, Long maxSize) { 703 synchronized (recorder) { 704 List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks))); 705 if (maxSize != null) { 706 if (begin != null && end == null) { 707 result = reduceFromBeginning(maxSize, result); 708 } else { 709 result = reduceFromEnd(maxSize, result); 710 } 711 } 712 int size = 0; 713 for (RepositoryChunk r : result) { 714 size += r.getSize(); 715 r.use(); 716 } 717 this.size = size; 718 for (RepositoryChunk r : chunks) { 719 r.release(); 720 } 721 chunks.clear(); 722 chunks.addAll(result); 723 } 724 } 725 726 private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) { 727 if (time == null) { 728 return input; 729 } 730 List<RepositoryChunk> result = new ArrayList<>(input.size()); 731 for (RepositoryChunk r : input) { 732 if (!r.getEndTime().isBefore(time)) { 733 result.add(r); 734 } 735 } 736 return result; 737 } 738 739 private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) { 740 if (time == null) { 741 return input; 742 } 743 List<RepositoryChunk> result = new ArrayList<>(input.size()); 744 for (RepositoryChunk r : input) { 745 if (!r.getStartTime().isAfter(time)) { 746 result.add(r); 747 } 748 } 749 return result; 750 } 751 752 private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) { 753 if (maxSize == null || input.isEmpty()) { 754 return input; 755 } 756 List<RepositoryChunk> result = new ArrayList<>(input.size()); 757 long total = 0; 758 for (RepositoryChunk r : input) { 759 total += r.getSize(); 760 if (total > maxSize) { 761 break; 762 } 763 result.add(r); 764 } 765 // always keep at least one chunk 766 if (result.isEmpty()) { 767 result.add(input.get(0)); 768 } 769 return result; 770 } 771 772 private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) { 773 Collections.reverse(input); 774 List<RepositoryChunk> result = reduceFromBeginning(maxSize, input); 775 Collections.reverse(result); 776 return result; 777 } 778 779 public void setDumpOnExitDirectory(SafePath directory) { 780 this.dumpOnExitDirectory = directory; 781 } 782 783 public SafePath getDumpOnExitDirectory() { 784 return this.dumpOnExitDirectory; 785 } 786 } 787