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