diff --git a/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java b/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java new file mode 100644 index 0000000000..2335e5a24f --- /dev/null +++ b/photon-core/src/main/java/org/photonvision/common/logging/KernelLogLogger.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.common.logging; + +import edu.wpi.first.util.RuntimeDetector; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.photonvision.common.util.TimedTaskManager; +import org.photonvision.jni.QueuedFileLogger; + +/** + * Listens for and reproduces Linux kernel logs, from /var/log/kern.log, into the Photon logger + * ecosystem + */ +public class KernelLogLogger { + private static KernelLogLogger INSTANCE; + + public static KernelLogLogger getInstance() { + if (INSTANCE == null) { + INSTANCE = new KernelLogLogger(); + } + return INSTANCE; + } + + QueuedFileLogger listener = null; + Logger logger = new Logger(KernelLogLogger.class, LogGroup.General); + + public KernelLogLogger() { + if (RuntimeDetector.isLinux()) { + logger.info("Listening for klogs on /var/log/dmesg ! Boot logs:"); + + try { + var bootlog = Files.readAllLines(Path.of("/var/log/dmesg")); + for (var line : bootlog) { + logger.log(line, LogLevel.DEBUG); + } + } catch (IOException e) { + logger.error("Couldn't read /var/log/dmesg - not printing boot logs"); + } + + listener = new QueuedFileLogger("/var/log/kern.log"); + } else { + System.out.println("NOT for klogs"); + } + + // arbitrary frequency to grab logs. The underlying native buffer will grow unbounded without + // this, lol + TimedTaskManager.getInstance().addTask("outputPrintk", this::outputNewPrintks, 1000); + } + + public void outputNewPrintks() { + for (var msg : listener.getNewlines()) { + // We currently set all logs to debug regardless of their actual level + logger.log(msg, LogLevel.DEBUG); + } + } +} diff --git a/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java b/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java index 0e2a1d8824..f68691cc01 100644 --- a/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java +++ b/photon-core/src/main/java/org/photonvision/common/logging/LogGroup.java @@ -26,4 +26,5 @@ public enum LogGroup { Config, CSCore, NetworkTables, + System, } diff --git a/photon-core/src/main/java/org/photonvision/common/logging/Logger.java b/photon-core/src/main/java/org/photonvision/common/logging/Logger.java index dc0ebefe72..7bba651b53 100644 --- a/photon-core/src/main/java/org/photonvision/common/logging/Logger.java +++ b/photon-core/src/main/java/org/photonvision/common/logging/Logger.java @@ -30,8 +30,34 @@ import org.photonvision.common.dataflow.events.OutgoingUIEvent; import org.photonvision.common.util.TimedTaskManager; -@SuppressWarnings("unused") +/** TODO: get rid of static {} blocks and refactor to singleton pattern */ public class Logger { + private static final HashMap levelMap = new HashMap<>(); + private static final List currentAppenders = new ArrayList<>(); + + private static final UILogAppender uiLogAppender = new UILogAppender(); + + // // TODO why's the logger care about this? split it out + // private static KernelLogLogger klogListener = null; + + static { + levelMap.put(LogGroup.Camera, LogLevel.INFO); + levelMap.put(LogGroup.General, LogLevel.INFO); + levelMap.put(LogGroup.WebServer, LogLevel.INFO); + levelMap.put(LogGroup.Data, LogLevel.INFO); + levelMap.put(LogGroup.VisionModule, LogLevel.INFO); + levelMap.put(LogGroup.Config, LogLevel.INFO); + levelMap.put(LogGroup.CSCore, LogLevel.TRACE); + levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG); + levelMap.put(LogGroup.System, LogLevel.DEBUG); + + currentAppenders.add(new ConsoleLogAppender()); + currentAppenders.add(uiLogAppender); + addFileAppender(PathManager.getInstance().getLogPath()); + + cleanLogs(PathManager.getInstance().getLogsDir()); + } + public static final String ANSI_RESET = "\u001B[0m"; public static final String ANSI_BLACK = "\u001B[30m"; public static final String ANSI_RED = "\u001B[31m"; @@ -50,8 +76,6 @@ public class Logger { private static final List> uiBacklog = new ArrayList<>(); private static boolean connected = false; - private static final UILogAppender uiLogAppender = new UILogAppender(); - private final String className; private final LogGroup group; @@ -89,27 +113,6 @@ public static String format( return builder.toString(); } - private static final HashMap levelMap = new HashMap<>(); - private static final List currentAppenders = new ArrayList<>(); - - static { - levelMap.put(LogGroup.Camera, LogLevel.INFO); - levelMap.put(LogGroup.General, LogLevel.INFO); - levelMap.put(LogGroup.WebServer, LogLevel.INFO); - levelMap.put(LogGroup.Data, LogLevel.INFO); - levelMap.put(LogGroup.VisionModule, LogLevel.INFO); - levelMap.put(LogGroup.Config, LogLevel.INFO); - levelMap.put(LogGroup.CSCore, LogLevel.TRACE); - levelMap.put(LogGroup.NetworkTables, LogLevel.DEBUG); - } - - static { - currentAppenders.add(new ConsoleLogAppender()); - currentAppenders.add(uiLogAppender); - addFileAppender(PathManager.getInstance().getLogPath()); - cleanLogs(PathManager.getInstance().getLogsDir()); - } - @SuppressWarnings("ResultOfMethodCallIgnored") public static void addFileAppender(Path logFilePath) { var file = logFilePath.toFile(); diff --git a/photon-server/build.gradle b/photon-server/build.gradle index eaf82419a5..80cf3a344a 100644 --- a/photon-server/build.gradle +++ b/photon-server/build.gradle @@ -11,6 +11,9 @@ apply from: "${rootDir}/shared/common.gradle" dependencies { implementation project(':photon-core') + // Zip + implementation 'org.zeroturnaround:zt-zip:1.14' + // Needed for Javalin Runtime Logging implementation "org.slf4j:slf4j-simple:2.0.7" } diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java index a88a9ca166..a3834abe5e 100644 --- a/photon-server/src/main/java/org/photonvision/Main.java +++ b/photon-server/src/main/java/org/photonvision/Main.java @@ -33,6 +33,7 @@ import org.photonvision.common.hardware.HardwareManager; import org.photonvision.common.hardware.PiVersion; import org.photonvision.common.hardware.Platform; +import org.photonvision.common.logging.KernelLogLogger; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogLevel; import org.photonvision.common.logging.Logger; @@ -437,6 +438,10 @@ public static void main(String[] args) { Logger.setLevel(LogGroup.General, logLevel); logger.info("Logging initialized in debug mode."); + // Add Linux kernel log->Photon logger + KernelLogLogger.getInstance(); + + // Add CSCore->Photon logger PvCSCoreLogger.getInstance(); logger.debug("Loading ConfigManager..."); diff --git a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java index 2b1df67433..1b17c68eb4 100644 --- a/photon-server/src/main/java/org/photonvision/server/RequestHandler.java +++ b/photon-server/src/main/java/org/photonvision/server/RequestHandler.java @@ -53,6 +53,7 @@ import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.camera.CameraQuirk; import org.photonvision.vision.processes.VisionModuleManager; +import org.zeroturnaround.zip.ZipUtil; public class RequestHandler { // Treat all 2XX calls as "INFO" @@ -422,20 +423,34 @@ public static void onLogExportRequest(Context ctx) { try { ShellExec shell = new ShellExec(); var tempPath = Files.createTempFile("photonvision-journalctl", ".txt"); - shell.executeBashCommand("journalctl -u photonvision.service > " + tempPath.toAbsolutePath()); + var tempPath2 = Files.createTempFile("photonvision-kernelogs", ".txt"); + shell.executeBashCommand( + "journalctl -u photonvision.service > " + + tempPath.toAbsolutePath() + + " && journalctl -k > " + + tempPath2.toAbsolutePath()); while (!shell.isOutputCompleted()) { // TODO: add timeout } if (shell.getExitCode() == 0) { - // Wrote to the temp file! Add it to the ctx - var stream = new FileInputStream(tempPath.toFile()); - ctx.contentType("text/plain"); - ctx.header("Content-Disposition", "attachment; filename=\"photonvision-journalctl.txt\""); - ctx.status(200); + // Wrote to the temp file! Zip and yeet it to the client + + var out = Files.createTempFile("photonvision-logs", "zip").toFile(); + + try { + ZipUtil.packEntries(new File[] {tempPath.toFile(), tempPath2.toFile()}, out); + } catch (Exception e) { + e.printStackTrace(); + } + + var stream = new FileInputStream(out); + ctx.contentType("application/zip"); + ctx.header("Content-Disposition", "attachment; filename=\"photonvision-logs.zip\""); ctx.result(stream); - logger.info("Uploading settings with size " + stream.available()); + ctx.status(200); + logger.info("Outputting log ZIP with size " + stream.available()); } else { ctx.status(500); ctx.result("The journalctl service was unable to export logs"); diff --git a/photon-targeting/build.gradle b/photon-targeting/build.gradle index bddca08c36..1215c062e4 100644 --- a/photon-targeting/build.gradle +++ b/photon-targeting/build.gradle @@ -220,3 +220,6 @@ nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpimath") nativeConfig.dependencies.add wpilibTools.deps.wpilib("wpinet") nativeConfig.dependencies.add wpilibTools.deps.wpilib("ntcore") nativeConfig.dependencies.add wpilibTools.deps.wpilib("hal") +nativeConfig.dependencies.add wpilibTools.deps.wpilib("cscore") +nativeConfig.dependencies.add wpilibTools.deps.wpilibOpenCv("frc" + openCVYear, wpi.versions.opencvVersion.get()) +nativeConfig.dependencies.add wpilibTools.deps.wpilib("apriltag") diff --git a/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java b/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java new file mode 100644 index 0000000000..eade168774 --- /dev/null +++ b/photon-targeting/src/main/java/org/photonvision/jni/QueuedFileLogger.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.photonvision.jni; + +public class QueuedFileLogger { + long m_handle = 0; + + public QueuedFileLogger(String path) { + m_handle = QueuedFileLogger.create(path); + } + + public String[] getNewlines() { + String newBuffer = null; + + synchronized (this) { + if (m_handle == 0) { + System.err.println("QueuedFileLogger use after free"); + return new String[0]; + } + + newBuffer = QueuedFileLogger.getNewLines(m_handle); + } + + if (newBuffer == null) { + return new String[0]; + } + + return newBuffer.split("\n"); + } + + public void stop() { + synchronized (this) { + if (m_handle != 0) { + QueuedFileLogger.destroy(m_handle); + m_handle = 0; + } + } + } + + private static native long create(String path); + + private static native void destroy(long handle); + + private static native String getNewLines(long handle); +} diff --git a/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp b/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp new file mode 100644 index 0000000000..c7a45e2de3 --- /dev/null +++ b/photon-targeting/src/main/native/jni/FileLoggerExtrasJNI.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include + +#include "jni_utils.h" +#include "org_photonvision_jni_QueuedFileLogger.h" + +struct QueuedFileLogger { + // ew ew ew ew ew ew ew ew + std::vector m_data{}; + + std::mutex m_mutex; + + wpi::FileLogger logger; + + explicit QueuedFileLogger(std::string_view file) + : logger{file, std::bind(&QueuedFileLogger::callback, this, + std::placeholders::_1)} { + // fmt::println("Watching {}", file); + } + + void callback(std::string_view newline) { + std::lock_guard lock{m_mutex}; + // fmt::println("FileLogger got: {}", newline); + m_data.insert(m_data.end(), newline.begin(), newline.end()); + } + + std::vector SwapData() { + std::vector ret; + { + std::lock_guard lock{m_mutex}; + ret.swap(m_data); + } + + return ret; + } +}; + +extern "C" { + +/* + * Class: org_photonvision_jni_QueuedFileLogger + * Method: create + * Signature: (Ljava/lang/String;)J + */ +JNIEXPORT jlong JNICALL +Java_org_photonvision_jni_QueuedFileLogger_create + (JNIEnv* env, jclass, jstring name) +{ + const char* c_name{env->GetStringUTFChars(name, 0)}; + std::string cpp_name{c_name}; + jlong ret{reinterpret_cast(new QueuedFileLogger(cpp_name))}; + env->ReleaseStringUTFChars(name, c_name); + return ret; +} + +/* + * Class: org_photonvision_jni_QueuedFileLogger + * Method: destroy + * Signature: (J)V + */ +JNIEXPORT void JNICALL +Java_org_photonvision_jni_QueuedFileLogger_destroy + (JNIEnv*, jclass, jlong handle) +{ + CHECK_PTR(handle); + delete reinterpret_cast(handle); +} + +/* + * Class: org_photonvision_jni_QueuedFileLogger + * Method: getNewLines + * Signature: (J)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL +Java_org_photonvision_jni_QueuedFileLogger_getNewLines + (JNIEnv* env, jclass, jlong handle) +{ + CHECK_PTR_RETURN(handle, nullptr); + QueuedFileLogger* logger = reinterpret_cast(handle); + + return env->NewStringUTF(logger->SwapData().data()); +} +} // extern "C" diff --git a/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp b/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp index 8b982865a5..82f3599635 100644 --- a/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp +++ b/photon-targeting/src/main/native/jni/TimeSyncClientJNI.cpp @@ -20,21 +20,11 @@ #include #include +#include "jni_utils.h" #include "net/TimeSyncClient.h" using namespace wpi::tsp; -#define CHECK_PTR(ptr) \ - if (!ptr) { \ - fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ - return; \ - } -#define CHECK_PTR_RETURN(ptr, default) \ - if (!ptr) { \ - fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ - return default; \ - } - /** * Finds a class and keeps it as a global reference. * diff --git a/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp b/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp index 2991ac5676..444a7ddd62 100644 --- a/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp +++ b/photon-targeting/src/main/native/jni/TimeSyncServerJNI.cpp @@ -20,21 +20,11 @@ #include +#include "jni_utils.h" #include "net/TimeSyncServer.h" using namespace wpi::tsp; -#define CHECK_PTR(ptr) \ - if (!ptr) { \ - fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ - return; \ - } -#define CHECK_PTR_RETURN(ptr, default) \ - if (!ptr) { \ - fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ - return default; \ - } - extern "C" { /* diff --git a/photon-targeting/src/main/native/jni/jni_utils.h b/photon-targeting/src/main/native/jni/jni_utils.h new file mode 100644 index 0000000000..036af25196 --- /dev/null +++ b/photon-targeting/src/main/native/jni/jni_utils.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#define CHECK_PTR(ptr) \ + if (!ptr) { \ + fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ + return; \ + } +#define CHECK_PTR_RETURN(ptr, default) \ + if (!ptr) { \ + fmt::println("Got invalid pointer?? {}:{}", __FILE__, __LINE__); \ + return default; \ + } diff --git a/photon-targeting/src/test/java/net/TimeSyncTest.java b/photon-targeting/src/test/java/net/TimeSyncTest.java index f5ea486f73..a5fd7b4ccb 100644 --- a/photon-targeting/src/test/java/net/TimeSyncTest.java +++ b/photon-targeting/src/test/java/net/TimeSyncTest.java @@ -36,6 +36,8 @@ public static void load_wpilib() throws UnsatisfiedLinkError, IOException { if (!PhotonTargetingJniLoader.load()) { fail(); } + + HAL.initialize(1000, 0); } @AfterAll @@ -45,8 +47,6 @@ public static void teardown() { @Test public void smoketest() throws InterruptedException { - HAL.initialize(1000, 0); - // NetworkTableInstance.getDefault().stopClient(); // NetworkTableInstance.getDefault().startServer(); diff --git a/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java b/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java new file mode 100644 index 0000000000..adc88e3f7b --- /dev/null +++ b/photon-targeting/src/test/java/wpiutil_extras/FileLoggerTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) Photon Vision. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package wpiutil_extras; + +import static org.junit.jupiter.api.Assertions.fail; + +import edu.wpi.first.hal.HAL; +import java.io.IOException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.photonvision.jni.PhotonTargetingJniLoader; +import org.photonvision.jni.QueuedFileLogger; +import org.photonvision.jni.WpilibLoader; + +public class FileLoggerTest { + @BeforeAll + public static void load_wpilib() throws UnsatisfiedLinkError, IOException { + if (!WpilibLoader.loadLibraries()) { + fail(); + } + if (!PhotonTargetingJniLoader.load()) { + fail(); + } + + HAL.initialize(1000, 0); + } + + @AfterAll + public static void teardown() { + HAL.shutdown(); + } + + @Test + public void smoketest() throws InterruptedException { + var logger = new QueuedFileLogger("/var/log/kern.log"); + for (int i = 0; i < 100; i++) { + Thread.sleep(1000); + + for (var line : logger.getNewlines()) { + System.out.println(" ->:" + line); + } + } + + logger.stop(); + } +}