Skip to content

Commit

Permalink
Normalize path translation files according to client OS. Fixes micros…
Browse files Browse the repository at this point in the history
  • Loading branch information
fabioz committed Apr 9, 2019
1 parent 66bd05a commit 1356d6f
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ def _parse_debug_options(opts):
except KeyError:
continue

if 'CLIENT_OS_TYPE' not in options:
options['CLIENT_OS_TYPE'] = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' # noqa

return options


Expand Down
57 changes: 46 additions & 11 deletions src/ptvsd/_vendored/pydevd/pydevd_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from _pydevd_bundle.pydevd_constants import IS_PY2, IS_PY3K, DebugInfoHolder, IS_WINDOWS, IS_JYTHON
from _pydev_bundle._pydev_filesystem_encoding import getfilesystemencoding
from _pydevd_bundle.pydevd_comm_constants import file_system_encoding, filesystem_encoding_is_utf8
from _pydev_bundle.pydev_log import error_once

import json
import os.path
Expand Down Expand Up @@ -184,6 +185,9 @@ def normcase(filename):

_ide_os = 'WINDOWS' if IS_WINDOWS else 'UNIX'

_normcase_from_client = normcase

DEBUG_CLIENT_SERVER_TRANSLATION = os.environ.get('DEBUG_PYDEVD_PATHS_TRANSLATION', 'False').lower() in ('1', 'true')

def set_ide_os(os):
'''
Expand All @@ -195,20 +199,38 @@ def set_ide_os(os):
'UNIX' or 'WINDOWS'
'''
global _ide_os
global _normcase_from_client
prev = _ide_os
if os == 'WIN': # Apparently PyCharm uses 'WIN' (https://github.com/fabioz/PyDev.Debugger/issues/116)
os = 'WINDOWS'

assert os in ('WINDOWS', 'UNIX')

if DEBUG_CLIENT_SERVER_TRANSLATION:
print('pydev debugger: client OS: %s' % (os,))

_normcase_from_client = normcase
if os == 'WINDOWS':

# Client in Windows and server in Unix, we need to normalize the case.
if not IS_WINDOWS:

def _normcase_from_client(filename):
return filename.lower()

else:
# Client in Unix and server in Windows, we can't normalize the case.
if IS_WINDOWS:

def _normcase_from_client(filename):
return filename

if prev != os:
_ide_os = os
# We need to (re)setup how the client <-> server translation works to provide proper separators.
setup_client_server_paths(_last_client_server_paths_set)


DEBUG_CLIENT_SERVER_TRANSLATION = os.environ.get('DEBUG_PYDEVD_PATHS_TRANSLATION', 'False').lower() in ('1', 'true')

# Caches filled as requested during the debug session.
NORM_PATHS_CONTAINER = {}
NORM_PATHS_AND_BASE_CONTAINER = {}
Expand Down Expand Up @@ -464,7 +486,7 @@ def setup_client_server_paths(paths):
path1 = _fix_path(path1, python_sep)
initial_paths[i] = (path0, path1)

paths_from_eclipse_to_python[i] = (normcase(path0), normcase(path1))
paths_from_eclipse_to_python[i] = (_normcase_from_client(path0), normcase(path1))

if not paths_from_eclipse_to_python:
# no translation step needed (just inline the calls)
Expand All @@ -484,24 +506,37 @@ def _norm_file_to_server(filename, cache=norm_filename_to_server_container):
filename = filename.replace(python_sep, eclipse_sep)

# used to translate a path from the client to the debug server
translated = normcase(filename)
translated = filename
translated_normalized = _normcase_from_client(filename)
for eclipse_prefix, server_prefix in paths_from_eclipse_to_python:
if translated.startswith(eclipse_prefix):
if translated_normalized.startswith(eclipse_prefix):
found_translation = True
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: replacing to server: %s\n' % (translated,))
translated = translated.replace(eclipse_prefix, server_prefix)
sys.stderr.write('pydev debugger: replacing to server: %s\n' % (filename,))
translated = server_prefix + filename[len(eclipse_prefix):]
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: sent to server: %s\n' % (translated,))
break
else:
if DEBUG_CLIENT_SERVER_TRANSLATION:
sys.stderr.write('pydev debugger: to server: unable to find matching prefix for: %s in %s\n' % \
(translated, [x[0] for x in paths_from_eclipse_to_python]))
found_translation = False

