80: Introduce an explicit Hub-managed JavaFX runtime service

This commit is contained in:
2026-09-02 23:23:16 +02:00
parent 0cb125ca9d
commit ed952fd6b1
23 changed files with 732 additions and 67 deletions
@@ -3,8 +3,8 @@ package com.r35157.evelyn.emc.impl.ref;
import com.fanitas.evelyn.core.Evelyn;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.process.NenjimProcess;
import com.r35157.service.javafx.JavaFxRuntimeService;
import org.jetbrains.annotations.NotNull;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
@@ -34,16 +34,20 @@ public final class EvelynMissionControlImpl implements
public EvelynMissionControlImpl(
@NotNull Evelyn evelynServiceProd,
@NotNull Evelyn evelynServiceTest
@NotNull Evelyn evelynServiceTest,
@NotNull JavaFxRuntimeService javaFxRuntimeService
) {
this.evelynProd = Objects.requireNonNull(evelynServiceProd, "evelynServiceProd");
this.evelynTest = Objects.requireNonNull(evelynServiceTest, "evelynServiceTest");
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
refreshTimeline.setCycleCount(Timeline.INDEFINITE);
}
@Override
public void start() {
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
}
private void showWindow() {
@@ -311,6 +315,7 @@ public final class EvelynMissionControlImpl implements
private final Evelyn evelynProd;
private final Evelyn evelynTest;
private final JavaFxRuntimeService javaFxRuntimeService;
private final Timeline refreshTimeline = new Timeline(new KeyFrame(
Duration.seconds(CHART_REFRESH_SECONDS),
event -> refreshCharts()
@@ -1,25 +0,0 @@
package com.r35157.libs.javafx;
import javafx.application.Platform;
public final class JavaFxRuntime {
public static void runLater(Runnable runnable) {
initialize();
Platform.runLater(runnable);
}
private static synchronized void initialize() {
if (initialized) {
return;
}
Platform.startup(() -> Platform.setImplicitExit(false));
initialized = true;
}
private JavaFxRuntime() {
}
private static boolean initialized;
}
@@ -1,14 +1,24 @@
package com.r35157.nenjim.composer.impl.ref;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.composer.NenjimComposer;
import com.r35157.service.javafx.JavaFxRuntimeService;
import javafx.stage.Stage;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class NenjimComposerImpl implements NenjimComposer {
public NenjimComposerImpl(
@NotNull JavaFxRuntimeService javaFxRuntimeService) {
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
}
@Override
public void start() {
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
}
private void showWindow() {
@@ -16,4 +26,6 @@ public final class NenjimComposerImpl implements NenjimComposer {
window.setTitle("Nenjim Composer");
window.show();
}
private final JavaFxRuntimeService javaFxRuntimeService;
}
@@ -45,6 +45,8 @@ import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl;
import com.r35157.libs.valuetypes.basic.TradingPair;
import com.r35157.service.notification.BoundNotificationService;
import com.r35157.service.notification.impl.discord.DiscordNotificationServiceImpl;
import com.r35157.service.javafx.JavaFxRuntimeService;
import com.r35157.service.javafx.impl.ref.JavaFxRuntimeServiceImpl;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -95,6 +97,7 @@ public final class NenjimHubImpl implements NenjimHub {
startProcess(registryService, EVELYN_SERVICE_PROD_ID);
startProcess(registryService, EVELYN_SERVICE_TEST_ID);
startProcess(registryService, EVELYN_IOU_BURNER_PROD_ID);
startProcess(registryService, JAVAFX_RUNTIME_SERVICE_ID);
startProcess(registryService, EVELYN_MISSION_CONTROL_ID);
startProcess(registryService, PROCESS_MANAGER_ID);
@@ -277,12 +280,22 @@ public final class NenjimHubImpl implements NenjimHub {
private void initializeApplications(
NenjimRegistryServiceManager registryServiceManager) {
missionControl = new EvelynMissionControlImpl(evelynServiceProd, evelynServiceTest);
javaFxRuntimeService = new JavaFxRuntimeServiceImpl();
registryServiceManager.registerComponent(
JAVAFX_RUNTIME_SERVICE_ID,
javaFxRuntimeService);
missionControl = new EvelynMissionControlImpl(
evelynServiceProd,
evelynServiceTest,
javaFxRuntimeService);
registryServiceManager.registerComponent(
EVELYN_MISSION_CONTROL_ID,
missionControl);
processManager = new NenjimProcessManagerImpl(this);
processManager = new NenjimProcessManagerImpl(
this,
javaFxRuntimeService);
registryServiceManager.registerComponent(
PROCESS_MANAGER_ID,
processManager);
@@ -364,6 +377,8 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("evelyn.iou-burner.prod");
private static final NenjimComponentId EVELYN_MISSION_CONTROL_ID =
new NenjimComponentId("evelyn.mission-control.default");
private static final NenjimComponentId JAVAFX_RUNTIME_SERVICE_ID =
new NenjimComponentId("nenjim.javafx-runtime.service");
private static final NenjimComponentId PROCESS_MANAGER_ID =
new NenjimComponentId("nenjim.process-manager.default");
private static final ΩRaydiumLiquidityPoolIdΩ EVE_USDT_RAYDIUM_POOL_ID =
@@ -404,6 +419,7 @@ public final class NenjimHubImpl implements NenjimHub {
private EvelynIOUBurnerService evelynIOUBurnerServiceProd;
// Application components
private JavaFxRuntimeService javaFxRuntimeService;
private EvelynMissionControl missionControl;
private NenjimProcessManager processManager;
}
@@ -1,8 +1,8 @@
package com.r35157.nenjim.npm.impl.ref;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.hubd.NenjimHub;
import com.r35157.nenjim.npm.NenjimProcessManager;
import com.r35157.service.javafx.JavaFxRuntimeService;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
@@ -18,8 +18,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class NenjimProcessManagerImpl implements NenjimProcessManager {
public NenjimProcessManagerImpl(@NotNull NenjimHub nenjimHub) {
public NenjimProcessManagerImpl(
@NotNull NenjimHub nenjimHub,
@NotNull JavaFxRuntimeService javaFxRuntimeService) {
this.nenjimHub = Objects.requireNonNull(nenjimHub, "nenjimHub");
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
}
@Override
@@ -29,7 +34,7 @@ public final class NenjimProcessManagerImpl implements NenjimProcessManager {
"Nenjim Process Manager start has already been attempted");
}
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
Thread pollingWorker = new Thread(
this::pollStartedProcesses,
@@ -69,7 +74,7 @@ public final class NenjimProcessManagerImpl implements NenjimProcessManager {
.stream()
.map(componentId -> componentId.value())
.toList();
JavaFxRuntime.runLater(
javaFxRuntimeService.runLater(
() -> replaceDisplayedSnapshot(processIds));
} catch (RuntimeException failure) {
log.error(
@@ -94,6 +99,7 @@ public final class NenjimProcessManagerImpl implements NenjimProcessManager {
NenjimProcessManagerImpl.class);
private static final Duration POLL_INTERVAL = Duration.ofSeconds(3);
private final NenjimHub nenjimHub;
private final JavaFxRuntimeService javaFxRuntimeService;
private final AtomicBoolean startAttempted = new AtomicBoolean();
private ListView<String> processList;
}
@@ -1,14 +1,24 @@
package com.r35157.nenjim.ntt.impl.ref;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.ntt.NenjimTestTool;
import com.r35157.service.javafx.JavaFxRuntimeService;
import javafx.stage.Stage;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class NenjimTestToolImpl implements NenjimTestTool {
public NenjimTestToolImpl(
@NotNull JavaFxRuntimeService javaFxRuntimeService) {
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
}
@Override
public void start() {
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
}
private void showWindow() {
@@ -16,4 +26,6 @@ public final class NenjimTestToolImpl implements NenjimTestTool {
window.setTitle("Nenjim Test Tool");
window.show();
}
private final JavaFxRuntimeService javaFxRuntimeService;
}
@@ -0,0 +1,9 @@
package com.r35157.service.javafx;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
/** Access to the JVM-wide JavaFX Application Thread. */
public interface JavaFxRuntimeService extends NenjimComponent {
void runLater(@NotNull Runnable runnable);
}
@@ -0,0 +1,81 @@
package com.r35157.service.javafx.impl.ref;
import com.r35157.nenjim.process.NenjimProcess;
import com.r35157.service.javafx.JavaFxRuntimeService;
import javafx.application.Platform;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/** Hub-managed owner of the single JavaFX toolkit used by this JVM. */
public final class JavaFxRuntimeServiceImpl implements
JavaFxRuntimeService,
NenjimProcess {
@Override
public void start() {
if (!startAttempted.compareAndSet(false, true)) {
throw new IllegalStateException(
"JavaFX runtime service start has already been attempted");
}
CountDownLatch startupCompleted = new CountDownLatch(1);
AtomicReference<Throwable> startupFailure = new AtomicReference<>();
try {
Platform.startup(() -> {
try {
Platform.setImplicitExit(false);
} catch (Throwable failure) {
startupFailure.set(failure);
} finally {
startupCompleted.countDown();
}
});
} catch (RuntimeException | Error failure) {
throw failedStartup(failure);
}
awaitStartupCompletion(startupCompleted);
Throwable failure = startupFailure.get();
if (failure != null) {
throw failedStartup(failure);
}
available = true;
}
@Override
public void runLater(@NotNull Runnable runnable) {
Objects.requireNonNull(runnable, "runnable");
if (!available) {
throw new IllegalStateException(
"JavaFX runtime service has not started successfully");
}
Platform.runLater(runnable);
}
private static void awaitStartupCompletion(
CountDownLatch startupCompleted) {
try {
startupCompleted.await();
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(
"Interrupted while waiting for JavaFX runtime startup",
interrupted);
}
}
private static IllegalStateException failedStartup(Throwable failure) {
return new IllegalStateException(
"JavaFX runtime service failed to start",
failure);
}
private final AtomicBoolean startAttempted = new AtomicBoolean();
private volatile boolean available;
}
@@ -1,14 +1,24 @@
package com.r35157.stm.impl.ref;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.service.javafx.JavaFxRuntimeService;
import com.r35157.stm.SodaTaskManager;
import javafx.stage.Stage;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class SodaTaskManagerImpl implements SodaTaskManager {
public SodaTaskManagerImpl(
@NotNull JavaFxRuntimeService javaFxRuntimeService) {
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
}
@Override
public void start() {
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
}
private void showWindow() {
@@ -16,4 +26,6 @@ public final class SodaTaskManagerImpl implements SodaTaskManager {
window.setTitle("Soda Task Manager");
window.show();
}
private final JavaFxRuntimeService javaFxRuntimeService;
}
@@ -1,14 +1,24 @@
package com.r35157.suwimo.hub.client.impl.j2fx;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.service.javafx.JavaFxRuntimeService;
import com.r35157.suwimo.hub.client.SuwimoClient;
import javafx.stage.Stage;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public final class SuwimoClientImpl implements SuwimoClient {
public SuwimoClientImpl(
@NotNull JavaFxRuntimeService javaFxRuntimeService) {
this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService,
"javaFxRuntimeService");
}
@Override
public void start() {
JavaFxRuntime.runLater(this::showWindow);
javaFxRuntimeService.runLater(this::showWindow);
}
private void showWindow() {
@@ -16,4 +26,6 @@ public final class SuwimoClientImpl implements SuwimoClient {
window.setTitle("Suwimo Client");
window.show();
}
private final JavaFxRuntimeService javaFxRuntimeService;
}