handlers.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # Copyright (c) Microsoft Corporation. All rights reserved.
  2. # Licensed under the MIT License. See LICENSE in the project root
  3. # for license information.
  4. import os
  5. import sys
  6. import debugpy
  7. from debugpy import launcher
  8. from debugpy.common import json
  9. from debugpy.launcher import debuggee
  10. def launch_request(request):
  11. debug_options = set(request("debugOptions", json.array(str)))
  12. # Handling of properties that can also be specified as legacy "debugOptions" flags.
  13. # If property is explicitly set to false, but the flag is in "debugOptions", treat
  14. # it as an error. Returns None if the property wasn't explicitly set either way.
  15. def property_or_debug_option(prop_name, flag_name):
  16. assert prop_name[0].islower() and flag_name[0].isupper()
  17. value = request(prop_name, bool, optional=True)
  18. if value == ():
  19. value = None
  20. if flag_name in debug_options:
  21. if value is False:
  22. raise request.isnt_valid(
  23. '{0}:false and "debugOptions":[{1}] are mutually exclusive',
  24. json.repr(prop_name),
  25. json.repr(flag_name),
  26. )
  27. value = True
  28. return value
  29. python = request("python", json.array(str, size=(1,)))
  30. cmdline = list(python)
  31. if not request("noDebug", json.default(False)):
  32. # see https://github.com/microsoft/debugpy/issues/861
  33. if sys.version_info[:2] >= (3, 11):
  34. cmdline += ["-X", "frozen_modules=off"]
  35. port = request("port", int)
  36. cmdline += [
  37. os.path.dirname(debugpy.__file__),
  38. "--connect",
  39. launcher.adapter_host + ":" + str(port),
  40. ]
  41. if not request("subProcess", True):
  42. cmdline += ["--configure-subProcess", "False"]
  43. qt_mode = request(
  44. "qt",
  45. json.enum(
  46. "none", "auto", "pyside", "pyside2", "pyqt4", "pyqt5", optional=True
  47. ),
  48. )
  49. cmdline += ["--configure-qt", qt_mode]
  50. adapter_access_token = request("adapterAccessToken", str, optional=True)
  51. if adapter_access_token != ():
  52. cmdline += ["--adapter-access-token", adapter_access_token]
  53. debugpy_args = request("debugpyArgs", json.array(str))
  54. cmdline += debugpy_args
  55. # Use the copy of arguments that was propagated via the command line rather than
  56. # "args" in the request itself, to allow for shell expansion.
  57. cmdline += sys.argv[1:]
  58. process_name = request("processName", sys.executable)
  59. env = os.environ.copy()
  60. env_changes = request("env", json.object((str, type(None))))
  61. if sys.platform == "win32":
  62. # Environment variables are case-insensitive on Win32, so we need to normalize
  63. # both dicts to make sure that env vars specified in the debug configuration
  64. # overwrite the global env vars correctly. If debug config has entries that
  65. # differ in case only, that's an error.
  66. env = {k.upper(): v for k, v in os.environ.items()}
  67. new_env_changes = {}
  68. for k, v in env_changes.items():
  69. k_upper = k.upper()
  70. if k_upper in new_env_changes:
  71. if new_env_changes[k_upper] == v:
  72. continue
  73. else:
  74. raise request.isnt_valid(
  75. 'Found duplicate in "env": {0}.'.format(k_upper)
  76. )
  77. new_env_changes[k_upper] = v
  78. env_changes = new_env_changes
  79. if "DEBUGPY_TEST" in env:
  80. # If we're running as part of a debugpy test, make sure that codecov is not
  81. # applied to the debuggee, since it will conflict with pydevd.
  82. env.pop("COV_CORE_SOURCE", None)
  83. env.update(env_changes)
  84. env = {k: v for k, v in env.items() if v is not None}
  85. if request("gevent", False):
  86. env["GEVENT_SUPPORT"] = "True"
  87. console = request(
  88. "console",
  89. json.enum(
  90. "internalConsole", "integratedTerminal", "externalTerminal", optional=True
  91. ),
  92. )
  93. redirect_output = property_or_debug_option("redirectOutput", "RedirectOutput")
  94. if redirect_output is None:
  95. # If neither the property nor the option were specified explicitly, choose
  96. # the default depending on console type - "internalConsole" needs it to
  97. # provide any output at all, but it's unnecessary for the terminals.
  98. redirect_output = console == "internalConsole"
  99. if redirect_output:
  100. # sys.stdout buffering must be disabled - otherwise we won't see the output
  101. # at all until the buffer fills up.
  102. env["PYTHONUNBUFFERED"] = "1"
  103. # Force UTF-8 output to minimize data loss due to re-encoding.
  104. env["PYTHONIOENCODING"] = "utf-8"
  105. if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"):
  106. if console == "internalConsole":
  107. raise request.isnt_valid(
  108. '"waitOnNormalExit" is not supported for "console":"internalConsole"'
  109. )
  110. debuggee.wait_on_exit_predicates.append(lambda code: code == 0)
  111. if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"):
  112. if console == "internalConsole":
  113. raise request.isnt_valid(
  114. '"waitOnAbnormalExit" is not supported for "console":"internalConsole"'
  115. )
  116. debuggee.wait_on_exit_predicates.append(lambda code: code != 0)
  117. debuggee.spawn(process_name, cmdline, env, redirect_output)
  118. return {}
  119. def terminate_request(request):
  120. del debuggee.wait_on_exit_predicates[:]
  121. request.respond({})
  122. debuggee.kill()
  123. def disconnect():
  124. del debuggee.wait_on_exit_predicates[:]
  125. debuggee.kill()