implements PLN-0045 play stop end to end flow
This commit is contained in:
parent
17f9a190d5
commit
a8f7830ccb
@ -29,6 +29,8 @@ public final class StudioActivityEventMapper {
|
|||||||
Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true));
|
Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true));
|
||||||
case StudioPackerOperationEvent packerEvent ->
|
case StudioPackerOperationEvent packerEvent ->
|
||||||
Optional.of(new StudioActivityEntry("Assets", packerEvent.summary(), severity(packerEvent), packerEvent.kind() == p.packer.events.PackerEventKind.ACTION_FAILED));
|
Optional.of(new StudioActivityEntry("Assets", packerEvent.summary(), severity(packerEvent), packerEvent.kind() == p.packer.events.PackerEventKind.ACTION_FAILED));
|
||||||
|
case StudioExecutionLifecycleEvent executionEvent ->
|
||||||
|
Optional.of(new StudioActivityEntry(executionEvent.source(), executionEvent.message(), executionEvent.severity(), executionEvent.sticky()));
|
||||||
default -> Optional.empty();
|
default -> Optional.empty();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ public final class StudioActivityFeedControl extends VBox implements StudioContr
|
|||||||
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshedEvent.class, this::onAssetsRefreshFinished));
|
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshedEvent.class, this::onAssetsRefreshFinished));
|
||||||
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshFailedEvent.class, this::onAssetsRefreshFailed));
|
subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshFailedEvent.class, this::onAssetsRefreshFailed));
|
||||||
subscriptions.add(eventBus.subscribe(StudioPackerOperationEvent.class, this::onPackerOperation));
|
subscriptions.add(eventBus.subscribe(StudioPackerOperationEvent.class, this::onPackerOperation));
|
||||||
|
subscriptions.add(eventBus.subscribe(StudioExecutionLifecycleEvent.class, this::onEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -11,13 +11,27 @@ import javafx.scene.layout.StackPane;
|
|||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||||
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
||||||
|
import p.studio.execution.StudioExecutionSessionService;
|
||||||
|
import p.studio.execution.StudioExecutionState;
|
||||||
|
import p.studio.utilities.events.EventSubscription;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
|
|
||||||
public final class StudioRunSurfaceControl extends HBox implements StudioControlLifecycle {
|
public final class StudioRunSurfaceControl extends HBox implements StudioControlLifecycle {
|
||||||
private final BooleanProperty running = new SimpleBooleanProperty(false);
|
private final BooleanProperty running = new SimpleBooleanProperty(false);
|
||||||
|
private final StudioExecutionSessionService executionSessionService;
|
||||||
|
private final Runnable playAction;
|
||||||
|
private final Runnable stopAction;
|
||||||
|
private final Button toggleButton = new Button();
|
||||||
|
private EventSubscription sessionSubscription;
|
||||||
|
|
||||||
public StudioRunSurfaceControl() {
|
public StudioRunSurfaceControl(
|
||||||
|
final StudioExecutionSessionService executionSessionService,
|
||||||
|
final Runnable playAction,
|
||||||
|
final Runnable stopAction) {
|
||||||
StudioControlLifecycleSupport.install(this, this);
|
StudioControlLifecycleSupport.install(this, this);
|
||||||
|
this.executionSessionService = executionSessionService;
|
||||||
|
this.playAction = playAction;
|
||||||
|
this.stopAction = stopAction;
|
||||||
getStyleClass().add("studio-run-surface");
|
getStyleClass().add("studio-run-surface");
|
||||||
setSpacing(8);
|
setSpacing(8);
|
||||||
setAlignment(Pos.CENTER_LEFT);
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
@ -38,10 +52,9 @@ public final class StudioRunSurfaceControl extends HBox implements StudioControl
|
|||||||
final HBox content = new HBox(8, iconShell, text);
|
final HBox content = new HBox(8, iconShell, text);
|
||||||
content.setAlignment(Pos.CENTER_LEFT);
|
content.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
final Button toggleButton = new Button();
|
|
||||||
toggleButton.getStyleClass().add("studio-run-toggle-button");
|
toggleButton.getStyleClass().add("studio-run-toggle-button");
|
||||||
toggleButton.graphicProperty().set(content);
|
toggleButton.graphicProperty().set(content);
|
||||||
toggleButton.setOnAction(ignored -> running.set(!running.get()));
|
toggleButton.setOnAction(ignored -> handleToggleRequested());
|
||||||
|
|
||||||
running.addListener((ignored, oldValue, newValue) -> {
|
running.addListener((ignored, oldValue, newValue) -> {
|
||||||
toggleButton.getStyleClass().removeAll("studio-run-toggle-play", "studio-run-toggle-stop");
|
toggleButton.getStyleClass().removeAll("studio-run-toggle-play", "studio-run-toggle-stop");
|
||||||
@ -59,4 +72,36 @@ public final class StudioRunSurfaceControl extends HBox implements StudioControl
|
|||||||
public BooleanProperty runningProperty() {
|
public BooleanProperty runningProperty() {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void subscribe() {
|
||||||
|
if (sessionSubscription != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionSubscription = executionSessionService.subscribe(snapshot -> running.set(isRunningState(snapshot.state())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsubscribe() {
|
||||||
|
if (sessionSubscription == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionSubscription.unsubscribe();
|
||||||
|
sessionSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleToggleRequested() {
|
||||||
|
final StudioExecutionState state = executionSessionService.snapshot().state();
|
||||||
|
if (isRunningState(state)) {
|
||||||
|
stopAction.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playAction.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunningState(final StudioExecutionState state) {
|
||||||
|
return state == StudioExecutionState.PREPARING
|
||||||
|
|| state == StudioExecutionState.CONNECTING
|
||||||
|
|| state == StudioExecutionState.RUNNING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package p.studio.debug.runtime;
|
||||||
|
|
||||||
|
import p.studio.execution.StudioExecutionSessionService;
|
||||||
|
|
||||||
|
public interface StudioRuntimeHandshakeClient {
|
||||||
|
StudioRuntimeHandshakeResult connect(final StudioExecutionSessionService session);
|
||||||
|
|
||||||
|
StudioRuntimeHandshakeResult connect(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioRuntimeDebugConnectionSettings settings);
|
||||||
|
|
||||||
|
StudioRuntimeHandshakeResult connectWithRetry(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioRuntimeDebugConnectionSettings settings,
|
||||||
|
final int maxAttempts,
|
||||||
|
final long retryDelayMillis);
|
||||||
|
|
||||||
|
void disconnect(final StudioExecutionSessionService session);
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
public final class StudioRuntimeHandshakeService {
|
public final class StudioRuntimeHandshakeService implements StudioRuntimeHandshakeClient {
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
private final StudioBackgroundTasks backgroundTasks;
|
private final StudioBackgroundTasks backgroundTasks;
|
||||||
private final Object monitor = new Object();
|
private final Object monitor = new Object();
|
||||||
@ -41,56 +41,75 @@ public final class StudioRuntimeHandshakeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public StudioRuntimeHandshakeResult connect(final StudioExecutionSessionService session) {
|
public StudioRuntimeHandshakeResult connect(final StudioExecutionSessionService session) {
|
||||||
return connect(session, StudioRuntimeDebugConnectionSettings.defaults());
|
return connectWithRetry(session, StudioRuntimeDebugConnectionSettings.defaults(), 1, 0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public StudioRuntimeHandshakeResult connect(
|
public StudioRuntimeHandshakeResult connect(
|
||||||
final StudioExecutionSessionService session,
|
final StudioExecutionSessionService session,
|
||||||
final StudioRuntimeDebugConnectionSettings settings) {
|
final StudioRuntimeDebugConnectionSettings settings) {
|
||||||
|
return connectWithRetry(session, settings, 1, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudioRuntimeHandshakeResult connectWithRetry(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioRuntimeDebugConnectionSettings settings,
|
||||||
|
final int maxAttempts,
|
||||||
|
final long retryDelayMillis) {
|
||||||
final StudioExecutionSessionService safeSession = Objects.requireNonNull(session, "session");
|
final StudioExecutionSessionService safeSession = Objects.requireNonNull(session, "session");
|
||||||
final StudioRuntimeDebugConnectionSettings safeSettings = Objects.requireNonNull(settings, "settings");
|
final StudioRuntimeDebugConnectionSettings safeSettings = Objects.requireNonNull(settings, "settings");
|
||||||
safeSession.transitionToConnecting();
|
safeSession.transitionToConnecting();
|
||||||
try {
|
final int attempts = Math.max(1, maxAttempts);
|
||||||
synchronized (monitor) {
|
Exception failure = null;
|
||||||
cleanupLocked();
|
for (int attempt = 1; attempt <= attempts; attempt += 1) {
|
||||||
intentionalDisconnect = false;
|
try {
|
||||||
socket = new Socket();
|
synchronized (monitor) {
|
||||||
socket.connect(new InetSocketAddress(safeSettings.host(), safeSettings.port()), 5_000);
|
cleanupLocked();
|
||||||
socket.setSoTimeout(5_000);
|
intentionalDisconnect = false;
|
||||||
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
socket = new Socket();
|
||||||
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
|
socket.connect(new InetSocketAddress(safeSettings.host(), safeSettings.port()), 5_000);
|
||||||
}
|
socket.setSoTimeout(5_000);
|
||||||
|
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||||
final String handshakeLine = readHandshakeLine();
|
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
|
||||||
final StudioRuntimeHandshakeRuntimeToClient handshake = mapper.readValue(
|
|
||||||
handshakeLine,
|
|
||||||
StudioRuntimeHandshakeRuntimeToClient.class);
|
|
||||||
writeMessage(mapper.writeValueAsString(new StudioRuntimeHandshakeClientToRuntime("start")));
|
|
||||||
synchronized (monitor) {
|
|
||||||
if (socket != null) {
|
|
||||||
socket.setSoTimeout(0);
|
|
||||||
}
|
}
|
||||||
receiverTask = backgroundTasks.submit(() -> receiveLoop(safeSession));
|
|
||||||
}
|
|
||||||
|
|
||||||
safeSession.appendRuntimeLog(
|
final String handshakeLine = readHandshakeLine();
|
||||||
StudioExecutionLogSeverity.INFO,
|
final StudioRuntimeHandshakeRuntimeToClient handshake = mapper.readValue(
|
||||||
"Runtime handshake connected to %s:%d".formatted(safeSettings.host(), safeSettings.port()));
|
handshakeLine,
|
||||||
safeSession.appendRuntimeLog(
|
StudioRuntimeHandshakeRuntimeToClient.class);
|
||||||
StudioExecutionLogSeverity.INFO,
|
writeMessage(mapper.writeValueAsString(new StudioRuntimeHandshakeClientToRuntime("start")));
|
||||||
"Runtime protocol %d (%s)".formatted(handshake.protocolVersion(), handshake.runtimeVersion()));
|
synchronized (monitor) {
|
||||||
safeSession.transitionToRunning();
|
if (socket != null) {
|
||||||
return StudioRuntimeHandshakeResult.success(safeSettings, handshake.protocolVersion(), handshake.runtimeVersion());
|
socket.setSoTimeout(0);
|
||||||
} catch (Exception exception) {
|
}
|
||||||
cleanup();
|
receiverTask = backgroundTasks.submit(() -> receiveLoop(safeSession));
|
||||||
safeSession.appendRuntimeLog(
|
}
|
||||||
StudioExecutionLogSeverity.ERROR,
|
|
||||||
"Runtime handshake failed: " + failureMessage(exception));
|
safeSession.appendRuntimeLog(
|
||||||
safeSession.transitionToRuntimeFailed();
|
StudioExecutionLogSeverity.INFO,
|
||||||
return StudioRuntimeHandshakeResult.failure(safeSettings, failureMessage(exception));
|
"Runtime handshake connected to %s:%d".formatted(safeSettings.host(), safeSettings.port()));
|
||||||
|
safeSession.appendRuntimeLog(
|
||||||
|
StudioExecutionLogSeverity.INFO,
|
||||||
|
"Runtime protocol %d (%s)".formatted(handshake.protocolVersion(), handshake.runtimeVersion()));
|
||||||
|
safeSession.transitionToRunning();
|
||||||
|
return StudioRuntimeHandshakeResult.success(safeSettings, handshake.protocolVersion(), handshake.runtimeVersion());
|
||||||
|
} catch (Exception exception) {
|
||||||
|
failure = exception;
|
||||||
|
cleanup();
|
||||||
|
if (attempt < attempts && retryDelayMillis > 0L) {
|
||||||
|
sleepQuietly(retryDelayMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
safeSession.appendRuntimeLog(
|
||||||
|
StudioExecutionLogSeverity.ERROR,
|
||||||
|
"Runtime handshake failed: " + failureMessage(failure == null ? new IOException("unknown handshake failure") : failure));
|
||||||
|
safeSession.transitionToRuntimeFailed();
|
||||||
|
return StudioRuntimeHandshakeResult.failure(safeSettings, failureMessage(failure == null ? new IOException("unknown handshake failure") : failure));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void disconnect(final StudioExecutionSessionService session) {
|
public void disconnect(final StudioExecutionSessionService session) {
|
||||||
final StudioExecutionSessionService safeSession = Objects.requireNonNull(session, "session");
|
final StudioExecutionSessionService safeSession = Objects.requireNonNull(session, "session");
|
||||||
final boolean wasConnected;
|
final boolean wasConnected;
|
||||||
@ -238,4 +257,12 @@ public final class StudioRuntimeHandshakeService {
|
|||||||
? exception.getClass().getSimpleName()
|
? exception.getClass().getSimpleName()
|
||||||
: message;
|
: message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sleepQuietly(final long retryDelayMillis) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(retryDelayMillis);
|
||||||
|
} catch (InterruptedException interruptedException) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,238 @@
|
|||||||
|
package p.studio.execution;
|
||||||
|
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.StudioBackgroundTasks;
|
||||||
|
import p.studio.controls.shell.StudioActivityEntrySeverity;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeDebugConnectionSettings;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeHandshakeClient;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeHandshakeResult;
|
||||||
|
import p.studio.execution.runtime.StudioExternalRuntimeProcessLauncher;
|
||||||
|
import p.studio.execution.runtime.StudioRuntimeProcessHandle;
|
||||||
|
import p.studio.execution.runtime.StudioRuntimeProcessLauncher;
|
||||||
|
import p.studio.lsp.events.StudioExecutionLifecycleEvent;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.shipper.StudioShipperService;
|
||||||
|
import p.studio.workspaces.WorkspaceId;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class StudioPlayStopCoordinator {
|
||||||
|
private final Object monitor = new Object();
|
||||||
|
private final ProjectReference projectReference;
|
||||||
|
private final ProjectLocalStudioSetup projectSetup;
|
||||||
|
private final StudioExecutionSessionService executionSession;
|
||||||
|
private final StudioShipperPreparationRunner shipperRunner;
|
||||||
|
private final StudioRuntimeHandshakeClient handshakeClient;
|
||||||
|
private final StudioRuntimeProcessLauncher runtimeProcessLauncher;
|
||||||
|
private final StudioBackgroundTasks backgroundTasks;
|
||||||
|
private final Consumer<WorkspaceId> workspaceSelector;
|
||||||
|
private final Consumer<StudioExecutionLifecycleEvent> lifecyclePublisher;
|
||||||
|
private StudioRuntimeProcessHandle activeProcess;
|
||||||
|
private boolean stopRequested;
|
||||||
|
|
||||||
|
public StudioPlayStopCoordinator(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final ProjectLocalStudioSetup projectSetup,
|
||||||
|
final StudioExecutionSessionService executionSession,
|
||||||
|
final Consumer<WorkspaceId> workspaceSelector) {
|
||||||
|
this(
|
||||||
|
projectReference,
|
||||||
|
projectSetup,
|
||||||
|
executionSession,
|
||||||
|
new StudioShipperService()::prepare,
|
||||||
|
new p.studio.debug.runtime.StudioRuntimeHandshakeService(),
|
||||||
|
new StudioExternalRuntimeProcessLauncher(),
|
||||||
|
Container.backgroundTasks(),
|
||||||
|
workspaceSelector,
|
||||||
|
event -> Container.eventBus().publish(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
StudioPlayStopCoordinator(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final ProjectLocalStudioSetup projectSetup,
|
||||||
|
final StudioExecutionSessionService executionSession,
|
||||||
|
final StudioShipperPreparationRunner shipperRunner,
|
||||||
|
final StudioRuntimeHandshakeClient handshakeClient,
|
||||||
|
final StudioRuntimeProcessLauncher runtimeProcessLauncher,
|
||||||
|
final StudioBackgroundTasks backgroundTasks,
|
||||||
|
final Consumer<WorkspaceId> workspaceSelector,
|
||||||
|
final Consumer<StudioExecutionLifecycleEvent> lifecyclePublisher) {
|
||||||
|
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
|
this.projectSetup = Objects.requireNonNull(projectSetup, "projectSetup");
|
||||||
|
this.executionSession = Objects.requireNonNull(executionSession, "executionSession");
|
||||||
|
this.shipperRunner = Objects.requireNonNull(shipperRunner, "shipperRunner");
|
||||||
|
this.handshakeClient = Objects.requireNonNull(handshakeClient, "handshakeClient");
|
||||||
|
this.runtimeProcessLauncher = Objects.requireNonNull(runtimeProcessLauncher, "runtimeProcessLauncher");
|
||||||
|
this.backgroundTasks = Objects.requireNonNull(backgroundTasks, "backgroundTasks");
|
||||||
|
this.workspaceSelector = Objects.requireNonNull(workspaceSelector, "workspaceSelector");
|
||||||
|
this.lifecyclePublisher = Objects.requireNonNull(lifecyclePublisher, "lifecyclePublisher");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void play() {
|
||||||
|
synchronized (monitor) {
|
||||||
|
if (isBusyState(executionSession.snapshot().state())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopRequested = false;
|
||||||
|
}
|
||||||
|
workspaceSelector.accept(WorkspaceId.DEBUG);
|
||||||
|
executionSession.beginPreparationCycle();
|
||||||
|
publishLifecycle("Play", "Play started", StudioActivityEntrySeverity.INFO, false);
|
||||||
|
backgroundTasks.submit(this::runPlayFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
final StudioExecutionState state = executionSession.snapshot().state();
|
||||||
|
if (state == StudioExecutionState.PREPARING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final StudioRuntimeProcessHandle processToStop;
|
||||||
|
synchronized (monitor) {
|
||||||
|
stopRequested = true;
|
||||||
|
processToStop = activeProcess;
|
||||||
|
activeProcess = null;
|
||||||
|
}
|
||||||
|
handshakeClient.disconnect(executionSession);
|
||||||
|
if (processToStop != null && processToStop.isAlive()) {
|
||||||
|
processToStop.destroyForcibly();
|
||||||
|
}
|
||||||
|
publishLifecycle("Play", "Runtime stop requested", StudioActivityEntrySeverity.INFO, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runPlayFlow() {
|
||||||
|
final var prepareResult = shipperRunner.prepare(projectReference, executionSession::appendShipperLog);
|
||||||
|
if (!prepareResult.success()) {
|
||||||
|
executionSession.transitionToPrepareFailed();
|
||||||
|
publishLifecycle("Play", "Preparation failed", StudioActivityEntrySeverity.ERROR, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executionSession.transitionToConnecting();
|
||||||
|
final String runtimePath = validateRuntimePath();
|
||||||
|
if (runtimePath == null) {
|
||||||
|
executionSession.transitionToRuntimeFailed();
|
||||||
|
publishLifecycle("Play", "Runtime preflight failed", StudioActivityEntrySeverity.ERROR, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final StudioRuntimeProcessHandle processHandle;
|
||||||
|
try {
|
||||||
|
processHandle = runtimeProcessLauncher.launch(projectReference, runtimePath);
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: " + ioException.getMessage());
|
||||||
|
executionSession.transitionToRuntimeFailed();
|
||||||
|
publishLifecycle("Play", "Runtime spawn failed", StudioActivityEntrySeverity.ERROR, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (monitor) {
|
||||||
|
activeProcess = processHandle;
|
||||||
|
stopRequested = false;
|
||||||
|
}
|
||||||
|
streamProcessOutput(processHandle.stdout(), StudioExecutionLogSeverity.INFO);
|
||||||
|
streamProcessOutput(processHandle.stderr(), StudioExecutionLogSeverity.ERROR);
|
||||||
|
processHandle.onExit().thenAccept(exitCode -> handleProcessExit(processHandle, exitCode));
|
||||||
|
|
||||||
|
final StudioRuntimeHandshakeResult handshake = handshakeClient.connectWithRetry(
|
||||||
|
executionSession,
|
||||||
|
StudioRuntimeDebugConnectionSettings.defaults(),
|
||||||
|
20,
|
||||||
|
250L);
|
||||||
|
if (!handshake.success()) {
|
||||||
|
processHandle.destroyForcibly();
|
||||||
|
synchronized (monitor) {
|
||||||
|
if (activeProcess == processHandle) {
|
||||||
|
activeProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publishLifecycle("Debug", "Runtime handshake failed", StudioActivityEntrySeverity.ERROR, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishLifecycle("Debug", "Runtime started", StudioActivityEntrySeverity.SUCCESS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleProcessExit(
|
||||||
|
final StudioRuntimeProcessHandle processHandle,
|
||||||
|
final int exitCode) {
|
||||||
|
final boolean wasStopRequested;
|
||||||
|
synchronized (monitor) {
|
||||||
|
if (activeProcess == processHandle) {
|
||||||
|
activeProcess = null;
|
||||||
|
}
|
||||||
|
wasStopRequested = stopRequested;
|
||||||
|
}
|
||||||
|
if (wasStopRequested || executionSession.snapshot().state() == StudioExecutionState.STOPPED) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.INFO, "Runtime process stopped.");
|
||||||
|
publishLifecycle("Debug", "Runtime stopped", StudioActivityEntrySeverity.INFO, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exitCode == 0) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.INFO, "Runtime process exited successfully.");
|
||||||
|
executionSession.transitionToStopped();
|
||||||
|
publishLifecycle("Debug", "Runtime finished", StudioActivityEntrySeverity.SUCCESS, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime process exited with code " + exitCode + ".");
|
||||||
|
executionSession.transitionToRuntimeFailed();
|
||||||
|
publishLifecycle("Debug", "Runtime failed", StudioActivityEntrySeverity.ERROR, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void streamProcessOutput(
|
||||||
|
final InputStream stream,
|
||||||
|
final StudioExecutionLogSeverity severity) {
|
||||||
|
backgroundTasks.submit(() -> {
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
executionSession.appendRuntimeLog(severity, line);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String validateRuntimePath() {
|
||||||
|
final String runtimePathValue = projectSetup.prometeuRuntimePath();
|
||||||
|
if (runtimePathValue == null) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: prometeuRuntimePath is missing.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Path runtimePath = Path.of(runtimePathValue).toAbsolutePath().normalize();
|
||||||
|
if (!Files.isRegularFile(runtimePath)) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: runtime not found at " + runtimePath + ".");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!Files.isExecutable(runtimePath)) {
|
||||||
|
executionSession.appendRuntimeLog(StudioExecutionLogSeverity.ERROR, "Runtime preflight failed: runtime is not executable at " + runtimePath + ".");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return runtimePath.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publishLifecycle(
|
||||||
|
final String source,
|
||||||
|
final String message,
|
||||||
|
final StudioActivityEntrySeverity severity,
|
||||||
|
final boolean sticky) {
|
||||||
|
lifecyclePublisher.accept(new StudioExecutionLifecycleEvent(source, message, severity, sticky));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBusyState(final StudioExecutionState state) {
|
||||||
|
return state == StudioExecutionState.PREPARING
|
||||||
|
|| state == StudioExecutionState.CONNECTING
|
||||||
|
|| state == StudioExecutionState.RUNNING;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package p.studio.execution;
|
||||||
|
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.shipper.StudioShipperLogEntry;
|
||||||
|
import p.studio.shipper.StudioShipperPrepareResult;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface StudioShipperPreparationRunner {
|
||||||
|
StudioShipperPrepareResult prepare(ProjectReference projectReference, Consumer<StudioShipperLogEntry> logSink);
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package p.studio.execution.runtime;
|
||||||
|
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public final class StudioExternalRuntimeProcessLauncher implements StudioRuntimeProcessLauncher {
|
||||||
|
@Override
|
||||||
|
public StudioRuntimeProcessHandle launch(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final String runtimePath) throws IOException {
|
||||||
|
final Process process = new ProcessBuilder(runtimePath, "run", "build")
|
||||||
|
.directory(projectReference.rootPath().toFile())
|
||||||
|
.start();
|
||||||
|
return new ExternalProcessHandle(process);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExternalProcessHandle(Process process) implements StudioRuntimeProcessHandle {
|
||||||
|
@Override
|
||||||
|
public InputStream stdout() {
|
||||||
|
return process.getInputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream stderr() {
|
||||||
|
return process.getErrorStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlive() {
|
||||||
|
return process.isAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyForcibly() {
|
||||||
|
process.descendants().forEach(ProcessHandle::destroyForcibly);
|
||||||
|
process.destroyForcibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Integer> onExit() {
|
||||||
|
return process.onExit().thenApply(Process::exitValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package p.studio.execution.runtime;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface StudioRuntimeProcessHandle {
|
||||||
|
InputStream stdout();
|
||||||
|
|
||||||
|
InputStream stderr();
|
||||||
|
|
||||||
|
boolean isAlive();
|
||||||
|
|
||||||
|
void destroyForcibly();
|
||||||
|
|
||||||
|
CompletableFuture<Integer> onExit();
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package p.studio.execution.runtime;
|
||||||
|
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface StudioRuntimeProcessLauncher {
|
||||||
|
StudioRuntimeProcessHandle launch(ProjectReference projectReference, String runtimePath) throws IOException;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package p.studio.lsp.events;
|
||||||
|
|
||||||
|
import p.studio.controls.shell.StudioActivityEntrySeverity;
|
||||||
|
|
||||||
|
public record StudioExecutionLifecycleEvent(
|
||||||
|
String source,
|
||||||
|
String message,
|
||||||
|
StudioActivityEntrySeverity severity,
|
||||||
|
boolean sticky) implements StudioEvent {
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package p.studio.window;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.controls.shell.*;
|
import p.studio.controls.shell.*;
|
||||||
|
import p.studio.execution.StudioPlayStopCoordinator;
|
||||||
import p.studio.projectstate.ProjectLocalStudioSetup;
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
import p.studio.lsp.events.StudioWorkspaceSelectedEvent;
|
||||||
import p.studio.projectstate.ProjectLocalStudioState;
|
import p.studio.projectstate.ProjectLocalStudioState;
|
||||||
@ -25,6 +26,7 @@ public final class MainView extends BorderPane {
|
|||||||
private final AssetWorkspace assetWorkspace;
|
private final AssetWorkspace assetWorkspace;
|
||||||
private final EditorWorkspace editorWorkspace;
|
private final EditorWorkspace editorWorkspace;
|
||||||
private final StudioWorkspaceRailControl<WorkspaceId> workspaceRail;
|
private final StudioWorkspaceRailControl<WorkspaceId> workspaceRail;
|
||||||
|
private final StudioPlayStopCoordinator playStopCoordinator;
|
||||||
private boolean initializing;
|
private boolean initializing;
|
||||||
private boolean workspaceLifecycleInitialized;
|
private boolean workspaceLifecycleInitialized;
|
||||||
|
|
||||||
@ -35,7 +37,15 @@ public final class MainView extends BorderPane {
|
|||||||
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
|
final ProjectLocalStudioState persistedState = projectSession.projectLocalStudioState();
|
||||||
final ProjectLocalStudioSetup projectSetup = projectSession.projectLocalStudioSetup();
|
final ProjectLocalStudioSetup projectSetup = projectSession.projectLocalStudioSetup();
|
||||||
final var menuBar = new StudioShellMenuBarControl();
|
final var menuBar = new StudioShellMenuBarControl();
|
||||||
final var runSurface = new StudioRunSurfaceControl();
|
playStopCoordinator = new StudioPlayStopCoordinator(
|
||||||
|
projectReference,
|
||||||
|
projectSetup,
|
||||||
|
projectSession.executionSession(),
|
||||||
|
this::selectWorkspace);
|
||||||
|
final var runSurface = new StudioRunSurfaceControl(
|
||||||
|
projectSession.executionSession(),
|
||||||
|
playStopCoordinator::play,
|
||||||
|
playStopCoordinator::stop);
|
||||||
setTop(new StudioShellTopBarControl(menuBar));
|
setTop(new StudioShellTopBarControl(menuBar));
|
||||||
|
|
||||||
assetWorkspace = new AssetWorkspace(projectReference);
|
assetWorkspace = new AssetWorkspace(projectReference);
|
||||||
@ -103,6 +113,7 @@ public final class MainView extends BorderPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
|
playStopCoordinator.shutdown();
|
||||||
persistProjectLocalState();
|
persistProjectLocalState();
|
||||||
host.deactivateCurrentWorkspace();
|
host.deactivateCurrentWorkspace();
|
||||||
}
|
}
|
||||||
@ -116,6 +127,11 @@ public final class MainView extends BorderPane {
|
|||||||
Container.eventBus().publish(new StudioWorkspaceSelectedEvent(workspaceId));
|
Container.eventBus().publish(new StudioWorkspaceSelectedEvent(workspaceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectWorkspace(final WorkspaceId workspaceId) {
|
||||||
|
workspaceRail.select(workspaceId);
|
||||||
|
loadWorkspace(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
private WorkspaceId restoreInitialWorkspace(final ProjectLocalStudioState.OpenShellState openShellState) {
|
private WorkspaceId restoreInitialWorkspace(final ProjectLocalStudioState.OpenShellState openShellState) {
|
||||||
if (openShellState.selectedWorkspaceId() == null) {
|
if (openShellState.selectedWorkspaceId() == null) {
|
||||||
return WorkspaceId.ASSETS;
|
return WorkspaceId.ASSETS;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package p.studio.controls.shell;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import p.packer.events.PackerEventKind;
|
import p.packer.events.PackerEventKind;
|
||||||
|
import p.studio.lsp.events.StudioExecutionLifecycleEvent;
|
||||||
import p.studio.lsp.events.StudioPackerOperationEvent;
|
import p.studio.lsp.events.StudioPackerOperationEvent;
|
||||||
import p.studio.lsp.events.StudioProjectOpenedEvent;
|
import p.studio.lsp.events.StudioProjectOpenedEvent;
|
||||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceRefreshFailedEvent;
|
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceRefreshFailedEvent;
|
||||||
@ -54,4 +55,15 @@ final class StudioActivityEventMapperTest {
|
|||||||
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
|
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
|
||||||
assertEquals("Asset created.", entry.message());
|
assertEquals("Asset created.", entry.message());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapsExecutionLifecycleToActivityEntry() {
|
||||||
|
final StudioActivityEntry entry = StudioActivityEventMapper
|
||||||
|
.map(new StudioExecutionLifecycleEvent("Debug", "Runtime started", StudioActivityEntrySeverity.SUCCESS, false))
|
||||||
|
.orElseThrow();
|
||||||
|
|
||||||
|
assertEquals("Debug", entry.source());
|
||||||
|
assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity());
|
||||||
|
assertEquals("Runtime started", entry.message());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,252 @@
|
|||||||
|
package p.studio.execution;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import p.studio.StudioBackgroundTasks;
|
||||||
|
import p.studio.controls.shell.StudioActivityEntrySeverity;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeDebugConnectionSettings;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeHandshakeClient;
|
||||||
|
import p.studio.debug.runtime.StudioRuntimeHandshakeResult;
|
||||||
|
import p.studio.execution.runtime.StudioRuntimeProcessHandle;
|
||||||
|
import p.studio.execution.runtime.StudioRuntimeProcessLauncher;
|
||||||
|
import p.studio.projectstate.ProjectLocalStudioSetup;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.shipper.StudioShipperPrepareResult;
|
||||||
|
import p.studio.shipper.StudioShipperPreparationStatus;
|
||||||
|
import p.studio.workspaces.WorkspaceId;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class StudioPlayStopCoordinatorTest {
|
||||||
|
@Test
|
||||||
|
void playNavigatesToDebugAndStopsOnPreparationFailure() throws Exception {
|
||||||
|
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||||
|
final AtomicReference<WorkspaceId> selectedWorkspace = new AtomicReference<>();
|
||||||
|
final AtomicInteger launcherCalls = new AtomicInteger();
|
||||||
|
final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newCachedThreadPool());
|
||||||
|
try {
|
||||||
|
final StudioPlayStopCoordinator coordinator = new StudioPlayStopCoordinator(
|
||||||
|
projectReference(),
|
||||||
|
new ProjectLocalStudioSetup("/bin/echo", ProjectLocalStudioSetup.EditorIndentationSetup.defaults()),
|
||||||
|
session,
|
||||||
|
(project, sink) -> new StudioShipperPrepareResult(StudioShipperPreparationStatus.FAILED, List.of(), null, null, null, null),
|
||||||
|
new RecordingHandshakeClient(),
|
||||||
|
(projectReference, runtimePath) -> {
|
||||||
|
launcherCalls.incrementAndGet();
|
||||||
|
return new FakeRuntimeProcessHandle();
|
||||||
|
},
|
||||||
|
backgroundTasks,
|
||||||
|
selectedWorkspace::set,
|
||||||
|
ignored -> { });
|
||||||
|
|
||||||
|
coordinator.play();
|
||||||
|
|
||||||
|
assertEquals(WorkspaceId.DEBUG, selectedWorkspace.get());
|
||||||
|
assertTrue(waitForState(session, StudioExecutionState.PREPARE_FAILED));
|
||||||
|
assertEquals(0, launcherCalls.get());
|
||||||
|
} finally {
|
||||||
|
backgroundTasks.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatedPlayIsIgnoredWhileRuntimeIsActiveAndStopKillsProcess() throws Exception {
|
||||||
|
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||||
|
final ArrayList<WorkspaceId> selected = new ArrayList<>();
|
||||||
|
final AtomicInteger launches = new AtomicInteger();
|
||||||
|
final RecordingHandshakeClient handshakeClient = new RecordingHandshakeClient();
|
||||||
|
final FakeRuntimeProcessHandle processHandle = new FakeRuntimeProcessHandle();
|
||||||
|
final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newCachedThreadPool());
|
||||||
|
try {
|
||||||
|
final StudioPlayStopCoordinator coordinator = new StudioPlayStopCoordinator(
|
||||||
|
projectReference(),
|
||||||
|
new ProjectLocalStudioSetup("/bin/echo", ProjectLocalStudioSetup.EditorIndentationSetup.defaults()),
|
||||||
|
session,
|
||||||
|
(project, sink) -> new StudioShipperPrepareResult(StudioShipperPreparationStatus.SUCCESS, List.of(), null, null, null, null),
|
||||||
|
handshakeClient,
|
||||||
|
(projectReference, runtimePath) -> {
|
||||||
|
launches.incrementAndGet();
|
||||||
|
return processHandle;
|
||||||
|
},
|
||||||
|
backgroundTasks,
|
||||||
|
selected::add,
|
||||||
|
ignored -> { });
|
||||||
|
|
||||||
|
coordinator.play();
|
||||||
|
assertTrue(waitForState(session, StudioExecutionState.RUNNING));
|
||||||
|
|
||||||
|
coordinator.play();
|
||||||
|
assertEquals(1, launches.get());
|
||||||
|
|
||||||
|
coordinator.stop();
|
||||||
|
assertTrue(waitForState(session, StudioExecutionState.STOPPED));
|
||||||
|
assertTrue(processHandle.destroyed);
|
||||||
|
assertEquals(1, handshakeClient.disconnectCalls.get());
|
||||||
|
assertFalse(selected.isEmpty());
|
||||||
|
} finally {
|
||||||
|
backgroundTasks.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stopDuringPreparingIsANoOp() throws Exception {
|
||||||
|
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||||
|
final CountDownLatch started = new CountDownLatch(1);
|
||||||
|
final CountDownLatch release = new CountDownLatch(1);
|
||||||
|
final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newCachedThreadPool());
|
||||||
|
try {
|
||||||
|
final StudioPlayStopCoordinator coordinator = new StudioPlayStopCoordinator(
|
||||||
|
projectReference(),
|
||||||
|
new ProjectLocalStudioSetup("/bin/echo", ProjectLocalStudioSetup.EditorIndentationSetup.defaults()),
|
||||||
|
session,
|
||||||
|
(project, sink) -> {
|
||||||
|
started.countDown();
|
||||||
|
await(release);
|
||||||
|
return new StudioShipperPrepareResult(StudioShipperPreparationStatus.FAILED, List.of(), null, null, null, null);
|
||||||
|
},
|
||||||
|
new RecordingHandshakeClient(),
|
||||||
|
(projectReference, runtimePath) -> new FakeRuntimeProcessHandle(),
|
||||||
|
backgroundTasks,
|
||||||
|
ignored -> { },
|
||||||
|
ignored -> { });
|
||||||
|
|
||||||
|
coordinator.play();
|
||||||
|
assertTrue(started.await(2, TimeUnit.SECONDS));
|
||||||
|
assertEquals(StudioExecutionState.PREPARING, session.snapshot().state());
|
||||||
|
|
||||||
|
coordinator.stop();
|
||||||
|
assertEquals(StudioExecutionState.PREPARING, session.snapshot().state());
|
||||||
|
|
||||||
|
release.countDown();
|
||||||
|
assertTrue(waitForState(session, StudioExecutionState.PREPARE_FAILED));
|
||||||
|
} finally {
|
||||||
|
backgroundTasks.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void spawnFailureBecomesRuntimeFailed() throws Exception {
|
||||||
|
final StudioExecutionSessionService session = new StudioExecutionSessionService();
|
||||||
|
final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newCachedThreadPool());
|
||||||
|
try {
|
||||||
|
final StudioPlayStopCoordinator coordinator = new StudioPlayStopCoordinator(
|
||||||
|
projectReference(),
|
||||||
|
new ProjectLocalStudioSetup("/definitely/missing/runtime", ProjectLocalStudioSetup.EditorIndentationSetup.defaults()),
|
||||||
|
session,
|
||||||
|
(project, sink) -> new StudioShipperPrepareResult(StudioShipperPreparationStatus.SUCCESS, List.of(), null, null, null, null),
|
||||||
|
new RecordingHandshakeClient(),
|
||||||
|
(projectReference, runtimePath) -> new FakeRuntimeProcessHandle(),
|
||||||
|
backgroundTasks,
|
||||||
|
ignored -> { },
|
||||||
|
ignored -> { });
|
||||||
|
|
||||||
|
coordinator.play();
|
||||||
|
|
||||||
|
assertTrue(waitForState(session, StudioExecutionState.RUNTIME_FAILED));
|
||||||
|
assertTrue(session.snapshot().logs().stream().anyMatch(entry -> entry.message().contains("Runtime preflight failed")));
|
||||||
|
} finally {
|
||||||
|
backgroundTasks.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean waitForState(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioExecutionState expected) throws InterruptedException {
|
||||||
|
for (int attempt = 0; attempt < 40; attempt += 1) {
|
||||||
|
if (session.snapshot().state() == expected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Thread.sleep(50L);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void await(final CountDownLatch latch) {
|
||||||
|
try {
|
||||||
|
latch.await(2, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException interruptedException) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProjectReference projectReference() {
|
||||||
|
return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/prometeu-play-stop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingHandshakeClient implements StudioRuntimeHandshakeClient {
|
||||||
|
private final AtomicInteger disconnectCalls = new AtomicInteger();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudioRuntimeHandshakeResult connect(final StudioExecutionSessionService session) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudioRuntimeHandshakeResult connect(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioRuntimeDebugConnectionSettings settings) {
|
||||||
|
return connectWithRetry(session, settings, 1, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StudioRuntimeHandshakeResult connectWithRetry(
|
||||||
|
final StudioExecutionSessionService session,
|
||||||
|
final StudioRuntimeDebugConnectionSettings settings,
|
||||||
|
final int maxAttempts,
|
||||||
|
final long retryDelayMillis) {
|
||||||
|
session.transitionToRunning();
|
||||||
|
session.appendRuntimeLog(StudioExecutionLogSeverity.INFO, "Fake runtime handshake connected.");
|
||||||
|
return StudioRuntimeHandshakeResult.success(settings, 1, "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect(final StudioExecutionSessionService session) {
|
||||||
|
disconnectCalls.incrementAndGet();
|
||||||
|
if (session.snapshot().state() == StudioExecutionState.CONNECTING || session.snapshot().state() == StudioExecutionState.RUNNING) {
|
||||||
|
session.transitionToStopped();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FakeRuntimeProcessHandle implements StudioRuntimeProcessHandle {
|
||||||
|
private final CompletableFuture<Integer> exit = new CompletableFuture<>();
|
||||||
|
private boolean destroyed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream stdout() {
|
||||||
|
return new ByteArrayInputStream(new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream stderr() {
|
||||||
|
return new ByteArrayInputStream(new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlive() {
|
||||||
|
return !destroyed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyForcibly() {
|
||||||
|
destroyed = true;
|
||||||
|
exit.complete(137);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Integer> onExit() {
|
||||||
|
return exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user