Skip to content

server

Routes HTTP Server.

Attributes

mlogger = get_logger(__name__) module-attribute

REQUEST_HNDLR = handler.RequestHandler() module-attribute

EVENT_HNDLR = UI.ExternalEvent.Create(REQUEST_HNDLR) module-attribute

Classes

HttpRequestHandler

Bases: BaseHTTPRequestHandler

HTTP Requests Handler.

Methods:

log_message(message_format, *args)

Log request messages through pyRevit's logger.

Source code in pyrevitlib/pyrevit/routes/server/server.py
def log_message(self, message_format, *args):
    """Log request messages through pyRevit's logger."""
    mlogger.debug(
        "Routes request from %s | %s",
        self.client_address[0],
        message_format % args,
    )
do_POST()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_POST(self):
    self._process_request(method="POST")
do_GET()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_GET(self):
    self._process_request(method="GET")
do_PUT()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_PUT(self):
    self._process_request(method="PUT")
do_DELETE()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_DELETE(self):
    self._process_request(method="DELETE")
do_HEAD()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_HEAD(self):
    self._process_request(method="HEAD")
do_CONNECT()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_CONNECT(self):
    self._process_request(method="CONNECT")
do_OPTIONS()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_OPTIONS(self):
    self._process_request(method="OPTIONS")
do_TRACE()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_TRACE(self):
    self._process_request(method="TRACE")
do_PATCH()
Source code in pyrevitlib/pyrevit/routes/server/server.py
def do_PATCH(self):
    self._process_request(method="PATCH")

ThreadedHttpServer

Bases: ThreadingMixIn, HTTPServer

Threaded HTTP server.

Requests are served on threads that are never STA, while pyRevit directs stdout and stderr to its script output console, a WPF window that can only be created on Revit's STA UI thread. An exception escaping one of these threads is printed there and terminates the Revit process, so nothing here may write to stderr and no exception may leave a thread.

Request threads are daemons and are not joined on close, because a request in flight when the session is reloaded must not hold the host: they are abandoned with the process, and server_close() runs on whatever thread collects the server, including a session reload that is waiting on it.

Attributes

allow_reuse_address = True class-attribute instance-attribute
daemon_threads = True class-attribute instance-attribute
block_on_close = False class-attribute instance-attribute

Methods:

handle_error(request, client_address)

Report a failed request without writing to stderr.

See the class docstring for why stderr is unsafe here.

Note

Called outside an except block there is no exception to format, and IronPython 3's traceback.format_exc() raises on that instead of returning a placeholder, so the traceback is only formatted when one is active.

Parameters:

Name Type Description Default
request

The request being processed.

required
client_address tuple

Client address the request came from.

required
Source code in pyrevitlib/pyrevit/routes/server/server.py
def handle_error(self, request, client_address):
    """Report a failed request without writing to stderr.

    See the class docstring for why stderr is unsafe here.

    Note:
        Called outside an ``except`` block there is no exception to format,
        and IronPython 3's ``traceback.format_exc()`` raises on that instead
        of returning a placeholder, so the traceback is only formatted when
        one is active.

    Args:
        request: The request being processed.
        client_address (tuple): Client address the request came from.
    """
    error_type, error, error_traceback = sys.exc_info()
    if error_type is None:
        details = "no active exception"
    else:
        details = "".join(
            traceback.format_exception(error_type, error, error_traceback)
        )
    mlogger.debug("Routes request error | %s | %s", client_address, details)
process_request_thread(request, client_address)

Serve a request, letting no exception escape the thread.

Stopping the server closes sockets while requests are still in flight, so the per-request cleanup can raise. The inherited implementation leaves that path unguarded, and an exception escaping here terminates Revit for the reason given in the class docstring.

Parameters:

Name Type Description Default
request

The request to process.

required
client_address tuple

Client address the request came from.

