Chronaxis用户手册English
本页目录

chronaxis._control_wire

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""内部有限控制消息;Connection 只收发 bytes,不使用隐含 pickle。23CCP1 的固定头为版本、通道、命令和 uint64 总字节数,主体分片不超过 64KiB。4普通CAN/fast由已有工作预算限定;独立batch沿用无全批RAM硬上限的边界。5任务与固定结果逐片接收,不按声明总长预分配,也不截断合法身份。6"""7import json8from math import isfinite9import struct1011from ._control_models import pack_message, restore_message, native, check_message_fields1213CHUNK = 6553614_HEADER = struct.Struct('!4sBBQ')15_FAMILIES = {'can':1, 'fast':2, 'batch':3}16_COMMANDS = {17    'can': ('file','next','close','ready','block','sent','error'),18    'fast': ('file','program_file','direct','spool','next','close','offer','delivered','stored','eof','error'),19    'batch': ('run','fast','cleanup','close','result','fast_result','cleaned','cleanup_failed'),20}21_PARENT_COMMANDS = {22    'can': {'file','next','close'},23    'fast': {'file','program_file','direct','spool','next','close'},24    'batch': {'run','fast','cleanup','close'},25}26_LARGE = {'file','program_file','run','fast','result','fast_result','eof'}272829class ControlProtocolError(ValueError):30    """内部协议版本、长度、命令或固定字段错误。"""313233def _pairs(items):34    """建立不重复的JSON对象。3536    参数: items为json解析器给出的键值对。37    返回: dict。38    异常: 重复键时ControlProtocolError。39    """40    result = {}41    for key,value in items:42        if key in result:43            raise ControlProtocolError('控制JSON包含重复键')44        result[key] = value45    return result464748def _depth(data):49    """在解析前核JSON容器深度。5051    参数: data为受长度限定的bytes或bytearray。52    返回: None,忽略字符串内的括号。53    异常: 超过320层或结构未闭合时ControlProtocolError。54    """55    depth = 056    quoted = escaped = False57    for char in data:58        if quoted:59            if escaped:60                escaped = False61            elif char == 92:62                escaped = True63            elif char == 34:64                quoted = False65        elif char == 34:66            quoted = True67        elif char in (91,123):68            depth += 169            if depth > 320:70                raise ControlProtocolError('控制JSON嵌套超过320层')71        elif char in (93,125):72            depth -= 173            if depth < 0:74                raise ControlProtocolError('控制JSON容器结构非法')75    if quoted or depth:76        raise ControlProtocolError('控制JSON截断或容器结构非法')777879def encode_json(value, maximum):80    """逐片编码有限JSON结构。8182    参数: value为已检查原生树,maximum为字节上限或父当前计划的None。83    返回: bytearray。84    异常: JSON类型、非有限数或超过额度。85    """86    output = bytearray()87    encoder = json.JSONEncoder(ensure_ascii=True,allow_nan=False,separators=(',',':'))88    for part in encoder.iterencode(value):89        data = part.encode('ascii')90        if maximum is not None and len(output)+len(data) > maximum:91            raise ControlProtocolError('控制消息超过本次接收额度')92        output.extend(data)93    return output949596def decode_json(data):97    """解码有限JSON为原生值。9899    参数: data为已按本次额度接收的字节。100    返回: dict/list/原生标量。101    异常: 重复键、非有限数、语法或深度错误。102    """103    _depth(data)104    def constant(value):105        """拒绝JSON宽松常量。106107        参数: value为NaN/Infinity类原标记。108        返回: 无,始终抛错。109        异常: ControlProtocolError。110        """111        raise ControlProtocolError('控制JSON不允许非有限常量')112    try:113        value = json.loads(data,object_pairs_hook=_pairs,parse_constant=constant)114        # JSON 1e999 不走parse_constant,需要核最终数值是否有限。115        return native(value)116    except (UnicodeError,json.JSONDecodeError,RecursionError) as error:117        raise ControlProtocolError('控制JSON不是完整有效结构') from error118119120def _integer(value, minimum=0):121    """判断原生整数下界。122123    参数: value候选值,minimum默认0。124    返回: bool,不把bool当整数。125    异常: 无。126    """127    return type(value) is int and value >= minimum128129130def _time(value):131    """判断有限非负计时。132133    参数: value为候选秒数。134    返回: bool。135    异常: 无。136    """137    return type(value) in (int,float) and isfinite(value) and value >= 0138139140def _check(family, command, args):141    """核控制命令基础形状。142143    参数: family通道、command命令、args参数列表或元组。144    返回: None。145    异常: 数量或字段基本类型不符时ControlProtocolError。146    """147    if type(args) not in (tuple,list):148        raise ControlProtocolError('控制消息参数非法')149    if command in ('next','close','direct','spool','cleanup'):150        valid = len(args)==0151    elif command == 'error':152        valid = len(args)==2 and all(type(v) is str for v in args)153    elif family == 'can':154        valid = (command=='file' and len(args)==5155            or command=='ready' and len(args)==2 and _integer(args[0],1) and _integer(args[1],1)156            or command=='block' and len(args)==4 and _integer(args[0],1) and all(_time(v) for v in args[1:])157            or command=='sent' and len(args)==1 and _time(args[0]))158    elif family == 'fast':159        valid = (command in ('file','program_file') and len(args)==4 and _integer(args[0])160                    and type(args[1]) is dict and type(args[2]) is str and type(args[3]) is str161            or command in ('offer','delivered','stored') and len(args)==3 and all(_integer(v) for v in args)162            or command=='eof' and len(args)==5 and all(_integer(v) for v in args[:2])163                    and type(args[2]) is dict and type(args[3]) is dict and _integer(args[4],1))164    else:165        valid = (command in ('run','fast') and len(args)==3 and type(args[0]) is dict166                    and type(args[1]) is str and (args[2] is None or type(args[2]) is str)167            or command=='result' and len(args)==2 and type(args[1]) is bool168            or command=='fast_result' and len(args) in (2,3) and type(args[0]) is dict and type(args[1]) is bool169            or command=='cleaned' and len(args)==1 and args[0] is None170            or command=='cleanup_failed' and len(args)==1 and type(args[0]) in (list,tuple)171                    and all(type(v) is str for v in args[0]))172    if not valid:173        raise ControlProtocolError('控制命令参数结构不符:'+command)174175176def _short(value, maximum):177    # 只编码有限前缀,不为截短而遍历完整巨型错误文本。178    """按JSON转义后的字节数截短诊断。179180    参数: value文本,maximum为字节上限。181    返回: 短文本,发生截断时明确注明。182    异常: 输入不是文本时类型错误。183    """184    prefix = value[:maximum]185    encoded = json.dumps(prefix,ensure_ascii=True)186    if len(value)<=maximum and len(encoded)<=maximum:187        return value188    while len(encoded)>maximum-100 and prefix:189        prefix = prefix[:max(0,len(prefix)*3//4)]190        encoded = json.dumps(prefix,ensure_ascii=True)191    return prefix+'…[诊断已截断]'192193194def _diagnostics(family, command, args):195    """限制仅供展示的错误信息。196197    参数: family通道、command命令、args已展开参数。198    返回: 参数列表,只修改独立结果字典中的诊断。199    异常: 诊断字段类型非法。200    """201    if command=='error':202        return [_short(args[0],128),_short(args[1],4096)]203    if family=='batch' and command=='cleanup_failed':204        return [[_short(value,1024) for value in args[0][:16]]]205    if family=='batch' and command in ('result','fast_result'):206        args[0] = dict(args[0])207        for name,maximum in (('error_type',128),('error_message',4096)):208            if args[0].get(name) is not None:209                args[0][name] = _short(args[0][name],maximum)210        args[0]['error_chain'] = [_short(value,1024) for value in args[0].get('error_chain',())[:16]]211    return args212213214class ControlChannel:215    """单个管道端的有限控制编码;不拥有额外句柄,close关闭实际Connection。216217    parent表示父端,只允许向worker派发已定义命令;worker只能回报已定义事实。218    max_bytes是CAN/fast已有预算。独立batch任务及固定结果总长没有原有硬上限;219    每片仍至多64KiB,只随实际收到内容增长,不能把分片长度当作总RAM上限。220    """221    def __init__(self, connection, family, *, parent, max_bytes=None):222        """绑定一个已有控制端点。223224        参数: connection真实Connection,family通道,parent方向,max_bytes已有预算或batch的None。225        返回: 新编码端点,不新增句柄。226        异常: 通道、方向或额度错误。227        """228        if family not in _FAMILIES or type(parent) is not bool:229            raise ValueError('控制通道类型非法')230        if max_bytes is not None and not _integer(max_bytes,1):231            raise ValueError('控制接收额度非法')232        self._connection, self.family, self.parent = connection,family,parent233        self.maximum = max_bytes234        self.last_payload_bytes = 0235236    def _limit(self, operation, *, sending):237        """取得当前消息可用额度。238239        参数: operation固定命令,sending区分发送或接收。240        返回: 字节上限。241        异常: batch固定大消息沿用None;CAN/fast未提供预算时ControlProtocolError。242        """243        if operation not in _LARGE:244            return CHUNK245        if self.family != 'batch':246            if self.maximum is None:247                raise ControlProtocolError('CAN/fast控制通道缺少既有预算上限')248            return self.maximum249        # 路径任务的合法身份长度不能从短路径推断;不新增父端配置读取或任意阈值。250        # recv仅按实际片增长,不按对端声明的总长预约或预分配。251        return None252253    def _direction(self, operation, *, sending):254        """核发送者是否有权发此命令。255256        参数: operation命令,sending为本端发送与否。257        返回: None。258        异常: 父子方向不符时ControlProtocolError。259        """260        from_parent = self.parent if sending else not self.parent261        if (operation in _PARENT_COMMANDS[self.family]) != from_parent:262            raise ControlProtocolError('控制命令发送方向非法')263264    def send(self, message):265        """发送一条固定命令。266267        参数: message为现有控制tuple/list。268        返回: None,返回前释放编码副本。269        异常: 字段、额度或实际管道写入错误。270        """271        if type(message) not in (tuple,list) or not message or message[0] not in _COMMANDS[self.family]:272            raise ControlProtocolError('控制命令未知')273        operation = message[0]274        self._direction(operation,sending=True)275        _check(self.family,operation,message[1:])276        arguments = _diagnostics(self.family,operation,pack_message(self.family,message))277        check_message_fields(self.family,operation,arguments)278        payload = encode_json(arguments,self._limit(operation,sending=True))279        self.last_payload_bytes = len(payload)280        try:281            # 小消息只写一次管道,避免每个原控制通知变成两次系统调用。282            # 大消息首片含固定头,其后每片至多64KiB;不会缓存整批任务。283            first_size=min(len(payload),CHUNK-_HEADER.size)284            first=bytearray(_HEADER.pack(b'CCP1',_FAMILIES[self.family],285                _COMMANDS[self.family].index(operation),len(payload)))286            view = memoryview(payload)287            try:288                first.extend(view[:first_size])289                self._connection.send_bytes(first)290                first=None291                for offset in range(first_size,len(payload),CHUNK):292                    self._connection.send_bytes(view[offset:offset+CHUNK])293            finally:294                view.release()295        finally:296            payload = arguments = first = None297298    def recv(self):299        """接收一条固定消息。300301        参数: 无。302        返回: 现有消费者使用的tuple。303        异常: 版本、长度、字段、截断或管道错误。304        """305        first = self._connection.recv_bytes(CHUNK)306        if len(first)<_HEADER.size:307            raise ControlProtocolError('控制消息头截断')308        magic,family,code,size = _HEADER.unpack_from(first)309        if magic!=b'CCP1' or family!=_FAMILIES[self.family] or code>=len(_COMMANDS[self.family]):310            raise ControlProtocolError('控制消息版本/通道/命令未知')311        operation = _COMMANDS[self.family][code]312        self._direction(operation,sending=False)313        maximum = self._limit(operation,sending=False)314        if size==0 or maximum is not None and size>maximum:315            raise ControlProtocolError('控制声明长度超过本次接收额度')316        first_size=min(size,CHUNK-_HEADER.size)317        if len(first)!=_HEADER.size+first_size:318            raise ControlProtocolError('控制首片长度不符')319        payload = bytearray(memoryview(first)[_HEADER.size:])320        first=None321        try:322            while len(payload)<size:323                expected = min(CHUNK,size-len(payload))324                packet = self._connection.recv_bytes(expected)325                if len(packet)!=expected:326                    raise ControlProtocolError('控制分片截断或长度不符')327                payload.extend(packet)328            arguments = decode_json(payload)329            _check(self.family,operation,arguments)330            check_message_fields(self.family,operation,arguments)331            self.last_payload_bytes = size332            return restore_message(self.family,operation,arguments)333        finally:334            payload = None335336    def __enter__(self):337        """保持原管道上下文用法。338339        参数: 无。340        返回: 当前编码端点。341        异常: 无。342        """343        return self344345    def __exit__(self, *args):346        """按原管道上下文关闭。347348        参数: args为上下文异常三元组。349        返回: 原Connection.__exit__返回值。350        异常: 真实关闭错误。351        """352        return self._connection.__exit__(*args)353354    def poll(self, *args):355        """查询实际管道是否可读。356357        参数: args沿用Connection.poll的等待参数。358        返回: bool。359        异常: 真实管道错误。360        """361        return self._connection.poll(*args)362363    def fileno(self):364        """返回同一实际管道句柄。365366        参数: 无。367        返回: int,供multiprocessing.wait使用。368        异常: 已关闭等底层句柄错误。369        """370        return self._connection.fileno()371372    @property373    def closed(self):374        """报告实际管道关闭状态。375376        参数: 无。377        返回: bool。378        异常: 无。379        """380        return self._connection.closed381382    def close(self):383        """关闭本端实际管道。384385        参数: 无。386        返回: None。387        异常: 真实关闭错误。388        """389        return self._connection.close()390
Chronaxis 用户手册 · 离线阅读