Skip to content

Commit 39c575d

Browse files
committed
reconnect correctly to debugger after disconnect
1 parent 3ab223e commit 39c575d

File tree

4 files changed

+79
-34
lines changed

4 files changed

+79
-34
lines changed

robotcode/debugger/__main__.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import argparse
22
import asyncio
3+
import functools
34
import logging
45
import logging.config
56
import os
67
import sys
78
import threading
89
from logging.handlers import RotatingFileHandler
910
from pathlib import Path
10-
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Union, cast
11+
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Sequence, Union, cast
1112

1213
__file__ = os.path.abspath(__file__)
1314
if __file__.endswith((".pyc", ".pyo")):
@@ -65,27 +66,33 @@ async def wait() -> None:
6566

6667

6768
@_logger.call
68-
async def _debug_adapter_server_(host: str, port: int) -> None:
69+
async def _debug_adapter_server_(
70+
host: str, port: int, on_config_done_callback: Optional[Callable[["DebugAdapterServer"], None]]
71+
) -> None:
6972
from ..jsonrpc2.server import TcpParams
7073
from .server import DebugAdapterServer
7174

72-
async with DebugAdapterServer(tcp_params=TcpParams(host, port)) as server:
73-
set_server(cast(DebugAdapterServer, server))
75+
async with DebugAdapterServer(tcp_params=TcpParams(host, port)) as s:
76+
server = cast(DebugAdapterServer, s)
77+
if on_config_done_callback is not None:
78+
server.protocol.received_configuration_done_callback = functools.partial(on_config_done_callback, server)
79+
set_server(server)
7480
await server.serve()
7581

7682

7783
DEFAULT_TIMEOUT = 10.0
7884

7985

