001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.http;
019
020import java.io.File;
021import java.io.IOException;
022import java.nio.charset.StandardCharsets;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.nio.file.Paths;
026import java.nio.file.StandardOpenOption;
027import java.nio.file.attribute.PosixFilePermissions;
028import java.time.Instant;
029import java.util.concurrent.TimeUnit;
030import java.util.concurrent.atomic.AtomicInteger;
031import java.util.concurrent.locks.Lock;
032import java.util.concurrent.locks.ReentrantLock;
033import javax.servlet.http.HttpServlet;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036import org.apache.yetus.audience.InterfaceAudience;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * Servlet that runs async-profiler as a web endpoint.
042 * <p>
043 * Query parameters:
044 * <ul>
045 * <li>{@code event} - profiling event: cpu|alloc|lock|cache-misses etc. (default: cpu)</li>
046 * <li>{@code duration} - run profiling for N seconds, clamped to [1,
047 * {@value #MAX_DURATION_SECONDS}] (default: 10)</li>
048 * <li>{@code interval} - sampling interval in nanoseconds (long)</li>
049 * <li>{@code jstackdepth} - maximum Java stack depth (integer)</li>
050 * <li>{@code bufsize} - frame buffer size (long); honored only by BinaryBackend</li>
051 * <li>{@code thread} - profile different threads separately (flag)</li>
052 * <li>{@code simple} - simple class names instead of FQN (flag)</li>
053 * <li>{@code output} - output format: summary|traces|flat|collapsed|tree|jfr|html (default:
054 * html)</li>
055 * <li>{@code width} - flame graph width in pixels; honored only by BinaryBackend</li>
056 * <li>{@code height} - flame graph frame height in pixels; honored only by BinaryBackend</li>
057 * <li>{@code minwidth} - skip frames smaller than this width in pixels (double)</li>
058 * <li>{@code reverse} - generate stack-reversed FlameGraph / Call tree (flag)</li>
059 * <li>{@code pid} - target process ID; LibraryBackend only supports the current JVM (returns 400
060 * for other PIDs), BinaryBackend supports external PIDs</li>
061 * <li>{@code refreshDelay} - extra seconds added to the auto-refresh delay (integer)</li>
062 * <li>{@code last} - instead of starting a new session, redirect to the most recently completed
063 * profiling result. Returns 404 if no result is cached yet. The last result is kept in memory for
064 * the lifetime of the JVM.</li>
065 * </ul>
066 * <p>
067 * Examples:
068 *
069 * <pre>
070 * # 30-second CPU profile (default)
071 * curl "http://localhost:10002/prof"
072 *
073 * # 1-minute allocation profile in tree format
074 * curl "http://localhost:10002/prof?event=alloc&amp;output=tree&amp;duration=60"
075 *
076 * # Redirect to the most recent profiling result
077 * curl "http://localhost:10002/prof?last"
078 * </pre>
079 * <p>
080 * Profiling is single-flight: only one session runs at a time. A second request while a session is
081 * active returns HTTP 409 Conflict with the URL of the last completed result (if any). Closing the
082 * browser tab does not cancel a running session — the stopper thread runs to completion on the
083 * server.
084 */
085@InterfaceAudience.Private
086public class ProfileServlet extends HttpServlet {
087
088  private static final long serialVersionUID = 1L;
089  private static final Logger LOG = LoggerFactory.getLogger(ProfileServlet.class);
090
091  private static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
092  private static final String ALLOWED_METHODS = "GET";
093  private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
094  private static final String CONTENT_TYPE_TEXT = "text/plain; charset=utf-8";
095  private static final int DEFAULT_DURATION_SECONDS = 10;
096  static final int MAX_DURATION_SECONDS = 3600;
097  private static final AtomicInteger ID_GEN = new AtomicInteger(0);
098  static final String OUTPUT_DIR = System.getProperty("java.io.tmpdir") + "/prof-output-hbase";
099  // ProfileOutputServlet considers a file complete when its size exceeds this threshold.
100  // Error messages written to the output file must be padded past this limit.
101  static final int PROF_OUTPUT_MIN_BYTES = 100;
102
103  private static final String ASYNC_PROFILER_HOME_ENV = "ASYNC_PROFILER_HOME";
104  private static final String ASYNC_PROFILER_HOME_SYSTEM_PROPERTY = "async.profiler.home";
105
106  /** Immutable record of a completed profiling session. */
107  static final class ProfileResult {
108    final String relativeUrl;
109    final String event;
110    final int durationSeconds;
111    final Instant completedAt;
112
113    ProfileResult(String relativeUrl, String event, int durationSeconds, Instant completedAt) {
114      this.relativeUrl = relativeUrl;
115      this.event = event;
116      this.durationSeconds = durationSeconds;
117      this.completedAt = completedAt;
118    }
119  }
120
121  // Last completed profiling result — static so it survives servlet reloads within the same JVM.
122  private static volatile ProfileResult lastResult = null;
123
124  // Cached backend detection result — computed once at class-load time so that isAvailable()
125  // and the default constructor do not each pay the reflective detection cost.
126  private static final ProfilerBackend DETECTED_BACKEND =
127    ProfilerBackend.detect(getAsyncProfilerHome());
128
129  enum Event {
130    CPU("cpu"),
131    WALL("wall"),
132    ALLOC("alloc"),
133    LOCK("lock"),
134    PAGE_FAULTS("page-faults"),
135    CONTEXT_SWITCHES("context-switches"),
136    CYCLES("cycles"),
137    INSTRUCTIONS("instructions"),
138    CACHE_REFERENCES("cache-references"),
139    CACHE_MISSES("cache-misses"),
140    BRANCHES("branches"),
141    BRANCH_MISSES("branch-misses"),
142    BUS_CYCLES("bus-cycles"),
143    L1_DCACHE_LOAD_MISSES("L1-dcache-load-misses"),
144    LLC_LOAD_MISSES("LLC-load-misses"),
145    DTLB_LOAD_MISSES("dTLB-load-misses"),
146    MEM_BREAKPOINT("mem:breakpoint"),
147    TRACE_TRACEPOINT("trace:tracepoint"),;
148
149    private final String internalName;
150
151    Event(final String internalName) {
152      this.internalName = internalName;
153    }
154
155    public String getInternalName() {
156      return internalName;
157    }
158
159    public static Event fromInternalName(final String name) {
160      for (Event event : values()) {
161        if (event.getInternalName().equalsIgnoreCase(name)) {
162          return event;
163        }
164      }
165
166      return null;
167    }
168  }
169
170  enum Output {
171    SUMMARY,
172    TRACES,
173    FLAT,
174    COLLAPSED,
175    // SVG dropped in async-profiler 2.0 (HBASE-25685); remapped to HTML by ProfilerCommandMapper.
176    SVG,
177    TREE,
178    JFR,
179    // In 2.x asyncprofiler, this is how you get flamegraphs.
180    HTML
181  }
182
183  // Static so a second ProfileServlet instance (servlet reload, test harness) shares the same
184  // lock and flag as the first — both gate the same JVM-global AsyncProfiler singleton.
185  private static final Lock profilerLock = new ReentrantLock();
186  private static volatile boolean profiling;
187  private final long currentPid = ProcessHandle.current().pid();
188  @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "SE_BAD_FIELD",
189      justification = "This class is never serialized nor restored.")
190  private final ProfilerBackend backend;
191
192  @InterfaceAudience.Private
193  public static final class ProfileRequest {
194    private final int duration;
195    private final Output output;
196    private final Event event;
197    private final Long interval;
198    private final Integer jstackDepth;
199    private final Long bufsize;
200    private final boolean thread;
201    private final boolean simple;
202    private final Integer width;
203    private final Integer height;
204    private final Double minwidth;
205    private final boolean reverse;
206    private final int refreshDelay;
207    private final Integer pid;
208
209    private ProfileRequest(int duration, Output output, Event event, Long interval,
210      Integer jstackDepth, Long bufsize, boolean thread, boolean simple, Integer width,
211      Integer height, Double minwidth, boolean reverse, int refreshDelay, Integer pid) {
212      this.duration = duration;
213      this.output = output;
214      this.event = event;
215      this.interval = interval;
216      this.jstackDepth = jstackDepth;
217      this.bufsize = bufsize;
218      this.thread = thread;
219      this.simple = simple;
220      this.width = width;
221      this.height = height;
222      this.minwidth = minwidth;
223      this.reverse = reverse;
224      this.refreshDelay = refreshDelay;
225      this.pid = pid;
226    }
227
228    public int getDuration() {
229      return duration;
230    }
231
232    public Output getOutput() {
233      return output;
234    }
235
236    public Event getEvent() {
237      return event;
238    }
239
240    public Long getInterval() {
241      return interval;
242    }
243
244    public Integer getJstackDepth() {
245      return jstackDepth;
246    }
247
248    public Long getBufsize() {
249      return bufsize;
250    }
251
252    public boolean isThread() {
253      return thread;
254    }
255
256    public boolean isSimple() {
257      return simple;
258    }
259
260    public Integer getWidth() {
261      return width;
262    }
263
264    public Integer getHeight() {
265      return height;
266    }
267
268    public Double getMinwidth() {
269      return minwidth;
270    }
271
272    public boolean isReverse() {
273      return reverse;
274    }
275
276    public int getRefreshDelay() {
277      return refreshDelay;
278    }
279
280    public Integer getPid() {
281      return pid;
282    }
283  }
284
285  public ProfileServlet() {
286    this.backend = DETECTED_BACKEND;
287    LOG.info("ProfileServlet initialized with backend: {}",
288      backend != null ? backend.getClass().getSimpleName() : "none");
289  }
290
291  // visible for testing
292  ProfileServlet(ProfilerBackend backend) {
293    this.backend = backend;
294  }
295
296  @Override
297  public void init() throws javax.servlet.ServletException {
298    super.init();
299    try {
300      ensureOutputDir();
301    } catch (IOException e) {
302      // Log and continue rather than failing daemon startup — a read-only or full /tmp is not
303      // a reason to refuse to bring up Master/RegionServer. Profiling will fail at request time.
304      LOG.warn("Failed to create profiler output directory {}; profiling requests will fail. "
305        + "Check that java.io.tmpdir is writable.", OUTPUT_DIR, e);
306    }
307  }
308
309  /**
310   * Creates {@link #OUTPUT_DIR} with permissions 0700 (owner-only) when the filesystem supports
311   * POSIX permissions, so other local users cannot plant symlinks or read profiling output. No-ops
312   * if the directory already exists. Silently skips the permission step on non-POSIX filesystems
313   * (Windows, some container runtimes).
314   */
315  static void ensureOutputDir() throws IOException {
316    Path dir = Paths.get(OUTPUT_DIR);
317    if (Files.exists(dir)) {
318      return;
319    }
320    try {
321      Files.createDirectories(dir,
322        PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
323    } catch (UnsupportedOperationException e) {
324      // Non-POSIX filesystem (Windows, some container setups) — fall back to plain mkdir.
325      Files.createDirectories(dir);
326    }
327  }
328
329  static String getAsyncProfilerHome() {
330    String home = System.getenv(ASYNC_PROFILER_HOME_ENV);
331    if (home == null || home.trim().isEmpty()) {
332      home = System.getProperty(ASYNC_PROFILER_HOME_SYSTEM_PROPERTY);
333    }
334    return home;
335  }
336
337  /**
338   * Returns true if a profiler backend was detected at class-load time. Detection is a one-shot
339   * operation: a library added to the classpath after the JVM starts requires a restart. A backend
340   * that resolved successfully here may still fail on first use if the native binary is
341   * incompatible with the OS/kernel — that error surfaces at request time via the
342   * {@code catch(Error | RuntimeException)} block in {@link #doGet}.
343   */
344  public static boolean isAvailable() {
345    return DETECTED_BACKEND != null;
346  }
347
348  public ProfileRequest parseProfileRequest(final HttpServletRequest req) {
349    // Note: when using in-process async-profiler Java API, we can only profile this JVM.
350    // We keep the pid parameter for API compatibility, but do not support external processes.
351    Integer requestedPid = getInteger(req, "pid", null);
352
353    final int duration = Math.min(
354      Math.max(getInteger(req, "duration", DEFAULT_DURATION_SECONDS), 1), MAX_DURATION_SECONDS);
355    final Output output = getOutput(req);
356    final Event event = getEvent(req);
357    final Long interval = getLong(req, "interval");
358    final Integer jstackDepth = getInteger(req, "jstackdepth", null);
359    final Long bufsize = getLong(req, "bufsize");
360    final boolean thread = req.getParameterMap().containsKey("thread");
361    final boolean simple = req.getParameterMap().containsKey("simple");
362    final Integer width = getInteger(req, "width", null);
363    final Integer height = getInteger(req, "height", null);
364    final Double minwidth = getMinWidth(req);
365    final boolean reverse = req.getParameterMap().containsKey("reverse");
366    int refreshDelay = getInteger(req, "refreshDelay", 0);
367
368    return new ProfileRequest(duration, output, event, interval, jstackDepth, bufsize, thread,
369      simple, width, height, minwidth, reverse, refreshDelay, requestedPid);
370  }
371
372  protected String executeStart(ProfileRequest request, File outputFile) throws IOException {
373    return backend.executeStart(request, outputFile);
374  }
375
376  protected String executeStop(ProfileRequest request, File outputFile) throws IOException {
377    return backend.executeStop(request, outputFile);
378  }
379
380  @Override
381  protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
382    throws IOException {
383    if (!checkInstrumentationAccess(req, resp)) {
384      return;
385    }
386
387    // ?last — redirect to the most recent completed profiling result.
388    if (req.getParameterMap().containsKey("last")) {
389      ProfileResult last = lastResult;
390      if (last == null) {
391        writeError(resp, HttpServletResponse.SC_NOT_FOUND,
392          "No profiling results available yet. Run /prof to start a session.");
393        return;
394      }
395      // The output file may have been removed by tmpwatch / fs cleanup or a JVM restart.
396      // Verify the file still exists before redirecting; otherwise clear the stale pointer.
397      String fileName = last.relativeUrl.substring(last.relativeUrl.lastIndexOf('/') + 1);
398      File outputFile = new File(OUTPUT_DIR, fileName);
399      if (!outputFile.exists()) {
400        lastResult = null;
401        writeError(resp, HttpServletResponse.SC_NOT_FOUND,
402          "The most recent profiling result (" + last.relativeUrl + ") no longer exists "
403            + "(may have been cleaned up by the OS). Run /prof to start a new session.");
404        return;
405      }
406      setResponseHeader(resp);
407      resp.sendRedirect(last.relativeUrl);
408      return;
409    }
410
411    final ProfileRequest request = parseProfileRequest(req);
412
413    // Reject non-positive PIDs regardless of backend — no valid process has pid <= 0.
414    if (request.getPid() != null && request.getPid() <= 0) {
415      writeError(resp, HttpServletResponse.SC_BAD_REQUEST,
416        "Invalid pid " + request.getPid() + ": must be a positive process ID.");
417      return;
418    }
419
420    // LibraryBackend can only profile the current JVM; BinaryBackend supports external PIDs.
421    if (
422      request.getPid() != null && request.getPid().longValue() != currentPid
423        && backend instanceof LibraryBackend
424    ) {
425      LOG.warn("Rejected profiling request for PID {} (current PID: {}) — "
426        + "LibraryBackend only supports the current process", request.getPid(), currentPid);
427      writeError(resp, HttpServletResponse.SC_BAD_REQUEST,
428        "The 'pid' parameter is only supported for the current process when using the "
429          + "LibraryBackend (in-process async-profiler). Use ASYNC_PROFILER_HOME to enable "
430          + "the BinaryBackend for cross-process profiling.");
431      return;
432    }
433
434    boolean locked = false;
435    boolean thisRequestSetProfiling = false;
436    boolean stopperStarted = false;
437    File outputFile = null;
438    try {
439      locked = profilerLock.tryLock(100, TimeUnit.MILLISECONDS);
440      if (!locked) {
441        LOG.info("Profiler lock busy; returning 409 immediately.");
442        writeError(resp, HttpServletResponse.SC_CONFLICT,
443          "Another instance of profiler is already running or the lock is contended. "
444            + "Try again in a moment.");
445        return;
446      }
447
448      // Re-check under the lock to close the TOCTOU window.
449      if (profiling) {
450        StringBuilder msg = new StringBuilder("Another instance of profiler is already running.");
451        ProfileResult last = lastResult;
452        if (last != null) {
453          msg.append(" Last result: ").append(last.relativeUrl).append(" (").append(last.event)
454            .append(", ").append(last.durationSeconds).append("s, completed ")
455            .append(last.completedAt).append("). Use /prof?last to view it.");
456        }
457        writeError(resp, HttpServletResponse.SC_CONFLICT, msg.toString());
458        return;
459      }
460
461      outputFile = createOutputFile(request);
462      final String relativeUrl = "/prof-output-hbase/" + outputFile.getName();
463      // Write the placeholder before starting the profiler. Using CREATE_NEW (O_CREAT|O_EXCL)
464      // so a pre-planted symlink causes an immediate IOException rather than following the link
465      // and truncating the symlink target (symlink-attack mitigation).
466      Files.write(outputFile.toPath(), new byte[0], StandardOpenOption.CREATE_NEW);
467      executeStart(request, outputFile);
468      profiling = true;
469      thisRequestSetProfiling = true;
470
471      startStopperThread(request.getDuration(), request, outputFile, relativeUrl);
472      stopperStarted = true;
473
474      writeAcceptedResponse(resp, request, relativeUrl);
475    } catch (InterruptedException e) {
476      Thread.currentThread().interrupt();
477      LOG.warn("Interrupted while acquiring profile lock.", e);
478      writeError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
479        "Interrupted while acquiring profile lock.");
480    } catch (IOException | Error | RuntimeException e) {
481      // Catches:
482      // - IOException: AsyncProfiler.execute() throws IOException for invalid agent commands
483      // - UnsatisfiedLinkError / other Error: native lib absent or incompatible OS/kernel
484      // - IllegalStateException / IllegalArgumentException (RuntimeException): double-start,
485      // unsupported event, rejected format from the profiler API
486      LOG.warn("Profiler failed to start or execute", e);
487      if (!resp.isCommitted()) {
488        writeError(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
489          "Profiler error: " + e.getMessage()
490            + ". Check that the async-profiler native library is compatible with this OS/kernel.");
491      }
492      // If executeStart succeeded but startStopperThread failed (e.g. t.start() threw
493      // OutOfMemoryError), the LibraryBackend AsyncProfiler instance is still in "started" state
494      // with no stopper to drain it. A best-effort stop closes it so future requests don't hit
495      // IllegalStateException("profiler already started").
496      if (thisRequestSetProfiling && !stopperStarted) {
497        try {
498          executeStop(request, outputFile);
499        } catch (Exception stopEx) {
500          LOG.warn("Best-effort stop after failed startStopperThread also failed", stopEx);
501        }
502      }
503      // Delete the placeholder whenever the stopper thread was never started — in that case
504      // no client received the output URL and the stopper will never write a result to the file.
505      // Guard on !stopperStarted (not !thisRequestSetProfiling) to also cover the rare path where
506      // executeStart succeeded but t.start() threw, leaving a 0-byte file with no owner.
507      if (!stopperStarted && outputFile != null) {
508        try {
509          Files.deleteIfExists(outputFile.toPath());
510        } catch (IOException ioe) {
511          LOG.warn("Unable to delete orphan placeholder {}", outputFile.getName(), ioe);
512        }
513      }
514    } finally {
515      // Only reset the profiling flag if THIS request was the one that set it, and the stopper
516      // thread was never started (e.g. t.start() threw OutOfMemoryError). Using a separate flag
517      // avoids incorrectly clearing profiling=true for a concurrently-running session when this
518      // request exited early via the 409 conflict path.
519      if (thisRequestSetProfiling && !stopperStarted) {
520        profiling = false;
521      }
522      if (locked) {
523        profilerLock.unlock();
524      }
525    }
526  }
527
528  private void startStopperThread(final int durationSeconds, final ProfileRequest request,
529    final File outputFile, final String relativeUrl) {
530    Thread t = new Thread(() -> {
531      boolean succeeded = false;
532      Throwable failure = null;
533      try {
534        TimeUnit.SECONDS.sleep(durationSeconds);
535        executeStop(request, outputFile);
536        // C5: executeStop may succeed but the profiler wrote nothing (e.g. zero samples collected).
537        // ProfileOutputServlet polls until size >= PROF_OUTPUT_MIN_BYTES, so pad a short/empty
538        // output file to unblock it rather than letting the browser auto-refresh forever.
539        if (outputFile.length() < PROF_OUTPUT_MIN_BYTES) {
540          String pad = "Profiling completed but output was empty or very short.";
541          while (pad.length() <= PROF_OUTPUT_MIN_BYTES) {
542            pad += " ";
543          }
544          Files.write(outputFile.toPath(), pad.getBytes(StandardCharsets.UTF_8),
545            java.nio.file.StandardOpenOption.APPEND);
546        }
547        lastResult = new ProfileResult(relativeUrl, request.getEvent().getInternalName(),
548          durationSeconds, Instant.now());
549        succeeded = true;
550      } catch (InterruptedException e) {
551        Thread.currentThread().interrupt();
552        failure = e;
553        LOG.warn("Profiler stopper thread interrupted; attempting best-effort stop.", e);
554        // C3: The LibraryBackend AsyncProfiler is still in "started" state after an interrupt.
555        // Best-effort stop so future /prof requests don't hit IllegalStateException.
556        try {
557          executeStop(request, outputFile);
558        } catch (Exception stopEx) {
559          LOG.warn("Best-effort stop after stopper interrupt also failed", stopEx);
560        }
561      } catch (Throwable e) {
562        failure = e;
563        LOG.warn("Profiler stop/dump failed", e);
564      } finally {
565        // C4: Reset profiling flag FIRST, before any I/O that could throw an Error and skip it.
566        profiling = false;
567        // If the session did not complete successfully, pad the output file to >100 bytes so
568        // ProfileOutputServlet's size check treats it as done and stops auto-refreshing.
569        if (!succeeded && failure != null) {
570          try {
571            String msg = (failure instanceof InterruptedException)
572              ? "Profiler session interrupted before stop completed."
573              : "Profiler stop/dump failed: " + failure.getMessage();
574            // PROF_OUTPUT_MIN_BYTES is checked by ProfileOutputServlet to determine completion.
575            while (msg.length() <= PROF_OUTPUT_MIN_BYTES) {
576              msg += " ";
577            }
578            Files.write(outputFile.toPath(), msg.getBytes(StandardCharsets.UTF_8));
579          } catch (IOException ioe) {
580            LOG.warn("Unable to write profiler error to output file", ioe);
581          }
582        }
583      }
584    }, "ProfileServlet-stopper");
585    t.setDaemon(true);
586    t.start();
587  }
588
589  private boolean checkInstrumentationAccess(final HttpServletRequest req,
590    final HttpServletResponse resp) throws IOException {
591    if (!HttpServer.isInstrumentationAccessAllowed(getServletContext(), req, resp)) {
592      resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
593      setResponseHeader(resp);
594      resp.getWriter().write("Unauthorized: Instrumentation access is not allowed!");
595      return false;
596    }
597    return true;
598  }
599
600  @Override
601  public void destroy() {
602    if (backend != null) {
603      backend.destroy();
604    }
605    super.destroy();
606  }
607
608  private void writeError(final HttpServletResponse resp, final int status, final String message)
609    throws IOException {
610    resp.setStatus(status);
611    setResponseHeader(resp);
612    resp.getWriter().write(message);
613  }
614
615  private File createOutputFile(final ProfileRequest request) throws IOException {
616    final long pid = request.getPid() != null ? request.getPid().longValue() : currentPid;
617    // Use the remapped file extension so that (e.g.) SVG→HTML remap is reflected in the
618    // filename. toFileExtension is used here (not toFormatString) to avoid a duplicate
619    // LOG.warn — the warning is emitted once by toLibraryStopCommand or toCliCommand.
620    String ext = ProfilerCommandMapper.toFileExtension(request.getOutput());
621    File outputFile = new File(OUTPUT_DIR, "async-prof-pid-" + pid + "-"
622      + request.getEvent().name().toLowerCase() + "-" + ID_GEN.incrementAndGet() + "." + ext);
623    return outputFile;
624  }
625
626  private void writeAcceptedResponse(final HttpServletResponse resp, final ProfileRequest request,
627    final String relativeUrl) throws IOException {
628    setResponseHeader(resp);
629    resp.setStatus(HttpServletResponse.SC_ACCEPTED);
630    StringBuilder body = new StringBuilder();
631    body.append("Started [").append(request.getEvent().getInternalName())
632      .append("] profiling. This page will automatically redirect to ").append(relativeUrl)
633      .append(" after ").append(request.getDuration()).append(" seconds. ")
634      .append("If empty diagram and Linux 4.6+, see 'Basic Usage' section on the Async ")
635      .append("Profiler Home Page, https://github.com/jvm-profiling-tools/async-profiler.");
636    if (request.getOutput() == Output.SVG) {
637      body.append("\nNote: output=svg is not supported in async-profiler 2.0+; serving html.");
638    }
639    if (backend instanceof LibraryBackend) {
640      if (request.getBufsize() != null) {
641        body.append("\nNote: bufsize= is not supported by the in-process LibraryBackend"
642          + " (async-profiler 4.x) and was ignored."
643          + " Set ASYNC_PROFILER_HOME to use BinaryBackend if you need -b support.");
644      }
645      if (request.getWidth() != null || request.getHeight() != null) {
646        body.append("\nNote: width= and height= are not supported by the in-process LibraryBackend"
647          + " (async-profiler 4.x) and were ignored."
648          + " Set ASYNC_PROFILER_HOME to use BinaryBackend if you need --width/--height support.");
649      }
650    }
651    resp.getWriter().write(body.toString());
652    resp.setHeader("Refresh",
653      (request.getDuration() + request.getRefreshDelay()) + ";" + relativeUrl);
654    resp.getWriter().flush();
655  }
656
657  private Integer getInteger(final HttpServletRequest req, final String param,
658    final Integer defaultValue) {
659    final String value = req.getParameter(param);
660    if (value != null) {
661      try {
662        return Integer.valueOf(value);
663      } catch (NumberFormatException e) {
664        return defaultValue;
665      }
666    }
667    return defaultValue;
668  }
669
670  private Long getLong(final HttpServletRequest req, final String param) {
671    final String value = req.getParameter(param);
672    if (value != null) {
673      try {
674        return Long.valueOf(value);
675      } catch (NumberFormatException e) {
676        return null;
677      }
678    }
679    return null;
680  }
681
682  private Double getMinWidth(final HttpServletRequest req) {
683    final String value = req.getParameter("minwidth");
684    if (value != null) {
685      try {
686        return Double.valueOf(value);
687      } catch (NumberFormatException e) {
688        return null;
689      }
690    }
691    return null;
692  }
693
694  private Event getEvent(final HttpServletRequest req) {
695    final String eventArg = req.getParameter("event");
696    if (eventArg != null) {
697      Event event = Event.fromInternalName(eventArg);
698      return event == null ? Event.CPU : event;
699    }
700    return Event.CPU;
701  }
702
703  private Output getOutput(final HttpServletRequest req) {
704    final String outputArg = req.getParameter("output");
705    if (req.getParameter("output") != null) {
706      try {
707        return Output.valueOf(outputArg.trim().toUpperCase());
708      } catch (IllegalArgumentException e) {
709        return Output.HTML;
710      }
711    }
712    return Output.HTML;
713  }
714
715  static void setResponseHeader(final HttpServletResponse response) {
716    response.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS);
717    response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
718    response.setContentType(CONTENT_TYPE_TEXT);
719  }
720
721  public static class DisabledServlet extends HttpServlet {
722
723    private static final long serialVersionUID = 1L;
724
725    /** Init-param key for the human-readable disable reason. */
726    static final String REASON_PARAM = "disabledReason";
727
728    @Override
729    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
730      throws IOException {
731      resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
732      setResponseHeader(resp);
733      String reason = getInitParameter(REASON_PARAM);
734      if (reason == null || reason.isEmpty()) {
735        reason = "The profiler servlet was disabled at startup.";
736      }
737      resp.getWriter().write(reason + "\n\nFor more information please see "
738        + "https://hbase.apache.org/docs/profiler\n");
739    }
740
741  }
742
743}