Super Mario 64 OpenGL port for PC. Mirror of https://github.com/sm64pc/sm64pc https://github.com/sm64pc/sm64pc
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

738 lines
25 KiB

  1. #!/usr/bin/env python3
  2. import sys
  3. import re
  4. import os
  5. import ast
  6. import argparse
  7. import subprocess
  8. import difflib
  9. import string
  10. import itertools
  11. import threading
  12. import queue
  13. import time
  14. def fail(msg):
  15. print(msg, file=sys.stderr)
  16. sys.exit(1)
  17. try:
  18. import attr
  19. from colorama import Fore, Style, Back
  20. import ansiwrap
  21. import watchdog
  22. except ModuleNotFoundError as e:
  23. fail(f"Missing prerequisite python module {e.name}. "
  24. "Run `python3 -m pip install --user colorama ansiwrap attrs watchdog` to install prerequisites.")
  25. # Prefer to use diff_settings.py from the current working directory
  26. sys.path.insert(0, '.')
  27. try:
  28. import diff_settings
  29. except ModuleNotFoundError:
  30. fail("Unable to find diff_settings.py in the same directory.")
  31. # ==== CONFIG ====
  32. parser = argparse.ArgumentParser(
  33. description="Diff MIPS assembly.")
  34. parser.add_argument('start',
  35. help="Function name or address to start diffing from.")
  36. parser.add_argument('end', nargs='?',
  37. help="Address to end diff at.")
  38. parser.add_argument('-o', dest='diff_obj', action='store_true',
  39. help="Diff .o files rather than a whole binary. This makes it possible to see symbol names. (Recommended)")
  40. parser.add_argument('--base-asm', dest='base_asm', metavar='FILE',
  41. help="Read assembly from given file instead of configured base img.")
  42. parser.add_argument('--write-asm', dest='write_asm', metavar='FILE',
  43. help="Write the current assembly output to file, e.g. for use with --base-asm.")
  44. parser.add_argument('-m', '--make', dest='make', action='store_true',
  45. help="Automatically run 'make' on the .o file or binary before diffing.")
  46. parser.add_argument('-l', '--skip-lines', dest='skip_lines', type=int, default=0,
  47. help="Skip the first N lines of output.")
  48. parser.add_argument('-f', '--stop-jr-ra', dest='stop_jrra', action='store_true',
  49. help="Stop disassembling at the first 'jr ra'. Some functions have multiple return points, so use with care!")
  50. parser.add_argument('-i', '--ignore-large-imms', dest='ignore_large_imms', action='store_true',
  51. help="Pretend all large enough immediates are the same.")
  52. parser.add_argument('-B', '--no-show-branches', dest='show_branches', action='store_false',
  53. help="Don't visualize branches/branch targets.")
  54. parser.add_argument('-S', '--base-shift', dest='base_shift', type=str, default='0',
  55. help="Diff position X in our img against position X + shift in the base img. "
  56. "Arithmetic is allowed, so e.g. |-S \"0x1234 - 0x4321\"| is a reasonable "
  57. "flag to pass if it is known that position 0x1234 in the base img syncs "
  58. "up with position 0x4321 in our img. Not supported together with -o.")
  59. parser.add_argument('-w', '--watch', dest='watch', action='store_true',
  60. help="Automatically update when source/object files change. "
  61. "Recommended in combination with -m.")
  62. parser.add_argument('--width', dest='column_width', type=int, default=50,
  63. help="Sets the width of the left and right view column.")
  64. # Project-specific flags, e.g. different versions/make arguments.
  65. if hasattr(diff_settings, "add_custom_arguments"):
  66. diff_settings.add_custom_arguments(parser)
  67. args = parser.parse_args()
  68. # Set imgs, map file and make flags in a project-specific manner.
  69. config = {}
  70. diff_settings.apply(config, args)
  71. baseimg = config.get('baseimg', None)
  72. myimg = config.get('myimg', None)
  73. mapfile = config.get('mapfile', None)
  74. makeflags = config.get('makeflags', [])
  75. source_directories = config.get('source_directories', None)
  76. MAX_FUNCTION_SIZE_LINES = 1024
  77. MAX_FUNCTION_SIZE_BYTES = 1024 * 4
  78. COLOR_ROTATION = [
  79. Fore.MAGENTA,
  80. Fore.CYAN,
  81. Fore.GREEN,
  82. Fore.RED,
  83. Fore.LIGHTYELLOW_EX,
  84. Fore.LIGHTMAGENTA_EX,
  85. Fore.LIGHTCYAN_EX,
  86. Fore.LIGHTGREEN_EX,
  87. Fore.LIGHTBLACK_EX,
  88. ]
  89. BUFFER_CMD = ["tail", "-c", str(10**9)]
  90. LESS_CMD = ["less", "-Ric"]
  91. DEBOUNCE_DELAY = 0.1
  92. FS_WATCH_EXTENSIONS = ['.c', '.h']
  93. # ==== LOGIC ====
  94. binutils_prefix = None
  95. for binutils_cand in ['mips-linux-gnu-', 'mips64-elf-']:
  96. try:
  97. subprocess.check_call([binutils_cand + "objdump", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  98. binutils_prefix = binutils_cand
  99. break
  100. except subprocess.CalledProcessError:
  101. pass
  102. except FileNotFoundError:
  103. pass
  104. if not binutils_prefix:
  105. fail("Missing binutils; please ensure mips-linux-gnu-objdump or mips64-elf-objdump exist.")
  106. def eval_int(expr, emsg=None):
  107. try:
  108. ret = ast.literal_eval(expr)
  109. if not isinstance(ret, int):
  110. raise Exception("not an integer")
  111. return ret
  112. except Exception:
  113. if emsg is not None:
  114. fail(emsg)
  115. return None
  116. def run_make(target, capture_output=False):
  117. if capture_output:
  118. return subprocess.run(["make"] + makeflags + [target], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
  119. else:
  120. subprocess.check_call(["make"] + makeflags + [target])
  121. def restrict_to_function(dump, fn_name):
  122. out = []
  123. search = f'<{fn_name}>:'
  124. found = False
  125. for line in dump.split('\n'):
  126. if found:
  127. if len(out) >= MAX_FUNCTION_SIZE_LINES:
  128. break
  129. out.append(line)
  130. elif search in line:
  131. found = True
  132. return '\n'.join(out)
  133. def run_objdump(cmd):
  134. flags, target, restrict = cmd
  135. out = subprocess.check_output([binutils_prefix + "objdump"] + flags + [target], universal_newlines=True)
  136. if restrict is not None:
  137. return restrict_to_function(out, restrict)
  138. return out
  139. base_shift = eval_int(args.base_shift, "Failed to parse --base-shift (-S) argument as an integer.")
  140. def search_map_file(fn_name):
  141. if not mapfile:
  142. fail(f"No map file configured; cannot find function {fn_name}.")
  143. try:
  144. with open(mapfile) as f:
  145. lines = f.read().split('\n')
  146. except Exception:
  147. fail(f"Failed to open map file {mapfile} for reading.")
  148. try:
  149. cur_objfile = None
  150. ram_to_rom = None
  151. cands = []
  152. last_line = ''
  153. for line in lines:
  154. if line.startswith(' .text'):
  155. cur_objfile = line.split()[3]
  156. if 'load address' in line:
  157. tokens = last_line.split() + line.split()
  158. ram = int(tokens[1], 0)
  159. rom = int(tokens[5], 0)
  160. ram_to_rom = rom - ram
  161. if line.endswith(' ' + fn_name):
  162. ram = int(line.split()[0], 0)
  163. if cur_objfile is not None and ram_to_rom is not None:
  164. cands.append((cur_objfile, ram + ram_to_rom))
  165. last_line = line
  166. except Exception as e:
  167. import traceback
  168. traceback.print_exc()
  169. fail(f"Internal error while parsing map file")
  170. if len(cands) > 1:
  171. fail(f"Found multiple occurrences of function {fn_name} in map file.")
  172. if len(cands) == 1:
  173. return cands[0]
  174. return None, None
  175. def dump_objfile():
  176. if base_shift:
  177. fail("--base-shift not compatible with -o")
  178. if args.end is not None:
  179. fail("end address not supported together with -o")
  180. if args.start.startswith('0'):
  181. fail("numerical start address not supported with -o; pass a function name")
  182. objfile, _ = search_map_file(args.start)
  183. if not objfile:
  184. fail("Not able to find .o file for function.")
  185. if args.make:
  186. run_make(objfile)
  187. if not os.path.isfile(objfile):
  188. fail("Not able to find .o file for function.")
  189. refobjfile = "expected/" + objfile
  190. if not os.path.isfile(refobjfile):
  191. fail(f'Please ensure an OK .o file exists at "{refobjfile}".')
  192. objdump_flags = ["-drz"]
  193. return (
  194. objfile,
  195. (objdump_flags, refobjfile, args.start),
  196. (objdump_flags, objfile, args.start)
  197. )
  198. def dump_binary():
  199. if not baseimg or not myimg:
  200. fail("Missing myimg/baseimg in config.")
  201. if args.make:
  202. run_make(myimg)
  203. start_addr = eval_int(args.start)
  204. if start_addr is None:
  205. _, start_addr = search_map_file(args.start)
  206. if start_addr is None:
  207. fail("Not able to find function in map file.")
  208. if args.end is not None:
  209. end_addr = eval_int(args.end, "End address must be an integer expression.")
  210. else:
  211. end_addr = start_addr + MAX_FUNCTION_SIZE_BYTES
  212. objdump_flags = ['-Dz', '-bbinary', '-mmips', '-EB']
  213. flags1 = [f"--start-address={start_addr + base_shift}", f"--stop-address={end_addr + base_shift}"]
  214. flags2 = [f"--start-address={start_addr}", f"--stop-address={end_addr}"]
  215. return (
  216. myimg,
  217. (objdump_flags + flags1, baseimg, None),
  218. (objdump_flags + flags2, myimg, None)
  219. )
  220. # Alignment with ANSI colors is broken, let's fix it.
  221. def ansi_ljust(s, width):
  222. needed = width - ansiwrap.ansilen(s)
  223. if needed > 0:
  224. return s + ' ' * needed
  225. else:
  226. return s
  227. re_int = re.compile(r'[0-9]+')
  228. re_comments = re.compile(r'<.*?>')
  229. re_regs = re.compile(r'\b(a[0-3]|t[0-9]|s[0-7]|at|v[01]|f[12]?[0-9]|f3[01]|fp)\b')
  230. re_sprel = re.compile(r',([1-9][0-9]*|0x[1-9a-f][0-9a-f]*)\(sp\)')
  231. re_large_imm = re.compile(r'-?[1-9][0-9]{2,}|-?0x[0-9a-f]{3,}')
  232. forbidden = set(string.ascii_letters + '_')
  233. branch_likely_instructions = set([
  234. 'beql', 'bnel', 'beqzl', 'bnezl', 'bgezl', 'bgtzl', 'blezl', 'bltzl',
  235. 'bc1tl', 'bc1fl'
  236. ])
  237. branch_instructions = set([
  238. 'b', 'beq', 'bne', 'beqz', 'bnez', 'bgez', 'bgtz', 'blez', 'bltz',
  239. 'bc1t', 'bc1f'
  240. ] + list(branch_likely_instructions))
  241. def hexify_int(row, pat):
  242. full = pat.group(0)
  243. if len(full) <= 1:
  244. # leave one-digit ints alone
  245. return full
  246. start, end = pat.span()
  247. if start and row[start - 1] in forbidden:
  248. return full
  249. if end < len(row) and row[end] in forbidden:
  250. return full
  251. return hex(int(full))
  252. def parse_relocated_line(line):
  253. try:
  254. ind2 = line.rindex(',')
  255. except ValueError:
  256. ind2 = line.rindex('\t')
  257. before = line[:ind2+1]
  258. after = line[ind2+1:]
  259. ind2 = after.find('(')
  260. if ind2 == -1:
  261. imm, after = after, ''
  262. else:
  263. imm, after = after[:ind2], after[ind2:]
  264. if imm == '0x0':
  265. imm = '0'
  266. return before, imm, after
  267. def process_reloc(row, prev):
  268. before, imm, after = parse_relocated_line(prev)
  269. repl = row.split()[-1]
  270. if imm != '0':
  271. if before.strip() == 'jal' and not imm.startswith('0x'):
  272. imm = '0x' + imm
  273. repl += '+' + imm if int(imm,0) > 0 else imm
  274. if 'R_MIPS_LO16' in row:
  275. repl = f'%lo({repl})'
  276. elif 'R_MIPS_HI16' in row:
  277. # Ideally we'd pair up R_MIPS_LO16 and R_MIPS_HI16 to generate a
  278. # correct addend for each, but objdump doesn't give us the order of
  279. # the relocations, so we can't find the right LO16. :(
  280. repl = f'%hi({repl})'
  281. else:
  282. assert 'R_MIPS_26' in row, f"unknown relocation type '{row}'"
  283. return before + repl + after
  284. def process(lines):
  285. mnemonics = []
  286. diff_rows = []
  287. skip_next = False
  288. originals = []
  289. line_nums = []
  290. branch_targets = []
  291. if not args.diff_obj:
  292. lines = lines[7:]
  293. if lines and not lines[-1]:
  294. lines.pop()
  295. for row in lines:
  296. if args.diff_obj and ('>:' in row or not row):
  297. continue
  298. if 'R_MIPS_' in row:
  299. if diff_rows[-1] != '<delay-slot>':
  300. diff_rows[-1] = process_reloc(row, diff_rows[-1])
  301. originals[-1] = process_reloc(row, originals[-1])
  302. continue
  303. row = re.sub(re_comments, '', row)
  304. row = row.rstrip()
  305. tabs = row.split('\t')
  306. row = '\t'.join(tabs[2:])
  307. line_num = tabs[0].strip()
  308. row_parts = row.split('\t', 1)
  309. mnemonic = row_parts[0].strip()
  310. if mnemonic not in branch_instructions:
  311. row = re.sub(re_int, lambda s: hexify_int(row, s), row)
  312. original = row
  313. if skip_next:
  314. skip_next = False
  315. row = '<delay-slot>'
  316. mnemonic = '<delay-slot>'
  317. if mnemonic in branch_likely_instructions:
  318. skip_next = True
  319. row = re.sub(re_regs, '<reg>', row)
  320. row = re.sub(re_sprel, ',addr(sp)', row)
  321. if args.ignore_large_imms:
  322. row = re.sub(re_large_imm, '<imm>', row)
  323. # Replace tabs with spaces
  324. mnemonics.append(mnemonic)
  325. diff_rows.append(row)
  326. originals.append(original)
  327. line_nums.append(line_num)
  328. if mnemonic in branch_instructions:
  329. target = row_parts[1].strip().split(',')[-1]
  330. if mnemonic in branch_likely_instructions:
  331. target = hex(int(target, 16) - 4)[2:]
  332. branch_targets.append(target)
  333. else:
  334. branch_targets.append(None)
  335. if args.stop_jrra and mnemonic == 'jr' and row_parts[1].strip() == 'ra':
  336. break
  337. # Cleanup whitespace
  338. originals = [original.strip() for original in originals]
  339. originals = [''.join(f'{o:<8s}' for o in original.split('\t')) for original in originals]
  340. # return diff_rows, diff_rows, line_nums
  341. return mnemonics, diff_rows, originals, line_nums, branch_targets
  342. def format_single_line_diff(line1, line2, column_width):
  343. return f"{ansi_ljust(line1,column_width)}{ansi_ljust(line2,column_width)}"
  344. class SymbolColorer:
  345. def __init__(self, base_index):
  346. self.color_index = base_index
  347. self.symbol_colors = {}
  348. def color_symbol(self, s, t=None):
  349. try:
  350. color = self.symbol_colors[s]
  351. except:
  352. color = COLOR_ROTATION[self.color_index % len(COLOR_ROTATION)]
  353. self.color_index += 1
  354. self.symbol_colors[s] = color
  355. t = t or s
  356. return f'{color}{t}{Fore.RESET}'
  357. def normalize_large_imms(row):
  358. if args.ignore_large_imms:
  359. row = re.sub(re_large_imm, '<imm>', row)
  360. return row
  361. def do_diff(basedump, mydump):
  362. asm_lines1 = basedump.split('\n')
  363. asm_lines2 = mydump.split('\n')
  364. output = []
  365. # TODO: status line?
  366. # output.append(sha1sum(mydump))
  367. mnemonics1, asm_lines1, originals1, line_nums1, branch_targets1 = process(asm_lines1)
  368. mnemonics2, asm_lines2, originals2, line_nums2, branch_targets2 = process(asm_lines2)
  369. sc1 = SymbolColorer(0)
  370. sc2 = SymbolColorer(0)
  371. sc3 = SymbolColorer(4)
  372. sc4 = SymbolColorer(4)
  373. sc5 = SymbolColorer(0)
  374. sc6 = SymbolColorer(0)
  375. bts1 = set()
  376. bts2 = set()
  377. if args.show_branches:
  378. for (bts, btset, sc) in [(branch_targets1, bts1, sc5), (branch_targets2, bts2, sc6)]:
  379. for bt in bts:
  380. if bt is not None:
  381. btset.add(bt + ":")
  382. sc.color_symbol(bt + ":")
  383. differ: difflib.SequenceMatcher = difflib.SequenceMatcher(a=mnemonics1, b=mnemonics2, autojunk=False)
  384. for (tag, i1, i2, j1, j2) in differ.get_opcodes():
  385. lines1 = asm_lines1[i1:i2]
  386. lines2 = asm_lines2[j1:j2]
  387. for k, (line1, line2) in enumerate(itertools.zip_longest(lines1, lines2)):
  388. if tag == 'replace':
  389. if line1 is None:
  390. tag = 'insert'
  391. elif line2 is None:
  392. tag = 'delete'
  393. try:
  394. original1 = originals1[i1+k]
  395. line_num1 = line_nums1[i1+k]
  396. except:
  397. original1 = ''
  398. line_num1 = ''
  399. try:
  400. original2 = originals2[j1+k]
  401. line_num2 = line_nums2[j1+k]
  402. except:
  403. original2 = ''
  404. line_num2 = ''
  405. line_color = Fore.RESET
  406. line_prefix = ' '
  407. if line1 == line2:
  408. if normalize_large_imms(original1) == normalize_large_imms(original2):
  409. out1 = f'{original1}'
  410. out2 = f'{original2}'
  411. elif line1 == '<delay-slot>':
  412. out1 = f'{Style.DIM}{original1}'
  413. out2 = f'{Style.DIM}{original2}'
  414. else:
  415. line_color = Fore.YELLOW
  416. line_prefix = 'r'
  417. out1 = f'{Fore.YELLOW}{original1}{Style.RESET_ALL}'
  418. out2 = f'{Fore.YELLOW}{original2}{Style.RESET_ALL}'
  419. out1 = re.sub(re_regs, lambda s: sc1.color_symbol(s.group()), out1)
  420. out2 = re.sub(re_regs, lambda s: sc2.color_symbol(s.group()), out2)
  421. out1 = re.sub(re_sprel, lambda s: sc3.color_symbol(s.group()), out1)
  422. out2 = re.sub(re_sprel, lambda s: sc4.color_symbol(s.group()), out2)
  423. elif tag in ['replace', 'equal']:
  424. line_prefix = '|'
  425. line_color = Fore.BLUE
  426. out1 = f"{Fore.BLUE}{original1}{Style.RESET_ALL}"
  427. out2 = f"{Fore.BLUE}{original2}{Style.RESET_ALL}"
  428. elif tag == 'delete':
  429. line_prefix = '<'
  430. line_color = Fore.RED
  431. out1 = f"{Fore.RED}{original1}{Style.RESET_ALL}"
  432. out2 = ''
  433. elif tag == 'insert':
  434. line_prefix = '>'
  435. line_color = Fore.GREEN
  436. out1 = ''
  437. out2 = f"{Fore.GREEN}{original2}{Style.RESET_ALL}"
  438. in_arrow1 = ' '
  439. in_arrow2 = ' '
  440. out_arrow1 = ''
  441. out_arrow2 = ''
  442. line_num1 = line_num1 if out1 else ''
  443. line_num2 = line_num2 if out2 else ''
  444. if args.show_branches and out1:
  445. if line_num1 in bts1:
  446. in_arrow1 = sc5.color_symbol(line_num1, '~>')
  447. if branch_targets1[i1+k] is not None:
  448. out_arrow1 = ' ' + sc5.color_symbol(branch_targets1[i1+k] + ":", '~>')
  449. if args.show_branches and out2:
  450. if line_num2 in bts2:
  451. in_arrow2 = sc6.color_symbol(line_num2, '~>')
  452. if branch_targets2[j1+k] is not None:
  453. out_arrow2 = ' ' + sc6.color_symbol(branch_targets2[j1+k] + ":", '~>')
  454. out1 = f"{line_color}{line_num1} {in_arrow1} {out1}{Style.RESET_ALL}{out_arrow1}"
  455. out2 = f"{line_color}{line_prefix} {line_num2} {in_arrow2} {out2}{Style.RESET_ALL}{out_arrow2}"
  456. output.append(format_single_line_diff(out1, out2, args.column_width))
  457. return output[args.skip_lines:]
  458. def debounced_fs_watch(targets, outq, debounce_delay):
  459. import watchdog.events
  460. import watchdog.observers
  461. class WatchEventHandler(watchdog.events.FileSystemEventHandler):
  462. def __init__(self, queue, file_targets):
  463. self.queue = queue
  464. self.file_targets = file_targets
  465. def on_modified(self, ev):
  466. if isinstance(ev, watchdog.events.FileModifiedEvent):
  467. self.changed(ev.src_path)
  468. def on_moved(self, ev):
  469. if isinstance(ev, watchdog.events.FileMovedEvent):
  470. self.changed(ev.dest_path)
  471. def should_notify(self, path):
  472. for target in self.file_targets:
  473. if path == target:
  474. return True
  475. if args.make and any(path.endswith(suffix) for suffix in FS_WATCH_EXTENSIONS):
  476. return True
  477. return False
  478. def changed(self, path):
  479. if self.should_notify(path):
  480. self.queue.put(time.time())
  481. def debounce_thread():
  482. listenq = queue.Queue()
  483. file_targets = []
  484. event_handler = WatchEventHandler(listenq, file_targets)
  485. observer = watchdog.observers.Observer()
  486. observed = set()
  487. for target in targets:
  488. if os.path.isdir(target):
  489. observer.schedule(event_handler, target, recursive=True)
  490. else:
  491. file_targets.append(target)
  492. target = os.path.dirname(target)
  493. if target not in observed:
  494. observed.add(target)
  495. observer.schedule(event_handler, target)
  496. observer.start()
  497. while True:
  498. t = listenq.get()
  499. more = True
  500. while more:
  501. delay = t + debounce_delay - time.time()
  502. if delay > 0:
  503. time.sleep(delay)
  504. # consume entire queue
  505. more = False
  506. try:
  507. while True:
  508. t = listenq.get(block=False)
  509. more = True
  510. except queue.Empty:
  511. pass
  512. outq.put(t)
  513. th = threading.Thread(target=debounce_thread, daemon=True)
  514. th.start()
  515. class Display():
  516. def __init__(self, basedump, mydump):
  517. self.basedump = basedump
  518. self.mydump = mydump
  519. self.emsg = None
  520. def run_less(self):
  521. if self.emsg is not None:
  522. output = self.emsg
  523. else:
  524. output = '\n'.join(do_diff(self.basedump, self.mydump))
  525. # Pipe the output through 'tail' and only then to less, to ensure the
  526. # write call doesn't block. ('tail' has to buffer all its input before
  527. # it starts writing.) This also means we don't have to deal with pipe
  528. # closure errors.
  529. buffer_proc = subprocess.Popen(BUFFER_CMD, stdin=subprocess.PIPE,
  530. stdout=subprocess.PIPE)
  531. less_proc = subprocess.Popen(LESS_CMD, stdin=buffer_proc.stdout)
  532. buffer_proc.stdin.write(output.encode())
  533. buffer_proc.stdin.close()
  534. buffer_proc.stdout.close()
  535. return (buffer_proc, less_proc)
  536. def run_sync(self):
  537. proca, procb = self.run_less()
  538. procb.wait()
  539. proca.wait()
  540. def run_async(self, watch_queue):
  541. self.watch_queue = watch_queue
  542. self.ready_queue = queue.Queue()
  543. self.pending_update = None
  544. dthread = threading.Thread(target=self.display_thread)
  545. dthread.start()
  546. self.ready_queue.get()
  547. def display_thread(self):
  548. proca, procb = self.run_less()
  549. self.less_proc = procb
  550. self.ready_queue.put(0)
  551. while True:
  552. ret = procb.wait()
  553. proca.wait()
  554. self.less_proc = None
  555. if ret != 0:
  556. # fix the terminal
  557. os.system("tput reset")
  558. if ret != 0 and self.pending_update is not None:
  559. # killed by program with the intent to refresh
  560. msg, error = self.pending_update
  561. self.pending_update = None
  562. if not error:
  563. self.mydump = msg
  564. self.emsg = None
  565. else:
  566. self.emsg = msg
  567. proca, procb = self.run_less()
  568. self.less_proc = procb
  569. self.ready_queue.put(0)
  570. else:
  571. # terminated by user, or killed
  572. self.watch_queue.put(None)
  573. self.ready_queue.put(0)
  574. break
  575. def progress(self, msg):
  576. # Write message to top-left corner
  577. sys.stdout.write("\x1b7\x1b[1;1f{}\x1b8".format(msg + " "))
  578. sys.stdout.flush()
  579. def update(self, text, error):
  580. if not error and not self.emsg and text == self.mydump:
  581. self.progress("Unchanged. ")
  582. return
  583. self.pending_update = (text, error)
  584. if not self.less_proc:
  585. return
  586. self.less_proc.kill()
  587. self.ready_queue.get()
  588. def terminate(self):
  589. if not self.less_proc:
  590. return
  591. self.less_proc.kill()
  592. self.ready_queue.get()
  593. def main():
  594. if args.diff_obj:
  595. make_target, basecmd, mycmd = dump_objfile()
  596. else:
  597. make_target, basecmd, mycmd = dump_binary()
  598. if args.write_asm is not None:
  599. mydump = run_objdump(mycmd)
  600. with open(args.write_asm) as f:
  601. f.write(mydump)
  602. print(f"Wrote assembly to {args.write_asm}.")
  603. sys.exit(0)
  604. if args.base_asm is not None:
  605. with open(args.base_asm) as f:
  606. basedump = f.read()
  607. else:
  608. basedump = run_objdump(basecmd)
  609. mydump = run_objdump(mycmd)
  610. display = Display(basedump, mydump)
  611. if not args.watch:
  612. display.run_sync()
  613. else:
  614. if not args.make:
  615. yn = input("Warning: watch-mode (-w) enabled without auto-make (-m). You will have to run make manually. Ok? (Y/n) ")
  616. if yn.lower() == 'n':
  617. return
  618. if args.make:
  619. watch_sources = None
  620. if hasattr(diff_settings, "watch_sources_for_target"):
  621. watch_sources = diff_settings.watch_sources_for_target(make_target)
  622. watch_sources = watch_sources or source_directories
  623. if not watch_sources:
  624. fail("Missing source_directories config, don't know what to watch.")
  625. else:
  626. watch_sources = [make_target]
  627. q = queue.Queue()
  628. debounced_fs_watch(watch_sources, q, DEBOUNCE_DELAY)
  629. display.run_async(q)
  630. last_build = 0
  631. try:
  632. while True:
  633. t = q.get()
  634. if t is None:
  635. break
  636. if t < last_build:
  637. continue
  638. last_build = time.time()
  639. if args.make:
  640. display.progress("Building...")
  641. ret = run_make(make_target, capture_output=True)
  642. if ret.returncode != 0:
  643. display.update(ret.stderr.decode() or ret.stdout.decode(), error=True)
  644. continue
  645. mydump = run_objdump(mycmd)
  646. display.update(mydump, error=False)
  647. except KeyboardInterrupt:
  648. display.terminate()
  649. main()