Improve network communication between Emacs & MATLAB

matlab-netshell.el:
(matlab-netshell-acc, matlab-netshell-filter):
Keep an accumulator for input from the tcp process.
Packets are seperated by \0.
Commands start, end with newline, then data follows.
Call below with found commands.
(matlab-netshell-execute-command):
Log commands recieved.
Evaluate different commands.  Implement ACK.
(matlab-netshell-sentinel):
Create buffer for newly formed network processes.
(matlab-netshell-send): Fix to send CMD,DATA, not just msg.
(matlab-netshell-eval): Send request to eval something in ML.
(matlab-netshell-ack): Send ACK request.

+emacs/@EmacsServer/EmacsServer.m
NEW.  Object to handle MATLAB state for connecting over tcp
with Emacs.
Implementation started in emacsnetshell.m
Added command parsing (same as Emacs) and command execution.
Implement ACK

emacsnetshell.m:
Remove old impl.  Use EmacsServer for core behavior.
Act as interface to object so user can send commands
via this function.
This commit is contained in:
Eric Ludlam 2019-11-22 17:52:39 -05:00
parent 25c2fe36c6
commit 8296c09180
3 changed files with 253 additions and 74 deletions

View file

@ -60,15 +60,62 @@
(delete-process matlab-netshell-name)
)
(defvar matlab-netshell-acc ""
"Text Accumulator for MATLAB's netshell.")
(make-variable-buffer-local 'matlab-netshell-acc)
(defun matlab-netshell-filter (proc string)
"Filter used for MATLAB Netshell processes."
;; We recieved a command from PROC. Interpret the command.
(cond ((string-match "^init" string)
;; Accumulate from the process
(setq matlab-netshell-acc (concat matlab-netshell-acc string))
;; Wait for a NULL command terminator.
(while (string-match "\0" matlab-netshell-acc)
(let* ((cmdstr (substring matlab-netshell-acc 0 (match-beginning 0))))
;; Trim off our new command from accumulator.
(setq matlab-netshell-acc (substring matlab-netshell-acc (match-end 0)))
;; Is this a test packet? If so, we're done.
(when (not (string= cmdstr ""))
;; Find the newline.
(unless (string-match "\n" cmdstr)
(message "Unable to find command in MATLAB command. Ignoring."))
(let ((cmd (substring cmdstr 0 (match-beginning 0)))
(data
(let ((me (match-end 0)))
(if (> (length cmdstr) me)
(substring cmdstr me)
""))))
(matlab-netshell-execute-command proc cmd data)
)))))
(defun matlab-netshell-execute-command (proc cmd data)
"For MATLAB associated with PROC, execute CMD with DATA.
The CMD is a request from MATLAB to do something in Emacs.
A common command might be to display data to the user as a
response from some Emacs based request."
;; Log the command
(with-current-buffer (process-buffer proc)
(goto-char (point-max))
(when (not (= (current-column) 0))
(insert "\n"))
(insert "Command: " cmd "\n")
(when (not (string= data ""))
(insert "Data: :" data "\n"))
)
;; Interpret the command.
(cond ((string= "init" cmd)
nil ; Yep, thanks for letting me know
(message "MATLAB connection initialized.")
)
((string= "ack" cmd)
(message "Ack recieved. Send ACK back.")
(matlab-netshell-send "nowledge" ""))
((string= "nowledge" cmd)
(message "Acknowledgement recieved."))
(t
(message "Unknown command from matlab: %S" string)
(message "Unknown command from matlab: %S" cmd)
)))
(defun matlab-netshell-sentinel (proc msg)
@ -77,23 +124,37 @@ Identify when a connection is lost, and close down services."
(cond ((string-match "^open from " msg)
;; New connection - set it up.
(setq matlab-netshell-clients (cons proc matlab-netshell-clients))
(let ((newbuff (get-buffer-create (process-name proc))))
(set-process-buffer proc newbuff))
(message "MATLAB Has connected!"))
((string= msg "connection broken by remote peer\n")
(setq matlab-netshell-clients (delq proc matlab-netshell-clients))
(message (format "MATLAB has dropped its connecction" proc)))
(message (format "MATLAB has dropped its connecction")))
(t
(message "Unhandle event."))))
(message "Unhandled event."))))
(defun matlab-netshell-send (msg)
"Send MSG to the active MATLAB shell connection."
(defun matlab-netshell-send(cmd data)
"Send a command to MATLAB shell connection with DATA."
(let ((C (car matlab-netshell-clients)))
(if C
(process-send-string C (concat cmd "\n" data "\0"))
(error "No MATLAB network connection to send to."))))
(defun matlab-netshell-eval (msg)
"Send MSG to the active MATLAB shell connection to eval."
(interactive "sMsg: ")
(let ((C (car matlab-netshell-clients)))
(if C
(process-send-string C (concat msg "\n"))
(error "No MATLAB network connection to send to.")
)))
(process-send-string C (concat "eval\n" msg "\0"))
(error "No MATLAB network connection to send to."))))
(defun matlab-netshell-ack ()
"Send an ACK to MATLAB to see if it can respond."
(interactive)
(matlab-netshell-send "ack" ""))
;;(matlab-netshell-server-start)
;;(sleep-for 300)

