pyflyby 1.10.4__cp311-cp311-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. pyflyby/__init__.py +61 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2228 -0
  4. pyflyby/_cmdline.py +591 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1383 -0
  7. pyflyby/_dynimp.py +154 -0
  8. pyflyby/_fast_iter_modules.cpython-311-darwin.so +0 -0
  9. pyflyby/_file.py +771 -0
  10. pyflyby/_flags.py +230 -0
  11. pyflyby/_format.py +186 -0
  12. pyflyby/_idents.py +227 -0
  13. pyflyby/_import_sorting.py +165 -0
  14. pyflyby/_importclns.py +658 -0
  15. pyflyby/_importdb.py +535 -0
  16. pyflyby/_imports2s.py +643 -0
  17. pyflyby/_importstmt.py +723 -0
  18. pyflyby/_interactive.py +2113 -0
  19. pyflyby/_livepatch.py +793 -0
  20. pyflyby/_log.py +107 -0
  21. pyflyby/_modules.py +646 -0
  22. pyflyby/_parse.py +1396 -0
  23. pyflyby/_py.py +2165 -0
  24. pyflyby/_saveframe.py +1145 -0
  25. pyflyby/_saveframe_reader.py +471 -0
  26. pyflyby/_util.py +458 -0
  27. pyflyby/_version.py +8 -0
  28. pyflyby/autoimport.py +20 -0
  29. pyflyby/etc/pyflyby/canonical.py +10 -0
  30. pyflyby/etc/pyflyby/common.py +27 -0
  31. pyflyby/etc/pyflyby/forget.py +10 -0
  32. pyflyby/etc/pyflyby/mandatory.py +10 -0
  33. pyflyby/etc/pyflyby/numpy.py +156 -0
  34. pyflyby/etc/pyflyby/std.py +335 -0
  35. pyflyby/importdb.py +19 -0
  36. pyflyby/libexec/pyflyby/colordiff +34 -0
  37. pyflyby/libexec/pyflyby/diff-colorize +148 -0
  38. pyflyby/share/emacs/site-lisp/pyflyby.el +112 -0
  39. pyflyby-1.10.4.data/scripts/collect-exports +76 -0
  40. pyflyby-1.10.4.data/scripts/collect-imports +58 -0
  41. pyflyby-1.10.4.data/scripts/find-import +38 -0
  42. pyflyby-1.10.4.data/scripts/prune-broken-imports +34 -0
  43. pyflyby-1.10.4.data/scripts/pyflyby-diff +34 -0
  44. pyflyby-1.10.4.data/scripts/reformat-imports +27 -0
  45. pyflyby-1.10.4.data/scripts/replace-star-imports +37 -0
  46. pyflyby-1.10.4.data/scripts/saveframe +299 -0
  47. pyflyby-1.10.4.data/scripts/tidy-imports +170 -0
  48. pyflyby-1.10.4.data/scripts/transform-imports +47 -0
  49. pyflyby-1.10.4.dist-info/METADATA +605 -0
  50. pyflyby-1.10.4.dist-info/RECORD +53 -0
  51. pyflyby-1.10.4.dist-info/WHEEL +6 -0
  52. pyflyby-1.10.4.dist-info/entry_points.txt +4 -0
  53. pyflyby-1.10.4.dist-info/licenses/LICENSE.txt +19 -0
