Repository: microsoft/debugpy

Issue:
Path mappings not working with mixed windows-style/linux path slash
I have a launch.json configuration to attach the debugger to a remote service.
This used to work for a long time, not sure when it stopped working exactly.
```
{
            "name": "Service1",
            "type": "python",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 6005
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceRoot}/Source",
                    "remoteRoot": "/app"
                }
            ],
            "logToFile": true,
            "justMyCode": false
        }
      ...
}
```
Gives error:
```
pydev debugger: unable to find translation for: "c:\path\to\workspace\Source\...\app.py" in ["C:\path\to\workspace/Source/","C:\path\to\workspace/Source"] (please revise your path mappings).
```

The local root is translated into 
`C:\path\to\workspace/Source/` (notice the forward slashes at the end, and capital C )
While the pydev debugger is looking for 
`c:\path\to\workspace\Source\...\app.py`

I tried several ways to make it work, by replacing the ${workspaceRoot} variable with the value with lower c, and correct slashes. 
But the only way I managed to make it work is to map **exactly** the full path of the specific file I want to debug. That's the only way I could see and hit the breakpoint on vscode window.
```
            "pathMappings": [
                {
                    "localRoot": "c:\\path\\to\\workspace\\Source\\...\\app.py",
                    "remoteRoot": "/app/../app.py"
                }
            ]
```

There are other ways I have tried, many variations with the slashes, but none of them worked. Also sometimes I don't receive the error above, but rather only `Breakpoint in file that does not exist`.

Is this a vscode issue or debugpy issue?

Discussion:
@gramster: @karthiknadig, does this seem like debugpy or PVSC?@karthiknadig: For this can you try adding `"clientOS": "windows"` to the configuration? This can occur when debugpy is not able to identify the client OS from the server side.@karthiknadig: If you are using the latest version of python extension which is 2023.8.* and debugpy 1.6.7 and you are seeing this issue let us know. It could also be a bug in Python extension where the client OS is not being explictly sent to debugpy.@andreeas26: > If you are using the latest version of python extension which is 2023.8.* and debugpy 1.6.7 and you are seeing this issue let us know. It could also be a bug in Python extension where the client OS is not being explictly sent to debugpy.

Hi! I am having the same problem. My python extension is 2023.10, but debugpy is 1.5.1 because I am using a rather old version of python. I cannot add "clientOS" to the configuration. I get: Property "clientOS" is not allowed. Below you can see my configuration file. Do you think I should downgrade the python extension? It's a bit more difficult to update the environment I am using.

