diff --git a/org.knime.core.tests/src/org/knime/core/internal/diagnostics/DiagnosticInstructionsSerializationTest.java b/org.knime.core.tests/src/org/knime/core/internal/diagnostics/DiagnosticInstructionsSerializationTest.java new file mode 100644 index 0000000000..ab2079eea6 --- /dev/null +++ b/org.knime.core.tests/src/org/knime/core/internal/diagnostics/DiagnosticInstructionsSerializationTest.java @@ -0,0 +1,132 @@ +/* + * ------------------------------------------------------------------------ + * + * Copyright by KNIME AG, Zurich, Switzerland + * Website: http://www.knime.com; Email: contact@knime.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, Version 3, as + * published by the Free Software Foundation. + * + * 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 . + * + * Additional permission under GNU GPL version 3 section 7: + * + * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. + * Hence, KNIME and ECLIPSE are both independent programs and are not + * derived from each other. Should, however, the interpretation of the + * GNU GPL Version 3 ("License") under any applicable laws result in + * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants + * you the additional permission to use and propagate KNIME together with + * ECLIPSE with only the license terms in place for ECLIPSE applying to + * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the + * license terms of ECLIPSE themselves allow for the respective use and + * propagation of ECLIPSE together with KNIME. + * + * Additional permission relating to nodes for KNIME that extend the Node + * Extension (and in particular that are based on subclasses of NodeModel, + * NodeDialog, and NodeView) and that only interoperate with KNIME through + * standard APIs ("Nodes"): + * Nodes are deemed to be separate and independent programs and to not be + * covered works. Notwithstanding anything to the contrary in the + * License, the License does not apply to Nodes, you are not required to + * license Nodes under the License, and you are granted a license to + * prepare and propagate Nodes, in each case even if such Nodes are + * propagated with or for interoperation with KNIME. The owner of a Node + * may freely choose the license terms applicable to such Node, including + * when such Node is propagated with or for interoperation with KNIME. + * --------------------------------------------------------------------- + * + * History + * Dec 13, 2025 (github-copilot): created + */ +package org.knime.core.internal.diagnostics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Path; + +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * Tests for serialization of {@link DiagnosticInstructions}. + */ +public class DiagnosticInstructionsSerializationTest { + + /** + * Tests serialization of DiagnosticInstructions from JSON. + * + * @throws Exception if an error occurs + */ + @Test + public void testSerialization() throws Exception { + final var objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + final var defaultsAsString = + objectMapper.writeValueAsString(DiagnosticInstructions.createDefaults(Path.of("diagnostic-output"))); + + final var json = """ + { + "outputDirectory" : "diagnostic-output", + "heapDump" : null, + "systemInfo" : true, + "jvmInfo" : true, + "gcInfo" : true, + "applicationHealth" : true, + "knimeInfo" : true, + "workflowManagers" : true, + "threadDump" : true + } + """; + + assertEquals("Serialized JSON should match expected", defaultsAsString.trim(), json.trim()); + } + + /** + * Tests deserialization of DiagnosticInstructions from JSON. + * + * @throws Exception if an error occurs + */ + @Test + public void testDeserialization() throws Exception { + final var objectMapper = new ObjectMapper(); + + final var json = """ + { + "outputDirectory" : "diagnostic-output", + "heapDump" : null, + "systemInfo" : false, + "jvmInfo" : false, + "gcInfo" : false, + "applicationHealth" : false, + "knimeInfo" : false, + "workflowManagers" : false, + "threadDump" : false + } + """; + + final var instructions = objectMapper.readValue(json, DiagnosticInstructions.class); + + assertEquals("Output directory should match", Path.of("diagnostic-output"), instructions.outputDirectory()); + assertNull("Heap dump path should be null", instructions.heapDumpPath()); + assertTrue("System info should be false", !instructions.systemInfo()); + assertTrue("JVM info should be false", !instructions.jvmInfo()); + assertTrue("GC info should be false", !instructions.gcInfo()); + assertTrue("Application health should be false", !instructions.applicationHealth()); + assertTrue("KNIME info should be false", !instructions.knimeInfo()); + assertTrue("Workflow managers should be false", !instructions.workflowManagers()); + assertTrue("Thread dump should be false", !instructions.threadDump()); + } + +} diff --git a/org.knime.core/src/eclipse/org/knime/core/internal/ApplicationHealthEarlyStartup.java b/org.knime.core/src/eclipse/org/knime/core/internal/ApplicationHealthEarlyStartup.java index d8bc260364..ac4d54656a 100644 --- a/org.knime.core/src/eclipse/org/knime/core/internal/ApplicationHealthEarlyStartup.java +++ b/org.knime.core/src/eclipse/org/knime/core/internal/ApplicationHealthEarlyStartup.java @@ -180,8 +180,27 @@ private static Path initializeDiagnosticsDirectory() throws IOException { final var knimeHome = KNIMEConstants.getKNIMEHomeDir(); final var diagDir = Path.of(knimeHome).resolve("diagnostics"); Files.createDirectories(diagDir); - Files.writeString(diagDir.resolve("created-by-knime"), - "This directory was created by the KNIME diagnostics system", StandardOpenOption.CREATE); + + // Generate JSON example at runtime to ensure it stays in sync with any format changes + final var objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + + final var exampleInstructions = DiagnosticInstructions.createDefaults(Path.of("diagnostic-output")); + final var jsonExample = objectMapper.writeValueAsString(exampleInstructions); + + final var instructions = String.format(""" + This directory was created by the KNIME diagnostics system. + + To create a diagnostics dump, launch an instance with the following system property: + -D%s=true + (If you see this file, you probably already did that.) + + Then create a file in this folder called 'diagnostics-output.json' with instructions such as: + + %s + """, FEATURE_FLAG_DIAGNOSTICS, jsonExample); + + Files.writeString(diagDir.resolve("created-by-knime"), instructions, StandardOpenOption.CREATE); return diagDir; } diff --git a/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/DiagnosticInstructions.java b/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/DiagnosticInstructions.java index 2a68b36349..da2d488b88 100644 --- a/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/DiagnosticInstructions.java +++ b/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/DiagnosticInstructions.java @@ -48,10 +48,14 @@ */ package org.knime.core.internal.diagnostics; +import java.io.IOException; import java.nio.file.Path; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; /** * Instructions for diagnostic dumps, specifying what to collect and where to store the output. @@ -69,10 +73,9 @@ * * @since 5.10 */ -@JsonInclude(JsonInclude.Include.NON_NULL) public record DiagnosticInstructions(// - @JsonProperty("outputDirectory") Path outputDirectory, // - @JsonProperty("heapDump") Path heapDumpPath, // + @JsonSerialize(using = PathAsStringSerializer.class) @JsonProperty("outputDirectory") Path outputDirectory, // + @JsonSerialize(using = PathAsStringSerializer.class) @JsonProperty("heapDump") Path heapDumpPath, // @JsonProperty("systemInfo") boolean systemInfo, // SystemInfoCollector @JsonProperty("jvmInfo") boolean jvmInfo, // JvmInfoCollector @JsonProperty("gcInfo") boolean gcInfo, // GcInfoCollector @@ -120,4 +123,22 @@ public static DiagnosticInstructions createWithHeapDump(final Path outputDirecto true // Thread dump ); } + + /** + * Custom serializer for {@link Path} objects that outputs simple path strings instead of file:// URLs. Without + * this, Jackson's default Path serialization produces URL format (e.g., "file:///path") instead of plain path + * syntax. + */ + static final class PathAsStringSerializer extends JsonSerializer { + @Override + public void serialize(final Path path, final JsonGenerator jsonGenerator, + final SerializerProvider serializerProvider) throws IOException { + if (path != null) { + jsonGenerator.writeString(path.toString()); + } else { + jsonGenerator.writeNull(); + } + } + } } + diff --git a/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/collectors/SystemInfoCollector.java b/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/collectors/SystemInfoCollector.java index ef13dda1f8..5dd77de76f 100644 --- a/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/collectors/SystemInfoCollector.java +++ b/org.knime.core/src/eclipse/org/knime/core/internal/diagnostics/collectors/SystemInfoCollector.java @@ -218,7 +218,7 @@ private static void writeProcessInfo(final String os, final JsonGenerator genera final String[][] topProcesses; try { - topProcesses = getTopProcesses(os, 5); + topProcesses = getTopProcesses(os, 100); // some large (but limited) number if (topProcesses != null && topProcesses.length > 0) { generator.writeArrayFieldStart("topProcesses"); for (String[] process : topProcesses) {