// Copyright © 2011-2014, Esko Luontola <www.orfjackal.net>
// This software is released under the Apache License 2.0.
// The license text is at http://www.apache.org/licenses/LICENSE-2.0
package fi.jumi.launcher.remote;
import fi.jumi.actors.ActorRef;
import fi.jumi.actors.eventizers.Event;
import fi.jumi.actors.eventizers.dynamic.DynamicEventizer;
import fi.jumi.actors.queue.*;
import fi.jumi.core.api.*;
import fi.jumi.core.config.*;
import fi.jumi.core.events.suiteListener.*;
import fi.jumi.core.ipc.api.RequestListener;
import fi.jumi.core.network.*;
import fi.jumi.core.util.Boilerplate;
import fi.jumi.core.util.timeout.InitialMessageTimeout;
import fi.jumi.launcher.daemon.Steward;
import fi.jumi.launcher.process.*;
import org.apache.commons.io.IOUtils;
import javax.annotation.WillClose;
import javax.annotation.concurrent.*;
import java.io.*;
import java.nio.file.Paths;
import java.util.concurrent.*;
@NotThreadSafe
public class ProcessStartingDaemonSummoner implements DaemonSummoner {
private static final DynamicEventizer<DaemonListener> eventizer = new DynamicEventizer<>(DaemonListener.class);
private final Steward steward;
private final ProcessStarter processStarter;
private final NetworkServer daemonConnector;
private final OutputStream outputListener; // TODO: remove me
public ProcessStartingDaemonSummoner(Steward steward,
ProcessStarter processStarter,
NetworkServer daemonConnector,
@WillClose OutputStream outputListener) {
this.steward = steward;
this.processStarter = processStarter;
this.daemonConnector = daemonConnector;
this.outputListener = outputListener;
}
@Override
public void connectToDaemon(SuiteConfiguration suite,
DaemonConfiguration daemon,
ActorRef<DaemonListener> listener) {
// XXX: should we handle multiple connections properly, even though we are expecting only one?
int port = daemonConnector.listenOnAnyPort(
new OneTimeDaemonListenerFactory(
withInitialMessageTimeout(listener.tell(), daemon.getStartupTimeout())));
daemon = daemon.melt()
.setDaemonDir(steward.createDaemonDir(daemon.getJumiHome()))
.setLauncherPort(port)
.freeze();
try {
JvmArgs jvmArgs = new JvmArgsBuilder()
.setExecutableJar(steward.getDaemonJar(daemon.getJumiHome()))
.setWorkingDir(Paths.get(suite.getWorkingDirectory()))
.setJvmOptions(suite.getJvmOptions())
.setSystemProperties(daemon.toSystemProperties())
.setProgramArgs(daemon.toProgramArgs())
.freeze();
Process process = processStarter.startJavaProcess(jvmArgs);
copyInBackground(process.getInputStream(), outputListener); // TODO: write the output to a log file using OS pipes, read it from there with AppRunner
} catch (Exception e) {
throw Boilerplate.rethrow(e);
}
}
private static DaemonListener withInitialMessageTimeout(DaemonListener listener, long timeoutMillis) {
return eventizer.newFrontend(
new InitialMessageTimeout<>(
eventizer.newBackend(listener),
getTimeoutMessages(timeoutMillis),
timeoutMillis, TimeUnit.MILLISECONDS));
}
private static MessageReceiver<Event<DaemonListener>> getTimeoutMessages(long timeoutMillis) {
MessageQueue<Event<DaemonListener>> timeoutMessages = new MessageQueue<>();
DaemonListener listener = eventizer.newFrontend(timeoutMessages);
listener.onMessage(new OnSuiteStartedEvent());
listener.onMessage(new OnInternalErrorEvent("Failed to start the test runner daemon process",
StackTrace.from(new RuntimeException("Could not connect to the daemon: timed out after " + timeoutMillis + " ms"))));
listener.onMessage(new OnSuiteFinishedEvent());
return timeoutMessages;
}
private static void copyInBackground(@WillClose InputStream src, @WillClose OutputStream dest) {
// TODO: after removing me, update also ReleasingResourcesTest
Thread t = new Thread(() -> {
try {
IOUtils.copy(src, dest);
} catch (IOException e) {
throw Boilerplate.rethrow(e);
} finally {
IOUtils.closeQuietly(src);
IOUtils.closeQuietly(dest);
}
}, "Daemon Output Copier");
t.setDaemon(true);
t.start();
}
@ThreadSafe
private static class OneTimeDaemonListenerFactory implements NetworkEndpointFactory<Event<SuiteListener>, Event<RequestListener>> {
private final BlockingQueue<DaemonListener> oneTimeListener = new ArrayBlockingQueue<>(1);
public OneTimeDaemonListenerFactory(DaemonListener listener) {
this.oneTimeListener.add(listener);
}
@Override
public NetworkEndpoint<Event<SuiteListener>, Event<RequestListener>> createEndpoint() {
DaemonListener listener = oneTimeListener.poll();
if (listener == null) {
throw new IllegalStateException("already connected once");
}
return listener;
}
}
}