Example #1
0
def _handle_subprocess(n, stream):
    class Handlers(object):
        _pid = None

        def ptvsd_subprocess_request(self, request):
            # When child process is spawned, the notification it sends only
            # contains information about itself and its immediate parent.
            # Add information about the root process before passing it on.
            arguments = dict(request.arguments)
            arguments.update({
                'rootProcessId': os.getpid(),
                'rootStartRequest': root_start_request,
            })

            self._pid = arguments['processId']
            with subprocess_lock:
                subprocesses[self._pid] = channel

            debug('ptvsd_subprocess: %r' % arguments)
            response = {'incomingConnection': False}
            subprocess_queue.put((arguments, response))
            subprocess_queue.join()
            return response

        def disconnect(self):
            if self._pid is not None:
                with subprocess_lock:
                    subprocesses.pop(self._pid, None)

    name = 'SubprocessListener-%d' % n
    channel = JsonMessageChannel(stream, Handlers(), name)
    channel.start()
Example #2
0
    def test_events(self):
        EVENTS = [
            {'seq': 1, 'type': 'event', 'event': 'stopped', 'body': {'reason': 'pause'}},
            {'seq': 2, 'type': 'event', 'event': 'unknown', 'body': {'something': 'else'}},
        ]

        events_received = []

        class Handlers(object):
            def stopped_event(self, channel, body):
                events_received.append((channel, body))

            def event(self, channel, event, body):
                events_received.append((channel, event, body))

        input, input_exhausted = self.iter_with_event(EVENTS)
        stream = LoggingJsonStream(JsonMemoryStream(input, []))
        channel = JsonMessageChannel(stream, Handlers())
        channel.start()
        input_exhausted.wait()

        assert events_received == [
            (channel, EVENTS[0]['body']),
            (channel, 'unknown', EVENTS[1]['body']),
        ]
Example #3
0
def _handle_subprocess(n, stream):
    class Handlers(object):
        _pid = None

        def ptvsd_subprocess_request(self, request):
            # When child process is spawned, the notification it sends only
            # contains information about itself and its immediate parent.
            # Add information about the root process before passing it on.
            arguments = dict(request.arguments)
            arguments.update({
                'rootProcessId': os.getpid(),
                'rootStartRequest': root_start_request,
            })

            self._pid = arguments['processId']
            with subprocess_lock:
                subprocesses[self._pid] = channel

            debug('ptvsd_subprocess: %r' % arguments)
            response = {'incomingConnection': False}
            subprocess_queue.put((arguments, response))
            subprocess_queue.join()
            return response

        def disconnect(self):
            if self._pid is not None:
                with subprocess_lock:
                    subprocesses.pop(self._pid, None)

    name = 'SubprocessListener-%d' % n
    channel = JsonMessageChannel(stream, Handlers(), name)
    channel.start()
Example #4
0
 def _setup_channel(self):
     self.stream = LoggingJsonStream(JsonIOStream.from_socket(self.socket),
                                     'ptvsd#%d' % self.ptvsd_port)
     handlers = MessageHandlers(request=self._process_request,
                                event=self._process_event)
     self.channel = JsonMessageChannel(self.stream, handlers)
     self.channel.start()
     self.connected.set()
Example #5
0
    def prepare_to_run(self,
                       perform_handshake=True,
                       filename=None,
                       module=None):
        """Spawns ptvsd using the configured method, telling it to execute the
        provided Python file or module, and establishes a message channel to it.

        If perform_handshake is True, calls self.handshake() before returning.
        """

        argv = [sys.executable]
        if self.method != 'attach_pid':
            argv += ['-m', 'ptvsd']

        if self.method == 'attach_socket':
            if self.ptvsd_port is None:
                self.ptvsd_port = 5678
            argv += ['--port', str(self.ptvsd_port)]
        else:
            port = self._listen()
            argv += ['--host', 'localhost', '--port', str(port)]

        if filename:
            assert not module
            argv += [filename]
        elif module:
            assert not filename
            argv += ['-m', module]

        env = os.environ.copy()
        env.update({'PYTHONPATH': PTVSD_SYS_PATH})

        print('Spawning %r' % argv)
        self.process = subprocess.Popen(argv, env=env, stdin=subprocess.PIPE)
        if self.ptvsd_port:
            # ptvsd will take some time to spawn and start listening on the port,
            # so just hammer at it until it responds (or we time out).
            while not self.socket:
                try:
                    self._connect()
                except socket.error:
                    pass
                time.sleep(0.1)
        else:
            self.connected.wait()
            assert self.socket

        self.stream = LoggingJsonStream(JsonIOStream.from_socket(self.socket))

        handlers = MessageHandlers(request=self._process_request,
                                   event=self._process_event)
        self.channel = JsonMessageChannel(self.stream, handlers)
        self.channel.start()

        if perform_handshake:
            self.handshake()
Example #6
0
    def test_requests(self):
        REQUESTS = [
            {'seq': 1, 'type': 'request', 'command': 'next', 'arguments': {'threadId': 3}},
            {'seq': 2, 'type': 'request', 'command': 'unknown', 'arguments': {'answer': 42}},
            {'seq': 3, 'type': 'request', 'command': 'pause', 'arguments': {'threadId': 5}},
        ]

        requests_received = []

        class Handlers(object):
            def next_request(self, request):
                assert request.command == 'next'
                requests_received.append((request.channel, request.arguments))
                return {'threadId': 7}

            def request(self, request):
                requests_received.append((request.channel, request.command, request.arguments))

            def pause_request(self, request):
                assert request.command == 'pause'
                requests_received.append((request.channel, request.arguments))
                raise RequestFailure('pause error')

        input, input_exhausted = self.iter_with_event(REQUESTS)
        output = []
        stream = LoggingJsonStream(JsonMemoryStream(input, output))
        channel = JsonMessageChannel(stream, Handlers())
        channel.start()
        input_exhausted.wait()

        assert requests_received == [
            (channel, REQUESTS[0]['arguments']),
            (channel, 'unknown', REQUESTS[1]['arguments']),
            (channel, REQUESTS[2]['arguments']),
        ]

        assert output == [
            {'seq': 1, 'type': 'response', 'request_seq': 1, 'command': 'next', 'success': True, 'body': {'threadId': 7}},
            {'seq': 2, 'type': 'response', 'request_seq': 2, 'command': 'unknown', 'success': True},
            {'seq': 3, 'type': 'response', 'request_seq': 3, 'command': 'pause', 'success': False, 'message': 'pause error'},
        ]
Example #7
0
def notify_root(port):
    assert options.subprocess_of

    debug('Subprocess %d notifying root process at port %d' % (os.getpid(), options.subprocess_notify))
    conn = create_client()
    conn.connect(('localhost', options.subprocess_notify))
    stream = JsonIOStream.from_socket(conn)
    channel = JsonMessageChannel(stream)
    channel.start()

    # Send the notification about ourselves to root, and wait for it to tell us
    # whether an incoming connection is anticipated. This will be true if root
    # had successfully propagated the notification to the IDE, and false if it
    # couldn't do so (e.g. because the IDE is not attached). There's also the
    # possibility that connection to root will just drop, e.g. if it crashes -
    # in that case, just exit immediately.

    request = channel.send_request('ptvsd_subprocess', {
        'parentProcessId': options.subprocess_of,
        'processId': os.getpid(),
        'port': port,
    })

    try:
        response = request.wait_for_response()
    except Exception:
        print('Failed to send subprocess notification; exiting', file=sys.__stderr__)
        traceback.print_exc()
        sys.exit(0)

    # Keep the channel open until we exit - root process uses open channels to keep
    # track of which subprocesses are alive and which are not.
    atexit.register(lambda: channel.close())

    if not response['incomingConnection']:
        debugger = get_global_debugger()
        while debugger is None:
            time.sleep(0.1)
            debugger = get_global_debugger()
        debugger.ready_to_run = True
Example #8
0
def notify_root(port):
    assert options.subprocess_of

    debug('Subprocess %d notifying root process at port %d' %
          (os.getpid(), options.subprocess_notify))
    conn = create_client()
    conn.connect(('localhost', options.subprocess_notify))
    stream = JsonIOStream.from_socket(conn)
    channel = JsonMessageChannel(stream)
    channel.start()

    # Send the notification about ourselves to root, and wait for it to tell us
    # whether an incoming connection is anticipated. This will be true if root
    # had successfully propagated the notification to the IDE, and false if it
    # couldn't do so (e.g. because the IDE is not attached). There's also the
    # possibility that connection to root will just drop, e.g. if it crashes -
    # in that case, just exit immediately.

    request = channel.send_request(
        'ptvsd_subprocess', {
            'parentProcessId': options.subprocess_of,
            'processId': os.getpid(),
            'port': port,
        })

    try:
        response = request.wait_for_response()
    except Exception:
        print('Failed to send subprocess notification; exiting',
              file=sys.__stderr__)
        traceback.print_exc()
        sys.exit(0)

    if not response['incomingConnection']:
        debugger = get_global_debugger()
        while debugger is None:
            time.sleep(0.1)
            debugger = get_global_debugger()
        debugger.ready_to_run = True
Example #9
0
    def test_responses(self):
        request1_sent = threading.Event()
        request2_sent = threading.Event()
        request3_sent = threading.Event()

        def iter_responses():
            request1_sent.wait()
            yield {'seq': 1, 'type': 'response', 'request_seq': 1, 'command': 'next', 'success': True, 'body': {'threadId': 3}}
            request2_sent.wait()
            yield {'seq': 2, 'type': 'response', 'request_seq': 2, 'command': 'pause', 'success': False, 'message': 'pause error'}
            request3_sent.wait()
            yield {'seq': 3, 'type': 'response', 'request_seq': 3, 'command': 'next', 'success': True, 'body': {'threadId': 5}}

        stream = LoggingJsonStream(JsonMemoryStream(iter_responses(), []))
        channel = JsonMessageChannel(stream, None)
        channel.start()

        # Blocking wait.
        request1 = channel.send_request('next')
        request1_sent.set()
        response1 = request1.wait_for_response()
        assert response1 == Response(True, 'next', body={'threadId': 3})

        # Async callback, registered before response is received.
        request2 = channel.send_request('pause')
        response2 = [None]
        response2_received = threading.Event()
        def response2_handler(response):
            response2[0] = response
            response2_received.set()
        request2.on_response(response2_handler)
        request2_sent.set()
        response2_received.wait()
        assert response2[0] == Response(False, 'pause', error_message='pause error')

        # Async callback, registered after response is received.
        request3 = channel.send_request('next')
        request3_sent.set()
        request3.wait_for_response()
        response3 = [None]
        response3_received = threading.Event()
        def response3_handler(response):
            response3[0] = response
            response3_received.set()
        request3.on_response(response3_handler)
        response3_received.wait()
        assert response3[0] == Response(True, 'next', body={'threadId': 5})
Example #10
0
def notify_root(port):
    assert options.subprocess_of

    ptvsd.log.debug('Subprocess (PID={0}) notifying root process at port {1}',
                    os.getpid(), options.subprocess_notify)
    conn = create_client()
    conn.connect(('localhost', options.subprocess_notify))
    stream = JsonIOStream.from_socket(conn, 'root-process')
    channel = JsonMessageChannel(stream)
    channel.start()

    # Send the notification about ourselves to root, and wait for it to tell us
    # whether an incoming connection is anticipated. This will be true if root
    # had successfully propagated the notification to the IDE, and false if it
    # couldn't do so (e.g. because the IDE is not attached). There's also the
    # possibility that connection to root will just drop, e.g. if it crashes -
    # in that case, just exit immediately.

    request = channel.send_request(
        'ptvsd_subprocess', {
            'parentProcessId': options.subprocess_of,
            'processId': os.getpid(),
            'port': port,
        })

    try:
        response = request.wait_for_response()
    except Exception:
        ptvsd.log.exception('Failed to send subprocess notification; exiting')
        sys.exit(0)

    # Keep the channel open until we exit - root process uses open channels to keep
    # track of which subprocesses are alive and which are not.
    atexit.register(lambda: channel.close())

    if not response['incomingConnection']:
        ptvsd.log.debug(
            'No IDE connection is expected for this subprocess; unpausing.')
        debugger = get_global_debugger()
        while debugger is None:
            time.sleep(0.1)
            debugger = get_global_debugger()
        debugger.ready_to_run = True
