437 lines
14 KiB
Python
437 lines
14 KiB
Python
|
# vim:set et sw=4 ts=8:
|
||
|
|
||
|
import json
|
||
|
import socket
|
||
|
import sys
|
||
|
import os
|
||
|
import threading
|
||
|
import vim
|
||
|
import logging
|
||
|
import msgpack
|
||
|
import neovim_rpc_server_api_info
|
||
|
import neovim_rpc_methods
|
||
|
import threading
|
||
|
import socket
|
||
|
import time
|
||
|
import subprocess
|
||
|
from neovim.api import common as neovim_common
|
||
|
import neovim_rpc_protocol
|
||
|
|
||
|
vim_error = vim.Function('neovim_rpc#_error')
|
||
|
|
||
|
# protable devnull
|
||
|
if sys.version_info.major==2:
|
||
|
DEVNULL = open(os.devnull, 'wb')
|
||
|
else:
|
||
|
from subprocess import DEVNULL
|
||
|
|
||
|
|
||
|
if sys.version_info.major == 2:
|
||
|
from Queue import Queue, Empty as QueueEmpty
|
||
|
else:
|
||
|
from queue import Queue, Empty as QueueEmpty
|
||
|
|
||
|
# NVIM_PYTHON_LOG_FILE=nvim.log NVIM_PYTHON_LOG_LEVEL=INFO vim test.md
|
||
|
|
||
|
try:
|
||
|
# Python 3
|
||
|
import socketserver
|
||
|
except ImportError:
|
||
|
# Python 2
|
||
|
import SocketServer as socketserver
|
||
|
|
||
|
# globals
|
||
|
logger = logging.getLogger(__name__)
|
||
|
# supress the annoying error message:
|
||
|
# No handlers could be found for logger "neovim_rpc_server"
|
||
|
logger.addHandler(logging.NullHandler())
|
||
|
|
||
|
request_queue = Queue()
|
||
|
responses = {}
|
||
|
|
||
|
def _channel_id_new():
|
||
|
with _channel_id_new._lock:
|
||
|
_channel_id_new._counter += 1
|
||
|
return _channel_id_new._counter
|
||
|
# static local
|
||
|
_channel_id_new._counter = 0
|
||
|
_channel_id_new._lock = threading.Lock()
|
||
|
|
||
|
|
||
|
class VimHandler(socketserver.BaseRequestHandler):
|
||
|
|
||
|
_lock = threading.Lock()
|
||
|
_sock = None
|
||
|
|
||
|
@classmethod
|
||
|
def notify(cls,cmd="call neovim_rpc#_callback()"):
|
||
|
try:
|
||
|
if not VimHandler._sock:
|
||
|
return
|
||
|
with VimHandler._lock:
|
||
|
encoded = json.dumps(['ex', cmd])
|
||
|
logger.info("sending notification: %s",encoded)
|
||
|
VimHandler._sock.send(encoded.encode('utf-8'))
|
||
|
except Exception as ex:
|
||
|
logger.exception('VimHandler notify exception for [%s]: %s', cmd, ex)
|
||
|
|
||
|
@classmethod
|
||
|
def notify_exited(cls,channel):
|
||
|
try:
|
||
|
cmd = "call neovim_rpc#_on_exit(%s)" % channel
|
||
|
cls.notify(cmd)
|
||
|
except Exception as ex:
|
||
|
logger.exception('notify_exited for channel [%s] exception: %s',channel,ex)
|
||
|
|
||
|
# each connection is a thread
|
||
|
def handle(self):
|
||
|
logger.info("=== socket opened ===")
|
||
|
data = None
|
||
|
while True:
|
||
|
try:
|
||
|
rcv = self.request.recv(4096)
|
||
|
# 16k buffer by default
|
||
|
if data:
|
||
|
data += rcv
|
||
|
else:
|
||
|
data = rcv
|
||
|
except socket.error:
|
||
|
logger.info("=== socket error ===")
|
||
|
break
|
||
|
except IOError:
|
||
|
logger.info("=== socket closed ===")
|
||
|
break
|
||
|
if len(rcv) == 0:
|
||
|
logger.info("=== socket closed ===")
|
||
|
break
|
||
|
logger.info("received: %s", data)
|
||
|
try:
|
||
|
decoded = json.loads(data.decode('utf-8'))
|
||
|
except ValueError:
|
||
|
logger.exception("json decoding failed, wait for more data")
|
||
|
continue
|
||
|
data = None
|
||
|
|
||
|
# Send a response if the sequence number is positive.
|
||
|
# Negative numbers are used for "eval" responses.
|
||
|
if len(decoded)>=2 and decoded[0] >= 0 and decoded[1] == 'neovim_rpc_setup':
|
||
|
|
||
|
VimHandler._sock = self.request
|
||
|
|
||
|
# initial setup
|
||
|
encoded = json.dumps(['ex', "scall neovim_rpc#_callback()"])
|
||
|
logger.info("sending {0}".format(encoded))
|
||
|
self.request.send(encoded.encode('utf-8'))
|
||
|
|
||
|
else:
|
||
|
# recognize as rpcrequest
|
||
|
reqid = decoded[0]
|
||
|
channel = decoded[1][1]
|
||
|
event = decoded[1][2]
|
||
|
args = decoded[1][3]
|
||
|
rspid = decoded[1][4]
|
||
|
NvimHandler.request(self.request,
|
||
|
channel,
|
||
|
reqid,
|
||
|
event,
|
||
|
args,
|
||
|
rspid)
|
||
|
|
||
|
# wait for response
|
||
|
|
||
|
class SocketToStream():
|
||
|
|
||
|
def __init__(self,sock):
|
||
|
self._sock = sock
|
||
|
|
||
|
def read(self,cnt):
|
||
|
if cnt>4096:
|
||
|
cnt = 4096
|
||
|
return self._sock.recv(cnt)
|
||
|
|
||
|
def write(self,w):
|
||
|
return self._sock.send(w)
|
||
|
|
||
|
class NvimHandler(socketserver.BaseRequestHandler):
|
||
|
|
||
|
channel_sockets = {}
|
||
|
|
||
|
def handle(self):
|
||
|
|
||
|
logger.info("=== socket opened for client ===")
|
||
|
|
||
|
channel = _channel_id_new()
|
||
|
|
||
|
sock = self.request
|
||
|
chinfo = dict(sock=sock)
|
||
|
NvimHandler.channel_sockets[channel] = chinfo
|
||
|
|
||
|
try:
|
||
|
f = SocketToStream(sock)
|
||
|
unpacker = msgpack.Unpacker(f)
|
||
|
for unpacked in unpacker:
|
||
|
logger.info("unpacked: %s", unpacked)
|
||
|
|
||
|
# response format:
|
||
|
# - msg[0]: 1
|
||
|
# - msg[1]: the request id
|
||
|
# - msg[2]: error(if any), format: [code,str]
|
||
|
# - msg[3]: result(if not errored)
|
||
|
if int(unpacked[0]) == 1:
|
||
|
unpacked = neovim_rpc_protocol.from_client(unpacked)
|
||
|
reqid = int(unpacked[1])
|
||
|
rspid, vimsock = chinfo[reqid]
|
||
|
err = unpacked[2]
|
||
|
result = unpacked[3]
|
||
|
# VIM fails to parse response when there a sleep in neovim
|
||
|
# client. I cannot figure out why. Use global responses to
|
||
|
# workaround this issue.
|
||
|
responses[rspid] = [err, result]
|
||
|
content = [reqid, '']
|
||
|
tosend = json.dumps(content)
|
||
|
# vimsock.send
|
||
|
vimsock.send(tosend.encode('utf-8'))
|
||
|
chinfo.pop(reqid)
|
||
|
continue
|
||
|
|
||
|
|
||
|
request_queue.put((f,channel,unpacked))
|
||
|
# notify vim in order to process request in main thread, and
|
||
|
# avoiding the stupid json protocol
|
||
|
VimHandler.notify()
|
||
|
|
||
|
logger.info('channel %s closed.', channel)
|
||
|
|
||
|
except:
|
||
|
logger.exception('unpacker failed.')
|
||
|
finally:
|
||
|
try:
|
||
|
NvimHandler.channel_sockets.pop(channel)
|
||
|
sock.close()
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
@classmethod
|
||
|
def notify(cls,channel,event,args):
|
||
|
try:
|
||
|
channel = int(channel)
|
||
|
if channel not in cls.channel_sockets:
|
||
|
logger.info("channel[%s] not in NvimHandler", channel)
|
||
|
return
|
||
|
sock = cls.channel_sockets[channel]['sock']
|
||
|
|
||
|
# notification format:
|
||
|
# - msg[0] type, which is 2
|
||
|
# - msg[1] method
|
||
|
# - msg[2] arguments
|
||
|
content = [2, event, args]
|
||
|
|
||
|
logger.info("notify channel[%s]: %s", channel, content)
|
||
|
packed = msgpack.packb(neovim_rpc_protocol.to_client(content))
|
||
|
sock.send(packed)
|
||
|
except Exception as ex:
|
||
|
logger.exception("notify failed: %s", ex)
|
||
|
|
||
|
@classmethod
|
||
|
def request(cls, vimsock, channel, reqid, event, args, rspid):
|
||
|
try:
|
||
|
reqid = int(reqid)
|
||
|
channel = int(channel)
|
||
|
chinfo = cls.channel_sockets[channel]
|
||
|
|
||
|
if channel not in cls.channel_sockets:
|
||
|
logger.info("channel[%s] not in NvimHandler", channel)
|
||
|
return
|
||
|
|
||
|
sock = chinfo['sock']
|
||
|
# request format:
|
||
|
# - msg[0] type, which is 0
|
||
|
# - msg[1] request id
|
||
|
# - msg[2] method
|
||
|
# - msg[3] arguments
|
||
|
content = [0, reqid, event, args]
|
||
|
|
||
|
chinfo[reqid] = [rspid, vimsock]
|
||
|
|
||
|
logger.info("request channel[%s]: %s", channel, content)
|
||
|
packed = msgpack.packb(neovim_rpc_protocol.to_client(content))
|
||
|
sock.send(packed)
|
||
|
except Exception as ex:
|
||
|
logger.exception("request failed: %s", ex)
|
||
|
|
||
|
@classmethod
|
||
|
def shutdown(cls):
|
||
|
# close all sockets
|
||
|
for channel in list(cls.channel_sockets.keys()):
|
||
|
chinfo = cls.channel_sockets.get(channel,None)
|
||
|
if chinfo:
|
||
|
sock = chinfo['sock']
|
||
|
logger.info("closing client %s", channel)
|
||
|
# if don't shutdown the socket, vim will never exit because the
|
||
|
# recv thread is still blocking
|
||
|
sock.shutdown(socket.SHUT_RDWR)
|
||
|
sock.close()
|
||
|
|
||
|
|
||
|
# copied from neovim python-client/neovim/__init__.py
|
||
|
def _setup_logging(name):
|
||
|
"""Setup logging according to environment variables."""
|
||
|
logger = logging.getLogger(__name__)
|
||
|
if 'NVIM_PYTHON_LOG_FILE' in os.environ:
|
||
|
prefix = os.environ['NVIM_PYTHON_LOG_FILE'].strip()
|
||
|
major_version = sys.version_info[0]
|
||
|
logfile = '{}_py{}_{}'.format(prefix, major_version, name)
|
||
|
handler = logging.FileHandler(logfile, 'w', encoding='utf-8')
|
||
|
handler.formatter = logging.Formatter(
|
||
|
'%(asctime)s [%(levelname)s @ '
|
||
|
'%(filename)s:%(funcName)s:%(lineno)s] %(process)s - %(message)s')
|
||
|
logging.root.addHandler(handler)
|
||
|
level = logging.INFO
|
||
|
if 'NVIM_PYTHON_LOG_LEVEL' in os.environ:
|
||
|
l = getattr(logging,
|
||
|
os.environ['NVIM_PYTHON_LOG_LEVEL'].strip(),
|
||
|
level)
|
||
|
if isinstance(l, int):
|
||
|
level = l
|
||
|
logger.setLevel(level)
|
||
|
|
||
|
def start():
|
||
|
|
||
|
_setup_logging('neovim_rpc_server')
|
||
|
|
||
|
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||
|
pass
|
||
|
|
||
|
# 0 for random port
|
||
|
global _vim_server
|
||
|
global _nvim_server
|
||
|
_vim_server = ThreadedTCPServer(("127.0.0.1", 0), VimHandler)
|
||
|
_nvim_server = ThreadedTCPServer(("127.0.0.1", 0), NvimHandler)
|
||
|
_vim_server.daemon_threads = True
|
||
|
_nvim_server.daemon_threads = True
|
||
|
|
||
|
# Start a thread with the server -- that thread will then start one
|
||
|
# more thread for each request
|
||
|
main_server_thread = threading.Thread(target=_vim_server.serve_forever)
|
||
|
clients_server_thread = threading.Thread(target=_nvim_server.serve_forever)
|
||
|
|
||
|
# Exit the server thread when the main thread terminates
|
||
|
main_server_thread.daemon = True
|
||
|
main_server_thread.start()
|
||
|
clients_server_thread.daemon = True
|
||
|
clients_server_thread.start()
|
||
|
|
||
|
return ["{addr[0]}:{addr[1]}".format(addr=_nvim_server.server_address), "{addr[0]}:{addr[1]}".format(addr=_vim_server.server_address)]
|
||
|
|
||
|
def process_pending_requests():
|
||
|
|
||
|
logger.info("process_pending_requests")
|
||
|
while True:
|
||
|
|
||
|
item = None
|
||
|
try:
|
||
|
|
||
|
item = request_queue.get(False)
|
||
|
|
||
|
f, channel, msg = item
|
||
|
|
||
|
msg = neovim_rpc_protocol.from_client(msg)
|
||
|
|
||
|
logger.info("get msg from channel [%s]: %s", channel, msg)
|
||
|
|
||
|
# request format:
|
||
|
# - msg[0] type, which is 0
|
||
|
# - msg[1] request id
|
||
|
# - msg[2] method
|
||
|
# - msg[3] arguments
|
||
|
|
||
|
# notification format:
|
||
|
# - msg[0] type, which is 2
|
||
|
# - msg[1] method
|
||
|
# - msg[2] arguments
|
||
|
|
||
|
if msg[0] == 0:
|
||
|
#request
|
||
|
|
||
|
req_typed, req_id, method, args = msg
|
||
|
|
||
|
try:
|
||
|
err=None
|
||
|
result = _process_request(channel,method,args)
|
||
|
except Exception as ex:
|
||
|
logger.exception("process failed: %s", ex)
|
||
|
# error uccor
|
||
|
err = [1,str(ex)]
|
||
|
result = None
|
||
|
|
||
|
result = [1,req_id,err,result]
|
||
|
logger.info("sending result: %s", result)
|
||
|
packed = msgpack.packb(neovim_rpc_protocol.to_client(result))
|
||
|
f.write(packed)
|
||
|
logger.info("sent")
|
||
|
if msg[0] == 2:
|
||
|
# notification
|
||
|
req_typed, method, args = msg
|
||
|
try:
|
||
|
result = _process_request(channel,method,args)
|
||
|
logger.info('notification process result: [%s]', result)
|
||
|
except Exception as ex:
|
||
|
logger.exception("process failed: %s", ex)
|
||
|
|
||
|
except QueueEmpty as em:
|
||
|
pass
|
||
|
except Exception as ex:
|
||
|
logger.exception("exception during process: %s", ex)
|
||
|
finally:
|
||
|
if item:
|
||
|
request_queue.task_done()
|
||
|
else:
|
||
|
# item==None means the queue is empty
|
||
|
break
|
||
|
|
||
|
def _process_request(channel,method,args):
|
||
|
if method=='vim_get_api_info':
|
||
|
# this is the first request send from neovim client
|
||
|
api_info = neovim_rpc_server_api_info.API_INFO
|
||
|
return [channel,api_info]
|
||
|
if hasattr(neovim_rpc_methods,method):
|
||
|
return getattr(neovim_rpc_methods,method)(*args)
|
||
|
else:
|
||
|
logger.error("method %s not implemented", method)
|
||
|
vim_error("rpc method [%s] not implemented in pythonx/neovim_rpc_methods.py. Please send PR or contact the mantainer." % method)
|
||
|
raise Exception('%s not implemented' % method)
|
||
|
|
||
|
def rpcnotify(channel,method,args):
|
||
|
NvimHandler.notify(channel,method,args)
|
||
|
|
||
|
|
||
|
def stop():
|
||
|
|
||
|
logger.info("stop begin")
|
||
|
|
||
|
# close tcp channel server
|
||
|
_nvim_server.shutdown()
|
||
|
_nvim_server.server_close()
|
||
|
|
||
|
# close the main channel
|
||
|
try:
|
||
|
vim.command('call ch_close(g:_neovim_rpc_main_channel)')
|
||
|
except Exception as ex:
|
||
|
logger.info("ch_close failed: %s", ex)
|
||
|
|
||
|
# remove all sockets
|
||
|
NvimHandler.shutdown()
|
||
|
|
||
|
try:
|
||
|
# stop the main channel
|
||
|
_vim_server.shutdown()
|
||
|
except Exception as ex:
|
||
|
logger.info("_vim_server shutodwn failed: %s", ex)
|
||
|
|
||
|
try:
|
||
|
_vim_server.server_close()
|
||
|
except Exception as ex:
|
||
|
logger.info("_vim_server close failed: %s", ex)
|
||
|
|
||
|
|