Skip to content
This repository was archived by the owner on Aug 2, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ptvsd/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pydevd

import ptvsd.wrapper
from ptvsd.pydevd_hooks import install


__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>"
Expand Down Expand Up @@ -58,7 +58,7 @@ def _run_argv(address, filename, extra, _prog=sys.argv[0]):
] + extra


def _run(argv, _pydevd=pydevd, _install=ptvsd.wrapper.install, **kwargs):
def _run(argv, _pydevd=pydevd, _install=install, **kwargs):
"""Start pydevd with the given commandline args."""
#print(' '.join(argv))

Expand All @@ -80,12 +80,12 @@ def _run(argv, _pydevd=pydevd, _install=ptvsd.wrapper.install, **kwargs):
sys.modules['__main___orig'] = sys.modules['__main__']
sys.modules['__main__'] = _pydevd

_install(_pydevd, **kwargs)
daemon = _install(_pydevd, **kwargs)
sys.argv[:] = argv
try:
_pydevd.main()
except SystemExit as ex:
ptvsd.wrapper.ptvsd_sys_exit_code = int(ex.code)
daemon.exitcode = int(ex.code)
raise


Expand Down
183 changes: 183 additions & 0 deletions ptvsd/daemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import atexit
import os
import platform
import signal
import sys

from ptvsd import wrapper
from ptvsd.socket import close_socket


def _wait_on_exit():
if sys.__stdout__ is not None:
try:
import msvcrt
except ImportError:
sys.__stdout__.write('Press Enter to continue . . . ')
sys.__stdout__.flush()
sys.__stdin__.read(1)
else:
sys.__stdout__.write('Press any key to continue . . . ')
sys.__stdout__.flush()
msvcrt.getch()


class DaemonClosedError(RuntimeError):
"""Indicates that a Daemon was unexpectedly closed."""
def __init__(self, msg='closed'):
super(DaemonClosedError, self).__init__(msg)


class Daemon(object):
"""The process-level manager for the VSC protocol debug adapter."""

exitcode = 0

def __init__(self, wait_on_exit=_wait_on_exit,
addhandlers=True, killonclose=True):
self.wait_on_exit = wait_on_exit
self.addhandlers = addhandlers
self.killonclose = killonclose

self._closed = False

self._pydevd = None
self._server = None
self._client = None
self._adapter = None

@property
def pydevd(self):
return self._pydevd

@property
def server(self):
return self._server

@property
def client(self):
return self._client

@property
def adapter(self):
return self._adapter

def start(self, server=None):
"""Return the "socket" to use for pydevd after setting it up."""
if self._closed:
raise DaemonClosedError()
if self._pydevd is not None:
raise RuntimeError('already started')
self._pydevd = wrapper.PydevdSocket(
self._handle_pydevd_message,
self._handle_pydevd_close,
self._getpeername,
self._getsockname,
)
self._server = server
return self._pydevd

def set_connection(self, client):
"""Set the client socket to use for the debug adapter.

A VSC message loop is started for the client.
"""
if self._closed:
raise DaemonClosedError()
if self._pydevd is None:
raise RuntimeError('not started yet')
if self._client is not None:
raise RuntimeError('connection already set')
self._client = client

self._adapter = wrapper.VSCodeMessageProcessor(
client,
self._pydevd.pydevd_notify,
self._pydevd.pydevd_request,
self._handle_vsc_disconnect,
self._handle_vsc_close,
)
name = 'ptvsd.Client' if self._server is None else 'ptvsd.Server'
self._adapter.start(name)
if self.addhandlers:
self._add_atexit_handler()
self._set_signal_handlers()
return self._adapter

def close(self):
"""Stop all loops and release all resources."""
if self._closed:
raise DaemonClosedError('already closed')
self._closed = True

if self._adapter is not None:
normal, abnormal = self._adapter._wait_options()
if (normal and not self.exitcode) or (abnormal and self.exitcode):
self.wait_on_exit_func()

if self._pydevd is not None:
close_socket(self._pydevd)
if self._client is not None:
self._release_connection()

# internal methods

def _add_atexit_handler(self):
def handler():
if not self._closed:
self.close()
if self._adapter is not None:
# TODO: Do this in VSCodeMessageProcessor.close()?
self._adapter._wait_for_server_thread()
atexit.register(handler)

