All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.n52.matlab.control.RemoteMatlabProxyFactory Maven / Gradle / Ivy

The newest version!
package org.n52.matlab.control;

/*
 * Copyright (c) 2013, Joshua Kaplan
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *  - Redistributions of source code must retain the above copyright notice, this list of conditions and the following
 *    disclaimer.
 *  - 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.
 *  - Neither the name of matlabcontrol 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.
 */

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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.UUID;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;

import org.n52.matlab.control.MatlabProxy.Identifier;
import org.n52.matlab.control.MatlabProxyFactory.Request;
import org.n52.matlab.control.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(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 allowed to connect to a previously controlled session and a connection could be made
            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");
        
        //Code that MATLAB will run on start. Tells MATLAB to:
        // - 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
        String codeLocation = Configuration.getSupportCodeLocation();
        String runArg = "javaaddpath '" + codeLocation + "'; " + 
                        MatlabClassLoaderHelper.class.getName() + ".configureClassLoading(); " +
                        "javarmpath '" + codeLocation + "'; " +
                        MatlabConnector.class.getName() + ".connectFromMatlab('" + receiver.getReceiverID() + "', " +
                            _options.getPort() + ");";
        processArguments.add(runArg);
        
        //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(), "Input").start();
                new ProcessStreamDrainer(process.getErrorStream(), "Error").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);
        }
    }
    
    /**
     * 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 InputStream _stream;
        
        private ProcessStreamDrainer(InputStream stream, String type)
        {
            _stream = stream;
            
            this.setDaemon(true);
            this.setName("ProcessStreamDrainer - " + type);
        }
        
        @Override
        public void run()
        {
            try
            {
                BufferedReader in = new BufferedReader(new InputStreamReader(_stream));
                while(in.readLine() != null);
            }
            catch(IOException 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