debugpy/ptvsd/_util.py
Pavel Minaev 23c118aae4 Fix #811: new socket server to receive pid and port for subprocesses (#871)
* Fix #811: new socket server to receive pid and port for subprocesses

* Install test dependencies from test_requirements.txt on Travis

* Multiproc support can't handle os.fork() as implemented.

On Python 3.4+, use set_start_method('spawn') to ensure that multiprocessing module doesn't use fork.

On lower versions, skip the test.

* Restrict the multiprocessing test to Windows for now.
2018-10-11 14:01:39 -07:00

415 lines
10 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import print_function
import contextlib
import os
import threading
import time
import sys
DEBUG = False
if os.environ.get('PTVSD_DEBUG', ''):
DEBUG = True
def debug(*msg, **kwargs):
if not DEBUG:
return
tb = kwargs.pop('tb', False)
assert not kwargs
if tb:
import traceback
traceback.print_exc()
print(*msg, file=sys.__stderr__)
sys.__stderr__.flush()
@contextlib.contextmanager
def ignore_errors(log=None):
"""A context manager that masks any raised exceptions."""
try:
yield
except Exception as exc:
if log is not None:
log('ignoring error', exc)
def call_all(callables, *args, **kwargs):
"""Return the result of calling every given object."""
results = []
for call in callables:
try:
call(*args, **kwargs)
except Exception as exc:
results.append((call, exc))
else:
results.append((call, None))
return results
########################
# pydevd stuff
from _pydevd_bundle import pydevd_comm # noqa
def log_pydevd_msg(cmdid, seq, args, inbound,
log=debug, prefix=None, verbose=False):
"""Log a representation of the given pydevd msg."""
if log is None or (log is debug and not DEBUG):
return
if not verbose and cmdid == pydevd_comm.CMD_WRITE_TO_CONSOLE:
return
if prefix is None:
prefix = '-> ' if inbound else '<- '
try:
cmdname = pydevd_comm.ID_TO_MEANING[str(cmdid)]
except KeyError:
for cmdname, value in vars(pydevd_comm).items():
if cmdid == value:
break
else:
cmdname = '???'
cmd = '{} ({})'.format(cmdid, cmdname)
args = args.replace('\n', '\\n')
msg = '{}{:28} [{:>10}]: |{}|'.format(prefix, cmd, seq, args)
log(msg)
########################
# threading stuff
try:
ThreadError = threading.ThreadError
except AttributeError:
ThreadError = RuntimeError
try:
base = __builtins__.TimeoutError
except AttributeError:
base = OSError
class TimeoutError(base): # noqa
"""Timeout expired."""
timeout = None
reason = None
@classmethod
def from_timeout(cls, timeout, reason=None):
"""Return a TimeoutError with the given timeout."""
msg = 'timed out (after {} seconds)'.format(timeout)
if reason is not None:
msg += ' ' + reason
self = cls(msg)
self.timeout = timeout
self.reason = reason
return self
del base # noqa
def wait(check, timeout=None, reason=None):
"""Wait for the given func to return True.
If a timeout is given and reached then raise TimeoutError.
"""
if timeout is None or timeout <= 0:
while not check():
time.sleep(0.01)
else:
if not _wait(check, timeout):
raise TimeoutError.from_timeout(timeout, reason)
def is_locked(lock):
"""Return True if the lock is locked."""
if lock is None:
return False
if not lock.acquire(False):
return True
lock_release(lock)
return False
def lock_release(lock):
"""Ensure that the lock is released."""
if lock is None:
return
try:
lock.release()
except ThreadError: # already unlocked
pass
def lock_wait(lock, timeout=None, reason='waiting for lock'):
"""Wait until the lock is not locked."""
if not _lock_acquire(lock, timeout):
raise TimeoutError.from_timeout(timeout, reason)
lock_release(lock)
if sys.version_info >= (3,):
def _lock_acquire(lock, timeout):
if timeout is None:
timeout = -1
return lock.acquire(timeout=timeout)
else:
def _lock_acquire(lock, timeout):
if timeout is None or timeout <= 0:
return lock.acquire()
def check():
return lock.acquire(False)
return _wait(check, timeout)
def _wait(check, timeout):
if check():
return True
for _ in range(int(timeout * 100)):
time.sleep(0.01)
if check():
return True
else:
return False
def new_hidden_thread(name, target, prefix='ptvsd.', daemon=True, **kwargs):
"""Return a thread that will be ignored by pydevd."""
if prefix is not None and not name.startswith(prefix):
name = prefix + name
t = threading.Thread(
name=name,
target=target,
**kwargs
)
t.pydev_do_not_trace = True
if daemon:
t.is_pydev_daemon_thread = True
t.daemon = True
return t
########################
# closing stuff
class ClosedError(RuntimeError):
"""Indicates that the object is closed."""
def close_all(closeables):
"""Return the result of closing every given object."""
results = []
for obj in closeables:
try:
obj.close()
except Exception as exc:
results.append((obj, exc))
else:
results.append((obj, None))
return results
class Closeable(object):
"""A base class for types that may be closed."""
NAME = None
FAIL_ON_ALREADY_CLOSED = True
def __init__(self):
super(Closeable, self).__init__()
self._closed = False
self._closedlock = threading.Lock()
self._handlers = []
def __del__(self):
try:
self.close()
except ClosedError:
pass
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
@property
def closed(self):
return self._closed
def add_resource_to_close(self, resource, before=False):
"""Add a resource to be closed when closing."""
close = resource.close
if before:
def handle_closing(before):
if not before:
return
close()
else:
def handle_closing(before):
if before:
return
close()
self.add_close_handler(handle_closing)
def add_close_handler(self, handle_closing, nodupe=True):
"""Add a func to be called when closing.
The func takes one arg: True if it was called before the main
close func and False if after.
"""
with self._closedlock:
if self._closed:
if self.FAIL_ON_ALREADY_CLOSED:
raise ClosedError('already closed')
return
if nodupe and handle_closing in self._handlers:
raise ValueError('close func already added')
self._handlers.append(handle_closing)
def check_closed(self):
"""Raise ClosedError if closed."""
if self._closed:
if self.NAME:
raise ClosedError('{} closed'.format(self.NAME))
else:
raise ClosedError('closed')
@contextlib.contextmanager
def while_not_closed(self):
"""A context manager under which the object will not be closed."""
with self._closedlock:
self.check_closed()
yield
def close(self):
"""Release any owned resources and clean up."""
with self._closedlock:
if self._closed:
if self.FAIL_ON_ALREADY_CLOSED:
raise ClosedError('already closed')
return
self._closed = True
handlers = list(self._handlers)
results = call_all(handlers, True)
self._log_results(results)
self._close()
results = call_all(handlers, False)
self._log_results(results)
# implemented by subclasses
def _close(self):
pass
# internal methods
def _log_results(self, results, log=None):
if log is None:
return
for obj, exc in results:
if exc is None:
continue
log('failed to close {!r} ({!r})'.format(obj, exc))
########################
# running stuff
class NotRunningError(RuntimeError):
"""Something isn't currently running."""
class AlreadyStartedError(RuntimeError):
"""Something was already started."""
class AlreadyRunningError(AlreadyStartedError):
"""Something is already running."""
class Startable(object):
"""A base class for types that may be started."""
RESTARTABLE = False
FAIL_ON_ALREADY_STOPPED = True
def __init__(self):
super(Startable, self).__init__()
self._is_running = None
self._startlock = threading.Lock()
self._numstarts = 0
def is_running(self, checkclosed=True):
"""Return True if currently running."""
if checkclosed and hasattr(self, 'check_closed'):
self.check_closed()
is_running = self._is_running
if is_running is None:
return False
return is_running()
def start(self, *args, **kwargs):
"""Begin internal execution."""
with self._startlock:
if self.is_running():
raise AlreadyRunningError()
if not self.RESTARTABLE and self._numstarts > 0:
raise AlreadyStartedError()
self._is_running = self._start(*args, **kwargs)
self._numstarts += 1
def stop(self, *args, **kwargs):
"""Stop execution and wait until done."""
with self._startlock:
# TODO: Call self.check_closed() here?
if not self.is_running(checkclosed=False):
if not self.FAIL_ON_ALREADY_STOPPED:
return
raise NotRunningError()
self._is_running = None
self._stop(*args, **kwargs)
# implemented by subclasses
def _start(self, *args, **kwargs):
"""Return an "is_running()" func after starting."""
raise NotImplementedError
def _stop(self):
raise NotImplementedError
def is_py34():
return sys.version_info >= (3, 4,) and sys.version_info < (3, 5,)
def get_line_for_traceback(file_path, line_no):
try:
with open(file_path, 'r') as f:
return f.readlines()[line_no - 1]
except Exception:
return None
_enable_debug_break = False
def _allow_debug_break(enabled=True):
"""Enable breaking into debugger feature.
"""
global _enable_debug_break
_enable_debug_break = enabled
def _is_debug_break_allowed():
return _enable_debug_break