mirror of
https://github.com/platformio/platformio-core.git
synced 2025-07-30 10:07:14 +02:00
This commit is contained in:
@ -12,20 +12,20 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-statements
|
# pylint: disable=too-many-arguments, too-many-locals
|
||||||
# pylint: disable=too-many-locals, too-many-branches
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
from os.path import isfile
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from platformio import app, exception, fs, proc
|
from platformio import app, exception, fs, proc
|
||||||
from platformio.commands.debug import helpers
|
|
||||||
from platformio.commands.debug.exception import DebugInvalidOptionsError
|
|
||||||
from platformio.commands.platform import platform_install as cmd_platform_install
|
from platformio.commands.platform import platform_install as cmd_platform_install
|
||||||
from platformio.package.manager.core import inject_contrib_pysite
|
from platformio.compat import WINDOWS
|
||||||
|
from platformio.debug import helpers
|
||||||
|
from platformio.debug.exception import DebugInvalidOptionsError
|
||||||
|
from platformio.debug.process.client import DebugClientProcess
|
||||||
from platformio.platform.exception import UnknownPlatform
|
from platformio.platform.exception import UnknownPlatform
|
||||||
from platformio.platform.factory import PlatformFactory
|
from platformio.platform.factory import PlatformFactory
|
||||||
from platformio.project.config import ProjectConfig
|
from platformio.project.config import ProjectConfig
|
||||||
@ -131,7 +131,7 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro
|
|||||||
ide_data["prog_path"]
|
ide_data["prog_path"]
|
||||||
) or not helpers.has_debug_symbols(ide_data["prog_path"])
|
) or not helpers.has_debug_symbols(ide_data["prog_path"])
|
||||||
else:
|
else:
|
||||||
rebuild_prog = not isfile(ide_data["prog_path"])
|
rebuild_prog = not os.path.isfile(ide_data["prog_path"])
|
||||||
|
|
||||||
if preload or (not rebuild_prog and load_mode != "always"):
|
if preload or (not rebuild_prog and load_mode != "always"):
|
||||||
# don't load firmware through debug server
|
# don't load firmware through debug server
|
||||||
@ -157,19 +157,17 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro
|
|||||||
if load_mode == "modified":
|
if load_mode == "modified":
|
||||||
helpers.is_prog_obsolete(ide_data["prog_path"])
|
helpers.is_prog_obsolete(ide_data["prog_path"])
|
||||||
|
|
||||||
if not isfile(ide_data["prog_path"]):
|
if not os.path.isfile(ide_data["prog_path"]):
|
||||||
raise DebugInvalidOptionsError("Program/firmware is missed")
|
raise DebugInvalidOptionsError("Program/firmware is missed")
|
||||||
|
|
||||||
# run debugging client
|
loop = asyncio.ProactorEventLoop() if WINDOWS else asyncio.get_event_loop()
|
||||||
inject_contrib_pysite()
|
asyncio.set_event_loop(loop)
|
||||||
|
client = DebugClientProcess(project_dir, __unprocessed, debug_options, env_options)
|
||||||
# pylint: disable=import-outside-toplevel
|
coro = client.run(ide_data["gdb_path"], ide_data["prog_path"])
|
||||||
from platformio.commands.debug.process.client import GDBClient, reactor
|
loop.run_until_complete(coro)
|
||||||
|
if WINDOWS:
|
||||||
client = GDBClient(project_dir, __unprocessed, debug_options, env_options)
|
# an issue with asyncio executor and STIDIN, it cannot be closed gracefully
|
||||||
client.spawn(ide_data["gdb_path"], ide_data["prog_path"])
|
os._exit(0) # pylint: disable=protected-access
|
||||||
|
loop.close()
|
||||||
signal.signal(signal.SIGINT, lambda *args, **kwargs: None)
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
return True
|
return True
|
@ -1,93 +0,0 @@
|
|||||||
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
|
||||||
#
|
|
||||||
# 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 signal
|
|
||||||
import time
|
|
||||||
|
|
||||||
import click
|
|
||||||
from twisted.internet import protocol # pylint: disable=import-error
|
|
||||||
|
|
||||||
from platformio import fs
|
|
||||||
from platformio.compat import string_types
|
|
||||||
from platformio.proc import get_pythonexe_path
|
|
||||||
from platformio.project.helpers import get_project_core_dir
|
|
||||||
|
|
||||||
|
|
||||||
class BaseProcess(protocol.ProcessProtocol, object):
|
|
||||||
|
|
||||||
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._last_activity = 0
|
|
||||||
|
|
||||||
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 onStdInData(self, data):
|
|
||||||
self._last_activity = time.time()
|
|
||||||
if self.LOG_FILE:
|
|
||||||
with open(self.LOG_FILE, "ab") as fp:
|
|
||||||
fp.write(data)
|
|
||||||
|
|
||||||
def outReceived(self, data):
|
|
||||||
self._last_activity = time.time()
|
|
||||||
if self.LOG_FILE:
|
|
||||||
with open(self.LOG_FILE, "ab") as fp:
|
|
||||||
fp.write(data)
|
|
||||||
while data:
|
|
||||||
chunk = data[: self.STDOUT_CHUNK_SIZE]
|
|
||||||
click.echo(chunk, nl=False)
|
|
||||||
data = data[self.STDOUT_CHUNK_SIZE :]
|
|
||||||
|
|
||||||
def errReceived(self, data):
|
|
||||||
self._last_activity = time.time()
|
|
||||||
if self.LOG_FILE:
|
|
||||||
with open(self.LOG_FILE, "ab") as fp:
|
|
||||||
fp.write(data)
|
|
||||||
click.echo(data, nl=False, err=True)
|
|
||||||
|
|
||||||
def processEnded(self, _):
|
|
||||||
self._last_activity = time.time()
|
|
||||||
# Allow terminating via SIGINT/CTRL+C
|
|
||||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
@ -22,9 +22,9 @@ from os.path import isfile
|
|||||||
|
|
||||||
from platformio import fs, util
|
from platformio import fs, util
|
||||||
from platformio.commands import PlatformioCLI
|
from platformio.commands import PlatformioCLI
|
||||||
from platformio.commands.debug.exception import DebugInvalidOptionsError
|
|
||||||
from platformio.commands.run.command import cli as cmd_run
|
from platformio.commands.run.command import cli as cmd_run
|
||||||
from platformio.compat import is_bytes
|
from platformio.compat import is_bytes
|
||||||
|
from platformio.debug.exception import DebugInvalidOptionsError
|
||||||
from platformio.project.config import ProjectConfig
|
from platformio.project.config import ProjectConfig
|
||||||
from platformio.project.options import ProjectOptions
|
from platformio.project.options import ProjectOptions
|
||||||
|
|
189
platformio/debug/process/base.py
Normal file
189
platformio/debug/process/base.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
|
||||||
|
#
|
||||||
|
# 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 asyncio
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from platformio import fs
|
||||||
|
from platformio.compat import (
|
||||||
|
create_task,
|
||||||
|
get_locale_encoding,
|
||||||
|
get_running_loop,
|
||||||
|
string_types,
|
||||||
|
)
|
||||||
|
from platformio.proc import get_pythonexe_path
|
||||||
|
from platformio.project.helpers import get_project_core_dir
|
||||||
|
|
||||||
|
|
||||||
|
class DebugSubprocessProtocol(asyncio.SubprocessProtocol):
|
||||||
|
def __init__(self, factory):
|
||||||
|
self.factory = factory
|
||||||
|
self._is_exited = False
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
self.factory.connection_made(transport)
|
||||||
|
|
||||||
|
def pipe_data_received(self, fd, data):
|
||||||
|
pipe_to_cb = [
|
||||||
|
self.factory.stdin_data_received,
|
||||||
|
self.factory.stdout_data_received,
|
||||||
|
self.factory.stderr_data_received,
|
||||||
|
]
|
||||||
|
pipe_to_cb[fd](data)
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
self.process_exited()
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
if self._is_exited:
|
||||||
|
return
|
||||||
|
self.factory.process_exited()
|
||||||
|
self._is_exited = True
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
self._last_activity = 0
|
||||||
|
self._exit_future = None
|
||||||
|
self._stdin_read_task = None
|
||||||
|
self._std_encoding = get_locale_encoding()
|
||||||
|
|
||||||
|
async def spawn(self, *args, **kwargs):
|
||||||
|
wait_until_exit = False
|
||||||
|
if "wait_until_exit" in kwargs:
|
||||||
|
wait_until_exit = kwargs["wait_until_exit"]
|
||||||
|
del kwargs["wait_until_exit"]
|
||||||
|
for pipe in ("stdin", "stdout", "stderr"):
|
||||||
|
if pipe not in kwargs:
|
||||||
|
kwargs[pipe] = subprocess.PIPE
|
||||||
|
loop = get_running_loop()
|
||||||
|
await loop.subprocess_exec(
|
||||||
|
lambda: DebugSubprocessProtocol(self), *args, **kwargs
|
||||||
|
)
|
||||||
|
if wait_until_exit:
|
||||||
|
self._exit_future = loop.create_future()
|
||||||
|
await self._exit_future
|
||||||
|
|
||||||
|
def is_running(self):
|
||||||
|
return self._is_running
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
self._is_running = True
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
|
def connect_stdin_pipe(self):
|
||||||
|
self._stdin_read_task = create_task(self._read_stdin_pipe())
|
||||||
|
|
||||||
|
async def _read_stdin_pipe(self):
|
||||||
|
loop = get_running_loop()
|
||||||
|
try:
|
||||||
|
loop.add_reader(
|
||||||
|
sys.stdin.fileno(),
|
||||||
|
lambda: self.stdin_data_received(sys.stdin.buffer.readline()),
|
||||||
|
)
|
||||||
|
except NotImplementedError:
|
||||||
|
while True:
|
||||||
|
self.stdin_data_received(
|
||||||
|
await loop.run_in_executor(None, sys.stdin.buffer.readline)
|
||||||
|
)
|
||||||
|
|
||||||
|
def stdin_data_received(self, data):
|
||||||
|
self._last_activity = time.time()
|
||||||
|
if self.LOG_FILE:
|
||||||
|
with open(self.LOG_FILE, "ab") as fp:
|
||||||
|
fp.write(data)
|
||||||
|
|
||||||
|
def stdout_data_received(self, data):
|
||||||
|
self._last_activity = time.time()
|
||||||
|
if self.LOG_FILE:
|
||||||
|
with open(self.LOG_FILE, "ab") as fp:
|
||||||
|
fp.write(data)
|
||||||
|
while data:
|
||||||
|
chunk = data[: self.STDOUT_CHUNK_SIZE]
|
||||||
|
print(chunk.decode(self._std_encoding, "replace"), end="", flush=True)
|
||||||
|
data = data[self.STDOUT_CHUNK_SIZE :]
|
||||||
|
|
||||||
|
def stderr_data_received(self, data):
|
||||||
|
self._last_activity = time.time()
|
||||||
|
if self.LOG_FILE:
|
||||||
|
with open(self.LOG_FILE, "ab") as fp:
|
||||||
|
fp.write(data)
|
||||||
|
print(
|
||||||
|
data.decode(self._std_encoding, "replace"),
|
||||||
|
end="",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
self._is_running = False
|
||||||
|
self._last_activity = time.time()
|
||||||
|
# Allow terminating via SIGINT/CTRL+C
|
||||||
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||||
|
if self._stdin_read_task:
|
||||||
|
self._stdin_read_task.cancel()
|
||||||
|
self._stdin_read_task = None
|
||||||
|
if self._exit_future:
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
self.transport.kill()
|
||||||
|
self.transport.close()
|
||||||
|
except: # pylint: disable=bare-except
|
||||||
|
pass
|
@ -12,80 +12,75 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from hashlib import sha1
|
|
||||||
from os.path import basename, dirname, isdir, join, realpath, splitext
|
|
||||||
from tempfile import mkdtemp
|
|
||||||
|
|
||||||
from twisted.internet import defer # pylint: disable=import-error
|
|
||||||
from twisted.internet import protocol # pylint: disable=import-error
|
|
||||||
from twisted.internet import reactor # pylint: disable=import-error
|
|
||||||
from twisted.internet import stdio # pylint: disable=import-error
|
|
||||||
from twisted.internet import task # pylint: disable=import-error
|
|
||||||
|
|
||||||
from platformio import fs, proc, telemetry, util
|
from platformio import fs, proc, telemetry, util
|
||||||
from platformio.cache import ContentCache
|
from platformio.cache import ContentCache
|
||||||
from platformio.commands.debug import helpers
|
from platformio.compat import get_running_loop, hashlib_encode_data, is_bytes
|
||||||
from platformio.commands.debug.exception import DebugInvalidOptionsError
|
from platformio.debug import helpers
|
||||||
from platformio.commands.debug.initcfgs import get_gdb_init_config
|
from platformio.debug.exception import DebugInvalidOptionsError
|
||||||
from platformio.commands.debug.process.base import BaseProcess
|
from platformio.debug.initcfgs import get_gdb_init_config
|
||||||
from platformio.commands.debug.process.server import DebugServer
|
from platformio.debug.process.base import DebugBaseProcess
|
||||||
from platformio.compat import hashlib_encode_data, is_bytes
|
from platformio.debug.process.server import DebugServerProcess
|
||||||
from platformio.project.helpers import get_project_cache_dir
|
from platformio.project.helpers import get_project_cache_dir
|
||||||
|
|
||||||
|
|
||||||
class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes
|
class DebugClientProcess(
|
||||||
|
DebugBaseProcess
|
||||||
|
): # pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
PIO_SRC_NAME = ".pioinit"
|
PIO_SRC_NAME = ".pioinit"
|
||||||
INIT_COMPLETED_BANNER = "PlatformIO: Initialization completed"
|
INIT_COMPLETED_BANNER = "PlatformIO: Initialization completed"
|
||||||
|
|
||||||
def __init__(self, project_dir, args, debug_options, env_options):
|
def __init__(self, project_dir, args, debug_options, env_options):
|
||||||
super(GDBClient, self).__init__()
|
super(DebugClientProcess, self).__init__()
|
||||||
self.project_dir = project_dir
|
self.project_dir = project_dir
|
||||||
self.args = list(args)
|
self.args = list(args)
|
||||||
self.debug_options = debug_options
|
self.debug_options = debug_options
|
||||||
self.env_options = env_options
|
self.env_options = env_options
|
||||||
|
|
||||||
self._debug_server = DebugServer(debug_options, env_options)
|
self._server_process = DebugServerProcess(debug_options, env_options)
|
||||||
self._session_id = None
|
self._session_id = None
|
||||||
|
|
||||||
if not isdir(get_project_cache_dir()):
|
if not os.path.isdir(get_project_cache_dir()):
|
||||||
os.makedirs(get_project_cache_dir())
|
os.makedirs(get_project_cache_dir())
|
||||||
self._gdbsrc_dir = mkdtemp(dir=get_project_cache_dir(), prefix=".piodebug-")
|
self._gdbsrc_dir = tempfile.mkdtemp(
|
||||||
|
dir=get_project_cache_dir(), prefix=".piodebug-"
|
||||||
|
)
|
||||||
|
|
||||||
self._target_is_run = False
|
self._target_is_running = False
|
||||||
self._auto_continue_timer = None
|
|
||||||
self._errors_buffer = b""
|
self._errors_buffer = b""
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
async def run(self, gdb_path, prog_path):
|
||||||
def spawn(self, gdb_path, prog_path):
|
|
||||||
session_hash = gdb_path + prog_path
|
session_hash = gdb_path + prog_path
|
||||||
self._session_id = sha1(hashlib_encode_data(session_hash)).hexdigest()
|
self._session_id = hashlib.sha1(hashlib_encode_data(session_hash)).hexdigest()
|
||||||
self._kill_previous_session()
|
self._kill_previous_session()
|
||||||
|
|
||||||
patterns = {
|
patterns = {
|
||||||
"PROJECT_DIR": self.project_dir,
|
"PROJECT_DIR": self.project_dir,
|
||||||
"PROG_PATH": prog_path,
|
"PROG_PATH": prog_path,
|
||||||
"PROG_DIR": dirname(prog_path),
|
"PROG_DIR": os.path.dirname(prog_path),
|
||||||
"PROG_NAME": basename(splitext(prog_path)[0]),
|
"PROG_NAME": os.path.basename(os.path.splitext(prog_path)[0]),
|
||||||
"DEBUG_PORT": self.debug_options["port"],
|
"DEBUG_PORT": self.debug_options["port"],
|
||||||
"UPLOAD_PROTOCOL": self.debug_options["upload_protocol"],
|
"UPLOAD_PROTOCOL": self.debug_options["upload_protocol"],
|
||||||
"INIT_BREAK": self.debug_options["init_break"] or "",
|
"INIT_BREAK": self.debug_options["init_break"] or "",
|
||||||
"LOAD_CMDS": "\n".join(self.debug_options["load_cmds"] or []),
|
"LOAD_CMDS": "\n".join(self.debug_options["load_cmds"] or []),
|
||||||
}
|
}
|
||||||
|
|
||||||
yield self._debug_server.spawn(patterns)
|
await self._server_process.run(patterns)
|
||||||
if not patterns["DEBUG_PORT"]:
|
if not patterns["DEBUG_PORT"]:
|
||||||
patterns["DEBUG_PORT"] = self._debug_server.get_debug_port()
|
patterns["DEBUG_PORT"] = self._server_process.get_debug_port()
|
||||||
|
|
||||||
self.generate_pioinit(self._gdbsrc_dir, patterns)
|
self.generate_pioinit(self._gdbsrc_dir, patterns)
|
||||||
|
|
||||||
# start GDB client
|
# start GDB client
|
||||||
args = [
|
args = [
|
||||||
"piogdb",
|
gdb_path,
|
||||||
"-q",
|
"-q",
|
||||||
"--directory",
|
"--directory",
|
||||||
self._gdbsrc_dir,
|
self._gdbsrc_dir,
|
||||||
@ -101,18 +96,16 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes
|
|||||||
if gdb_data_dir:
|
if gdb_data_dir:
|
||||||
args.extend(["--data-directory", gdb_data_dir])
|
args.extend(["--data-directory", gdb_data_dir])
|
||||||
args.append(patterns["PROG_PATH"])
|
args.append(patterns["PROG_PATH"])
|
||||||
|
await self.spawn(*args, cwd=self.project_dir, wait_until_exit=True)
|
||||||
transport = reactor.spawnProcess(
|
|
||||||
self, gdb_path, args, path=self.project_dir, env=os.environ
|
|
||||||
)
|
|
||||||
defer.returnValue(transport)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_data_dir(gdb_path):
|
def _get_data_dir(gdb_path):
|
||||||
if "msp430" in gdb_path:
|
if "msp430" in gdb_path:
|
||||||
return None
|
return None
|
||||||
gdb_data_dir = realpath(join(dirname(gdb_path), "..", "share", "gdb"))
|
gdb_data_dir = os.path.realpath(
|
||||||
return gdb_data_dir if isdir(gdb_data_dir) else None
|
os.path.join(os.path.dirname(gdb_path), "..", "share", "gdb")
|
||||||
|
)
|
||||||
|
return gdb_data_dir if os.path.isdir(gdb_data_dir) else None
|
||||||
|
|
||||||
def generate_pioinit(self, dst_dir, patterns):
|
def generate_pioinit(self, dst_dir, patterns):
|
||||||
# default GDB init commands depending on debug tool
|
# default GDB init commands depending on debug tool
|
||||||
@ -153,72 +146,57 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes
|
|||||||
footer = ["echo %s\\n" % self.INIT_COMPLETED_BANNER]
|
footer = ["echo %s\\n" % self.INIT_COMPLETED_BANNER]
|
||||||
commands = banner + commands + footer
|
commands = banner + commands + footer
|
||||||
|
|
||||||
with open(join(dst_dir, self.PIO_SRC_NAME), "w") as fp:
|
with open(os.path.join(dst_dir, self.PIO_SRC_NAME), "w") as fp:
|
||||||
fp.write("\n".join(self.apply_patterns(commands, patterns)))
|
fp.write("\n".join(self.apply_patterns(commands, patterns)))
|
||||||
|
|
||||||
def connectionMade(self):
|
def connection_made(self, transport):
|
||||||
self._lock_session(self.transport.pid)
|
super(DebugClientProcess, self).connection_made(transport)
|
||||||
|
self._lock_session(transport.get_pid())
|
||||||
|
# Disable SIGINT and allow GDB's Ctrl+C interrupt
|
||||||
|
signal.signal(signal.SIGINT, lambda *args, **kwargs: None)
|
||||||
|
self.connect_stdin_pipe()
|
||||||
|
|
||||||
p = protocol.Protocol()
|
def stdin_data_received(self, data):
|
||||||
p.dataReceived = self.onStdInData
|
super(DebugClientProcess, self).stdin_data_received(data)
|
||||||
stdio.StandardIO(p)
|
|
||||||
|
|
||||||
def onStdInData(self, data):
|
|
||||||
super(GDBClient, self).onStdInData(data)
|
|
||||||
if b"-exec-run" in data:
|
if b"-exec-run" in data:
|
||||||
if self._target_is_run:
|
if self._target_is_running:
|
||||||
token, _ = data.split(b"-", 1)
|
token, _ = data.split(b"-", 1)
|
||||||
self.outReceived(token + b"^running\n")
|
self.stdout_data_received(token + b"^running\n")
|
||||||
return
|
return
|
||||||
data = data.replace(b"-exec-run", b"-exec-continue")
|
data = data.replace(b"-exec-run", b"-exec-continue")
|
||||||
|
|
||||||
if b"-exec-continue" in data:
|
if b"-exec-continue" in data:
|
||||||
self._target_is_run = True
|
self._target_is_running = True
|
||||||
if b"-gdb-exit" in data or data.strip() in (b"q", b"quit"):
|
if b"-gdb-exit" in data or data.strip() in (b"q", b"quit"):
|
||||||
# Allow terminating via SIGINT/CTRL+C
|
# Allow terminating via SIGINT/CTRL+C
|
||||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||||
self.transport.write(b"pio_reset_run_target\n")
|
self.transport.get_pipe_transport(0).write(b"pio_reset_run_target\n")
|
||||||
self.transport.write(data)
|
self.transport.get_pipe_transport(0).write(data)
|
||||||
|
|
||||||
def processEnded(self, reason): # pylint: disable=unused-argument
|
def stdout_data_received(self, data):
|
||||||
self._unlock_session()
|
super(DebugClientProcess, self).stdout_data_received(data)
|
||||||
if self._gdbsrc_dir and isdir(self._gdbsrc_dir):
|
|
||||||
fs.rmtree(self._gdbsrc_dir)
|
|
||||||
if self._debug_server:
|
|
||||||
self._debug_server.terminate()
|
|
||||||
|
|
||||||
reactor.stop()
|
|
||||||
|
|
||||||
def outReceived(self, data):
|
|
||||||
super(GDBClient, self).outReceived(data)
|
|
||||||
self._handle_error(data)
|
self._handle_error(data)
|
||||||
# go to init break automatically
|
# go to init break automatically
|
||||||
if self.INIT_COMPLETED_BANNER.encode() in data:
|
if self.INIT_COMPLETED_BANNER.encode() in data:
|
||||||
telemetry.send_event(
|
telemetry.send_event(
|
||||||
"Debug", "Started", telemetry.dump_run_environment(self.env_options)
|
"Debug", "Started", telemetry.dump_run_environment(self.env_options)
|
||||||
)
|
)
|
||||||
self._auto_continue_timer = task.LoopingCall(self._auto_exec_continue)
|
self._auto_exec_continue()
|
||||||
self._auto_continue_timer.start(0.1)
|
|
||||||
|
|
||||||
def errReceived(self, data):
|
|
||||||
super(GDBClient, self).errReceived(data)
|
|
||||||
self._handle_error(data)
|
|
||||||
|
|
||||||
def console_log(self, msg):
|
def console_log(self, msg):
|
||||||
if helpers.is_gdbmi_mode():
|
if helpers.is_gdbmi_mode():
|
||||||
msg = helpers.escape_gdbmi_stream("~", msg)
|
msg = helpers.escape_gdbmi_stream("~", msg)
|
||||||
self.outReceived(msg if is_bytes(msg) else msg.encode())
|
self.stdout_data_received(msg if is_bytes(msg) else msg.encode())
|
||||||
|
|
||||||
def _auto_exec_continue(self):
|
def _auto_exec_continue(self):
|
||||||
auto_exec_delay = 0.5 # in seconds
|
auto_exec_delay = 0.5 # in seconds
|
||||||
if self._last_activity > (time.time() - auto_exec_delay):
|
if self._last_activity > (time.time() - auto_exec_delay):
|
||||||
|
get_running_loop().call_later(0.1, self._auto_exec_continue)
|
||||||
return
|
return
|
||||||
if self._auto_continue_timer:
|
|
||||||
self._auto_continue_timer.stop()
|
|
||||||
self._auto_continue_timer = None
|
|
||||||
|
|
||||||
if not self.debug_options["init_break"] or self._target_is_run:
|
if not self.debug_options["init_break"] or self._target_is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.console_log(
|
self.console_log(
|
||||||
"PlatformIO: Resume the execution to `debug_init_break = %s`\n"
|
"PlatformIO: Resume the execution to `debug_init_break = %s`\n"
|
||||||
% self.debug_options["init_break"]
|
% self.debug_options["init_break"]
|
||||||
@ -226,10 +204,14 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes
|
|||||||
self.console_log(
|
self.console_log(
|
||||||
"PlatformIO: More configuration options -> http://bit.ly/pio-debug\n"
|
"PlatformIO: More configuration options -> http://bit.ly/pio-debug\n"
|
||||||
)
|
)
|
||||||
self.transport.write(
|
self.transport.get_pipe_transport(0).write(
|
||||||
b"0-exec-continue\n" if helpers.is_gdbmi_mode() else b"continue\n"
|
b"0-exec-continue\n" if helpers.is_gdbmi_mode() else b"continue\n"
|
||||||
)
|
)
|
||||||
self._target_is_run = True
|
self._target_is_running = True
|
||||||
|
|
||||||
|
def stderr_data_received(self, data):
|
||||||
|
super(DebugClientProcess, self).stderr_data_received(data)
|
||||||
|
self._handle_error(data)
|
||||||
|
|
||||||
def _handle_error(self, data):
|
def _handle_error(self, data):
|
||||||
self._errors_buffer = (self._errors_buffer + data)[-8192:] # keep last 8 KBytes
|
self._errors_buffer = (self._errors_buffer + data)[-8192:] # keep last 8 KBytes
|
||||||
@ -248,7 +230,15 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes
|
|||||||
last_erros,
|
last_erros,
|
||||||
)
|
)
|
||||||
telemetry.send_exception("DebugInitError: %s" % err)
|
telemetry.send_exception("DebugInitError: %s" % err)
|
||||||
self.transport.loseConnection()
|
self.transport.close()
|
||||||
|
|
||||||
|
def process_exited(self):
|
||||||
|
self._unlock_session()
|
||||||
|
if self._gdbsrc_dir and os.path.isdir(self._gdbsrc_dir):
|
||||||
|
fs.rmtree(self._gdbsrc_dir)
|
||||||
|
if self._server_process:
|
||||||
|
self._server_process.terminate()
|
||||||
|
super(DebugClientProcess, self).process_exited()
|
||||||
|
|
||||||
def _kill_previous_session(self):
|
def _kill_previous_session(self):
|
||||||
assert self._session_id
|
assert self._session_id
|
@ -12,53 +12,47 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from os.path import isdir, isfile, join
|
|
||||||
|
|
||||||
from twisted.internet import defer # pylint: disable=import-error
|
|
||||||
from twisted.internet import reactor # pylint: disable=import-error
|
|
||||||
|
|
||||||
from platformio import fs, util
|
from platformio import fs, util
|
||||||
from platformio.commands.debug.exception import DebugInvalidOptionsError
|
from platformio.debug.exception import DebugInvalidOptionsError
|
||||||
from platformio.commands.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode
|
from platformio.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode
|
||||||
from platformio.commands.debug.process.base import BaseProcess
|
from platformio.debug.process.base import DebugBaseProcess
|
||||||
from platformio.proc import where_is_program
|
from platformio.proc import where_is_program
|
||||||
|
|
||||||
|
|
||||||
class DebugServer(BaseProcess):
|
class DebugServerProcess(DebugBaseProcess):
|
||||||
def __init__(self, debug_options, env_options):
|
def __init__(self, debug_options, env_options):
|
||||||
super(DebugServer, self).__init__()
|
super(DebugServerProcess, self).__init__()
|
||||||
self.debug_options = debug_options
|
self.debug_options = debug_options
|
||||||
self.env_options = env_options
|
self.env_options = env_options
|
||||||
|
|
||||||
self._debug_port = ":3333"
|
self._debug_port = ":3333"
|
||||||
self._transport = None
|
|
||||||
self._process_ended = False
|
|
||||||
self._ready = False
|
self._ready = False
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
async def run(self, patterns): # pylint: disable=too-many-branches
|
||||||
def spawn(self, patterns): # pylint: disable=too-many-branches
|
|
||||||
systype = util.get_systype()
|
systype = util.get_systype()
|
||||||
server = self.debug_options.get("server")
|
server = self.debug_options.get("server")
|
||||||
if not server:
|
if not server:
|
||||||
defer.returnValue(None)
|
return None
|
||||||
server = self.apply_patterns(server, patterns)
|
server = self.apply_patterns(server, patterns)
|
||||||
server_executable = server["executable"]
|
server_executable = server["executable"]
|
||||||
if not server_executable:
|
if not server_executable:
|
||||||
defer.returnValue(None)
|
return None
|
||||||
if server["cwd"]:
|
if server["cwd"]:
|
||||||
server_executable = join(server["cwd"], server_executable)
|
server_executable = os.path.join(server["cwd"], server_executable)
|
||||||
if (
|
if (
|
||||||
"windows" in systype
|
"windows" in systype
|
||||||
and not server_executable.endswith(".exe")
|
and not server_executable.endswith(".exe")
|
||||||
and isfile(server_executable + ".exe")
|
and os.path.isfile(server_executable + ".exe")
|
||||||
):
|
):
|
||||||
server_executable = server_executable + ".exe"
|
server_executable = server_executable + ".exe"
|
||||||
|
|
||||||
if not isfile(server_executable):
|
if not os.path.isfile(server_executable):
|
||||||
server_executable = where_is_program(server_executable)
|
server_executable = where_is_program(server_executable)
|
||||||
if not isfile(server_executable):
|
if not os.path.isfile(server_executable):
|
||||||
raise DebugInvalidOptionsError(
|
raise DebugInvalidOptionsError(
|
||||||
"\nCould not launch Debug Server '%s'. Please check that it "
|
"\nCould not launch Debug Server '%s'. Please check that it "
|
||||||
"is installed and is included in a system PATH\n\n"
|
"is installed and is included in a system PATH\n\n"
|
||||||
@ -70,6 +64,7 @@ class DebugServer(BaseProcess):
|
|||||||
openocd_pipe_allowed = all(
|
openocd_pipe_allowed = all(
|
||||||
[not self.debug_options["port"], "openocd" in server_executable]
|
[not self.debug_options["port"], "openocd" in server_executable]
|
||||||
)
|
)
|
||||||
|
openocd_pipe_allowed = False
|
||||||
if openocd_pipe_allowed:
|
if openocd_pipe_allowed:
|
||||||
args = []
|
args = []
|
||||||
if server["cwd"]:
|
if server["cwd"]:
|
||||||
@ -83,34 +78,31 @@ class DebugServer(BaseProcess):
|
|||||||
)
|
)
|
||||||
self._debug_port = '| "%s" %s' % (server_executable, str_args)
|
self._debug_port = '| "%s" %s' % (server_executable, str_args)
|
||||||
self._debug_port = fs.to_unix_path(self._debug_port)
|
self._debug_port = fs.to_unix_path(self._debug_port)
|
||||||
defer.returnValue(self._debug_port)
|
return self._debug_port
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
# prepend server "lib" folder to LD path
|
# prepend server "lib" folder to LD path
|
||||||
if (
|
if (
|
||||||
"windows" not in systype
|
"windows" not in systype
|
||||||
and server["cwd"]
|
and server["cwd"]
|
||||||
and isdir(join(server["cwd"], "lib"))
|
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 "darwin" in systype else "LD_LIBRARY_PATH"
|
||||||
env[ld_key] = join(server["cwd"], "lib")
|
env[ld_key] = os.path.join(server["cwd"], "lib")
|
||||||
if os.environ.get(ld_key):
|
if os.environ.get(ld_key):
|
||||||
env[ld_key] = "%s:%s" % (env[ld_key], os.environ.get(ld_key))
|
env[ld_key] = "%s:%s" % (env[ld_key], os.environ.get(ld_key))
|
||||||
# prepend BIN to PATH
|
# prepend BIN to PATH
|
||||||
if server["cwd"] and isdir(join(server["cwd"], "bin")):
|
if server["cwd"] and os.path.isdir(os.path.join(server["cwd"], "bin")):
|
||||||
env["PATH"] = "%s%s%s" % (
|
env["PATH"] = "%s%s%s" % (
|
||||||
join(server["cwd"], "bin"),
|
os.path.join(server["cwd"], "bin"),
|
||||||
os.pathsep,
|
os.pathsep,
|
||||||
os.environ.get("PATH", os.environ.get("Path", "")),
|
os.environ.get("PATH", os.environ.get("Path", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._transport = reactor.spawnProcess(
|
await self.spawn(
|
||||||
self,
|
*([server_executable] + server["arguments"]), cwd=server["cwd"], env=env
|
||||||
server_executable,
|
|
||||||
[server_executable] + server["arguments"],
|
|
||||||
path=server["cwd"],
|
|
||||||
env=env,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if "mspdebug" in server_executable.lower():
|
if "mspdebug" in server_executable.lower():
|
||||||
self._debug_port = ":2000"
|
self._debug_port = ":2000"
|
||||||
elif "jlink" in server_executable.lower():
|
elif "jlink" in server_executable.lower():
|
||||||
@ -118,19 +110,18 @@ class DebugServer(BaseProcess):
|
|||||||
elif "qemu" in server_executable.lower():
|
elif "qemu" in server_executable.lower():
|
||||||
self._debug_port = ":1234"
|
self._debug_port = ":1234"
|
||||||
|
|
||||||
yield self._wait_until_ready()
|
await self._wait_until_ready()
|
||||||
|
|
||||||
defer.returnValue(self._debug_port)
|
return self._debug_port
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
async def _wait_until_ready(self):
|
||||||
def _wait_until_ready(self):
|
|
||||||
ready_pattern = self.debug_options.get("server", {}).get("ready_pattern")
|
ready_pattern = self.debug_options.get("server", {}).get("ready_pattern")
|
||||||
timeout = 60 if ready_pattern else 10
|
timeout = 60 if ready_pattern else 10
|
||||||
elapsed = 0
|
elapsed = 0
|
||||||
delay = 0.5
|
delay = 0.5
|
||||||
auto_ready_delay = 0.5
|
auto_ready_delay = 0.5
|
||||||
while not self._ready and not self._process_ended and elapsed < timeout:
|
while not self._ready and self.is_running() and elapsed < timeout:
|
||||||
yield self.async_sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
if not ready_pattern:
|
if not ready_pattern:
|
||||||
self._ready = self._last_activity < (time.time() - auto_ready_delay)
|
self._ready = self._last_activity < (time.time() - auto_ready_delay)
|
||||||
elapsed += delay
|
elapsed += delay
|
||||||
@ -143,33 +134,15 @@ class DebugServer(BaseProcess):
|
|||||||
self._ready = ready_pattern.encode() in data
|
self._ready = ready_pattern.encode() in data
|
||||||
return self._ready
|
return self._ready
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def async_sleep(secs):
|
|
||||||
d = defer.Deferred()
|
|
||||||
reactor.callLater(secs, d.callback, None)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def get_debug_port(self):
|
def get_debug_port(self):
|
||||||
return self._debug_port
|
return self._debug_port
|
||||||
|
|
||||||
def outReceived(self, data):
|
def stdout_data_received(self, data):
|
||||||
super(DebugServer, self).outReceived(
|
super(DebugServerProcess, self).stdout_data_received(
|
||||||
escape_gdbmi_stream("@", data) if is_gdbmi_mode() else data
|
escape_gdbmi_stream("@", data) if is_gdbmi_mode() else data
|
||||||
)
|
)
|
||||||
self._check_ready_by_pattern(data)
|
self._check_ready_by_pattern(data)
|
||||||
|
|
||||||
def errReceived(self, data):
|
def stderr_data_received(self, data):
|
||||||
super(DebugServer, self).errReceived(data)
|
super(DebugServerProcess, self).stderr_data_received(data)
|
||||||
self._check_ready_by_pattern(data)
|
self._check_ready_by_pattern(data)
|
||||||
|
|
||||||
def processEnded(self, reason):
|
|
||||||
self._process_ended = True
|
|
||||||
super(DebugServer, self).processEnded(reason)
|
|
||||||
|
|
||||||
def terminate(self):
|
|
||||||
if self._process_ended or not self._transport:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._transport.signalProcess("KILL")
|
|
||||||
except: # pylint: disable=bare-except
|
|
||||||
pass
|
|
@ -15,11 +15,8 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from platformio import fs, telemetry, util
|
from platformio import fs, telemetry, util
|
||||||
from platformio.commands.debug.exception import (
|
|
||||||
DebugInvalidOptionsError,
|
|
||||||
DebugSupportError,
|
|
||||||
)
|
|
||||||
from platformio.compat import PY2
|
from platformio.compat import PY2
|
||||||
|
from platformio.debug.exception import DebugInvalidOptionsError, DebugSupportError
|
||||||
from platformio.exception import UserSideException
|
from platformio.exception import UserSideException
|
||||||
from platformio.platform.exception import InvalidBoardManifest
|
from platformio.platform.exception import InvalidBoardManifest
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user