required
Source code in pyrevitlib/pyrevit/routes/server/server.py
def process_request_thread(self, request, client_address):
    """Serve a request, letting no exception escape the thread.

    Stopping the server closes sockets while requests are still in flight,
    so the per-request cleanup can raise. The inherited implementation
    leaves that path unguarded, and an exception escaping here terminates
    Revit for the reason given in the class docstring.

    Args:
        request: The request to process.
        client_address (tuple): Client address the request came from.
    """
    try:
        self.finish_request(request, client_address)
    except Exception:
        try:
            self.handle_error(request, client_address)
        except Exception:
            mlogger.debug(
                "Routes handle_error failed | %s | %s",
                client_address,
                traceback.format_exc(),
            )
    finally:
        try:
            self.shutdown_request(request)
        except Exception:
            mlogger.debug(
                "Routes request cleanup error | %s | %s",
                client_address,
                traceback.format_exc(),
            )
shutdown()

Stop the accept loop, and always release the listening socket.

The socket release is in a finally because the inherited shutdown waits for the accept loop to acknowledge, which is the step that fails when the socket is already gone - and a socket left bound is the port a reloaded session cannot rebind.

Source code in pyrevitlib/pyrevit/routes/server/server.py
def shutdown(self):
    """Stop the accept loop, and always release the listening socket.

    The socket release is in a ``finally`` because the inherited shutdown
    waits for the accept loop to acknowledge, which is the step that fails
    when the socket is already gone - and a socket left bound is the port a
    reloaded session cannot rebind.
    """
    try:
        HTTPServer.shutdown(self)
    finally:
        try:
            self.socket.close()
        except Exception:
            mlogger.debug(
                "Routes listening socket close failed | %s",
                traceback.format_exc(),
            )

RoutesServer(host, port)

Bases: object

Route server thread handler.

It runs an HTTP server on the given host and port.

Parameters:

Name Type Description Default
host str

host

required
port int

port

required
Source code in pyrevitlib/pyrevit/routes/server/server.py
def __init__(self, host, port):
    self.server = ThreadedHttpServer((host, port), HttpRequestHandler)
    self.host = host
    self.port = port
    self.server_thread = None
    self.start()

Attributes

server = ThreadedHttpServer((host, port), HttpRequestHandler) instance-attribute
host = host instance-attribute
port = port instance-attribute
server_thread = None instance-attribute
is_running property

Check if the accept loop is still up.

Returns:

Type Description
bool

True if the server is accepting requests.

Methods:

start()

Start the accept loop, at most once, on a guarded thread.

Activation starts each server more than once, which left a second accept loop running against the same socket that nothing tracked and shutdown never joined. The thread is guarded because an exception escaping it terminates Revit; see the ThreadedHttpServer docstring.

Source code in pyrevitlib/pyrevit/routes/server/server.py
def start(self):
    """Start the accept loop, at most once, on a guarded thread.

    Activation starts each server more than once, which left a second
    accept loop running against the same socket that nothing tracked and
    shutdown never joined. The thread is guarded because an exception
    escaping it terminates Revit; see the ``ThreadedHttpServer`` docstring.
    """
    if self.is_running:
        return

    def serve_forever_guarded():
        try:
            self.server.serve_forever()
        except Exception:
            mlogger.debug("Routes server loop exited | %s", traceback.format_exc())

    self.server_thread = threading.Thread(target=serve_forever_guarded)
    self.server_thread.daemon = True
    self.server_thread.start()
waitForThread(timeout=5.0)

Join the accept loop thread, reporting a loop that outlives the wait.

Parameters:

Name Type Description Default
timeout float

seconds to wait for the accept loop to finish.

5.0
Source code in pyrevitlib/pyrevit/routes/server/server.py
def waitForThread(self, timeout=5.0):
    """Join the accept loop thread, reporting a loop that outlives the wait.

    Args:
        timeout (float): seconds to wait for the accept loop to finish.
    """
    if self.server_thread is None:
        return
    self.server_thread.join(timeout)
    if self.server_thread.is_alive():
        mlogger.error(
            "Routes accept loop on port %s did not stop within %s seconds",
            self.port,
            timeout,
        )