View file

@ -0,0 +1,157 @@
classdef EmacsServer < handle
% Class EMACSSERVER - Support a TCP connection to an Emacs server.
% Collects input from Emacs, and runs commands.
% Sends commands to Emacs from MATLAB if needed.
%
properties (Access='protected')
tcpclient;
timer;
end
methods
function ES = EmacsServer()
% Construct the Emacs Server. Create the TCP client and
ES.tcpclient = tcpclient('localhost', 32475);
if isempty(ES.tcpclient)
delete(ES);
error('Unable to connect to Emacs.');
end
ES.timer = timer('Name','Emacs NetShell timer', ...
'TimerFcn', {@watch_emacs ES}, ...
'Period', 3,...
'BusyMode', 'drop',...
'ErrorFcn', {@drop_emacs ES},...
'ExecutionMode', 'fixedSpacing');
start(ES.timer);
ES.SendCommand('init');
end
function delete(ES)
stop(ES.timer);
delete(ES.tcpclient);
delete(ES.timer);
end
function SendCommand(ES, cmd, data)
% Commands have 2 parts, the COMMAND and DATA.
% COMMAND is sent to emacs followed by a newline. This should be a single
% word. SendCommand adds the newline.
% DATA can be any string.
% The full command is terminated by a NULL -> uint8(0)
write(ES.tcpclient, uint8([ cmd newline]));
if nargin > 2 && ~isempty(data)
write(ES.tcpclient, uint8(data));
end
write(ES.tcpclient, uint8(0));
disp([ 'Sent: ' cmd uint8(0)])
end
end
properties (Access='protected')
accumulator = '';
end
methods (Access='protected')
function ReadCommand(ES)
msg = char(read(ES.tcpclient));
ES.accumulator = [ ES.accumulator msg ];
while ~isempty(ES.accumulator)
k = strfind(ES.accumulator, char(0));
if isempty(k)
% No complete commands. Exit.
disp(['partial accumulation: ' ES.accumulator]);
return;
end
datamsg = ES.accumulator(1:k(1)-1);
ES.accumulator = ES.accumulator(k(1)+1:end);
% Now peal the datamsg into it's constituant parts.
cr = strfind(datamsg, newline);
if isempty(cr)
cmd = datamsg;
data = '';
else
cmd = datamsg(1:cr(1)-1);
data = datamsg(cr(1)+1:end);
end
ES.ExecuteRemoteCommand(cmd, data);
end
end
function ExecuteRemoteCommand(ES, cmd, data)
% When we recieve a command from Emacs, eval it.
switch cmd
case 'nowledge'
disp('Acknowledgement recieved.');
case 'ack'
disp('Ack Recieved. Sending ack back.');
write(ES.tcpclient, uint8('nowledge'));
case 'eval'
try
OUT = evalc(data);
catch ERR
OUT = ERR.message;
end
write(ES.tcpclient, uint8(OUT));
end
end
end
end
function watch_emacs(~, ~, ES)
% Timer Callback Function:
% Watch for bytes available from the Emacs network connection, and act on any events.
ba = ES.tcpclient.BytesAvailable;
if ba > 0
ES.ReadCommand();
else
% Nothing recieved
%disp('No Luv from Emacs');
% Check if we are still alive. We can only do that with a
% write- so send an empty message.
try
write(ES.tcpclient, 0);
catch
disp('Connection to Emacs lost. Shutting down net server');
delete(ES);
end
end
end
function drop_emacs(~, ~, ES)
% If the timer throws an error, then shutdown.
delete(ES);
disp('Error in timer, dropping connection to Emacs.');
end

