diff --git a/.gitignore b/.gitignore index 5bd58c1..de7e727 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ rebuildTags.sh # caused py setup.py develop pymatbridge.egg-info + +# so vim-ers can use "YouCompleteMe" +.ycm_extra_conf.py diff --git a/pymatbridge/matlab/matlabserver.m b/pymatbridge/matlab/matlabserver.m index e70bc71..b722ac3 100644 --- a/pymatbridge/matlab/matlabserver.m +++ b/pymatbridge/matlab/matlabserver.m @@ -1,12 +1,18 @@ -function matlabserver(socket_address) +function matlabserver(socket_address, exit_when_done) +%MATLABSERVER Take Commands from Python via ZMQ +% matlabserver(socket_address, exit_when_done) % This function takes a socket address as input and initiates a ZMQ session -% over the socket. I then enters the listen-respond mode until it gets an -% "exit" command - -json_startup +% over the socket. It then enters the listen-respond mode until it gets an +% "exit" command, at which point it returns (or exits if exit_when_done), or +% until it gets a "separate" command, at which point it launches the MATLAB +% desktop client and returns. +% + +initialize_environment; +json_startup; messenger('init', socket_address); -c=onCleanup(@()exit); +c = onCleanup(@stop_messenger); while(1) msg_in = messenger('listen'); @@ -17,15 +23,41 @@ function matlabserver(socket_address) messenger('respond', 'connected'); case {'exit'} - messenger('exit'); break; case {'eval'} resp = pymat_eval(req); messenger('respond', resp); + case {'separate'} + desktop; %no-op if desktop is already up + exit_when_done = false; + break; + otherwise - messenger('respond', 'i dont know what you want'); + messenger('respond', 'Unknown command recieved by matlabserver via ZMQ.'); + + end +end + +if nargin > 1 && exit_when_done + %c.task(); % already executed by "finish" + exit; +end + +end %matlabserver + +function stop_messenger() + if messenger('check') + messenger('exit'); end +end +function initialize_environment() + if ~exist('json_startup', 'file') + [pathstr, ~, ~] = fileparts(mfilename('fullpath')); + old_warning_state = warning('off','all'); + addpath(genpath(pathstr)); + warning(old_warning_state); + end end diff --git a/pymatbridge/matlab/util/finish.m b/pymatbridge/matlab/util/finish.m new file mode 100644 index 0000000..7f2d2cd --- /dev/null +++ b/pymatbridge/matlab/util/finish.m @@ -0,0 +1,6 @@ +% make sure socket gets closed no matter how MATLAB exists (sans crash) +if exist('messenger', 'file') + if messenger('check') + messenger('exit'); + end +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m index c085226..f65fa28 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m @@ -106,7 +106,7 @@ double_struct = struct; double_struct.ndarray = 1; value = double(value); - if isreal(value) + if isreal(value) double_struct.data = base64encode(typecast(value(:), 'uint8')); else double_struct.real = base64encode(typecast(real(value(:)), 'uint8')); @@ -142,9 +142,13 @@ obj.put(dump_data_(value{i}, options)); end elseif isnumeric(value) - if isreal(value) + if ~isfinite(value) + json_non_finite = struct; + json_non_finite.json_non_finite = lower(num2str(value)); + obj = dump_data_(json_non_finite, options); + elseif isreal(value) obj = value; - % Encode complex number as a struct + % Encode finite complex number as a struct else complex_struct = struct; complex_struct.real = real(value); @@ -158,7 +162,7 @@ keys = fieldnames(value); for i = 1:length(keys) try - obj.put(keys{i},dump_data_(value.(keys{i}), options)); + obj.put(keys{i}, dump_data_(value.(keys{i}), options)); catch ME obj.put(keys{i}, dump_data_(ME.message, options)) end diff --git a/pymatbridge/matlab/util/pymat_eval.m b/pymatbridge/matlab/util/pymat_eval.m index f38856f..6f19672 100644 --- a/pymatbridge/matlab/util/pymat_eval.m +++ b/pymatbridge/matlab/util/pymat_eval.m @@ -1,4 +1,4 @@ -function json_response = pymat_eval(req); +function json_response = pymat_eval(req) % PYMAT_EVAL: Returns a json object of the result of calling the function % % json_response = pymat_eval(req); diff --git a/pymatbridge/matlab_magic.py b/pymatbridge/matlab_magic.py index de3233f..dd35875 100644 --- a/pymatbridge/matlab_magic.py +++ b/pymatbridge/matlab_magic.py @@ -23,7 +23,7 @@ from .compat import text_type -class MatlabInterperterError(RuntimeError): +class MatlabInterpreterError(RuntimeError): """ Some error occurs while matlab is running """ @@ -87,7 +87,7 @@ def eval(self, line): run_dict = self.Matlab.run_code(line) if not run_dict['success']: - raise MatlabInterperterError(line, run_dict['content']['stdout']) + raise MatlabInterpreterError(line, run_dict['content']['stdout']) # This is the matlab stdout: return run_dict @@ -99,7 +99,7 @@ def set_matlab_var(self, name, value): run_dict = self.Matlab.set_variable(name, value) if not run_dict['success']: - raise MatlabInterperterError(line, run_dict['content']['stdout']) + raise MatlabInterpreterError(line, run_dict['content']['stdout']) @magic_arguments() @@ -161,7 +161,7 @@ def matlab(self, line, cell=None, local_ns=None): try: result_dict = self.eval(code) - except MatlabInterperterError: + except MatlabInterpreterError: raise except: raise RuntimeError('\n'.join([ diff --git a/pymatbridge/messenger/make.py b/pymatbridge/messenger/make.py index 20045bc..7c68011 100755 --- a/pymatbridge/messenger/make.py +++ b/pymatbridge/messenger/make.py @@ -172,7 +172,7 @@ def get_config(): def do_build(make_cmd, messenger_exe): - print('Building %s...' % messenger_exe) + print('Building {}...'.format(messenger_exe)) print(make_cmd) messenger_dir = get_messenger_dir() subprocess.check_output(shlex.split(make_cmd), shell=use_shell) @@ -188,7 +188,7 @@ def do_build(make_cmd, messenger_exe): def build_octave(): paths = "-L%(octave_lib)s -I%(octave_inc)s -L%(zmq_lib)s -I%(zmq_inc)s" paths = paths % get_config() - make_cmd = "mkoctfile --mex %s -lzmq ./src/messenger.c" % paths + make_cmd = "mkoctfile --mex {} -lzmq ./src/messenger.c".format(paths) do_build(make_cmd, 'messenger.mex') @@ -264,10 +264,10 @@ def build_matlab(static=False): # Build the mex file mex = esc(os.path.join(matlab_bin, "mex")) paths = "-L%(zmq_lib)s -I%(zmq_inc)s" % cfg - make_cmd = '%s -O %s -lzmq ./src/messenger.c' % (mex, paths) + make_cmd = '{} -O {} -lzmq ./src/messenger.c'.format(mex, paths) if static: make_cmd += ' -DZMQ_STATIC' - do_build(make_cmd, 'messenger.%s' % extension) + do_build(make_cmd, 'messenger.{}'.format(extension)) if __name__ == '__main__': diff --git a/pymatbridge/messenger/mexa64/messenger.mexa64 b/pymatbridge/messenger/mexa64/messenger.mexa64 index f585eab..4bf464f 100755 Binary files a/pymatbridge/messenger/mexa64/messenger.mexa64 and b/pymatbridge/messenger/mexa64/messenger.mexa64 differ diff --git a/pymatbridge/messenger/src/messenger.c b/pymatbridge/messenger/src/messenger.c index 102999e..84e8814 100644 --- a/pymatbridge/messenger/src/messenger.c +++ b/pymatbridge/messenger/src/messenger.c @@ -48,6 +48,7 @@ void cleanup (void) { mexPrintf("Socket closed\n"); zmq_term(ctx); mexPrintf("Context terminated\n"); + initialized = 0; } @@ -55,54 +56,50 @@ void cleanup (void) { void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { char *cmd; + mxLogical *p; /* If no input argument, print out the usage */ if (nrhs == 0) { mexErrMsgTxt("Usage: messenger('init|listen|respond', extra1, extra2, ...)"); } - + if (nlhs > 1) { + mexErrMsgTxt("messenger: too many outputs, I return a single bool"); + } /* Get the input command */ if(!(cmd = mxArrayToString(prhs[0]))) { mexErrMsgTxt("Cannot read the command"); } - + /* we'll return true on completion of valid command, else false */ + plhs[0] = mxCreateLogicalMatrix(1, 1); + p = mxGetLogicals(plhs[0]); + p[0] = false; /* Initialize a new server session */ if (strcmp(cmd, "init") == 0) { char *socket_addr; - mxLogical *p; - /* Check if the input format is valid */ if (nrhs != 2) { - mexErrMsgTxt("Missing argument: socket address"); + mexErrMsgTxt("Usage: messenger('init', socket_addr)"); } if (!(socket_addr = mxArrayToString(prhs[1]))) { mexErrMsgTxt("Cannot read socket address"); } - - plhs[0] = mxCreateLogicalMatrix(1, 1); - p = mxGetLogicals(plhs[0]); - if (!initialized) { if (!initialize(socket_addr)) { - p[0] = 1; + p[0] = true; mexPrintf("Socket created at: %s\n", socket_addr); } else { - p[0] = 0; mexErrMsgTxt("Socket creation failed."); } } else { mexErrMsgTxt("One socket has already been initialized."); } - - return; - /* Listen over an existing socket */ } else if (strcmp(cmd, "listen") == 0) { int byte_recvd; char *recv_buffer = mxCalloc(BUFLEN, sizeof(char)); zmq_pollitem_t polls[] = {{socket_ptr, 0, ZMQ_POLLIN, 0}}; - + if (!checkInitialized()) return; - + /* allow MATLAB to draw its graphics every 20ms */ while (zmq_poll(polls, 1, 20000) == 0) { mexEvalString("drawnow"); @@ -116,17 +113,14 @@ void mexFunction(int nlhs, mxArray *plhs[], } else if (byte_recvd > BUFLEN){ mexErrMsgTxt("Receiver buffer overflow. Message truncated"); } else { - sprintf(recv_buffer, "Failed to receive a message due to ZMQ error %s", strerror(errno)); + sprintf(recv_buffer, "Failed to receive a message due to ZMQ error %s", strerror(errno)); mexErrMsgTxt(recv_buffer); } - - return; - + p[0] = true; /* Send a message out */ } else if (strcmp(cmd, "respond") == 0) { size_t msglen; char *msg_out; - mxLogical *p; /* Check if the input format is valid */ if (nrhs != 2) { @@ -138,24 +132,26 @@ void mexFunction(int nlhs, mxArray *plhs[], msglen = mxGetNumberOfElements(prhs[1]); msg_out = mxArrayToString(prhs[1]); - plhs[0] = mxCreateLogicalMatrix(1, 1); - p = mxGetLogicals(plhs[0]); - - if (msglen == zmq_send(socket_ptr, msg_out, msglen, 0)) { - p[0] = 1; + if (msglen == (size_t) zmq_send(socket_ptr, msg_out, msglen, 0)) { + p[0] = true; } else { - p[0] = 0; mexErrMsgTxt("Failed to send message due to ZMQ error"); } - - return; - /* Close the socket and context */ } else if (strcmp(cmd, "exit") == 0) { - cleanup(); - - return; + if (initialized) { + cleanup(); + p[0] = true; + initialized = 0; + } else { + mexErrMsgTxt("No open socket to exit."); + } + } else if (strcmp(cmd, "check") == 0) { + if (initialized) { + p[0] = true; + } } else { mexErrMsgTxt("Unidentified command"); } + return; } diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 18b3b8b..e02e074 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -32,6 +32,7 @@ import types import weakref import random +import logging from uuid import uuid4 from numpy import ndarray, generic, float64, frombuffer, asfortranarray @@ -107,6 +108,8 @@ def decode_pymat(dct): return data.reshape(shape, order='F') elif 'real' in dct and 'imag' in dct: return complex(dct['real'], dct['imag']) + elif 'json_non_finite' in dct: + return float(dct['json_non_finite']) return dct MATLAB_FOLDER = '%s/matlab' % os.path.realpath(os.path.dirname(__file__)) @@ -119,9 +122,10 @@ class _Session(object): this directly; rather, use the Matlab or Octave subclasses. """ - def __init__(self, executable, socket_addr=None, - id='python-matlab-bridge', log=False, maxtime=60, - platform=None, startup_options=None): + def __init__(self, executable=None, socket_addr=None, + id='python-matlab-bridge', log="", maxtime=15, + platform=None, startup_options=None, + loglevel=logging.WARNING): """ Initialize this thing. @@ -139,12 +143,12 @@ def __init__(self, executable, socket_addr=None, id : str An identifier for this instance of the pymatbridge. - log : bool - Whether to save a log file in some known location. + log : str + Location to log to, defaults to sys.stdout maxtime : float The maximal time to wait for a response from the session (optional, - Default is 10 sec) + Default is 15 sec) platform : string The OS of the machine on which this is running. Per default this @@ -162,40 +166,39 @@ def __init__(self, executable, socket_addr=None, self.maxtime = maxtime self.platform = platform if platform is not None else sys.platform self.startup_options = startup_options - + self.loglevel = loglevel if socket_addr is None: - self.socket_addr = "tcp://127.0.0.1" if self.platform == "win32" else "ipc:///tmp/pymatbridge-%s"%str(uuid4()) - - if self.log: - startup_options += ' > ./pymatbridge/logs/bashlog_%s.txt' % self.id - + if self.platform == "win32": + self.socket_addr = "tcp://127.0.0.1" + else: + self.socket_addr = "ipc:///tmp/pymatbridge-%s" % str(uuid4()) + else: + self.socket_addr = socket_addr self.context = None self.socket = None + self.logger = logging.getLogger(self.id) + self.logger.setLevel(self.loglevel) + if self.log: + self.loghandler = logging.FileHandler(self.log) + else: + self.loghandler = logging.StreamHandler(stream=sys.stdout) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + self.loghandler.setFormatter(formatter) + self.logger.addHandler(self.loghandler) atexit.register(self.stop) def _program_name(self): # pragma: no cover - raise NotImplemented - - def _preamble_code(self): - # suppress warnings while loading the path, in the case of - # overshadowing a built-in function on a newer version of - # Matlab (e.g. isrow) - return ["old_warning_state = warning('off','all');", - "addpath(genpath('%s'));" % MATLAB_FOLDER, - "warning(old_warning_state);", - "clear('old_warning_state');", - "cd('%s');" % os.getcwd()] + raise NotImplementedError def _execute_flag(self): # pragma: no cover - raise NotImplemented + raise NotImplementedError def _run_server(self): - code = self._preamble_code() - code.extend([ - "matlabserver('%s')" % self.socket_addr - ]) + code = "cd('{}'); matlabserver('{}', true);".format( + MATLAB_FOLDER, self.socket_addr) command = '%s %s %s "%s"' % (self.executable, self.startup_options, - self._execute_flag(), ','.join(code)) + self._execute_flag(), code) + self.logger.info("Running: %s", command) subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) @@ -209,9 +212,13 @@ def start(self): self.socket_addr = self.socket_addr + ":%s"%rndport # Start the MATLAB server in a new process - print("Starting %s on ZMQ socket %s" % (self._program_name(), self.socket_addr)) - print("Send 'exit' command to kill the server") - self._run_server() + self.logger.info("Starting ZMQ socket %s", self.socket_addr) + #self.logger.info("Run _Session.stop/separate to kill the server.") + + if self.executable: + self.logger.info("Launching %s, sending ZMQ 'exit' will kill it.", \ + self._program_name()) + self._run_server() # Start the client self.socket.connect(self.socket_addr) @@ -219,35 +226,48 @@ def start(self): self.started = True # Test if connection is established - if self.is_connected(): - print("%s started and connected!" % self._program_name()) - self.set_plot_settings() - return self - else: + if not self.is_connected(): raise ValueError("%s failed to start" % self._program_name()) + self.logger.info("%s started and connected!", self._program_name()) + self.set_plot_settings() + return self + def _response(self, **kwargs): req = json.dumps(kwargs, cls=PymatEncoder) self.socket.send_string(req) resp = self.socket.recv_string() return resp + # open desktop matlab and disconnect + def separate(self): + if not self.started: + raise ValueError('Session not started, use start()') + if self._response(cmd='separate') != 'exit': + raise ValueError('Failed to separate from {}'.format(self._program_name)) + self.socket.close() + self.context.term() + self.logger.info("%s split off", self._program_name()) + self.started = False + return True + # Stop the Matlab server def stop(self): if not self.started: return True # Matlab should respond with "exit" if successful - if self._response(cmd='exit') == "exit": - print("%s closed" % self._program_name()) - + if self._response(cmd='exit') != "exit": + raise ValueError("Failed to stop {}".format(self._program_name)) + self.socket.close() + self.context.term() + self.logger.info("%s closed", self._program_name()) self.started = False return True # To test if the client can talk to the server def is_connected(self): if not self.started: - time.sleep(2) return False req = json.dumps(dict(cmd="connect"), cls=PymatEncoder) @@ -259,10 +279,11 @@ def is_connected(self): resp = self.socket.recv_string(flags=zmq.NOBLOCK) return resp == "connected" except zmq.ZMQError: - sys.stdout.write('.') time.sleep(1) if time.time() - start_time > self.maxtime: - print("%s session timed out after %d seconds" % (self._program_name(), self.maxtime)) + timed_out_str = "%s session timed out after %d seconds" \ + % (self._program_name(), self.maxtime) + self.logger.warn(timed_out_str) return False def is_function_processor_working(self): @@ -320,6 +341,12 @@ def run_code(self, code): """ return self.run_func('evalin', 'base', code, nargout=0) + def _init_run_code(self): + """Code to run at start of headless session to ensure sane + environment.""" + code = "cd('%s')" % os.getcwd() + return self.run_code(code) + def get_variable(self, varname, default=None): resp = self.run_func('evalin', 'base', varname) return resp['result'] if resp['success'] else default @@ -413,8 +440,9 @@ def _bind_method(self, name, unconditionally=False): class Matlab(_Session): def __init__(self, executable='matlab', socket_addr=None, - id='python-matlab-bridge', log=False, maxtime=60, - platform=None, startup_options=None): + id='python-matlab-bridge', log=False, maxtime=15, + platform=None, startup_options=None, + loglevel=logging.WARNING): """ Initialize this thing. @@ -423,21 +451,22 @@ def __init__(self, executable='matlab', socket_addr=None, executable : str A string that would start Matlab at the terminal. Per default, this - is set to 'matlab', so that you can alias in your bash setup + is set to 'matlab'. socket_addr : str A string that represents a valid ZMQ socket address, such as - "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. Default is + to choose a random IPC file name, or a random socket (for TCP). id : str An identifier for this instance of the pymatbridge. - log : bool - Whether to save a log file in some known location. + log : str + Location to log to, defaults to sys.stdout maxtime : float The maximal time to wait for a response from matlab (optional, - Default is 10 sec) + Default is 15 sec) platform : string The OS of the machine on which this is running. Per default this @@ -455,9 +484,9 @@ def __init__(self, executable='matlab', socket_addr=None, else: startup_options = ' -nodesktop -nosplash' if log: - startup_options += ' -logfile ./pymatbridge/logs/matlablog_%s.txt' % id + startup_options += ' -logfile "{}"'.format(log) super(Matlab, self).__init__(executable, socket_addr, id, log, maxtime, - platform, startup_options) + platform, startup_options, loglevel) def _program_name(self): return 'MATLAB' @@ -468,8 +497,9 @@ def _execute_flag(self): class Octave(_Session): def __init__(self, executable='octave', socket_addr=None, - id='python-matlab-bridge', log=False, maxtime=60, - platform=None, startup_options=None): + id='python-matlab-bridge', log=False, maxtime=15, + platform=None, startup_options=None, + loglevel=logging.WARNING): """ Initialize this thing. @@ -478,7 +508,7 @@ def __init__(self, executable='octave', socket_addr=None, executable : str A string that would start Octave at the terminal. Per default, this - is set to 'octave', so that you can alias in your bash setup + is set to 'octave'. socket_addr : str A string that represents a valid ZMQ socket address, such as @@ -492,7 +522,7 @@ def __init__(self, executable='octave', socket_addr=None, maxtime : float The maximal time to wait for a response from octave (optional, - Default is 10 sec) + Default is 15 sec) platform : string The OS of the machine on which this is running. Per default this @@ -510,12 +540,12 @@ def __init__(self, executable='octave', socket_addr=None, def _program_name(self): return 'Octave' - def _preamble_code(self): - code = super(Octave, self)._preamble_code() - if self.log: - code.append("diary('./pymatbridge/logs/octavelog_%s.txt')" % self.id) + def _init_run_code(self): + code = super(Octave, self)._init_run_code() code.append("graphics_toolkit('gnuplot')") - return code + if self.log: + code.append("diary('{}')".format(self.id)) + self.run_code(code) def _execute_flag(self): return '--eval' diff --git a/pymatbridge/tests/test_magic.py b/pymatbridge/tests/test_magic.py index f9ecb00..c892ba4 100644 --- a/pymatbridge/tests/test_magic.py +++ b/pymatbridge/tests/test_magic.py @@ -1,7 +1,7 @@ import os import pymatbridge as pymat -from pymatbridge.matlab_magic import MatlabInterperterError +from pymatbridge.matlab_magic import MatlabInterpreterError from IPython.testing.globalipapp import get_ipython import numpy.testing as npt @@ -99,5 +99,5 @@ def test_struct(self): npt.assert_equal(self.ip.user_ns['obj']['str'], self.ip.user_ns['str']) def test_faulty(self): - npt.assert_raises(MatlabInterperterError, + npt.assert_raises(MatlabInterpreterError, lambda: self.ip.run_line_magic('matlab', '1 = 2')) diff --git a/pymatbridge/tests/test_run_code.py b/pymatbridge/tests/test_run_code.py index e4255e7..b2c7b6f 100644 --- a/pymatbridge/tests/test_run_code.py +++ b/pymatbridge/tests/test_run_code.py @@ -71,7 +71,7 @@ def test_stack_traces(self): this_dir = os.path.abspath(os.path.dirname(__file__)) test_file = os.path.join(this_dir, 'test_stack_trace.m') - self.mlab.run_code("addpath('%s')" % this_dir) + self.mlab.run_code("addpath('{}')".format(this_dir)) response = self.mlab.run_code('test_stack_trace(10)') npt.assert_equal(response['stack'], [ {'name': 'baz', 'line': 14, 'file': test_file},