request_shutdown(timeout)

Ask the accept loop to stop, without waiting on it indefinitely.

BaseServer.shutdown sets the shutdown flag and then blocks until the accept loop acknowledges it, with no timeout of its own. That wait is the one unbounded step left in a teardown the host runs on a worker thread, so it is issued on its own daemon thread and given the same bound as the join that follows. A loop that never acknowledges is abandoned rather than waited for: the thread is a daemon, and the socket is closed on the way out either way, which is what stops a wedged loop in the end.

A failure is contained inside the thread, because an exception leaving a thread is printed to stderr by the runtime, which this class may not do.

Parameters:

Name Type Description Default
timeout float

seconds to wait for the loop to acknowledge.

required
Source code in pyrevitlib/pyrevit/routes/server/server.py
def request_shutdown(self, timeout):
    """Ask the accept loop to stop, without waiting on it indefinitely.

    ``BaseServer.shutdown`` sets the shutdown flag and then blocks until the
    accept loop acknowledges it, with no timeout of its own. That wait is
    the one unbounded step left in a teardown the host runs on a worker
    thread, so it is issued on its own daemon thread and given the same
    bound as the join that follows. A loop that never acknowledges is
    abandoned rather than waited for: the thread is a daemon, and the socket
    is closed on the way out either way, which is what stops a wedged loop
    in the end.

    A failure is contained inside the thread, because an exception leaving a
    thread is printed to stderr by the runtime, which this class may not do.

    Args:
        timeout (float): seconds to wait for the loop to acknowledge.
    """

    def request_stop():
        try:
            self.server.shutdown()
        except Exception:
            mlogger.error(
                "Routes server shutdown failed | %s", traceback.format_exc()
            )

    requester = threading.Thread(target=request_stop)
    requester.daemon = True
    requester.start()
    requester.join(timeout)
    if requester.is_alive():
        mlogger.error(
            "Routes accept loop on port %s did not acknowledge shutdown within "
            "%s seconds",
            self.port,
            timeout,
        )
stop(timeout=5.0)

Stop serving and release the port. Never raises, never waits longer than timeout.

This runs while a session is being reloaded or torn down, so a failure here would be reported through the output console and, unhandled on a worker thread, terminate Revit (see ThreadedHttpServer). Every failure is logged instead.

The two waits share one budget. The shutdown request and the join that follows are given the time the first one did not use, so a wedged accept loop delays the reload by at most timeout in total rather than once per wait.

Parameters:

Name Type Description Default
timeout float

seconds to wait, in total, for the accept loop to finish.

5.0
Source code in pyrevitlib/pyrevit/routes/server/server.py
def stop(self, timeout=5.0):
    """Stop serving and release the port. Never raises, never waits longer than ``timeout``.

    This runs while a session is being reloaded or torn down, so a failure
    here would be reported through the output console and, unhandled on a
    worker thread, terminate Revit (see ``ThreadedHttpServer``). Every
    failure is logged instead.

    The two waits share one budget. The shutdown request and the join that
    follows are given the time the first one did not use, so a wedged accept
    loop delays the reload by at most ``timeout`` in total rather than once
    per wait.

    Args:
        timeout (float): seconds to wait, in total, for the accept loop to finish.
    """
    deadline = time.time() + timeout
    try:
        if self.is_running:
            self.request_shutdown(timeout)
    except Exception:
        mlogger.error(
            "Routes server shutdown request failed | %s", traceback.format_exc()
        )

    try:
        self.server.server_close()
    except Exception:
        mlogger.error("Routes server close failed | %s", traceback.format_exc())

    self.waitForThread(max(0.0, deadline - time.time()))
    self.server_thread = None
    mlogger.debug("Routes server stopped | %s", self)

Functions: