matlabcontrol.RemoteMatlabProxyFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of matconsolectl Show documentation
Show all versions of matconsolectl Show documentation
MatConsoleCtl - control MATLAB from Java
/*
* Code licensed under new-style BSD (see LICENSE).
* All code up to tags/original: Copyright (c) 2013, Joshua Kaplan
* All code after tags/original: Copyright (c) 2016, DiffPlug
*/
package matlabcontrol;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.rmi.AlreadyBoundException;
import java.rmi.NoSuchObjectException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import matlabcontrol.MatlabProxy.Identifier;
import matlabcontrol.MatlabProxyFactory.Request;
import matlabcontrol.MatlabProxyFactory.RequestCallback;
/**
* Creates remote instances of {@link MatlabProxy}. Creating a proxy will either connect to an existing session of
* MATLAB or launch a new session of MATLAB. This factory can be used to create any number of proxies.
*
* @since 3.0.0
*
* @author Joshua Kaplan
*/
class RemoteMatlabProxyFactory implements ProxyFactory {
/**
* The options that configure this instance of the factory.
*/
private final MatlabProxyFactoryOptions _options;
/**
* {@link RemoteRequestReceiver} instances. They need to be stored because the RMI registry only holds weak
* references to exported objects.
*/
private final CopyOnWriteArrayList _receivers = new CopyOnWriteArrayList();
/**
* The frequency (in milliseconds) with which to check if a receiver is still bound to the registry.
*/
static final long RECEIVER_CHECK_PERIOD = 1000L;
/**
* The RMI registry used to communicate between JVMs.
*/
private volatile Registry _registry = null;
public RemoteMatlabProxyFactory(MatlabProxyFactoryOptions options) {
_options = options;
}
@Override
public Request requestProxy(final RequestCallback requestCallback) throws MatlabConnectionException {
if (_options.getOsgiClassloaderFriendly()) {
return callWithClassLoader(new Callable() {
@Override
public Request call() throws Exception {
return requestProxyImp(requestCallback);
}
});
} else {
return requestProxyImp(requestCallback);
}
}
/** Calls the given callable while temporarily using our classloader. */
private Request callWithClassLoader(Callable callable) throws MatlabConnectionException {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = null;
try {
// backup the "real" classloader (likely to be OSGi's)
originalClassLoader = currentThread.getContextClassLoader();
// set it to the classloader that loaded this class (if it has us, it probably has the rest of what we need too.)
currentThread.setContextClassLoader(RemoteMatlabProxyFactory.class.getClassLoader());
// call the callable
return callable.call();
} catch (MatlabConnectionException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// if we succeeded in backing up the classloader, restore what we backed up
if (originalClassLoader != null) {
currentThread.setContextClassLoader(originalClassLoader);
}
}
}
private Request requestProxyImp(RequestCallback requestCallback) throws MatlabConnectionException {
//Unique identifier for the proxy
RemoteIdentifier proxyID = new RemoteIdentifier();
Request request;
//Initialize the registry (does nothing if already initialized)
initRegistry(false);
//Create and bind the receiver
RemoteRequestReceiver receiver = new RemoteRequestReceiver(requestCallback, proxyID,
Configuration.getClassPathAsRMICodebase(), Configuration.getClassPathAsCanonicalPaths());
_receivers.add(receiver);
try {
_registry.bind(receiver.getReceiverID(), LocalHostRMIHelper.exportObject(receiver));
} catch (RemoteException ex) {
_receivers.remove(receiver);
throw new MatlabConnectionException("Could not bind proxy receiver to the RMI registry", ex);
} catch (AlreadyBoundException ex) {
_receivers.remove(receiver);
throw new MatlabConnectionException("Could not bind proxy receiver to the RMI registry", ex);
}
//Connect to MATLAB
RequestMaintainer maintainer = new RequestMaintainer(receiver);
try {
if (_options.getCopyPasteCallback() != null) {
// send the copy-paste code to the user, and then begin the request
_options.getCopyPasteCallback().copyPaste(getRunArg(receiver));
request = new RemoteRequest(proxyID, null, receiver, maintainer);
}
//If allowed to connect to a previously controlled session and a connection could be made
else if (_options.getUsePreviouslyControlledSession() &&
MatlabSessionImpl.connectToRunningSession(receiver.getReceiverID(), _options.getPort())) {
request = new RemoteRequest(proxyID, null, receiver, maintainer);
}
//Else, launch a new session of MATLAB
else {
Process process = createProcess(receiver);
request = new RemoteRequest(proxyID, process, receiver, maintainer);
}
} catch (MatlabConnectionException e) {
maintainer.shutdown();
receiver.shutdown();
throw e;
}
return request;
}
@Override
public MatlabProxy getProxy() throws MatlabConnectionException {
//Request proxy
GetProxyRequestCallback callback = new GetProxyRequestCallback();
Request request = this.requestProxy(callback);
try {
//It is possible (although very unlikely) for the proxy to have been created before the following call to
//sleep occurs. If this happens then the proxy will not be returned until the timeout is reached.
//Wait until the proxy is received or until timeout
try {
Thread.sleep(_options.getProxyTimeout());
} catch (InterruptedException e) {
//If interrupted, it should be because the proxy has been returned - if not throw an exception
if (callback.getProxy() == null) {
throw new MatlabConnectionException("Thread was interrupted while waiting for MATLAB proxy", e);
}
}
//If the proxy has not be received before the timeout
if (callback.getProxy() == null) {
throw new MatlabConnectionException("MATLAB proxy could not be created in " +
_options.getProxyTimeout() + " milliseconds");
}
return callback.getProxy();
} catch (MatlabConnectionException e) {
request.cancel();
throw e;
}
}
/**
* Initializes the registry if it has not already been set up.
*
* @param force if {@code true}, forces creating / getting a registry
* @throws MatlabConnectionException
*/
private synchronized void initRegistry(boolean force) throws MatlabConnectionException {
//If the registry hasn't been created
if (_registry == null || force) {
//Create a RMI registry
try {
_registry = LocalHostRMIHelper.createRegistry(_options.getPort());
}
//If we can't create one, try to retrieve an existing one
catch (Exception e) {
try {
_registry = LocalHostRMIHelper.getRegistry(_options.getPort());
} catch (Exception ex) {
throw new MatlabConnectionException("Could not create or connect to the RMI registry", ex);
}
}
}
}
/**
* Uses the {@link #_options} and the arguments to create a {@link Process} that will launch MATLAB and
* connect it to this JVM.
*
* @param receiver
* @return
* @throws MatlabConnectionException
*/
private Process createProcess(RemoteRequestReceiver receiver) throws MatlabConnectionException {
List processArguments = new ArrayList();
//Location of MATLAB
if (_options.getMatlabLocation() != null) {
processArguments.add(_options.getMatlabLocation());
} else {
processArguments.add(Configuration.getMatlabLocation());
}
//MATLAB flags
if (_options.getHidden()) {
if (Configuration.isWindows()) {
processArguments.add("-automation");
} else {
processArguments.add("-nosplash");
processArguments.add("-nodesktop");
}
} else {
//If running on *NIX based system the -desktop flag is necessary for MATLAB to appear when not executing
//MATLAB from a shell
if (!Configuration.isWindows()) {
processArguments.add("-desktop");
}
}
if (_options.getLicenseFile() != null) {
processArguments.add("-c");
processArguments.add(_options.getLicenseFile());
}
if (_options.getLogFile() != null) {
processArguments.add("-logfile");
processArguments.add(_options.getLogFile());
}
if (_options.getJavaDebugger() != null) {
processArguments.add("-jdb");
processArguments.add(_options.getJavaDebugger().toString());
}
if (_options.getUseSingleComputationalThread()) {
processArguments.add("-singleCompThread");
}
//Argument to follow this will be the code to run on startup
processArguments.add("-r");
processArguments.add(getRunArg(receiver));
//Create process
ProcessBuilder builder = new ProcessBuilder(processArguments);
builder.directory(_options.getStartingDirectory());
try {
Process process = builder.start();
//If running under UNIX and MATLAB is hidden these streams need to be read so that MATLAB does not block
if (_options.getHidden() && !Configuration.isWindows()) {
new ProcessStreamDrainer(process.getInputStream(), "Output", _options.getOutputWriter()).start();
new ProcessStreamDrainer(process.getErrorStream(), "Error", _options.getErrorWriter()).start();
}
return process;
} catch (IOException e) {
//Generate a detailed exception to help in debugging a common cause of this issue
String errorMsg = "Could not launch MATLAB. This is likely caused by MATLAB not being in a known " +
"location or on a known path. MATLAB's location can be explicitly provided by using " +
MatlabProxyFactoryOptions.Builder.class.getCanonicalName() + "'s setMatlabLocation(...) method.\n" +
"OS: " + Configuration.getOperatingSystem() + "\n" +
"Command: " + builder.command() + "\n" +
"Environment: " + builder.environment();
throw new MatlabConnectionException(errorMsg, e);
}
}
/**
* Returns a chunk of MATLAB code which will cause MATLAB to connect with us.
* - Adds matlabcontrol to MATLAB's dynamic class path
* - Adds matlabcontrol to Java's system class loader's class path (to work with RMI properly)
* - Removes matlabcontrol from MATLAB's dynamic class path
* - Tells matlabcontrol running in MATLAB to establish the connection to this JVM
*/
private String getRunArg(RemoteRequestReceiver receiver) throws MatlabConnectionException {
String codeLocation = Configuration.getSupportCodeLocation();
String runArg = "javaaddpath '" + codeLocation + "'; " +
MatlabClassLoaderHelper.class.getName() + ".configureClassLoading(); " +
"javarmpath '" + codeLocation + "'; " +
MatlabConnector.class.getName() + ".connectFromMatlab('" + receiver.getReceiverID() + "', " +
_options.getPort() + ");";
return runArg;
}
/**
* Continously reads the contents of the stream using this daemon thread to prevent the MATLAB process from
* blocking.
*/
private static class ProcessStreamDrainer extends Thread {
private final Reader _reader;
private final Writer _writer;
private ProcessStreamDrainer(InputStream stream, String type, Writer writer) {
_reader = new InputStreamReader(stream, Charset.defaultCharset());
_writer = writer;
this.setDaemon(true);
this.setName("ProcessStreamDrainer - " + type);
}
@Override
public void run() {
try {
char[] buffer = new char[1024];
if (_writer != null) {
int len = 0;
while ((len = _reader.read(buffer)) != -1) {
_writer.write(buffer, 0, len);
_writer.flush();
}
} else {
while (_reader.read(buffer) != -1)
;
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
/**
* Receives a wrapper around JMI from MATLAB.
*/
private class RemoteRequestReceiver implements RequestReceiver {
private final RequestCallback _requestCallback;
private final RemoteIdentifier _proxyID;
private final String _codebase;
private final String[] _canonicalPaths;
private final String _receiverID;
private volatile boolean _receivedJMIWrapper = false;
public RemoteRequestReceiver(RequestCallback requestCallback, RemoteIdentifier proxyID,
String codebase, String[] canonicalPaths) {
_requestCallback = requestCallback;
_proxyID = proxyID;
_codebase = codebase;
_canonicalPaths = canonicalPaths;
_receiverID = "PROXY_RECEIVER_" + proxyID.getUUIDString();
}
@Override
public void receiveJMIWrapper(JMIWrapperRemote jmiWrapper, boolean existingSession) {
//Remove self from the list of receivers
_receivers.remove(this);
//Create proxy
RemoteMatlabProxy proxy = new RemoteMatlabProxy(jmiWrapper, this, _proxyID, existingSession);
proxy.init();
//Record wrapper has been received
_receivedJMIWrapper = true;
//Notify the callback
_requestCallback.proxyCreated(proxy);
}
@Override
public String getReceiverID() {
return _receiverID;
}
public boolean shutdown() {
_receivers.remove(this);
boolean success;
try {
success = UnicastRemoteObject.unexportObject(this, true);
} catch (NoSuchObjectException e) {
success = true;
}
return success;
}
public boolean hasReceivedJMIWrapper() {
return _receivedJMIWrapper;
}
@Override
public String getClassPathAsRMICodebase() throws RemoteException {
return _codebase;
}
@Override
public String[] getClassPathAsCanonicalPaths() throws RemoteException {
return _canonicalPaths;
}
}
/**
* Uses a timer to ensure that a {@link RemoteRequestReceiver} stays bound to the registry.
*/
private class RequestMaintainer {
private final Timer _timer;
RequestMaintainer(final RemoteRequestReceiver receiver) {
_timer = new Timer("MLC Request Maintainer " + receiver.getReceiverID());
_timer.schedule(new TimerTask() {
@Override
public void run() {
//Check if the registry is connected
try {
//Will succeed if connected and the receiver is still exported
_registry.lookup(receiver.getReceiverID());
}
//Receiver is no longer exported
catch (NotBoundException e) {
//Force unexport (it might be bound to a previous registry)
try {
UnicastRemoteObject.unexportObject(receiver, true);
} catch (NoSuchObjectException ex) {}
//Bind the receiver
try {
_registry.bind(receiver.getReceiverID(), LocalHostRMIHelper.exportObject(receiver));
} catch (RemoteException ex) {} catch (AlreadyBoundException ex) {}
}
//Registry is no longer connected
catch (RemoteException e) {
try {
//Create new registry
initRegistry(true);
//Force unexport (it might be bound to a previous registry)
try {
UnicastRemoteObject.unexportObject(receiver, true);
} catch (NoSuchObjectException ex) {}
//Bind the receiver
try {
_registry.bind(receiver.getReceiverID(), LocalHostRMIHelper.exportObject(receiver));
} catch (RemoteException ex) {} catch (AlreadyBoundException ex) {}
} catch (MatlabConnectionException ex) {}
}
//Shutdown maintainer once the JMI wrapper has been received
if (receiver.hasReceivedJMIWrapper()) {
_timer.cancel();
}
}
}, RECEIVER_CHECK_PERIOD, RECEIVER_CHECK_PERIOD);
}
void shutdown() {
_timer.cancel();
}
}
private static class GetProxyRequestCallback implements RequestCallback {
private final Thread _requestingThread;
private volatile MatlabProxy _proxy;
public GetProxyRequestCallback() {
_requestingThread = Thread.currentThread();
}
@Override
public void proxyCreated(MatlabProxy proxy) {
_proxy = proxy;
_requestingThread.interrupt();
}
public MatlabProxy getProxy() {
return _proxy;
}
}
private static final class RemoteIdentifier implements Identifier {
private final UUID _id;
private RemoteIdentifier() {
_id = UUID.randomUUID();
}
@Override
public boolean equals(Object other) {
boolean equals;
if (other instanceof RemoteIdentifier) {
equals = ((RemoteIdentifier) other)._id.equals(_id);
} else {
equals = false;
}
return equals;
}
@Override
public int hashCode() {
return _id.hashCode();
}
@Override
public String toString() {
return "PROXY_REMOTE_" + _id;
}
String getUUIDString() {
return _id.toString();
}
}
private static class RemoteRequest implements Request {
private final Identifier _proxyID;
private final Process _process;
private final RemoteRequestReceiver _receiver;
private final RequestMaintainer _maintainer;
private boolean _isCancelled = false;
private RemoteRequest(Identifier proxyID, Process process, RemoteRequestReceiver receiver,
RequestMaintainer maintainer) {
_proxyID = proxyID;
_process = process;
_receiver = receiver;
_maintainer = maintainer;
}
@Override
public Identifier getProxyIdentifer() {
return _proxyID;
}
@Override
public synchronized boolean cancel() {
if (!_isCancelled) {
_maintainer.shutdown();
boolean success;
if (!this.isCompleted()) {
if (_process != null) {
_process.destroy();
}
success = _receiver.shutdown();
} else {
success = false;
}
_isCancelled = success;
}
return _isCancelled;
}
@Override
public synchronized boolean isCancelled() {
return _isCancelled;
}
@Override
public boolean isCompleted() {
return _receiver.hasReceivedJMIWrapper();
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy