Skip to content

Commit 7c40e24

Browse files
committed
FEATURE: connect to existing matlab sessions
Only compiled for mexa64. Also, miscelaneous PEP8 and doc updates and logging now includes _Session output.
1 parent 3de505b commit 7c40e24

File tree

7 files changed

+77
-44
lines changed

7 files changed

+77
-44
lines changed

pymatbridge/matlab/matlabserver.m

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
function matlabserver(socket_address, exit_when_done)
22
%MATLABSERVER Take Commands from Python via ZMQ
3+
% matlabserver(socket_address, exit_when_done)
34
% This function takes a socket address as input and initiates a ZMQ session
4-
% over the socket. I then enters the listen-respond mode until it gets an
5-
% "exit" command
5+
% over the socket. It then enters the listen-respond mode until it gets an
6+
% "exit" command, at which point it returns (or exits if exit_when_done), or
7+
% until it gets a "separate" command, at which point it launches the MATLAB
8+
% desktop client and returns.
9+
%
610

7-
json_startup
11+
initialize_environment;
12+
json_startup;
813
messenger('init', socket_address);
914

1015
if nargin > 1 && exit_when_done
@@ -28,10 +33,15 @@ function matlabserver(socket_address, exit_when_done)
2833
resp = pymat_eval(req);
2934
messenger('respond', resp);
3035

36+
case {'separate'}
37+
desktop; %no-op if desktop is already up
38+
c = onCleanup(@stop_messenger);
39+
break;
40+
3141
otherwise
3242
messenger('respond', 'i dont know what you want');
33-
end
3443

44+
end
3545
end
3646

3747
end %matlabserver
@@ -44,3 +54,12 @@ function stop_messenger_and_exit()
4454
function stop_messenger()
4555
messenger('exit');
4656
end
57+
58+
function initialize_environment()
59+
if ~exist('json_startup', 'file')
60+
[pathstr, ~, ~] = fileparts(mfilename('fullpath'));
61+
old_warning_state = warning('off','all');
62+
addpath(genpath(pathstr));
63+
warning(old_warning_state);
64+
end
65+
end

pymatbridge/matlab/util/finish.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
% make sure socket gets closed no matter how MATLAB exists (sans crash)
2+
if exist('messenger', 'file')
3+
if messenger('check')
4+
messenger('exit');
5+
end
6+
end

pymatbridge/messenger/make.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def get_config():
172172

173173

174174
def do_build(make_cmd, messenger_exe):
175-
print('Building %s...' % messenger_exe)
175+
print('Building {}...'.format(messenger_exe))
176176
print(make_cmd)
177177
messenger_dir = get_messenger_dir()
178178
subprocess.check_output(shlex.split(make_cmd), shell=use_shell)
@@ -188,7 +188,7 @@ def do_build(make_cmd, messenger_exe):
188188
def build_octave():
189189
paths = "-L%(octave_lib)s -I%(octave_inc)s -L%(zmq_lib)s -I%(zmq_inc)s"
190190
paths = paths % get_config()
191-
make_cmd = "mkoctfile --mex %s -lzmq ./src/messenger.c" % paths
191+
make_cmd = "mkoctfile --mex {} -lzmq ./src/messenger.c".format(paths)
192192
do_build(make_cmd, 'messenger.mex')
193193

194194

@@ -264,10 +264,10 @@ def build_matlab(static=False):
264264
# Build the mex file
265265
mex = esc(os.path.join(matlab_bin, "mex"))
266266
paths = "-L%(zmq_lib)s -I%(zmq_inc)s" % cfg
267-
make_cmd = '%s -O %s -lzmq ./src/messenger.c' % (mex, paths)
267+
make_cmd = '{} -O {} -lzmq ./src/messenger.c'.format(mex, paths)
268268
if static:
269269
make_cmd += ' -DZMQ_STATIC'
270-
do_build(make_cmd, 'messenger.%s' % extension)
270+
do_build(make_cmd, 'messenger.{}'.format(extension))
271271

272272

273273
if __name__ == '__main__':
48 Bytes
Binary file not shown.