```
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Remote Attach",
            "type": "python",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 2599
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "."
                }
                
            ],
            "justMyCode": true,
        }
    ]
}

```@karthiknadig: If you are seeing this you can ignore the warning:
![image](https://github.com/microsoft/debugpy/assets/3840081/fd68f72d-10b5-46f1-9495-96ceceefd26c)


Which version of python extension are you using?
@andreeas26: > If you are seeing this you can ignore the warning: ![image](https://user-images.githubusercontent.com/3840081/244776258-fd68f72d-10b5-46f1-9495-96ceceefd26c.png)
> 
> Which version of python extension are you using?
I use v2023.10.0. It still doesn't work even with "clientOS"@devmessias: Any news about this issue?@rchiodo: Did you try this suggestion?

```
            "pathMappings": [
                {
                    "localRoot": "c:\\path\\to\\workspace\\Source\\...\\app.py",
                    "remoteRoot": "/app/../app.py"
                }
            ]
```

Putting the full path to a file in the path mappings?@devmessias: No, this is used by multiple employees. I'm looking for a solution using {workspace} to avoid everyone changing the launch.json for each specific folder. In addition, I tried to declare clientOS, but I got the following warning, which is strange because I looked inside the debugpy code and it seems that this is a valid argument.

![Image](https://github.com/user-attachments/assets/c7b44b37-db8c-4311-8b4d-2ef8d6866e85)
@justynbell: I'm having this same issue.

VSCode: 1.96.2
Python Debugger plugin: 2024-12-11
pydebug (on remote): 1.5.1

Putting the full path in localRoot works: `"localRoot": "c:\\dev\\test\\`
Using: `"localRoot": "${workspaceFolder}",`  Does not.  

When using the `${workspaceFolder}` localRoot, pydebug spits out:

`pydev debugger: unable to find translation for: "c:\dev\test\Test.py" in ["C:\dev\test/", "C:\dev\test"] (please revise your path mappings).`

It's some issue with either the slashes, or the "c" vs "C", I don't know.

Is there a workaround, because hard-coding paths is not acceptable in my use-case either.

Using the `"clientOS": "windows"` property doesn't change anything for me.@rchiodo: I would hazard a guess that VS code changed the result for `${workspaceFolder}` to be upper case. Debugpy doesn't do the expansion of the `${workspaceFolder}` variable AFAIK, that happens on the VS code side as Debugpy knows nothing of the workspace. 

We'll likely have to do case insensitive lookups. Not sure why it wasn't already doing this for windows. 

The translation happens here: 

https://github.com/microsoft/debugpy/blob/02723de14048aaa414cd4be01268b0a0f4c786ad/src/debugpy/_vendored/pydevd/pydevd_file_utils.py#L761@justynbell: It looks like in Windows `${workspacefolder}` would expand to `C:\dev\test` for me (without the trailing `\`).  It looks like debugpy wouldn't like that, even with the case-insensitive path, as if I set my localRoot to `"localRoot": "c:\\dev\\test"`, although debugpy doesn't spit out an error, I can't set breakpoints in my source.

I could fix the issue by adding `\\` at the end of `${workspaceFolder}` but that wouldn't be portable if I opened my project on a Linux platform.

I don't know if that's also an issue for debugpy or the VSCode plugin, how they interact, etc, etc.  Admittedly, I haven't really dug into anything, just giving clues.@rchiodo: I believe `/` in a launch.json are translated appropriately based on platform. You might try this.

```
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}/",
                    "remoteRoot": "/blah/"
                }
            ]
```

Maybe that's the change. `${workspaceFolder}` no longer includes the '/'.@justynbell: If that's the case, you'd expect
`"localRoot": "c:/dev/test/"` to work just as
`"localRoot": "c:\\dev\\test\\",`
right?

~Although the former doesn't throw an error for pydev about "unable to find translation", I'm not able to set any breakpoints in my source code. (if I hover over the gray breakpoint it says "Breakpoint in file that does not exist")~

~![Image](https://github.com/user-attachments/assets/01b0ef4a-f011-41a6-b7a1-f8fb349085df)~

~This is with the second "localRoot" line~

~![Image](https://github.com/user-attachments/assets/22f79ee6-53d0-4fd0-8c04-cdef45c34a26)~

EDIT (sorry for all the edits): it does throw an error if I use `"localRoot": "c:/dev/test/"` after I restarted my application running debugpy on my target:

>pydev debugger: unable to find translation for: "c:\dev\test\Test.py" in ["c:/dev/test/"] (please revise your path mappings).

ONLY the second line allows me to remotely debug my python with breakpoints.@justynbell: I know it's confusing - I was confused for almost a full day trying to get the *perfect* syntax for my localRoot parameter in order for anything to work.  But from the clues I gathered, it's BOTH the case-sensitive "C", and also the path need to be escaped windows-style to work.

>"localRoot": "${workspaceFolder}/"
>pydev debugger: unable to find translation for: "c:\dev\test\Test.py" in ["C:\dev\test/"] (please revise your path mappings).

>"localRoot": "${workspaceFolder}\\\\",
>pydev debugger: unable to find translation for: "c:\dev\test\Test.py" in ["C:\dev\test\\\"] (please revise your path mappings).

>"localRoot": "c:\\\\dev\\\\test\\\\",
< works >@rchiodo: I can't reproduce the path or path escape problems myself, but in my situation the client OS is being sent. This part is crucial in the launch.json though:

```
"clientOS": "windows"
```

That makes the debugger on the server side normalize all casing and path separators.

The error that VS code shows is a red herring. It's simply because this item wasn't added here (I'll fix that in the next release):

https://github.com/microsoft/vscode-python-debugger/blob/1b6ac0d2836a6288131de032e68d56f4770640e0/package.json#L277

This is also a problem:

```
"pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "." // <-- '.' is not supported here.
                }
            ]
```

I'll submit something to make relative paths on the server paths resolve themselves before being used, but the current version requires the full remote path for path mappings to work. You can't just put `'remotePath': '.'`.

You might also want to set this environment variable on the server:

```
set DEBUG_PYDEVD_PATHS_TRANSLATION=True
```

That will output more path information when this isn't working. For example it should print out the client os it thinks is being used:

```
pydev debugger: client OS: WINDOWS
```
@justynbell: Thanks for following up with this @rchiodo and helping us out.

I wasn't aware of the env variable.  When set my pydev debugger output shows
> pydev debugger: client OS: UNIX
> pydev debugger: unable to find translation for: "c:\dev\test\Test.py" in ["c:/dev/test/"] (please revise your path mappings).

However, like @andreeas26 , I'm using debugpy 1.5.1, as I'm on an embedded platform, and it's simply all I'm stuck with.  Maybe the old version of debugpy doesn't support the clientOS parameter from VSCode?

Is there any other way to set this on my target (the debugpy server) directly?@rchiodo: Sorry but with 1.51, there isn't a way to set that.

You can search for it here:
https://github.dev/microsoft/debugpy/tree/v1.5.1/src/debugpy

It wasn't even part of the source at that point@rchiodo: You might try 'ideOS'? It looks like that served the same purpose. Not sure if it will work@rchiodo: The relative path part of this issue should be fixed in the latest build of debugpy. It should ship with VS code in a future version of the [Python Debugger Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) or you can install your own debugpy from pypi. 

Closing for now as the other problem really can't be fixed with an older debugpy.@rchiodo: Take that back, our release to PyPI failed when it looked like it succeeded. Fix will be coming after we resolve that problem.@rchiodo: PyPI release succeeded. v1.8.12 is now available

Solution patch:
diff --git a/src/debugpy/_vendored/pydevd/pydevd_file_utils.py b/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
index b9fbe4d2..390f903c 100644
--- a/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
+++ b/src/debugpy/_vendored/pydevd/pydevd_file_utils.py
@@ -653,6 +653,12 @@ def _fix_path(path, sep, add_end_sep=False):
 
     if sep != "/":
         path = path.replace("/", sep)
+
+    if path.startswith("."):
+        # We need the full path if this relative because all comparisons below
+        # check if the path is substring of another
+        path = os.path.realpath(path)
+
     return path
 
 
@@ -747,6 +753,16 @@ def setup_client_server_paths(paths):
         initial_paths_with_end_sep.append((path0, path1))
         paths_from_eclipse_to_python_with_end_sep.append((_normcase_from_client(path0), normcase(path1)))
 
+    if DEBUG_CLIENT_SERVER_TRANSLATION:
+        pydev_log.critical(
+            "pydev debugger: paths_from_eclipse_to_python %s",
+            ", ".join(
+                [
+                    '"%s=>%s"' % (x[0], x[1])
+                    for x in paths_from_eclipse_to_python
+                ]
+            ),
+        )
     # Fix things so that we always match the versions with a slash in the end first.
     initial_paths = initial_paths_with_end_sep + initial_paths
     paths_from_eclipse_to_python = paths_from_eclipse_to_python_with_end_sep + paths_from_eclipse_to_python


Test patch:
diff --git a/src/debugpy/_vendored/pydevd/tests_python/test_convert_utilities.py b/src/debugpy/_vendored/pydevd/tests_python/test_convert_utilities.py
index 4dbac3b3..fd640ab8 100644
--- a/src/debugpy/_vendored/pydevd/tests_python/test_convert_utilities.py
+++ b/src/debugpy/_vendored/pydevd/tests_python/test_convert_utilities.py
@@ -150,16 +150,25 @@ def test_source_reference(tmpdir):
         # Client on windows and server on unix
         pydevd_file_utils.set_ide_os("WINDOWS")
 
-        pydevd_file_utils.setup_client_server_paths([("c:\\foo", "/bar")])
+        pydevd_file_utils.setup_client_server_paths([("C:\\foo", "/bar")])
 
-        assert pydevd_file_utils.map_file_to_client("/bar/my") == ("c:\\foo\\my", True)
-        assert pydevd_file_utils.get_client_filename_source_reference("c:\\foo\\my") == 0
+        assert pydevd_file_utils.map_file_to_client("/bar/my") == ("C:\\foo\\my", True)
+        assert pydevd_file_utils.get_client_filename_source_reference("C:\\foo\\my") == 0
 
         assert pydevd_file_utils.map_file_to_client("/another/my") == ("\\another\\my", False)
         source_reference = pydevd_file_utils.get_client_filename_source_reference("\\another\\my")
         assert source_reference != 0
         assert pydevd_file_utils.get_server_filename_from_source_reference(source_reference) == "/another/my"
 
+        # Allow relative paths in the server path
+        pydevd_file_utils.setup_client_server_paths([("C:\\foo", "./bar")])
+        root = os.getcwd()
+        server_path = os.path.join(root, "bar/my")
+
+        assert pydevd_file_utils.map_file_to_client(server_path) == ("C:\\foo\\my", True)
+        assert pydevd_file_utils.get_client_filename_source_reference("C:\\foo\\my") == 0
+        assert pydevd_file_utils.map_file_to_server("c:\\foo\\my") == server_path
+
 
 @pytest.mark.skipif(sys.platform != "win32", reason="Windows-only test.")
 def test_translate_only_drive():