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