From 66c237dba3965e5595cf01902ae92b4698684a3b Mon Sep 17 00:00:00 2001 From: Suchita Chaturvedi Date: Thu, 29 Jan 2026 16:56:25 +0000 Subject: [PATCH 1/6] 8526: Update third parties for 10.0.0 Reviewed-by: aptmac --- core/license/LICENSE.txt | 4 ++-- core/license/THIRD_PARTY_LICENSES.txt | 2 +- core/org.openjdk.jmc.common/pom.xml | 6 +++--- license/LICENSE.txt | 4 ++-- license/THIRDPARTYREADME.txt | 4 ++-- .../platform-definition-2024-12.target | 10 +++++----- .../platform-definition-2024-12/pom.xml | 4 ++-- .../platform-definition-2025-03.target | 10 +++++----- .../platform-definition-2025-03/pom.xml | 4 ++-- .../platform-definition-2025-06.target | 10 +++++----- .../platform-definition-2025-06/pom.xml | 4 ++-- .../platform-definition-2025-09.target | 8 ++++---- .../platform-definition-2025-09/pom.xml | 4 ++-- releng/platform-definitions/pom.xml | 2 +- releng/third-party/pom.xml | 10 +++++----- 15 files changed, 43 insertions(+), 43 deletions(-) diff --git a/core/license/LICENSE.txt b/core/license/LICENSE.txt index 0134be2728..a84a6f60fe 100644 --- a/core/license/LICENSE.txt +++ b/core/license/LICENSE.txt @@ -1,6 +1,6 @@ Oracle provides this software under a dual license, either the UPL or the BSD: -Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. The Universal Permissive License (UPL), Version 1.0 @@ -40,7 +40,7 @@ SOFTWARE. The BSD 3-Clause License (BSD): -Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved. +Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/core/license/THIRD_PARTY_LICENSES.txt b/core/license/THIRD_PARTY_LICENSES.txt index 6c78f0f145..af3ffa0a02 100644 --- a/core/license/THIRD_PARTY_LICENSES.txt +++ b/core/license/THIRD_PARTY_LICENSES.txt @@ -47,7 +47,7 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== -## org.lz4:lz4-java@1.8.0 (Apache-2.0) +## at.yawk.lz4:lz4-java@1.10.2 (Apache-2.0) URL for License – http://opensource.org/licenses/Apache-2.0 diff --git a/core/org.openjdk.jmc.common/pom.xml b/core/org.openjdk.jmc.common/pom.xml index a80a8a1b22..157589e1ff 100644 --- a/core/org.openjdk.jmc.common/pom.xml +++ b/core/org.openjdk.jmc.common/pom.xml @@ -1,6 +1,6 @@ - 2.0.3 - 2.0.2 - 2.1.3 + 2.0.5 + 2.0.3 + 2.1.4 1.0.3 1.2.3 - 1.8.0 + 1.10.2 2.2.2 2.0.0 12.1.0 @@ -104,7 +104,7 @@ org.owasp.encoder:encoder:${owasp.encoder.version} - org.lz4:lz4-java:${lz4.version} + at.yawk.lz4:lz4-java:${lz4.version} org.hdrhistogram:HdrHistogram:${hdrhistogram.version} From 643f98113fef8917664fb7a813900ff0e3176c0a Mon Sep 17 00:00:00 2001 From: Marcus Hirt Date: Thu, 29 Jan 2026 17:16:00 +0000 Subject: [PATCH 2/6] 8512: Fixing various minor typos and nits Reviewed-by: aptmac, clanger --- CONTRIBUTING.md | 3 +-- CONTRIBUTORS.md | 4 ++-- README.md | 10 +++++----- docs/devguide/README.md | 6 +++--- releng/tools/README.md | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc94eb7ef0..5235d2c186 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ message should be "6789: Commit message". The mailing list for JDK Mission Control is `jmc-dev@openjdk.java.net`. See for instructions -on how to subscribe of if you want to read the archives. +on how to subscribe or if you want to read the archives. ## Issues @@ -45,4 +45,3 @@ If not, ask for an invite in the jmc-dev mailing list. If you have a question or need help, please send an email to our mailing list `jmc-dev@openjdk.java.net` or stop by the JMC slack channel. - diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bd4ccad0aa..9ae18cd268 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,4 +1,4 @@ -#Contributors +# Contributors A lot of people have been involved with Mission Control over the years. This document is a thank you to everyone who has ever contributed to @@ -31,4 +31,4 @@ List of contributors, approximately in order of time spent on project: * Suchita Chaturvedi * Guru Hb * Sharath Ballal -* Peter Boström \ No newline at end of file +* Peter Boström diff --git a/README.md b/README.md index fc861c571b..c3227b9783 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mission Control is an open source production time profiling and diagnostics tool for Java. -Builds of Mission Control can currently be found in the Oracle JDK on supported platforms and in the Eclipse marketplace. +Builds of Mission Control can currently be found in the Oracle JDK on supported platforms and in the Eclipse Marketplace. For more information on Mission Control, see https://www.oracle.com/missioncontrol. @@ -217,7 +217,7 @@ Prerequisites for building Mission Control: 2. Install a JDK 21 distribution and make sure that it too is declared in the local maven toolchain. -3. Install Maven (version 3.5.x. or above) +3. Install Maven (version 3.5.x or above) On Linux or macOS you can use the `build.sh` script to build JMC: ``` @@ -268,7 +268,7 @@ If maven reports a toolchain error, e.g. : [ERROR] Please make sure you define the required toolchains in your ~/.m2/toolchains.xml file. ``` -Create or amend the local maven toolchain file by pointing to the right/any JDK 17. +Create or amend the local maven toolchain file by pointing to any JDK 17 installation.
~/.m2/toolchains.xml @@ -346,7 +346,7 @@ mvn verify -P uitests -Dspotbugs.skip=true ``` ## Filtering Test Runs -Aside from the from the simple -test Maven flag test classes that should be run/not run can be specified by means of the system properties "test.includes" and/or "test.excludes". Multiple patterns can be specified by comma separation. +Aside from the simple -test Maven flag, test classes that should be run/not run can be specified by means of the system properties "test.includes" and/or "test.excludes". Multiple patterns can be specified by comma separation. For example: @@ -422,7 +422,7 @@ There is another update site for the Eclipse plug-ins, providing plug-ins for ru application/org.openjdk.jmc.updatesite.ide/target/ ``` -To install it into Eclipe, simply open Eclipse and select Help | Install New Software... In the dialog, click Add... and then click the Archive... button. Select the built update site, e.g. +To install it into Eclipse, simply open Eclipse and select Help | Install New Software... In the dialog, click Add... and then click the Archive... button. Select the built update site, e.g. ```bash application/org.openjdk.jmc.updatesite.ide/target/org.openjdk.jmc.updatesite.ide-10.0.0-SNAPSHOT.zip diff --git a/docs/devguide/README.md b/docs/devguide/README.md index c5c17a1142..b040333eb6 100644 --- a/docs/devguide/README.md +++ b/docs/devguide/README.md @@ -38,7 +38,7 @@ Then go to _Java / Installed JREs / Execution Environments_ and select your JDK1 Ensure Eclipse compiler is set to Java 17. For that, go to _Preferences | Java / Compiler_, then for _Compiler compliance level_ choose `17`. -![Set compiler comliance level](images/setcompilercompliancelevel.png) +![Set compiler compliance level](images/setcompilercompliancelevel.png) ### Setting up the PDE target runtime environment @@ -67,7 +67,7 @@ Now there is one final preparation for the import – we need to turn off certai **Optional: Show diff against git** -By default Eclipse uses the version on disk. It may be practical to use instead the git version. Open _Window | Preferences_ then _General | Editors | Text Editors | Quick Diff_. Select _Git Revision for the reference source. +By default Eclipse uses the version on disk. It may be practical to use instead the git version. Open _Window | Preferences_ then _General | Editors | Text Editors | Quick Diff_. Select _Git Revision_ for the reference source. ![Set quick diff reference source](images/setquickdiffreferencesource.png) @@ -120,4 +120,4 @@ If you have the spotbugs plug-in installed, you should also import the spotbugs For dynamic working sets, see http://hirt.se/blog/?p=1149. -For testing: Run all tests as "JUnit Plugin-In Test" tests in eclipse and use the scripts in the `scripts` folder for running the tests. Run the class `org.openjdk.jmc.rjmx.test.testutil.JVMKeepAlive` with the VM arguments `-Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false` alongside. +For testing: Run all tests as "JUnit Plug-in Test" tests in eclipse and use the scripts in the `scripts` folder for running the tests. Run the class `org.openjdk.jmc.rjmx.test.testutil.JVMKeepAlive` with the VM arguments `-Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false` alongside. diff --git a/releng/tools/README.md b/releng/tools/README.md index 79136b3ef3..43d00a2e15 100644 --- a/releng/tools/README.md +++ b/releng/tools/README.md @@ -1,2 +1,2 @@ -#Releng Tools -This is the location to put release engineering related tools. \ No newline at end of file +# Releng Tools +This is the location to put release engineering related tools. From 8b1fbae10836f0cb41c4206094b24d74b6172117 Mon Sep 17 00:00:00 2001 From: Marcus Hirt Date: Thu, 29 Jan 2026 17:16:24 +0000 Subject: [PATCH 3/6] 8527: Add AGENTS.md file Reviewed-by: aptmac --- AGENTS.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..0cd87fae44 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,142 @@ +# JDK Mission Control (JMC) - Agent Instructions + +## Project Overview +- Java Mission Control (JMC) is an OpenJDK project with core libraries and an Eclipse RCP UI. +- Main areas: + - `core/` - Headless Java 17 libraries (can run standalone, read recordings from JDK 7+) + - `org.openjdk.jmc.common` - Core APIs and utilities + - `org.openjdk.jmc.flightrecorder` - JFR parsing and analysis + - `org.openjdk.jmc.flightrecorder.rules` - Rule framework + - `org.openjdk.jmc.flightrecorder.rules.jdk` - JDK-specific analysis rules + - `org.openjdk.jmc.testlib` - Test utilities + - `application/` - Eclipse RCP UI components (Java 21) + - `agent/` - JFR Agent for bytecode instrumentation + +## Build & Test +- `./build.sh --installCore` - Install core libraries +- `./build.sh --packageJmc` - Package full JMC application +- `./build.sh --test` - Run standard tests +- `./build.sh --testUi` - Run UI tests +- `mvn verify -Dtest.includes=**/*TestName*` - Run specific tests +- `mvn verify -Dspotbugs.skip=true` - Skip SpotBugs during verification + +### Build Scripts +- `scripts/runcoretests.sh` - Run core library tests +- `scripts/runapptests.sh` - Run application tests +- `scripts/runagenttests.sh` - Run agent tests +- `scripts/startp2.sh` - Start local P2 repository for application build + +### Eclipse Platform Profiles +Build against specific Eclipse platform versions using Maven profiles: +```bash +mvn verify -P 2024-12 +``` + +## Java Version & Toolchains +- **Java Version**: JDK 21 for application build, JDK 17 for core components +- Requires both JDK 17 and JDK 21 configured in `~/.m2/toolchains.xml`. + +Example `~/.m2/toolchains.xml`: +```xml + + + jdk + + JavaSE-17 + 17 + + + /path/to/jdk17 + + + + jdk + + JavaSE-21 + 21 + + + /path/to/jdk21 + + + +``` + +## Code Style & Formatting +- Java formatting follows the Eclipse profile in `configuration/ide/eclipse/formatting/formatting.xml`. +- Java cleanup rules are in `configuration/ide/eclipse/formatting/clean-up-with-formatting.xml`. +- JavaScript formatting follows `configuration/ide/eclipse/formatting/formattingjs.xml`. +- Key formatter settings (from the Eclipse profiles): + - Tabs for indentation (`tabulation.char=tab`, size 4). + - Line length 120 (`lineSplit=120`). + - Javadoc/comment line length 100 (`comment.line_length=100`). +- Avoid star imports; remove unused imports. +- **Naming**: Follow Eclipse/Java standard conventions, be consistent. + +## Logging / Error Handling +- Use `Level.FINE` for expected exceptions (e.g., `CancellationException`). +- Use `Level.SEVERE` for unexpected failures. +- Filter cancellations from error logs during model rebuilding. +- Check SpotBugs exceptions for guidance. + +## Commit Messages +- Format: `JIRA_NUMBER: Commit message` (example: `6789: Fix bug`). +- Issue tracker: https://bugs.openjdk.org/projects/JMC/issues + +## Copyright Headers +- All modified files must have the current year in the copyright header. +- CI validates this via `scripts/checkcopyrightyear.sh`. +- Affected file types: `*.java`, `*.htm`, `pom.xml`, `*.properties`. + +## Static Analysis +- **Checkstyle**: Enforces no star imports, no redundant/unused imports (`configuration/checkstyle/checkstyle.xml`). +- **SpotBugs**: Static bug detection with exclusions in `configuration/spotbugs/spotbugs-exclude.xml`. +- **Spotless**: Code formatting enforced during Maven validate phase. + +## Internationalization (i18n) +- Core modules: Place `messages.properties`, `messages_ja.properties`, `messages_zh.properties` in `internal` packages. +- Application modules: Use separate l10n plugin modules (e.g., `org.openjdk.jmc.*.ja`, `org.openjdk.jmc.*.zh_CN`). +- Access strings via `Messages.getString(Messages.MESSAGE_KEY)`. + +## Writing Flight Recorder Rules +Rules analyze JFR recordings and provide recommendations to users. + +### Creating a New Rule +1. Extend `AbstractRule`: +```java +public class MyRule extends AbstractRule { + public MyRule() { + super("MyRuleId", Messages.getString(Messages.MY_RULE_NAME), + JfrRuleTopics.TOPIC_NAME, CONFIGURATION_ATTRIBUTES, + RESULT_ATTRIBUTES, Collections.emptyMap()); + } + + @Override + protected IResult getResult(IItemCollection items, IPreferenceValueProvider vp, + IResultValueProvider rp) { + // Analyze items using ItemFilters, Aggregators, etc. + return ResultBuilder.createFor(this, vp) + .setSeverity(Severity.get(score)) + .setSummary("Summary message") + .setExplanation("Detailed explanation") + .build(); + } +} +``` + +2. Register the rule in `META-INF/services/org.openjdk.jmc.flightrecorder.rules.IRule`: +``` +org.openjdk.jmc.flightrecorder.rules.jdk.mypackage.MyRule +``` + +### Core Item Processing APIs +- `IItemCollection` - Collection of JFR events to query +- `IItemFilter` - Filter events (use `ItemFilters` factory) +- `IAggregator` - Aggregate values (use `Aggregators` factory) +- `IAttribute` - Access event attributes (see `JdkAttributes`) +- `RulesToolkit` - Utility methods for rule implementations + +## Testing +- Unit tests in `src/test/java` using JUnit 4. +- Test resources in `src/test/resources`. +- JDP multicast tests are automatically skipped on macOS. From a7a5ff7071702da561572745f76db454fe9eccf5 Mon Sep 17 00:00:00 2001 From: Marcus Hirt Date: Mon, 2 Feb 2026 13:05:58 +0000 Subject: [PATCH 4/6] 8528: UI tests fail after platform upgrade Reviewed-by: aptmac --- .../jmc/rjmx/internal/ServerHandle.java | 21 +++++++------------ .../internal/DefaultConnectionHandle.java | 8 +++---- .../platform-definition-2025-09.target | 2 ++ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/application/org.openjdk.jmc.rjmx/src/main/java/org/openjdk/jmc/rjmx/internal/ServerHandle.java b/application/org.openjdk.jmc.rjmx/src/main/java/org/openjdk/jmc/rjmx/internal/ServerHandle.java index 1ddabb3391..67471e5d19 100644 --- a/application/org.openjdk.jmc.rjmx/src/main/java/org/openjdk/jmc/rjmx/internal/ServerHandle.java +++ b/application/org.openjdk.jmc.rjmx/src/main/java/org/openjdk/jmc/rjmx/internal/ServerHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -224,29 +224,22 @@ public synchronized State getState() { } private static class CleanupAction implements Runnable { - private final List connectionHandles; + private final RJMXConnection connection; CleanupAction(RJMXConnection connection, List connectionHandles) { - this.connectionHandles = new ArrayList<>(connectionHandles); + // Store the connection so we can close it as a fallback if dispose() was never called. + // Note: We don't copy connectionHandles because it would be empty at construction time + // and handles are managed via explicit close() calls anyway. + this.connection = connection; } @Override public void run() { try { - disconnectQuietly(); + connection.close(); } catch (Exception e) { // Ignore all exceptions during cleanup } } - - private void disconnectQuietly() { - for (DefaultConnectionHandle handle : connectionHandles) { - try { - IOToolkit.closeSilently(handle); - } catch (Exception e) { - // Ignore exceptions during cleanup - } - } - } } } diff --git a/core/org.openjdk.jmc.rjmx.common/src/main/java/org/openjdk/jmc/rjmx/common/internal/DefaultConnectionHandle.java b/core/org.openjdk.jmc.rjmx.common/src/main/java/org/openjdk/jmc/rjmx/common/internal/DefaultConnectionHandle.java index be0c355602..8ed0789ef5 100644 --- a/core/org.openjdk.jmc.rjmx.common/src/main/java/org/openjdk/jmc/rjmx/common/internal/DefaultConnectionHandle.java +++ b/core/org.openjdk.jmc.rjmx.common/src/main/java/org/openjdk/jmc/rjmx/common/internal/DefaultConnectionHandle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -241,18 +241,18 @@ public String getDescription() { private static class CleanupAction implements Runnable { private final Map, Object> services; - private final RJMXConnection connection; CleanupAction(Map, Object> services, RJMXConnection connection) { this.services = new LinkedHashMap<>(services); - this.connection = connection; + // Note: We intentionally do NOT store or close the RJMXConnection here. + // The connection is shared among multiple DefaultConnectionHandle instances + // and is owned by ServerHandle, which is responsible for closing it. } @Override public void run() { try { shutdownServicesQuietly(services); - connection.close(); } catch (Exception e) { // Ignore all exceptions during cleanup } diff --git a/releng/platform-definitions/platform-definition-2025-09/platform-definition-2025-09.target b/releng/platform-definitions/platform-definition-2025-09/platform-definition-2025-09.target index 27544a7a79..1b3e48a9cb 100644 --- a/releng/platform-definitions/platform-definition-2025-09/platform-definition-2025-09.target +++ b/releng/platform-definitions/platform-definition-2025-09/platform-definition-2025-09.target @@ -85,6 +85,8 @@ + + From 020f06d717e32451b641b6b75e391410f0089b69 Mon Sep 17 00:00:00 2001 From: Jaroslav Bachorik Date: Mon, 2 Feb 2026 13:31:05 +0000 Subject: [PATCH 5/6] 8475: Writing events with fields not explicitly set can corrupt the recording Reviewed-by: hirt --- .../jmc/flightrecorder/writer/Chunk.java | 20 +- .../flightrecorder/writer/TypedValueImpl.java | 58 +++- .../jmc/flightrecorder/writer/api/Type.java | 28 ++ .../writer/api/TypedValueBuilder.java | 35 +- .../writer/ImplicitEventFieldsTest.java | 299 ++++++++++++++++++ 5 files changed, 426 insertions(+), 14 deletions(-) create mode 100644 core/tests/org.openjdk.jmc.flightrecorder.writer.test/src/main/java/org/openjdk/jmc/flightrecorder/writer/ImplicitEventFieldsTest.java diff --git a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/Chunk.java b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/Chunk.java index d49e13477b..e417179193 100644 --- a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/Chunk.java +++ b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/Chunk.java @@ -93,10 +93,6 @@ private void writeBuiltinType(LEB128Writer writer, TypedValueImpl typedValue) { throw new IllegalArgumentException(); } - if (value == null && builtin != TypesImpl.Builtin.STRING) { - // skip the non-string built-in values - return; - } switch (builtin) { case STRING: { if (value == null) { @@ -114,35 +110,35 @@ private void writeBuiltinType(LEB128Writer writer, TypedValueImpl typedValue) { break; } case BYTE: { - writer.writeByte((byte) value); + writer.writeByte(value == null ? (byte) 0 : (byte) value); break; } case CHAR: { - writer.writeChar((char) value); + writer.writeChar(value == null ? (char) 0 : (char) value); break; } case SHORT: { - writer.writeShort((short) value); + writer.writeShort(value == null ? (short) 0 : (short) value); break; } case INT: { - writer.writeInt((int) value); + writer.writeInt(value == null ? 0 : (int) value); break; } case LONG: { - writer.writeLong((long) value); + writer.writeLong(value == null ? 0L : (long) value); break; } case FLOAT: { - writer.writeFloat((float) value); + writer.writeFloat(value == null ? 0.0f : (float) value); break; } case DOUBLE: { - writer.writeDouble((double) value); + writer.writeDouble(value == null ? 0.0 : (double) value); break; } case BOOLEAN: { - writer.writeBoolean((boolean) value); + writer.writeBoolean(value != null && (boolean) value); break; } default: { diff --git a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/TypedValueImpl.java b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/TypedValueImpl.java index a70a1c9f64..b5114f862b 100644 --- a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/TypedValueImpl.java +++ b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/TypedValueImpl.java @@ -33,6 +33,7 @@ */ package org.openjdk.jmc.flightrecorder.writer; +import org.openjdk.jmc.flightrecorder.writer.api.Annotation; import org.openjdk.jmc.flightrecorder.writer.api.TypedValueBuilder; import org.openjdk.jmc.flightrecorder.writer.api.TypedValue; import org.openjdk.jmc.flightrecorder.writer.util.NonZeroHashCode; @@ -142,13 +143,68 @@ public List getFieldValues() { for (TypedFieldImpl field : type.getFields()) { TypedFieldValueImpl value = fields.get(field.getName()); if (value == null) { - value = new TypedFieldValueImpl(field, field.getType().nullValue()); + value = new TypedFieldValueImpl(field, getDefaultImplicitFieldValue(field)); } values.add(value); } return values; } + /** + * Gets the default value for a field when not explicitly provided by the user. + *

+ * For event types (jdk.jfr.Event): + *

    + *
  • Fields annotated with {@code @Timestamp} receive {@link System#nanoTime()} as default, + * providing a monotonic timestamp that will be >= the chunk's startTicks
  • + *
  • Other fields receive null values
  • + *
+ *

+ * Note: JFR timestamps are stored as ticks relative to the chunk start, so the parser will + * convert this absolute tick value to chunk-relative during reading. + *

+ * Tick Frequency Assumption: This implementation assumes a 1:1 tick frequency + * (1 tick = 1 nanosecond) as currently hardcoded in {@code RecordingImpl}. If the tick + * frequency becomes configurable in the future, {@link System#nanoTime()} values will need to + * be converted to ticks using: {@code nanoTime * ticksPerSecond / 1_000_000_000L}. + * + * @param field + * the field to get default value for + * @return the default value for the field + */ + private TypedValueImpl getDefaultImplicitFieldValue(TypedFieldImpl field) { + if (!"jdk.jfr.Event".equals(type.getSupertype())) { + return field.getType().nullValue(); + } + + // Check if field is annotated with @Timestamp (any value means it's chunk-relative) + if (hasTimestampAnnotation(field)) { + // Use current nanoTime as default - will be valid and >= chunk startTicks + // NOTE: Assumes 1:1 tick frequency (1 tick = 1 ns) as per RecordingImpl line 280 + return field.getType().asValue(System.nanoTime()); + } + + // For all other fields, return null value + // Null builtin values are handled properly by Chunk.writeBuiltinType() + return field.getType().nullValue(); + } + + /** + * Checks if a field has the {@code @Timestamp} annotation. + * + * @param field + * the field to check + * @return true if the field is annotated with @Timestamp + */ + private boolean hasTimestampAnnotation(TypedFieldImpl field) { + for (Annotation annotation : field.getAnnotations()) { + if ("jdk.jfr.Timestamp".equals(annotation.getType().getTypeName())) { + return true; + } + } + return false; + } + long getConstantPoolIndex() { return cpIndex; } diff --git a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/Type.java b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/Type.java index bc10ff2250..4265666453 100644 --- a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/Type.java +++ b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/Type.java @@ -201,6 +201,34 @@ public interface Type extends NamedType { TypedValue asValue(Object value); /** + * Creates a typed null value for this type. + *

+ * Use this method when you need to pass a null value for optional or missing complex-type + * fields. Passing {@code null} directly to + * {@link TypedValueBuilder#putField(String, TypedValue)} causes compilation ambiguity because + * the method is overloaded with multiple parameter types. + *

+ * For primitive types (int, long, String, etc.), you can pass primitive default/null values + * directly. For complex types (Thread, StackTrace, custom types), use this method to create a + * properly typed null value. + *

+ * Example: + * + *

+	 * {
+	 * 	@code
+	 * 	Types types = recording.getTypes();
+	 * 	Type stackTraceType = types.getType(Types.JDK.STACK_TRACE);
+	 * 	Type threadType = types.getType(Types.JDK.THREAD);
+	 *
+	 * 	Type eventType = recording.registerEventType("custom.Event");
+	 * 	recording.writeEvent(eventType.asValue(builder -> {
+	 * 		builder.putField("startTime", System.nanoTime()).putField("stackTrace", stackTraceType.nullValue()) // typed null
+	 * 				.putField("eventThread", threadType.nullValue()); // typed null
+	 * 	}));
+	 * }
+	 * 
+ * * @return a specific {@linkplain TypedValue} instance designated as the {@literal null} value * for this type */ diff --git a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/TypedValueBuilder.java b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/TypedValueBuilder.java index 3c4338826a..2937345ed3 100644 --- a/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/TypedValueBuilder.java +++ b/core/org.openjdk.jmc.flightrecorder.writer/src/main/java/org/openjdk/jmc/flightrecorder/writer/api/TypedValueBuilder.java @@ -36,7 +36,40 @@ import java.util.Map; import java.util.function.Consumer; -/** A fluent API for lazy initialization of a composite type value */ +/** + * A fluent API for lazy initialization of a composite type value. + *

+ * This builder provides a chainable interface for setting field values in complex types. Use it + * with {@link Type#asValue(java.util.function.Consumer)} to construct typed values. + *

Handling Null Values

+ *

+ * When setting field values, avoid passing {@code null} directly as it causes compilation ambiguity + * due to overloaded methods. Instead: + *

    + *
  • For primitive types (String, int, long, etc.): cast to the specific type, e.g., + * {@code (String) null}
  • + *
  • For complex types (Thread, StackTrace, custom types): use {@link Type#nullValue()}
  • + *
+ *

+ * Example: + * + *

+ * {
+ * 	@code
+ * 	Types types = recording.getTypes();
+ * 	Type threadType = types.getType(Types.JDK.THREAD);
+ *
+ * 	Type eventType = recording.registerEventType("custom.Event", builder -> {
+ * 		builder.addField("message", Types.Builtin.STRING).addField("thread", Types.JDK.THREAD);
+ * 	});
+ *
+ * 	recording.writeEvent(eventType.asValue(builder -> {
+ * 		builder.putField("message", (String) null) // primitive null with cast
+ * 				.putField("thread", threadType.nullValue()); // complex type null
+ * 	}));
+ * }
+ * 
+ */ public interface TypedValueBuilder { Type getType(); diff --git a/core/tests/org.openjdk.jmc.flightrecorder.writer.test/src/main/java/org/openjdk/jmc/flightrecorder/writer/ImplicitEventFieldsTest.java b/core/tests/org.openjdk.jmc.flightrecorder.writer.test/src/main/java/org/openjdk/jmc/flightrecorder/writer/ImplicitEventFieldsTest.java new file mode 100644 index 0000000000..d6923c8669 --- /dev/null +++ b/core/tests/org.openjdk.jmc.flightrecorder.writer.test/src/main/java/org/openjdk/jmc/flightrecorder/writer/ImplicitEventFieldsTest.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, Datadog, Inc. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at http://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.flightrecorder.writer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openjdk.jmc.common.item.Attribute; +import org.openjdk.jmc.common.item.IItem; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.common.item.IMemberAccessor; +import org.openjdk.jmc.common.unit.IQuantity; +import org.openjdk.jmc.common.unit.UnitLookup; +import org.openjdk.jmc.flightrecorder.JfrAttributes; +import org.openjdk.jmc.flightrecorder.JfrLoaderToolkit; +import org.openjdk.jmc.flightrecorder.writer.api.Recording; +import org.openjdk.jmc.flightrecorder.writer.api.Recordings; +import org.openjdk.jmc.flightrecorder.writer.api.Type; +import org.openjdk.jmc.flightrecorder.writer.api.Types; + +/** + * Tests for implicit event fields (stackTrace, eventThread, startTime) handling. + *

+ * These tests verify that events can be written without explicitly providing values for implicit + * fields, and that the Writer API automatically provides default values for them. + */ +@SuppressWarnings("restriction") +class ImplicitEventFieldsTest { + private Recording recording; + private Path jfrPath; + + @BeforeEach + void setup() throws Exception { + jfrPath = Files.createTempFile("jfr-writer-test-implicit-", ".jfr"); + recording = Recordings.newRecording(jfrPath); + } + + @AfterEach + void teardown() throws Exception { + if (recording != null) { + recording.close(); + } + if (jfrPath != null) { + Files.deleteIfExists(jfrPath); + } + } + + /** + * Tests that an event can be written without implicit fields explicitly set. + *

+ * This test reproduces the issue where field values appear shifted when implicit fields are not + * provided. After the fix, this should pass. + */ + @Test + void eventWithoutImplicitFields() throws Exception { + Type eventType = recording.registerEventType("test.MinimalEvent", builder -> { + builder.addField("customField", Types.Builtin.LONG); + }); + + // Write event WITHOUT setting implicit fields + recording.writeEvent(eventType.asValue(builder -> { + builder.putField("customField", 12345L); + })).close(); + + // Verify recording parses correctly + IItemCollection events = JfrLoaderToolkit.loadEvents(jfrPath.toFile()); + assertTrue(events.hasItems(), "Recording should contain events"); + + events.forEach(itemType -> { + itemType.forEach(item -> { + // Verify implicit fields have defaults + IQuantity startTime = JfrAttributes.START_TIME.getAccessor(itemType.getType()).getMember(item); + assertNotNull(startTime, "startTime should have a default value"); + + // Verify custom field has correct value (not shifted to startTime) + IMemberAccessor accessor = Attribute + .attr("customField", "customField", UnitLookup.RAW_NUMBER).getAccessor(itemType.getType()); + assertNotNull(accessor, "Accessor for customField should not be null"); + Number customFieldValue = accessor.getMember(item); + assertNotNull(customFieldValue, "customField should have a value"); + assertEquals(12345L, customFieldValue.longValue(), + "customField should be 12345, not shifted to startTime"); + }); + }); + } + + /** + * Tests that explicit startTime values are respected and not overridden by the default. + *

+ * Note: JFR stores timestamps as ticks relative to the chunk start, so the parser converts the + * stored tick value to absolute epoch nanoseconds using the chunk header. We verify that the + * timestamp is reasonable (positive epoch time) rather than checking for exact equality. + */ + @Test + void eventWithExplicitStartTime() throws Exception { + long explicitTime = System.nanoTime(); + Type eventType = recording.registerEventType("test.ExplicitTime"); + + recording.writeEvent(eventType.asValue(builder -> { + builder.putField("startTime", explicitTime); + })).close(); + + IItemCollection events = JfrLoaderToolkit.loadEvents(jfrPath.toFile()); + assertTrue(events.hasItems(), "Recording should contain events"); + + events.forEach(itemType -> { + itemType.forEach(item -> { + IQuantity time = JfrAttributes.START_TIME.getAccessor(itemType.getType()).getMember(item); + assertNotNull(time, "startTime should not be null"); + // Verify that the timestamp is reasonable (epoch time in nanoseconds) + // Should be a recent positive timestamp, not negative or zero + long epochNanos = time.longValue(); + assertTrue(epochNanos > 0L, "Timestamp should be positive (epoch nanos)"); + }); + }); + } + + /** + * Tests an event with only implicit fields and no custom fields. + */ + @Test + void eventWithOnlyImplicitFields() throws Exception { + Type eventType = recording.registerEventType("test.ImplicitOnly"); + + recording.writeEvent(eventType.asValue(builder -> { + // Don't set any fields - rely on defaults + })).close(); + + IItemCollection events = JfrLoaderToolkit.loadEvents(jfrPath.toFile()); + assertTrue(events.hasItems(), "Recording should contain events"); + + events.forEach(itemType -> { + itemType.forEach(item -> { + IQuantity startTime = JfrAttributes.START_TIME.getAccessor(itemType.getType()).getMember(item); + assertNotNull(startTime, "startTime should have a default value"); + }); + }); + } + + /** + * Tests that multiple custom fields maintain correct alignment when implicit fields are not + * provided. + */ + @Test + void eventWithMultipleCustomFields() throws Exception { + Type eventType = recording.registerEventType("test.MultiField", builder -> { + builder.addField("field1", Types.Builtin.LONG).addField("field2", Types.Builtin.STRING).addField("field3", + Types.Builtin.INT); + }); + + recording.writeEvent(eventType.asValue(builder -> { + builder.putField("field1", 111L).putField("field2", "test-string").putField("field3", 333); + })).close(); + + IItemCollection events = JfrLoaderToolkit.loadEvents(jfrPath.toFile()); + assertTrue(events.hasItems(), "Recording should contain events"); + + events.forEach(itemType -> { + itemType.forEach(item -> { + // Verify all custom fields have correct values + // field1 is LONG → raw number + IMemberAccessor field1Accessor = Attribute + .attr("field1", "field1", UnitLookup.RAW_NUMBER).getAccessor(itemType.getType()); + assertEquals(111L, field1Accessor.getMember(item).longValue(), "field1 should be 111"); + + IMemberAccessor field2Accessor = Attribute + .attr("field2", "field2", UnitLookup.PLAIN_TEXT).getAccessor(itemType.getType()); + assertEquals("test-string", field2Accessor.getMember(item), "field2 should be 'test-string'"); + + // field3 is INT → linear number + IMemberAccessor field3Accessor = Attribute.attr("field3", "field3", UnitLookup.NUMBER) + .getAccessor(itemType.getType()); + assertEquals(333, field3Accessor.getMember(item).longValue(), "field3 should be 333"); + }); + }); + } + + /** + * Tests that all builtin types receive proper default values when not explicitly set. + *

+ * This test verifies the fix for builtin type field skipping. When builtin fields are not + * explicitly set, they should receive type-appropriate defaults (0 for numbers, false for + * boolean, null for String) instead of being skipped during serialization, which would cause + * field alignment issues. + *

+ * The test includes a final field with an explicit value to verify that field alignment remains + * correct after all the default builtin fields. + */ + @Test + void eventWithAllBuiltinFieldsUnset() throws Exception { + Type eventType = recording.registerEventType("test.AllBuiltins", builder -> { + builder.addField("byteField", Types.Builtin.BYTE).addField("charField", Types.Builtin.CHAR) + .addField("shortField", Types.Builtin.SHORT).addField("intField", Types.Builtin.INT) + .addField("longField", Types.Builtin.LONG).addField("floatField", Types.Builtin.FLOAT) + .addField("doubleField", Types.Builtin.DOUBLE).addField("booleanField", Types.Builtin.BOOLEAN) + .addField("stringField", Types.Builtin.STRING).addField("finalField", Types.Builtin.LONG); + }); + + // Write event WITHOUT setting builtin field values - all should get defaults + // Set finalField to verify field alignment is correct + recording.writeEvent(eventType.asValue(builder -> { + builder.putField("finalField", 99999L); + })).close(); + + // Verify recording parses correctly and contains the event + IItemCollection events = JfrLoaderToolkit.loadEvents(jfrPath.toFile()); + assertTrue(events.hasItems(), "Recording should contain events"); + + events.forEach(itemType -> { + itemType.forEach(item -> { + // Verify all builtin fields have appropriate default values + // BYTE → linear number + IMemberAccessor byteAccessor = Attribute + .attr("byteField", "byteField", UnitLookup.NUMBER).getAccessor(itemType.getType()); + assertEquals(0, byteAccessor.getMember(item).longValue(), "byteField should default to 0"); + + // SHORT → linear number + IMemberAccessor shortAccessor = Attribute + .attr("shortField", "shortField", UnitLookup.NUMBER).getAccessor(itemType.getType()); + assertEquals(0, shortAccessor.getMember(item).longValue(), "shortField should default to 0"); + + // INT → linear number + IMemberAccessor intAccessor = Attribute + .attr("intField", "intField", UnitLookup.NUMBER).getAccessor(itemType.getType()); + assertEquals(0, intAccessor.getMember(item).longValue(), "intField should default to 0"); + + // LONG → raw number + IMemberAccessor longAccessor = Attribute + .attr("longField", "longField", UnitLookup.RAW_NUMBER).getAccessor(itemType.getType()); + assertEquals(0L, longAccessor.getMember(item).longValue(), "longField should default to 0"); + + // FLOAT → linear number + IMemberAccessor floatAccessor = Attribute + .attr("floatField", "floatField", UnitLookup.NUMBER).getAccessor(itemType.getType()); + assertEquals(0.0, floatAccessor.getMember(item).doubleValue(), 0.001, + "floatField should default to 0.0"); + + // DOUBLE → linear number + IMemberAccessor doubleAccessor = Attribute + .attr("doubleField", "doubleField", UnitLookup.NUMBER).getAccessor(itemType.getType()); + assertEquals(0.0, doubleAccessor.getMember(item).doubleValue(), 0.001, + "doubleField should default to 0.0"); + + IMemberAccessor booleanAccessor = Attribute + .attr("booleanField", "booleanField", UnitLookup.FLAG).getAccessor(itemType.getType()); + assertEquals(false, booleanAccessor.getMember(item), "booleanField should default to false"); + + IMemberAccessor stringAccessor = Attribute + .attr("stringField", "stringField", UnitLookup.PLAIN_TEXT).getAccessor(itemType.getType()); + assertEquals(null, stringAccessor.getMember(item), "stringField should default to null"); + + // Verify the explicit field value is read correctly (proves field alignment is correct) + // LONG → raw number + IMemberAccessor finalAccessor = Attribute + .attr("finalField", "finalField", UnitLookup.RAW_NUMBER).getAccessor(itemType.getType()); + assertEquals(99999L, finalAccessor.getMember(item).longValue(), + "finalField should be 99999, confirming correct field alignment after all default builtin fields"); + }); + }); + } +} From f9ba5ef03796c1c05640c1180e003c6649f2e81a Mon Sep 17 00:00:00 2001 From: Alex Macdonald Date: Mon, 2 Feb 2026 14:15:50 +0000 Subject: [PATCH 6/6] 8476: Websocket server is tied to JfrEditor Reviewed-by: hirt --- application/coverage/pom.xml | 9 +- .../org.openjdk.jmc.feature.core/feature.xml | 9 +- .../META-INF/MANIFEST.MF | 1 + .../flightrecorder/ui/FlightRecorderUI.java | 19 +-- .../jmc/flightrecorder/ui/JfrEditor.java | 33 +---- .../ui/messages/internal/Messages.java | 5 +- .../ui/preferences/GeneralPage.java | 90 +----------- .../ui/messages/internal/messages.properties | 5 +- .../META-INF/MANIFEST.MF | 25 ++++ .../build.properties | 39 +++++ .../plugin.properties | 34 +++++ .../org.openjdk.jmc.ui.websocket/plugin.xml | 52 +++++++ .../org.openjdk.jmc.ui.websocket/pom.xml | 49 +++++++ .../jmc/ui/websocket/MCWebsocketServer.java} | 85 ++++------- .../jmc/ui/websocket/WebsocketPlugin.java | 134 ++++++++++++++++++ .../ui/websocket/preferences/Messages.java | 51 +++++++ .../preferences/PreferenceConstants.java | 41 ++++++ .../preferences/PreferenceInitializer.java | 47 ++++++ .../preferences/WebsocketPreferencePage.java | 86 +++++++++++ .../websocket/preferences/messages.properties | 3 + application/pom.xml | 3 +- configuration/spotbugs/spotbugs-exclude.xml | 2 + 22 files changed, 621 insertions(+), 201 deletions(-) create mode 100644 application/org.openjdk.jmc.ui.websocket/META-INF/MANIFEST.MF create mode 100644 application/org.openjdk.jmc.ui.websocket/build.properties create mode 100644 application/org.openjdk.jmc.ui.websocket/plugin.properties create mode 100644 application/org.openjdk.jmc.ui.websocket/plugin.xml create mode 100644 application/org.openjdk.jmc.ui.websocket/pom.xml rename application/{org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/websocket/WebsocketServer.java => org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/MCWebsocketServer.java} (72%) create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/WebsocketPlugin.java create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/Messages.java create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceConstants.java create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceInitializer.java create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/WebsocketPreferencePage.java create mode 100644 application/org.openjdk.jmc.ui.websocket/src/main/resources/org/openjdk/jmc/ui/websocket/preferences/messages.properties diff --git a/application/coverage/pom.xml b/application/coverage/pom.xml index 3b0a7b32a7..b86a865921 100644 --- a/application/coverage/pom.xml +++ b/application/coverage/pom.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + diff --git a/application/org.openjdk.jmc.ui.websocket/pom.xml b/application/org.openjdk.jmc.ui.websocket/pom.xml new file mode 100644 index 0000000000..1bca6528d9 --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + org.openjdk.jmc + missioncontrol.application + ${revision}${changelist} + + org.openjdk.jmc.ui.websocket + eclipse-plugin + + ${project.basedir}/../../configuration + + diff --git a/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/websocket/WebsocketServer.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/MCWebsocketServer.java similarity index 72% rename from application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/websocket/WebsocketServer.java rename to application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/MCWebsocketServer.java index f8356e7922..d49334a4cc 100644 --- a/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/websocket/WebsocketServer.java +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/MCWebsocketServer.java @@ -1,6 +1,6 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. - * Copyright (c) 2021, 2025, Datadog, Inc. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Datadog, Inc. All rights reserved. * * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -31,9 +31,10 @@ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.openjdk.jmc.flightrecorder.ui.websocket; +package org.openjdk.jmc.ui.websocket; import java.io.IOException; +import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.HashMap; import java.util.List; @@ -45,13 +46,12 @@ import java.util.logging.Level; import java.util.stream.Collectors; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.ee9.servlet.ServletContextHandler; import org.eclipse.jetty.ee9.websocket.api.Session; import org.eclipse.jetty.ee9.websocket.api.WebSocketAdapter; import org.eclipse.jetty.ee9.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.ee9.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; import org.openjdk.jmc.common.item.IItemCollection; import org.openjdk.jmc.flightrecorder.serializers.dot.DotSerializer; import org.openjdk.jmc.flightrecorder.serializers.json.FlameGraphJsonSerializer; @@ -60,14 +60,8 @@ import org.openjdk.jmc.flightrecorder.stacktrace.FrameSeparator.FrameCategorization; import org.openjdk.jmc.flightrecorder.stacktrace.graph.StacktraceGraphModel; import org.openjdk.jmc.flightrecorder.stacktrace.tree.StacktraceTreeModel; -import org.openjdk.jmc.flightrecorder.ui.FlightRecorderUI; - -public class WebsocketServer { - - private static int MAX_MESSAGE_SIZE = 1024 * 1024 * 1024; - private static int IDLE_TIMEOUT_MINUTES = 5; - private final int port; +public class MCWebsocketServer { private Server server; private List handlers = new CopyOnWriteArrayList<>(); private List treeHandlers = new CopyOnWriteArrayList<>(); @@ -75,16 +69,11 @@ public class WebsocketServer { private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private IItemCollection currentSelection = null; - public WebsocketServer(int port) { - this.port = port; - executorService.execute(() -> startServer()); + public MCWebsocketServer(int port) { + executorService.execute(() -> startServer(port)); } - public int getPort() { - return port; - } - - private void startServer() { + public void startServer(int port) { server = new Server(); ServerConnector connector = new ServerConnector(server); connector.setHost("127.0.0.1"); @@ -96,24 +85,22 @@ private void startServer() { server.setHandler(context); JettyWebSocketServletContainerInitializer.configure(context, (servletContext, container) -> { - container.setMaxBinaryMessageSize(MAX_MESSAGE_SIZE); - container.setIdleTimeout(Duration.ofMinutes(IDLE_TIMEOUT_MINUTES)); + container.setMaxBinaryMessageSize(Long.MAX_VALUE); + container.setIdleTimeout(Duration.ofMinutes(Long.MAX_VALUE)); container.addMapping("/events/*", (req, resp) -> { - // try to send the current selection when the client connects - // for simplicity, we serialise for every new connection - String eventsJson = WebsocketServer.toEventsJsonString(currentSelection); + String eventsJson = MCWebsocketServer.toEventsJsonString(currentSelection); WebsocketConnectionHandler handler = new WebsocketConnectionHandler(eventsJson); handlers.add(handler); return handler; }); container.addMapping("/tree/*", (req, resp) -> { - String treeJson = WebsocketServer.toTreeModelJsonString(currentSelection); + String treeJson = MCWebsocketServer.toTreeModelJsonString(currentSelection); WebsocketConnectionHandler handler = new WebsocketConnectionHandler(treeJson); treeHandlers.add(handler); return handler; }); container.addMapping("/graph/*", (req, resp) -> { - String dot = WebsocketServer.toGraphModelDotString(currentSelection); + String dot = MCWebsocketServer.toGraphModelDotString(currentSelection); WebsocketConnectionHandler handler = new WebsocketConnectionHandler(dot); graphHandlers.add(handler); return handler; @@ -121,13 +108,9 @@ private void startServer() { }); try { - WebSocketUpgradeFilter.ensureFilter(context.getServletContext()); - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, - "Starting websocket server listening on port " + port); server.start(); - server.join(); } catch (Exception e) { - FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to start websocket server", e); + WebsocketPlugin.getLogger().log(Level.SEVERE, "Failed to start websocket server", e); } } @@ -139,15 +122,15 @@ public void notifyAll(IItemCollection events) { } private void notifyAllEventHandlers(IItemCollection events) { - handlers = notifyAllHandlers(events, handlers, WebsocketServer::toEventsJsonString); + handlers = notifyAllHandlers(events, handlers, MCWebsocketServer::toEventsJsonString); } private void notifyAllGraphHandlers(IItemCollection events) { - graphHandlers = notifyAllHandlers(events, graphHandlers, WebsocketServer::toGraphModelDotString); + graphHandlers = notifyAllHandlers(events, graphHandlers, MCWebsocketServer::toGraphModelDotString); } private void notifyAllTreeHandlers(IItemCollection events) { - treeHandlers = notifyAllHandlers(events, treeHandlers, WebsocketServer::toTreeModelJsonString); + treeHandlers = notifyAllHandlers(events, treeHandlers, MCWebsocketServer::toTreeModelJsonString); } private static String toEventsJsonString(IItemCollection items) { @@ -187,15 +170,8 @@ private List notifyAllHandlers( return handlers; } - public void shutdown() { - try { - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, - "Stopping websocket server listening on port " + port); - server.stop(); - // TODO: see if we need to cleanup executor service and thread - } catch (Exception e) { - FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to stop websocket server", e); - } + public void shutdown() throws Exception { + server.stop(); } private static class WebsocketConnectionHandler extends WebSocketAdapter { @@ -207,12 +183,12 @@ private static class WebsocketConnectionHandler extends WebSocketAdapter { public void sendMessage(String message) { if (getSession() != null && isConnected()) { - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, + WebsocketPlugin.getLogger().log(Level.INFO, "Sending message to " + getSession().getRemoteAddress().toString()); try { getSession().getRemote().sendString(message); } catch (IOException e) { - FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to send websocket message", e); + WebsocketPlugin.getLogger().log(Level.SEVERE, "Failed to send websocket message", e); } } } @@ -220,15 +196,14 @@ public void sendMessage(String message) { @Override public void onWebSocketConnect(Session sess) { super.onWebSocketConnect(sess); - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, - "Socket connected to " + sess.getRemoteAddress().toString()); + WebsocketPlugin.getLogger().log(Level.INFO, "Socket connected to " + sess.getRemoteAddress().toString()); try { if (firstMessage != null) { getSession().getRemote().sendString(firstMessage); firstMessage = null; } } catch (IOException e) { - FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Failed to show outline view", e); + WebsocketPlugin.getLogger().log(Level.SEVERE, "Failed to show outline view", e); } } @@ -240,16 +215,18 @@ public void onWebSocketText(String message) { @Override public void onWebSocketClose(int statusCode, String reason) { super.onWebSocketClose(statusCode, reason); - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, "Socket closed: [" + statusCode + "] " + reason); + WebsocketPlugin.getLogger().log(Level.INFO, "Socket closed: [" + statusCode + "] " + reason); } @Override public void onWebSocketError(Throwable cause) { super.onWebSocketError(cause); - if (cause.getCause() instanceof TimeoutException) { - FlightRecorderUI.getDefault().getLogger().log(Level.INFO, "Websocket timed out"); + if (cause instanceof TimeoutException) { + WebsocketPlugin.getLogger().log(Level.INFO, "Websocket timed out"); + } else if (cause instanceof ClosedChannelException) { + WebsocketPlugin.getLogger().log(Level.INFO, "Websocket channel has closed"); } else { - FlightRecorderUI.getDefault().getLogger().log(Level.SEVERE, "Websocket error", cause); + WebsocketPlugin.getLogger().log(Level.SEVERE, "Websocket error", cause); } } } diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/WebsocketPlugin.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/WebsocketPlugin.java new file mode 100644 index 0000000000..ac7a9ab1a5 --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/WebsocketPlugin.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026 IBM Corporation. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.ui.websocket; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.ui.IStartup; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.openjdk.jmc.common.item.IItemCollection; +import org.openjdk.jmc.ui.websocket.preferences.PreferenceConstants; +import org.osgi.framework.BundleContext; + +public class WebsocketPlugin extends AbstractUIPlugin implements IStartup { + + public final static String PLUGIN_ID = "org.openjdk.jmc.ui.websocket"; //$NON-NLS-1$ + private static final Logger LOGGER = Logger.getLogger(PLUGIN_ID); + + private static WebsocketPlugin plugin; + private MCWebsocketServer server; + + public WebsocketPlugin() { + } + + public static WebsocketPlugin getDefault() { + return plugin; + } + + public static Logger getLogger() { + return LOGGER; + } + + public void start(BundleContext bundleContext) throws Exception { + super.start(bundleContext); + this.getPreferenceStore().addPropertyChangeListener(preferenceChangeListener); + plugin = this; + startServer(getCryostatPort()); + LOGGER.log(Level.INFO, "JMC Websocket Server is live!"); + } + + public void stop(BundleContext bundleContext) throws Exception { + if (server != null) { + server.shutdown(); + server = null; + } + plugin = null; + super.stop(bundleContext); + } + + public void notifyAll(IItemCollection events) { + if (server != null) { + server.notifyAll(events); + } + } + + private void startServer(int port) { + if (getServerEnabled()) { + server = new MCWebsocketServer(port); + } + } + + private void stopServer() { + if (server != null) { + try { + server.shutdown(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error shutting down the Jetty WebSocket server", e); + } + } + } + + private IPropertyChangeListener preferenceChangeListener = new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(PreferenceConstants.P_SERVER_ENABLED)) { + if (getServerEnabled()) { + startServer(getCryostatPort()); + } else { + stopServer(); + } + } + if (event.getProperty().equals(PreferenceConstants.P_SERVER_PORT) && getServerEnabled()) { + stopServer(); + startServer(getCryostatPort()); + } + } + }; + + private int getCryostatPort() { + return this.getPreferenceStore().getInt(PreferenceConstants.P_SERVER_PORT); + } + + public boolean getServerEnabled() { + return this.getPreferenceStore().getBoolean(PreferenceConstants.P_SERVER_ENABLED); + } + + @Override + public void earlyStartup() { + // do nothing, we're only implementing IStartup to force a load of the plugin + } +} diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/Messages.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/Messages.java new file mode 100644 index 0000000000..60b35bc07c --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/Messages.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026 IBM Corporation. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.ui.websocket.preferences; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = "org.openjdk.jmc.ui.websocket.preferences.messages"; //$NON-NLS-1$ + + public static String WebsocketPreferencePage_DESCRIPTION; + public static String WebsocketPreferencePage_ENABLE; + public static String WebsocketPreferencePage_PORT; + + static { + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() { + } +} diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceConstants.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceConstants.java new file mode 100644 index 0000000000..ed7ef192a2 --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026 IBM Corporation. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.ui.websocket.preferences; + +public class PreferenceConstants { + public static final String P_SERVER_ENABLED = "websocket.server.enabled"; + public static final String P_SERVER_PORT = "websocket.server.port"; + public static final boolean DEFAULT_ENABLED = false; + public static final int DEFAULT_PORT = 8029; +} diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceInitializer.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceInitializer.java new file mode 100644 index 0000000000..fb8335ff2e --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/PreferenceInitializer.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026 IBM Corporation. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.ui.websocket.preferences; + +import org.openjdk.jmc.ui.websocket.WebsocketPlugin; +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; + +public class PreferenceInitializer extends AbstractPreferenceInitializer { + @Override + public void initializeDefaultPreferences() { + IPreferenceStore store = WebsocketPlugin.getDefault().getPreferenceStore(); + store.setDefault(PreferenceConstants.P_SERVER_PORT, PreferenceConstants.DEFAULT_PORT); + store.setDefault(PreferenceConstants.P_SERVER_ENABLED, PreferenceConstants.DEFAULT_ENABLED); + } +} diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/WebsocketPreferencePage.java b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/WebsocketPreferencePage.java new file mode 100644 index 0000000000..b7fc4ccf44 --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/java/org/openjdk/jmc/ui/websocket/preferences/WebsocketPreferencePage.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026 IBM Corporation. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.ui.websocket.preferences; + +import org.openjdk.jmc.ui.websocket.WebsocketPlugin; +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; + +public class WebsocketPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + private IntegerFieldEditor portField; + + public WebsocketPreferencePage() { + super(GRID); + setPreferenceStore(WebsocketPlugin.getDefault().getPreferenceStore()); + setDescription(Messages.WebsocketPreferencePage_DESCRIPTION); + } + + @Override + public void init(IWorkbench workbench) { + } + + @Override + protected void createFieldEditors() { + addField(new BooleanFieldEditor(PreferenceConstants.P_SERVER_ENABLED, Messages.WebsocketPreferencePage_ENABLE, + getFieldEditorParent())); + portField = new IntegerFieldEditor(PreferenceConstants.P_SERVER_PORT, Messages.WebsocketPreferencePage_PORT, + getFieldEditorParent()); + addField(portField); + enableWebsocketFields(isWebsocketPluginEnabled()); + } + + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(FieldEditor.VALUE)) { + FieldEditor editor = (FieldEditor) event.getSource(); + if (PreferenceConstants.P_SERVER_ENABLED.equals(editor.getPreferenceName())) { + enableWebsocketFields((boolean) event.getNewValue()); + } + } + } + + private boolean isWebsocketPluginEnabled() { + return WebsocketPlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.P_SERVER_ENABLED); + } + + private void enableWebsocketFields(boolean enable) { + portField.setEnabled(enable, getFieldEditorParent()); + } +} diff --git a/application/org.openjdk.jmc.ui.websocket/src/main/resources/org/openjdk/jmc/ui/websocket/preferences/messages.properties b/application/org.openjdk.jmc.ui.websocket/src/main/resources/org/openjdk/jmc/ui/websocket/preferences/messages.properties new file mode 100644 index 0000000000..19710c4b5d --- /dev/null +++ b/application/org.openjdk.jmc.ui.websocket/src/main/resources/org/openjdk/jmc/ui/websocket/preferences/messages.properties @@ -0,0 +1,3 @@ +WebsocketPreferencePage_DESCRIPTION=Settings for the JMC Websocket Server +WebsocketPreferencePage_ENABLE=Enable +WebsocketPreferencePage_PORT=WebSocket Port for JFR transfer: diff --git a/application/pom.xml b/application/pom.xml index fbd9cc8b2d..f96e309eeb 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ + +