2019-04-19 19:56:16 +03:00
|
|
|
# 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 json
|
|
|
|
import os
|
|
|
|
import re
|
2019-07-04 17:47:26 +03:00
|
|
|
import signal
|
2019-04-19 19:56:16 +03:00
|
|
|
import time
|
|
|
|
from hashlib import sha1
|
|
|
|
from os.path import abspath, basename, dirname, isdir, join, splitext
|
|
|
|
from tempfile import mkdtemp
|
|
|
|
|
2019-05-10 13:00:53 +03:00
|
|
|
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
|
2019-04-19 19:56:16 +03:00
|
|
|
|
|
|
|
from platformio import app, exception, util
|
|
|
|
from platformio.commands.debug import helpers, initcfgs
|
|
|
|
from platformio.commands.debug.process import BaseProcess
|
|
|
|
from platformio.commands.debug.server import DebugServer
|
2019-06-03 13:30:35 +03:00
|
|
|
from platformio.compat import hashlib_encode_data
|
2019-05-27 22:25:22 +03:00
|
|
|
from platformio.project.helpers import get_project_cache_dir
|
2019-04-19 19:56:16 +03:00
|
|
|
from platformio.telemetry import MeasurementProtocol
|
|
|
|
|
|
|
|
LOG_FILE = None
|
|
|
|
|
|
|
|
|
|
|
|
class GDBClient(BaseProcess): # 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):
|
|
|
|
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._session_id = None
|
|
|
|
|
2019-05-27 22:25:22 +03:00
|
|
|
if not isdir(get_project_cache_dir()):
|
|
|
|
os.makedirs(get_project_cache_dir())
|
2019-05-30 23:42:15 +03:00
|
|
|
self._gdbsrc_dir = mkdtemp(dir=get_project_cache_dir(),
|
|
|
|
prefix=".piodebug-")
|
2019-04-19 19:56:16 +03:00
|
|
|
|
|
|
|
self._target_is_run = False
|
|
|
|
self._last_server_activity = 0
|
|
|
|
self._auto_continue_timer = None
|
|
|
|
|
|
|
|
def spawn(self, gdb_path, prog_path):
|
|
|
|
session_hash = gdb_path + prog_path
|
2019-06-03 13:30:35 +03:00
|
|
|
self._session_id = sha1(hashlib_encode_data(session_hash)).hexdigest()
|
2019-04-19 19:56:16 +03:00
|
|
|
self._kill_previous_session()
|
|
|
|
|
|
|
|
patterns = {
|
2019-07-15 14:20:14 +03:00
|
|
|
"PROJECT_DIR": self.project_dir,
|
|
|
|
"PROG_PATH": prog_path,
|
|
|
|
"PROG_DIR": dirname(prog_path),
|
2019-04-19 19:56:16 +03:00
|
|
|
"PROG_NAME": basename(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 "",
|
2019-05-31 14:45:48 +03:00
|
|
|
"LOAD_CMDS": "\n".join(self.debug_options['load_cmds'] or []),
|
2019-04-19 19:56:16 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
self._debug_server.spawn(patterns)
|
|
|
|
|
|
|
|
if not patterns['DEBUG_PORT']:
|
|
|
|
patterns['DEBUG_PORT'] = self._debug_server.get_debug_port()
|
|
|
|
self.generate_pioinit(self._gdbsrc_dir, patterns)
|
|
|
|
|
|
|
|
# start GDB client
|
|
|
|
args = [
|
|
|
|
"piogdb",
|
|
|
|
"-q",
|
|
|
|
"--directory", self._gdbsrc_dir,
|
|
|
|
"--directory", self.project_dir,
|
|
|
|
"-l", "10"
|
|
|
|
] # yapf: disable
|
|
|
|
args.extend(self.args)
|
|
|
|
if not gdb_path:
|
|
|
|
raise exception.DebugInvalidOptions("GDB client is not configured")
|
|
|
|
gdb_data_dir = self._get_data_dir(gdb_path)
|
|
|
|
if gdb_data_dir:
|
|
|
|
args.extend(["--data-directory", gdb_data_dir])
|
|
|
|
args.append(patterns['PROG_PATH'])
|
|
|
|
|
2019-05-30 23:42:15 +03:00
|
|
|
return reactor.spawnProcess(self,
|
|
|
|
gdb_path,
|
|
|
|
args,
|
|
|
|
path=self.project_dir,
|
|
|
|
env=os.environ)
|
2019-04-19 19:56:16 +03:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _get_data_dir(gdb_path):
|
|
|
|
if "msp430" in gdb_path:
|
|
|
|
return None
|
|
|
|
gdb_data_dir = abspath(join(dirname(gdb_path), "..", "share", "gdb"))
|
|
|
|
return gdb_data_dir if isdir(gdb_data_dir) else None
|
|
|
|
|
|
|
|
def generate_pioinit(self, dst_dir, patterns):
|
2019-05-30 23:42:15 +03:00
|
|
|
server_exe = (self.debug_options.get("server")
|
|
|
|
or {}).get("executable", "").lower()
|
2019-04-19 19:56:16 +03:00
|
|
|
if "jlink" in server_exe:
|
|
|
|
cfg = initcfgs.GDB_JLINK_INIT_CONFIG
|
|
|
|
elif "st-util" in server_exe:
|
|
|
|
cfg = initcfgs.GDB_STUTIL_INIT_CONFIG
|
|
|
|
elif "mspdebug" in server_exe:
|
2019-04-19 19:58:34 +03:00
|
|
|
cfg = initcfgs.GDB_MSPDEBUG_INIT_CONFIG
|
2019-06-27 15:07:13 +03:00
|
|
|
elif "qemu" in server_exe:
|
|
|
|
cfg = initcfgs.GDB_QEMU_INIT_CONFIG
|
2019-04-19 19:56:16 +03:00
|
|
|
elif self.debug_options['require_debug_port']:
|
|
|
|
cfg = initcfgs.GDB_BLACKMAGIC_INIT_CONFIG
|
|
|
|
else:
|
|
|
|
cfg = initcfgs.GDB_DEFAULT_INIT_CONFIG
|
|
|
|
commands = cfg.split("\n")
|
|
|
|
|
|
|
|
if self.debug_options['init_cmds']:
|
|
|
|
commands = self.debug_options['init_cmds']
|
|
|
|
commands.extend(self.debug_options['extra_cmds'])
|
|
|
|
|
|
|
|
if not any("define pio_reset_target" in cmd for cmd in commands):
|
|
|
|
commands = [
|
|
|
|
"define pio_reset_target",
|
|
|
|
" echo Warning! Undefined pio_reset_target command\\n",
|
|
|
|
" mon reset",
|
|
|
|
"end"
|
|
|
|
] + commands # yapf: disable
|
|
|
|
if not any("define pio_reset_halt_target" in cmd for cmd in commands):
|
|
|
|
commands = [
|
|
|
|
"define pio_reset_halt_target",
|
|
|
|
" echo Warning! Undefined pio_reset_halt_target command\\n",
|
|
|
|
" mon reset halt",
|
|
|
|
"end"
|
|
|
|
] + commands # yapf: disable
|
|
|
|
if not any("define pio_restart_target" in cmd for cmd in commands):
|
|
|
|
commands += [
|
|
|
|
"define pio_restart_target",
|
|
|
|
" pio_reset_halt_target",
|
|
|
|
" $INIT_BREAK",
|
|
|
|
" %s" % ("continue" if patterns['INIT_BREAK'] else "next"),
|
|
|
|
"end"
|
|
|
|
] # yapf: disable
|
|
|
|
|
|
|
|
banner = [
|
2019-05-30 22:26:42 +03:00
|
|
|
"echo PlatformIO Unified Debugger -> http://bit.ly/pio-debug\\n",
|
2019-07-11 14:09:15 +03:00
|
|
|
"echo PlatformIO: debug_tool = %s\\n" % self.debug_options['tool'],
|
2019-04-19 19:56:16 +03:00
|
|
|
"echo PlatformIO: Initializing remote target...\\n"
|
|
|
|
]
|
|
|
|
footer = ["echo %s\\n" % self.INIT_COMPLETED_BANNER]
|
|
|
|
commands = banner + commands + footer
|
|
|
|
|
|
|
|
with open(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)
|
|
|
|
|
|
|
|
p = protocol.Protocol()
|
|
|
|
p.dataReceived = self.onStdInData
|
|
|
|
stdio.StandardIO(p)
|
|
|
|
|
|
|
|
def onStdInData(self, data):
|
|
|
|
if LOG_FILE:
|
|
|
|
with open(LOG_FILE, "ab") as fp:
|
|
|
|
fp.write(data)
|
|
|
|
|
2019-06-27 13:38:46 +03:00
|
|
|
self._last_server_activity = time.time()
|
|
|
|
|
2019-04-19 19:56:16 +03:00
|
|
|
if b"-exec-run" in data:
|
|
|
|
if self._target_is_run:
|
|
|
|
token, _ = data.split(b"-", 1)
|
|
|
|
self.outReceived(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
|
|
|
|
if b"-gdb-exit" in data or data.strip() in (b"q", b"quit"):
|
2019-07-04 17:47:26 +03:00
|
|
|
# Allow terminating via SIGINT/CTRL+C
|
|
|
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
2019-04-19 19:56:16 +03:00
|
|
|
self.transport.write(b"pio_reset_target\n")
|
|
|
|
self.transport.write(data)
|
|
|
|
|
|
|
|
def processEnded(self, reason): # pylint: disable=unused-argument
|
|
|
|
self._unlock_session()
|
|
|
|
if self._gdbsrc_dir and isdir(self._gdbsrc_dir):
|
|
|
|
util.rmtree_(self._gdbsrc_dir)
|
|
|
|
if self._debug_server:
|
|
|
|
self._debug_server.terminate()
|
|
|
|
|
|
|
|
reactor.stop()
|
|
|
|
|
|
|
|
def outReceived(self, data):
|
2019-06-27 13:38:46 +03:00
|
|
|
if LOG_FILE:
|
|
|
|
with open(LOG_FILE, "ab") as fp:
|
|
|
|
fp.write(data)
|
|
|
|
|
2019-04-19 19:56:16 +03:00
|
|
|
self._last_server_activity = time.time()
|
|
|
|
super(GDBClient, self).outReceived(data)
|
|
|
|
self._handle_error(data)
|
|
|
|
# go to init break automatically
|
|
|
|
if self.INIT_COMPLETED_BANNER.encode() in data:
|
|
|
|
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)
|
|
|
|
|
|
|
|
def console_log(self, msg):
|
|
|
|
if helpers.is_mi_mode(self.args):
|
|
|
|
self.outReceived(('~"%s\\n"\n' % msg).encode())
|
|
|
|
else:
|
|
|
|
self.outReceived(("%s\n" % msg).encode())
|
|
|
|
|
|
|
|
def _auto_exec_continue(self):
|
|
|
|
auto_exec_delay = 0.5 # in seconds
|
|
|
|
if self._last_server_activity > (time.time() - auto_exec_delay):
|
|
|
|
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:
|
|
|
|
return
|
|
|
|
self.console_log(
|
|
|
|
"PlatformIO: Resume the execution to `debug_init_break = %s`" %
|
|
|
|
self.debug_options['init_break'])
|
2019-05-30 22:26:42 +03:00
|
|
|
self.console_log("PlatformIO: More configuration options -> "
|
|
|
|
"http://bit.ly/pio-debug")
|
2019-04-19 19:56:16 +03:00
|
|
|
self.transport.write(b"0-exec-continue\n" if helpers.
|
|
|
|
is_mi_mode(self.args) else b"continue\n")
|
|
|
|
self._target_is_run = True
|
|
|
|
|
|
|
|
def _handle_error(self, data):
|
|
|
|
if (self.PIO_SRC_NAME.encode() not in data
|
|
|
|
or b"Error in sourced" not in data):
|
|
|
|
return
|
|
|
|
configuration = {"debug": self.debug_options, "env": self.env_options}
|
|
|
|
exd = re.sub(r'\\(?!")', "/", json.dumps(configuration))
|
|
|
|
exd = re.sub(r'"(?:[a-z]\:)?((/[^"/]+)+)"', lambda m: '"%s"' % join(
|
|
|
|
*m.group(1).split("/")[-2:]), exd, re.I | re.M)
|
|
|
|
mp = MeasurementProtocol()
|
|
|
|
mp['exd'] = "DebugGDBPioInitError: %s" % exd
|
|
|
|
mp['exf'] = 1
|
|
|
|
mp.send("exception")
|
|
|
|
self.transport.loseConnection()
|
|
|
|
|
|
|
|
def _kill_previous_session(self):
|
|
|
|
assert self._session_id
|
|
|
|
pid = None
|
|
|
|
with app.ContentCache() as cc:
|
|
|
|
pid = cc.get(self._session_id)
|
|
|
|
cc.delete(self._session_id)
|
|
|
|
if not pid:
|
|
|
|
return
|
|
|
|
if "windows" in util.get_systype():
|
|
|
|
kill = ["Taskkill", "/PID", pid, "/F"]
|
|
|
|
else:
|
|
|
|
kill = ["kill", pid]
|
|
|
|
try:
|
|
|
|
util.exec_command(kill)
|
|
|
|
except: # pylint: disable=bare-except
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _lock_session(self, pid):
|
|
|
|
if not self._session_id:
|
|
|
|
return
|
|
|
|
with app.ContentCache() as cc:
|
|
|
|
cc.set(self._session_id, str(pid), "1h")
|
|
|
|
|
|
|
|
def _unlock_session(self):
|
|
|
|
if not self._session_id:
|
|
|
|
return
|
|
|
|
with app.ContentCache() as cc:
|
|
|
|
cc.delete(self._session_id)
|