Example #11
0
class DebugSession(object):
    WAIT_FOR_EXIT_TIMEOUT = 10
    BACKCHANNEL_TIMEOUT = 20

    StopInfo = namedtuple('StopInfo',
                          'thread_stopped, stacktrace, thread_id, frame_id')

    def __init__(self, start_method='launch', ptvsd_port=None, pid=None):
        assert start_method in ('launch', 'attach_pid',
                                'attach_socket_cmdline',
                                'attach_socket_import')
        assert ptvsd_port is None or start_method.startswith('attach_socket_')

        print('New debug session with method %r' % str(start_method))

        self.target = ('code', 'print("OK")')
        self.start_method = start_method
        self.start_method_args = {}
        self.no_debug = False
        self.ptvsd_port = ptvsd_port or PTVSD_PORT
        self.multiprocess = False
        self.multiprocess_port_range = None
        self.debug_options = ['RedirectOutput']
        self.path_mappings = []
        self.success_exitcodes = None
        self.rules = []
        self.env = os.environ.copy()
        self.env['PYTHONPATH'] = os.path.dirname(debuggee.__file__)
        self.cwd = None
        self.expected_returncode = 0
        self.program_args = []
        self.log_dir = None

        self.is_running = False
        self.process = None
        self.pid = pid
        self.psutil_process = psutil.Process(self.pid) if self.pid else None
        self.kill_ptvsd = True
        self.skip_capture = False
        self.socket = None
        self.server_socket = None
        self.connected = threading.Event()
        self.backchannel_socket = None
        self.backchannel_port = None
        self.backchannel_established = threading.Event()
        self._output_capture_threads = []
        self.output_data = {'OUT': [], 'ERR': []}

        self.timeline = Timeline(ignore_unobserved=[
            Event('output'),
            Event('thread', ANY.dict_with({'reason': 'exited'}))
        ])
        self.timeline.freeze()
        self.perform_handshake = True
        self.use_backchannel = False

        # Expose some common members of timeline directly - these should be the ones
        # that are the most straightforward to use, and are difficult to use incorrectly.
        # Conversely, most tests should restrict themselves to this subset of the API,
        # and avoid calling members of timeline directly unless there is a good reason.
        self.new = self.timeline.new
        self.observe = self.timeline.observe
        self.wait_for_next = self.timeline.wait_for_next
        self.proceed = self.timeline.proceed
        self.expect_new = self.timeline.expect_new
        self.expect_realized = self.timeline.expect_realized
        self.all_occurrences_of = self.timeline.all_occurrences_of
        self.observe_all = self.timeline.observe_all

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        was_final = self.timeline.is_final
        self.close()
        assert exc_type is not None or was_final, (
            'Session timeline must be finalized before session goes out of scope at the end of the '
            'with-statement. Use wait_for_exit(), wait_for_termination(), or wait_for_disconnect() '
            'as appropriate.')

    def __contains__(self, expectation):
        return expectation in self.timeline

    @property
    def ignore_unobserved(self):
        return self.timeline.ignore_unobserved

    @ignore_unobserved.setter
    def ignore_unobserved(self, value):
        self.timeline.ignore_unobserved = value

    def close(self):
        if self.socket:
            try:
                self.socket.shutdown(socket.SHUT_RDWR)
                print('Closed socket to ptvsd#%d' % self.ptvsd_port)
            except socket.error as ex:
                print('Error while closing socket to ptvsd#%d: %s' %
                      (self.ptvsd_port, str(ex)))
                self.socket = None

        if self.server_socket:
            try:
                self.server_socket.shutdown(socket.SHUT_RDWR)
                print('Closed server socket to ptvsd#%d' % self.ptvsd_port)
            except socket.error as ex:
                print('Error while closing server socket to ptvsd#%d: %s' %
                      (self.ptvsd_port, str(ex)))
                self.server_socket = None

        if self.backchannel_socket:
            try:
                self.backchannel_socket.shutdown(socket.SHUT_RDWR)
                print('Closed backchannel to ptvsd#%d' % self.backchannel_port)
            except socket.error as ex:
                print(
                    'Error while closing backchannel socket to ptvsd#%d: %s' %
                    (self.ptvsd_port, str(ex)))
                self.backchannel_socket = None

        if self.process:
            if self.kill_ptvsd:
                try:
                    self._kill_process_tree()
                    print('Killed ptvsd process tree %d' % self.pid)
                except:
                    print('Error killing ptvsd process tree %d' % self.pid)
                    traceback.print_exc()
                    pass

            # Clean up pipes to avoid leaking OS handles.
            try:
                self.process.stdin.close()
            except:
                pass
            try:
                self.process.stdout.close()
            except:
                pass
            try:
                self.process.stderr.close()
            except:
                pass

        self._wait_for_remaining_output()

    def _get_argv_for_attach_using_import(self):
        argv = [sys.executable]
        return argv

    def _get_argv_for_launch(self):
        argv = [sys.executable]
        argv += [os.path.dirname(ptvsd.__file__)]
        argv += ['--client']
        argv += ['--host', 'localhost', '--port', str(self.ptvsd_port)]
        return argv

    def _get_argv_for_attach_using_cmdline(self):
        argv = [sys.executable]
        argv += [os.path.dirname(ptvsd.__file__)]
        argv += ['--wait']
        argv += ['--host', 'localhost', '--port', str(self.ptvsd_port)]
        return argv

    def _get_argv_for_attach_using_pid(self):
        argv = [sys.executable]
        argv += [os.path.dirname(ptvsd.__file__)]
        argv += [
            '--client', '--host', 'localhost', '--port',
            str(self.ptvsd_port)
        ]
        # argv += ['--pid', '<pid>']  # pid value to be appended later
        return argv

    def _get_target(self):
        argv = []
        run_as, path_or_code = self.target
        if run_as == 'file':
            assert os.path.isfile(path_or_code)
            argv += [path_or_code]
        elif run_as == 'module':
            if os.path.isfile(path_or_code) or os.path.isdir(path_or_code):
                self.env['PYTHONPATH'] += os.pathsep + os.path.dirname(
                    path_or_code)
                try:
                    module = path_or_code[len(os.path.dirname(path_or_code)) +
                                          1:-3]
                except Exception:
                    module = 'code_to_debug'
                argv += ['-m', module]
            else:
                argv += ['-m', path_or_code]
        elif run_as == 'code':
            if os.path.isfile(path_or_code):
                with open(path_or_code, 'r') as f:
                    code = f.read()
                argv += ['-c', code]
            else:
                argv += ['-c', path_or_code]
        else:
            pytest.fail()
        return argv

    def _setup_session(self, **kwargs):
        self.ignore_unobserved += [
            Event('thread', ANY.dict_with({'reason': 'started'})),
            Event('module')
        ] + kwargs.pop('ignore_unobserved', [])

        self.env.update(kwargs.pop('env', {}))
        self.start_method_args.update(kwargs.pop('args', {}))

        self.path_mappings += kwargs.pop('path_mappings', [])
        self.debug_options += kwargs.pop('debug_options', [])
        self.program_args += kwargs.pop('program_args', [])
        self.rules += kwargs.pop('rules', [])

        for k, v in kwargs.items():
            setattr(self, k, v)

        assert self.start_method in ('launch', 'attach_pid',
                                     'attach_socket_cmdline',
                                     'attach_socket_import')
        assert len(self.target) == 2
        assert self.target[0] in ('file', 'module', 'code')

    def initialize(self, **kwargs):
        """Spawns ptvsd using the configured method, telling it to execute the
        provided Python file, module, or code, and establishes a message channel
        to it.

        If use_backchannel is True, calls self.setup_backchannel() before returning.

        If perform_handshake is True, calls self.handshake() before returning.
        """
        self._setup_session(**kwargs)
        print('Initializing debug session for ptvsd#%d' % self.ptvsd_port)
        dbg_argv = []
        usr_argv = []
        if self.start_method == 'launch':
            self._listen()
            dbg_argv += self._get_argv_for_launch()
        elif self.start_method == 'attach_socket_cmdline':
            dbg_argv += self._get_argv_for_attach_using_cmdline()
        elif self.start_method == 'attach_socket_import':
            dbg_argv += self._get_argv_for_attach_using_import()
            # TODO: Remove adding to python path after enabling TOX
            ptvsd_path = os.path.dirname(
                os.path.dirname(ptvsd.__main__.__file__))
            self.env['PYTHONPATH'] = ptvsd_path + os.pathsep + self.env[
                'PYTHONPATH']
            self.env[PTVSD_ENABLE_KEY] = '1'
            self.env[PTVSD_HOST_KEY] = 'localhost'
            self.env[PTVSD_PORT_KEY] = str(self.ptvsd_port)
        elif self.start_method == 'attach_pid':
            self._listen()
            dbg_argv += self._get_argv_for_attach_using_pid()
        else:
            pytest.fail()

        if self.log_dir:
            dbg_argv += ['--log-dir', self.log_dir]

        if self.no_debug:
            dbg_argv += ['--nodebug']

        if self.start_method == 'attach_pid':
            usr_argv += [sys.executable]
            usr_argv += self._get_target()
        else:
            dbg_argv += self._get_target()

        if self.program_args:
            if self.start_method == 'attach_pid':
                usr_argv += list(self.program_args)
            else:
                dbg_argv += list(self.program_args)

        if self.multiprocess and 'Multiprocess' not in self.debug_options:
            self.debug_options += ['Multiprocess']

        if self.use_backchannel:
            self.setup_backchannel()
        if self.backchannel_port:
            self.env['PTVSD_BACKCHANNEL_PORT'] = str(self.backchannel_port)

        print('ptvsd: %s' % ptvsd.__file__)
        print('Start method: %s' % self.start_method)
        print('Target: (%s) %s' % self.target)
        print('Current directory: %s' % self.cwd)
        print('PYTHONPATH: %s' % self.env['PYTHONPATH'])
        if self.start_method == 'attach_pid':
            print('Spawning %r' % usr_argv)
        else:
            print('Spawning %r' % dbg_argv)

        spawn_args = usr_argv if self.start_method == 'attach_pid' else dbg_argv

        # ensure env is all string, this is needed for python 2.7 on windows
        temp_env = {}
        for k, v in self.env.items():
            temp_env[str(k)] = str(v)

        self.process = subprocess.Popen(spawn_args,
                                        env=temp_env,
                                        stdin=subprocess.PIPE,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,
                                        cwd=self.cwd)
        self.pid = self.process.pid
        self.psutil_process = psutil.Process(self.pid)
        self.is_running = True
        # watchdog.create(self.pid)

        if not self.skip_capture:
            self._capture_output(self.process.stdout, 'OUT')
            self._capture_output(self.process.stderr, 'ERR')

        if self.start_method == 'attach_pid':
            # This is a temp process spawned to inject debugger into the
            # running process
            dbg_argv += ['--pid', str(self.pid)]
            print('Spawning %r' % dbg_argv)
            temp_process = subprocess.Popen(dbg_argv)
            print('temp process has pid=%d' % temp_process.pid)

        if self.start_method not in ('launch', 'attach_pid'):
            self.connect()
        self.connected.wait()
        assert self.ptvsd_port
        assert self.socket
        print('ptvsd#%d has pid=%d' % (self.ptvsd_port, self.pid))

        telemetry = self.timeline.wait_for_next(Event('output'))
        assert telemetry == Event(
            'output', {
                'category': 'telemetry',
                'output': 'ptvsd',
                'data': {
                    'version': ptvsd.__version__
                },
            })

        if self.perform_handshake:
            return self.handshake()

    def wait_for_disconnect(self, close=True):
        """Waits for the connected ptvsd process to disconnect.
        """

        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d to disconnect' % self.ptvsd_port +
              colors.RESET)

        # self.channel.wait()
        self.channel.close()

        self.timeline.finalize()
        if close:
            self.timeline.close()

        wait_for_output()

    def wait_for_termination(self):
        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d to terminate' % self.ptvsd_port +
              colors.RESET)

        # BUG: ptvsd sometimes exits without sending 'terminate' or 'exited', likely due to
        # https://github.com/Microsoft/ptvsd/issues/530. So rather than wait for them, wait until
        # we disconnect, then check those events for proper body only if they're actually present.

        self.wait_for_disconnect(close=False)

        if Event('exited') in self:
            expected_returncode = self.expected_returncode

            # Due to https://github.com/Microsoft/ptvsd/issues/1278, exit code is not recorded
            # in the "exited" event correctly in attach scenarios on Windows.
            if self.start_method == 'attach_socket_import' and platform.system(
            ) == 'Windows':
                expected_returncode = ANY.int

            self.expect_realized(
                Event('exited', {'exitCode': expected_returncode}))

        if Event('terminated') in self:
            self.expect_realized(Event('exited') >> Event('terminated', {}))

        self.timeline.close()
        wait_for_output()

    def wait_for_exit(self):
        """Waits for the spawned ptvsd process to exit. If it doesn't exit within
        WAIT_FOR_EXIT_TIMEOUT seconds, forcibly kills the process. After the process
        exits, validates its return code to match expected_returncode.
        """

        if not self.is_running:
            return

        assert self.psutil_process is not None

        def kill():
            time.sleep(self.WAIT_FOR_EXIT_TIMEOUT)
            if self.is_running:
                print('ptvsd#%r (pid=%d) timed out, killing it' %
                      (self.ptvsd_port, self.pid))
                self._kill_process_tree()

        kill_thread = threading.Thread(target=kill,
                                       name='ptvsd#%r watchdog (pid=%d)' %
                                       (self.ptvsd_port, self.pid))
        kill_thread.daemon = True
        kill_thread.start()

        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d (pid=%d) to terminate' %
              (self.ptvsd_port, self.pid) + colors.RESET)
        returncode = self.psutil_process.wait()

        assert returncode == self.expected_returncode

        self.is_running = False
        self.wait_for_termination()

    def _kill_process_tree(self):
        assert self.psutil_process is not None
        procs = [self.psutil_process]
        try:
            procs += self.psutil_process.children(recursive=True)
        except:
            pass
        for p in procs:
            try:
                p.kill()
            except:
                pass

    def _listen(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.bind(('localhost', 0))
        _, self.ptvsd_port = self.server_socket.getsockname()
        self.server_socket.listen(0)

        def accept_worker():
            print('Listening for incoming connection from ptvsd#%d' %
                  self.ptvsd_port)
            self.socket, _ = self.server_socket.accept()
            print('Incoming ptvsd#%d connection accepted' % self.ptvsd_port)
            self._setup_channel()

        accept_thread = threading.Thread(target=accept_worker,
                                         name='ptvsd#%d listener' %
                                         self.ptvsd_port)
        accept_thread.daemon = True
        accept_thread.start()

    def connect(self):
        # ptvsd will take some time to spawn and start listening on the port,
        # so just hammer at it until it responds (or we time out).
        while not self.socket:
            try:
                self._try_connect()
            except socket.error as ex:
                print('Error connecting to ptvsd#%d: %s' %
                      (self.ptvsd_port, str(ex)))
            time.sleep(0.1)

    def _try_connect(self):
        print('Trying to connect to ptvsd#%d' % self.ptvsd_port)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(('localhost', self.ptvsd_port))
        print('Successfully connected to ptvsd#%d' % self.ptvsd_port)
        self.socket = sock
        self._setup_channel()

    def _setup_channel(self):
        self.stream = LoggingJsonStream(JsonIOStream.from_socket(self.socket),
                                        'ptvsd#%d' % self.ptvsd_port)
        handlers = MessageHandlers(request=self._process_request,
                                   event=self._process_event)
        self.channel = JsonMessageChannel(self.stream, handlers)
        self.channel.start()
        self.connected.set()

    def send_request(self, command, arguments=None, proceed=True):
        if self.timeline.is_frozen and proceed:
            self.proceed()

        request = self.timeline.record_request(command, arguments)
        request.sent = self.channel.send_request(command, arguments)
        request.sent.on_response(
            lambda response: self._process_response(request, response))

        def causing(*expectations):
            for exp in expectations:
                (request >> exp).wait()
            return request

        request.causing = causing

        return request

    def handshake(self):
        """Performs the handshake that establishes the debug session ('initialized'
        and 'launch' or 'attach').

        After this method returns, ptvsd is not running any code yet, but it is
        ready to accept any configuration requests (e.g. for initial breakpoints).
        Once initial configuration is complete, start_debugging() should be called
        to finalize the configuration stage, and start running code.
        """

        self.send_request('initialize', {
            'adapterID': 'test'
        }).wait_for_response()
        self.wait_for_next(Event('initialized', {}))

        request = 'launch' if self.start_method == 'launch' else 'attach'
        self.start_method_args.update({
            'debugOptions': self.debug_options,
            'pathMappings': self.path_mappings,
            'rules': self.rules,
        })
        if self.success_exitcodes is not None:
            self.start_method_args['successExitCodes'] = self.success_exitcodes
        self.send_request(request, self.start_method_args).wait_for_response()

        if not self.no_debug:
            # Issue 'threads' so that we get the 'thread' event for the main thread now,
            # rather than at some random time later during the test.
            (self.send_request('threads').causing(
                Event('thread')).wait_for_response())

    def start_debugging(self, freeze=True):
        """Finalizes the configuration stage, and issues a 'configurationDone' request
        to start running code under debugger.

        After this method returns, ptvsd is running the code in the script file or module
        that was specified via self.target.
        """

        configurationDone_request = self.send_request('configurationDone')

        if self.no_debug:
            start = self.wait_for_next(Response(configurationDone_request))
        else:
            # The relative ordering of 'process' and 'configurationDone' is not deterministic.
            # (implementation varies depending on whether it's launch or attach, but in any
            # case, it is an implementation detail).
            start = self.wait_for_next(
                Event('process') & Response(configurationDone_request))

            self.expect_new(
                Event(
                    'process', {
                        'name':
                        ANY.str,
                        'isLocalProcess':
                        True,
                        'startMethod':
                        'launch'
                        if self.start_method == 'launch' else 'attach',
                        'systemProcessId':
                        self.pid if self.pid is not None else ANY.int,
                    }))

        if not freeze:
            self.proceed()

        return start

    def _process_event(self, event):
        self.timeline.record_event(event.event, event.body, block=False)

    def _process_response(self, request_occ, response):
        self.timeline.record_response(request_occ, response.body, block=False)

    def _process_request(self, request):
        assert False, 'ptvsd should not be sending requests.'

    def setup_backchannel(self):
        self.backchannel_socket = socket.socket(socket.AF_INET,
                                                socket.SOCK_STREAM)
        self.backchannel_socket.settimeout(self.BACKCHANNEL_TIMEOUT)
        self.backchannel_socket.bind(('localhost', 0))
        _, self.backchannel_port = self.backchannel_socket.getsockname()
        self.backchannel_socket.listen(0)

        backchannel_thread = threading.Thread(target=self._backchannel_worker,
                                              name='bchan#%d listener' %
                                              self.ptvsd_port)
        backchannel_thread.daemon = True
        backchannel_thread.start()

    def _backchannel_worker(self):
        print('Listening for incoming backchannel connection for bchan#%d' %
              self.ptvsd_port)
        sock = None

        try:
            sock, _ = self.backchannel_socket.accept()
        except socket.timeout:
            assert sock is not None, 'bchan#%r timed out!' % self.ptvsd_port

        print('Incoming bchan#%d backchannel connection accepted' %
              self.ptvsd_port)
        sock.settimeout(None)
        self._backchannel_stream = LoggingJsonStream(
            JsonIOStream.from_socket(sock), 'bchan#%d' % self.ptvsd_port)
        self.backchannel_established.set()

    @property
    def backchannel(self):
        assert self.backchannel_port, 'backchannel() must be called after setup_backchannel()'
        self.backchannel_established.wait()
        return self._backchannel_stream

    def read_json(self):
        return self.backchannel.read_json()

    def write_json(self, value):
        self.timeline.unfreeze()
        t = self.timeline.mark(('sending', value))
        self.backchannel.write_json(value)
        return t

    def _capture_output(self, pipe, name):
        def _output_worker():
            while True:
                try:
                    line = pipe.readline()
                    if not line:
                        break
                    self.output_data[name].append(line)
                except Exception:
                    break
                else:
                    prefix = 'ptvsd#%d %s ' % (self.ptvsd_port, name)
                    line = colors.LIGHT_BLUE + prefix + colors.RESET + line.decode(
                        'utf-8')
                    print(line, end='')

        thread = threading.Thread(target=_output_worker,
                                  name='ptvsd#%r %s' % (self.ptvsd_port, name))
        thread.daemon = True
        thread.start()
        self._output_capture_threads.append(thread)

    def _wait_for_remaining_output(self):
        for thread in self._output_capture_threads:
            thread.join()

    def set_breakpoints(self, path, lines=()):
        return self.send_request('setBreakpoints',
                                 arguments={
                                     'source': {
                                         'path': path
                                     },
                                     'breakpoints': [{
                                         'line': bp_line
                                     } for bp_line in lines],
                                 }).wait_for_response().body.get(
                                     'breakpoints', None)

    def wait_for_thread_stopped(self, reason=ANY, text=None, description=None):
        thread_stopped = self.wait_for_next(
            Event('stopped', ANY.dict_with({'reason': reason})))

        if text is not None:
            assert text == thread_stopped.body['text']

        if description is not None:
            assert description == thread_stopped.body['description']

        tid = thread_stopped.body['threadId']

        assert thread_stopped.body['allThreadsStopped']
        assert thread_stopped.body['preserveFocusHint'] == \
            (thread_stopped.body['reason'] not in ['step', 'exception', 'breakpoint'])

        assert tid is not None

        resp_stacktrace = self.send_request('stackTrace',
                                            arguments={
                                                'threadId': tid,
                                            }).wait_for_response()
        assert resp_stacktrace.body['totalFrames'] > 0
        frames = resp_stacktrace.body['stackFrames']

        fid = frames[0]['id']

        return self.StopInfo(thread_stopped, resp_stacktrace, tid, fid)

    def connect_to_child_session(self, ptvsd_subprocess):
        child_port = ptvsd_subprocess.body['port']
        assert child_port != 0

        child_session = DebugSession(start_method='attach_socket_cmdline',
                                     ptvsd_port=child_port)
        try:
            child_session.ignore_unobserved = self.ignore_unobserved
            child_session.debug_options = self.debug_options
            child_session.rules = self.rules
            child_session.connect()
            child_session.handshake()
        except:
            child_session.close()
            raise
        else:
            return child_session

    def connect_to_next_child_session(self):
        ptvsd_subprocess = self.wait_for_next(Event('ptvsd_subprocess'))
        return self.connect_to_child_session(ptvsd_subprocess)

    def get_stdout_as_string(self):
        return b''.join(self.output_data['OUT'])

    def get_stderr_as_string(self):
        return b''.join(self.output_data['ERR'])

    def connect_with_new_session(self, **kwargs):
        ns = DebugSession(start_method='attach_socket_import',
                          ptvsd_port=self.ptvsd_port)
        try:
            ns._setup_session(**kwargs)
            ns.ignore_unobserved = self.ignore_unobserved
            ns.debug_options = self.debug_options
            ns.rules = self.rules

            ns.pid = self.pid
            ns.process = self.process
            ns.psutil_process = psutil.Process(ns.pid)
            ns.is_running = True

            ns.connect()
            ns.connected.wait()
            ns.handshake()
        except:
            ns.close()
        else:
            return ns
Example #12
0
class DebugSession(object):
    WAIT_FOR_EXIT_TIMEOUT = 5

    def __init__(self, method='launch', ptvsd_port=None):
        assert method in ('launch', 'attach_pid', 'attach_socket')
        assert ptvsd_port is None or method == 'attach_socket'

        print('New debug session with method %r' % method)

        self.method = method
        self.ptvsd_port = ptvsd_port
        self.process = None
        self.socket = None
        self.server_socket = None
        self.connected = threading.Event()
        self.timeline = Timeline()

    def stop(self):
        if self.process:
            try:
                self.process.kill()
            except:
                pass
        if self.socket:
            try:
                self.socket.shutdown(socket.SHUT_RDWR)
            except:
                self.socket = None
        if self.server_socket:
            try:
                self.server_socket.shutdown(socket.SHUT_RDWR)
            except:
                self.server_socket = None

    def prepare_to_run(self,
                       perform_handshake=True,
                       filename=None,
                       module=None):
        """Spawns ptvsd using the configured method, telling it to execute the
        provided Python file or module, and establishes a message channel to it.

        If perform_handshake is True, calls self.handshake() before returning.
        """

        argv = [sys.executable]
        if self.method != 'attach_pid':
            argv += ['-m', 'ptvsd']

        if self.method == 'attach_socket':
            if self.ptvsd_port is None:
                self.ptvsd_port = 5678
            argv += ['--port', str(self.ptvsd_port)]
        else:
            port = self._listen()
            argv += ['--host', 'localhost', '--port', str(port)]

        if filename:
            assert not module
            argv += [filename]
        elif module:
            assert not filename
            argv += ['-m', module]

        env = os.environ.copy()
        env.update({'PYTHONPATH': PTVSD_SYS_PATH})

        print('Spawning %r' % argv)
        self.process = subprocess.Popen(argv, env=env, stdin=subprocess.PIPE)
        if self.ptvsd_port:
            # ptvsd will take some time to spawn and start listening on the port,
            # so just hammer at it until it responds (or we time out).
            while not self.socket:
                try:
                    self._connect()
                except socket.error:
                    pass
                time.sleep(0.1)
        else:
            self.connected.wait()
            assert self.socket

        self.stream = LoggingJsonStream(JsonIOStream.from_socket(self.socket))

        handlers = MessageHandlers(request=self._process_request,
                                   event=self._process_event)
        self.channel = JsonMessageChannel(self.stream, handlers)
        self.channel.start()

        if perform_handshake:
            self.handshake()

    def wait_for_exit(self, expected_returncode=0):
        """Waits for the spawned ptvsd process to exit. If it doesn't exit within
        WAIT_FOR_EXIT_TIMEOUT seconds, forcibly kills the process. After the process
        exits, validates its return code to match expected_returncode.
        """

        process = self.process
        if not process:
            return

        def kill():
            time.sleep(self.WAIT_FOR_EXIT_TIMEOUT)
            print('ptvsd process timed out, killing it')
            p = process
            if p:
                p.kill()

        kill_thread = threading.Thread(target=kill)
        kill_thread.daemon = True
        kill_thread.start()

        process.wait()
        assert process.returncode == expected_returncode

    def _listen(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.bind(('localhost', 0))
        _, port = self.server_socket.getsockname()
        self.server_socket.listen(0)

        def accept_worker():
            print('Listening for incoming connection from ptvsd on port %d' %
                  port)
            self.socket, _ = self.server_socket.accept()
            print('Incoming ptvsd connection accepted')
            self.connected.set()

        accept_thread = threading.Thread(target=accept_worker)
        accept_thread.daemon = True
        accept_thread.start()

        return port

    def _connect(self):
        print('Trying to connect to ptvsd on port %d' % self.ptvsd_port)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(('localhost', self.ptvsd_port))
        print('Successfully connected to ptvsd')
        self.socket = sock

    def send_request(self, command, arguments=None):
        request = self.timeline.record_request(command, arguments)
        request.sent = self.channel.send_request(command, arguments)
        request.sent.on_response(
            lambda response: self._process_response(request, response))
        assert Request(command, arguments) in self.timeline
        return request

    def mark(self, id):
        return self.timeline.mark(id)

    @contextlib.contextmanager
    def causing(self, expectation):
        assert expectation not in self.timeline
        promised_occurrence = ['ONLY VALID AFTER END OF BLOCK!']
        yield promised_occurrence
        occ = self.wait_until(expectation)
        promised_occurrence[:] = (occ, )

    def handshake(self):
        """Performs the handshake that establishes the debug session.

        After this method returns, ptvsd is not running any code yet, but it is
        ready to accept any configuration requests (e.g. for initial breakpoints).
        Once initial configuration is complete, start_debugging() should be called
        to finalize the configuration stage, and start running code.
        """

        with self.causing(Event('initialized', {})):
            self.send_request('initialize', {
                'adapterID': 'test'
            }).wait_for_response()

    def start_debugging(self, arguments=None, force_threads=True):
        """Finalizes the configuration stage, and issues a 'launch' or an 'attach' request
        to start running code under debugger, passing arguments through.

        After this method returns, ptvsd is running the code in the script file or module
        that was specified in run().
        """

        request = 'launch' if self.method == 'launch' else 'attach'
        self.send_request(request, arguments).wait_for_response()
        if force_threads:
            self.send_request('threads').wait_for_response()
        self.send_request('configurationDone').wait_for_response()

    def _process_event(self, channel, event, body):
        self.timeline.record_event(event, body)
        if event == 'terminated':
            self.channel.close()

    def _process_response(self, request, response):
        body = response.body if response.success else RequestFailure(
            response.error_message)
        self.timeline.record_response(request, body)
        assert Response(request, body) in self.timeline

    def _process_request(self, channel, command, arguments):
        assert False, 'ptvsd should not be sending requests.'

    def wait_until(self, expectation):
        return self.timeline.wait_until(expectation)

    def history(self):
        return self.timeline.history
Example #13
0
class DebugSession(object):
    WAIT_FOR_EXIT_TIMEOUT = 5
    BACKCHANNEL_TIMEOUT = 15

    def __init__(self, method='launch', ptvsd_port=None, pid=None):
        assert method in ('launch', 'attach_pid', 'attach_socket')
        assert ptvsd_port is None or method == 'attach_socket'

        print('New debug session with method %r' % method)

        self.method = method
        self.ptvsd_port = ptvsd_port or 5678
        self.multiprocess = False
        self.multiprocess_port_range = None
        self.debug_options = ['RedirectOutput']
        self.env = os.environ.copy()
        self.env['PYTHONPATH'] = PTVSD_SYS_PATH
        self.cwd = None
        self.expected_returncode = 0
        self.program_args = []

        self.is_running = False
        self.process = None
        self.pid = pid
        self.psutil_process = psutil.Process(self.pid) if self.pid else None
        self.socket = None
        self.server_socket = None
        self.connected = threading.Event()
        self.backchannel_socket = None
        self.backchannel_port = None
        self.backchannel_established = threading.Event()
        self._output_capture_threads = []

        self.timeline = Timeline(ignore_unobserved=[
            Event('output'),
            Event('thread', ANY.dict_with({'reason': 'exited'}))
        ])
        self.timeline.freeze()

        # Expose some common members of timeline directly - these should be the ones
        # that are the most straightforward to use, and are difficult to use incorrectly.
        # Conversely, most tests should restrict themselves to this subset of the API,
        # and avoid calling members of timeline directly unless there is a good reason.
        self.new = self.timeline.new
        self.observe = self.timeline.observe
        self.wait_for_next = self.timeline.wait_for_next
        self.proceed = self.timeline.proceed
        self.expect_new = self.timeline.expect_new
        self.expect_realized = self.timeline.expect_realized
        self.all_occurrences_of = self.timeline.all_occurrences_of

    def add_file_to_pythonpath(self, filename):
        self.env['PYTHONPATH'] += os.pathsep + os.path.dirname(filename)

    def __contains__(self, expectation):
        return expectation in self.timeline

    @property
    def ignore_unobserved(self):
        return self.timeline.ignore_unobserved

    @ignore_unobserved.setter
    def ignore_unobserved(self, value):
        self.timeline.ignore_unobserved = value

    def stop(self):
        if self.socket:
            try:
                self.socket.shutdown(socket.SHUT_RDWR)
            except:
                self.socket = None

        if self.server_socket:
            try:
                self.server_socket.shutdown(socket.SHUT_RDWR)
            except:
                self.server_socket = None

        if self.backchannel_socket:
            try:
                self.backchannel_socket.shutdown(socket.SHUT_RDWR)
            except:
                self.backchannel_socket = None

        if self.process:
            try:
                self._kill_process_tree()
            except:
                traceback.print_exc()
                pass

        self._wait_for_remaining_output()

    def prepare_to_run(self,
                       perform_handshake=True,
                       filename=None,
                       module=None,
                       code=None,
                       backchannel=False):
        """Spawns ptvsd using the configured method, telling it to execute the
        provided Python file, module, or code, and establishes a message channel
        to it.

        If backchannel is True, calls self.setup_backchannel() before returning.

        If perform_handshake is True, calls self.handshake() before returning.
        """

        argv = [sys.executable]
        if self.method != 'attach_pid':
            argv += ['-m', 'ptvsd']

        if self.method == 'attach_socket':
            argv += ['--wait']
        else:
            self._listen()
            argv += ['--client']
        argv += ['--host', 'localhost', '--port', str(self.ptvsd_port)]

        if self.multiprocess:
            argv += ['--multiprocess']

        if self.multiprocess_port_range:
            argv += [
                '--multiprocess-port-range',
                '%d-%d' % self.multiprocess_port_range
            ]

        if filename:
            assert not module and not code
            argv += [filename]
        elif module:
            assert not filename and not code
            argv += ['-m', module]
        elif code:
            assert not filename and not module
            argv += ['-c', code]

        if self.program_args:
            argv += list(self.program_args)

        if backchannel:
            self.setup_backchannel()
        if self.backchannel_port:
            self.env['PTVSD_BACKCHANNEL_PORT'] = str(self.backchannel_port)

        print('Current directory: %s' % os.getcwd())
        print('PYTHONPATH: %s' % self.env['PYTHONPATH'])
        print('Spawning %r' % argv)
        self.process = subprocess.Popen(argv,
                                        env=self.env,
                                        stdin=subprocess.PIPE,
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,
                                        cwd=self.cwd)
        self.pid = self.process.pid
        self.psutil_process = psutil.Process(self.pid)
        self.is_running = True
        watchdog.create(self.pid)

        self._capture_output(self.process.stdout, 'OUT')
        self._capture_output(self.process.stderr, 'ERR')

        if self.method == 'attach_socket':
            self.connect()
        self.connected.wait()
        assert self.ptvsd_port
        assert self.socket
        print('ptvsd#%d has pid=%d' % (self.ptvsd_port, self.pid))

        telemetry = self.timeline.wait_for_next(Event('output'))
        assert telemetry == Event(
            'output', {
                'category': 'telemetry',
                'output': 'ptvsd',
                'data': {
                    'version': ptvsd.__version__
                }
            })

        if perform_handshake:
            return self.handshake()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.wait_for_exit()

    def wait_for_disconnect(self, close=True):
        """Waits for the connected ptvsd process to disconnect.
        """

        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d to disconnect' % self.ptvsd_port +
              colors.RESET)

        self.channel.wait()
        self.channel.close()

        self.timeline.finalize()
        if close:
            self.timeline.close()

    def wait_for_termination(self):
        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d to terminate' % self.ptvsd_port +
              colors.RESET)

        # BUG: ptvsd sometimes exits without sending 'terminate' or 'exited', likely due to
        # https://github.com/Microsoft/ptvsd/issues/530. So rather than wait for them, wait until
        # we disconnect, then check those events for proper body only if they're actually present.

        self.wait_for_disconnect(close=False)

        if sys.version_info < (3, ) or Event('exited') in self:
            self.expect_realized(
                Event('exited', {'exitCode': self.expected_returncode}))

        if sys.version_info < (3, ) or Event('terminated') in self:
            self.expect_realized(Event('exited') >> Event('terminated', {}))

        self.timeline.close()

    def wait_for_exit(self):
        """Waits for the spawned ptvsd process to exit. If it doesn't exit within
        WAIT_FOR_EXIT_TIMEOUT seconds, forcibly kills the process. After the process
        exits, validates its return code to match expected_returncode.
        """

        assert self.psutil_process is not None

        def kill():
            time.sleep(self.WAIT_FOR_EXIT_TIMEOUT)
            if self.is_running:
                print('ptvsd#%r (pid=%d) timed out, killing it' %
                      (self.ptvsd_port, self.pid))
                self._kill_process_tree()

        kill_thread = threading.Thread(target=kill,
                                       name='ptvsd#%r watchdog (pid=%d)' %
                                       (self.ptvsd_port, self.pid))
        kill_thread.daemon = True
        kill_thread.start()

        print(colors.LIGHT_MAGENTA +
              'Waiting for ptvsd#%d (pid=%d) to terminate' %
              (self.ptvsd_port, self.pid) + colors.RESET)
        self.psutil_process.wait()

        self.is_running = False

        if self.process is not None:
            self.process.wait()
            assert self.process.returncode == self.expected_returncode

        self.wait_for_termination()

    def _kill_process_tree(self):
        assert self.psutil_process is not None
        procs = [self.psutil_process]
        try:
            procs += self.psutil_process.children(recursive=True)
        except:
            pass
        for p in procs:
            try:
                p.kill()
            except:
                pass

    def _listen(self):
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.bind(('localhost', 0))
        _, self.ptvsd_port = self.server_socket.getsockname()
        self.server_socket.listen(0)

        def accept_worker():
            print('Listening for incoming connection from ptvsd#%d' %
                  self.ptvsd_port)
            self.socket, _ = self.server_socket.accept()
            print('Incoming ptvsd#%d connection accepted' % self.ptvsd_port)
            self._setup_channel()

        accept_thread = threading.Thread(target=accept_worker,
                                         name='ptvsd#%d listener' %
                                         self.ptvsd_port)
        accept_thread.daemon = True
        accept_thread.start()

    def connect(self):
        # ptvsd will take some time to spawn and start listening on the port,
        # so just hammer at it until it responds (or we time out).
        while not self.socket:
            try:
                self._try_connect()
            except socket.error:
                pass
            time.sleep(0.1)

    def _try_connect(self):
        print('Trying to connect to ptvsd#%d' % self.ptvsd_port)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(('localhost', self.ptvsd_port))
        print('Successfully connected to ptvsd#%d' % self.ptvsd_port)
        self.socket = sock
        self._setup_channel()

    def _setup_channel(self):
        self.stream = LoggingJsonStream(JsonIOStream.from_socket(self.socket),
                                        'ptvsd#%d' % self.ptvsd_port)
        handlers = MessageHandlers(request=self._process_request,
                                   event=self._process_event)
        self.channel = JsonMessageChannel(self.stream, handlers)
        self.channel.start()
        self.connected.set()

    def send_request(self, command, arguments=None, proceed=True):
        if self.timeline.is_frozen and proceed:
            self.proceed()

        request = self.timeline.record_request(command, arguments)
        request.sent = self.channel.send_request(command, arguments)
        request.sent.on_response(
            lambda response: self._process_response(request, response))

        def causing(*expectations):
            for exp in expectations:
                (request >> exp).wait()
            return request

        request.causing = causing

        return request

    def handshake(self):
        """Performs the handshake that establishes the debug session ('initialized'
        and 'launch' or 'attach').

        After this method returns, ptvsd is not running any code yet, but it is
        ready to accept any configuration requests (e.g. for initial breakpoints).
        Once initial configuration is complete, start_debugging() should be called
        to finalize the configuration stage, and start running code.
        """

        self.send_request('initialize', {
            'adapterID': 'test'
        }).wait_for_response()
        self.wait_for_next(Event('initialized', {}))

        request = 'launch' if self.method == 'launch' else 'attach'
        self.send_request(request, {
            'debugOptions': self.debug_options
        }).wait_for_response()

        # Issue 'threads' so that we get the 'thread' event for the main thread now,
        # rather than at some random time later during the test.
        (self.send_request('threads').causing(
            Event('thread')).wait_for_response())

    def start_debugging(self, freeze=True):
        """Finalizes the configuration stage, and issues a 'configurationDone' request
        to start running code under debugger.

        After this method returns, ptvsd is running the code in the script file or module
        that was specified in prepare_to_run().
        """

        configurationDone_request = self.send_request('configurationDone')

        # The relative ordering of 'process' and 'configurationDone' is not deterministic.
        # (implementation varies depending on whether it's launch or attach, but in any
        # case, it is an implementation detail).
        start = self.wait_for_next(
            Event('process') & Response(configurationDone_request))

        self.expect_new(
            Event(
                'process', {
                    'name':
                    ANY.str,
                    'isLocalProcess':
                    True,
                    'startMethod':
                    'launch' if self.method == 'launch' else 'attach',
                    'systemProcessId':
                    self.pid if self.pid is not None else ANY.int,
                }))

        if not freeze:
            self.proceed()

        if self.backchannel_port:
            self.backchannel_established.wait()

        return start

    def _process_event(self, event):
        self.timeline.record_event(event.event, event.body, block=False)

    def _process_response(self, request_occ, response):
        self.timeline.record_response(request_occ, response.body, block=False)

    def _process_request(self, request):
        assert False, 'ptvsd should not be sending requests.'

    def setup_backchannel(self):
        self.backchannel_socket = socket.socket(socket.AF_INET,
                                                socket.SOCK_STREAM)
        self.backchannel_socket.settimeout(self.BACKCHANNEL_TIMEOUT)
        self.backchannel_socket.bind(('localhost', 0))
        _, self.backchannel_port = self.backchannel_socket.getsockname()
        self.backchannel_socket.listen(0)

        backchannel_thread = threading.Thread(target=self._backchannel_worker,
                                              name='bchan#%d listener' %
                                              self.ptvsd_port)
        backchannel_thread.daemon = True
        backchannel_thread.start()

    def _backchannel_worker(self):
        print('Listening for incoming backchannel connection for bchan#%d' %
              self.ptvsd_port)
        sock = None

        try:
            sock, _ = self.backchannel_socket.accept()
        except socket.timeout:
            assert sock is not None, 'bchan#%r timed out!' % self.ptvsd_port

        print('Incoming bchan#%d backchannel connection accepted' %
              self.ptvsd_port)
        sock.settimeout(None)
        self._backchannel_stream = LoggingJsonStream(
            JsonIOStream.from_socket(sock), 'bchan#%d' % self.ptvsd_port)
        self.backchannel_established.set()

    @property
    def backchannel(self):
        assert self.backchannel_port, 'backchannel() must be called after setup_backchannel()'
        self.backchannel_established.wait()
        return self._backchannel_stream

    def read_json(self):
        return self.backchannel.read_json()

    def write_json(self, value):
        self.timeline.unfreeze()
        t = self.timeline.mark(('sending', value))
        self.backchannel.write_json(value)
        return t

    def _capture_output(self, pipe, name):
        def _output_worker():
            while True:
                try:
                    line = pipe.readline()
                    if not line:
                        break
                except Exception:
                    break
                else:
                    prefix = 'ptvsd#%d %s ' % (self.ptvsd_port, name)
                    line = colors.LIGHT_BLUE + prefix + colors.RESET + line.decode(
                        'utf-8')
                    print(line, end='')

        thread = threading.Thread(target=_output_worker,
                                  name='ptvsd#%r %s' % (self.ptvsd_port, name))
        thread.daemon = True
        thread.start()
        self._output_capture_threads.append(thread)

    def _wait_for_remaining_output(self):
        for thread in self._output_capture_threads:
            thread.join()
