Chronaxis用户手册English
本页目录

chronaxis.legacy.process_read

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在独立进程中逐次读取并解码BLF,确认退出后才交付完整EOF。23输入是冻结的单BLF请求和借用ResourceBudget,输出LegacyDecodedFrame。4构造不启动进程;start加载DBC和公开元数据。每次next只授权一个PULL,5不预取下一帧;子进程通过共用BLFRead读取,并按显式串行政策解码。6子进程在EOF、STOP或失败时关闭自己持有的读取器;父层关闭管道,等待7或终止自己启动的进程,确认退出后才释放对应进程责任。89固定状态、IPC最大包、元数据和当前记录分别计入新层保守RAM费用。10DBC、第三方读取器与解释器的整进程RSS未由本账本限制。11本层不写源文件、不导出BLF、不关闭借用账本或其他read。失败时保留原父12异常;清理仍失败时以can_process_read_cleanup_owner暴露同一真实owner。13跨进程只能保留远端异常事实,不能承诺远端异常对象与父对象相同。14"""1516# 进程和传输对象仅在start中建立,普通导入不加载CAN库或DBC。17from collections.abc import Mapping18import multiprocessing19from ..can_dependencies import runtime_dependency_versions20from sys import getsizeof21from types import MappingProxyType2223# 原样数据模型和有界JSON协议由worker与父进程共用,不接任意pickle对象。24from .records import (MAX_WIRE_BYTES, LegacyCANRequest, decode_packet,25                      decoded_from_dict, encode_packet, field_from_dict, field_to_dict)26from .worker import worker_main27from ..results.budget import ResourceBudget28from ..results.errors import AnalysisError, ResultClosedError29from ..results.spool import _cleanup_owner, _release_owned_ram, _reserve_owned_ram30from ..sources.protocol import SourceReadError313233def _wire_metadata_bytes(value):34    """估算已通过有界wire检查的JSON树,复制冻结前先预约保留费用。3536    参数: value为decode_packet得到的dict/list/原生标量。返回: 基础字节估算。37    异常: TypeError为内部未校验类型;不借用只接受冻结tuple的历史元数据估算器。38    """39    # wire的深度与总字节已校验;按原容器计数即可,不先复制成冻结树。40    if type(value) is dict:41        return getsizeof(value) + sum(128 + _wire_metadata_bytes(key)42            + _wire_metadata_bytes(item) for key, item in value.items())43    if type(value) is list:44        return getsizeof(value) + sum(32 + _wire_metadata_bytes(item) for item in value)45    if value is None or type(value) in (str, int, bool, float):46        return getsizeof(value) + 6447    raise TypeError('旧CAN元数据只接受已校验JSON树')484950def _freeze(value):51    """复制一份已经通过JSON边界检查的元数据,拒绝后续原地修改。5253    参数: value为有界协议JSON树。返回: 只读映射、tuple或原标量。54    异常: 原分配错误传播;本函数不读取文件或保留进程对象。55    """56    # 映射和数组逐层冻结,外部消费者不能修改下一次读取的合同。57    if isinstance(value, Mapping):58        return MappingProxyType({key: _freeze(item) for key, item in value.items()})59    if isinstance(value, (list, tuple)):60        return tuple(_freeze(item) for item in value)61    return value626364class RemoteLegacyError(RuntimeError):65    """保存远端错误的有限事实,明确区别于父进程中的原异常对象。"""6667    def __init__(self, facts, phase, request_id):68        """将有界错误包保存为供SourceReadError引用的cause。6970        参数: facts为错误字段映射;phase和request_id为原协议位置。71        返回: 新远端事实异常。异常: 原分配/格式错误,不执行清理。72        """73        # 不动态import远端异常类型,也不根据类型名执行任意代码。74        self.facts = _freeze(facts)75        self.phase, self.request_id = phase, request_id76        module = facts.get('module', 'unknown')77        error_type = facts.get('type', 'unknown')78        message = facts.get('message', '')79        super().__init__(f'旧CAN子进程{phase}失败: {module}.{error_type}: {message}')808182class LegacyCANProcessRead:83    """持有一次读取的真实进程、两端连接及各自费用,不与其他read共享。8485    _waiting表示已有一个发送尝试尚未取得完整回应,禁止叠加许可。86    _process_stopped只在实际确认退出后置真;关闭失败保留真实对象。87    finished仅表示真实协议EOF、正常退出和本owner关闭均成功。88    """8990    def __init__(self, request, budget, *, _worker_target=None):91        """保存请求并预约固定状态,不启动进程或访问DBC。9293        参数: request为LegacyCANRequest;budget为借用ResourceBudget;94            _worker_target仅为隔离协议测试的私有spawn目标,生产使用worker_main。95        返回: 尚未启动的独立read。异常: 配置/预算/分配错误;清理持续失败96            在原异常上提供can_process_read_cleanup_owner,不补共享余额差。97        """98        # 限定初始化包的结构规模,超出当前声明能力时明确拒绝。99        if not isinstance(request, LegacyCANRequest) or not isinstance(budget, ResourceBudget):100            raise TypeError('旧CAN读取需要LegacyCANRequest和ResourceBudget')101        if len(request.fields) > 256:102            raise ValueError('当前单CAN来源最多声明256个字段,且初始化包不能超过64KiB')103        # 第一次预算回调前建立全部真实责任槽,覆盖未返回构造的取消。104        self._budget, self._request = budget, request105        self._worker_target = worker_main if _worker_target is None else _worker_target106        self._connection = self._child_connection = self._process = None107        self._worker_pid = self._exit_code = None108        self._process_stopped = self._waiting = self._started = False109        self._closed = self._closing = self._failed = self._finished = False110        self._eof_received = False111        self._metadata = self._current = self._pending_reply = None112        self._fixed_cost = self._pipe_cost = self._process_cost = 0113        self._wire_cost = self._metadata_cost = self._current_cost = self._held_cost = 0114        self._pulls_issued = self._items_returned = self._items_accepted = 0115        try:116            # 有限初始化字段/路径引用和序列化临时结构均由这一固定预约覆盖。117            _reserve_owned_ram(self, 1048576, '_fixed_cost')118        except BaseException as exc:119            self._failed = True120            _cleanup_owner(self, exc, 'can_process_read_cleanup_owner', '旧CAN进程read构造')121            raise122123    @property124    def closed(self):125        """返回全部真实责任及费用是否已结束。126127        参数: 无。返回: bool。异常: 无,不触碰已关闭Process属性。128        """129        return self._closed130131    @property132    def finished(self):133        """返回本次真实协议EOF是否已完整交付。134135        参数: 无。返回: bool;提前close或失败永远不伪装EOF。异常: 无。136        """137        return self._finished138139    @property140    def worker_pid(self):141        """返回实际启动过的PID,进程对象关闭后仍可用于验证。142143        参数: 无。返回: int或None。异常: 无,不重新启动进程。144        """145        return self._worker_pid146147    @property148    def counters(self):149        """区分许可发送尝试、已接收ITEM和实际交付记录的有限计数。150151        参数: 无。返回: 独立dict;发送中取消时issued是已授予序号的上界,152            不等于已被子进程接收或完成。异常: 原小字典分配错误。153        """154        return {'pulls_issued': self._pulls_issued, 'items_returned': self._items_returned,155                'items_accepted': self._items_accepted}156157    @property158    def metadata(self):159        """读取已完成INIT的同次DBC公开元数据,不隐式启动。160161        参数: 无。返回: 冻结JSON映射,借用至close;长期保留须由消费者计费。162        异常: ResultClosedError为关闭中/已关闭,RuntimeError为尚未READY。163        """164        self._require_open()165        if self._metadata is None:166            raise RuntimeError('start完成前没有旧CAN运行元数据')167        return self._metadata168169    def _require_open(self):170        """阻止失败或半关闭对象再次授权读取。171172        参数: 无。返回: None。异常: ResultClosedError为已关/正在关;173            SourceReadError为失败后继续推进;close仍可重试。174        """175        if self._closed or self._closing:176            raise ResultClosedError('旧CAN进程read已关闭或正在关闭')177        if self._failed:178            raise SourceReadError('失败的旧CAN进程read只允许关闭')179180    def _fail(self, exc, stage):181        """以原父错误为主关闭实际owner,必要时包装来源位置。182183        参数: exc为原错误;stage为有限业务位置。返回: 不返回。184        异常: 父KI/SystemExit、预算及来源错误原对象传播;其他错误作为185            SourceReadError的cause,清理副错只附note/真实恢复owner。186        """187        self._failed = True188        # 先确定对外原错,再把清理证据附在调用者真正会接收到的异常上。189        if isinstance(exc, (KeyboardInterrupt, SystemExit, AnalysisError, SourceReadError)):190            failure = exc191        else:192            failure = SourceReadError(f'旧CAN进程{stage}失败: {exc}')193            failure.__cause__ = exc194        _cleanup_owner(self, failure, 'can_process_read_cleanup_owner', '旧CAN进程read')195        if failure is exc:196            raise failure197        raise failure from exc198199    def start(self):200        """实际启动隔离进程并取得READY,不授权任何消息读取。201202        参数: 无。返回: self,重复成功调用不重启/重载DBC。203        异常: 启动、协议、远端或预算错误;父取消保原对象和关闭责任。204        """205        self._require_open()206        if self._started:207            return self208        try:209            # 两进程的有界wire和解码临时容器预先计费;DBC及整进程RSS另列限制。210            _reserve_owned_ram(self, 128 * MAX_WIRE_BYTES, '_wire_cost')211            _reserve_owned_ram(self, 65536, '_pipe_cost')212            context = multiprocessing.get_context('spawn')213            self._connection, self._child_connection = context.Pipe(duplex=True)214            _reserve_owned_ram(self, 65536, '_process_cost')215            self._process = context.Process(target=self._worker_target,216                args=(self._child_connection,), name='chronaxis-legacy-can')217            try:218                # Process先存真实槽;start成功后取消仍能从同owner回收PID。219                self._process.start()220            finally:221                self._worker_pid = self._process.pid222            self._close_connection('_child_connection')223            packet = {'op': 'init', 'version': 1, 'path': str(self._request.input_path),224                      'dbc_path': str(self._request.dbc_path),225                      'fields': [field_to_dict(field) for field in self._request.fields]}226            ready = self._exchange(packet)227            self._check_ready(ready)228            # 复制冻结元数据前预约实际保留量,wire预约仍覆盖旧解码包。229            _reserve_owned_ram(self, 4096 + 16 * _wire_metadata_bytes(ready), '_metadata_cost')230            self._metadata = _freeze(ready)231            self._pending_reply = None232            self._started = True233            return self234        except BaseException as exc:235            self._fail(exc, '初始化')236237    def _check_ready(self, packet):238        """核INIT回应来自同一真实PID和同一字段请求,不猜元数据。239240        参数: packet为有界READY dict。返回: None。241        异常: SourceReadError为profile/版本/PID/字段位置不符。242        """243        if (set(packet) != {'op', 'version', 'profile', 'worker_pid', 'fields',244                           'dependency_versions'}245                or packet.get('op') != 'ready' or type(packet.get('version')) is not int or packet.get('version') != 1246                or packet.get('profile') != self._request.profile247                or type(packet.get('worker_pid')) is not int248                or packet.get('worker_pid') != self._worker_pid):249            raise SourceReadError('旧CAN READY身份、版本或实际PID不符')250        self._check_ready_metadata(packet)251252    def _check_ready_metadata(self, packet, *, cycle_fields=(), metadata_fields=()):253        """核两种READY共有的字段定位、单位、依赖,不改协议头。254255        参数: packet已由对应读取入口核过完整键集、版本、profile和真实PID;256            cycle_fields仅由确认了周期请求的新版批读取器传入,默认不允许cycle_ns;metadata_fields仅增加有限候选声明。257        返回: None。异常: 原SourceReadError;检查顺序和错误文本沿用逐条入口。258        默认v1和显式按需v2分别检查各自头部,不能伪造v1包绕过策略核对。259        """260        expected_fields = tuple(dict.fromkeys((*self._request.fields,*cycle_fields,*metadata_fields)))261        fields = packet.get('fields')262        if not isinstance(fields, list) or len(fields) != len(expected_fields):263            raise SourceReadError('旧CAN READY字段数量不符')264        for actual, expected in zip(fields, expected_fields):265            if (not isinstance(actual, dict)266                    or set(actual) - ({'cycle_ns'} if expected in cycle_fields else set()) - {'dbc_frames'} not in267                       ({'locator', 'unit'}, {'locator', 'unit', 'frame_format_compatibility'})268                    or actual['unit'] is not None and type(actual['unit']) is not str):269                raise SourceReadError('旧CAN READY没有对应原请求的字段定位')270            if expected in cycle_fields and (type(actual.get('cycle_ns')) is not int271                    or not 0 < actual['cycle_ns'] <= (1 << 63)-1):272                raise SourceReadError('DBC周期元数据缺失或不是正int64纳秒')273            # dict相等会把False与0混同;先按原生int/bool合同重建,再比较定位。274            try:275                locator = field_from_dict(actual['locator'])276            except (TypeError, ValueError) as exc:277                raise SourceReadError('旧CAN READY字段定位类型非法') from exc278            if locator != expected:279                raise SourceReadError('旧CAN READY没有对应原请求的字段定位')280            needs_candidates = locator.is_extended is None or locator.is_fd is None281            if ('dbc_frames' in actual) != needs_candidates:282                raise SourceReadError('旧CAN READY候选帧型与省略参数声明不符')283            if 'dbc_frames' in actual:284                pairs = actual['dbc_frames']285                if (type(pairs) is not list or not 1 <= len(pairs) <= 4286                        or any(type(pair) is not list or len(pair) != 2287                               or any(type(flag) is not bool for flag in pair) for pair in pairs)):288                    raise SourceReadError('旧CAN READY候选帧型非法')289                if len({tuple(pair) for pair in pairs}) != len(pairs) or any(290                        locator.is_extended is not None and pair[0] != locator.is_extended291                        or locator.is_fd is True and pair[1] is not True for pair in pairs):292                    raise SourceReadError('旧CAN READY候选帧型与显式参数冲突或重复')293            if 'frame_format_compatibility' in actual:294                # 只承认固定单向提示,不将0/1当bool,也不接受其他兼容或额外字段。295                notice = actual['frame_format_compatibility']296                if (type(notice) is not dict297                        or set(notice) != {'code', 'dbc_is_fd', 'requested_is_fd'}298                        or type(notice['code']) is not str or notice['code'] != 'dbc_fd_classic_selector'299                        or notice['dbc_is_fd'] is not True or notice['requested_is_fd'] is not False300                        or locator.is_fd is not False):301                    raise SourceReadError('旧CAN READY帧型兼容提示非法或与原定位不同')302        # 安装范围由包元数据约束;父子必须使用同一解释器与实际依赖版本。303        expected_versions = runtime_dependency_versions()304        if packet['dependency_versions'] != expected_versions:305            raise SourceReadError('CAN READY依赖版本与父进程不一致')306307    def _exchange(self, packet):308        """完成一个有界请求/回应;STATUS/ACK也不隐含PULL许可。309310        参数: packet为协议dict。返回: 有界回应dict。311        异常: 原父取消/管道错误、SourceReadError或明确远端cause。312        边界: 最多一项在途,父每50ms回到Python检查取消;本方法发送后立即等待。313        """314        try:315            self._send_request(packet)316            return self._receive_response(packet)317        finally:318            packet = None319320    def _send_request(self, packet):321        """发送唯一在途许可;发送中断时仍保留实际连接和未确认责任。322323        参数: packet为调用方已经为相应工作空间付款的协议dict。324        返回: None。异常: 原编码/发送/取消错误;失败不可按未发送重试。325        边界: 原逐条入口立即接收;批次入口可在消费当前批时保留这一项在途请求。326        """327        payload = None328        try:329            if self._connection is None or self._waiting:330                raise SourceReadError('旧CAN连接不可用或已有一个在途请求')331            payload = encode_packet(packet)332            self._waiting = True333            self._connection.send_bytes(payload)334        finally:335            packet = payload = None336337    def _receive_response(self, packet):338        """接收先前发送的许可,并严格核对远端错误事实与请求身份。339340        参数: packet为同一在途许可,不再次发送。返回: 有界回应dict。341        异常: 原连接/格式/取消错误,或带RemoteLegacyError cause的来源错误。342        边界: 必须已有一个在途请求;没有真实协议EOF的退出仍是错误。343        """344        reply = facts = remote = None345        try:346            if self._connection is None or not self._waiting:347                raise SourceReadError('旧CAN连接没有对应的在途请求')348            # poll不授予读取许可;子进程意外退出也不能当成真实EOF。349            while not self._connection.poll(0.05):350                if self._process is not None and not self._process.is_alive():351                    raise SourceReadError(f'旧CAN进程未交付完整协议回应便退出: {self._process.exitcode}')352            reply = self._receive_reply()353            self._waiting = False354            if reply.get('op') == 'error':355                facts = reply.get('error')356                if (set(reply) != {'op', 'request_id', 'phase', 'error'}357                        or type(reply.get('phase')) is not str358                        or reply.get('request_id') is not None and type(reply.get('request_id')) is not int359                        or not isinstance(facts, dict)360                        or set(facts) != {'module', 'type', 'message', 'traceback', 'notes'}361                        or any(type(facts[key]) is not str for key in ('module', 'type', 'message', 'traceback'))362                        or type(facts['notes']) is not list363                        or any(type(note) is not str for note in facts['notes'])):364                    raise SourceReadError('旧CAN远端错误包缺少明确事实')365                if reply['request_id'] != packet.get('request_id'):366                    raise SourceReadError('旧CAN远端错误不属于当前请求')367                remote = RemoteLegacyError(facts, reply.get('phase'), reply.get('request_id'))368                raise SourceReadError(str(remote)) from remote369            return reply370        finally:371            # 原错误可留在调用方;已结束调用的临时引用不继续占有整批对象。372            packet = reply = facts = remote = None373374    def _receive_reply(self):375        """读取本协议的一条严格JSON回应,供批次子类替换物理数据包边界。376377        参数: 无,使用本owner的实际Connection。378        返回: 已校验的回应dict。异常: 原管道、长度、JSON或取消错误。379        边界: 原逐条入口仍只接收64KiB;子类不能绕过共享接收流程的在途及远端错误检查。380        """381        self._pending_reply = self._connection.recv_bytes(MAX_WIRE_BYTES)382        return decode_packet(self._pending_reply)383384    def __iter__(self):385        """返回本次独立read,不因取得迭代器就启动或读取。386387        参数: 无。返回: self。异常: 原错误只在实际next/start时报告。388        """389        return self390391    def __next__(self):392        """只授权并返回一帧,真实完整EOF之后才StopIteration。393394        参数: 无。返回: LegacyDecodedFrame;调用者保留引用须另计费。395        异常: StopIteration为已确认EOF;源/预算/协议错误或父原取消。396        """397        if self._finished:398            raise StopIteration399        self._require_open()400        try:401            self.start()402            # 上一记录停止由本read持有后才归还其费用,失败不会继续发PULL。403            self._current = None404            _release_owned_ram(self, self._current_cost, '_current_cost')405            self._pending_reply = None406            request_id = self._items_accepted407            self._pulls_issued += 1408            reply = self._exchange({'op': 'pull', 'request_id': request_id})409            if type(reply.get('request_id')) is not int or reply.get('request_id') != request_id:410                raise SourceReadError('旧CAN回应不是本次唯一许可序号')411            if reply.get('op') == 'eof':412                if (set(reply) != {'op', 'request_id', 'frame_count'}413                        or type(reply.get('frame_count')) is not int414                        or reply.get('frame_count') != self._items_accepted):415                    raise SourceReadError('旧CAN EOF原帧计数不符')416                self._eof_received = True417                self.close()418                if self._exit_code != 0:419                    raise SourceReadError('旧CAN虽然发EOF,但没有正常退出')420                self._finished = True421            elif reply.get('op') == 'item':422                if set(reply) != {'op', 'request_id', 'record'}:423                    raise SourceReadError('旧CAN ITEM字段缺失或多余')424                self._items_returned += 1425                raw = reply.get('record')426                _reserve_owned_ram(self, 4096 + 16 * _wire_metadata_bytes(raw), '_current_cost')427                self._current = decoded_from_dict(raw)428                if self._current.sequence != request_id:429                    raise SourceReadError('旧CAN原yield序号不连续,禁止重排或补号')430                self._items_accepted += 1431                self._pending_reply = None432                return self._current433            else:434                raise SourceReadError('旧CAN PULL必须得到ITEM或真实EOF')435        except BaseException as exc:436            self._fail(exc, '逐帧读取')437        # 正常StopIteration不进入失败清理,已关闭read也能重复返回EOF。438        raise StopIteration439440    def _close_connection(self, slot):441        """关闭一个真实自有连接,成功后取消也不丢失其实际关闭事实。442443        参数: slot为本owner连接字段。返回: None。444        异常: 原close错误;未关闭对象仍在同槽,费用稍后单独归还。445        """446        connection = getattr(self, slot)447        if connection is None:448            return449        try:450            connection.close()451        finally:452            if connection.closed:453                setattr(self, slot, None)454455    def _close_process(self):456        """确认自己启动的进程退出后关闭其实际句柄,不动其他read。457458        参数: 无。返回: None,实际exitcode保留供EOF判断。459        异常: join/terminate/kill/close错误保原process和停止确认状态以便重试。460        边界: 三次显式join最多约6秒;Windows kill仍是TerminateProcess,461            必须实际取得exitcode,不能把终止请求成功当作进程已经退出。462        """463        process = self._process464        if process is None:465            return466        if not self._process_stopped:467            # start前失败没有PID,仍须关闭Process自身已经建立的描述。468            if process.pid is not None:469                process.join(timeout=0.5)470                if process.exitcode is None:471                    process.terminate()472                    process.join(timeout=0.5)473                if process.exitcode is None:474                    process.kill()475                    # Windows终止后的OS清理可能晚于0.5秒;有限等待真实退出信号。476                    process.join(timeout=5.0)477                exit_code = process.exitcode478                if exit_code is None:479                    failure = SourceReadError('尚未确认旧CAN子进程退出,不能结束关闭责任')480                    failure.can_process_exit_state = {'pid': self._worker_pid,481                        'exit_code': None, 'join_wait_limits_seconds': (0.5, 0.5, 5.0)}482                    raise failure483                self._exit_code = exit_code484            self._process_stopped = True485        # 此确认先于close,覆盖真实close后取消时已关闭Process属性不可再读的情况。486        process.close()487        self._process = None488489    def _release_closed_state(self):490        """实际连接和进程全部关闭后清状态、逐槽退费,供批次子类补充责任。491492        参数: 无;调用方必须已完成本owner实际资源关闭。返回: None。493        异常: 原预算回调错误;费用槽保留实际入账事实,同一close可继续恢复。494        """495        self._metadata = self._current = self._pending_reply = self._request = None496        self._worker_target = None497        self._waiting = False498        for slot in ('_current_cost', '_metadata_cost', '_wire_cost', '_pipe_cost', '_process_cost', '_fixed_cost'):499            _release_owned_ram(self, getattr(self, slot), slot)500501    def close(self):502        """停止授予许可,关闭自有IPC/进程,最后归还各自费用。503504        参数: 无。返回: None,完成后重复调用幂等。505        异常: 实际关闭或预算回调错误;对象仍可同owner重试。506        边界: 关管道令等待命令的child退出;在途旧IO不响应时有限等待后终止,507            不为关闭继续next,也不把强制退出当正常分析完成。508        """509        if self._closed:510            return511        self._closing = True512        try:513            # 任一真实资源未关时即保留父责任,不先释放其内存描述或借用余额。514            self._close_connection('_child_connection')515            self._close_connection('_connection')516            self._close_process()517            # 每笔实际申请有独立槽,释放后取消由既有helper确认,不重复抵销别人的费用。518            self._release_closed_state()519            self._closed = True520        except BaseException as exc:521            # 直接close的调用者也能从同一异常找到真实责任,不制造替代owner。522            exc.can_process_read_cleanup_owner = self523            raise524
Chronaxis 用户手册 · 离线阅读