Skip to content

Commit

Permalink
Refactor debugging configuration, add support for server_ready_patter…
Browse files Browse the repository at this point in the history
…n // Resolve #3401
  • Loading branch information
ivankravets committed Mar 18, 2021
1 parent 2745dbd commit dbb9998
Show file tree
Hide file tree
Showing 17 changed files with 622 additions and 452 deletions.
2 changes: 1 addition & 1 deletion docs
90 changes: 40 additions & 50 deletions platformio/commands/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@

import asyncio
import os
import subprocess

import click

from platformio import app, exception, fs, proc
from platformio.commands.platform import platform_install as cmd_platform_install
from platformio.compat import WINDOWS
from platformio.debug import helpers
from platformio.debug.config.factory import DebugConfigFactory
from platformio.debug.exception import DebugInvalidOptionsError
from platformio.debug.process.client import DebugClientProcess
from platformio.platform.exception import UnknownPlatform
from platformio.platform.factory import PlatformFactory
from platformio.project.config import ProjectConfig
from platformio.project.exception import ProjectEnvsNotAvailableError
from platformio.project.helpers import is_platformio_project, load_project_ide_data
from platformio.project.helpers import is_platformio_project


@click.command(
Expand Down Expand Up @@ -62,46 +64,40 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro
app.set_session_var("custom_project_conf", project_conf)

# use env variables from Eclipse or CLion
for sysenv in ("CWD", "PWD", "PLATFORMIO_PROJECT_DIR"):
for name in ("CWD", "PWD", "PLATFORMIO_PROJECT_DIR"):
if is_platformio_project(project_dir):
break
if os.getenv(sysenv):
project_dir = os.getenv(sysenv)
if os.getenv(name):
project_dir = os.getenv(name)

with fs.cd(project_dir):
config = ProjectConfig.get_instance(project_conf)
config.validate(envs=[environment] if environment else None)

env_name = environment or helpers.get_default_debug_env(config)
env_options = config.items(env=env_name, as_dict=True)
if not set(env_options.keys()) >= set(["platform", "board"]):
raise ProjectEnvsNotAvailableError()

try:
platform = PlatformFactory.new(env_options["platform"])
except UnknownPlatform:
ctx.invoke(
cmd_platform_install,
platforms=[env_options["platform"]],
skip_default_package=True,
)
platform = PlatformFactory.new(env_options["platform"])

debug_options = helpers.configure_initial_debug_options(platform, env_options)
assert debug_options
project_config = ProjectConfig.get_instance(project_conf)
project_config.validate(envs=[environment] if environment else None)
env_name = environment or helpers.get_default_debug_env(project_config)

if not interface:
return helpers.predebug_project(ctx, project_dir, env_name, False, verbose)

ide_data = load_project_ide_data(project_dir, env_name)
if not ide_data:
raise DebugInvalidOptionsError("Could not load a build configuration")
env_options = project_config.items(env=env_name, as_dict=True)
if not set(env_options.keys()) >= set(["platform", "board"]):
raise ProjectEnvsNotAvailableError()

try:
platform = PlatformFactory.new(env_options["platform"])
except UnknownPlatform:
ctx.invoke(
cmd_platform_install,
platforms=[env_options["platform"]],
skip_default_package=True,
)
platform = PlatformFactory.new(env_options["platform"])

debug_config = DebugConfigFactory.new(platform, project_config, env_name)

if "--version" in __unprocessed:
result = proc.exec_command([ide_data["gdb_path"], "--version"])
if result["returncode"] == 0:
return click.echo(result["out"])
raise exception.PlatformioException("\n".join([result["out"], result["err"]]))
return subprocess.run(
[debug_config.client_executable_path, "--version"], check=True
)

try:
fs.ensure_udev_rules()
Expand All @@ -113,29 +109,23 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro
nl=False,
)