# Note that when going to the server, we do the replace first and only later do the norm file.
if eclipse_sep != python_sep:
translated = translated.replace(eclipse_sep, python_sep)
translated = _NormFile(translated)

if found_translation:
translated = _NormFile(translated)
else:
if not os.path.exists(translated):
if not translated.startswith('<'):
# This is a configuration error, so, write it always so
# that the user can fix it.
error_once('pydev debugger: to server: unable to find translation for: "%s" in [%s] (please revise your path mappings).\n' % \
(filename, ', '.join(['"%s"' % (x[0],) for x in paths_from_eclipse_to_python])))
else:
# It's possible that we had some round trip (say, we sent /usr/lib and received
# it back, so, having no translation is ok too).
translated = _NormFile(translated)

cache[filename] = translated
return translated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ def check(obtained, expected):

pydevd_file_utils.setup_client_server_paths(PATHS_FROM_ECLIPSE_TO_PYTHON)
assert pydevd_file_utils.norm_file_to_server('c:\\foo\\my') == '/báéíóúr/my'
assert pydevd_file_utils.norm_file_to_server('C:\\foo\\my') == '/báéíóúr/my'
assert pydevd_file_utils.norm_file_to_server('C:\\foo\\MY') == '/báéíóúr/MY'
assert pydevd_file_utils.norm_file_to_server('C:\\foo\\MY\\') == '/báéíóúr/MY'
assert pydevd_file_utils.norm_file_to_server('c:\\foo\\my\\file.py') == '/báéíóúr/my/file.py'
assert pydevd_file_utils.norm_file_to_server('c:\\foo\\my\\other\\file.py') == '/báéíóúr/my/other/file.py'
assert pydevd_file_utils.norm_file_to_server('c:/foo/my') == '/báéíóúr/my'
Expand All @@ -202,6 +205,12 @@ def check(obtained, expected):
assert pydevd_file_utils.norm_file_to_server('\\usr\\bin') == '/usr/bin'
assert pydevd_file_utils.norm_file_to_server('\\usr\\bin\\') == '/usr/bin'

# When we have a client file and there'd be no translation, and making it absolute would
# do something as '$cwd/$file_received' (i.e.: $cwd/c:/another in the case below),
# warn the user that it's not correct and the path that should be translated instead
# and don't make it absolute.
assert pydevd_file_utils.norm_file_to_server('c:\\another') == 'c:/another'

# Client and server on unix
pydevd_file_utils.set_ide_os('UNIX')
in_eclipse = '/foo'
Expand Down
16 changes: 14 additions & 2 deletions src/ptvsd/_vendored/pydevd/tests_python/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from _pydevd_bundle.pydevd_constants import IS_WINDOWS
from _pydevd_bundle.pydevd_comm_constants import CMD_RELOAD_CODE
import json
import pydevd_file_utils
try:
from urllib import unquote
except ImportError:
Expand Down Expand Up @@ -1858,7 +1859,14 @@ def _ignore_stderr_line(line):
writer.finished_ok = True


def test_path_translation(case_setup):
def _path_equals(path1, path2):
path1 = pydevd_file_utils.normcase(path1)
path2 = pydevd_file_utils.normcase(path2)
return path1 == path2


@pytest.mark.parametrize('mixed_case', [True, False] if sys.platform == 'win32' else [False])
def test_path_translation(case_setup, mixed_case):

def get_file_in_client(writer):
# Instead of using: test_python/_debugger_case_path_translation.py
Expand All @@ -1873,9 +1881,13 @@ def get_environ(writer):
env["PYTHONIOENCODING"] = 'utf-8'

assert writer.TEST_FILE.endswith('_debugger_case_path_translation.py')
file_in_client = get_file_in_client(writer)
if mixed_case:
new_file_in_client = ''.join([file_in_client[i].upper() if i % 2 == 0 else file_in_client[i].lower() for i in range(len(file_in_client))])
assert _path_equals(file_in_client, new_file_in_client)
env["PATHS_FROM_ECLIPSE_TO_PYTHON"] = json.dumps([
(
os.path.dirname(get_file_in_client(writer)),
os.path.dirname(file_in_client),
os.path.dirname(writer.TEST_FILE)
)
])
Expand Down
34 changes: 27 additions & 7 deletions src/ptvsd/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import errno
import json
import os
import re
import platform
import pydevd_file_utils
import site
Expand Down Expand Up @@ -687,9 +688,6 @@ def _parse_debug_options(opts):
except KeyError:
continue

