Skip to content

Commit

Permalink
Make python language server exit when parent process dies (#410)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiahuiJiang authored and gatesn committed Aug 28, 2018
1 parent 28df4c0 commit 4fc34c6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 23 deletions.
18 changes: 18 additions & 0 deletions pyls/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,21 @@ def clip_column(column, lines, line_number):
# https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#position
max_column = len(lines[line_number].rstrip('\r\n')) if len(lines) > line_number else 0
return min(column, max_column)


def is_process_alive(pid):
""" Check whether the process with the given pid is still alive.
Args:
pid (int): process ID
Returns:
bool: False if the process is not alive or don't have permission to check, True otherwise.
"""
try:
os.kill(pid, 0)
except OSError:
# no such process or process is already dead
return False
else:
return True
7 changes: 6 additions & 1 deletion pyls/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

class Config(object):

def __init__(self, root_uri, init_opts):
def __init__(self, root_uri, init_opts, process_id):
self._root_path = uris.to_fs_path(root_uri)
self._root_uri = root_uri
self._init_opts = init_opts
self._process_id = process_id

self._settings = {}
self._plugin_settings = {}
Expand Down Expand Up @@ -77,6 +78,10 @@ def init_opts(self):
def root_uri(self):
return self._root_uri

@property
def process_id(self):
return self._process_id

def settings(self, document_path=None):
"""Settings are constructed from a few sources:
Expand Down
17 changes: 16 additions & 1 deletion pyls/python_ls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2017 Palantir Technologies, Inc.
import logging
import socketserver
import threading

from jsonrpc.dispatchers import MethodDispatcher
from jsonrpc.endpoint import Endpoint
Expand All @@ -14,6 +15,7 @@


LINT_DEBOUNCE_S = 0.5 # 500 ms
PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s


class _StreamHandlerWrapper(socketserver.StreamRequestHandler, object):
Expand Down Expand Up @@ -149,10 +151,23 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
rootUri = uris.from_fs_path(rootPath) if rootPath is not None else ''

self.workspace = Workspace(rootUri, self._endpoint)
self.config = config.Config(rootUri, initializationOptions or {})
self.config = config.Config(rootUri, initializationOptions or {}, processId)
self._dispatchers = self._hook('pyls_dispatchers')
self._hook('pyls_initialize')

if processId is not None:
def watch_parent_process(pid):
# exist when the given pid is not alive
if not _utils.is_process_alive(pid):
log.info("parent process %s is not alive", pid)
self.m_exit()
log.debug("parent process %s is still alive", pid)
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process(pid)).start()

watching_thread = threading.Thread(target=watch_parent_process, args=[processId])
watching_thread.daemon = True
watching_thread.start()

# Get our capabilities
return {'capabilities': self.capabilities()}

Expand Down
2 changes: 1 addition & 1 deletion test/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def workspace(tmpdir):
@pytest.fixture
def config(workspace): # pylint: disable=redefined-outer-name
"""Return a config object."""
return Config(workspace.root_uri, {})
return Config(workspace.root_uri, {}, 0)


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion test/plugins/test_pycodestyle_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_pycodestyle_config(workspace):
doc_uri = uris.from_fs_path(os.path.join(workspace.root_path, 'test.py'))
workspace.put_document(doc_uri, DOC)
doc = workspace.get_document(doc_uri)
config = Config(workspace.root_uri, {})
config = Config(workspace.root_uri, {}, 1234)

# Make sure we get a warning for 'indentation contains tabs'
diags = pycodestyle_lint.pyls_lint(config, doc)
Expand Down
62 changes: 43 additions & 19 deletions test/test_language_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,66 @@ def start_client(client):
client.start()


class _ClientServer(object):
""" A class to setup a client/server pair """
def __init__(self):
# Client to Server pipe
csr, csw = os.pipe()
# Server to client pipe
scr, scw = os.pipe()

self.server_thread = Thread(target=start_io_lang_server, args=(
os.fdopen(csr, 'rb'), os.fdopen(scw, 'wb'), PythonLanguageServer
))
self.server_thread.daemon = True
self.server_thread.start()

self.client = PythonLanguageServer(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb'))
self.client_thread = Thread(target=start_client, args=[self.client])
self.client_thread.daemon = True
self.client_thread.start()


@pytest.fixture
def client_server():
""" A fixture to setup a client/server """
""" A fixture that sets up a client/server pair and shuts down the server """
client_server_pair = _ClientServer()

# Client to Server pipe
csr, csw = os.pipe()
# Server to client pipe
scr, scw = os.pipe()
yield client_server_pair.client

shutdown_response = client_server_pair.client._endpoint.request('shutdown').result(timeout=CALL_TIMEOUT)
assert shutdown_response is None
client_server_pair.client._endpoint.notify('exit')

server_thread = Thread(target=start_io_lang_server, args=(
os.fdopen(csr, 'rb'), os.fdopen(scw, 'wb'), PythonLanguageServer
))
server_thread.daemon = True
server_thread.start()

client = PythonLanguageServer(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb'))
client_thread = Thread(target=start_client, args=[client])
client_thread.daemon = True
client_thread.start()
@pytest.fixture
def client_exited_server():
""" A fixture that sets up a client/server pair and assert the server has already exited """
client_server_pair = _ClientServer()

yield client
yield client_server_pair.client

shutdown_response = client._endpoint.request('shutdown').result(timeout=CALL_TIMEOUT)
assert shutdown_response is None
client._endpoint.notify('exit')
assert client_server_pair.server_thread.is_alive() is False


def test_initialize(client_server): # pylint: disable=redefined-outer-name
response = client_server._endpoint.request('initialize', {
'processId': 1234,
'rootPath': os.path.dirname(__file__),
'initializationOptions': {}
}).result(timeout=CALL_TIMEOUT)
assert 'capabilities' in response


def test_exit_with_parent_process_died(client_exited_server): # pylint: disable=redefined-outer-name
# language server should have already exited before responding
with pytest.raises(Exception):
client_exited_server._endpoint.request('initialize', {
'processId': 1234,
'rootPath': os.path.dirname(__file__),
'initializationOptions': {}
}).result(timeout=CALL_TIMEOUT)


def test_missing_message(client_server): # pylint: disable=redefined-outer-name
with pytest.raises(JsonRpcMethodNotFound):
client_server._endpoint.request('unknown_method').result(timeout=CALL_TIMEOUT)

0 comments on commit 4fc34c6

Please sign in to comment.