Example #14
0
    def test_fuzz(self):
        # Set up two channels over the same stream that send messages to each other
        # asynchronously, and record everything that they send and receive.
        # All records should match at the end.

        class Fuzzer(object):
            def __init__(self, name):
                self.name = name
                self.lock = threading.Lock()
                self.sent = []
                self.received = []
                self.responses_sent = []
                self.responses_received = []

            def start(self, channel):
                self._worker = threading.Thread(name=self.name, target=lambda: self._send_requests_and_events(channel))
                self._worker.daemon = True
                self._worker.start()

            def wait(self):
                self._worker.join()

            def fizz_event(self, channel, body):
                with self.lock:
                    self.received.append(('event', 'fizz', body))

            def buzz_event(self, channel, body):
                with self.lock:
                    self.received.append(('event', 'buzz', body))

            def event(self, channel, event, body):
                with self.lock:
                    self.received.append(('event', event, body))

            def make_and_log_response(self, command):
                x = random.randint(-100, 100)
                if x >= 0:
                    response = Response(True, command, body=x)
                else:
                    response = Response(False, command, error_message=str(x))
                with self.lock:
                    self.responses_sent.append(response)
                if response.success:
                    return x
                else:
                    raise RuntimeError(response.error_message)

            def fizz_request(self, channel, arguments):
                with self.lock:
                    self.received.append(('request', 'fizz', arguments))
                return self.make_and_log_response('fizz')

            def buzz_request(self, channel, arguments):
                with self.lock:
                    self.received.append(('request', 'buzz', arguments))
                return self.make_and_log_response('buzz')

            def request(self, channel, command, arguments):
                with self.lock:
                    self.received.append(('request', command, arguments))
                return self.make_and_log_response(command)

            def _send_requests_and_events(self, channel):
                pending_requests = [0]
                for _ in range(0, 100):
                    typ = random.choice(('event', 'request'))
                    name = random.choice(('fizz', 'buzz', 'fizzbuzz'))
                    body = random.randint(0, 100)
                    with self.lock:
                        self.sent.append((typ, name, body))
                    if typ == 'event':
                        channel.send_event(name, body)
                    elif typ == 'request':
                        with self.lock:
                            pending_requests[0] += 1
                        req = channel.send_request(name, body)
                        def response_handler(response):
                            with self.lock:
                                self.responses_received.append(response)
                                pending_requests[0] -= 1
                        req.on_response(response_handler)
                # Spin until we get responses to all requests.
                while True:
                    with self.lock:
                        if pending_requests[0] == 0:
                            break
                    time.sleep(0.1)

        fuzzer1 = Fuzzer('fuzzer1')
        fuzzer2 = Fuzzer('fuzzer2')

        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind(('localhost', 0))
        _, port = server_socket.getsockname()
        server_socket.listen(0)

        socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        socket1_thread = threading.Thread(target=lambda: socket1.connect(('localhost', port)))
        socket1_thread.start()
        socket2, _ = server_socket.accept()
        socket1_thread.join()

        try:
            io1 = socket1.makefile('rwb', 0)
            io2 = socket2.makefile('rwb', 0)

            stream1 = LoggingJsonStream(JsonIOStream(io1, io1))
            channel1 = JsonMessageChannel(stream1, fuzzer1)
            channel1.start()
            fuzzer1.start(channel1)

            stream2 = LoggingJsonStream(JsonIOStream(io2, io2))
            channel2 = JsonMessageChannel(stream2, fuzzer2)
            channel2.start()
            fuzzer2.start(channel2)

            fuzzer1.wait()
            fuzzer2.wait()

        finally:
            socket1.close()
            socket2.close()

        assert fuzzer1.sent == fuzzer2.received
        assert fuzzer2.sent == fuzzer1.received
        assert fuzzer1.responses_sent == fuzzer2.responses_received
        assert fuzzer2.responses_sent == fuzzer1.responses_received
