Refactor debugging configuration, add support for server_ready_pattern // Resolve #3401

This commit is contained in:
Ivan Kravets
2021-03-18 23:42:54 +02:00
parent 2745dbd124
commit dbb9998f69
17 changed files with 622 additions and 452 deletions
-36
View File
@@ -18,16 +18,12 @@ import subprocess
import sys
import time
from platformio import fs
from platformio.compat import (
WINDOWS,
aio_create_task,
aio_get_running_loop,
get_locale_encoding,
string_types,
)
from platformio.proc import get_pythonexe_path
from platformio.project.helpers import get_project_core_dir
class DebugSubprocessProtocol(asyncio.SubprocessProtocol):
@@ -61,12 +57,6 @@ class DebugBaseProcess:
STDOUT_CHUNK_SIZE = 2048
LOG_FILE = None
COMMON_PATTERNS = {
"PLATFORMIO_HOME_DIR": get_project_core_dir(),
"PLATFORMIO_CORE_DIR": get_project_core_dir(),
"PYTHONEXE": get_pythonexe_path(),
}
def __init__(self):
self.transport = None
self._is_running = False
@@ -155,32 +145,6 @@ class DebugBaseProcess:
self._exit_future.set_result(True)
self._exit_future = None
def apply_patterns(self, source, patterns=None):
_patterns = self.COMMON_PATTERNS.copy()
_patterns.update(patterns or {})
for key, value in _patterns.items():
if key.endswith(("_DIR", "_PATH")):
_patterns[key] = fs.to_unix_path(value)
def _replace(text):
for key, value in _patterns.items():
pattern = "$%s" % key
text = text.replace(pattern, value or "")
return text
if isinstance(source, string_types):
source = _replace(source)
elif isinstance(source, (list, dict)):
items = enumerate(source) if isinstance(source, list) else source.items()
for key, value in items:
if isinstance(value, string_types):
source[key] = _replace(value)
elif isinstance(value, (list, dict)):
source[key] = self.apply_patterns(value, patterns)
return source
def terminate(self):
if not self.is_running() or not self.transport:
return
+25 -43
View File
@@ -23,8 +23,6 @@ from platformio import fs, proc, telemetry, util
from platformio.cache import ContentCache
from platformio.compat import aio_get_running_loop, hashlib_encode_data, is_bytes
from platformio.debug import helpers
from platformio.debug.exception import DebugInvalidOptionsError
from platformio.debug.initcfgs import get_gdb_init_config
from platformio.debug.process.base import DebugBaseProcess
from platformio.debug.process.server import DebugServerProcess
from platformio.project.helpers import get_project_cache_dir
@@ -37,14 +35,12 @@ class DebugClientProcess(
PIO_SRC_NAME = ".pioinit"
INIT_COMPLETED_BANNER = "PlatformIO: Initialization completed"
def __init__(self, project_dir, args, debug_options, env_options):
def __init__(self, project_dir, debug_config):
super(DebugClientProcess, self).__init__()
self.project_dir = project_dir
self.args = list(args)
self.debug_options = debug_options
self.env_options = env_options
self.debug_config = debug_config
self._server_process = DebugServerProcess(debug_options, env_options)
self._server_process = DebugServerProcess(debug_config)
self._session_id = None
if not os.path.isdir(get_project_cache_dir()):
@@ -56,27 +52,13 @@ class DebugClientProcess(
self._target_is_running = False
self._errors_buffer = b""
async def run(self, gdb_path, prog_path):
session_hash = gdb_path + prog_path
async def run(self, extra_args):
gdb_path = self.debug_config.client_executable_path
session_hash = gdb_path + self.debug_config.program_path
self._session_id = hashlib.sha1(hashlib_encode_data(session_hash)).hexdigest()
self._kill_previous_session()
patterns = {
"PROJECT_DIR": self.project_dir,
"PROG_PATH": prog_path,
"PROG_DIR": os.path.dirname(prog_path),
"PROG_NAME": os.path.basename(os.path.splitext(prog_path)[0]),
"DEBUG_PORT": self.debug_options["port"],
"UPLOAD_PROTOCOL": self.debug_options["upload_protocol"],
"INIT_BREAK": self.debug_options["init_break"] or "",
"LOAD_CMDS": "\n".join(self.debug_options["load_cmds"] or []),
}
await self._server_process.run(patterns)
if not patterns["DEBUG_PORT"]:
patterns["DEBUG_PORT"] = self._server_process.get_debug_port()
self.generate_pioinit(self._gdbsrc_dir, patterns)
self.debug_config.port = await self._server_process.run()
self.generate_init_script(os.path.join(self._gdbsrc_dir, self.PIO_SRC_NAME))
# start GDB client
args = [
@@ -89,13 +71,11 @@ class DebugClientProcess(
"-l",
"10",
]
args.extend(self.args)
if not gdb_path:
raise DebugInvalidOptionsError("GDB client is not configured")
args.extend(list(extra_args or []))
gdb_data_dir = self._get_data_dir(gdb_path)
if gdb_data_dir:
args.extend(["--data-directory", gdb_data_dir])
args.append(patterns["PROG_PATH"])
args.append(self.debug_config.program_path)
await self.spawn(*args, cwd=self.project_dir, wait_until_exit=True)
@staticmethod
@@ -107,13 +87,13 @@ class DebugClientProcess(
)
return gdb_data_dir if os.path.isdir(gdb_data_dir) else None
def generate_pioinit(self, dst_dir, patterns):
def generate_init_script(self, dst):
# default GDB init commands depending on debug tool
commands = get_gdb_init_config(self.debug_options).split("\n")
commands = self.debug_config.get_init_script("gdb").split("\n")
if self.debug_options["init_cmds"]:
commands = self.debug_options["init_cmds"]
commands.extend(self.debug_options["extra_cmds"])
if self.debug_config.init_cmds:
commands = self.debug_config.init_cmds
commands.extend(self.debug_config.extra_cmds)
if not any("define pio_reset_run_target" in cmd for cmd in commands):
commands = [
@@ -134,20 +114,20 @@ class DebugClientProcess(
"define pio_restart_target",
" pio_reset_halt_target",
" $INIT_BREAK",
" %s" % ("continue" if patterns["INIT_BREAK"] else "next"),
" %s" % ("continue" if self.debug_config.init_break else "next"),
"end",
]
banner = [
"echo PlatformIO Unified Debugger -> http://bit.ly/pio-debug\\n",
"echo PlatformIO: debug_tool = %s\\n" % self.debug_options["tool"],
"echo PlatformIO: debug_tool = %s\\n" % self.debug_config.tool_name,
"echo PlatformIO: Initializing remote target...\\n",
]
footer = ["echo %s\\n" % self.INIT_COMPLETED_BANNER]
commands = banner + commands + footer
with open(os.path.join(dst_dir, self.PIO_SRC_NAME), "w") as fp:
fp.write("\n".join(self.apply_patterns(commands, patterns)))
with open(dst, "w") as fp:
fp.write("\n".join(self.debug_config.reveal_patterns(commands)))
def connection_made(self, transport):
super(DebugClientProcess, self).connection_made(transport)
@@ -179,7 +159,9 @@ class DebugClientProcess(
# go to init break automatically
if self.INIT_COMPLETED_BANNER.encode() in data:
telemetry.send_event(
"Debug", "Started", telemetry.dump_run_environment(self.env_options)
"Debug",
"Started",
telemetry.dump_run_environment(self.debug_config.env_options),
)
self._auto_exec_continue()
@@ -194,12 +176,12 @@ class DebugClientProcess(
aio_get_running_loop().call_later(0.1, self._auto_exec_continue)
return
if not self.debug_options["init_break"] or self._target_is_running:
if not self.debug_config.init_break or self._target_is_running:
return
self.console_log(
"PlatformIO: Resume the execution to `debug_init_break = %s`\n"
% self.debug_options["init_break"]
% self.debug_config.init_break
)
self.console_log(
"PlatformIO: More configuration options -> http://bit.ly/pio-debug\n"
@@ -226,7 +208,7 @@ class DebugClientProcess(
last_erros = re.sub(r'((~|&)"|\\n\"|\\t)', " ", last_erros, flags=re.M)
err = "%s -> %s" % (
telemetry.dump_run_environment(self.env_options),
telemetry.dump_run_environment(self.debug_config.env_options),
last_erros,
)
telemetry.send_exception("DebugInitError: %s" % err)
+20 -33
View File
@@ -13,10 +13,12 @@
# limitations under the License.
import asyncio
import fnmatch
import os
import time
from platformio import fs, util
from platformio import fs
from platformio.compat import MACOS, WINDOWS
from platformio.debug.exception import DebugInvalidOptionsError
from platformio.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode
from platformio.debug.process.base import DebugBaseProcess
@@ -24,27 +26,22 @@ from platformio.proc import where_is_program
class DebugServerProcess(DebugBaseProcess):
def __init__(self, debug_options, env_options):
def __init__(self, debug_config):
super(DebugServerProcess, self).__init__()
self.debug_options = debug_options
self.env_options = env_options
self._debug_port = ":3333"
self.debug_config = debug_config
self._ready = False
async def run(self, patterns): # pylint: disable=too-many-branches
systype = util.get_systype()
server = self.debug_options.get("server")
async def run(self): # pylint: disable=too-many-branches
server = self.debug_config.server
if not server:
return None
server = self.apply_patterns(server, patterns)
server_executable = server["executable"]
if not server_executable:
return None
if server["cwd"]:
server_executable = os.path.join(server["cwd"], server_executable)
if (
"windows" in systype
WINDOWS
and not server_executable.endswith(".exe")
and os.path.isfile(server_executable + ".exe")
):
@@ -56,15 +53,18 @@ class DebugServerProcess(DebugBaseProcess):
raise DebugInvalidOptionsError(
"\nCould not launch Debug Server '%s'. Please check that it "
"is installed and is included in a system PATH\n\n"
"See documentation or contact contact@platformio.org:\n"
"See documentation:\n"
"https://docs.platformio.org/page/plus/debugging.html\n"
% server_executable
)
openocd_pipe_allowed = all(
[not self.debug_options["port"], "openocd" in server_executable]
[
not self.debug_config.env_options.get("debug_port"),
"gdb" in self.debug_config.client_executable_path,
"openocd" in server_executable,
]
)
# openocd_pipe_allowed = False
if openocd_pipe_allowed:
args = []
if server["cwd"]:
@@ -76,18 +76,16 @@ class DebugServerProcess(DebugBaseProcess):
str_args = " ".join(
[arg if arg.startswith("-") else '"%s"' % arg for arg in args]
)
self._debug_port = '| "%s" %s' % (server_executable, str_args)
self._debug_port = fs.to_unix_path(self._debug_port)
return self._debug_port
return fs.to_unix_path('| "%s" %s' % (server_executable, str_args))
env = os.environ.copy()
# prepend server "lib" folder to LD path
if (
"windows" not in systype
not WINDOWS
and server["cwd"]
and os.path.isdir(os.path.join(server["cwd"], "lib"))
):
ld_key = "DYLD_LIBRARY_PATH" if "darwin" in systype else "LD_LIBRARY_PATH"
ld_key = "DYLD_LIBRARY_PATH" if MACOS else "LD_LIBRARY_PATH"
env[ld_key] = os.path.join(server["cwd"], "lib")
if os.environ.get(ld_key):
env[ld_key] = "%s:%s" % (env[ld_key], os.environ.get(ld_key))
@@ -102,20 +100,12 @@ class DebugServerProcess(DebugBaseProcess):
await self.spawn(
*([server_executable] + server["arguments"]), cwd=server["cwd"], env=env
)
if "mspdebug" in server_executable.lower():
self._debug_port = ":2000"
elif "jlink" in server_executable.lower():
self._debug_port = ":2331"
elif "qemu" in server_executable.lower():
self._debug_port = ":1234"
await self._wait_until_ready()
return self._debug_port
return self.debug_config.port
async def _wait_until_ready(self):
ready_pattern = self.debug_options.get("server", {}).get("ready_pattern")
ready_pattern = self.debug_config.server_ready_pattern
timeout = 60 if ready_pattern else 10
elapsed = 0
delay = 0.5
@@ -129,14 +119,11 @@ class DebugServerProcess(DebugBaseProcess):
def _check_ready_by_pattern(self, data):
if self._ready:
return self._ready
ready_pattern = self.debug_options.get("server", {}).get("ready_pattern")
ready_pattern = self.debug_config.server_ready_pattern
if ready_pattern:
self._ready = ready_pattern.encode() in data
return self._ready
def get_debug_port(self):
return self._debug_port
def stdout_data_received(self, data):
super(DebugServerProcess, self).stdout_data_received(
escape_gdbmi_stream("@", data) if is_gdbmi_mode() else data