/* * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.jfr.internal; import static jdk.jfr.internal.LogLevel.DEBUG; import static jdk.jfr.internal.LogLevel.WARN; import static jdk.jfr.internal.LogTag.JFR; import java.io.IOException; import java.io.InputStream; import java.nio.channels.FileChannel; import java.nio.file.StandardOpenOption; import java.security.AccessControlContext; import java.security.AccessController; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringJoiner; import java.util.TimerTask; import java.util.TreeMap; import jdk.jfr.Configuration; import jdk.jfr.FlightRecorderListener; import jdk.jfr.Recording; import jdk.jfr.RecordingState; public final class PlatformRecording implements AutoCloseable { private final PlatformRecorder recorder; private final long id; // Recording settings private Map settings = new LinkedHashMap<>(); private Duration duration; private Duration maxAge; private long maxSize; private WriteableUserPath destination; private boolean toDisk = true; private String name; private boolean dumpOnExit; // Timestamp information private Instant stopTime; private Instant startTime; // Misc, information private RecordingState state = RecordingState.NEW; private long size; private final LinkedList chunks = new LinkedList<>(); private volatile Recording recording; private TimerTask stopTask; private TimerTask startTask; private AccessControlContext noDestinationDumpOnExitAccessControlContext; private boolean shuoldWriteActiveRecordingEvent = true; PlatformRecording(PlatformRecorder recorder, long id) { // Typically the access control context is taken // when you call dump(Path) or setDdestination(Path), // but if no destination is set and dumponexit=true // the control context of the recording is taken when the // Recording object is constructed. This works well for // -XX:StartFlightRecording and JFR.dump this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext(); this.id = id; this.recorder = recorder; this.name = String.valueOf(id); } public void start() { RecordingState oldState; RecordingState newState; synchronized (recorder) { oldState = recording.getState(); if (!Utils.isBefore(state, RecordingState.RUNNING)) { throw new IllegalStateException("Recording can only be started once."); } if (startTask != null) { startTask.cancel(); startTask = null; startTime = null; } recorder.start(this); Logger.log(LogTag.JFR, LogLevel.INFO, () -> { // Only print non-default values so it easy to see // which options were added StringJoiner options = new StringJoiner(", "); if (!toDisk) { options.add("disk=false"); } if (maxAge != null) { options.add("maxage=" + Utils.formatTimespan(maxAge, "")); } if (maxSize != 0) { options.add("maxsize=" + Utils.formatBytes(maxSize, "")); } if (dumpOnExit) { options.add("dumponexit=true"); } if (duration != null) { options.add("duration=" + Utils.formatTimespan(duration, "")); } if (destination != null) { options.add("filename=" + destination.getText()); } String optionText = options.toString(); if (optionText.length() != 0) { optionText = "{" + optionText + "}"; } return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText; }); newState = recording.getState(); } notifyIfStateChanged(oldState, newState); } public boolean stop(String reason) { return stop(reason, null); } public boolean stop(String reason, WriteableUserPath alternativePath) { return stop(reason, alternativePath, Collections.emptyMap()); } boolean stop(String reason, WriteableUserPath alternativePath, Map overlaySettings) { RecordingState oldState; RecordingState newState; synchronized (recorder) { oldState = recording.getState(); if (stopTask != null) { stopTask.cancel(); stopTask = null; } recorder.stop(this); String endTExt = reason == null ? "" : ". Reason \"" + reason + "\"."; Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + recording.getName() + "\" (" + recording.getId()+ ")" + endTExt); this.stopTime = Instant.now(); newState = recording.getState(); } WriteableUserPath dest = getDestination(); if (dest == null && alternativePath != null) { dest = alternativePath; } if (dest != null) { try { copyTo(dest, reason, overlaySettings); Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + recording.getName() + "\" (" + recording.getId()+ ") to " + dest.getText()); notifyIfStateChanged(newState, oldState); close(); // remove if copied out } catch (IOException e) { // throw e; // BUG8925030 } } else { notifyIfStateChanged(newState, oldState); } return true; } public void scheduleStart(Duration delay) { synchronized (recorder) { ensureOkForSchedule(); startTime = Instant.now().plus(delay); LocalDateTime now = LocalDateTime.now().plus(delay); setState(RecordingState.DELAYED); startTask = createStartTask(); recorder.getTimer().schedule(startTask, delay.toMillis()); Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + recording.getName() + "\" (" + recording.getId()+ ") to start at " + now); } } private void ensureOkForSchedule() { if (getState() != RecordingState.NEW) { throw new IllegalStateException("Only a new recoridng can be scheduled for start"); } } private TimerTask createStartTask() { // Taking ref. to recording here. // Opens up for memory leaks. return new TimerTask() { @Override public void run() { synchronized (recorder) { if (getState() != RecordingState.DELAYED) { return; } start(); } } }; } void scheduleStart(Instant startTime) { synchronized (recorder) { ensureOkForSchedule(); this.startTime = startTime; setState(RecordingState.DELAYED); startTask = createStartTask(); recorder.getTimer().schedule(startTask, startTime.toEpochMilli()); } } public Map getSettings() { synchronized (recorder) { return settings; } } public long getSize() { return size; } public Instant getStopTime() { synchronized (recorder) { return stopTime; } } public Instant getStartTime() { synchronized (recorder) { return startTime; } } public Long getMaxSize() { synchronized (recorder) { return maxSize; } } public Duration getMaxAge() { synchronized (recorder) { return maxAge; } } public String getName() { synchronized (recorder) { return name; } } public RecordingState getState() { synchronized (recorder) { return state; } } @Override public void close() { RecordingState oldState; RecordingState newState; synchronized (recorder) { oldState = getState(); if (RecordingState.CLOSED != getState()) { if (startTask != null) { startTask.cancel(); startTask = null; } recorder.finish(this); for (RepositoryChunk c : chunks) { removed(c); } chunks.clear(); setState(RecordingState.CLOSED); Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId()+ ")"); } newState = getState(); } notifyIfStateChanged(newState, oldState); } public void copyTo(WriteableUserPath path, String reason, Map dumpSettings) throws IOException { synchronized (recorder) { RecordingState state = getState(); if (state == RecordingState.CLOSED) { throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write"); } if (state == RecordingState.DELAYED || state == RecordingState.NEW) { throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write"); } if (state == RecordingState.STOPPED) { // have all we need, just write it dumpToFile(path, reason, getId()); return; } // Recording is RUNNING, create a clone try(Recording r = new Recording()) { PlatformRecording clone = PrivateAccess.getInstance().getPlatformRecording(r); clone.setShouldWriteActiveRecordingEvent(false); clone.setName(getName()); clone.setDestination(path); clone.setToDisk(true); // We purposely don't clone settings, since // a union a == a if (!isToDisk()) { // force memory contents to disk clone.start(); } else { // using existing chunks on disk for (RepositoryChunk c : chunks) { clone.add(c); } clone.setState(RecordingState.RUNNING); clone.setStartTime(getStartTime()); } if (dumpSettings.isEmpty()) { clone.setSettings(getSettings()); clone.stop(reason); // dumps to destination path here } else { // Risk of violating lock order here, since // clone.stop() will take recorder lock inside // metadata lock, but OK if we already // have recorder lock when we entered metadata lock Thread.holdsLock(recorder); synchronized(MetadataRepository.getInstance()) { Thread.holdsLock(recorder); Map oldSettings = getSettings(); Map newSettings = new HashMap<>(oldSettings); // replace with dump settings newSettings.putAll(dumpSettings); clone.setSettings(newSettings); clone.stop(reason); } } } return; } } private void dumpToFile(WriteableUserPath userPath, String reason, long id) throws IOException { userPath.doPriviligedIO(() -> { try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { cc.transferTo(fc); fc.force(true); } return null; }); } public boolean isToDisk() { synchronized (recorder) { return toDisk; } } public void setMaxSize(long maxSize) { synchronized (recorder) { if (getState() == RecordingState.CLOSED) { throw new IllegalStateException("Can't set max age when recording is closed"); } this.maxSize = maxSize; trimToSize(); } } public void setDestination(WriteableUserPath userSuppliedPath) throws IOException { synchronized (recorder) { if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed"); } this.destination = userSuppliedPath; } } public WriteableUserPath getDestination() { synchronized (recorder) { return destination; } } void setState(RecordingState state) { synchronized (recorder) { this.state = state; } } void setStartTime(Instant startTime) { synchronized (recorder) { this.startTime = startTime; } } void setStopTime(Instant timeStamp) { synchronized (recorder) { stopTime = timeStamp; } } public long getId() { synchronized (recorder) { return id; } } public void setName(String name) { synchronized (recorder) { ensureNotClosed(); this.name = name; } } private void ensureNotClosed() { if (getState() == RecordingState.CLOSED) { throw new IllegalStateException("Can't change name on a closed recording"); } } public void setDumpOnExit(boolean dumpOnExit) { synchronized (recorder) { this.dumpOnExit = dumpOnExit; } } public boolean getDumpOnExit() { synchronized (recorder) { return dumpOnExit; } } public void setToDisk(boolean toDisk) { synchronized (recorder) { if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) { this.toDisk = toDisk; } else { throw new IllegalStateException("Recording option disk can't be changed after recording has started"); } } } public void setSetting(String id, String value) { synchronized (recorder) { this.settings.put(id, value); if (getState() == RecordingState.RUNNING) { recorder.updateSettings(); } } } public void setSettings(Map settings) { setSettings(settings, true); } private void setSettings(Map settings, boolean update) { if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) { TreeMap ordered = new TreeMap<>(settings); Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); for (Map.Entry entry : ordered.entrySet()) { String text = entry.getKey() + "=\"" + entry.getValue() + "\""; Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); } } synchronized (recorder) { this.settings = new LinkedHashMap<>(settings); if (getState() == RecordingState.RUNNING && update) { recorder.updateSettings(); } } } private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { if (oldState == newState) { return; } for (FlightRecorderListener cl : PlatformRecorder.getListeners()) { try { cl.recordingStateChanged(getRecording()); } catch (RuntimeException re) { Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage()); } } } public void setRecording(Recording recording) { this.recording = recording; } public Recording getRecording() { return recording; } @Override public String toString() { return getName() + " (id=" + getId() + ") " + getState(); } public void setConfiguration(Configuration c) { setSettings(c.getSettings()); } public void setMaxAge(Duration maxAge) { synchronized (recorder) { if (getState() == RecordingState.CLOSED) { throw new IllegalStateException("Can't set max age when recording is closed"); } this.maxAge = maxAge; if (maxAge != null) { trimToAge(Instant.now().minus(maxAge)); } } } void appendChunk(RepositoryChunk chunk) { if (!chunk.isFinished()) { throw new Error("not finished chunk " + chunk.getStartTime()); } synchronized (recorder) { if (!toDisk) { return; } if (maxAge != null) { trimToAge(chunk.getEndTime().minus(maxAge)); } chunks.addLast(chunk); added(chunk); trimToSize(); } } private void trimToSize() { if (maxSize == 0) { return; } while (size > maxSize && chunks.size() > 1) { RepositoryChunk c = chunks.removeFirst(); removed(c); } } private void trimToAge(Instant oldest) { while (!chunks.isEmpty()) { RepositoryChunk oldestChunk = chunks.peek(); if (oldestChunk.getEndTime().isAfter(oldest)) { return; } chunks.removeFirst(); removed(oldestChunk); } } void add(RepositoryChunk c) { chunks.add(c); added(c); } private void added(RepositoryChunk c) { c.use(); size += c.getSize(); Logger.log(JFR, DEBUG, ()-> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size); } private void removed(RepositoryChunk c) { size -= c.getSize(); Logger.log(JFR, DEBUG, ()-> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size); c.release(); } public List getChunks() { return chunks; } public InputStream open(Instant start, Instant end) throws IOException { synchronized (recorder) { if (getState() != RecordingState.STOPPED) { throw new IOException("Recording must be stopped before it can be read."); } List chunksToUse = new ArrayList(); for (RepositoryChunk chunk : chunks) { if (chunk.isFinished()) { Instant chunkStart = chunk.getStartTime(); Instant chunkEnd = chunk.getEndTime(); if (start == null || !chunkEnd.isBefore(start)) { if (end == null || !chunkStart.isAfter(end)) { chunksToUse.add(chunk); } } } } if (chunksToUse.isEmpty()) { return null; } return new ChunkInputStream(chunksToUse); } } public Duration getDuration() { synchronized (recorder) { return duration; } } void setInternalDuration(Duration duration) { this.duration = duration; } public void setDuration(Duration duration) { synchronized (recorder) { if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed"); } setInternalDuration(duration); if (getState() != RecordingState.NEW) { updateTimer(); } } } void updateTimer() { if (stopTask != null) { stopTask.cancel(); stopTask = null; } if (getState() == RecordingState.CLOSED) { return; } if (duration != null) { stopTask = createStopTask(); recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli())); } } TimerTask createStopTask() { return new TimerTask() { @Override public void run() { try { stop("End of duration reached"); } catch (Throwable t) { // Prevent malicious user to propagate exception callback in the wrong context Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording. " + t.getMessage()); } } }; } public Recording newCopy(boolean stop) { return recorder.newCopy(this, stop); } void setStopTask(TimerTask stopTask) { synchronized (recorder) { this.stopTask = stopTask; } } void clearDestination() { destination = null; } public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { return noDestinationDumpOnExitAccessControlContext; } void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { this.shuoldWriteActiveRecordingEvent = shouldWrite; } boolean shouldWriteMetadataEvent() { return shuoldWriteActiveRecordingEvent; } }