pyflyby/_dbg.py ADDED
@@ -0,0 +1,1383 @@
1
+ # pyflyby/_dbg.py.
2
+ # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2018 Karl Chen.
3
+ # License: MIT http://opensource.org/licenses/MIT
4
+
5
+
6
+ import builtins
7
+ from contextlib import contextmanager
8
+ import errno
9
+ from functools import wraps
10
+ import os
11
+ import pwd
12
+ import signal
13
+ import sys
14
+ import time
15
+ from types import CodeType, FrameType, TracebackType
16
+
17
+ from collections.abc import Callable
18
+
19
+ from pyflyby._file import Filename
20
+
21
+
22
+ """
23
+ Used by wait_for_debugger_to_attach to record whether we're waiting to attach,
24
+ and if so what.
25
+ """
26
+ _waiting_for_debugger = None
27
+
28
+
29
+ _ORIG_SYS_EXCEPTHOOK = sys.excepthook
30
+
31
+ def _reset_excepthook():
32
+ if _ORIG_SYS_EXCEPTHOOK:
33
+ sys.excepthook = _ORIG_SYS_EXCEPTHOOK
34
+ return True
35
+ return False
36
+
37
+
38
+ def _override_excepthook(hook):
39
+ """
40
+ Override sys.excepthook with `hook` but also support resetting.
41
+
42
+ Users should call this function instead of directly overiding
43
+ sys.excepthook. This is helpful in resetting sys.excepthook in certain cases.
44
+ """
45
+ global _ORIG_SYS_EXCEPTHOOK
46
+ _ORIG_SYS_EXCEPTHOOK = hook
47
+ sys.excepthook = hook
48
+
49
+
50
+ class _NoTtyError(Exception):
51
+ pass
52
+
53
+
54
+ _memoized_dev_tty_fd = Ellipsis
55
+ def _dev_tty_fd():
56
+ """
57
+ Return a file descriptor opened to /dev/tty.
58
+ Memoized.
59
+ """
60
+ global _memoized_dev_tty_fd
61
+ if _memoized_dev_tty_fd is Ellipsis:
62
+ try:
63
+ _memoized_dev_tty_fd = os.open("/dev/tty", os.O_RDWR)
64
+ except OSError:
65
+ _memoized_dev_tty_fd = None
66
+ if _memoized_dev_tty_fd is None:
67
+ raise _NoTtyError
68
+ return _memoized_dev_tty_fd
69
+
70
+
71
+ def tty_is_usable():
72
+ """
73
+ Return whether /dev/tty is usable.
74
+
75
+ In interactive sessions, /dev/tty is usable; in non-interactive sessions,
76
+ /dev/tty is not usable::
77
+
78
+ $ ssh -t localhost py -q pyflyby._dbg.tty_is_usable
79
+ True
80
+
81
+ $ ssh -T localhost py -q pyflyby._dbg.tty_is_usable
82
+ False
83
+
84
+ tty_is_usable() is useful for deciding whether we are in an interactive
85
+ terminal. In an interactive terminal we can enter the debugger directly;
86
+ in a non-interactive terminal, we need to wait_for_debugger_to_attach.
87
+
88
+ Note that this is different from doing e.g. isatty(0). isatty would
89
+ return False if a program was piped, even though /dev/tty is usable.
90
+ """
91
+ try:
92
+ _dev_tty_fd()
93
+ return True
94
+ except _NoTtyError:
95
+ return False
96
+
97
+
98
+ @contextmanager
99
+ def _FdCtx(target_fd, src_fd):
100
+ assert target_fd != src_fd
101
+ saved_fd = os.dup(target_fd)
102
+ assert saved_fd > 2, "saved_fd == %d" % (saved_fd,)
103
+ assert saved_fd != target_fd and saved_fd != src_fd
104
+ os.dup2(src_fd, target_fd)
105
+ try:
106
+ yield
107
+ finally:
108
+ os.dup2(saved_fd, target_fd)
109
+
110
+
111
+ _in_StdioCtx = []
112
+
113
+ @contextmanager
114
+ def _StdioCtx(tty="/dev/tty"):
115
+ '''
116
+ Within the context, force fd {0, 1, 2}, sys.__{stdin,stdout,stderr}__,
117
+ sys.{stdin,stdout,stderr} to fd. This allows us to use the debugger even
118
+ if stdio is otherwise redirected.
119
+
120
+ :type tty:
121
+ ``int`` or ``str``
122
+ :param tty:
123
+ Tty to use. Either a file descriptor or a name of a tty.
124
+ '''
125
+ from ._interactive import UpdateIPythonStdioCtx
126
+ to_close = None
127
+ if isinstance(tty, int):
128
+ fd = tty
129
+ elif isinstance(tty, str):
130
+ if tty == "/dev/tty":
131
+ fd = _dev_tty_fd()
132
+ else:
133
+ fd = os.open(tty, os.O_RDWR)
134
+ to_close = fd
135
+ else:
136
+ raise TypeError("_StdioCtx(): tty should be an int or str")
137
+ if _in_StdioCtx and _in_StdioCtx[-1] == fd:
138
+ # Same context; do nothing.
139
+ assert to_close is None
140
+ yield
141
+ return
142
+ if not fd > 2:
143
+ raise ValueError("_StdioCtx: unsafe to use fd<=2; fd==%d" % (fd,))
144
+ _in_StdioCtx.append(fd)
145
+ saved_stdin = sys.stdin
146
+ saved_stdin__ = sys.__stdin__
147
+ saved_stdout = sys.stdout
148
+ saved_stdout__ = sys.__stdout__
149
+ saved_stderr = sys.stderr
150
+ saved_stderr__ = sys.__stderr__
151
+ try:
152
+ sys.stdout.flush(); sys.__stdout__.flush()
153
+ sys.stderr.flush(); sys.__stderr__.flush()
154
+ from ._util import nested
155
+ with nested(_FdCtx(0, fd), _FdCtx(1, fd), _FdCtx(2, fd)):
156
+ with nested(os.fdopen(0, 'r'),
157
+ os.fdopen(1, 'w'),
158
+ os.fdopen(2, 'w', 1)) as (fd0, fd1, fd2):
159
+ sys.stdin = sys.__stdin__ = fd0
160
+ sys.stdout = sys.__stdout__ = fd1
161
+ sys.stderr = sys.__stderr__ = fd2
162
+ # Update IPython's stdin/stdout/stderr temporarily.
163
+ with UpdateIPythonStdioCtx():
164
+ yield
165
+ finally:
166
+ assert _in_StdioCtx and _in_StdioCtx[-1] == fd
167
+ _in_StdioCtx.pop(-1)
168
+ sys.stdin = saved_stdin
169
+ sys.__stdin__ = saved_stdin__
170
+ sys.stdout = saved_stdout
171
+ sys.__stdout__ = saved_stdout__
172
+ sys.stderr = saved_stderr
173
+ sys.__stderr__ = saved_stderr__
174
+ if to_close is not None:
175
+ try:
176
+ os.close(to_close)
177
+ except (OSError, IOError):
178
+ pass
179
+
180
+
181
+ @contextmanager
182
+ def _ExceptHookCtx():
183
+ '''
184
+ Context manager that restores ``sys.excepthook`` upon exit.
185
+ '''
186
+ saved_excepthook = sys.excepthook
187
+ try:
188
+ # TODO: should we set sys.excepthook = sys.__excepthook__ ?
189
+ yield
190
+ finally:
191
+ sys.excepthook = saved_excepthook
192
+
193
+
194
+ @contextmanager
195
+ def _DisplayHookCtx():
196
+ '''
197
+ Context manager that resets ``sys.displayhook`` to the default value upon
198
+ entry, and restores the pre-context value upon exit.
199
+ '''
200
+ saved_displayhook = sys.displayhook
201
+ try:
202
+ sys.displayhook = sys.__displayhook__
203
+ yield
204
+ finally:
205
+ sys.displayhook = saved_displayhook
206
+
207
+
208
+ def print_traceback(*exc_info):
209
+ """
210
+ Print a traceback, using IPython's ultraTB if possible.
211
+
212
+ Output goes to /dev/tty.
213
+
214
+ :param exc_info:
215
+ 3 arguments as returned by sys.exc_info().
216
+ """
217
+ from pyflyby._interactive import print_verbose_tb
218
+ if not exc_info:
219
+ exc_info = sys.exc_info()
220
+ with _StdioCtx():
221
+ print_verbose_tb(*exc_info)
222
+
223
+
224
+ @contextmanager
225
+ def _DebuggerCtx(tty="/dev/tty"):
226
+ """
227
+ A context manager that sets up the environment (stdio, sys hooks) for a
228
+ debugger, initializes IPython if necessary, and creates a debugger instance.
229
+
230
+ :return:
231
+ Context manager that yields a Pdb instance.
232
+ """
233
+ from pyflyby._interactive import new_IPdb_instance
234
+ with _StdioCtx(tty):
235
+ with _ExceptHookCtx():
236
+ with _DisplayHookCtx():
237
+ pdb = new_IPdb_instance()
238
+ pdb.reset()
239
+ yield pdb
240
+
241
+
242
+ def _get_caller_frame():
243
+ '''
244
+ Get the closest frame from outside this module.
245
+
246
+ :rtype:
247
+ ``FrameType``
248
+ '''
249
+ this_filename = _get_caller_frame.__code__.co_filename
250
+ f = sys._getframe()
251
+ while (f.f_back and (
252
+ f.f_code.co_filename == this_filename or
253
+ (f.f_back.f_back and
254
+ f.f_code.co_filename == "<string>" and
255
+ f.f_code.co_name == "<module>" and
256
+ f.f_back.f_code.co_filename == this_filename))):
257
+ f = f.f_back
258
+ if f.f_code.co_filename == "<string>" and f.f_code.co_name == "<module>":
259
+ # Skip an extra string eval frame for attaching a debugger.
260
+ # TODO: pass in a frame or maximum number of string frames to skip.
261
+ # We shouldn't skip "<string>" if it comes from regular user code.
262
+ f = f.f_back
263
+ return f
264
+
265
+
266
+ def _prompt_continue_waiting_for_debugger():
267
+ """
268
+ Prompt while exiting the debugger to get user opinion on keeping the
269
+ process waiting for debugger to attach.
270
+ """
271
+ count_invalid = 0
272
+ max_invalid_entries = 3
273
+ while count_invalid < max_invalid_entries:
274
+ sys.stdout.flush()
275
+ response = input("Keep the process running for debugger to be "
276
+ "attached later ? (y)es/(n)o\n")
277
+
278
+ if response.lower() in ('n', 'no'):
279
+ global _waiting_for_debugger
280
+ _waiting_for_debugger = None
281
+ break
282
+
283
+ if response.lower() not in ('y', 'yes'):
284
+ print("Invalid response: {}".format(repr(response)))
285
+ count_invalid += 1
286
+ else:
287
+ break
288
+ else:
289
+ print("Exiting after {} invalid responses.".format(max_invalid_entries))
290
+ _waiting_for_debugger = None
291
+ # Sleep for a fraction of second for the print statements to get printed.
292
+ time.sleep(0.01)
293
+
294
+
295
+ def _debug_exception(*exc_info, **kwargs):
296
+ """
297
+ Debug an exception -- print a stack trace and enter the debugger.
298
+
299
+ Suitable to be assigned to sys.excepthook.
300
+ """
301
+ from pyflyby._interactive import print_verbose_tb
302
+ tty = kwargs.pop("tty", "/dev/tty")
303
+ debugger_attached = kwargs.pop("debugger_attached", False)
304
+ if kwargs:
305
+ raise TypeError("debug_exception(): unexpected kwargs %s"
306
+ % (', '.join(sorted(kwargs.keys()))))
307
+ if not exc_info:
308
+ exc_info = sys.exc_info()
309
+ if len(exc_info) == 1 and type(exc_info[0]) is tuple:
310
+ exc_info = exc_info[0]
311
+ if len(exc_info) == 1 and type(exc_info[0]) is TracebackType:
312
+ # Allow the input to be just the traceback. The exception instance is
313
+ # only used for printing the traceback. It's not needed by the
314
+ # debugger.
315
+ # We don't know the exception in this case. For now put "", "". This
316
+ # will cause print_verbose_tb to include a line with just a colon.
317
+ # TODO: avoid that line.
318
+ exc_info = ("", "", exc_info)
319
+ if exc_info[1]:
320
+ # Explicitly set sys.last_value / sys.last_exc to ensure they are available
321
+ # in the debugger. One use case is that this allows users to call
322
+ # pyflyby.saveframe() within the debugger.
323
+ if sys.version_info < (3, 12):
324
+ sys.last_value = exc_info[1]
325
+ else:
326
+ sys.last_exc = exc_info[1]
327
+
328
+ with _DebuggerCtx(tty=tty) as pdb:
329
+ if debugger_attached:
330
+ # If debugger is attached to the process made waiting by
331
+ # 'wait_for_debugger_to_attach', check with the user whether to
332
+ # keep the process waiting for debugger to attach.
333
+ pdb.postloop = _prompt_continue_waiting_for_debugger
334
+ print_verbose_tb(*exc_info)
335
+ # Starting Py3.13, pdb.interaction() supports chained exceptions in case
336
+ # exception (and not traceback) is specified. This support is backported
337
+ # to IPython8.16 for earlier Python versions. So the conditions where
338
+ # chained exceptions won't be supported from here would be with the
339
+ # Python version < 3.13 and ipython not installed, or IPython's version
340
+ # is lesser than 8.16.
341
+ tb_or_exc = exc_info[2]
342
+ if sys.version_info < (3, 13):
343
+ # Check if the instance is of IPython's Pdb and its version.
344
+ try:
345
+ import IPython
346
+ if IPython.version_info >= (8, 16):
347
+ from IPython.core.debugger import Pdb as IPdb
348
+ # This is expected to be True, hence just a safe check.
349
+ if isinstance(pdb, IPdb):
350
+ tb_or_exc = exc_info[1]
351
+ except ModuleNotFoundError:
352
+ pass
353
+ else:
354
+ tb_or_exc = exc_info[1]
355
+ pdb.interaction(None, tb_or_exc)
356
+
357
+
358
+ def _debug_code(arg, globals=None, locals=None, auto_import=True, tty="/dev/tty"):
359
+ """
360
+ Run code under the debugger.
361
+
362
+ :type arg:
363
+ ``str``, ``Callable``, ``CodeType``, ``PythonStatement``, ``PythonBlock``,
364
+ ``FileText``
365
+ """
366
+ if globals is None or locals is None:
367
+ caller_frame = _get_caller_frame()
368
+ if globals is None:
369
+ globals = caller_frame.f_globals
370
+ if locals is None:
371
+ locals = caller_frame.f_locals
372
+ del caller_frame
373
+ with _DebuggerCtx(tty=tty) as pdb:
374
+ print("Entering debugger. Use 'n' to step, 'c' to run, 'q' to stop.")
375
+ print("")
376
+ from ._parse import PythonStatement, PythonBlock, FileText
377
+
378
+ if isinstance(arg, (str, PythonStatement, PythonBlock, FileText)):
379
+ # Compile the block so that we can get the right compile mode.
380
+ arg = PythonBlock(arg)
381
+ # TODO: enter text into linecache
382
+ autoimp_arg = arg
383
+ code = arg.compile()
384
+ elif isinstance(arg, CodeType):
385
+ autoimp_arg = arg
386
+ code = arg
387
+ elif isinstance(arg, Callable):
388
+ # TODO: check argspec to make sure it's a zero-arg callable.
389
+ code = arg.__code__
390
+ autoimp_arg = code
391
+ else:
392
+ raise TypeError(
393
+ "debug_code(): expected a string/callable/lambda; got a %s"
394
+ % (type(arg).__name__,))
395
+ if auto_import:
396
+ from ._autoimp import auto_import as auto_import_f
397
+ auto_import_f(autoimp_arg, [globals, locals])
398
+ return pdb.runeval(code, globals=globals, locals=locals)
399
+
400
+
401
+ _CURRENT_FRAME = object()
402
+
403
+
404
+ def debugger(*args, **kwargs):
405
+ '''
406
+ Entry point for debugging.
407
+
408
+ ``debugger()`` can be used in the following ways::
409
+
410
+ 1. Breakpoint mode, entering debugger in executing code::
411
+ >> def foo():
412
+ .. bar()
413
+ .. debugger()
414
+ .. baz()
415
+
416
+ This allow stepping through code after the debugger() call - i.e. between
417
+ bar() and baz(). This is similar to 'import pdb; pdb.set_trace()'::
418
+
419
+ 2. Debug a python statement::
420
+
421
+ >> def foo(x):
422
+ .. ...
423
+ >> X = 5
424
+
425
+ >> debugger("foo(X)")
426
+
427
+ The auto-importer is run on the given python statement.
428
+
429
+ 3. Debug a callable::
430
+
431
+ >> def foo(x=5):
432
+ .. ...
433
+
434
+ >> debugger(foo)
435
+ >> debugger(lambda: foo(6))
436
+
437
+ 4. Debug an exception::
438
+
439
+ >> try:
440
+ .. ...
441
+ .. except:
442
+ .. debugger(sys.exc_info())
443
+
444
+
445
+ If the process is waiting on for a debugger to attach to debug a frame or
446
+ exception traceback, then calling debugger(None) will debug that target.
447
+ If it is frame, then the user can step through code. If it is an
448
+ exception traceback, then the debugger will operate in post-mortem mode
449
+ with no stepping allowed. The process will continue running after this
450
+ debug session's "continue".
451
+
452
+ ``debugger()`` is suitable to be called interactively, from scripts, in
453
+ sys.excepthook, and in signal handlers.
454
+
455
+ :param args:
456
+ What to debug:
457
+ - If a string or callable, then run it under the debugger.
458
+ - If a frame, then debug the frame.
459
+ - If a traceback, then debug the traceback.
460
+ - If a 3-tuple as returned by sys.exc_info(), then debug the traceback.
461
+ - If the process is waiting to for a debugger to attach, then attach
462
+ the debugger there. This is only relevant when an external process
463
+ is attaching a debugger.
464
+ - If nothing specified, then enter the debugger at the statement
465
+ following the call to debug().
466
+ :kwarg tty:
467
+ Tty to connect to. If ``None`` (default): if /dev/tty is usable, then
468
+ use it; else call wait_for_debugger_to_attach() instead (unless
469
+ wait_for_attach==False).
470
+ :kwarg on_continue:
471
+ Function to call upon exiting the debugger and continuing with regular
472
+ execution.
473
+ :kwarg wait_for_attach:
474
+ Whether to wait for a remote terminal to attach (with 'py -d PID').
475
+ If ``True``, then always wait for a debugger to attach.
476
+ If ``False``, then never wait for a debugger to attach; debug in the
477
+ current terminal.
478
+ If unset, then defaults to true only when ``tty`` is unspecified and
479
+ /dev/tty is not usable.
480
+ :kwarg background:
481
+ If ``False``, then pause execution to debug.
482
+ If ``True``, then fork a process and wait for a debugger to attach in the
483
+ forked child.
484
+ '''
485
+ from ._parse import PythonStatement, PythonBlock, FileText
486
+ if len(args) in {1, 2}:
487
+ arg = args[0]
488
+ elif len(args) == 0:
489
+ arg = None
490
+ else:
491
+ arg = args
492
+ tty = kwargs.pop("tty" , None)
493
+ on_continue = kwargs.pop("on_continue" , lambda: None)
494
+ globals = kwargs.pop("globals" , None)
495
+ locals = kwargs.pop("locals" , None)
496
+ wait_for_attach = kwargs.pop("wait_for_attach", Ellipsis)
497
+ background = kwargs.pop("background" , False)
498
+ _debugger_attached = False
499
+ if kwargs:
500
+ raise TypeError("debugger(): unexpected kwargs %s"
501
+ % (', '.join(sorted(kwargs))))
502
+ if arg is None and tty is not None and wait_for_attach != True:
503
+ # If _waiting_for_debugger is not None, then attach to that
504
+ # (whether it's a frame, traceback, etc).
505
+ arg = _waiting_for_debugger
506
+ _debugger_attached = True
507
+ if arg is None:
508
+ # Debug current frame.
509
+ arg = _CURRENT_FRAME
510
+ if arg is _CURRENT_FRAME:
511
+ arg = _get_caller_frame()
512
+ if background:
513
+ # Fork a process and wait for a debugger to attach in the background.
514
+ # Todo: implement on_continue()
515
+ wait_for_debugger_to_attach(arg, background=True)
516
+ return
517
+ if wait_for_attach == True:
518
+ wait_for_debugger_to_attach(arg)
519
+ return
520
+ if tty is None:
521
+ if tty_is_usable():
522
+ tty = "/dev/tty"
523
+ elif wait_for_attach != False:
524
+ # If the tty isn't usable, then default to waiting for the
525
+ # debugger to attach from another (interactive) terminal.
526
+ # Todo: implement on_continue()
527
+ # TODO: capture globals/locals when relevant.
528
+ wait_for_debugger_to_attach(arg)
529
+ return
530
+ if isinstance(
531
+ arg, (str, PythonStatement, PythonBlock, FileText, CodeType, Callable)
532
+ ):
533
+ _debug_code(arg, globals=globals, locals=locals, tty=tty)
534
+ on_continue()
535
+ return
536
+ if (isinstance(arg, TracebackType) or
537
+ type(arg) is tuple and len(arg) == 3 and type(arg[2]) is TracebackType):
538
+ _debug_exception(arg, tty=tty, debugger_attached=_debugger_attached)
539
+ on_continue()
540
+ return
541
+ import threading
542
+ # If `arg` is an instance of `tuple` that contains
543
+ # `threading.ExceptHookArgs`, extract the exc_info from it.
544
+ if type(arg) is tuple and len(arg) == 1 and type(arg[0]) is threading.ExceptHookArgs:
545
+ arg = arg[0][:3]
546
+ _debug_exception(arg, tty=tty, debugger_attached=_debugger_attached)
547
+ on_continue()
548
+ return
549
+ if not isinstance(arg, FrameType):
550
+ raise TypeError(
551
+ "debugger(): expected a frame/traceback/str/code; got %s"
552
+ % (arg,))
553
+ frame = arg
554
+ if globals is not None or locals is not None:
555
+ raise NotImplementedError(
556
+ "debugger(): globals/locals only relevant when debugging code")
557
+ pdb_context = _DebuggerCtx(tty)
558
+ pdb = pdb_context.__enter__()
559
+ print("Entering debugger. Use 'n' to step, 'c' to continue running, 'q' to quit Python completely.")
560
+ def set_continue():
561
+ # Continue running code outside the debugger.
562
+ pdb.stopframe = pdb.botframe
563
+ pdb.returnframe = None
564
+ sys.settrace(None)
565
+ print("Continuing execution.")
566
+ pdb_context.__exit__(None, None, None)
567
+ on_continue()
568
+ def set_quit():
569
+ # Quit the program. Note that if we're inside IPython, then this
570
+ # won't actually exit IPython. We do want to call the context
571
+ # __exit__ here to make sure we restore sys.displayhook, etc.
572
+ # TODO: raise something else here if in IPython
573
+ pdb_context.__exit__(None, None, None)
574
+ raise SystemExit("Quitting as requested while debugging.")
575
+ pdb.set_continue = set_continue
576
+ pdb.set_quit = set_quit
577
+ pdb.do_EOF = pdb.do_continue
578
+ pdb.set_trace(frame)
579
+ # Note: set_trace() installs a tracer and returns; that means we can't use
580
+ # context managers around set_trace(): the __exit__() would be called
581
+ # right away, not after continuing/quitting.
582
+ # We also want this to be the very last thing called in the function (and
583
+ # not in a nested function). This way the very next thing the user sees
584
+ # is his own code.
585
+
586
+
587
+
588
+ _cached_py_commandline = None
589
+ def _find_py_commandline():
590
+ global _cached_py_commandline
591
+ if _cached_py_commandline is not None:
592
+ return _cached_py_commandline
593
+ import pyflyby
594
+ pkg_path = Filename(pyflyby.__path__[0]).real
595
+ assert pkg_path.base == "pyflyby"
596
+ d = pkg_path.dir
597
+ if d.base == "bin":
598
+ # Running from source tree
599
+ bindir = d
600
+ else:
601
+ # Installed by setup.py
602
+ while d.dir != d:
603
+ d = d.dir
604
+ bindir = d / "bin"
605
+ if bindir.exists:
606
+ break
607
+ else:
608
+ raise ValueError(
609
+ "Couldn't find 'py' script: "
610
+ "couldn't find 'bin' dir from package path %s" % (pkg_path,))
611
+ candidate = bindir / "py"
612
+ if not candidate.exists:
613
+ raise ValueError(
614
+ "Couldn't find 'py' script: expected it at %s" % (candidate,))
615
+ if not candidate.isexecutable:
616
+ raise ValueError(
617
+ "Found 'py' script at %s but it's not executable" % (candidate,))
618
+ _cached_py_commandline = candidate
619
+ return candidate
620
+
621
+
622
+
623
+ class DebuggerAttachTimeoutError(Exception):
624
+ pass
625
+
626
+
627
+ def _sleep_until_debugger_attaches(arg, timeout=86400):
628
+ assert arg is not None
629
+ global _waiting_for_debugger
630
+ try:
631
+ deadline = time.time() + timeout
632
+ _waiting_for_debugger = arg
633
+ while _waiting_for_debugger is not None:
634
+ if time.time() > deadline:
635
+ raise DebuggerAttachTimeoutError
636
+ time.sleep(0.5)
637
+ finally:
638
+ _waiting_for_debugger = None
639
+
640
+
641
+ def wait_for_debugger_to_attach(arg, mailto=None, background=False, timeout=86400):
642
+ """
643
+ Send email to user and wait for debugger to attach.
644
+
645
+ :param arg:
646
+ What to debug. Should be a sys.exc_info() result or a sys._getframe()
647
+ result.
648
+ :param mailto:
649
+ Recipient to email. Defaults to $USER or current user.
650
+ :param background:
651
+ If True, fork a child process. The parent process continues immediately
652
+ without waiting. The child process waits for a debugger to attach, and
653
+ exits when the debugging session completes.
654
+ :param timeout:
655
+ Maximum number of seconds to wait for user to attach debugger.
656
+ """
657
+ import traceback
658
+ if background:
659
+ originalpid = os.getpid()
660
+ if os.fork() != 0:
661
+ return
662
+ else:
663
+ originalpid = None
664
+ try:
665
+ # Reset the exception hook after the first exception.
666
+ #
667
+ # In case the code injected by the remote client causes some error in
668
+ # the debugged process, another email is sent for the new exception. This can
669
+ # lead to an infinite loop of sending mail for each successive exceptions
670
+ # everytime a remote client tries to connect. Our process might never get
671
+ # a chance to exit and the remote client might just hang.
672
+ #
673
+ if not _reset_excepthook():
674
+ raise ValueError("Couldn't reset sys.excepthook. Aborting remote "
675
+ "debugging.")
676
+ # Send email.
677
+ _send_email_with_attach_instructions(arg, mailto, originalpid=originalpid)
678
+ # Sleep until the debugger to attaches.
679
+ _sleep_until_debugger_attaches(arg, timeout=timeout)
680
+ except:
681
+ traceback.print_exception(*sys.exc_info())
682
+ finally:
683
+ if background:
684
+ # Exit. Note that the original process already continued.
685
+ # We do this in a 'finally' to make sure that we always exit
686
+ # here. We don't want to do cleanup actions (finally clauses,
687
+ # atexit functions) in the parent, since that can affect the
688
+ # parent (e.g. deleting temp files while the parent process is
689
+ # still using them).
690
+ os._exit(1)
691
+
692
+
693
+ def debug_on_exception(function, background=False):
694
+ """
695
+ Decorator that wraps a function so that we enter a debugger upon exception.
696
+ """
697
+ @wraps(function)
698
+ def wrapped_function(*args, **kwargs):
699
+ try:
700
+ return function(*args, **kwargs)
701
+ except:
702
+ debugger(sys.exc_info(), background=background)
703
+ raise
704
+ return wrapped_function
705
+
706
+
707
+ def _send_email_with_attach_instructions(arg, mailto, originalpid):
708
+ from email.mime.text import MIMEText
709
+ import smtplib
710
+ import socket
711
+ import traceback
712
+ # Prepare variables we'll use in the email.
713
+ d = dict()
714
+ user = pwd.getpwuid(os.geteuid()).pw_name
715
+ argv = ' '.join(sys.argv)
716
+ d.update(
717
+ argv =argv ,
718
+ argv_abbrev=argv[:40] ,
719
+ event ="breakpoint" ,
720
+ exc =None ,
721
+ exctype =None ,
722
+ hostname =socket.getfqdn() ,
723
+ originalpid=originalpid ,
724
+ pid =os.getpid() ,
725
+ py =_find_py_commandline(),
726
+ time =time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime()),
727
+ traceback =None ,
728
+ username =user ,
729
+ )
730
+ tb = None
731
+ frame = None
732
+ stacktrace = None
733
+ if isinstance(arg, FrameType):
734
+ frame = arg
735
+ stacktrace = ''.join(traceback.format_stack(frame))
736
+ elif isinstance(arg, TracebackType):
737
+ frame = d['tb'].tb_frame
738
+ stacktrace = ''.join(traceback.format_tb(arg))
739
+ elif isinstance(arg, tuple) and len(arg) == 3 and isinstance(arg[2], TracebackType):
740
+ d.update(
741
+ exctype=arg[0].__name__,
742
+ exc =arg[1] ,
743
+ event =arg[0].__name__,
744
+ )
745
+ tb = arg[2]
746
+ while tb.tb_next:
747
+ tb = tb.tb_next
748
+ frame = tb.tb_frame
749
+ stacktrace = ''.join(traceback.format_tb(arg[2])) + (
750
+ " %s: %s\n" % (arg[0].__name__, arg[1]))
751
+ if not frame:
752
+ frame = _get_caller_frame()
753
+ d.update(
754
+ function = frame.f_code.co_name ,
755
+ filename = frame.f_code.co_filename,
756
+ line = frame.f_lineno ,
757
+ )
758
+ d.update(
759
+ filename_abbrev = _abbrev_filename(d['filename']),
760
+ )
761
+ if tb:
762
+ d['stacktrace'] = tb and ''.join(" %s\n" % (line,) for line in stacktrace.splitlines())
763
+ # Construct a template for the email body.
764
+ template = []
765
+ template += [
766
+ "While running {argv_abbrev}, {event} in {function} at {filename}:{line}",
767
+ "",
768
+ "Please run:",
769
+ " ssh -t {hostname} {py} -d {pid}",
770
+ "",
771
+ ]
772
+ if d['originalpid']:
773
+ template += [
774
+ "As process {originalpid}, I have forked to process {pid} and am waiting for a debugger to attach."
775
+ ]
776
+ else:
777
+ template += [
778
+ "As process {pid}, I am waiting for a debugger to attach."
779
+ ]
780
+ template += [
781
+ "",
782
+ "Details:",
783
+ " Time : {time}",
784
+ " Host : {hostname}",
785
+ ]
786
+ if d['originalpid']:
787
+ template += [
788
+ " Original process : {originalpid}",
789
+ " Forked process : {pid}",
790
+ ]
791
+ else:
792
+ template += [
793
+ " Process : {pid}",
794
+ ]
795
+ template += [
796
+ " Username : {username}",
797
+ " Command line : {argv}",
798
+ ]
799
+ if d['exc']:
800
+ template += [
801
+ " Exception : {exctype}: {exc}",
802
+ ]
803
+ if d.get('stacktrace'):
804
+ template += [
805
+ " Traceback :",
806
+ "{stacktrace}",
807
+ ]
808
+ # Build email body.
809
+ email_body = '\n'.join(template).format(**d)
810
+ # Print to stderr.
811
+ prefixed = "".join("[PYFLYBY] %s\n" % line
812
+ for line in email_body.splitlines())
813
+ sys.stderr.write(prefixed)
814
+ # Send email.
815
+ if mailto is None:
816
+ mailto = os.getenv("USER") or user
817
+ msg = MIMEText(email_body)
818
+ msg['Subject'] = (
819
+ "ssh {hostname} py -d {pid}"
820
+ " # {event} in {argv_abbrev} in {function} at {filename_abbrev}:{line}"
821
+ ).format(**d)
822
+ msg['From'] = user
823
+ msg['To'] = mailto
824
+ s = smtplib.SMTP("localhost")
825
+ s.sendmail(user, [mailto], msg.as_string())
826
+ s.quit()
827
+
828
+
829
+ def _abbrev_filename(filename):
830
+ splt = filename.rsplit("/", 4)
831
+ if len(splt) >= 4:
832
+ splt[:2] = ["..."]
833
+ return '/'.join(splt)
834
+
835
+
836
+ def syscall_marker(msg):
837
+ """
838
+ Execute a dummy syscall that is visible in truss/strace.
839
+ """
840
+ try:
841
+ s = ("/### %s" % (msg,)).ljust(70)
842
+ os.stat(s)
843
+ except OSError:
844
+ pass
845
+
846
+
847
+ _ORIG_PID = os.getpid()
848
+
849
+ def _signal_handler_debugger(signal_number, interrupted_frame):
850
+ if os.getpid() != _ORIG_PID:
851
+ # We're in a forked subprocess. Ignore this SIGQUIT.
852
+ return
853
+ fd_tty = _dev_tty_fd()
854
+ os.write(fd_tty, b"\nIntercepted SIGQUIT; entering debugger. Resend ^\\ to dump core (and 'stty sane' to reset terminal settings).\n\n")
855
+ frame = _get_caller_frame()
856
+ enable_signal_handler_debugger(False)
857
+ debugger(
858
+ frame,
859
+ on_continue=enable_signal_handler_debugger)
860
+ signal.signal(signal.SIGQUIT, _signal_handler_debugger)
861
+
862
+
863
+ def enable_signal_handler_debugger(enable=True):
864
+ r'''
865
+ Install a signal handler for SIGQUIT so that Control-\ or external SIGQUIT
866
+ enters debugger. Suitable to be called from site.py.
867
+ '''
868
+ # Idea from bzrlib.breakin
869
+ # (http://bazaar.launchpad.net/~bzr/bzr/trunk/annotate/head:/bzrlib/breakin.py)
870
+ if enable:
871
+ signal.signal(signal.SIGQUIT, _signal_handler_debugger)
872
+ else:
873
+ signal.signal(signal.SIGQUIT, signal.SIG_DFL)
874
+
875
+
876
+ def enable_exception_handler_debugger():
877
+ '''
878
+ Enable ``sys.excepthook = debugger`` so that we automatically enter
879
+ the debugger upon uncaught exceptions.
880
+ '''
881
+ _override_excepthook(debugger)
882
+
883
+
884
+ # Handle SIGTERM with traceback+exit.
885
+ def _sigterm_handler(signum, frame):
886
+ # faulthandler.dump_traceback(all_threads=True)
887
+ import traceback
888
+ traceback.print_stack()
889
+ # raise SigTermReceived
890
+ signal.signal(signum, signal.SIG_DFL)
891
+ os.kill(os.getpid(), signum)
892
+ os._exit(99) # shouldn't get here
893
+
894
+
895
+ def enable_sigterm_handler(on_existing_handler='raise'):
896
+ """
897
+ Install a handler for SIGTERM that causes Python to print a stack trace
898
+ before exiting.
899
+
900
+ :param on_existing_handler:
901
+ What to do when a SIGTERM handler was already registered.
902
+ - If ``"raise"``, then keep the existing handler and raise an exception.
903
+ - If ``"keep_existing"``, then silently keep the existing handler.
904
+ - If ``"warn_and_override"``, then override the existing handler and log a warning.
905
+ - If ``"silently_override"``, then silently override the existing handler.
906
+ """
907
+ old_handler = signal.signal(signal.SIGTERM, _sigterm_handler)
908
+ if old_handler == signal.SIG_DFL or old_handler == _sigterm_handler:
909
+ return
910
+ if on_existing_handler == "silently_override":
911
+ return
912
+ if on_existing_handler == "warn_and_override":
913
+ from ._log import logger
914
+ logger.warning("enable_sigterm_handler(): Overriding existing SIGTERM handler")
915
+ return
916
+ signal.signal(signal.SIGTERM, old_handler)
917
+ if on_existing_handler == "keep_existing":
918
+ return
919
+ elif on_existing_handler == "raise":
920
+ raise ValueError(
921
+ "enable_sigterm_handler(on_existing_handler='raise'): SIGTERM handler already exists" + repr(old_handler))
922
+ else:
923
+ raise ValueError(
924
+ "enable_sigterm_handler(): SIGTERM handler already exists, "
925
+ "and invalid on_existing_handler=%r"
926
+ % (on_existing_handler,))
927
+
928
+
929
+ def enable_faulthandler():
930
+ try:
931
+ import faulthandler
932
+ except ImportError:
933
+ pass
934
+ else:
935
+ # Print Python user-level stack trace upon SIGSEGV/etc.
936
+ faulthandler.enable()
937
+
938
+
939
+ def add_debug_functions_to_builtins(*, add_deprecated: bool):
940
+ """
941
+ Install debugger(), etc. in the builtin global namespace.
942
+ """
943
+ functions_to_add = [
944
+ 'debugger',
945
+ 'debug_on_exception',
946
+ 'print_traceback',
947
+ ]
948
+ if add_deprecated:
949
+ # DEPRECATED: In the future, the following will not be added to builtins.
950
+ # Use debugger() instead.
951
+ functions_to_add += [
952
+ "breakpoint",
953
+ "debug_exception",
954
+ "debug_statement",
955
+ "waitpoint",
956
+ ]
957
+ for name in functions_to_add:
958
+ setattr(builtins, name, globals()[name])
959
+
960
+ # TODO: allow attaching remotely (winpdb/rpdb2) upon sigquit. Or rpc like http://code.activestate.com/recipes/576515/
961
+ # TODO: http://sourceware.org/gdb/wiki/PythonGdb
962
+
963
+
964
+ def get_executable(pid):
965
+ """
966
+ Get the full path for the target process.
967
+
968
+ :type pid:
969
+ ``int``
970
+ :rtype:
971
+ `Filename`
972
+ """
973
+ uname = os.uname()[0]
974
+ if uname == 'Linux':
975
+ result = os.readlink('/proc/%d/exe' % (pid,))
976
+ elif uname == 'SunOS':
977
+ result = os.readlink('/proc/%d/path/a.out' % (pid,))
978
+ else:
979
+ # Use psutil to try to answer this. This should also work for the
980
+ # above cases too, but it's simple enough to implement it directly and
981
+ # avoid this dependency on those platforms.
982
+ import psutil
983
+ result = psutil.Process(pid).exe()
984
+ result = Filename(result).real
985
+ if not result.isfile:
986
+ raise ValueError("Couldn't get executable for pid %s" % (pid,))
987
+ if not result.isreadable:
988
+ raise ValueError("Executable %s for pid %s is not readable"
989
+ % (result, pid))
990
+ return result
991
+
992
+
993
+ _gdb_safe_chars = (
994
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
995
+ r"0123456789,./-_=+:;'[]{}\|`~!@#%^&*()<>? ")
996
+
997
+ def _escape_for_gdb(string):
998
+ """
999
+ Escape a string to make it safe for passing to gdb.
1000
+ """
1001
+ result = []
1002
+ for char in string:
1003
+ if char in _gdb_safe_chars:
1004
+ result.append(char)
1005
+ else:
1006
+ result.append(r"\0{0:o}".format(ord(char)))
1007
+ return ''.join(result)
1008
+
1009
+
1010
+ _memoized_dev_null = None
1011
+ def _dev_null():
1012
+ """
1013
+ Return a file object opened for reading/writing to /dev/null.
1014
+ Memoized.
1015
+
1016
+ :rtype:
1017
+ ``file``
1018
+ """
1019
+ global _memoized_dev_null
1020
+ if _memoized_dev_null is None:
1021
+ _memoized_dev_null = open("/dev/null", 'w+')
1022
+ return _memoized_dev_null
1023
+
1024
+
1025
+ def inject(pid, statements, wait=True, show_gdb_output=False):
1026
+ """
1027
+ Execute ``statements`` in a running Python process.
1028
+
1029
+ :type pid:
1030
+ ``int``
1031
+ :param pid:
1032
+ Id of target process
1033
+ :type statements:
1034
+ Iterable of strings
1035
+ :param statements:
1036
+ Python statements to execute.
1037
+ :return:
1038
+ Then process ID of the gdb process if ``wait`` is False; ``None`` if
1039
+ ``wait`` is True.
1040
+ """
1041
+ import subprocess
1042
+ os.kill(pid, 0) # raises OSError "No such process" unless pid exists
1043
+
1044
+ # Check if we have permissions to attach to the process before proceeding
1045
+ try:
1046
+ subprocess.run(
1047
+ ["gdb", "-n", "-batch", "--interpreter=mi", "-p", str(pid)],
1048
+ check=True,
1049
+ capture_output=True,
1050
+ )
1051
+ except subprocess.CalledProcessError as e:
1052
+ raise Exception(
1053
+ f"Unable to attach to pid {pid} with gdb (exit code {e.returncode}). "
1054
+ f"Reason: {e.stderr.decode()}"
1055
+ ) from e
1056
+
1057
+ if isinstance(statements, str):
1058
+ statements = (statements,)
1059
+ else:
1060
+ statements = tuple(statements)
1061
+ for statement in statements:
1062
+ if not isinstance(statement, str):
1063
+ raise TypeError(
1064
+ "Expected iterable of strings, not %r" % (type(statement),))
1065
+ # Based on
1066
+ # https://github.com/lmacken/pyrasite/blob/master/pyrasite/inject.py
1067
+ # TODO: add error checking
1068
+ # TODO: consider using lldb, especially on Darwin.
1069
+ gdb_commands = (
1070
+ [ 'PyGILState_Ensure()' ]
1071
+ + [ 'PyRun_SimpleString("%s")' % (_escape_for_gdb(statement),)
1072
+ for statement in statements ]
1073
+ + [ 'PyGILState_Release($1)' ])
1074
+ python_path = get_executable(pid)
1075
+ if "python" not in python_path.base:
1076
+ raise ValueError(
1077
+ "pid %s uses executable %s, which does not appear to be python"
1078
+ % (pid, python_path))
1079
+ # TODO: check that gdb is found and that the version is new enough (7.x)
1080
+ #
1081
+ # A note about --interpreter=mi: mi stands for Machine Interface and it's
1082
+ # the blessed way to control gdb from a pipe, since the output is much
1083
+ # easier to parse than the normal human-oriented output (it is also worth
1084
+ # noting that at the moment we are never parsig the output, but it's still
1085
+ # a good practice to use --interpreter=mi).
1086
+ command = (
1087
+ ['gdb', '-n', str(python_path), '-p', str(pid), '-batch', '--interpreter=mi']
1088
+ + [ '-eval-command=call %s' % (c,) for c in gdb_commands ])
1089
+
1090
+ output = subprocess.PIPE if show_gdb_output else _dev_null()
1091
+
1092
+ process = subprocess.Popen(command,
1093
+ stdin=_dev_null(),
1094
+ stdout=output,
1095
+ stderr=output)
1096
+ if wait:
1097
+ retcode = process.wait()
1098
+ if retcode:
1099
+ raise Exception(
1100
+ "Gdb command %r failed (exit code %r)"
1101
+ % (command, retcode))
1102
+ else:
1103
+ return process.pid
1104
+
1105
+ import tty
1106
+
1107
+
1108
+ # Copy of tty.setraw that does not set ISIG,
1109
+ # in order to keep CTRL-C sending Keybord Interrupt.
1110
+ def setraw_but_sigint(fd, when=tty.TCSAFLUSH):
1111
+ """Put terminal into a raw mode."""
1112
+ mode = tty.tcgetattr(fd)
1113
+ mode[tty.IFLAG] = mode[tty.IFLAG] & ~(
1114
+ tty.BRKINT | tty.ICRNL | tty.INPCK | tty.ISTRIP | tty.IXON
1115
+ )
1116
+ mode[tty.OFLAG] = mode[tty.OFLAG] & ~(tty.OPOST)
1117
+ mode[tty.CFLAG] = mode[tty.CFLAG] & ~(tty.CSIZE | tty.PARENB)
1118
+ mode[tty.CFLAG] = mode[tty.CFLAG] | tty.CS8
1119
+ mode[tty.LFLAG] = mode[tty.LFLAG] & ~(
1120
+ tty.ECHO | tty.ICANON | tty.IEXTEN
1121
+ ) # NOT ISIG HERE.
1122
+ mode[tty.CC][tty.VMIN] = 1
1123
+ mode[tty.CC][tty.VTIME] = 0
1124
+ tty.tcsetattr(fd, when, mode)
1125
+
1126
+
1127
+ class Pty(object):
1128
+ def __init__(self):
1129
+ import pty
1130
+ self.master_fd, self.slave_fd = pty.openpty()
1131
+ self.ttyname = os.ttyname(self.slave_fd)
1132
+
1133
+ def communicate(self):
1134
+ import tty
1135
+ import pty
1136
+ try:
1137
+ mode = tty.tcgetattr(pty.STDIN_FILENO)
1138
+ setraw_but_sigint(pty.STDIN_FILENO)
1139
+ restore = True
1140
+ except tty.error:
1141
+ restore = False
1142
+ try:
1143
+ pty._copy(self.master_fd)
1144
+ except KeyboardInterrupt:
1145
+ print('^C\r') # we need the \r because we are still in raw mode
1146
+ finally:
1147
+ if restore:
1148
+ tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
1149
+ os.close(self.master_fd)
1150
+
1151
+
1152
+ def process_exists(pid):
1153
+ """
1154
+ Return whether ``pid`` exists.
1155
+
1156
+ :type pid:
1157
+ ``int``
1158
+ :rtype:
1159
+ ``bool``
1160
+ """
1161
+ try:
1162
+ os.kill(pid, 0)
1163
+ return True
1164
+ except OSError as e:
1165
+ if e.errno == errno.ESRCH:
1166
+ return False
1167
+ raise
1168
+
1169
+
1170
+ def kill_process(pid, kill_signals):
1171
+ """
1172
+ Kill process ``pid`` using various signals.
1173
+
1174
+ :param kill_signals:
1175
+ Sequence of (signal, delay) tuples. Each signal is tried in sequence,
1176
+ waiting up to ``delay`` seconds before trying the next signal.
1177
+ """
1178
+ for sig, delay in kill_signals:
1179
+ start_time = time.time()
1180
+ try:
1181
+ os.kill(pid, sig)
1182
+ except OSError as e:
1183
+ if e.errno == errno.ESRCH:
1184
+ return True
1185
+ raise
1186
+ deadline = start_time + delay
1187
+ while time.time() < deadline:
1188
+ if not process_exists(pid):
1189
+ return True
1190
+ time.sleep(0.05)
1191
+
1192
+
1193
+ def attach_debugger(pid):
1194
+ """
1195
+ Attach command-line debugger to a running process.
1196
+
1197
+ :param pid:
1198
+ Process id of target process.
1199
+ """
1200
+ import pyflyby
1201
+ import signal
1202
+ class SigUsr1(Exception):
1203
+ pass
1204
+ def sigusr1_handler(*args):
1205
+ raise SigUsr1
1206
+ signal.signal(signal.SIGUSR1, sigusr1_handler)
1207
+ terminal = Pty()
1208
+ pyflyby_lib_path = os.path.dirname(pyflyby.__path__[0])
1209
+ # Inject a call to 'debugger()' into target process.
1210
+ # Set on_continue to signal ourselves that we're done.
1211
+ on_continue = "lambda: __import__('os').kill(%d, %d)" % (os.getpid(), signal.SIGUSR1)
1212
+
1213
+ # Use Python import machinery to import pyflyby from its directory.
1214
+ #
1215
+ # Adding the path to sys.path might have side effects. For e.g., a package
1216
+ # with the same name as a built-in module could exist in `pyflyby_dir`.
1217
+ # Adding `pyflyby_dir` to sys.path will make the package get imported from
1218
+ # `pyflyby_dir` instead of deferring this decision to the user Python
1219
+ # environment.
1220
+ #
1221
+ # As a concrete example, `typing` module is a package as well a built-in
1222
+ # module from Python version >= 3.5
1223
+ statements = [
1224
+ "loader = __import__('importlib').machinery.PathFinder.find_module("
1225
+ "fullname='pyflyby', path=['{pyflyby_dir}'])".format(
1226
+ pyflyby_dir=pyflyby_lib_path),
1227
+ "pyflyby = loader.load_module('pyflyby')"
1228
+ ]
1229
+ statements.append(
1230
+ ("pyflyby.debugger(tty=%r, on_continue=%s)"
1231
+ % (terminal.ttyname, on_continue))
1232
+ )
1233
+
1234
+ gdb_pid = inject(pid, statements=";".join(statements), wait=False)
1235
+ # Fork a watchdog process to make sure we exit if the target process or
1236
+ # gdb process exits, and make sure the gdb process exits if we exit.
1237
+ parent_pid = os.getpid()
1238
+ watchdog_pid = os.fork()
1239
+ if watchdog_pid == 0:
1240
+ while True:
1241
+ try:
1242
+ if not process_exists(gdb_pid):
1243
+ kill_process(
1244
+ parent_pid,
1245
+ [(signal.SIGUSR1, 5), (signal.SIGTERM, 15),
1246
+ (signal.SIGKILL, 60)])
1247
+ break
1248
+ if not process_exists(pid):
1249
+ start_time = time.time()
1250
+ os.kill(parent_pid, signal.SIGUSR1)
1251
+ kill_process(
1252
+ gdb_pid,
1253
+ [(0, 5), (signal.SIGTERM, 15), (signal.SIGKILL, 60)])
1254
+ kill_process(
1255
+ parent_pid,
1256
+ [(0, (5 + time.time() - start_time)),
1257
+ (signal.SIGTERM, 15), (signal.SIGKILL, 60)])
1258
+ break
1259
+ if not process_exists(parent_pid):
1260
+ kill_process(
1261
+ gdb_pid,
1262
+ [(0, 5), (signal.SIGTERM, 15), (signal.SIGKILL, 60)])
1263
+ break
1264
+ time.sleep(0.1)
1265
+ except KeyboardInterrupt:
1266
+ # if the user pressed CTRL-C the parent process is about to
1267
+ # die, so we will detect the death in the next iteration of
1268
+ # the loop and exit cleanly after killing also gdb
1269
+ pass
1270
+ os._exit(0)
1271
+ # Communicate with pseudo tty.
1272
+ try:
1273
+ terminal.communicate()
1274
+ except SigUsr1:
1275
+ print("\nDebugging complete.")
1276
+ pass
1277
+
1278
+
1279
+ def remote_print_stack(pid, output=1):
1280
+ """
1281
+ Tell a target process to print a stack trace.
1282
+
1283
+ This currently only handles the main thread.
1284
+ TODO: handle multiple threads.
1285
+
1286
+ :param pid:
1287
+ PID of target process.
1288
+ :type output:
1289
+ ``int``, ``file``, or ``str``
1290
+ :param output:
1291
+ Output file descriptor.
1292
+ """
1293
+ # Interpret ``output`` argument as a file-like object, file descriptor, or
1294
+ # filename.
1295
+ if hasattr(output, 'write'): # file-like object
1296
+ output_fh = output
1297
+ try:
1298
+ output.flush()
1299
+ except Exception:
1300
+ pass
1301
+ try:
1302
+ output_fd = output.fileno()
1303
+ except Exception:
1304
+ output_fd = None
1305
+ try:
1306
+ output_fn = Filename(output.name)
1307
+ except Exception:
1308
+ pass
1309
+ elif isinstance(output, int):
1310
+ output_fh = None
1311
+ output_fn = None
1312
+ output_fd = output
1313
+ elif isinstance(output, (str, Filename)):
1314
+ output_fh = None
1315
+ output_fn = Filename(output)
1316
+ output_fd = None
1317
+ else:
1318
+ raise TypeError(
1319
+ "remote_print_stack_trace(): expected file/str/int; got %s"
1320
+ % (type(output).__name__,))
1321
+ temp_file = None
1322
+ remote_fn = output_fn
1323
+ if remote_fn is None and output_fd is not None:
1324
+ remote_fn = Filename("/proc/%d/fd/%d" % (os.getpid(), output_fd))
1325
+ # Figure out whether the target process will be able to open output_fn for
1326
+ # writing. Since the target process would need to be running as the same
1327
+ # user as this process for us to be able to attach a debugger, we can
1328
+ # simply check whether we ourselves can open the file. Typically output
1329
+ # will be fd 1 and we will have access to write to it. However, if we're
1330
+ # sudoed, we won't be able to re-open it via the proc symlink, even though
1331
+ # we already currently have it open. Another case is ``output`` is a
1332
+ # file-like object that isn't a real file, e.g. a StringO. In each case
1333
+ # we we don't have a usable filename for the remote process yet. To
1334
+ # address these situations, we create a temporary file for the remote
1335
+ # process to write to.
1336
+ if remote_fn is None or not remote_fn.iswritable:
1337
+ if not output_fh or output_fd:
1338
+ assert remote_fn is not None
1339
+ raise OSError(errno.EACCESS, "Can't write to %s" % output_fn)
1340
+ # We can still use the /proc/$pid/fd approach with an unnamed temp
1341
+ # file. If it turns out there are situations where that doesn't work,
1342
+ # we can switch to using a NamedTemporaryFile.
1343
+ from tempfile import TemporaryFile
1344
+ temp_file = TemporaryFile()
1345
+ remote_fn = Filename(
1346
+ "/proc/%d/fd/%d" % (os.getpid(), temp_file.fileno()))
1347
+ assert remote_fn.iswritable
1348
+ # *** Do the code injection ***
1349
+ _remote_print_stack_to_file(pid, remote_fn)
1350
+ # Copy from temp file to the requested output.
1351
+ if temp_file is not None:
1352
+ data = temp_file.read()
1353
+ temp_file.close()
1354
+ if output_fh is not None:
1355
+ output_fh.write(data)
1356
+ output_fh.flush()
1357
+ elif output_fd is not None:
1358
+ with os.fdopen(output_fd, 'w') as f:
1359
+ f.write(data)
1360
+ else:
1361
+ raise AssertionError("unreacahable")
1362
+
1363
+
1364
+ def _remote_print_stack_to_file(pid, filename):
1365
+ inject(pid, [
1366
+ "import traceback",
1367
+ "with open(%r,'w') as f: traceback.print_stack(file=f)" % str(filename)
1368
+ ], wait=True)
1369
+
1370
+
1371
+
1372
+ # Deprecated wrapper for wait_for_debugger_to_attach().
1373
+ def waitpoint(frame=None, mailto=None, background=False, timeout=86400):
1374
+ if frame is None:
1375
+ frame = _get_caller_frame()
1376
+ wait_for_debugger_to_attach(frame, mailto=mailto,
1377
+ background=background, timeout=timeout)
1378
+
1379
+ breakpoint = debugger # deprecated alias
1380
+ debug_statement = debugger # deprecated alias
1381
+ debug_exception = debugger # deprecated alias
1382
+ enable_signal_handler_breakpoint = enable_signal_handler_debugger # deprecated alias
1383
+ enable_exception_handler = enable_exception_handler_debugger # deprecated alias