Skip to content

Commit e6421d9

Browse files
andfoyccordoba12
authored andcommitted
Shutdown TCP ServerSocket when parent process ends (#655)
1 parent 882117f commit e6421d9

File tree

3 files changed

+44
-20
lines changed

3 files changed

+44
-20
lines changed

appveyor.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
environment:
2+
global:
3+
APPVEYOR_RDP_PASSWORD: "dcca4c4863E30d56c2e0dda6327370b3#"
24
matrix:
35
- PYTHON: "C:\\Python27"
46
PYTHON_VERSION: "2.7.15"
@@ -21,6 +23,9 @@ install:
2123
test_script:
2224
- "%PYTHON%/Scripts/pytest.exe test/"
2325

26+
# on_finish:
27+
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
28+
2429
build: false # Not a C# project
2530

2631
cache:

pyls/python_ls.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,28 @@ def setup(self):
3434

3535
def handle(self):
3636
self.delegate.start()
37+
# pylint: disable=no-member
38+
self.SHUTDOWN_CALL()
3739

3840

3941
def start_tcp_lang_server(bind_addr, port, check_parent_process, handler_class):
4042
if not issubclass(handler_class, PythonLanguageServer):
4143
raise ValueError('Handler class must be an instance of PythonLanguageServer')
4244

45+
def shutdown_server(*args):
46+
# pylint: disable=unused-argument
47+
log.debug('Shutting down server')
48+
# Shutdown call must be done on a thread, to prevent deadlocks
49+
stop_thread = threading.Thread(target=server.shutdown)
50+
stop_thread.start()
51+
4352
# Construct a custom wrapper class around the user's handler_class
4453
wrapper_class = type(
4554
handler_class.__name__ + 'Handler',
4655
(_StreamHandlerWrapper,),
4756
{'DELEGATE_CLASS': partial(handler_class,
48-
check_parent_process=check_parent_process)}
57+
check_parent_process=check_parent_process),
58+
'SHUTDOWN_CALL': shutdown_server}
4959
)
5060

5161
server = socketserver.TCPServer((bind_addr, port), wrapper_class)
@@ -78,6 +88,7 @@ def __init__(self, rx, tx, check_parent_process=False):
7888
self.workspace = None
7989
self.config = None
8090
self.root_uri = None
91+
self.watching_thread = None
8192
self.workspaces = {}
8293
self.uri_workspace_mapper = {}
8394

@@ -187,19 +198,18 @@ def m_initialize(self, processId=None, rootUri=None, rootPath=None, initializati
187198
self._dispatchers = self._hook('pyls_dispatchers')
188199
self._hook('pyls_initialize')
189200

190-
if self._check_parent_process and processId is not None:
201+
if self._check_parent_process and processId is not None and self.watching_thread is None:
191202
def watch_parent_process(pid):
192203
# exit when the given pid is not alive
193204
if not _utils.is_process_alive(pid):
194205
log.info("parent process %s is not alive", pid)
195206
self.m_exit()
196-
log.debug("parent process %s is still alive", pid)
197-
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start()
198-
199-
watching_thread = threading.Thread(target=watch_parent_process, args=(processId,))
200-
watching_thread.daemon = True
201-
watching_thread.start()
207+
else:
208+
threading.Timer(PARENT_PROCESS_WATCH_INTERVAL, watch_parent_process, args=[pid]).start()
202209

210+
self.watching_thread = threading.Thread(target=watch_parent_process, args=(processId,))
211+
self.watching_thread.daemon = True
212+
self.watching_thread.start()
203213
# Get our capabilities
204214
return {'capabilities': self.capabilities()}
205215

test/test_language_server.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import os
3+
import time
4+
import multiprocessing
35
from threading import Thread
46

57
from test import unix_only
@@ -8,7 +10,7 @@
810

911
from pyls.python_ls import start_io_lang_server, PythonLanguageServer
1012

11-
CALL_TIMEOUT = 2
13+
CALL_TIMEOUT = 10
1214

1315

1416
def start_client(client):
@@ -23,11 +25,12 @@ def __init__(self, check_parent_process=False):
2325
# Server to client pipe
2426
scr, scw = os.pipe()
2527

26-
self.server_thread = Thread(target=start_io_lang_server, args=(
28+
ParallelKind = multiprocessing.Process if os.name != 'nt' else Thread
29+
30+
self.process = ParallelKind(target=start_io_lang_server, args=(
2731
os.fdopen(csr, 'rb'), os.fdopen(scw, 'wb'), check_parent_process, PythonLanguageServer
2832
))
29-
self.server_thread.daemon = True
30-
self.server_thread.start()
33+
self.process.start()
3134

3235
self.client = PythonLanguageServer(os.fdopen(scr, 'rb'), os.fdopen(csw, 'wb'), start_io_lang_server)
3336
self.client_thread = Thread(target=start_client, args=[self.client])
@@ -56,9 +59,10 @@ def client_exited_server():
5659
"""
5760
client_server_pair = _ClientServer(True)
5861

59-
yield client_server_pair.client
62+
# yield client_server_pair.client
63+
yield client_server_pair
6064

61-
assert client_server_pair.server_thread.is_alive() is False
65+
assert client_server_pair.process.is_alive() is False
6266

6367

6468
def test_initialize(client_server): # pylint: disable=redefined-outer-name
@@ -72,12 +76,17 @@ def test_initialize(client_server): # pylint: disable=redefined-outer-name
7276
@unix_only
7377
def test_exit_with_parent_process_died(client_exited_server): # pylint: disable=redefined-outer-name
7478
# language server should have already exited before responding
75-
with pytest.raises(Exception):
76-
client_exited_server._endpoint.request('initialize', {
77-
'processId': 1234,
78-
'rootPath': os.path.dirname(__file__),
79-
'initializationOptions': {}
80-
}).result(timeout=CALL_TIMEOUT)
79+
lsp_server, mock_process = client_exited_server.client, client_exited_server.process
80+
# with pytest.raises(Exception):
81+
lsp_server._endpoint.request('initialize', {
82+
'processId': mock_process.pid,
83+
'rootPath': os.path.dirname(__file__),
84+
'initializationOptions': {}
85+
}).result(timeout=CALL_TIMEOUT)
86+
87+
mock_process.terminate()
88+
time.sleep(CALL_TIMEOUT)
89+
assert not client_exited_server.client_thread.is_alive()
8190

8291

8392
def test_not_exit_without_check_parent_process_flag(client_server): # pylint: disable=redefined-outer-name

0 commit comments

Comments
 (0)