|
28 | 28 | "outputs": [], |
29 | 29 | "source": [ |
30 | 30 | "#| export\n", |
31 | | - "import time,os,sys,traceback,contextlib,inspect,signal,asyncio\n", |
| 31 | + "import time,os,sys,io,traceback,contextlib,inspect,signal,asyncio\n", |
32 | 32 | "from fastcore.basics import *\n", |
33 | 33 | "from fastcore.imports import *\n", |
34 | 34 | "from fastcore.foundation import *\n", |
|
50 | 50 | "id": "7fe8a875", |
51 | 51 | "metadata": {}, |
52 | 52 | "source": [ |
53 | | - "When a test run hangs, ctrl-c should say what each notebook was executing. Each worker installs a SIGINT handler that prints the in-flight notebook's stack as a single block (one `os.write`, so parallel workers' dumps don't interleave) and then exits immediately, rather than letting the interrupt surface inside the cell where the shell would catch it and carry on. An idle worker exits silently." |
| 53 | + "When a test run hangs, ctrl-c should say what each notebook was executing. Each worker installs a SIGINT handler that prints the in-flight notebook's stack — the sync frames plus every pending asyncio task, since a hang inside an awaited coroutine lives on the task, not the sync stack — as a single block (one `os.write`, so parallel workers' dumps don't interleave) and then exits immediately, rather than letting the interrupt surface inside the cell where the shell would catch it and carry on. An idle worker exits silently." |
54 | 54 | ] |
55 | 55 | }, |
56 | 56 | { |
|
63 | 63 | "#| export\n", |
64 | 64 | "_cur_nb = [None]\n", |
65 | 65 | "\n", |
| 66 | + "def _await_chain(t):\n", |
| 67 | + " \"One frame per coroutine in task `t`'s await chain, deepest last: where a suspended hang actually sits\"\n", |
| 68 | + " co = t.get_coro()\n", |
| 69 | + " while co is not None:\n", |
| 70 | + " f = getattr(co, 'cr_frame', None) or getattr(co, 'ag_frame', None) or getattr(co, 'gi_frame', None)\n", |
| 71 | + " if f is not None: yield f, f.f_lineno\n", |
| 72 | + " co = getattr(co, 'cr_await', None) or getattr(co, 'ag_await', None) or getattr(co, 'gi_yieldfrom', None)\n", |
| 73 | + "\n", |
66 | 74 | "def _int_handler(signum, frame):\n", |
67 | 75 | " \"Dump the running notebook's stack and exit; installed on SIGINT by `test_nb`\"\n", |
68 | 76 | " if _cur_nb[0] is not None:\n", |
69 | | - " stk = ''.join(traceback.format_stack(frame))\n", |
70 | | - " os.write(2, f'\\n=== nbdev-test interrupted: {_cur_nb[0]} ===\\n{stk}'.encode())\n", |
| 77 | + " buf = io.StringIO()\n", |
| 78 | + " traceback.print_stack(frame, file=buf)\n", |
| 79 | + " with contextlib.suppress(RuntimeError): # no running loop: sync frames already cover it\n", |
| 80 | + " for t in asyncio.all_tasks():\n", |
| 81 | + " print(f'\\n{t}', file=buf)\n", |
| 82 | + " buf.writelines(traceback.StackSummary.extract(_await_chain(t)).format())\n", |
| 83 | + " os.write(2, f'\\n=== nbdev-test interrupted: {_cur_nb[0]} ===\\n{buf.getvalue()}'.encode())\n", |
71 | 84 | " os._exit(130)" |
72 | 85 | ] |
73 | 86 | }, |
|
0 commit comments