|
1 | 1 | import unittest |
2 | 2 | import os |
| 3 | +import json |
3 | 4 | import textwrap |
4 | 5 | import contextlib |
5 | 6 | import importlib |
@@ -418,6 +419,91 @@ def _frame_to_lineno_tuple(frame): |
418 | 419 | filename, location, funcname, opcode = frame |
419 | 420 | return (filename, location.lineno, funcname, opcode) |
420 | 421 |
|
| 422 | + @contextmanager |
| 423 | + def _poisoned_debug_offsets_process(self, poison_code): |
| 424 | + script = ( |
| 425 | + textwrap.dedent( |
| 426 | + """\ |
| 427 | + import json |
| 428 | + import os |
| 429 | + import struct |
| 430 | + import sys |
| 431 | + import threading |
| 432 | + import time |
| 433 | +
|
| 434 | + cookie = b"xdebugpy" |
| 435 | + pid = os.getpid() |
| 436 | +
|
| 437 | + def find_debug_offsets(): |
| 438 | + with open(f"/proc/{pid}/maps") as f: |
| 439 | + for line in f: |
| 440 | + parts = line.split() |
| 441 | + if len(parts) < 2 or "rw" not in parts[1]: |
| 442 | + continue |
| 443 | + start, end = (int(x, 16) for x in parts[0].split("-")) |
| 444 | + if end - start > 10_000_000: |
| 445 | + continue |
| 446 | + try: |
| 447 | + fd = os.open(f"/proc/{pid}/mem", os.O_RDONLY) |
| 448 | + os.lseek(fd, start, 0) |
| 449 | + data = os.read(fd, end - start) |
| 450 | + os.close(fd) |
| 451 | + except OSError: |
| 452 | + continue |
| 453 | + off = data.find(cookie) |
| 454 | + if off == -1: |
| 455 | + continue |
| 456 | + version = struct.unpack_from("<Q", data, off + 8)[0] |
| 457 | + if ((version >> 24) & 0xFF) != sys.version_info.major: |
| 458 | + continue |
| 459 | + return start + off |
| 460 | + raise RuntimeError("debug offsets not found") |
| 461 | +
|
| 462 | + addr = find_debug_offsets() |
| 463 | + """ |
| 464 | + ) |
| 465 | + + textwrap.dedent(poison_code) |
| 466 | + + "\n" |
| 467 | + + textwrap.dedent( |
| 468 | + """\ |
| 469 | + print( |
| 470 | + json.dumps({"pid": pid, "native_tid": threading.get_native_id()}), |
| 471 | + flush=True, |
| 472 | + ) |
| 473 | + time.sleep(60) |
| 474 | + """ |
| 475 | + ) |
| 476 | + ) |
| 477 | + |
| 478 | + proc = subprocess.Popen( |
| 479 | + [sys.executable, "-c", script], |
| 480 | + stdout=subprocess.PIPE, |
| 481 | + stderr=subprocess.PIPE, |
| 482 | + text=True, |
| 483 | + ) |
| 484 | + try: |
| 485 | + line = proc.stdout.readline() |
| 486 | + if not line: |
| 487 | + stderr = proc.stderr.read() |
| 488 | + self.fail( |
| 489 | + "poisoned child failed to initialize: " |
| 490 | + f"{stderr.strip() or 'no stderr output'}" |
| 491 | + ) |
| 492 | + yield proc, json.loads(line) |
| 493 | + finally: |
| 494 | + try: |
| 495 | + proc.terminate() |
| 496 | + proc.wait(timeout=SHORT_TIMEOUT) |
| 497 | + except subprocess.TimeoutExpired: |
| 498 | + proc.kill() |
| 499 | + proc.wait(timeout=SHORT_TIMEOUT) |
| 500 | + except OSError: |
| 501 | + pass |
| 502 | + if proc.stdout is not None: |
| 503 | + proc.stdout.close() |
| 504 | + if proc.stderr is not None: |
| 505 | + proc.stderr.close() |
| 506 | + |
421 | 507 | def _extract_coroutine_stacks_lineno_only(self, stack_trace): |
422 | 508 | """Extract coroutine stacks with line numbers only (no column offsets). |
423 | 509 |
|
|
0 commit comments