File size: 31,341 Bytes
d1ceb73 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 |
# 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,
"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__))
adapter_host = request("debugAdapterHost", "127.0.0.1")
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'
)
if listen != ():
if servers.is_serving():
raise request.isnt_valid(
'Multiple concurrent "listen" sessions are not supported'
)
host = listen("host", "127.0.0.1")
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()
host, port = servers.listener.getsockname()
# 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()
def disconnect(self):
super().disconnect()
def report_sockets(self):
sockets = [
{
"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 [listener.getsockname()]
]
self.channel.send_event(
"debugpySockets",
{
"sockets": sockets
},
)
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"]:
body["connect"]["host"] = host if host is not None else "127.0.0.1"
if "port" not in body["connect"]:
if port is None:
_, port = listener.getsockname()
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 listener.getsockname()
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()
|