View file

@ -1,80 +1,41 @@
function emacsnetshell(sendmsg)
function emacsnetshell(cmd, data)
% Create a connection to an EMACS editor server.
% If we succeed then setup a timer to watch what comes in from the connection.
NETSHELL = getappdata(groot, 'EmacsNetShell');
EMACSSERVER = getappdata(groot, 'EmacsNetShell');
if nargin == 0
sendmsg = 'init';
cmd = 'init';
end
if strcmp(sendmsg, 'shutdown')
if strcmp(cmd, 'shutdown')
% Shutdown our connection to emacs.
if ~isempty(NETSHELL)
stop(NETSHELL.timer);
setappdata(groot, 'EmacsNetShell', []);
delete(NETSHELL.tcpclient);
delete(NETSHELL.timer);
setappdata(groot, 'EmacsNetShell', []);
if ~isempty(EMACSSERVER)
delete(EMACSSERVER);
end
else
if isempty(NETSHELL)
NETSHELL.tcpclient = tcpclient('localhost', 32475);
if isempty(NETSHELL.tcpclient)
error('Unable to connect to Emacs.');
end
NETSHELL.timer = timer('Name','Emacs NetShell timer', ...
'TimerFcn', @watch_emacs, ...
'Period', 3,...
'BusyMode', 'drop',...
'ErrorFcn', @drop_emacs,...
'ExecutionMode', 'fixedSpacing');
setappdata(groot, 'EmacsNetShell', NETSHELL);
start(NETSHELL.timer);
end
if isstring(sendmsg)
sendmsg = char(sendmsg);
if ~isempty(EMACSSERVER) && ~isvalid(EMACSSERVER)
EMACSSERVER = [];
setappdata(groot, 'EmacsNetShell', EMACSSERVER);
end
% We have a client, so lets send our msg.
write(NETSHELL.tcpclient, uint8([ sendmsg '\n']));
if isempty(EMACSSERVER)
EMACSSERVER = emacs.EmacsServer();
setappdata(groot, 'EmacsNetShell', EMACSSERVER);
end
if ~ischar(cmd)
error('Command must be a char vector.');
end
if nargin == 2
EMACSSERVER.SendCommand(cmd, data);
else
EMACSSERVER.SendCommand(cmd);
end
end
end
function watch_emacs(~, ~)
% Timer Callback Function:
% Watch for bytes available from the Emacs network connection, and act on any events.
NETSHELL = getappdata(groot, 'EmacsNetShell');
ba = NETSHELL.tcpclient.BytesAvailable;
if ba > 0
msg = char(read(NETSHELL.tcpclient));
OUT = evalc(msg);
write(NETSHELL.tcpclient, uint8(OUT));
else
% Nothing recieved
% disp('No Luv from Emacs');
end
end
function drop_emacs(~, ~)
% If the timer throws an error, then shutdown.
emacsnetshell('shutdown');
disp('Error in timer, dropping connection to Emacs.');
end