try:
debug_options = platform.configure_debug_options(debug_options, ide_data)
except NotImplementedError:
# legacy for ESP32 dev-platform <=2.0.0
debug_options["load_cmds"] = helpers.configure_esp32_load_cmds(
debug_options, ide_data
)

rebuild_prog = False
preload = debug_options["load_cmds"] == ["preload"]
load_mode = debug_options["load_mode"]
preload = debug_config.load_cmds == ["preload"]
load_mode = debug_config.load_mode
if load_mode == "always":
rebuild_prog = preload or not helpers.has_debug_symbols(ide_data["prog_path"])
rebuild_prog = preload or not helpers.has_debug_symbols(
debug_config.program_path
)
elif load_mode == "modified":
rebuild_prog = helpers.is_prog_obsolete(
ide_data["prog_path"]
) or not helpers.has_debug_symbols(ide_data["prog_path"])
debug_config.program_path
) or not helpers.has_debug_symbols(debug_config.program_path)
else:
rebuild_prog = not os.path.isfile(ide_data["prog_path"])
rebuild_prog = not os.path.isfile(debug_config.program_path)

if preload or (not rebuild_prog and load_mode != "always"):
# don't load firmware through debug server
debug_options["load_cmds"] = []
debug_config.load_cmds = []

if rebuild_prog:
if helpers.is_gdbmi_mode():
Expand All @@ -155,15 +145,15 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro

# save SHA sum of newly created prog
if load_mode == "modified":
helpers.is_prog_obsolete(ide_data["prog_path"])
helpers.is_prog_obsolete(debug_config.program_path)

if not os.path.isfile(ide_data["prog_path"]):
if not os.path.isfile(debug_config.program_path):
raise DebugInvalidOptionsError("Program/firmware is missed")

loop = asyncio.ProactorEventLoop() if WINDOWS else asyncio.get_event_loop()
asyncio.set_event_loop(loop)
client = DebugClientProcess(project_dir, __unprocessed, debug_options, env_options)
coro = client.run(ide_data["gdb_path"], ide_data["prog_path"])
client = DebugClientProcess(project_dir, debug_config)
coro = client.run(__unprocessed)
loop.run_until_complete(coro)
if WINDOWS:
# an issue with asyncio executor and STIDIN, it cannot be closed gracefully
Expand Down
13 changes: 13 additions & 0 deletions platformio/debug/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2014-present PlatformIO <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
227 changes: 227 additions & 0 deletions platformio/debug/config/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# Copyright (c) 2014-present PlatformIO <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from platformio import fs, proc, util
from platformio.compat import string_types
from platformio.debug.exception import DebugInvalidOptionsError
from platformio.debug.helpers import reveal_debug_port
from platformio.project.config import ProjectConfig
from platformio.project.helpers import get_project_core_dir, load_project_ide_data
from platformio.project.options import ProjectOptions


class DebugConfigBase: # pylint: disable=too-many-instance-attributes
def __init__(self, platform, project_config, env_name):
self.platform = platform
self.project_config = project_config
self.env_name = env_name
self.env_options = project_config.items(env=env_name, as_dict=True)
self.board_config = platform.board_config(self.env_options["board"])
self.build_data = self._load_build_data()
self.tool_name = self.board_config.get_debug_tool_name(
self.env_options.get("debug_tool")
)
self.tool_settings = (
self.board_config.get("debug", {}).get("tools", {}).get(self.tool_name, {})
)

self._load_cmds = None
self._port = None

self.server = self._configure_server()

try:
platform.configure_debug_session(self)
except NotImplementedError:
pass

@staticmethod
def cleanup_cmds(items):
items = ProjectConfig.parse_multi_values(items)
return ["$LOAD_CMDS" if item == "$LOAD_CMD" else item for item in items]

@property
def program_path(self):
return self.build_data["prog_path"]

@property
def client_executable_path(self):
return self.build_data["gdb_path"]