Example #15
0
    def test_responses(self):
        request1_sent = threading.Event()
        request2_sent = threading.Event()
        request3_sent = threading.Event()

        def iter_responses():
            request1_sent.wait()
            yield {
                'seq': 1,
                'type': 'response',
                'request_seq': 1,
                'command': 'next',
                'success': True,
                'body': {
                    'threadId': 3
                }
            }
            request2_sent.wait()
            yield {
                'seq': 2,
                'type': 'response',
                'request_seq': 2,
                'command': 'pause',
                'success': False,
                'message': 'pause error'
            }
            request3_sent.wait()
            yield {
                'seq': 3,
                'type': 'response',
                'request_seq': 3,
                'command': 'next',
                'success': True,
                'body': {
                    'threadId': 5
                }
            }

        stream = LoggingJsonStream(JsonMemoryStream(iter_responses(), []))
        channel = JsonMessageChannel(stream, None)
        channel.start()

        # Blocking wait.
        request1 = channel.send_request('next')
        request1_sent.set()
        response1_body = request1.wait_for_response()
        response1 = request1.response

        assert response1.success
        assert response1.request is request1
        assert response1.body == response1_body
        assert response1.body == {'threadId': 3}

        # Async callback, registered before response is received.
        request2 = channel.send_request('pause')
        response2 = []
        response2_received = threading.Event()

        def response2_handler(resp):
            response2.append(resp)
            response2_received.set()

        request2.on_response(response2_handler)
        request2_sent.set()
        response2_received.wait()
        response2, = response2

        assert not response2.success
        assert response2.request is request2
        assert response2 is request2.response
        assert response2.body == RequestFailure('pause error')

        # Async callback, registered after response is received.
        request3 = channel.send_request('next')
        request3_sent.set()
        request3.wait_for_response()
        response3 = []
        response3_received = threading.Event()

        def response3_handler(resp):
            response3.append(resp)
            response3_received.set()

        request3.on_response(response3_handler)
        response3_received.wait()
        response3, = response3

        assert response3.success
        assert response3.request is request3
        assert response3 is request3.response
        assert response3.body == {'threadId': 5}