debug_ext.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import json
  2. import os
  3. import re
  4. import shlex
  5. import subprocess
  6. import sys
  7. import threading
  8. import time
  9. from threading import Thread
  10. from idf_py_actions.errors import FatalError
  11. from idf_py_actions.tools import ensure_build_directory
  12. PYTHON = sys.executable
  13. def action_extensions(base_actions, project_path):
  14. OPENOCD_OUT_FILE = 'openocd_out.txt'
  15. GDBGUI_OUT_FILE = 'gdbgui_out.txt'
  16. # Internal dictionary of currently active processes, threads and their output files
  17. processes = {'threads_to_join': [], 'openocd_issues': None}
  18. def _check_for_common_openocd_issues(file_name, print_all=True):
  19. if processes['openocd_issues'] is not None:
  20. return processes['openocd_issues']
  21. try:
  22. message = 'Please check JTAG connection!'
  23. with open(file_name, 'r') as f:
  24. content = f.read()
  25. if print_all:
  26. print(content)
  27. if re.search(r'Address already in use', content):
  28. message = ('Please check if another process uses the mentioned ports. OpenOCD already running, perhaps in the background?\n'
  29. 'Please list all processes to check if OpenOCD is already running; if so, terminate it before starting OpenOCD from idf.py')
  30. finally:
  31. processes['openocd_issues'] = message
  32. return message
  33. def _check_openocd_errors(fail_if_openocd_failed, target, ctx):
  34. if fail_if_openocd_failed:
  35. if 'openocd' in processes and processes['openocd'] is not None:
  36. p = processes['openocd']
  37. name = processes['openocd_outfile_name']
  38. # watch OpenOCD (for 5x500ms) to check if it hasn't terminated or outputs an error
  39. for _ in range(5):
  40. if p.poll() is not None:
  41. print('OpenOCD exited with {}'.format(p.poll()))
  42. break
  43. with open(name, 'r') as f:
  44. content = f.read()
  45. if re.search(r'no device found', content):
  46. break
  47. if re.search(r'Listening on port \d+ for gdb connections', content):
  48. # expect OpenOCD has started successfully - stop watching
  49. return
  50. time.sleep(0.5)
  51. else:
  52. return
  53. # OpenOCD exited or error message detected -> print possible output and terminate
  54. raise FatalError('Action "{}" failed due to errors in OpenOCD:\n{}'.format(target, _check_for_common_openocd_issues(name)), ctx)
  55. def _terminate_async_target(target):
  56. if target in processes and processes[target] is not None:
  57. try:
  58. if target + '_outfile' in processes:
  59. processes[target + '_outfile'].close()
  60. p = processes[target]
  61. if p.poll() is None:
  62. p.terminate()
  63. # waiting 10x100ms for the process to terminate gracefully
  64. for _ in range(10):
  65. if p.poll() is not None:
  66. break
  67. time.sleep(0.1)
  68. else:
  69. p.kill()
  70. if target + '_outfile_name' in processes:
  71. if target == 'openocd':
  72. print(_check_for_common_openocd_issues(processes[target + '_outfile_name'], print_all=False))
  73. os.unlink(processes[target + '_outfile_name'])
  74. except Exception as e:
  75. print(e)
  76. print('Failed to close/kill {}'.format(target))
  77. processes[target] = None # to indicate this has ended
  78. def _get_commandline_options(ctx):
  79. """ Return all the command line options up to first action """
  80. # This approach ignores argument parsing done Click
  81. result = []
  82. for arg in sys.argv:
  83. if arg in ctx.command.commands_with_aliases:
  84. break
  85. result.append(arg)
  86. return result
  87. def create_local_gdbinit(gdbinit, elf_file):
  88. with open(gdbinit, 'w') as f:
  89. if os.name == 'nt':
  90. elf_file = elf_file.replace('\\','\\\\')
  91. f.write('file {}\n'.format(elf_file))
  92. f.write('target remote :3333\n')
  93. f.write('mon reset halt\n')
  94. f.write('flushregs\n')
  95. f.write('thb app_main\n')
  96. f.write('c\n')
  97. def debug_cleanup():
  98. print('cleaning up debug targets')
  99. for t in processes['threads_to_join']:
  100. if threading.currentThread() != t:
  101. t.join()
  102. _terminate_async_target('openocd')
  103. _terminate_async_target('gdbgui')
  104. _terminate_async_target('gdb')
  105. def post_debug(action, ctx, args, **kwargs):
  106. """ Deal with asynchronous targets, such as openocd running in background """
  107. if kwargs['block'] == 1:
  108. for target in ['openocd', 'gdbgui']:
  109. if target in processes and processes[target] is not None:
  110. break
  111. else:
  112. return
  113. try:
  114. p = processes[target]
  115. name = processes[target + '_outfile_name']
  116. pos = 0
  117. while True:
  118. with open(name, 'r') as f:
  119. f.seek(pos)
  120. for line in f:
  121. print(line.rstrip())
  122. pos = f.tell()
  123. if p.poll() is not None:
  124. print('"{}" exited with {}'.format(target, p.poll()))
  125. break
  126. time.sleep(0.5)
  127. except KeyboardInterrupt:
  128. print('Terminated -> exiting debug utility targets')
  129. _terminate_async_target('openocd')
  130. _terminate_async_target('gdbgui')
  131. def get_project_desc(args, ctx):
  132. desc_path = os.path.join(args.build_dir, 'project_description.json')
  133. if not os.path.exists(desc_path):
  134. ensure_build_directory(args, ctx.info_name)
  135. with open(desc_path, 'r') as f:
  136. project_desc = json.load(f)
  137. return project_desc
  138. def openocd(action, ctx, args, openocd_scripts, openocd_commands):
  139. """
  140. Execute openocd as external tool
  141. """
  142. OPENOCD_TAGET_CONFIG = {
  143. 'esp32': '-f board/esp32-wrover-kit-3.3v.cfg',
  144. 'esp32s2': '-f board/esp32s2-kaluga-1.cfg',
  145. }
  146. if os.getenv('OPENOCD_SCRIPTS') is None:
  147. raise FatalError('OPENOCD_SCRIPTS not found in the environment: Please run export.sh/export.bat', ctx)
  148. openocd_arguments = os.getenv('OPENOCD_COMMANDS') if openocd_commands is None else openocd_commands
  149. project_desc = get_project_desc(args, ctx)
  150. if openocd_arguments is None:
  151. # use default value if commands not defined in the environment nor command line
  152. target = project_desc['target']
  153. default_args = '-f interface/ftdi/esp32_devkitj_v1.cfg -f target/{}.cfg'.format(target)
  154. openocd_arguments = OPENOCD_TAGET_CONFIG.get(target, default_args)
  155. print('Note: OpenOCD cfg not found (via env variable OPENOCD_COMMANDS nor as a --openocd-commands argument)\n'
  156. 'OpenOCD arguments default to: "{}"'.format(openocd_arguments))
  157. # script directory is taken from the environment by OpenOCD, update only if command line arguments to override
  158. if openocd_scripts is not None:
  159. openocd_arguments += ' -s {}'.format(openocd_scripts)
  160. local_dir = project_desc['build_dir']
  161. args = ['openocd'] + shlex.split(openocd_arguments)
  162. openocd_out_name = os.path.join(local_dir, OPENOCD_OUT_FILE)
  163. openocd_out = open(openocd_out_name, 'a+')
  164. try:
  165. process = subprocess.Popen(args, stdout=openocd_out, stderr=subprocess.STDOUT, bufsize=1)
  166. except Exception as e:
  167. print(e)
  168. raise FatalError('Error starting openocd. Please make sure it is installed and is present in executable paths', ctx)
  169. processes['openocd'] = process
  170. processes['openocd_outfile'] = openocd_out
  171. processes['openocd_outfile_name'] = openocd_out_name
  172. print('OpenOCD started as a background task {}'.format(process.pid))
  173. def gdbui(action, ctx, args, gdbgui_port, gdbinit, require_openocd):
  174. """
  175. Asynchronous GDB-UI target
  176. """
  177. project_desc = get_project_desc(args, ctx)
  178. local_dir = project_desc['build_dir']
  179. gdb = project_desc['monitor_toolprefix'] + 'gdb'
  180. if gdbinit is None:
  181. gdbinit = os.path.join(local_dir, 'gdbinit')
  182. create_local_gdbinit(gdbinit, os.path.join(args.build_dir, project_desc['app_elf']))
  183. args = ['gdbgui', '-g', gdb, '--gdb-args="-x={}"'.format(gdbinit)]
  184. if gdbgui_port is not None:
  185. args += ['--port', gdbgui_port]
  186. gdbgui_out_name = os.path.join(local_dir, GDBGUI_OUT_FILE)
  187. gdbgui_out = open(gdbgui_out_name, 'a+')
  188. env = os.environ.copy()
  189. # The only known solution for https://github.com/cs01/gdbgui/issues/359 is to set the following environment
  190. # variable. The greenlet package cannot be downgraded for compatibility with other requirements (gdbgui,
  191. # pygdbmi).
  192. env['PURE_PYTHON'] = '1'
  193. try:
  194. process = subprocess.Popen(args, stdout=gdbgui_out, stderr=subprocess.STDOUT, bufsize=1, env=env)
  195. except Exception as e:
  196. print(e)
  197. raise FatalError('Error starting gdbgui. Please make sure gdbgui can be started', ctx)
  198. processes['gdbgui'] = process
  199. processes['gdbgui_outfile'] = gdbgui_out
  200. processes['gdbgui_outfile_name'] = gdbgui_out_name
  201. print('gdbgui started as a background task {}'.format(process.pid))
  202. _check_openocd_errors(fail_if_openocd_failed, action, ctx)
  203. def global_callback(ctx, global_args, tasks):
  204. def move_to_front(task_name):
  205. for index, task in enumerate(tasks):
  206. if task.name == task_name:
  207. tasks.insert(0, tasks.pop(index))
  208. break
  209. debug_targets = any([task.name in ('openocd', 'gdbgui') for task in tasks])
  210. if debug_targets:
  211. # Register the meta cleanup callback -> called on FatalError
  212. ctx.meta['cleanup'] = debug_cleanup
  213. move_to_front('gdbgui') # possibly 2nd
  214. move_to_front('openocd') # always 1st
  215. # followed by "monitor", "gdb" or "gdbtui" in any order
  216. post_action = ctx.invoke(ctx.command.get_command(ctx, 'post_debug'))
  217. if any([task.name in ('monitor', 'gdb', 'gdbtui') for task in tasks]):
  218. post_action.action_args['block'] = 0
  219. else:
  220. post_action.action_args['block'] = 1
  221. tasks.append(post_action) # always last
  222. if any([task.name == 'openocd' for task in tasks]):
  223. for task in tasks:
  224. if task.name in ('gdb', 'gdbgui', 'gdbtui'):
  225. task.action_args['require_openocd'] = True
  226. def run_gdb(gdb_args):
  227. p = subprocess.Popen(gdb_args)
  228. processes['gdb'] = p
  229. return p.wait()
  230. def gdbtui(action, ctx, args, gdbinit, require_openocd):
  231. """
  232. Synchronous GDB target with text ui mode
  233. """
  234. gdb(action, ctx, args, 1, gdbinit, require_openocd)
  235. def gdb(action, ctx, args, gdb_tui, gdbinit, require_openocd):
  236. """
  237. Synchronous GDB target
  238. """
  239. watch_openocd = Thread(target=_check_openocd_errors, args=(fail_if_openocd_failed, action, ctx, ))
  240. watch_openocd.start()
  241. processes['threads_to_join'].append(watch_openocd)
  242. desc_path = os.path.join(args.build_dir, 'project_description.json')
  243. if not os.path.exists(desc_path):
  244. ensure_build_directory(args, ctx.info_name)
  245. with open(desc_path, 'r') as f:
  246. project_desc = json.load(f)
  247. elf_file = os.path.join(args.build_dir, project_desc['app_elf'])
  248. if not os.path.exists(elf_file):
  249. raise FatalError('ELF file not found. You need to build & flash the project before running debug targets', ctx)
  250. gdb = project_desc['monitor_toolprefix'] + 'gdb'
  251. local_dir = project_desc['build_dir']
  252. if gdbinit is None:
  253. gdbinit = os.path.join(local_dir, 'gdbinit')
  254. create_local_gdbinit(gdbinit, elf_file)
  255. args = [gdb, '-x={}'.format(gdbinit)]
  256. if gdb_tui is not None:
  257. args += ['-tui']
  258. t = Thread(target=run_gdb, args=(args, ))
  259. t.start()
  260. while True:
  261. try:
  262. t.join()
  263. break
  264. except KeyboardInterrupt:
  265. # Catching Keyboard interrupt, as this is used for breaking running program in gdb
  266. continue
  267. finally:
  268. watch_openocd.join()
  269. try:
  270. processes['threads_to_join'].remove(watch_openocd)
  271. except ValueError:
  272. # Valid scenario: watch_openocd task won't be in the list if openocd not started from idf.py
  273. pass
  274. fail_if_openocd_failed = {
  275. 'names': ['--require-openocd', '--require_openocd'],
  276. 'help':
  277. ('Fail this target if openocd (this targets dependency) failed.\n'),
  278. 'is_flag': True,
  279. 'default': False,
  280. }
  281. gdbinit = {
  282. 'names': ['--gdbinit'],
  283. 'help': ('Specify the name of gdbinit file to use\n'),
  284. 'default': None,
  285. }
  286. debug_actions = {
  287. 'global_action_callbacks': [global_callback],
  288. 'actions': {
  289. 'openocd': {
  290. 'callback': openocd,
  291. 'help': 'Run openocd from current path',
  292. 'options': [
  293. {
  294. 'names': ['--openocd-scripts', '--openocd_scripts'],
  295. 'help':
  296. ('Script directory for openocd cfg files.\n'),
  297. 'default':
  298. None,
  299. },
  300. {
  301. 'names': ['--openocd-commands', '--openocd_commands'],
  302. 'help':
  303. ('Command line arguments for openocd.\n'),
  304. 'default': None,
  305. }
  306. ],
  307. 'order_dependencies': ['all', 'flash'],
  308. },
  309. 'gdb': {
  310. 'callback': gdb,
  311. 'help': 'Run the GDB.',
  312. 'options': [
  313. {
  314. 'names': ['--gdb-tui', '--gdb_tui'],
  315. 'help':
  316. ('run gdb in TUI mode\n'),
  317. 'default':
  318. None,
  319. }, gdbinit, fail_if_openocd_failed
  320. ],
  321. 'order_dependencies': ['all', 'flash'],
  322. },
  323. 'gdbgui': {
  324. 'callback': gdbui,
  325. 'help': 'GDB UI in default browser.',
  326. 'options': [
  327. {
  328. 'names': ['--gdbgui-port', '--gdbgui_port'],
  329. 'help':
  330. ('The port on which gdbgui will be hosted. Default: 5000\n'),
  331. 'default':
  332. None,
  333. }, gdbinit, fail_if_openocd_failed
  334. ],
  335. 'order_dependencies': ['all', 'flash'],
  336. },
  337. 'gdbtui': {
  338. 'callback': gdbtui,
  339. 'help': 'GDB TUI mode.',
  340. 'options': [gdbinit, fail_if_openocd_failed],
  341. 'order_dependencies': ['all', 'flash'],
  342. },
  343. 'post-debug': {
  344. 'callback': post_debug,
  345. 'help': 'Utility target to read the output of async debug action and stop them.',
  346. 'options': [
  347. {
  348. 'names': ['--block', '--block'],
  349. 'help':
  350. ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
  351. 'default': 0,
  352. },
  353. ],
  354. 'order_dependencies': [],
  355. },
  356. 'post_debug': {
  357. 'callback': post_debug,
  358. 'deprecated': {
  359. 'removed': 'v5.0',
  360. 'message': 'Please use "post-debug" instead.',
  361. },
  362. 'hidden': True,
  363. 'help': 'Utility target to read the output of async debug action and stop them.',
  364. 'options': [
  365. {
  366. 'names': ['--block', '--block'],
  367. 'help':
  368. ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
  369. 'default': 0,
  370. },
  371. ],
  372. 'order_dependencies': [],
  373. },
  374. },
  375. }
  376. return debug_actions