diff --git a/common/pom.xml b/common/pom.xml index 6d8f23f7..32caade2 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -93,6 +93,11 @@ com.fasterxml.jackson.core jackson-databind 2.10.0.pr1 + + + commons-collections + commons-collections + 3.2.2 diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index da70cc89..ca38017c 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -37,6 +37,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; @Slf4j public abstract class HttpClient { + @Getter private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES); @Getter diff --git a/common/src/main/java/ctbrec/recorder/FFmpeg.java b/common/src/main/java/ctbrec/recorder/FFmpeg.java index b6bb9ebc..bd1e4a19 100644 --- a/common/src/main/java/ctbrec/recorder/FFmpeg.java +++ b/common/src/main/java/ctbrec/recorder/FFmpeg.java @@ -1,67 +1,43 @@ package ctbrec.recorder; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.OutputStream; +import java.lang.ProcessBuilder.Redirect; import java.nio.file.Files; import java.util.Arrays; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; import java.util.function.Consumer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; -import ctbrec.io.DevNull; -import ctbrec.io.ProcessStreamRedirector; import ctbrec.recorder.download.ProcessExitedUncleanException; +@Slf4j public class FFmpeg { - - private static final Logger LOG = LoggerFactory.getLogger(FFmpeg.class); - - private static ScheduledExecutorService processOutputReader = Executors.newScheduledThreadPool(2, createThreadFactory("FFmpeg output stream reader")); - private Process process; private boolean logOutput = false; private Consumer startCallback; private Consumer exitCallback; private File ffmpegLog = null; - private OutputStream ffmpegLogStream; - private ProcessStreamRedirector stdoutRedirector; - private ProcessStreamRedirector stderrRedirector; private FFmpeg() {} - private static ThreadFactory createThreadFactory(String name) { - return r -> { - Thread t = new Thread(r); - t.setName(name); - t.setDaemon(true); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }; - } - public void exec(String[] cmdline, String[] env, File executionDir) throws IOException { - LOG.trace("FFmpeg command line: {}", Arrays.toString(cmdline)); - process = Runtime.getRuntime().exec(cmdline, env, executionDir); - afterStart(); + log.trace("FFmpeg command line: {}", Arrays.toString(cmdline)); + // process = Runtime.getRuntime().exec(cmdline, env, executionDir); + + var builder = new ProcessBuilder(cmdline); + // builder.environment().(); + builder.directory(executionDir); + setupLogging(builder); + process = builder.start(); + + notifyStartCallback(process); } - private void afterStart() throws IOException { - notifyStartCallback(process); - setupLogging(); - } public void shutdown(int exitCode) throws IOException { - LOG.trace("FFmpeg exit code was {}", exitCode); - ffmpegLogStream.flush(); - ffmpegLogStream.close(); - stdoutRedirector.setKeepGoing(false); - stderrRedirector.setKeepGoing(false); + process.destroy(); + log.trace("FFmpeg exit code was {}", exitCode); notifyExitCallback(exitCode); if (exitCode != 1) { if (ffmpegLog != null && ffmpegLog.exists()) { @@ -72,28 +48,27 @@ public class FFmpeg { } } - private void setupLogging() throws IOException { + private void setupLogging(ProcessBuilder builder) throws IOException { if (logOutput) { if (ffmpegLog == null) { ffmpegLog = File.createTempFile("ffmpeg_", ".log"); } - LOG.trace("Logging FFmpeg output to {}", ffmpegLog); + log.trace("Logging FFmpeg output to {}", ffmpegLog); ffmpegLog.deleteOnExit(); - ffmpegLogStream = new FileOutputStream(ffmpegLog); + + builder.redirectOutput(Redirect.to(ffmpegLog)); + builder.redirectErrorStream(true); } else { - ffmpegLogStream = new DevNull(); + builder.redirectOutput(Redirect.DISCARD); + builder.redirectError(Redirect.DISCARD); } - stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream); - stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream); - processOutputReader.submit(stdoutRedirector); - processOutputReader.submit(stderrRedirector); } private void notifyStartCallback(Process process) { try { startCallback.accept(process); } catch(Exception e) { - LOG.error("Exception in onStart callback", e); + log.error("Exception in onStart callback", e); } } @@ -101,7 +76,7 @@ public class FFmpeg { try { exitCallback.accept(exitCode); } catch(Exception e) { - LOG.error("Exception in onExit callback", e); + log.error("Exception in onExit callback", e); } } @@ -110,7 +85,7 @@ public class FFmpeg { try { shutdown(exitCode); } catch (IOException e) { - LOG.error("Error while shutting down FFmpeg process", e); + log.error("Error while shutting down FFmpeg process", e); } return exitCode; } diff --git a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java index 9e5eed51..536d2106 100644 --- a/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/SimplifiedLocalRecorder.java @@ -1,6 +1,7 @@ package ctbrec.recorder; import com.google.common.eventbus.Subscribe; + import ctbrec.*; import ctbrec.Recording.State; import ctbrec.event.*; @@ -29,16 +30,19 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.google.common.collect.Ordering; import static ctbrec.Recording.State.WAITING; import static ctbrec.SubsequentAction.*; import static ctbrec.event.Event.Type.MODEL_ONLINE; import static java.lang.Thread.MAX_PRIORITY; import static java.lang.Thread.MIN_PRIORITY; +import static java.util.concurrent.TimeUnit.SECONDS; @Slf4j public class SimplifiedLocalRecorder implements Recorder { - + public static final Statistics STATS = new Statistics(); + public static final boolean IGNORE_CACHE = true; private final List models = Collections.synchronizedList(new ArrayList<>()); private final Config config; @@ -51,28 +55,24 @@ public class SimplifiedLocalRecorder implements Recorder { private final RecordingManager recordingManager; private final RecordingPreconditions preconditions; - private final BlockingQueue recordings = new LinkedBlockingQueue<>(); - + // thread pools for downloads and post-processing - private final ScheduledExecutorService scheduler; - private final ExecutorService playlistDownloadPool = Executors.newFixedThreadPool(10); - private final ExecutorService segmentDownloadPool = Executors.newFixedThreadPool(10); - private final ExecutorService postProcessing; - private final ThreadPoolScaler threadPoolScaler; + private final ExecutorService segmentDownloadPool = Executors.newVirtualThreadPerTaskExecutor(); + private final ExecutorService recordingLoopPool = Executors.newVirtualThreadPerTaskExecutor(); + private final ThreadPoolExecutor postProcessing; + private final Thread maintenanceThread; private long lastSpaceCheck; public SimplifiedLocalRecorder(Config config, List sites) throws IOException { this.config = config; client = new RecorderHttpClient(config); - scheduler = Executors.newScheduledThreadPool(5, createThreadFactory("Download", MAX_PRIORITY)); - threadPoolScaler = new ThreadPoolScaler((ThreadPoolExecutor) scheduler, 5); recordingManager = new RecordingManager(config, sites); loadModels(sites); int ppThreads = config.getSettings().postProcessingThreads; BlockingQueue ppQueue = new LinkedBlockingQueue<>(); postProcessing = new ThreadPoolExecutor(ppThreads, ppThreads, 5, TimeUnit.MINUTES, ppQueue, createThreadFactory("PP", MIN_PRIORITY)); - + running = true; registerEventBusListener(); @@ -82,21 +82,20 @@ public class SimplifiedLocalRecorder implements Recorder { log.info("Models to record: {}", models); log.info("Saving recordings in {}", config.getSettings().recordingsDir); - startRecordingLoop(); + maintenanceThread = startMaintenanceLoop(); } - private void startRecordingLoop() { - new Thread(() -> { - while (running) { - Recording rec = recordings.poll(); - if (rec != null) { - processRecording(rec); - } + private Thread startMaintenanceLoop() { + var t = new Thread(() -> { + while (running && !Thread.currentThread().isInterrupted()) { checkFreeSpace(); - threadPoolScaler.tick(); - waitABit(100); + //threadPoolScaler.tick(); + waitABit(1000); } - }).start(); + }); + t.setName("Recording loop"); + t.start(); + return t; } private void checkFreeSpace() { @@ -123,33 +122,42 @@ public class SimplifiedLocalRecorder implements Recorder { } } - private void processRecording(Recording recording) { - if (recording.getCurrentIteration().isDone()) { - if (recording.getRecordingProcess().isRunning()) { - try { - Instant rescheduleAt = recording.getCurrentIteration().get().getRescheduleTime(); - Duration duration = Duration.between(Instant.now(), rescheduleAt); - long delayInMillis = Math.max(0, duration.toMillis()); - log.trace("Current iteration is done {}. Recording status {}. Rescheduling in {}ms", recording.getModel().getName(), recording.getStatus().name(), delayInMillis); - scheduleRecording(recording, delayInMillis); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - fail(recording); - } catch (ExecutionException e) { - log.error("Error while recording model {}. Stopping recording.", recording.getModel(), e); - fail(recording); + private void singleRecordingLoop(Recording recording) { + while (recording.getRecordingProcess().isRunning()) { + try { + // run single iteration + recording.getRecordingProcess().call(); + + if (recording.getModel().isSuspended()) { + log.info("Recording process for suspended model found: {}. Stopping now", recording.getModel()); + stopRecordingProcess(recording); + submitPostProcessingJob(recording); + break; } - } else { - removeRecordingProcess(recording); - if (deleteIfEmpty(recording)) { - return; - } - submitPostProcessingJob(recording); - tryRestartRecording(recording.getModel()); + + // wait necessary time + Instant rescheduleAt = recording.getRecordingProcess().getRescheduleTime(); + Duration duration = Duration.between(Instant.now(), rescheduleAt); + long delayInMillis = Math.max(0, duration.toMillis()); + log.trace("Current iteration is done {}. Recording status {}. Rescheduling in {}ms", recording.getModel().getName(), recording.getStatus().name(), delayInMillis); + Thread.sleep(delayInMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + fail(recording); + return; + } catch (Exception e) { + log.error("Error while recording model {}. Stopping recording.", recording.getModel(), e); + fail(recording); + return; } - } else { - recordings.add(recording); + } + + removeRecordingProcess(recording); + if (deleteIfEmpty(recording)) { + return; } + submitPostProcessingJob(recording); + tryRestartRecording(recording.getModel()); } private void removeRecordingProcess(Recording rec) { @@ -171,19 +179,6 @@ public class SimplifiedLocalRecorder implements Recorder { tryRestartRecording(recording.getModel()); } - private void scheduleRecording(Recording recording, long delayInMillis) { - if (recording.getModel().isSuspended()) { - log.info("Recording process for suspended model found: {}. Stopping now", recording.getModel()); - stopRecordingProcess(recording); - submitPostProcessingJob(recording); - return; - } - ScheduledFuture future = scheduler.schedule(recording.getRecordingProcess(), delayInMillis, TimeUnit.MILLISECONDS); - recording.setCurrentIteration(future); - recording.getSelectedResolution(); - recordings.add(recording); - } - private void loadModels(List sites) { config.getSettings().models .stream() @@ -215,9 +210,16 @@ public class SimplifiedLocalRecorder implements Recorder { private void stopRecordings() { log.info("Stopping all recordings"); - for (Recording recording : recordings) { - recording.getRecordingProcess().stop(); - recording.getRecordingProcess().awaitEnd(); + recorderLock.lock(); + try { + for (Recording recording : recordingProcesses) { + recording.getRecordingProcess().stop(); + } + for (Recording recording : recordingProcesses) { + recording.getRecordingProcess().awaitEnd(); + } + } finally { + recorderLock.unlock(); } waitForRecordingsToTerminate(); log.info("Recordings have been stopped"); @@ -516,11 +518,11 @@ public class SimplifiedLocalRecorder implements Recorder { public void shutdown(boolean immediately) { log.info("Shutting down"); shuttingDown = true; + maintenanceThread.interrupt(); if (!immediately) { try { stopRecordings(); - shutdownPool("Scheduler", scheduler, 60); - shutdownPool("PlaylistDownloadPool", playlistDownloadPool, 60); + shutdownPool("Recording loops", recordingLoopPool, 60); shutdownPool("SegmentDownloadPool", segmentDownloadPool, 60); shutdownPool("Post-Processing", postProcessing, 600); } catch (InterruptedException e) { @@ -726,23 +728,35 @@ public class SimplifiedLocalRecorder implements Recorder { } private void tryRestartRecording(Model model) { - if (!running) { + if (!running || shuttingDown) { // recorder is not in recording state return; } - try { - boolean modelInRecordingList = isTracked(model); - boolean online = model.isOnline(IGNORE_CACHE); - if (modelInRecordingList && online) { - log.info("Restarting recording for model {}", model); - startRecordingProcess(model); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Couldn't restart recording for model {}", model); - } catch (Exception e) { - log.error("Couldn't restart recording for model {}", model); + boolean modelInRecordingList = isTracked(model); + + if (modelInRecordingList) { + // .isOnline() check does blocking http request, so do this async + recordingLoopPool.submit(() -> { + try { + boolean online = model.isOnline(IGNORE_CACHE); + if (online) { + log.info("Restarting recording for model {}", model); + + try { + recorderLock.lock(); + startRecordingProcess(model); + } finally { + recorderLock.unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Couldn't restart recording for model {}", model); + } catch (Exception e) { + log.error("Couldn't restart recording for model {}", model); + } + }); } } @@ -770,8 +784,8 @@ public class SimplifiedLocalRecorder implements Recorder { }); } - private CompletableFuture startRecordingProcess(Model model) { - return CompletableFuture.runAsync(() -> { + private void startRecordingProcess(Model model) { + recordingLoopPool.submit(() -> { recorderLock.lock(); try { preconditions.check(model); @@ -781,7 +795,7 @@ public class SimplifiedLocalRecorder implements Recorder { setRecordingStatus(rec, State.RECORDING); rec.getModel().setLastRecorded(rec.getStartDate()); recordingManager.saveRecording(rec); - scheduleRecording(rec, 0); + recordingLoopPool.submit(() -> {singleRecordingLoop(rec);}); } catch (RecordUntilExpiredException e) { log.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage()); executeRecordUntilSubsequentAction(model); @@ -792,12 +806,12 @@ public class SimplifiedLocalRecorder implements Recorder { } finally { recorderLock.unlock(); } - }, segmentDownloadPool); + }); } private ThreadFactory createThreadFactory(String name, int priority) { return r -> { - Thread t = new Thread(r); + Thread t = Thread.ofPlatform().unstarted(r); t.setName(name + " " + UUID.randomUUID().toString().substring(0, 8)); t.setDaemon(true); t.setPriority(priority); diff --git a/common/src/main/java/ctbrec/recorder/Statistics.java b/common/src/main/java/ctbrec/recorder/Statistics.java new file mode 100644 index 00000000..61583164 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/Statistics.java @@ -0,0 +1,21 @@ +package ctbrec.recorder; + +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.commons.collections.buffer.CircularFifoBuffer; + + +public class Statistics { + public ReadWriteLock statsLock = new ReentrantReadWriteLock(); + public CircularFifoBuffer stats = new CircularFifoBuffer(100); + + public void add(Object val) { + statsLock.writeLock().lock(); + try { + stats.add(val); + } finally { + statsLock.writeLock().unlock(); + } + } +} diff --git a/common/src/main/java/ctbrec/recorder/download/RecordingProcess.java b/common/src/main/java/ctbrec/recorder/download/RecordingProcess.java index f502a6b9..30ee8612 100644 --- a/common/src/main/java/ctbrec/recorder/download/RecordingProcess.java +++ b/common/src/main/java/ctbrec/recorder/download/RecordingProcess.java @@ -62,4 +62,8 @@ public interface RecordingProcess extends Callable { void awaitEnd(); AtomicLong getDownloadedBytes(); + + default String getStats() { + return ""; + } } diff --git a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java index 8c161f8d..c09f5ab7 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/AbstractHlsDownload.java @@ -14,6 +14,7 @@ import ctbrec.io.HttpClient; import ctbrec.io.HttpConstants; import ctbrec.io.HttpException; import ctbrec.recorder.InvalidPlaylistException; +import ctbrec.recorder.SimplifiedLocalRecorder; import ctbrec.recorder.download.AbstractDownload; import ctbrec.recorder.download.HttpHeaderFactory; import ctbrec.recorder.download.StreamSource; @@ -108,6 +109,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload { @Override public AbstractHlsDownload call() throws Exception { + SimplifiedLocalRecorder.STATS.add(Duration.between(rescheduleTime, Instant.now()).toMillis()); try { if (segmentPlaylistUrl == null) { @@ -152,7 +154,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload { model.delay(); stop(); } else { - rescheduleTime = beforeLastPlaylistRequest; // try again as fast as possible + rescheduleTime = Instant.now(); // try again as fast as possible } } catch (EOFException e) { // end of playlist reached @@ -420,6 +422,8 @@ public abstract class AbstractHlsDownload extends AbstractDownload { private void calculateRescheduleTime() { rescheduleTime = beforeLastPlaylistRequest.plusMillis(1000); + if (Instant.now().isAfter(rescheduleTime)) + rescheduleTime = Instant.now(); recordingEvents.add(RecordingEvent.of("next playlist download scheduled for " + rescheduleTime.toString())); } diff --git a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java index 89a206f2..ceedd7cf 100644 --- a/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/hls/MergedFfmpegHlsDownload.java @@ -32,6 +32,14 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload { protected OutputStream ffmpegStdIn; protected BlockingQueue> queue = new LinkedBlockingQueue<>(); protected Lock ffmpegStreamLock = new ReentrantLock(); + + public String getStats() { + String text = (running ? "RUN" : "stp") + String.format(" %d: ", queue.size()); + for (var elem : queue) { + text += elem.isDone() ? "|" : "-"; + } + return text; + } public MergedFfmpegHlsDownload(HttpClient client) { super(client); diff --git a/server/src/main/java/ctbrec/recorder/server/DebugServlet.java b/server/src/main/java/ctbrec/recorder/server/DebugServlet.java new file mode 100644 index 00000000..d0a069b3 --- /dev/null +++ b/server/src/main/java/ctbrec/recorder/server/DebugServlet.java @@ -0,0 +1,108 @@ +package ctbrec.recorder.server; + +import ctbrec.Config; +import ctbrec.io.HttpClient; +import ctbrec.recorder.Recorder; +import ctbrec.recorder.SimplifiedLocalRecorder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.JSONObject; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URLDecoder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.text.MessageFormat; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.*; + +@Slf4j +public class DebugServlet extends AbstractCtbrecServlet { + + public static final String BASE_URL = "/debug"; + + private static final Pattern URL_PATTERN_DEBUG_STACK = Pattern.compile(BASE_URL + "/stack(/.*?)"); + private static final Pattern URL_PATTERN_DEBUG_STATS = Pattern.compile(BASE_URL + "/stats"); + + protected Recorder recorder; + + public DebugServlet(Recorder rec) + { + this.recorder = rec; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + String requestURI = req.getRequestURI().substring(req.getContextPath().length()); + try { + // boolean authenticated = checkAuthentication(req, ""); + // if (!authenticated) { + // sendResponse(resp, SC_UNAUTHORIZED, HMAC_ERROR_DOCUMENT); + // return; + // } + + Matcher m; + if ((m = URL_PATTERN_DEBUG_STACK.matcher(requestURI)).matches()) { + String threadUrl = URLDecoder.decode(m.group(1), UTF_8); + + var stacks = Thread.getAllStackTraces(); + var box = new Object() { String text = ""; };//stacks.toString(); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + // e.printStackTrace(pw); + + stacks.forEach((thread, stack) -> { + box.text += String.format("%s:\n", thread.getName()); + var idx = 0; + for (var s : stack) { + box.text += String.format("[%d] %s\n", idx++, s.toString()); + } + }); + + log.debug("Stacks Request {}", threadUrl); + resp.setContentType("text/plain"); + sendResponse(resp, SC_OK, box.text); + } else if ((m = URL_PATTERN_DEBUG_STATS.matcher(requestURI)).matches()) { + String text = ""; + text += String.format("GLOBAL_HTTP_CONN_POOL: connectionCount=%d, idleConnectionCount=%d\n", + HttpClient.getGLOBAL_HTTP_CONN_POOL().connectionCount(), + HttpClient.getGLOBAL_HTTP_CONN_POOL().idleConnectionCount()); + + var rlock = SimplifiedLocalRecorder.STATS.statsLock.readLock(); + rlock.lock(); + try { + text += String.format("Time between download iterations = %s\n", SimplifiedLocalRecorder.STATS.stats.toString()); + } finally { + rlock.unlock(); + } + + text += "\nRecording stats:\n"; + text = text.replace("\n", "
"); + + for (var rec : recorder.getRecordings()) { + var proc = rec.getRecordingProcess(); + if (proc == null) continue; + text += String.format("\n", proc.getModel().getDisplayName(), proc.getStats()); + } + + text += "
%s%s
"; + text += ""; + + log.debug("Stats Request"); + resp.setContentType("text/html"); + sendResponse(resp, SC_OK, text); + } else + sendResponse(resp, SC_NOT_FOUND, ""); + } catch (Exception e) { + log.error(INTERNAL_SERVER_ERROR, e); + sendResponse(resp, SC_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 7a4452ed..4f9e26af 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -258,6 +258,10 @@ public class HttpServer { ModelServlet modelServlet = new ModelServlet(config); holder = new ServletHolder(modelServlet); defaultContext.addServlet(holder, ModelServlet.BASE_URL + "/*"); + + DebugServlet debugServlet = new DebugServlet(recorder); + holder = new ServletHolder(debugServlet); + defaultContext.addServlet(holder, DebugServlet.BASE_URL + "/*"); if (this.config.getSettings().webinterface) { startWebInterface(defaultContext, basicAuthContext);