86+
config_done_callback: Optional[Callable[["DebugAdapterServer"], None]] = None
87+
88+
8089
@_logger.call
8190
async def start_debugpy_async(
82-
server: "DebugAdapterServer",
8391
debugpy_port: int = 5678,
8492
addresses: Union[Sequence[str], str, None] = None,
8593
wait_for_debugpy_client: bool = False,
8694
wait_for_client_timeout: float = DEFAULT_TIMEOUT,
8795
) -> None:
88-
from ..utils.async_tools import run_coroutine_from_thread_async
8996
from ..utils.debugpy import enable_debugpy, wait_for_debugpy_connected
9097
from ..utils.net import find_free_port
9198
from .dap_types import Event
@@ -94,19 +101,17 @@ async def start_debugpy_async(
94101
if port != debugpy_port:
95102
_logger.warning(f"start debugpy session on port {port}")
96103

97-
if enable_debugpy(port, addresses) and await run_coroutine_from_thread_async(
98-
server.protocol.wait_for_client, wait_for_client_timeout, loop=server.loop
99-
):
100-
await asyncio.wrap_future(
101-
asyncio.run_coroutine_threadsafe(
102-
server.protocol.send_event_async(
103-
Event(event="debugpyStarted", body={"port": port, "addresses": addresses})
104-
),
105-
loop=server.loop,
106-
)
107-
)
108-
if wait_for_debugpy_client:
109-
wait_for_debugpy_connected()
104+
if enable_debugpy(port, addresses):
105+
global config_done_callback
106+
107+
def connect_debugpy(server: "DebugAdapterServer") -> None:
108+
109+
server.protocol.send_event(Event(event="debugpyStarted", body={"port": port, "addresses": addresses}))
110+
111+
if wait_for_debugpy_client:
112+
wait_for_debugpy_connected()
113+
114+
config_done_callback = connect_debugpy
110115

111116

112117
@_logger.call
@@ -132,14 +137,17 @@ async def run_robot(
132137
run_coroutine_from_thread_async,
133138
run_coroutine_in_thread,
134139
)
135-
from ..utils.debugpy import is_debugpy_installed
140+
from ..utils.debugpy import is_debugpy_installed, wait_for_debugpy_connected
136141
from .dap_types import Event
137142
from .debugger import Debugger
138143

139144
if debugpy and not is_debugpy_installed():
140145
print("debugpy not installed.")
141146

142-
server_future = run_coroutine_in_thread(_debug_adapter_server_, addresses, port)
147+
if debugpy:
148+
await start_debugpy_async(debugpy_port, addresses, wait_for_debugpy_client, wait_for_client_timeout)
149+
150+
server_future = run_coroutine_in_thread(_debug_adapter_server_, addresses, port, config_done_callback)
143151

144152
server = await wait_for_server()
145153

@@ -166,8 +174,8 @@ async def run_robot(
166174
except asyncio.TimeoutError as e:
167175
raise ConnectionError("Timeout to get configuration from client.") from e
168176

169-
if debugpy:
170-
await start_debugpy_async(server, debugpy_port, addresses, wait_for_debugpy_client, wait_for_client_timeout)
177+
if debugpy and wait_for_debugpy_client:
178+
wait_for_debugpy_connected()
171179

172180
args = [
173181
"--listener",
@@ -210,6 +218,8 @@ async def run_robot(
210218
return exit_code
211219
except asyncio.CancelledError:
212220
pass
221+
except ConnectionError as e:
222+
print(e, file=sys.stderr)
213223
finally:
214224
if server.protocol.connected:
215225
await run_coroutine_from_thread_async(server.protocol.terminate, loop=server.loop)

robotcode/debugger/debugger.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class State(Enum):
8686
Stopped = 0
8787
Running = 1
8888
Paused = 2
89+
CallKeyword = 3
8990

9091

9192
class RequestedState(Enum):
@@ -222,6 +223,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
222223
raise RuntimeError(f"Attempt to create a '{cls.__qualname__}' instance outside of instance()")
223224

224225
def __init__(self) -> None:
226+
from robot.running.model import Keyword
227+
225228
self.breakpoints: Dict[pathlib.PurePath, BreakpointsEntry] = {}
226229

227230
self.exception_breakpoints: Set[ExceptionBreakpointsEntry] = set()
@@ -247,9 +250,15 @@ def __init__(self) -> None:
247250
self.stop_on_entry = False
248251
self.no_debug = False
249252
self.terminated = False
253+
self.terminated_requested = False
250254
self.attached = False
251255
self.path_mappings: List[PathMapping] = []
252256

257+
self.run_keyword: Optional[Keyword] = None
258+
self.run_keyword_event = threading.Event()
259+
self.run_keyword_event.set()
260+
self.after_run_keyword_event_state: Optional[State] = None
261+
253262
@property
254263
def debug(self) -> bool:
255264
return not self.no_debug
@@ -278,6 +287,10 @@ def robot_output_file(self) -> Optional[str]:
278287
def robot_output_file(self, value: Optional[str]) -> None:
279288
self._robot_output_file = value
280289

290+
@_logger.call
291+
def terminate_requested(self) -> None:
292+
self.terminated_requested = True
293+
281294
@_logger.call
282295
def terminate(self) -> None:
283296
self.terminated = True
@@ -304,16 +317,23 @@ def stop(self) -> None:
304317
self.condition.notify_all()
305318

306319
@_logger.call
307-
def continue_all(self) -> None:
320+
def continue_all(self, send_event: bool = True) -> None:
308321
if self.main_thread is not None and self.main_thread.ident is not None:
309-
self.continue_thread(self.main_thread.ident)
322+
self.continue_thread(self.main_thread.ident, send_event)
310323

311324
@_logger.call
312-
def continue_thread(self, thread_id: int) -> None:
325+
def continue_thread(self, thread_id: int, send_event: bool = False) -> None:
313326
if self.main_thread is None or thread_id != self.main_thread.ident:
314327
raise InvalidThreadId(thread_id)
315328

316329
with self.condition:
330+
if send_event:
331+
self.send_event(
332+
self,
333+
ContinuedEvent(
334+
body=ContinuedEventBody(thread_id=self.main_thread.ident, all_threads_continued=True)
335+
),
336+
)
317337
self.state = State.Running
318338
self.condition.notify_all()
319339

@@ -585,7 +605,12 @@ def process_end_state(self, status: str, filter_id: Set[str], description: str,
585605
def wait_for_running(self) -> None:
586606
if self.attached:
587607
with self.condition:
588-
self.condition.wait_for(lambda: self.state in [State.Running, State.Stopped])
608+
while True:
609+
self.condition.wait_for(lambda: self.state in [State.Running, State.Stopped, State.CallKeyword])
610+
611+
if self.state == State.CallKeyword:
612+
continue
613+
break
589614

590615
def start_output_group(self, name: str, attributes: Dict[str, Any], type: Optional[str] = None) -> None:
591616
if self.group_output:
@@ -1201,8 +1226,13 @@ def evaluate(
12011226

12021227
if splitted:
12031228
kw = Keyword(name=splitted[0], args=tuple(splitted[1:]), assign=tuple(variables))
1229+
old_state = self.state
1230+
self.state = State.CallKeyword
1231+
12041232
result = kw.run(evaluate_context)
12051233

1234+
self.state = old_state
1235+
12061236
elif self.IS_VARIABLE_RE.match(expression.strip()):
12071237
try:
12081238
result = VariableFinder(vars.store).find(expression)

robotcode/debugger/launcher/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ async def _launch(
262262
)
263263

264264
else:
265-
raise Exception(f'Unknown console type "{console}".')
265+
raise ValueError(f'Unknown console type "{console}".')
266266

267267
self.client = DAPClient(self, TcpParams(None, port))
268268
try:

robotcode/debugger/server.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import os
3-
from typing import Any, Dict, List, Literal, Optional, Union
3+
from typing import Any, Callable, Dict, List, Literal, Optional, Union
44

55
from ..jsonrpc2.protocol import rpc_method
66
from ..jsonrpc2.server import JsonRPCServer, JsonRpcServerMode, TcpParams
@@ -72,6 +72,7 @@ def __init__(self) -> None:
7272

7373
self._received_configuration_done_event = async_tools.Event()
7474
self._received_configuration_done = False
75+
self.received_configuration_done_callback: Optional[Callable[[], None]] = None
7576

7677
Debugger.instance().send_event.add(self.on_debugger_send_event)
7778

@@ -218,14 +219,15 @@ async def terminate(self) -> None:
218219
async def _terminate(self, arguments: Optional[TerminateArguments] = None, *args: Any, **kwargs: Any) -> None:
219220
import signal
220221

221-
Debugger.instance().terminate()
222-
223222
if not self._sigint_signaled:
224223
self._logger.info("Send SIGINT to process")
225224
signal.raise_signal(signal.SIGINT)
226225
self._sigint_signaled = True
226+
# Debugger.instance().continue_all()
227227
else:
228-
self.send_event(Event("terminateRequested"))
228+
await self.send_event_async(Event("terminateRequested"))
229+
230+
Debugger.instance().terminate()
229231

230232
self._logger.info("Send SIGTERM to process")
231233
signal.raise_signal(signal.SIGTERM)
@@ -241,9 +243,8 @@ async def _disconnect(self, arguments: Optional[DisconnectArguments] = None, *ar
241243
os._exit(-1)
242244
else:
243245
await self.send_event_async(Event("disconnectRequested"))
244-
await asyncio.sleep(3)
245246
Debugger.instance().attached = False
246-
Debugger.instance().continue_all()
247+
Debugger.instance().continue_all(False)
247248

248249
@rpc_method(name="setBreakpoints", param_type=SetBreakpointsArguments)
249250
async def _set_breakpoints(
@@ -263,6 +264,10 @@ async def _configuration_done(
263264
self._received_configuration_done = True
264265
self._received_configuration_done_event.set()
265266

267+
if self.received_configuration_done_callback is not None:
268+
269+
self.received_configuration_done_callback()
270+
266271
@_logger.call
267272
async def wait_for_configuration_done(self, timeout: float = 5) -> bool:
268273
await asyncio.wait_for(self._received_configuration_done_event.wait(), timeout)

0 commit comments

Comments
 (0)