Skip to content

Commit 73ef0e0

Browse files
committed
implement source mapping in debugger
1 parent c5fc3e0 commit 73ef0e0

File tree

3 files changed

+65
-14
lines changed

3 files changed

+65
-14
lines changed

robotcode/debugger/debugger.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ class HitCountEntry(NamedTuple):
187187
type: str
188188

189189

190+
class PathMapping(NamedTuple):
191+
local_root: Optional[str]
192+
remote_root: Optional[str]
193+
194+
190195
class Debugger:
191196
__instance = None
192197
__lock = threading.RLock()
@@ -217,7 +222,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Any:
217222
raise RuntimeError(f"Attempt to create a '{cls.__qualname__}' instance outside of instance()")
218223

219224
def __init__(self) -> None:
220-
self.breakpoints: Dict[str, BreakpointsEntry] = {}
225+
self.breakpoints: Dict[pathlib.PurePath, BreakpointsEntry] = {}
221226

222227
self.exception_breakpoints: Set[ExceptionBreakpointsEntry] = set()
223228
self.exception_breakpoints.add(
@@ -243,6 +248,7 @@ def __init__(self) -> None:
243248
self.no_debug = False
244249
self.terminated = False
245250
self.attached = False
251+
self.path_mappings: List[PathMapping] = []
246252

247253
@property
248254
def debug(self) -> bool:
@@ -400,7 +406,11 @@ def set_breakpoints(
400406
lines: Optional[List[int]] = None,
401407
source_modified: Optional[bool] = None,
402408
) -> List[Breakpoint]:
403-
path = str(Path(source.path).resolve()) if source.path else ""
409+
410+
if self.is_windows_path(source.path or ""):
411+
path = pathlib.PureWindowsPath(source.path or "")
412+
else:
413+
path = pathlib.PurePath(source.path or "")
404414

405415
if path in self.breakpoints and not breakpoints and not lines:
406416
self.breakpoints.pop(path)
@@ -409,7 +419,7 @@ def set_breakpoints(
409419
tuple(breakpoints) if breakpoints else (), tuple(lines) if lines else ()
410420
)
411421
return [
412-
Breakpoint(id=id(v), source=Source(path=path), verified=True, line=v.line) for v in result.breakpoints
422+
Breakpoint(id=id(v), source=Source(path=str(path)), verified=True, line=v.line) for v in result.breakpoints
413423
]
414424
else:
415425
self._logger.error("not supported breakpoint")
@@ -475,7 +485,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str)
475485
self.requested_state = RequestedState.Nothing
476486

477487
if source is not None:
478-
source = str(Path(source).resolve())
488+
source = self.map_path_to_client(str(Path(source).absolute()))
479489
if source in self.breakpoints:
480490
breakpoints = [v for v in self.breakpoints[source].breakpoints if v.line == line_no]
481491
if len(breakpoints) > 0:
@@ -520,7 +530,7 @@ def process_start_state(self, source: str, line_no: int, type: str, status: str)
520530
body=OutputEventBody(
521531
output=message + os.linesep,
522532
category=OutputCategory.CONSOLE,
523-
source=Source(path=source) if source else None,
533+
source=Source(path=str(source)) if source else None,
524534
line=line_no,
525535
)
526536
),
@@ -583,7 +593,7 @@ def start_output_group(self, name: str, attributes: Dict[str, Any], type: Option
583593
output=f"\u001b[38;5;14m{(type +' ') if type else ''}\u001b[0m{name}\n",
584594
category=OutputCategory.CONSOLE,
585595
group=OutputGroup.START,
586-
source=Source(path=source) if source else None,
596+
source=Source(path=str(self.map_path_to_client(source))) if source else None,
587597
line=line_no if source is not None else None,
588598
column=0 if source is not None else None,
589599
)
@@ -602,7 +612,7 @@ def end_output_group(self, name: str, attributes: Dict[str, Any]) -> None:
602612
output="",
603613
category=OutputCategory.CONSOLE,
604614
group=OutputGroup.END,
605-
source=Source(path=source) if source else None,
615+
source=Source(path=str(self.map_path_to_client(source))) if source else None,
606616
line=line_no,
607617
)
608618
),
@@ -864,6 +874,30 @@ def get_threads(self) -> List[Thread]:
864874

865875
return [Thread(id=main_thread.ident if main_thread.ident else 0, name=main_thread.name or "")]
866876

