diff --git a/platformio/commands/debug/command.py b/platformio/commands/debug.py similarity index 86% rename from platformio/commands/debug/command.py rename to platformio/commands/debug.py index 2ff96932..85e44116 100644 --- a/platformio/commands/debug/command.py +++ b/platformio/commands/debug.py @@ -12,20 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=too-many-arguments, too-many-statements -# pylint: disable=too-many-locals, too-many-branches +# pylint: disable=too-many-arguments, too-many-locals +# pylint: disable=too-many-branches, too-many-statements +import asyncio import os -import signal -from os.path import isfile import click 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.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.factory import PlatformFactory 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"] ) or not helpers.has_debug_symbols(ide_data["prog_path"]) 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"): # 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": 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") - # run debugging client - inject_contrib_pysite() - - # pylint: disable=import-outside-toplevel - from platformio.commands.debug.process.client import GDBClient, reactor - - client = GDBClient(project_dir, __unprocessed, debug_options, env_options) - client.spawn(ide_data["gdb_path"], ide_data["prog_path"]) - - signal.signal(signal.SIGINT, lambda *args, **kwargs: None) - reactor.run() + 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"]) + loop.run_until_complete(coro) + if WINDOWS: + # an issue with asyncio executor and STIDIN, it cannot be closed gracefully + os._exit(0) # pylint: disable=protected-access + loop.close() return True diff --git a/platformio/commands/debug/process/base.py b/platformio/commands/debug/process/base.py deleted file mode 100644 index 67557f3d..00000000 --- a/platformio/commands/debug/process/base.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright (c) 2014-present PlatformIO -# -# 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) diff --git a/platformio/commands/debug/__init__.py b/platformio/debug/__init__.py similarity index 100% rename from platformio/commands/debug/__init__.py rename to platformio/debug/__init__.py diff --git a/platformio/commands/debug/exception.py b/platformio/debug/exception.py similarity index 100% rename from platformio/commands/debug/exception.py rename to platformio/debug/exception.py diff --git a/platformio/commands/debug/helpers.py b/platformio/debug/helpers.py similarity index 99% rename from platformio/commands/debug/helpers.py rename to platformio/debug/helpers.py index e2935b5a..72c3ff4b 100644 --- a/platformio/commands/debug/helpers.py +++ b/platformio/debug/helpers.py @@ -22,9 +22,9 @@ from os.path import isfile from platformio import fs, util from platformio.commands import PlatformioCLI -from platformio.commands.debug.exception import DebugInvalidOptionsError from platformio.commands.run.command import cli as cmd_run from platformio.compat import is_bytes +from platformio.debug.exception import DebugInvalidOptionsError from platformio.project.config import ProjectConfig from platformio.project.options import ProjectOptions diff --git a/platformio/commands/debug/initcfgs.py b/platformio/debug/initcfgs.py similarity index 100% rename from platformio/commands/debug/initcfgs.py rename to platformio/debug/initcfgs.py diff --git a/platformio/commands/debug/process/__init__.py b/platformio/debug/process/__init__.py similarity index 100% rename from platformio/commands/debug/process/__init__.py rename to platformio/debug/process/__init__.py diff --git a/platformio/debug/process/base.py b/platformio/debug/process/base.py new file mode 100644 index 00000000..10bdc86e --- /dev/null +++ b/platformio/debug/process/base.py @@ -0,0 +1,189 @@ +# Copyright (c) 2014-present PlatformIO +# +# 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 diff --git a/platformio/commands/debug/process/client.py b/platformio/debug/process/client.py similarity index 68% rename from platformio/commands/debug/process/client.py rename to platformio/debug/process/client.py index 45374727..a5765b39 100644 --- a/platformio/commands/debug/process/client.py +++ b/platformio/debug/process/client.py @@ -12,80 +12,75 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hashlib import os import re import signal +import tempfile 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.cache import ContentCache -from platformio.commands.debug import helpers -from platformio.commands.debug.exception import DebugInvalidOptionsError -from platformio.commands.debug.initcfgs import get_gdb_init_config -from platformio.commands.debug.process.base import BaseProcess -from platformio.commands.debug.process.server import DebugServer -from platformio.compat import hashlib_encode_data, is_bytes +from platformio.compat import 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 -class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes +class DebugClientProcess( + DebugBaseProcess +): # pylint: disable=too-many-instance-attributes PIO_SRC_NAME = ".pioinit" INIT_COMPLETED_BANNER = "PlatformIO: Initialization completed" def __init__(self, project_dir, args, debug_options, env_options): - super(GDBClient, self).__init__() + 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_server = DebugServer(debug_options, env_options) + self._server_process = DebugServerProcess(debug_options, env_options) 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()) - 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._auto_continue_timer = None + self._target_is_running = False self._errors_buffer = b"" - @defer.inlineCallbacks - def spawn(self, gdb_path, prog_path): + async def run(self, 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() patterns = { "PROJECT_DIR": self.project_dir, "PROG_PATH": prog_path, - "PROG_DIR": dirname(prog_path), - "PROG_NAME": basename(splitext(prog_path)[0]), + "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 []), } - yield self._debug_server.spawn(patterns) + await self._server_process.run(patterns) 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) # start GDB client args = [ - "piogdb", + gdb_path, "-q", "--directory", self._gdbsrc_dir, @@ -101,18 +96,16 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes if gdb_data_dir: args.extend(["--data-directory", gdb_data_dir]) args.append(patterns["PROG_PATH"]) - - transport = reactor.spawnProcess( - self, gdb_path, args, path=self.project_dir, env=os.environ - ) - defer.returnValue(transport) + await self.spawn(*args, cwd=self.project_dir, wait_until_exit=True) @staticmethod def _get_data_dir(gdb_path): if "msp430" in gdb_path: return None - gdb_data_dir = realpath(join(dirname(gdb_path), "..", "share", "gdb")) - return gdb_data_dir if isdir(gdb_data_dir) else None + gdb_data_dir = os.path.realpath( + 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): # 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] 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))) - def connectionMade(self): - self._lock_session(self.transport.pid) + def connection_made(self, transport): + 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() - p.dataReceived = self.onStdInData - stdio.StandardIO(p) - - def onStdInData(self, data): - super(GDBClient, self).onStdInData(data) + def stdin_data_received(self, data): + super(DebugClientProcess, self).stdin_data_received(data) if b"-exec-run" in data: - if self._target_is_run: + if self._target_is_running: token, _ = data.split(b"-", 1) - self.outReceived(token + b"^running\n") + self.stdout_data_received(token + b"^running\n") return data = data.replace(b"-exec-run", b"-exec-continue") 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"): # Allow terminating via SIGINT/CTRL+C signal.signal(signal.SIGINT, signal.default_int_handler) - self.transport.write(b"pio_reset_run_target\n") - self.transport.write(data) + self.transport.get_pipe_transport(0).write(b"pio_reset_run_target\n") + self.transport.get_pipe_transport(0).write(data) - def processEnded(self, reason): # pylint: disable=unused-argument - self._unlock_session() - 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) + def stdout_data_received(self, data): + super(DebugClientProcess, self).stdout_data_received(data) self._handle_error(data) # 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) ) - self._auto_continue_timer = task.LoopingCall(self._auto_exec_continue) - self._auto_continue_timer.start(0.1) - - def errReceived(self, data): - super(GDBClient, self).errReceived(data) - self._handle_error(data) + self._auto_exec_continue() def console_log(self, msg): if helpers.is_gdbmi_mode(): 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): auto_exec_delay = 0.5 # in seconds if self._last_activity > (time.time() - auto_exec_delay): + get_running_loop().call_later(0.1, self._auto_exec_continue) 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 + self.console_log( "PlatformIO: Resume the execution to `debug_init_break = %s`\n" % self.debug_options["init_break"] @@ -226,10 +204,14 @@ class GDBClient(BaseProcess): # pylint: disable=too-many-instance-attributes self.console_log( "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" ) - 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): 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, ) 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): assert self._session_id diff --git a/platformio/commands/debug/process/server.py b/platformio/debug/process/server.py similarity index 64% rename from platformio/commands/debug/process/server.py rename to platformio/debug/process/server.py index 7a302c9b..8d97d4c4 100644 --- a/platformio/commands/debug/process/server.py +++ b/platformio/debug/process/server.py @@ -12,53 +12,47 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import os 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.commands.debug.exception import DebugInvalidOptionsError -from platformio.commands.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode -from platformio.commands.debug.process.base import BaseProcess +from platformio.debug.exception import DebugInvalidOptionsError +from platformio.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode +from platformio.debug.process.base import DebugBaseProcess from platformio.proc import where_is_program -class DebugServer(BaseProcess): +class DebugServerProcess(DebugBaseProcess): def __init__(self, debug_options, env_options): - super(DebugServer, self).__init__() + super(DebugServerProcess, self).__init__() self.debug_options = debug_options self.env_options = env_options self._debug_port = ":3333" - self._transport = None - self._process_ended = False self._ready = False - @defer.inlineCallbacks - def spawn(self, patterns): # pylint: disable=too-many-branches + async def run(self, patterns): # pylint: disable=too-many-branches systype = util.get_systype() server = self.debug_options.get("server") if not server: - defer.returnValue(None) + return None server = self.apply_patterns(server, patterns) server_executable = server["executable"] if not server_executable: - defer.returnValue(None) + return None if server["cwd"]: - server_executable = join(server["cwd"], server_executable) + server_executable = os.path.join(server["cwd"], server_executable) if ( "windows" in systype and not server_executable.endswith(".exe") - and isfile(server_executable + ".exe") + and os.path.isfile(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) - if not isfile(server_executable): + if not os.path.isfile(server_executable): raise DebugInvalidOptionsError( "\nCould not launch Debug Server '%s'. Please check that it " "is installed and is included in a system PATH\n\n" @@ -70,6 +64,7 @@ class DebugServer(BaseProcess): openocd_pipe_allowed = all( [not self.debug_options["port"], "openocd" in server_executable] ) + openocd_pipe_allowed = False if openocd_pipe_allowed: args = [] if server["cwd"]: @@ -83,34 +78,31 @@ class DebugServer(BaseProcess): ) self._debug_port = '| "%s" %s' % (server_executable, str_args) self._debug_port = fs.to_unix_path(self._debug_port) - defer.returnValue(self._debug_port) + return self._debug_port env = os.environ.copy() # prepend server "lib" folder to LD path if ( "windows" not in systype 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" - env[ld_key] = join(server["cwd"], "lib") + 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)) # 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" % ( - join(server["cwd"], "bin"), + os.path.join(server["cwd"], "bin"), os.pathsep, os.environ.get("PATH", os.environ.get("Path", "")), ) - self._transport = reactor.spawnProcess( - self, - server_executable, - [server_executable] + server["arguments"], - path=server["cwd"], - env=env, + 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(): @@ -118,19 +110,18 @@ class DebugServer(BaseProcess): elif "qemu" in server_executable.lower(): 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 - def _wait_until_ready(self): + async def _wait_until_ready(self): ready_pattern = self.debug_options.get("server", {}).get("ready_pattern") timeout = 60 if ready_pattern else 10 elapsed = 0 delay = 0.5 auto_ready_delay = 0.5 - while not self._ready and not self._process_ended and elapsed < timeout: - yield self.async_sleep(delay) + while not self._ready and self.is_running() and elapsed < timeout: + await asyncio.sleep(delay) if not ready_pattern: self._ready = self._last_activity < (time.time() - auto_ready_delay) elapsed += delay @@ -143,33 +134,15 @@ class DebugServer(BaseProcess): self._ready = ready_pattern.encode() in data return self._ready - @staticmethod - def async_sleep(secs): - d = defer.Deferred() - reactor.callLater(secs, d.callback, None) - return d - def get_debug_port(self): return self._debug_port - def outReceived(self, data): - super(DebugServer, self).outReceived( + def stdout_data_received(self, data): + super(DebugServerProcess, self).stdout_data_received( escape_gdbmi_stream("@", data) if is_gdbmi_mode() else data ) self._check_ready_by_pattern(data) - def errReceived(self, data): - super(DebugServer, self).errReceived(data) + def stderr_data_received(self, data): + super(DebugServerProcess, self).stderr_data_received(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 diff --git a/platformio/platform/board.py b/platformio/platform/board.py index 900892cd..34d9dc34 100644 --- a/platformio/platform/board.py +++ b/platformio/platform/board.py @@ -15,11 +15,8 @@ import os from platformio import fs, telemetry, util -from platformio.commands.debug.exception import ( - DebugInvalidOptionsError, - DebugSupportError, -) from platformio.compat import PY2 +from platformio.debug.exception import DebugInvalidOptionsError, DebugSupportError from platformio.exception import UserSideException from platformio.platform.exception import InvalidBoardManifest