Windows+VSCode+Python(on Linux)のリモートデバッグでブレークポイントが効かない問題と原因と対処

恐らく、WindowsでVSCodeを使ってリモートのLinux上のPythonプログラムをデバッグするような人はレアなんでしょうが、私が引っかかって具体的な対処も書かれておらずとても辛かったので、起こった問題、原因、対処法をここで共有します。

また、対処は暫定的なものです。

### TL;DR 

Windows上のVSCodeにおいて、ドライブレターの大文字・小文字の扱いにより本問題が起きている。

対処としては、`launch.json`の`pathMappings`の`localRoot`は**ドライブレターを小文字**で指定すると動作するようになる。
VSCodeには便利な変数`${workspaceFolder}`があるので指定したくなるが、これだと**ドライブレターが大文字**になり、本問題が起きる。

関連Issue: https://github.com/Microsoft/vscode-python/issues/2976

~~そもそもWindowsを使わなければ回避できる~~

### 問題


遭遇した環境は以下のような感じ。バージョンが古いという事はあまり関係なさそう。

- VSCode: 1.32.3
- VSCode Python Extension: 2019.3.6352
- Python: 3.6.6
- ptvsd: 4.2.6

リモート環境はLinux上のコンテナ(docker)でPythonアプリをptvsdを仕込んでリモートデバッグの待ち受けを行っている。

この状態でVSCodeでAtattchしてやるとちゃんと動き始めるが、設定したブレークポイントが効かない。

`launch.json`はこんな感じで、別に何も新しいことはしてない。ドキュメント通りやっている。

doc: https://code.visualstudio.com/docs/python/debugging#_remote-debugging

``` launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Remote Attach",
            "type": "python",
            "request": "attach",
            "port": 5678,
            "host": "192.168.XXX.XXX",
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "/mnt"
                }
            ],
        }
    ],
}
```

`/mnt`配下にworkspaceFolderの内容のファイルがある。

例外を起こしたときに出てくるスタックトレースからコードジャンプすることは出来るので、マッピングのミスではない気はする。

### 原因

結構頑張って調べた。

まず、マッピングが正しくない（正確にはファイルパスの置き換え後の結果がリモート上に存在しない）場合、Debug Consoleに以下のようなログが出力され、ブレークポイントが機能しない。

```
pydev debugger: warning: trying to add breakpoint to file that does not exist: /mnt/c:/path/to/program.py (will have no effect)
```

これはptvsd上は`launch.json`の`pathMappings`の値を素直にそのまま使うのと、python拡張がsetBreakPoints時に送るファイルパスを**小文字で送る**という2点の関係から起きているらしい。

実際の動きを追うために以下の2点を行う。

- `launch.json`の設定に`"logToFile": true,`を与える
- リモート側の環境変数に`DEBUG_PYDEVD_PATHS_TRANSLATION=1`を設定する（docker runなら`--env DEBUG_PYDEVD_PATHS_TRANSLATION=1`で渡す）

これでファイルパスの置き換えに関するログがデバッグコンソール側に出力されるようになり、ptvsdとのやり取り(JSON)のログが出力される。（extensionが配置されているのフォルダにログが出力される。具体的には`%USER_PROFILE%\.vscode\extensions\`の下など。）

`${workspaceFolder}`の挙動を見るには、pathMappingsの値を送る時の値を見れば分かる。
以下のようなログがファイルへ出る。(JSONフォーマット済み、一部センシティブな情報を編集済み)

```
From Client:
Content-Length: 412

{
    "command": "attach",
    "arguments": {
        "preLaunchTask": "Docker run",
        "name": "Python: Remote Attach",
        "type": "python",
        "request": "attach",
        "port": 5678,
        "host": "192.168.XXX.XXX",
        "pathMappings": [
            {
                "localRoot": "C:\\Path\\To\\Project",
                "remoteRoot": "/mnt"
            }
        ],
        "logToFile": true,
        "debugOptions": [
            "RedirectOutput",
            "WindowsClient"
        ],
        "workspaceFolder": "c:\\Path\\To\\Project",
        "__sessionId": "0a28fee6-35a0-47cf-826b-XXXXXXXXX"
    },
    "type": "request",
    "seq": 2
}
```

`arguments.pathMappings.localRoot`の値が大文字になっている。なので、`${workspaceFolder}`はドライブレターを大文字で扱っていることがわかる。

一方で、ブレークポイントを設定する際は、以下のようなログがファイルへ出る。

```
From Client:
Content-Length: 210

{
    "command": "setBreakpoints",
    "arguments": {
        "source": {
            "sourceReference": 0,
            "path": "c:/Path/To/Program.py"
        },
        "lines": [
            65,
            71
        ],
        "breakpoints": [
            {
                "line": 65
            },
            {
                "line": 71
            }
        ],
        "sourceModified": false
    },
    "type": "request",
    "seq": 16
}
```

`arguments.source.path`の値が小文字になっている。

そして、ptvsd側は以下の処理で前方完全一致で置き換えようとする。

https://github.com/Microsoft/ptvsd/blob/9c7c4615fd164882c6bb3763f0c5a9b283913ccf/src/ptvsd/_vendored/pydevd/pydevd_file_utils.py#L535

※`(eclipse_prefix, python_prefix) = (localRoot, remoteRoot)`で、`translated`はブレークポイント設定時のファイルパス。

ここで置き換えが出来ないと、リモート上のファイルパスとブレークポイントのファイルパスが異なるのでブレークポイントが効かなくなる。といった寸法らしい。

なので、ここを一致させればよい。手っ取り早いのは`launch.json`の`arguments.pathMappings.localRoot`の値を都合よく合わせる方法だと思う。
具体的には、`${workspaceFolder}`ではなく`c:\\Path\\To\\Project`という形で**ドライブレターを小文字**で明示すればよい。

これに関しては[issue](https://github.com/Microsoft/vscode-python/issues/2976)も上がっているが、あまり議論が進んでいないように見える。
`launch.json`のlocalRootに`${workspaceFolder}`を使えないのは面倒なので治ってほしい・・・