@property
def load_cmds(self):
if self._load_cmds is not None:
return self._load_cmds
result = self.env_options.get("debug_load_cmds")
if not result:
result = self.tool_settings.get("load_cmds")
if not result:
# legacy
result = self.tool_settings.get("load_cmd")
if not result:
result = ProjectOptions["env.debug_load_cmds"].default
return self.cleanup_cmds(result)

@load_cmds.setter
def load_cmds(self, cmds):
self._load_cmds = cmds

@property
def load_mode(self):
result = self.env_options.get("debug_load_mode")
if not result:
result = self.tool_settings.get("load_mode")
return result or ProjectOptions["env.debug_load_mode"].default

@property
def init_break(self):
result = self.env_options.get("debug_init_break")
if not result:
result = self.tool_settings.get("init_break")
return result or ProjectOptions["env.debug_init_break"].default

@property
def init_cmds(self):
return self.env_options.get(
"debug_init_cmds", self.tool_settings.get("init_cmds")
)

@property
def extra_cmds(self):
return self.cleanup_cmds(
self.env_options.get("debug_extra_cmds")
) + self.cleanup_cmds(self.tool_settings.get("extra_cmds"))

@property
def port(self):
return reveal_debug_port(
self.env_options.get("debug_port", self.tool_settings.get("port"))
or self._port,
self.tool_name,
self.tool_settings,
)

@port.setter
def port(self, value):
self._port = value

@property
def upload_protocol(self):
return self.env_options.get(
"upload_protocol", self.board_config.get("upload", {}).get("protocol")
)

@property
def speed(self):
return self.env_options.get("debug_speed", self.tool_settings.get("speed"))

@property
def server_ready_pattern(self):
return (self.server or {}).get("ready_pattern")

def _load_build_data(self):
data = load_project_ide_data(self.project_config.path, self.env_name)
if data:
return data
raise DebugInvalidOptionsError("Could not load a build configuration")

def _configure_server(self):
result = None
# specific server per a system
if isinstance(self.tool_settings.get("server", {}), list):
for item in self.tool_settings["server"][:]:
self.tool_settings["server"] = item
if util.get_systype() in item.get("system", []):
break

# user overwrites debug server
if self.env_options.get("debug_server"):
result = {
"cwd": None,
"executable": None,
"arguments": self.env_options.get("debug_server"),
}
result["executable"] = result["arguments"][0]
result["arguments"] = result["arguments"][1:]
elif "server" in self.tool_settings:
result = self.tool_settings["server"]
server_package = result.get("package")
server_package_dir = (
self.platform.get_package_dir(server_package)
if server_package
else None
)
if server_package and not server_package_dir:
self.platform.install_packages(
with_packages=[server_package],
skip_default_package=True,
silent=True,
)
server_package_dir = self.platform.get_package_dir(server_package)
result.update(
dict(
cwd=server_package_dir if server_package else None,
executable=result.get("executable"),
arguments=[
a.replace("$PACKAGE_DIR", server_package_dir)
if server_package_dir
else a
for a in result.get("arguments", [])
],
)
)
return self.reveal_patterns(result) if result else None

def get_init_script(self, debugger):
try:
return getattr(self, "%s_INIT_SCRIPT" % debugger.upper())
except AttributeError:
raise NotImplementedError

def reveal_patterns(self, source, recursive=True):
patterns = {
"PLATFORMIO_CORE_DIR": get_project_core_dir(),
"PYTHONEXE": proc.get_pythonexe_path(),
"PROJECT_DIR": self.project_config.path,
"PROG_PATH": self.program_path,
"PROG_DIR": os.path.dirname(self.program_path),
"PROG_NAME": os.path.basename(os.path.splitext(self.program_path)[0]),
"DEBUG_PORT": self.port,
"UPLOAD_PROTOCOL": self.upload_protocol,
"INIT_BREAK": self.init_break or "",
"LOAD_CMDS": "\n".join(self.load_cmds 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)) and recursive:
source[key] = self.reveal_patterns(value, patterns)

return source
Loading

0 comments on commit dbb9998

Please sign in to comment.