m2drive.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. #!/usr/bin/env python3
  2. """m2drive.py — pty driver for scripted Turbo Modula-2 (cpmulator) sessions.
  3. Usage: m2drive.py <drive-dir> <script.txt> [--timeout N]
  4. script.txt lines: EXPECT <regex> | SEND <text-with-\\r-\\n> | SLEEP <sec> | LOG <note>
  5. Matching is done on a cleaned screen buffer with an advancing cursor, so
  6. repeated EXPECTs (e.g. several "SYM produced") each wait for FRESH output.
  7. Exits 0 iff every EXPECT matched in order.
  8. """
  9. import os, pty, re, select, sys, time
  10. ANSI = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]|\x1b[()][AB0]')
  11. def main():
  12. drv, script = sys.argv[1], sys.argv[2]
  13. timeout = float(sys.argv[4]) if len(sys.argv) > 4 else 10.0
  14. steps = []
  15. for line in open(script):
  16. line = line.rstrip('\n')
  17. if not line or line.startswith('#'):
  18. continue
  19. cmd, _, arg = line.partition(' ')
  20. steps.append((cmd, arg))
  21. pid, fd = pty.fork()
  22. if pid == 0:
  23. os.execvp('/home/eric/bin/CPM/Cpmulator/cpmulator',
  24. ['cpmulator', '-drive-d', drv])
  25. clean = b'' # ANSI-stripped screen, consumed as matched
  26. try:
  27. for cmd, arg in steps:
  28. if cmd == 'SEND':
  29. data = arg.encode().decode('unicode_escape').encode('latin1')
  30. os.write(fd, data)
  31. time.sleep(1.0)
  32. clean += drain(fd, 1.0)
  33. elif cmd == 'SLEEP':
  34. clean += drain(fd, float(arg))
  35. elif cmd == 'EXPECT':
  36. t0 = time.time()
  37. while True:
  38. m = re.search(arg.encode(), clean)
  39. if m:
  40. clean = clean[m.end():]
  41. break
  42. if time.time() - t0 > timeout:
  43. print(f'MISS: {arg}')
  44. print('--- screen tail ---')
  45. print(clean[-2000:].decode('latin1', 'replace'))
  46. return 1
  47. clean += drain(fd, 0.5)
  48. print(f'HIT: {arg}', flush=True)
  49. elif cmd == 'LOG':
  50. print(f'# {arg}', flush=True)
  51. print('--- done ok ---')
  52. print('--- screen tail ---')
  53. print(cleantail(clean, 1500))
  54. return 0
  55. finally:
  56. try:
  57. os.close(fd)
  58. except OSError:
  59. pass
  60. def cleantail(clean, n):
  61. return clean[-n:].decode('latin1', 'replace')
  62. def drain(fd, wait):
  63. """Read available pty output for up to `wait` seconds, ANSI-stripped."""
  64. out = b''
  65. end = time.time() + wait
  66. while time.time() < end:
  67. r, _, _ = select.select([fd], [], [], max(0, end - time.time()))
  68. if not r:
  69. continue
  70. try:
  71. chunk = os.read(fd, 65536)
  72. except OSError:
  73. break
  74. if not chunk:
  75. break
  76. out += chunk
  77. return ANSI.sub(b'', out)
  78. sys.exit(main())