877+
WINDOW_PATH_REGEX = re.compile(r"^(([a-z]:[\\/])|(\\\\)).*$", re.RegexFlag.IGNORECASE)
878+
879+
@classmethod
880+
def is_windows_path(cls, path: os.PathLike[str]) -> bool:
881+
return bool(cls.WINDOW_PATH_REGEX.fullmatch(str(path)))
882+
883+
def map_path_to_client(self, path: os.PathLike[str]) -> pathlib.PurePath:
884+
if not self.path_mappings:
885+
return pathlib.PurePath(path)
886+
887+
for mapping in self.path_mappings:
888+
889+
remote_root_path = Path(mapping.remote_root or ".").absolute()
890+
891+
if Path(path).is_relative_to(remote_root_path):
892+
if self.is_windows_path(mapping.local_root):
893+
local_root_path = str(pathlib.PureWindowsPath(mapping.local_root))
894+
return pathlib.PureWindowsPath(path.replace(str(remote_root_path), local_root_path or ""))
895+
else:
896+
local_root_path = str(pathlib.PurePath(mapping.local_root))
897+
return pathlib.PurePath(path.replace(str(remote_root_path), local_root_path or ""))
898+
899+
return path
900+
867901
def get_stack_trace(
868902
self,
869903
thread_id: int,
@@ -879,7 +913,7 @@ def get_stack_trace(
879913

880914
def source_from_entry(entry: StackFrameEntry) -> Optional[Source]:
881915
if entry.source is not None and entry.is_file:
882-
return Source(path=entry.source)
916+
return Source(path=str(self.map_path_to_client(entry.source)))
883917
else:
884918
return None
885919

@@ -927,7 +961,7 @@ def log_message(self, message: Dict[str, Any]) -> None:
927961
self.last_fail_message = msg
928962

929963
current_frame = self.full_stack_frames[0] if self.full_stack_frames else None
930-
source = Source(path=current_frame.source) if current_frame else None
964+
source = Source(path=str(self.map_path_to_client(current_frame.source))) if current_frame else None
931965
line = current_frame.line if current_frame else None
932966

933967
if self.output_log:

robotcode/debugger/launcher/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from ...utils.logging import LoggingDescriptor
1111
from ..client import DAPClient, DAPClientError
1212
from ..dap_types import (
13+
AttachRequest,
14+
AttachRequestArguments,
1315
Capabilities,
1416
ConfigurationDoneArguments,
1517
ConfigurationDoneRequest,
@@ -269,6 +271,7 @@ async def _configuration_done(
269271
self, arguments: Optional[ConfigurationDoneArguments] = None, *args: Any, **kwargs: Any
270272
) -> None:
271273
await self.client.protocol.send_request_async(ConfigurationDoneRequest(arguments=arguments))
274+
await self.client.protocol.send_request_async(AttachRequest(arguments=AttachRequestArguments()))
272275

273276
@rpc_method(name="disconnect", param_type=DisconnectArguments)
274277
async def _disconnect(self, arguments: Optional[DisconnectArguments] = None, *args: Any, **kwargs: Any) -> None:

robotcode/debugger/server.py

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

55
from ..jsonrpc2.protocol import rpc_method
66
from ..jsonrpc2.server import JsonRPCServer, JsonRpcServerMode, TcpParams
@@ -44,7 +44,7 @@
4444
VariablesArguments,
4545
VariablesResponseBody,
4646
)
47-
from .debugger import Debugger
47+
from .debugger import Debugger, PathMapping
4848
from .protocol import DebugAdapterProtocol
4949

5050
TCP_DEFAULT_PORT = 6612
@@ -168,8 +168,23 @@ async def _initialize(self, arguments: InitializeRequestArguments, *args: Any, *
168168
)
169169

170170
@rpc_method(name="attach", param_type=AttachRequestArguments)
171-
async def _attach(self, arguments: AttachRequestArguments, *args: Any, **kwargs: Any) -> None:
172-
pass
171+
async def _attach(
172+
self,
173+
arguments: AttachRequestArguments,
174+
request: Optional[str] = None,
175+
type: Optional[str] = None,
176+
name: Optional[str] = None,
177+
restart: Optional[bool] = None,
178+
pathMappings: Optional[List[Dict[str, str]]] = None, # noqa: N803
179+
*args: Any,
180+
**kwargs: Any,
181+
) -> None:
182+
if pathMappings:
183+
Debugger.instance().path_mappings = [
184+
PathMapping(local_root=v.get("localRoot", None), remote_root=v.get("remoteRoot", None))
185+
for v in pathMappings
186+
]
187+
Debugger.instance().attached = True
173188

174189
@_logger.call
175190
async def initialized(self) -> None:
@@ -227,7 +242,6 @@ async def _configuration_done(
227242
) -> None:
228243
self._received_configuration_done = True
229244
self._received_configuration_done_event.set()
230-
Debugger.instance().attached = True
231245

232246
@_logger.call
233247
async def wait_for_configuration_done(self, timeout: float = 5) -> bool:

0 commit comments

Comments
 (0)