pymatbridge/messenger/src/messenger.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ void mexFunction(int nlhs, mxArray *plhs[],
145145
} else if (strcmp(cmd, "exit") == 0) {
146146
cleanup();
147147
p[0] = 1;
148+
} else if (strcmp(cmd, "check") == 0) {
149+
if (initialized) {
150+
p[0] = 1;
151+
}
148152
} else {
149153
mexErrMsgTxt("Unidentified command");
150154
}

pymatbridge/pymatbridge.py

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class _Session(object):
121121
"""
122122

123123
def __init__(self, executable=None, socket_addr=None,
124-
id='python-matlab-bridge', log=False, maxtime=15,
124+
id='python-matlab-bridge', log="", maxtime=15,
125125
platform=None, startup_options=None,
126126
loglevel=logging.WARNING):
127127
"""
@@ -141,7 +141,7 @@ def __init__(self, executable=None, socket_addr=None,
141141
id : str
142142
An identifier for this instance of the pymatbridge.
143143
144-
log : str | None
144+
log : str
145145
Location to log to, defaults to sys.stdout
146146
147147
maxtime : float
@@ -174,7 +174,7 @@ def __init__(self, executable=None, socket_addr=None,
174174
self.socket_addr = socket_addr
175175
self.context = None
176176
self.socket = None
177-
self.logger = logging.getLogger("pymatbridge")
177+
self.logger = logging.getLogger(self.id)
178178
self.logger.setLevel(self.loglevel)
179179
if self.log:
180180
self.loghandler = logging.FileHandler(self.log)
@@ -188,26 +188,15 @@ def __init__(self, executable=None, socket_addr=None,
188188
def _program_name(self): # pragma: no cover
189189
raise NotImplementedError
190190

191-
def _preamble_code(self):
192-
# suppress warnings while loading the path, in the case of
193-
# overshadowing a built-in function on a newer version of
194-
# Matlab (e.g. isrow)
195-
return ["old_warning_state = warning('off','all');",
196-
"addpath(genpath('%s'));" % MATLAB_FOLDER,
197-
"warning(old_warning_state);",
198-
"clear('old_warning_state');",
199-
"cd('%s');" % os.getcwd()]
200-
201191
def _execute_flag(self): # pragma: no cover
202192
raise NotImplementedError
203193

204194
def _run_server(self):
205-
code = self._preamble_code()
206-
code.extend([
207-
"matlabserver('%s')" % self.socket_addr
208-
])
195+
code = "cd('{}'); matlabserver('{}', true);".format(
196+
MATLAB_FOLDER, self.socket_addr)
209197
command = '%s %s %s "%s"' % (self.executable, self.startup_options,
210-
self._execute_flag(), ','.join(code))
198+
self._execute_flag(), code)
199+
self.logger.info("Running: %s", command)
211200
subprocess.Popen(command, shell=True, stdin=subprocess.PIPE,
212201
stdout=subprocess.PIPE)
213202

@@ -221,13 +210,12 @@ def start(self):
221210
self.socket_addr = self.socket_addr + ":%s"%rndport
222211

223212
# Start the MATLAB server in a new process
224-
self.logger.info("Starting ZMQ socket %s" \
225-
% (self.socket_addr, ))
226-
self.logger.info("Send 'exit' command to kill the server")
213+
self.logger.info("Starting ZMQ socket %s", self.socket_addr)
214+
#self.logger.info("Run _Session.stop/separate to kill the server.")
227215

228216
if self.executable is not None:
229-
self.logger.info("Launching %s, sending ZMQ 'exit' will kill it." \
230-
% (self._program_name(), ))
217+
self.logger.info("Launching %s, sending ZMQ 'exit' will kill it.", \
218+
self._program_name())
231219
self._run_server()
232220

233221
# Start the client
@@ -239,7 +227,7 @@ def start(self):
239227
if not self.is_connected():
240228
raise ValueError("%s failed to start" % self._program_name())
241229

242-
self.logger.info("%s started and connected!" % self._program_name())
230+
self.logger.info("%s started and connected!", self._program_name())
243231
self.set_plot_settings()
244232
return self
245233

@@ -249,14 +237,23 @@ def _response(self, **kwargs):
249237
resp = self.socket.recv_string()
250238
return resp
251239

240+
# open desktop matlab and disconnect
241+
def separate(self):
242+
if not self.started:
243+
raise ValueError('Session not started, use start()')
244+
if self._response(cmd='separate') == 'exit':
245+
self.logger.info("%s split off", self._program_name())
246+
self.started = False
247+
return True
248+
252249
# Stop the Matlab server
253250
def stop(self):
254251
if not self.started:
255252
return True
256253

257254
# Matlab should respond with "exit" if successful
258255
if self._response(cmd='exit') == "exit":
259-
self.logger.info("%s closed" % self._program_name())
256+
self.logger.info("%s closed", self._program_name())
260257

261258
self.started = False
262259
return True
@@ -337,6 +334,12 @@ def run_code(self, code):
337334
"""
338335
return self.run_func('evalin', 'base', code, nargout=0)
339336

337+
def _init_run_code(self):
338+
"""Code to run at start of headless session to ensure sane
339+
environment."""
340+
code = "cd('%s')" % os.getcwd()
341+
return self.run_code(code)
342+
340343
def get_variable(self, varname, default=None):
341344
resp = self.run_func('evalin', 'base', varname)
342345
return resp['result'] if resp['success'] else default
@@ -445,13 +448,14 @@ def __init__(self, executable='matlab', socket_addr=None,
445448
446449
socket_addr : str
447450
A string that represents a valid ZMQ socket address, such as
448-
"ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc.
451+
"ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. Default is
452+
to choose a random IPC file name, or a random socket (for TCP).
449453
450454
id : str
451455
An identifier for this instance of the pymatbridge.
452456
453-
log : bool
454-
Whether to save a log file in some known location.
457+
log : str
458+
Location to log to, defaults to sys.stdout
455459
456460
maxtime : float
457461
The maximal time to wait for a response from matlab (optional,
@@ -473,7 +477,7 @@ def __init__(self, executable='matlab', socket_addr=None,
473477
else:
474478
startup_options = ' -nodesktop -nosplash'
475479
if log:
476-
startup_options += ' -logfile ./pymatbridge/logs/matlablog_%s.txt' % id
480+
startup_options += ' -logfile "{}"'.format(log)
477481
super(Matlab, self).__init__(executable, socket_addr, id, log, maxtime,
478482
platform, startup_options, loglevel)
479483

@@ -497,7 +501,7 @@ def __init__(self, executable='octave', socket_addr=None,
497501
498502
executable : str
499503
A string that would start Octave at the terminal. Per default, this
500-
is set to 'octave', so that you can alias in your bash setup
504+
is set to 'octave'.
501505
502506
socket_addr : str
503507
A string that represents a valid ZMQ socket address, such as
@@ -529,12 +533,12 @@ def __init__(self, executable='octave', socket_addr=None,
529533
def _program_name(self):
530534
return 'Octave'
531535

532-
def _preamble_code(self):
533-
code = super(Octave, self)._preamble_code()
534-
if self.log:
535-
code.append("diary('./pymatbridge/logs/octavelog_%s.txt')" % self.id)
536+
def _init_run_code(self):
537+
code = super(Octave, self)._init_run_code()
536538
code.append("graphics_toolkit('gnuplot')")
537-
return code
539+
if self.log:
540+
code.append("diary('{}')".format(self.id))
541+
self.run_code(code)
538542

539543
def _execute_flag(self):
540544
return '--eval'

pymatbridge/tests/test_run_code.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_stack_traces(self):
7171
this_dir = os.path.abspath(os.path.dirname(__file__))
7272
test_file = os.path.join(this_dir, 'test_stack_trace.m')
7373

74-
self.mlab.run_code("addpath('%s')" % this_dir)
74+
self.mlab.run_code("addpath('{}')".format(this_dir))
7575
response = self.mlab.run_code('test_stack_trace(10)')
7676
npt.assert_equal(response['stack'], [
7777
{'name': 'baz', 'line': 14, 'file': test_file},

0 commit comments

Comments
 (0)