if 'CLIENT_OS_TYPE' not in options:
options['CLIENT_OS_TYPE'] = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' # noqa

return options

########################
Expand Down Expand Up @@ -1101,6 +1099,8 @@ def __init__(
self._pydevd_request = pydevd_request
self._notify_debugger_ready = notify_debugger_ready

self._client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX'

self.loop = None
self.event_loop_thread = None

Expand Down Expand Up @@ -1334,16 +1334,36 @@ def _initialize_path_maps(self, args):

def _send_cmd_version_command(self):
cmd = pydevd_comm.CMD_VERSION
default_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX'
client_os_type = self.debug_options.get('CLIENT_OS_TYPE', default_os_type)
os_id = client_os_type
msg = '1.1\t{}\tID'.format(os_id)

msg = '1.1\t{}\tID'.format(self._client_os_type)
return self.pydevd_request(cmd, msg)

@async_handler
def _handle_launch_or_attach(self, request, args):
self._path_mappings_received = True

client_os_type = self.debug_options.get('CLIENT_OS_TYPE', '').upper().strip()
if client_os_type and client_os_type not in ('WINDOWS', 'UNIX'):
ptvsd.log.warn('Invalid CLIENT_OS_TYPE passed: %s (must be either "WINDOWS" or "UNIX").' % (client_os_type,))
client_os_type = ''

if not client_os_type:
for pathMapping in args.get('pathMappings', []):
localRoot = pathMapping.get('localRoot', '')
if localRoot:
if localRoot.startswith('/'):
client_os_type = 'UNIX'
break

if re.match('^([a-zA-Z]):', localRoot): # Match drive letter
client_os_type = 'WINDOWS'
break

if not client_os_type:
client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX'

self._client_os_type = client_os_type

self.pydevd_request(pydevd_comm.CMD_SET_PROTOCOL, 'json')
yield self._send_cmd_version_command()

Expand Down
43 changes: 43 additions & 0 deletions tests/func/test_path_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,49 @@
from tests.helpers.session import DebugSession
from tests.helpers.timeline import Event
from tests.helpers.pathutils import get_test_root
from tests.helpers import get_marked_line_numbers
import pytest
import sys


@pytest.mark.skipif(sys.platform == 'win32', reason='Linux/Mac only test.')
def test_client_ide_from_path_mapping_linux_backend(pyfile, tmpdir, run_as, start_method):
'''
Test simulating that the backend is on Linux and the frontend is on Linux.
'''

@pyfile
def code_to_debug():
from dbgimporter import import_and_enable_debugger
import_and_enable_debugger()
import backchannel
import pydevd_file_utils
backchannel.write_json({'ide_os': pydevd_file_utils._ide_os})
print('done') # @break_here

with DebugSession() as session:
session.initialize(
target=(run_as, code_to_debug),
start_method=start_method,
ignore_unobserved=[Event('continued')],
use_backchannel=True,
path_mappings=[{
'localRoot': 'C:\\TEMP\\src',
'remoteRoot': os.path.dirname(code_to_debug),
}],
)
bp_line = get_marked_line_numbers(code_to_debug)['break_here']
session.set_breakpoints('c:\\temp\\src\\' + os.path.basename(code_to_debug), [bp_line])
session.start_debugging()
hit = session.wait_for_thread_stopped('breakpoint')
frames = hit.stacktrace.body['stackFrames']
assert frames[0]['source']['path'] == 'C:\\TEMP\\src\\' + os.path.basename(code_to_debug)

json_read = session.read_json()
assert json_read == {'ide_os': 'WINDOWS'}

session.send_request('continue').wait_for_response(freeze=False)
session.wait_for_exit()


def test_with_dot_remote_root(pyfile, tmpdir, run_as, start_method):
Expand Down

0 comments on commit 1356d6f

Please sign in to comment.