Chronaxis用户手册English
本页目录

chronaxis.legacy.export_write

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在独立进程把原始帧交给共用BLF写入器,真实收尾后才确认完成。23LegacyCANProcessWrite接收共享ResourceBudget和新输出路径,构造只计有限状态、4核路径而不spawn或打开输出。start加载依赖并由共用BLFWrite取得文件和写入器;每次5write只发一个有界PUT,收到同序ACK才承认该帧,FINISH没有额外帧写入许可。6finish须BLFWrite完成收尾、实际exitcode0、所有Pipe/Process及自有RAM清完,并核输出7stat不超过本writer直接预留,才公开finished。提前close、失败或取消不能伪完成。89进程/管道/wire/READY/当前帧分别先付并记真实槽;释放前后取消可重试,最高10can_process_write_cleanup_owner始终指向同一实际owner。磁盘在start前保守预留1165536 bytes、每PUT前4096 bytes,累计保留且不倒扣;不凭共享余额差接管费用。12原BLF压缩、库内部缓存及整进程RSS未测,也不承诺文件与源BLF字节一致。13这里只写控制器新工作根下的新文件,不读取来源CAN或DBC。14"""1516# 仅导入标准库和既有轻量协议;CAN依赖留在独立spawn子内。17import multiprocessing18from pathlib import Path19from .export_worker import worker_main20from ..can_dependencies import runtime_dependency_versions21from .process_read import RemoteLegacyError, _freeze, _wire_metadata_bytes22from .records import MAX_WIRE_BYTES, PROFILE_ID, LegacyRawFrame, decode_packet, encode_packet, frame_to_dict23from ..results.budget import ResourceBudget24from ..results.errors import AnalysisError, ResultClosedError25from ..results.spool import _cleanup_owner, _release_owned_ram, _reserve_owned_ram262728class LegacyCANWriteError(AnalysisError):29    """报告新外部writer协议或原子进程失败;远端原错仅作为明确事实cause。"""303132class LegacyCANProcessWrite:33    """拥有一个真实writer子进程与独立费用,借用账本但不关闭其他owner。3435    _waiting阻止第二个在途控制包;_process_stopped只由实际exitcode确认。36    _closed表示自有责任结束,_finished还须本次FINISH及输出大小验证成功。37    输出路径和累计spool事实在close后保留,READY借引用在close时解除。38    """3940    def __init__(self, budget, output_path):41        """预约固定协调状态并检查新路径,不启动进程或创建目标文件。4243        参数: budget为借用ResourceBudget;output_path为控制器已有新工作根下的44            str/Path目标,须为.blf后缀,不能已存在。45        返回: 未启动的独立writer。异常: 类型、路径或预算原错;未返回构造的持续46            清理失败附can_process_write_cleanup_owner,同对象恢复后close。47        """48        if not isinstance(budget, ResourceBudget) or not isinstance(output_path, (str, Path)):49            raise TypeError('原CAN写出需要ResourceBudget和新输出路径')50        # 所有真实资源和交易槽先安装;后续任何预算取消都有本owner可达责任。51        self._budget, self._output_path = budget, None52        self._connection = self._child_connection = self._process = None53        self._worker_pid = self._exit_code = self._metadata = self._pending_reply = self._frame = None54        self._fixed_cost = self._wire_cost = self._pipe_cost = self._process_cost = 055        self._metadata_cost = self._frame_cost = self._held_cost = self._spool_bytes = 056        self._puts_issued = self._acks_received = self._frames_written = 057        self._actual_bytes = None58        self._process_stopped = self._waiting = self._started = self._finish_received = False59        self._closed = self._closing = self._failed = self._finished = False60        try:61            _reserve_owned_ram(self, 1048576, '_fixed_cost')62            if len(str(output_path)) > 8192:63                raise ValueError('原CAN输出路径超过本层有限能力')64            self._output_path = Path(output_path).resolve()65            self._check_path()66        except BaseException as exc:67            self._failed = True68            _cleanup_owner(self, exc, 'can_process_write_cleanup_owner', '原CAN writer构造')69            raise7071    def _check_path(self):72        """拒绝已有目标及缺少的新工作根,不创建目录或文件。7374        参数: 无,使用已保存resolved路径。返回: None。75        异常: ValueError为后缀错误,FileExistsError为已有目标,76            FileNotFoundError为父目录不存在;真正独占创建由child执行。77        """78        path = self._output_path79        if path.suffix.lower() != '.blf':80            raise ValueError('原CAN写出仅支持新的BLF文件')81        if path.exists():82            raise FileExistsError(path)83        if not path.parent.is_dir():84            raise FileNotFoundError(path.parent)8586    @property87    def output_path(self):88        """读取实际新目标。参数: 无。返回: resolved Path或构造前失败时None。异常: 无。"""89        return self._output_path9091    @property92    def closed(self):93        """读取全部自有责任关闭状态。参数: 无。返回: bool。异常: 无。"""94        return self._closed9596    @property97    def finished(self):98        """读取完整FINISH事实。参数: 无。返回: bool,失败/提前close为False。异常: 无。"""99        return self._finished100101    @property102    def failed(self):103        """读取失败诊断状态。参数: 无。返回: bool。异常: 无。"""104        return self._failed105106    @property107    def worker_pid(self):108        """读取本次真实启动PID。参数: 无。返回: int或None,关闭后保留。异常: 无。"""109        return self._worker_pid110111    @property112    def spool_bytes(self):113        """读取本writer直接累计磁盘预留。参数: 无。返回: int bytes,不倒扣。异常: 无。"""114        return self._spool_bytes115116    @property117    def counters(self):118        """区分PUT尝试、合法ACK和本父确认帧数,不用断管推断写入成功。119120        参数: 无。返回: 新dict,取消时puts_issued仅为许可尝试上界。121        异常: 原小容器分配错误;不向child发送额外控制或写入。122        """123        return {'puts_issued': self._puts_issued, 'acks_received': self._acks_received,124            'frames_written': self._frames_written}125126    @property127    def metadata(self):128        """借读同次READY事实,不隐式start。参数: 无。返回: 冻结映射,借用至close。129        异常: ResultClosedError为关闭/失败,RuntimeError为尚无实际READY。130        """131        self._require_open()132        if self._metadata is None:133            raise RuntimeError('原CAN writer尚未READY')134        return self._metadata135136    def _require_open(self):137        """阻止关闭或失败后继续许可。参数: 无。返回: None。138        异常: ResultClosedError为不可推进;close仍可重复恢复。139        """140        if self._closed or self._closing or self._failed:141            raise ResultClosedError('原CAN writer已关闭、清理中或失败')142143    def _reserve_spool(self, amount):144        """逐笔记录本次直接磁盘预留,回调执行后取消也保留真实归属。145146        参数: amount为非负int bytes。返回: None。异常: 原配额/取消,文件及累计值不倒扣。147        """148        before = self._budget.spool_bytes149        try:150            self._budget.reserve_spool(amount)151        finally:152            if self._budget.spool_bytes == before + amount:153                self._spool_bytes += amount154155    def _fail(self, exc, phase):156        """以实际父错误为主收尾,普通底层错误包装并保cause及最高恢复owner。157158        参数: exc为原异常;phase为有限业务位置。返回: 不返回。159        异常: KI/SystemExit/AnalysisError原对象,其余LegacyCANWriteError保原cause;160            两次有限cleanup副错只附note,绝不伪称写出完成。161        """162        self._failed = True163        failure = exc if isinstance(exc, (KeyboardInterrupt, SystemExit, AnalysisError)) else LegacyCANWriteError(164            '原CAN writer' + phase + '失败')165        _cleanup_owner(self, failure, 'can_process_write_cleanup_owner', '原CAN writer')166        if failure is exc:167            raise failure168        raise failure from exc169170    def start(self):171        """实际创建独立spawn及原writer,READY不包含任何帧许可。172173        参数: 无。返回: self,重复成功调用不重建或重复预留。174        异常: 路径/依赖/管道/协议/配额/预算或父原取消,最高owner按实际槽可恢复。175        """176        self._require_open()177        if self._started:178            return self179        try:180            self._check_path()181            # 磁盘容量在spawn/原build_writer前预留,空BLF也保65536而非事后认领。182            self._reserve_spool(65536)183            _reserve_owned_ram(self, 128 * MAX_WIRE_BYTES, '_wire_cost')184            _reserve_owned_ram(self, 65536, '_pipe_cost')185            context = multiprocessing.get_context('spawn')186            self._connection, self._child_connection = context.Pipe(duplex=True)187            _reserve_owned_ram(self, 65536, '_process_cost')188            self._process = context.Process(target=worker_main, args=(self._child_connection,),189                name='chronaxis-legacy-can-export')190            try:191                self._process.start()192            finally:193                self._worker_pid = self._process.pid194            self._close_connection('_child_connection')195            reply = self._exchange({'op': 'init', 'version': 1, 'profile': PROFILE_ID,196                'path': str(self._output_path)})197            self._check_ready(reply)198            _reserve_owned_ram(self, 4096 + 16 * _wire_metadata_bytes(reply), '_metadata_cost')199            self._metadata = _freeze(reply)200            self._pending_reply = None201            self._started = True202            return self203        except BaseException as exc:204            self._fail(exc, '初始化')205206    def _check_ready(self, packet):207        """核同次实际PID及现有依赖,不接受bool伪版本或自称READY。208209        参数: packet为decode_packet得到的dict。返回: None。210        异常: LegacyCANWriteError为结构/类型/profile/PID/版本/摘要不符。211        """212        if (set(packet) != {'op', 'version', 'profile', 'worker_pid', 'dependency_versions'}213                or packet.get('op') != 'ready' or type(packet.get('version')) is not int or packet['version'] != 1214                or type(packet.get('profile')) is not str or packet['profile'] != PROFILE_ID215                or type(packet.get('worker_pid')) is not int or packet['worker_pid'] != self._worker_pid216                or packet['dependency_versions'] != runtime_dependency_versions()):217            raise LegacyCANWriteError('原CAN writer READY身份、版本或实际PID不符')218219    def _exchange(self, packet):220        """同步交换唯一控制包,禁止第二个在途PUT或把子退出当成ACK。221222        参数: packet为有界控制dict。返回: 严格JSON dict。223        异常: 父原取消/管道/编码错误或LegacyCANWriteError;远端失败以明确224            RemoteLegacyError事实为cause,不能声称跨进程保留异常对象身份。225        """226        if self._connection is None or self._waiting:227            raise LegacyCANWriteError('原CAN writer连接不可用或已有在途许可')228        payload = encode_packet(packet)229        self._waiting = True230        self._connection.send_bytes(payload)231        while not self._connection.poll(0.05):232            if self._process is not None and self._process.exitcode is not None:233                raise LegacyCANWriteError('原CAN writer未交付完整回应便退出')234        self._pending_reply = self._connection.recv_bytes(MAX_WIRE_BYTES)235        reply = decode_packet(self._pending_reply)236        self._waiting = False237        if reply.get('op') == 'error':238            facts = reply.get('error')239            if (set(reply) != {'op', 'request_id', 'phase', 'error'}240                    or type(reply.get('phase')) is not str241                    or reply.get('request_id') is not None and type(reply.get('request_id')) is not int242                    or reply.get('request_id') != packet.get('request_id')243                    or type(facts) is not dict or set(facts) != {'module', 'type', 'message', 'traceback', 'notes'}244                    or any(type(facts[key]) is not str for key in ('module', 'type', 'message', 'traceback'))245                    or type(facts['notes']) is not list or any(type(note) is not str for note in facts['notes'])):246                raise LegacyCANWriteError('原CAN writer远端错误包缺少明确事实或请求不符')247            remote = RemoteLegacyError(facts, reply['phase'], reply['request_id'])248            raise LegacyCANWriteError('原CAN writer子进程失败') from remote249        return reply250251    def write(self, frame):252        """完整发送一个原始帧并等唯一ACK,不缓存后继或按ID筛帧。253254        参数: frame为原生LegacyRawFrame,保全部公开字段及原浮点timestamp。255        返回: None,合法ACK后counters增加;无额外on_message_received。256        异常: TypeError为非原模型;原预算/磁盘/取消或writer/协议失败,之后只能close。257        """258        self._require_open()259        if type(frame) is not LegacyRawFrame:260            raise TypeError('原CAN writer.write需要LegacyRawFrame')261        if frame.is_rx is None:262            raise ValueError('BLF写出需要收发方向is_rx')263        try:264            self.start()265            # 当前记录、hex和wire容器均在复制前付款;只有本owner持有时才保留该槽。266            _reserve_owned_ram(self, 4259840, '_frame_cost')267            self._frame = frame268            self._reserve_spool(4096)269            request_id = self._frames_written270            self._puts_issued += 1271            reply = self._exchange({'op': 'put', 'request_id': request_id, 'frame': frame_to_dict(frame)})272            if (set(reply) != {'op', 'request_id', 'frame_count'} or reply.get('op') != 'ack'273                    or type(reply.get('request_id')) is not int or reply['request_id'] != request_id274                    or type(reply.get('frame_count')) is not int or reply['frame_count'] != request_id + 1):275                raise LegacyCANWriteError('原CAN writer ACK不属于本次唯一PUT')276            self._acks_received += 1277            self._frames_written += 1278            self._frame = self._pending_reply = None279            _release_owned_ram(self, self._frame_cost, '_frame_cost')280        except BaseException as exc:281            self._fail(exc, '逐帧写出')282283    def finish(self):284        """完成原stop、真实正常退出及全部费用,再核实际文件未超过预留。285286        参数: 无。返回: self,成功后重复调用不重发FINISH或重写文件。287        异常: 原取消/预算/关闭错误,或LegacyCANWriteError为不完整回应、异常退出288            或实际stat超本writer累计预留;任何失败都不置finished。289        """290        if self._finished:291            return self292        self._require_open()293        try:294            self.start()295            reply = self._exchange({'op': 'finish', 'frame_count': self._frames_written})296            if (set(reply) != {'op', 'frame_count'} or reply.get('op') != 'finished'297                    or type(reply.get('frame_count')) is not int or reply['frame_count'] != self._frames_written):298                raise LegacyCANWriteError('原CAN writer FINISH未确认全部原帧')299            self._finish_received = True300            self.close()301            if self._exit_code != 0:302                raise LegacyCANWriteError('原CAN writer虽发完成候选但未正常退出')303            self._actual_bytes = self._output_path.stat().st_size304            if self._actual_bytes > self._spool_bytes:305                raise LegacyCANWriteError('原BLF实际大小超过本writer保守磁盘预留')306            self._finished = True307            return self308        except BaseException as exc:309            self._fail(exc, '封口')310311    def _close_connection(self, slot):312        """实际关闭端点后才撤槽。参数: slot为本owner自有端点名。返回: None。313        异常: 原close/取消;执行后异常也按真实closed确认,不丢未关句柄。314        """315        connection = getattr(self, slot)316        if connection is not None:317            try:318                connection.close()319            finally:320                if connection.closed:321                    setattr(self, slot, None)322323    def _close_process(self):324        """有限等待自己实际进程退出再关句柄,绝不以终止请求代替退出证明。325326        参数: 无。返回: None。异常: 原join/terminate/kill/close或LegacyCANWriteError;327            未确认时保原进程/费用并附稳定退出诊断。显式join上限为.5/.5/5秒,328            Windows kill仍是TerminateProcess,必须实际exitcode非None。329        """330        process = self._process331        if process is None:332            return333        if not self._process_stopped:334            if process.pid is not None:335                process.join(timeout=.5)336                if process.exitcode is None:337                    process.terminate()338                    process.join(timeout=.5)339                if process.exitcode is None:340                    process.kill()341                    process.join(timeout=5.0)342                code = process.exitcode343                if code is None:344                    failure = LegacyCANWriteError('尚未确认原CAN writer子进程退出')345                    failure.can_process_write_exit_state = {'pid': self._worker_pid, 'exit_code': None,346                        'join_wait_limits_seconds': (.5, .5, 5.0)}347                    raise failure348                self._exit_code = code349            self._process_stopped = True350        # stopped先存,再实际close;close之后取消也不会再次读取已关闭Process属性。351        process.close()352        self._process = None353354    def close(self):355        """只关本writer实际IPC/进程和费用,产物与累计磁盘预留保留。356357        参数: 无。返回: None,完成后重复调用幂等。358        异常: 原关闭/预算/取消附can_process_write_cleanup_owner,当前槽供持续恢复;359            提前关闭不发送额外帧或FINISH,不设置finished,不修改借用预算的其他费用。360        """361        if self._closed:362            return363        self._closing = True364        try:365            self._close_connection('_child_connection')366            self._close_connection('_connection')367            self._close_process()368            self._metadata = self._frame = self._pending_reply = None369            self._waiting = False370            for slot in ('_frame_cost', '_metadata_cost', '_wire_cost', '_pipe_cost', '_process_cost', '_fixed_cost'):371                _release_owned_ram(self, getattr(self, slot), slot)372            self._closed = True373        except BaseException as exc:374            self._failed = True375            exc.can_process_write_cleanup_owner = self376            raise377
Chronaxis 用户手册 · 离线阅读