| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796 |
- # Copyright (c) Microsoft Corporation. All rights reserved.
- # Licensed under the MIT License. See LICENSE in the project root
- # for license information.
- from __future__ import annotations
- import atexit
- import os
- import sys
- import debugpy
- from debugpy import adapter, common, launcher
- from debugpy.common import json, log, messaging, sockets
- from debugpy.adapter import clients, components, launchers, servers, sessions
- class Client(components.Component):
- """Handles the client side of a debug session."""
- message_handler = components.Component.message_handler
- known_subprocesses: set[servers.Connection]
- """Server connections to subprocesses that this client has been made aware of.
- """
- class Capabilities(components.Capabilities):
- PROPERTIES = {
- "supportsVariableType": False,
- "supportsVariablePaging": False,
- "supportsRunInTerminalRequest": False,
- "supportsMemoryReferences": False,
- "supportsArgsCanBeInterpretedByShell": False,
- "supportsStartDebuggingRequest": False,
- }
- class Expectations(components.Capabilities):
- PROPERTIES = {
- "locale": "en-US",
- "linesStartAt1": True,
- "columnsStartAt1": True,
- "pathFormat": json.enum("path", optional=True), # we don't support "uri"
- }
- def __init__(self, sock):
- if sock == "stdio":
- log.info("Connecting to client over stdio...", self)
- self.using_stdio = True
- stream = messaging.JsonIOStream.from_stdio()
- # Make sure that nothing else tries to interfere with the stdio streams
- # that are going to be used for DAP communication from now on.
- sys.stdin = stdin = open(os.devnull, "r")
- atexit.register(stdin.close)
- sys.stdout = stdout = open(os.devnull, "w")
- atexit.register(stdout.close)
- else:
- self.using_stdio = False
- stream = messaging.JsonIOStream.from_socket(sock)
- with sessions.Session() as session:
- super().__init__(session, stream)
- self.client_id = None
- """ID of the connecting client. This can be 'test' while running tests."""
- self.has_started = False
- """Whether the "launch" or "attach" request was received from the client, and
- fully handled.
- """
- self.start_request = None
- """The "launch" or "attach" request as received from the client.
- """
- self.restart_requested = False
- """Whether the client requested the debug adapter to be automatically
- restarted via "restart":true in the start request.
- """
- self._initialize_request = None
- """The "initialize" request as received from the client, to propagate to the
- server later."""
- self._deferred_events = []
- """Deferred events from the launcher and the server that must be propagated
- only if and when the "launch" or "attach" response is sent.
- """
- self._forward_terminate_request = False
- self.known_subprocesses = set()
- session.client = self
- session.register()
- # For the transition period, send the telemetry events with both old and new
- # name. The old one should be removed once the new one lights up.
- self.channel.send_event(
- "output",
- {
- "category": "telemetry",
- "output": "ptvsd",
- "data": {"packageVersion": debugpy.__version__},
- },
- )
- self.channel.send_event(
- "output",
- {
- "category": "telemetry",
- "output": "debugpy",
- "data": {"packageVersion": debugpy.__version__},
- },
- )
- sessions.report_sockets()
- def propagate_after_start(self, event):
- # pydevd starts sending events as soon as we connect, but the client doesn't
- # expect to see any until it receives the response to "launch" or "attach"
- # request. If client is not ready yet, save the event instead of propagating
- # it immediately.
- if self._deferred_events is not None:
- self._deferred_events.append(event)
- log.debug("Propagation deferred.")
- else:
- self.client.channel.propagate(event)
- def _propagate_deferred_events(self):
- log.debug("Propagating deferred events to {0}...", self.client)
- for event in self._deferred_events:
- log.debug("Propagating deferred {0}", event.describe())
- self.client.channel.propagate(event)
- log.info("All deferred events propagated to {0}.", self.client)
- self._deferred_events = None
- # Generic event handler. There are no specific handlers for client events, because
- # there are no events from the client in DAP - but we propagate them if we can, in
- # case some events appear in future protocol versions.
- @message_handler
- def event(self, event):
- if self.server:
- self.server.channel.propagate(event)
- # Generic request handler, used if there's no specific handler below.
- @message_handler
- def request(self, request):
- return self.server.channel.delegate(request)
- @message_handler
- def initialize_request(self, request):
- if self._initialize_request is not None:
- raise request.isnt_valid("Session is already initialized")
- self.client_id = request("clientID", "")
- self.capabilities = self.Capabilities(self, request)
- self.expectations = self.Expectations(self, request)
- self._initialize_request = request
- exception_breakpoint_filters = [
- {
- "filter": "raised",
- "label": "Raised Exceptions",
- "default": False,
- "description": "Break whenever any exception is raised.",
- },
- {
- "filter": "uncaught",
- "label": "Uncaught Exceptions",
- "default": True,
- "description": "Break when the process is exiting due to unhandled exception.",
- },
- {
- "filter": "userUnhandled",
- "label": "User Uncaught Exceptions",
- "default": False,
- "description": "Break when exception escapes into library code.",
- },
- ]
- return {
- "supportsCompletionsRequest": True,
- "supportsConditionalBreakpoints": True,
- "supportsConfigurationDoneRequest": True,
- "supportsDebuggerProperties": True,
- "supportsDelayedStackTraceLoading": True,
- "supportsEvaluateForHovers": True,
- "supportsExceptionInfoRequest": True,
- "supportsExceptionOptions": True,
- "supportsFunctionBreakpoints": True,
- "supportsHitConditionalBreakpoints": True,
- "supportsLogPoints": True,
- "supportsModulesRequest": True,
- "supportsSetExpression": True,
- "supportsSetVariable": True,
- "supportsValueFormattingOptions": True,
- "supportsTerminateDebuggee": True,
- "supportsTerminateRequest": True,
- "supportsGotoTargetsRequest": True,
- "supportsClipboardContext": True,
- "exceptionBreakpointFilters": exception_breakpoint_filters,
- "supportsStepInTargetsRequest": True,
- }
- # Common code for "launch" and "attach" request handlers.
- #
- # See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
- # for the sequence of request and events necessary to orchestrate the start.
- def _start_message_handler(f):
- @components.Component.message_handler
- def handle(self, request):
- assert request.is_request("launch", "attach")
- if self._initialize_request is None:
- raise request.isnt_valid("Session is not initialized yet")
- if self.launcher or self.server:
- raise request.isnt_valid("Session is already started")
- self.session.no_debug = request("noDebug", json.default(False))
- if self.session.no_debug:
- servers.dont_wait_for_first_connection()
- self.session.debug_options = debug_options = set(
- request("debugOptions", json.array(str))
- )
- f(self, request)
- if request.response is not None:
- return
- if self.server:
- self.server.initialize(self._initialize_request)
- self._initialize_request = None
- arguments = request.arguments
- if self.launcher:
- redirecting = arguments.get("console") == "internalConsole"
- if "RedirectOutput" in debug_options:
- # The launcher is doing output redirection, so we don't need the
- # server to do it, as well.
- arguments = dict(arguments)
- arguments["debugOptions"] = list(
- debug_options - {"RedirectOutput"}
- )
- redirecting = True
- if arguments.get("redirectOutput"):
- arguments = dict(arguments)
- del arguments["redirectOutput"]
- redirecting = True
- arguments["isOutputRedirected"] = redirecting
- # pydevd doesn't send "initialized", and responds to the start request
- # immediately, without waiting for "configurationDone". If it changes
- # to conform to the DAP spec, we'll need to defer waiting for response.
- try:
- self.server.channel.request(request.command, arguments)
- except messaging.NoMoreMessages:
- # Server closed connection before we could receive the response to
- # "attach" or "launch" - this can happen when debuggee exits shortly
- # after starting. It's not an error, but we can't do anything useful
- # here at this point, either, so just bail out.
- request.respond({})
- self.session.finalize(
- "{0} disconnected before responding to {1}".format(
- self.server,
- json.repr(request.command),
- )
- )
- return
- except messaging.MessageHandlingError as exc:
- exc.propagate(request)
- if self.session.no_debug:
- self.start_request = request
- self.has_started = True
- request.respond({})
- self._propagate_deferred_events()
- return
- # Let the client know that it can begin configuring the adapter.
- self.channel.send_event("initialized")
- self.start_request = request
- return messaging.NO_RESPONSE # will respond on "configurationDone"
- return handle
- @_start_message_handler
- def launch_request(self, request):
- from debugpy.adapter import launchers
- if self.session.id != 1 or len(servers.connections()):
- raise request.cant_handle('"attach" expected')
- debug_options = set(request("debugOptions", json.array(str)))
- # Handling of properties that can also be specified as legacy "debugOptions" flags.
- # If property is explicitly set to false, but the flag is in "debugOptions", treat
- # it as an error. Returns None if the property wasn't explicitly set either way.
- def property_or_debug_option(prop_name, flag_name):
- assert prop_name[0].islower() and flag_name[0].isupper()
- value = request(prop_name, bool, optional=True)
- if value == ():
- value = None
- if flag_name in debug_options:
- if value is False:
- raise request.isnt_valid(
- '{0}:false and "debugOptions":[{1}] are mutually exclusive',
- json.repr(prop_name),
- json.repr(flag_name),
- )
- value = True
- return value
- # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try
- # the alternative. But if both are missing, the error message should say "python".
- python_key = "python"
- if python_key in request:
- if "pythonPath" in request:
- raise request.isnt_valid(
- '"pythonPath" is not valid if "python" is specified'
- )
- elif "pythonPath" in request:
- python_key = "pythonPath"
- python = request(python_key, json.array(str, vectorize=True, size=(0,)))
- if not len(python):
- python = [sys.executable]
- python += request("pythonArgs", json.array(str, size=(0,)))
- request.arguments["pythonArgs"] = python[1:]
- request.arguments["python"] = python
- launcher_python = request("debugLauncherPython", str, optional=True)
- if launcher_python == ():
- launcher_python = python[0]
- program = module = code = ()
- if "program" in request:
- program = request("program", str)
- args = [program]
- request.arguments["processName"] = program
- if "module" in request:
- module = request("module", str)
- args = ["-m", module]
- request.arguments["processName"] = module
- if "code" in request:
- code = request("code", json.array(str, vectorize=True, size=(1,)))
- args = ["-c", "\n".join(code)]
- request.arguments["processName"] = "-c"
- num_targets = len([x for x in (program, module, code) if x != ()])
- if num_targets == 0:
- raise request.isnt_valid(
- 'either "program", "module", or "code" must be specified'
- )
- elif num_targets != 1:
- raise request.isnt_valid(
- '"program", "module", and "code" are mutually exclusive'
- )
- console = request(
- "console",
- json.enum(
- "internalConsole",
- "integratedTerminal",
- "externalTerminal",
- optional=True,
- ),
- )
- console_title = request("consoleTitle", json.default("Python Debug Console"))
- # Propagate "args" via CLI so that shell expansion can be applied if requested.
- target_args = request("args", json.array(str, vectorize=True))
- args += target_args
- # If "args" was a single string rather than an array, shell expansion must be applied.
- shell_expand_args = len(target_args) > 0 and isinstance(
- request.arguments["args"], str
- )
- if shell_expand_args:
- if not self.capabilities["supportsArgsCanBeInterpretedByShell"]:
- raise request.isnt_valid(
- 'Shell expansion in "args" is not supported by the client'
- )
- if console == "internalConsole":
- raise request.isnt_valid(
- 'Shell expansion in "args" is not available for "console":"internalConsole"'
- )
- cwd = request("cwd", str, optional=True)
- if cwd == ():
- # If it's not specified, but we're launching a file rather than a module,
- # and the specified path has a directory in it, use that.
- cwd = None if program == () else (os.path.dirname(program) or None)
- sudo = bool(property_or_debug_option("sudo", "Sudo"))
- if sudo and sys.platform == "win32":
- raise request.cant_handle('"sudo":true is not supported on Windows.')
- on_terminate = request("onTerminate", str, optional=True)
- if on_terminate:
- self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
- launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
- localhost = sockets.get_default_localhost()
- adapter_host = request("debugAdapterHost", localhost)
- try:
- servers.serve(adapter_host)
- except Exception as exc:
- raise request.cant_handle(
- "{0} couldn't create listener socket for servers: {1}",
- self.session,
- exc,
- )
- launchers.spawn_debuggee(
- self.session,
- request,
- [launcher_python],
- launcher_path,
- adapter_host,
- args,
- shell_expand_args,
- cwd,
- console,
- console_title,
- sudo,
- )
- @_start_message_handler
- def attach_request(self, request):
- if self.session.no_debug:
- raise request.isnt_valid('"noDebug" is not supported for "attach"')
- host = request("host", str, optional=True)
- port = request("port", int, optional=True)
- listen = request("listen", dict, optional=True)
- connect = request("connect", dict, optional=True)
- pid = request("processId", (int, str), optional=True)
- sub_pid = request("subProcessId", int, optional=True)
- on_terminate = request("onTerminate", bool, optional=True)
- if on_terminate:
- self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
- if host != () or port != ():
- if listen != ():
- raise request.isnt_valid(
- '"listen" and "host"/"port" are mutually exclusive'
- )
- if connect != ():
- raise request.isnt_valid(
- '"connect" and "host"/"port" are mutually exclusive'
- )
- if listen != ():
- if connect != ():
- raise request.isnt_valid(
- '"listen" and "connect" are mutually exclusive'
- )
- if pid != ():
- raise request.isnt_valid(
- '"listen" and "processId" are mutually exclusive'
- )
- if sub_pid != ():
- raise request.isnt_valid(
- '"listen" and "subProcessId" are mutually exclusive'
- )
- if pid != () and sub_pid != ():
- raise request.isnt_valid(
- '"processId" and "subProcessId" are mutually exclusive'
- )
- localhost = sockets.get_default_localhost()
- if listen != ():
- if servers.is_serving():
- raise request.isnt_valid(
- 'Multiple concurrent "listen" sessions are not supported'
- )
- host = listen("host", localhost)
- port = listen("port", int)
- adapter.access_token = None
- self.restart_requested = request("restart", False)
- host, port = servers.serve(host, port)
- else:
- if not servers.is_serving():
- servers.serve(localhost)
- host, port = sockets.get_address(servers.listener)
- # There are four distinct possibilities here.
- #
- # If "processId" is specified, this is attach-by-PID. We need to inject the
- # debug server into the designated process, and then wait until it connects
- # back to us. Since the injected server can crash, there must be a timeout.
- #
- # If "subProcessId" is specified, this is attach to a known subprocess, likely
- # in response to a "debugpyAttach" event. If so, the debug server should be
- # connected already, and thus the wait timeout is zero.
- #
- # If "listen" is specified, this is attach-by-socket with the server expected
- # to connect to the adapter via debugpy.connect(). There is no PID known in
- # advance, so just wait until the first server connection indefinitely, with
- # no timeout.
- #
- # If "connect" is specified, this is attach-by-socket in which the server has
- # spawned the adapter via debugpy.listen(). There is no PID known to the client
- # in advance, but the server connection should be either be there already, or
- # the server should be connecting shortly, so there must be a timeout.
- #
- # In the last two cases, if there's more than one server connection already,
- # this is a multiprocess re-attach. The client doesn't know the PID, so we just
- # connect it to the oldest server connection that we have - in most cases, it
- # will be the one for the root debuggee process, but if it has exited already,
- # it will be some subprocess.
- if pid != ():
- if not isinstance(pid, int):
- try:
- pid = int(pid)
- except Exception:
- raise request.isnt_valid('"processId" must be parseable as int')
- debugpy_args = request("debugpyArgs", json.array(str))
- def on_output(category, output):
- self.channel.send_event(
- "output",
- {
- "category": category,
- "output": output,
- },
- )
- try:
- servers.inject(pid, debugpy_args, on_output)
- except Exception as e:
- log.swallow_exception()
- self.session.finalize(
- "Error when trying to attach to PID:\n%s" % (str(e),)
- )
- return
- timeout = common.PROCESS_SPAWN_TIMEOUT
- pred = lambda conn: conn.pid == pid
- else:
- if sub_pid == ():
- pred = lambda conn: True
- timeout = common.PROCESS_SPAWN_TIMEOUT if listen == () else None
- else:
- pred = lambda conn: conn.pid == sub_pid
- timeout = 0
- self.channel.send_event("debugpyWaitingForServer", {"host": host, "port": port})
- conn = servers.wait_for_connection(self.session, pred, timeout)
- if conn is None:
- if sub_pid != ():
- # If we can't find a matching subprocess, it's not always an error -
- # it might have already exited, or didn't even get a chance to connect.
- # To prevent the client from complaining, pretend that the "attach"
- # request was successful, but that the session terminated immediately.
- request.respond({})
- self.session.finalize(
- 'No known subprocess with "subProcessId":{0}'.format(sub_pid)
- )
- return
- raise request.cant_handle(
- (
- "Timed out waiting for debug server to connect."
- if timeout
- else "There is no debug server connected to this adapter."
- ),
- sub_pid,
- )
- try:
- conn.attach_to_session(self.session)
- except ValueError:
- request.cant_handle("{0} is already being debugged.", conn)
- @message_handler
- def configurationDone_request(self, request):
- if self.start_request is None or self.has_started:
- request.cant_handle(
- '"configurationDone" is only allowed during handling of a "launch" '
- 'or an "attach" request'
- )
- try:
- self.has_started = True
- try:
- result = self.server.channel.delegate(request)
- except messaging.NoMoreMessages:
- # Server closed connection before we could receive the response to
- # "configurationDone" - this can happen when debuggee exits shortly
- # after starting. It's not an error, but we can't do anything useful
- # here at this point, either, so just bail out.
- request.respond({})
- self.start_request.respond({})
- self.session.finalize(
- "{0} disconnected before responding to {1}".format(
- self.server,
- json.repr(request.command),
- )
- )
- return
- else:
- request.respond(result)
- except messaging.MessageHandlingError as exc:
- self.start_request.cant_handle(str(exc))
- finally:
- if self.start_request.response is None:
- self.start_request.respond({})
- self._propagate_deferred_events()
- # Notify the client of any child processes of the debuggee that aren't already
- # being debugged.
- for conn in servers.connections():
- if conn.server is None and conn.ppid == self.session.pid:
- self.notify_of_subprocess(conn)
- @message_handler
- def evaluate_request(self, request):
- propagated_request = self.server.channel.propagate(request)
- def handle_response(response):
- request.respond(response.body)
- propagated_request.on_response(handle_response)
- return messaging.NO_RESPONSE
- @message_handler
- def pause_request(self, request):
- request.arguments["threadId"] = "*"
- return self.server.channel.delegate(request)
- @message_handler
- def continue_request(self, request):
- request.arguments["threadId"] = "*"
- try:
- return self.server.channel.delegate(request)
- except messaging.NoMoreMessages:
- # pydevd can sometimes allow the debuggee to exit before the queued
- # "continue" response gets sent. Thus, a failed "continue" response
- # indicating that the server disconnected should be treated as success.
- return {"allThreadsContinued": True}
- @message_handler
- def debugpySystemInfo_request(self, request):
- result = {"debugpy": {"version": debugpy.__version__}}
- if self.server:
- try:
- pydevd_info = self.server.channel.request("pydevdSystemInfo")
- except Exception:
- # If the server has already disconnected, or couldn't handle it,
- # report what we've got.
- pass
- else:
- result.update(pydevd_info)
- return result
- @message_handler
- def terminate_request(self, request):
- # If user specifically requests to terminate, it means that they don't want
- # debug session auto-restart kicking in.
- self.restart_requested = False
- if self._forward_terminate_request:
- # According to the spec, terminate should try to do a gracefull shutdown.
- # We do this in the server by interrupting the main thread with a Ctrl+C.
- # To force the kill a subsequent request would do a disconnect.
- #
- # We only do this if the onTerminate option is set though (the default
- # is a hard-kill for the process and subprocesses).
- return self.server.channel.delegate(request)
- self.session.finalize('client requested "terminate"', terminate_debuggee=True)
- return {}
- @message_handler
- def disconnect_request(self, request):
- # If user specifically requests to disconnect, it means that they don't want
- # debug session auto-restart kicking in.
- self.restart_requested = False
- terminate_debuggee = request("terminateDebuggee", bool, optional=True)
- if terminate_debuggee == ():
- terminate_debuggee = None
- self.session.finalize('client requested "disconnect"', terminate_debuggee)
- request.respond({})
- if self.using_stdio:
- # There's no way for the client to reconnect to this adapter once it disconnects
- # from this session, so close any remaining server connections.
- servers.stop_serving()
- log.info("{0} disconnected from stdio; closing remaining server connections.", self)
- for conn in servers.connections():
- try:
- conn.channel.close()
- except Exception:
- log.swallow_exception()
- # Close the client channel since we disconnected from the client.
- try:
- self.channel.close()
- except Exception:
- log.swallow_exception(level="warning")
- def disconnect(self):
- super().disconnect()
- def report_sockets(self):
- socks = [
- {
- "host": host,
- "port": port,
- "internal": listener is not clients.listener,
- }
- for listener in [clients.listener, launchers.listener, servers.listener]
- if listener is not None
- for (host, port) in [sockets.get_address(listener)]
- ]
- self.channel.send_event(
- "debugpySockets",
- {
- "sockets": socks
- },
- )
- def notify_of_subprocess(self, conn):
- log.info("{1} is a subprocess of {0}.", self, conn)
- with self.session:
- if self.start_request is None or conn in self.known_subprocesses:
- return
- if "processId" in self.start_request.arguments:
- log.warning(
- "Not reporting subprocess for {0}, because the parent process "
- 'was attached to using "processId" rather than "port".',
- self.session,
- )
- return
- log.info("Notifying {0} about {1}.", self, conn)
- body = dict(self.start_request.arguments)
- self.known_subprocesses.add(conn)
- self.session.notify_changed()
- for key in "processId", "listen", "preLaunchTask", "postDebugTask", "request", "restart":
- body.pop(key, None)
- body["name"] = "Subprocess {0}".format(conn.pid)
- body["subProcessId"] = conn.pid
- for key in "args", "processName", "pythonArgs":
- body.pop(key, None)
- host = body.pop("host", None)
- port = body.pop("port", None)
- if "connect" not in body:
- body["connect"] = {}
- if "host" not in body["connect"]:
- localhost = sockets.get_default_localhost()
- body["connect"]["host"] = host or localhost
- if "port" not in body["connect"]:
- if port is None:
- _, port = sockets.get_address(listener)
- body["connect"]["port"] = port
- if self.capabilities["supportsStartDebuggingRequest"]:
- self.channel.request("startDebugging", {
- "request": "attach",
- "configuration": body,
- })
- else:
- body["request"] = "attach"
- self.channel.send_event("debugpyAttach", body)
- def serve(host, port):
- global listener
- listener = sockets.serve("Client", Client, host, port)
- sessions.report_sockets()
- return sockets.get_address(listener)
- def stop_serving():
- global listener
- if listener is not None:
- try:
- listener.close()
- except Exception:
- log.swallow_exception(level="warning")
- listener = None
- sessions.report_sockets()
|