Refactor Unified Debugger to native Python Asynchronous I/O stack // Resolve #3793 , Resolve #3595

This commit is contained in:
Ivan Kravets
2021-03-17 17:42:11 +02:00
parent edf724d20d
commit b54a8b40a4
11 changed files with 306 additions and 252 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View 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

View File

@ -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

View File

@ -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

View File

@ -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