diff --git a/pyls/_utils.py b/pyls/_utils.py index 25cf889d..d4f12924 100644 --- a/pyls/_utils.py +++ b/pyls/_utils.py @@ -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 diff --git a/pyls/config/config.py b/pyls/config/config.py index 9d0a2ccc..9e06f6ec 100644 --- a/pyls/config/config.py +++ b/pyls/config/config.py @@ -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 = {} @@ -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: diff --git a/pyls/python_ls.py b/pyls/python_ls.py index eda0e02f..ff0d908a 100644 --- a/pyls/python_ls.py +++ b/pyls/python_ls.py @@ -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 @@ -14,6 +15,7 @@ LINT_DEBOUNCE_S = 0.5 # 500 ms +PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s class _StreamHandlerWrapper(socketserver.StreamRequestHandler, object): @@ -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()} diff --git a/test/fixtures.py b/test/fixtures.py index d4b53d3c..4afc9295 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -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 diff --git a/test/plugins/test_pycodestyle_lint.py b/test/plugins/test_pycodestyle_lint.py index 583da797..698704fc 100644 --- a/test/plugins/test_pycodestyle_lint.py +++ b/test/plugins/test_pycodestyle_lint.py @@ -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) diff --git a/test/test_language_server.py b/test/test_language_server.py index 7597feb6..65f5b357 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -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)