def _set_signal_handlers(self):
if platform.system() == 'Windows':
return None

def handler(signum, frame):
if not self._closed:
self.close()
sys.exit(0)
signal.signal(signal.SIGHUP, handler)

def _release_connection(self):
if self._adapter is not None:
# TODO: This is not correct in the "attach" case.
self._adapter.handle_pydevd_stopped(self.exitcode)
self._adapter.close()
close_socket(self._client)

# internal methods for PyDevdSocket().

def _handle_pydevd_message(self, cmdid, seq, text):
if self._adapter is not None:
self._adapter.on_pydevd_event(cmdid, seq, text)

def _handle_pydevd_close(self):
if self._closed:
return
self.close()

def _getpeername(self):
if self._client is None:
raise NotImplementedError
return self._client.getpeername()

def _getsockname(self):
if self._client is None:
raise NotImplementedError
return self._client.getsockname()

# internal methods for VSCodeMessageProcessor

def _handle_vsc_disconnect(self, kill=False):
if not self._closed:
self.close()
if kill and self.killonclose:
os.kill(os.getpid(), signal.SIGTERM)

def _handle_vsc_close(self):
if self._closed:
return
self.close()
67 changes: 67 additions & 0 deletions ptvsd/pydevd_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import sys

from _pydevd_bundle import pydevd_comm

from ptvsd.socket import create_server, create_client
from ptvsd.daemon import Daemon


def start_server(daemon, port):
"""Return a socket to a (new) local pydevd-handling daemon.

The daemon supports the pydevd client wire protocol, sending
requests and handling responses (and events).

This is a replacement for _pydevd_bundle.pydevd_comm.start_server.
"""
server = create_server(port)
client, _ = server.accept()

pydevd = daemon.start(server)
daemon.set_connection(client)
return pydevd


def start_client(daemon, host, port):
"""Return a socket to an existing "remote" pydevd-handling daemon.

The daemon supports the pydevd client wire protocol, sending
requests and handling responses (and events).

This is a replacement for _pydevd_bundle.pydevd_comm.start_client.
"""
client = create_client()
client.connect((host, port))

pydevd = daemon.start()
daemon.set_connection(client)
return pydevd


def install(pydevd, start_server=start_server, start_client=start_client):
"""Configure pydevd to use our wrapper.

This is a bit of a hack to allow us to run our VSC debug adapter
in the same process as pydevd. Note that, as with most hacks,
this is somewhat fragile (since the monkeypatching sites may
change).
"""
daemon = Daemon()

_start_server = (lambda p: start_server(daemon, p))
_start_server.orig = start_server
_start_client = (lambda h, p: start_client(daemon, h, p))
_start_client.orig = start_client

# These are the functions pydevd invokes to get a socket to the client.
pydevd_comm.start_server = _start_server
pydevd_comm.start_client = _start_client

# Ensure that pydevd is using our functions.
pydevd.start_server = _start_server
pydevd.start_client = _start_client
__main__ = sys.modules['__main__']
if __main__ is not pydevd and __main__.__file__ == pydevd.__file__:
__main__.start_server = _start_server
__main__.start_client = _start_client
return daemon
57 changes: 57 additions & 0 deletions ptvsd/socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import absolute_import

import contextlib
import errno
import socket


NOT_CONNECTED = (
errno.ENOTCONN,
errno.EBADF,
)


def create_server(port):
"""Return a local server socket listening on the given port."""
server = _new_sock()
server.bind(('127.0.0.1', port))
server.listen(1)
return server


def create_client():
"""Return a client socket that may be connected to a remote address."""
return _new_sock()


def _new_sock():
sock = socket.socket(socket.AF_INET,
socket.SOCK_STREAM,
socket.IPPROTO_TCP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock


@contextlib.contextmanager
def ignored_errno(*ignored):
"""A context manager that ignores the given errnos."""
try:
yield
except OSError as exc:
if exc.errno not in ignored:
raise


def shut_down(sock, how=socket.SHUT_RDWR, ignored=NOT_CONNECTED):
"""Shut down the given socket."""
with ignored_errno(*ignored or ()):
sock.shutdown(how)


def close_socket(sock):
"""Shutdown and close the socket."""
try:
shut_down(sock)
except Exception:
pass
sock.close()
Loading