Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
_state.py368 linesDownload Raw Back to h11
1################################################################2# The core state machine3################################################################4#5# Rule 1: everything that affects the state machine and state transitions must6# live here in this file. As much as possible goes into the table-based7# representation, but for the bits that don't quite fit, the actual code and8# state must nonetheless live here.9#10# Rule 2: this file does not know about what role we're playing; it only knows11# about HTTP request/response cycles in the abstract. This ensures that we12# don't cheat and apply different rules to local and remote parties.13#14#15# Theory of operation16# ===================17#18# Possibly the simplest way to think about this is that we actually have 519# different state machines here. Yes, 5. These are:20#21# 1) The client state, with its complicated automaton (see the docs)22# 2) The server state, with its complicated automaton (see the docs)23# 3) The keep-alive state, with possible states {True, False}24# 4) The SWITCH_CONNECT state, with possible states {False, True}25# 5) The SWITCH_UPGRADE state, with possible states {False, True}26#27# For (3)-(5), the first state listed is the initial state.28#29# (1)-(3) are stored explicitly in member variables. The last30# two are stored implicitly in the pending_switch_proposals set as:31#   (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals)32#   (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals)33#34# And each of these machines has two different kinds of transitions:35#36# a) Event-triggered37# b) State-triggered38#39# Event triggered is the obvious thing that you'd think it is: some event40# happens, and if it's the right event at the right time then a transition41# happens. But there are somewhat complicated rules for which machines can42# "see" which events. (As a rule of thumb, if a machine "sees" an event, this43# means two things: the event can affect the machine, and if the machine is44# not in a state where it expects that event then it's an error.) These rules45# are:46#47# 1) The client machine sees all h11.events objects emitted by the client.48#49# 2) The server machine sees all h11.events objects emitted by the server.50#51#    It also sees the client's Request event.52#53#    And sometimes, server events are annotated with a _SWITCH_* event. For54#    example, we can have a (Response, _SWITCH_CONNECT) event, which is55#    different from a regular Response event.56#57# 3) The keep-alive machine sees the process_keep_alive_disabled() event58#    (which is derived from Request/Response events), and this event59#    transitions it from True -> False, or from False -> False. There's no way60#    to transition back.61#62# 4&5) The _SWITCH_* machines transition from False->True when we get a63#    Request that proposes the relevant type of switch (via64#    process_client_switch_proposals), and they go from True->False when we65#    get a Response that has no _SWITCH_* annotation.66#67# So that's event-triggered transitions.68#69# State-triggered transitions are less standard. What they do here is couple70# the machines together. The way this works is, when certain *joint*71# configurations of states are achieved, then we automatically transition to a72# new *joint* state. So, for example, if we're ever in a joint state with73#74#   client: DONE75#   keep-alive: False76#77# then the client state immediately transitions to:78#79#   client: MUST_CLOSE80#81# This is fundamentally different from an event-based transition, because it82# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state83# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive84# transitioned True -> False. Either way, once this precondition is satisfied,85# this transition is immediately triggered.86#87# What if two conflicting state-based transitions get enabled at the same88# time?  In practice there's only one case where this arises (client DONE ->89# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by90# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition.91#92# Implementation93# --------------94#95# The event-triggered transitions for the server and client machines are all96# stored explicitly in a table. Ditto for the state-triggered transitions that97# involve just the server and client state.98#99# The transitions for the other machines, and the state-triggered transitions100# that involve the other machines, are written out as explicit Python code.101#102# It'd be nice if there were some cleaner way to do all this. This isn't103# *too* terrible, but I feel like it could probably be better.104#105# WARNING106# -------107#108# The script that generates the state machine diagrams for the docs knows how109# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS110# tables. But it can't automatically read the transitions that are written111# directly in Python code. So if you touch those, you need to also update the112# script to keep it in sync!113from typing import cast, Dict, Optional, Set, Tuple, Type, Union114 115from ._events import *116from ._util import LocalProtocolError, Sentinel117 118# Everything in __all__ gets re-exported as part of the h11 public API.119__all__ = [120    "CLIENT",121    "SERVER",122    "IDLE",123    "SEND_RESPONSE",124    "SEND_BODY",125    "DONE",126    "MUST_CLOSE",127    "CLOSED",128    "MIGHT_SWITCH_PROTOCOL",129    "SWITCHED_PROTOCOL",130    "ERROR",131]132 133 134class CLIENT(Sentinel, metaclass=Sentinel):135    pass136 137 138class SERVER(Sentinel, metaclass=Sentinel):139    pass140 141 142# States143class IDLE(Sentinel, metaclass=Sentinel):144    pass145 146 147class SEND_RESPONSE(Sentinel, metaclass=Sentinel):148    pass149 150 151class SEND_BODY(Sentinel, metaclass=Sentinel):152    pass153 154 155class DONE(Sentinel, metaclass=Sentinel):156    pass157 158 159class MUST_CLOSE(Sentinel, metaclass=Sentinel):160    pass161 162 163class CLOSED(Sentinel, metaclass=Sentinel):164    pass165 166 167class ERROR(Sentinel, metaclass=Sentinel):168    pass169 170 171# Switch types172class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):173    pass174 175 176class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):177    pass178 179 180class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):181    pass182 183 184class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):185    pass186 187 188EventTransitionType = Dict[189    Type[Sentinel],190    Dict[191        Type[Sentinel],192        Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],193    ],194]195 196EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = {197    CLIENT: {198        IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED},199        SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},200        DONE: {ConnectionClosed: CLOSED},201        MUST_CLOSE: {ConnectionClosed: CLOSED},202        CLOSED: {ConnectionClosed: CLOSED},203        MIGHT_SWITCH_PROTOCOL: {},204        SWITCHED_PROTOCOL: {},205        ERROR: {},206    },207    SERVER: {208        IDLE: {209            ConnectionClosed: CLOSED,210            Response: SEND_BODY,211            # Special case: server sees client Request events, in this form212            (Request, CLIENT): SEND_RESPONSE,213        },214        SEND_RESPONSE: {215            InformationalResponse: SEND_RESPONSE,216            Response: SEND_BODY,217            (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL,218            (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL,219        },220        SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},221        DONE: {ConnectionClosed: CLOSED},222        MUST_CLOSE: {ConnectionClosed: CLOSED},223        CLOSED: {ConnectionClosed: CLOSED},224        SWITCHED_PROTOCOL: {},225        ERROR: {},226    },227}228 229StateTransitionType = Dict[230    Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]]231]232 233# NB: there are also some special-case state-triggered transitions hard-coded234# into _fire_state_triggered_transitions below.235STATE_TRIGGERED_TRANSITIONS: StateTransitionType = {236    # (Client state, Server state) -> new states237    # Protocol negotiation238    (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL},239    # Socket shutdown240    (CLOSED, DONE): {SERVER: MUST_CLOSE},241    (CLOSED, IDLE): {SERVER: MUST_CLOSE},242    (ERROR, DONE): {SERVER: MUST_CLOSE},243    (DONE, CLOSED): {CLIENT: MUST_CLOSE},244    (IDLE, CLOSED): {CLIENT: MUST_CLOSE},245    (DONE, ERROR): {CLIENT: MUST_CLOSE},246}247 248 249class ConnectionState:250    def __init__(self) -> None:251        # Extra bits of state that don't quite fit into the state model.252 253        # If this is False then it enables the automatic DONE -> MUST_CLOSE254        # transition. Don't set this directly; call .keep_alive_disabled()255        self.keep_alive = True256 257        # This is a subset of {UPGRADE, CONNECT}, containing the proposals258        # made by the client for switching protocols.259        self.pending_switch_proposals: Set[Type[Sentinel]] = set()260 261        self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}262 263    def process_error(self, role: Type[Sentinel]) -> None:264        self.states[role] = ERROR265        self._fire_state_triggered_transitions()266 267    def process_keep_alive_disabled(self) -> None:268        self.keep_alive = False269        self._fire_state_triggered_transitions()270 271    def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:272        self.pending_switch_proposals.add(switch_event)273        self._fire_state_triggered_transitions()274 275    def process_event(276        self,277        role: Type[Sentinel],278        event_type: Type[Event],279        server_switch_event: Optional[Type[Sentinel]] = None,280    ) -> None:281        _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type282        if server_switch_event is not None:283            assert role is SERVER284            if server_switch_event not in self.pending_switch_proposals:285                raise LocalProtocolError(286                    "Received server {} event without a pending proposal".format(287                        server_switch_event288                    )289                )290            _event_type = (event_type, server_switch_event)291        if server_switch_event is None and _event_type is Response:292            self.pending_switch_proposals = set()293        self._fire_event_triggered_transitions(role, _event_type)294        # Special case: the server state does get to see Request295        # events.296        if _event_type is Request:297            assert role is CLIENT298            self._fire_event_triggered_transitions(SERVER, (Request, CLIENT))299        self._fire_state_triggered_transitions()300 301    def _fire_event_triggered_transitions(302        self,303        role: Type[Sentinel],304        event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],305    ) -> None:306        state = self.states[role]307        try:308            new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]309        except KeyError:310            event_type = cast(Type[Event], event_type)311            raise LocalProtocolError(312                "can't handle event type {} when role={} and state={}".format(313                    event_type.__name__, role, self.states[role]314                )315            ) from None316        self.states[role] = new_state317 318    def _fire_state_triggered_transitions(self) -> None:319        # We apply these rules repeatedly until converging on a fixed point320        while True:321            start_states = dict(self.states)322 323            # It could happen that both these special-case transitions are324            # enabled at the same time:325            #326            #    DONE -> MIGHT_SWITCH_PROTOCOL327            #    DONE -> MUST_CLOSE328            #329            # For example, this will always be true of a HTTP/1.0 client330            # requesting CONNECT.  If this happens, the protocol switch takes331            # priority. From there the client will either go to332            # SWITCHED_PROTOCOL, in which case it's none of our business when333            # they close the connection, or else the server will deny the334            # request, in which case the client will go back to DONE and then335            # from there to MUST_CLOSE.336            if self.pending_switch_proposals:337                if self.states[CLIENT] is DONE:338                    self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL339 340            if not self.pending_switch_proposals:341                if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL:342                    self.states[CLIENT] = DONE343 344            if not self.keep_alive:345                for role in (CLIENT, SERVER):346                    if self.states[role] is DONE:347                        self.states[role] = MUST_CLOSE348 349            # Tabular state-triggered transitions350            joint_state = (self.states[CLIENT], self.states[SERVER])351            changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {})352            self.states.update(changes)353 354            if self.states == start_states:355                # Fixed point reached356                return357 358    def start_next_cycle(self) -> None:359        if self.states != {CLIENT: DONE, SERVER: DONE}:360            raise LocalProtocolError(361                "not in a reusable state. self.states={}".format(self.states)362            )363        # Can't reach DONE/DONE with any of these active, but still, let's be364        # sure.365        assert self.keep_alive366        assert not self.pending_switch_proposals367        self.states = {CLIENT: IDLE, SERVER: IDLE}368 
codekingpro/portable-devtools · Team Ai