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 }