转:Linux sys_exec中可执行文件映射的建立及读取

本文详细解析了在Linux环境下如何通过ELF映射创建vm_area_struct,并介绍了当程序试图访问未映射到物理内存的虚拟地址时,系统如何通过页故障机制动态分配物理页面并从磁盘加载内容。

来源:

http://bbs.chinaunix.net/thread-2058683-1-1.html


  发表于 2008-05-06 15:38:38  | 只看该作者  | 倒序浏览


   1. 创建一个vm_area_struct;
   2. 圈定一个虚用户空间,将其起始结束地址(elf段中已设置好)保存到vm_start和vm_end中;
   3. 将磁盘file句柄保存在vm_file中;
   4. 将对应段在磁盘file中的偏移值(elf段中已设置好)保存在vm_pgoff中;
   5. 将操作该磁盘file的磁盘操作函数保存在vm_ops中;
   6. 注意这里没有为对应的页目录表项创建页表,更不存在设置页表项了;

                          §                               § 
                          §                        +------§->+--------------+
                          §                        |      §  |  Disk file   |
                          §                        |      §  |              |
                          §    +----------------+  |  +---§->|--------------|
                          §    | vm_area_struct |  |  |   §  | Seg Content  |
                          §    |----------------|  |  |   §  |--------------|
      +----------------+<-§-------- vm_start    |  |  |   §  |              |
      | 圈定了一个未映  |  §  +----- vm_end      |  |  |   §  |              |
      | 射到物理内存的  |  §  | |    vm_file--------+  |   §  +--------------+
      | vm_area_struct |  §  | |    vm_pgoff ---------+   § 
      +----------------+<-§--+ |    vm_ops --------+      § 
                          §    |                |  |      § 
                          §    +----------------+  |      § 
                          §                        |      §
                          § +----------------------+      §
                          § |                             §
                          § +->+-----------------------+  §
                          §    |   file_private_map    |  §
                          §    |-----------------------|  §
                          §    | nopage:filemap_nopage |  §
                          §    |        .....          |  §
                          §    +-----------------------+  §
            user space    §           kernel              §     disk


[ 本帖最后由 frank_seng 于 2008-5-6 15:45 编辑 ]
 
   

Rank: 1

帖子
228
主题
94
精华
2
可用积分
252
专家积分
0
在线时间
53 小时
注册时间
2007-07-17
最后登录
2013-04-12
论坛徽章:
0
2[报告]
  发表于 2008-05-06 15:44:36  | 只看该作者

回复 #1 frank_seng 的帖子

从上文中可知elf_map时并没有将文件内容读入内存,假设现在程序中有一条指令需要读取上面vm_start---vm_end之间的某内容,例如mov [0x08000011], %eax,那么将会执行如下序列:

    * CPU依据CR3(current->pgd)找到0x08000011地址对应的pgd[ i ],由于该pgd[ i ]内容保持为初始化状态即为0,导致CPU异常;
    * do_page_fault被调用,在该函数中,为pgd[ i]在内存中分配一个页表,并让该表项指向它,如下图所示:

      pgd
      +-----+
      |-----|    pt
      |  i  |--->+-----+
      |-----|    |-----|
      |     |    |  j  |
      +-----+    |-----|
                 |     |
                 +-----+

      注意:这里i为0x08000011高10位,j为其中间10位,此时pt表项全部为0(pte[j]也为0);
    * 为pte[j]分配一个真正的物理内存页面,依据vm_area_struct中的vm_file、vm_pgoff和vm_ops,调用filemap_nopage将磁盘file中vm_pgoff偏移处的内容读入到该物理页面中,如下图所示:

                          +-------------+    
                          |  Disk file  |    
                          |             |    
                          |-------------|    
                          | Seg Content----+  
                          |-------------|  |  
      pgd                 |             |  |  
      +-----+             |             |  |②
      |-----|    pt       +-------------+  |  
      |  i  |--->+-----+                   |  
      |-----|    |-----|   ①   page       |  
      |     |    |  j  |------->+-----+<---+  
      +-----+    |-----|        |     |
                 |     |        |     |
                 +-----+        |     |
                                +-----+      
      ①.分配物理内存页面;
      ②.从磁盘文件中将内容读取到物理内存页面中;

# ui_main_window.py from datetime import datetime import json import os import sys from pathlib import Path from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QFileDialog, QComboBox, QGroupBox, QApplication, QMessageBox, QDialog, QCheckBox, QRadioButton ) from PyQt6.QtCore import QObject, pyqtSignal import logging print(f" 当前工作目录: {os.getcwd()}") print(f" 文件所在目录: {Path(__file__).resolve()}") from excel_to_clm import ExcelToCLMConverter from channel.range_channel_sync import CLMRangeSynchronizer from power.power_sync import PowerTableSynchronizer from rate_set.rate_sync import RateSetSynchronizer from utils import get_output_dir # ============= 日志重定向器 ============== class LogEmitter(QObject): log_signal = pyqtSignal(str) # ============= 主窗口类 ============== class CLMGeneratorUI(QWidget): # 类顶部定义(靠近 class CLMGeneratorUI(QWidget):) LOCALE_UI_MAPPING = [ ("2_4G", "locale_2g_idx"), ("2_4G_HT", "locale_2g_ht_idx"), ("5G", "locale_5g_idx"), ("5G_HT", "locale_5g_ht_idx"), ("6G", "locale_6g_idx"), ("6G_HT", "locale_6g_ht_idx"), ] def __init__(self, generator=None): super().__init__() self.generator = generator or ExcelToCLMConverter() self.log_emitter = LogEmitter() self.log_emitter.log_signal.connect(self.update_log_area) # 设置日志系统以捕获同步器输出 self._setup_logging_redirect() self.setWindowTitle("Wi-Fi CLM Generator Tool v1.2") self.resize(700, 600) self.init_ui() self.load_default_config_for_power_tables() def _setup_module_logger(self, module_name: str): """ 为指定模块名设置日志发到 UI :param module_name: 如 "channel", "power", "rate_set" """ logger = logging.getLogger(module_name) logger.setLevel(logging.INFO) # 定义统一的日志处理器类(在方法内定义,但会被外部引用) class QtStreamHandler(logging.Handler): def __init__(self, emitter): super().__init__() self.emitter = emitter self.setFormatter( logging.Formatter('%(levelname)s %(name)s: %(message)s') ) def emit(self, record): try: msg = self.format(record) if msg.strip(): self.emitter.log_signal.emit(msg.strip()) except Exception: self.handleError(record) # 移除旧的同类型 handler for h in logger.handlers[:]: if isinstance(h, QtStreamHandler): logger.removeHandler(h) # 添加新 handler handler = QtStreamHandler(self.log_emitter) logger.addHandler(handler) logger.propagate = False # 防止被父 logger 重复处理 def _setup_logging_redirect(self): """统一注册多个模块的日志发""" modules = ["channel", "power", "rate_set", "excel_to_clm"] for mod in modules: self._setup_module_logger(mod) class LogStream: """伪装成文件对象的日志流,用于 logging.Handler""" def __init__(self, emitter): self.emitter = emitter def write(self, msg): if msg.strip(): self.emitter.log_signal.emit(msg.strip()) def flush(self): pass def init_ui(self): layout = QVBoxLayout() # === 1. 输入区:Excel & Config === input_group = QGroupBox("输入配置") input_layout = QVBoxLayout() # Excel 文件 excel_row = QHBoxLayout() excel_row.addWidget(QLabel("Excel 文件:")) self.excel_path = QLineEdit("input/Archer BE900US 2.xlsx") self.btn_browse_excel = QPushButton("浏览...") self.btn_browse_excel.clicked.connect(self.select_excel) excel_row.addWidget(self.excel_path) excel_row.addWidget(self.btn_browse_excel) input_layout.addLayout(excel_row) # Config 文件 config_row = QHBoxLayout() config_row.addWidget(QLabel("Config 文件:")) self.config_path = QLineEdit("config/config.json") self.btn_browse_config = QPushButton("浏览...") self.btn_browse_config.clicked.connect(self.select_config_file) config_row.addWidget(self.config_path) config_row.addWidget(self.btn_browse_config) input_layout.addLayout(config_row) # --- C 源文件(目标文件)--- cfile_row = QHBoxLayout() cfile_row.addWidget(QLabel("目标 C 文件:")) self.c_file_path = QLineEdit("input/wlc_clm_data_6726b0.c") self.btn_browse_cfile = QPushButton("浏览...") self.btn_browse_cfile.clicked.connect(self.select_c_file) cfile_row.addWidget(self.c_file_path) cfile_row.addWidget(self.btn_browse_cfile) input_layout.addLayout(cfile_row) # 关键:放在这里! # Locale ID(可选,未来可能用于过滤) locale_row = QHBoxLayout() locale_row.addWidget(QLabel("默认国家码:")) self.locale_combo = QComboBox() self.locale_combo.addItems(["US", "FCC", "EU", "JP", "CN"]) locale_row.addWidget(self.locale_combo) input_layout.addLayout(locale_row) input_group.setLayout(input_layout) layout.addWidget(input_group) # === 2. 主流程控制区(分步操作)=== step_group = QGroupBox("主流程控制(请按顺序操作)") step_layout = QVBoxLayout() # === 0. 全局预览模式开关 === self.chk_dry_run = QCheckBox(" 启用预览模式(所有写入操作不实际修改文件)") self.chk_dry_run.setChecked(True) self.chk_dry_run.setToolTip("勾选后所有‘写入’操作仅模拟变更,不会保存到磁盘") layout.addWidget(self.chk_dry_run) # 步骤 1: 解析 Excel self.btn_parse = QPushButton(" 1. 解析 Excel 并生成数据") self.btn_parse.setToolTip("读取Excel并生成内部配置与RANGE宏") self.btn_parse.clicked.connect(self.run_parse) step_layout.addWidget(self.btn_parse) # 步骤 2: 同步选项(互斥选择) sync_choice_layout = QHBoxLayout() self.radio_channel = QRadioButton("插入信道范围") self.radio_power = QRadioButton("注入功率表") self.radio_channel.setChecked(True) # 默认选中 sync_choice_layout.addWidget(self.radio_channel) sync_choice_layout.addWidget(self.radio_power) step_layout.addLayout(sync_choice_layout) # 执行按钮 self.btn_execute_sync = QPushButton(" 2. 执行选定的同步任务") self.btn_execute_sync.clicked.connect(self.execute_selected_sync) step_layout.addWidget(self.btn_execute_sync) step_group.setLayout(step_layout) layout.addWidget(step_group) # === 3. 功率表名称配置(紧凑排布)=== power_table_group = QGroupBox("功率表名称配置") power_table_layout = QVBoxLayout() # 提示语 power_table_layout.addWidget( QLabel("为每个频段指定对应的功率表名称(如 us, eu, cn_5g):") ) # 使用网格布局限制宽度 + 控件大小 grid = QHBoxLayout() # 改为横向两列布局,节省垂直空间 left_col = QVBoxLayout() right_col = QVBoxLayout() table_configs = [ ("2_4G", "2.4G"), ("2_4G_HT", "2.4G HT"), ("5G", "5G"), ("5G_HT", "5G HT"), ("6G", "6G"), ("6G_HT", "6G HT"), ] self.power_table_edits = {} for idx, (key, label) in enumerate(table_configs): row_layout = QHBoxLayout() row_layout.addWidget(QLabel(f"{label}:"), 1) edit = QLineEdit(key.lower().replace("_ht", "").upper()) edit.setMaximumWidth(120) # 限制编辑框宽度 edit.setObjectName(f"edit_{key.replace('.', '_')}") self.power_table_edits[key] = edit row_layout.addWidget(edit, 2) # 分左右列填充 if idx < len(table_configs) // 2: left_col.addLayout(row_layout) else: right_col.addLayout(row_layout) grid.addLayout(left_col) grid.addLayout(right_col) power_table_layout.addLayout(grid) power_table_group.setLayout(power_table_layout) layout.addWidget(power_table_group) # === 5. 独立功能按钮:速率集同步(随时可用)=== utility_group = QGroupBox("独立工具") utility_layout = QVBoxLayout() self.btn_sync_rate_set = QPushButton(" 写入速率集(即时生效)") self.btn_sync_rate_set.clicked.connect(self.run_rate_set_sync) self.btn_sync_rate_set.setToolTip("直接将速率集数据写入C文件,不受主流程限制") utility_layout.addWidget(self.btn_sync_rate_set) utility_group.setLayout(utility_layout) layout.addWidget(utility_group) # === 6. 日志输出 === self.log_area = QTextEdit() self.log_area.setReadOnly(True) self.log_area.setFontFamily("Consolas") # 更适合日志查看的等宽字体 self.log_area.setStyleSheet("QTextEdit { font-size: 9pt; }") self.log_area.append("🔧 CLM Generator 已启动,请开始第一步...") layout.addWidget(self.log_area) self.setLayout(layout) # === 初始化状态控制 === self.update_button_states() # 绑定事件以动态更新按钮状态 self.excel_path.textChanged.connect(self.update_button_states) self.c_file_path.textChanged.connect(self.update_button_states) def update_button_states(self): """根据输入字段和解析状态来启用/禁用相关按钮""" # 检查 Excel 文件路径是否存在 excel_path = self.excel_path.text().strip() has_excel = bool(excel_path) and os.path.exists(excel_path) # 检查 C 文件路径是否填写 c_file_path = self.c_file_path.text().strip() has_c_file = bool(c_file_path) # 检查是否已完成解析(last_config 存在且非 None) has_parsed = ( hasattr(self.generator, 'last_config') and self.generator.last_config is not None ) # 所有判断都返回明确的布尔值 self.btn_parse.setEnabled(has_excel) can_sync = has_parsed and has_c_file self.btn_execute_sync.setEnabled(bool(can_sync)) # 确保为 bool def execute_selected_sync(self): """根据用户选择执行信道同步或功率表同步""" if self.radio_channel.isChecked(): self.run_range_sync() elif self.radio_power.isChecked(): self.run_power_sync() else: self.log("请选择要执行的操作类型") def run_power_sync(self): """执行功率表同步:调用 PowerTableSynchronizer 完成注入""" try: config_path_str = self.config_path.text().strip() or "config/config.json" config_path = Path(config_path_str) if not config_path.exists(): self.log(f"Config 文件不存在: {config_path}") QMessageBox.critical(self, "错误", f"配置文件不存在:\n{config_path}") return dry_run = self.chk_dry_run.isChecked() # ========== 添加日志文件支持 begin ========== file_handler = None log_file_path = None if not dry_run: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = get_output_dir() / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file_path = log_dir / f"power_sync_{timestamp}.log" file_handler = logging.FileHandler(log_file_path, encoding='utf-8') file_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S') ) power_logger = logging.getLogger("power") power_logger.addHandler(file_handler) self.log(f" 功率表日志文件已创建: {log_file_path.name}") # ========== end ========== self.log(" 初始化功率表同步器...") synchronizer = PowerTableSynchronizer( c_file_path=None, dry_run=dry_run, config_path=str(config_path), ) self.log(" 开始同步功率表与国家码...") was_modified = synchronizer.run() if was_modified: if dry_run: self.log(" 预览模式:检测到变更,但未写入文件") else: self.log(" 功率表与国家码同步完成!") self.log(" 所有修改已保存至目标 C 文件") else: self.log(" 所有 Locale 已存在,无需更新") QMessageBox.information(self, "成功", "功率表同步完成!详情见日志。") except Exception as e: self.log(f" 功率表同步失败: {str(e)}") import traceback self.log(f"错误详情:\n{traceback.format_exc()}") QMessageBox.critical(self, "错误", f"同步过程出错:\n{str(e)}") finally: # ========== 清理 FileHandler begin ========= if file_handler: try: file_handler.flush() file_handler.close() logging.getLogger("power").removeHandler(file_handler) # ✅ power self.log(" 功率表日志文件已关闭") except Exception as e: print(f"[WARN] 关闭 power 日志文件失败: {e}") if not dry_run and log_file_path: self.log(f" 完整日志已保存至:\n{log_file_path}") # ========== end ========== def run_rate_set_sync(self): """执行速率集同步""" try: config_path_str = self.config_path.text().strip() or "config/config.json" config_path = Path(config_path_str) if not config_path.exists(): self.log(f"Config 文件不存在: {config_path}") QMessageBox.critical(self, "错误", f"配置文件不存在:\n{config_path}") return c_file_path_str = self.c_file_path.text().strip() if not c_file_path_str: self.log(" 未指定目标 C 文件路径") QMessageBox.warning(self, "警告", "请先设置目标 C 文件路径") return c_file_path = Path(c_file_path_str) if not c_file_path.exists(): self.log(f"目标 C 文件不存在: {c_file_path}") QMessageBox.critical(self, "错误", f"C 文件不存在:\n{c_file_path}") return dry_run = self.chk_dry_run.isChecked() # ========== 添加日志文件支持 begin ========== file_handler = None log_file_path = None if not dry_run: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = get_output_dir() / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file_path = log_dir / f"rate_sync_{timestamp}.log" file_handler = logging.FileHandler(log_file_path, encoding='utf-8') file_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S') ) rate_logger = logging.getLogger("rate_set") rate_logger.addHandler(file_handler) self.log(f" 速率集日志文件已创建: {log_file_path.name}") # ========== end ========== self.log(" 初始化 [速率集同步器] ...") try: synchronizer = RateSetSynchronizer( c_file_path=str(c_file_path), dry_run=dry_run, config_path=str(config_path) ) except Exception as e: self.log(f" ❌ 初始化失败: {e}") QMessageBox.critical(self, "错误", f"无法创建速率集同步器:\n{str(e)}") return self.log(f" 开始同步速率集数据... (模式: {'预览' if dry_run else '执行'})") result = synchronizer.run() success = result.get("success", False) changed = result.get("changed", False) if success: if dry_run: if changed: self.log(" 📊 预览完成:检测到变更,确认无误后可关闭预览模式执行") else: self.log(" ℹ️ 预览完成:无新数据需要注入") QMessageBox.information(self, "预览完成", "预览模式下未修改文件。") else: if changed: backup = result.get("backup") self.log(f" ✅ 速率集同步成功!") self.log(f" 📁 修改文件: {result['file']}") if backup: self.log(f" 💾 备份文件: {Path(backup).name}") QMessageBox.information(self, "成功", f"速率集已写入!\n备份: {Path(backup).name}") else: self.log(" ℹ️ 所有速率集均已存在,无需更新") QMessageBox.information(self, "无需更新", "所有条目已存在,无需修改。") else: error = result.get("error", "未知错误") self.log(f" ❌ 同步失败: {error}") QMessageBox.critical(self, "失败", f"同步过程中发生错误:\n{error}") except Exception as e: self.log(f" ⚠️ 意外错误: {type(e).__name__}: {e}") import traceback self.log(f"详细堆栈:\n{traceback.format_exc()}") QMessageBox.critical(self, "严重错误", f"程序异常终止:\n{str(e)}") finally: # ========== 清理 FileHandler begin ========== if file_handler: try: file_handler.flush() file_handler.close() logging.getLogger("rate_set").removeHandler(file_handler) self.log(" 功率表日志文件已关闭") except Exception as e: print(f"[WARN] 关闭 power 日志文件失败: {e}") if not dry_run and log_file_path: self.log(f" 完整日志已保存至:\n{log_file_path}") # ========== end ========== def run_parse(self): """解析 Excel 并生成输出(使用真实的 ExcelToCLMConverter.convert 流程)""" file_handler = None log_file_path = None try: excel_path = self.excel_path.text().strip() config_path_str = self.config_path.text().strip() or "config/config.json" dry_run = False # 假设 parse 总是实际执行(非预览) # ========== 添加日志文件支持 begin ========== timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = get_output_dir() / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file_path = log_dir / f"parse_{timestamp}.log" file_handler = logging.FileHandler(log_file_path, encoding='utf-8') file_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S') ) # 使用独立的 logger 名称 parse_logger = logging.getLogger("excel_to_clm") parse_logger.addHandler(file_handler) parse_logger.setLevel(logging.INFO) self.log(f"📝 已创建解析日志文件: {log_file_path.name}") # ========== end ========== self.log("INFO 开始解析 Excel 文件...") if not excel_path: self.log("ERROR 请先选择 Excel 文件!") return if not os.path.exists(excel_path): self.log(f"ERROR Excel 文件不存在: {excel_path}") return if not os.path.exists(config_path_str): self.log(f"ERROR 配置文件不存在: {config_path_str}") return # === Step A: 收集 locale_map === import re locale_map = {} for display_name, enum_key in self.LOCALE_UI_MAPPING: text = self.power_table_edits[display_name].text().strip() if not text: self.log(f"ERROR [{display_name}] 功率表名称不能为空!") return if not re.match(r'^[a-zA-Z0-9_]+$', text): self.log(f"ERROR [{display_name}] 名称非法:'{text}' → 只能含字母、数字、下划线") return locale_map[enum_key] = text main_locale = locale_map["locale_2g_idx"] # === Step B: 更新 config.json 中的 assigned_locale === saved_names, full_config = self._read_locales_from_config(config_path_str) if full_config is None: self.log("ERROR 加载配置文件失败,终止解析") return updated_count = 0 for tgt in full_config.get("locale_targets", []): enum = tgt.get("enum") if enum in locale_map: new_loc = locale_map[enum] old_loc = tgt.get("assigned_locale") if old_loc != new_loc: tgt["assigned_locale"] = new_loc self.log(f"INFO {enum}: '{old_loc}' → '{new_loc}'") updated_count += 1 if updated_count > 0: try: with open(config_path_str, 'w', encoding='utf-8') as f: json.dump(full_config, f, indent=4, ensure_ascii=False) self.log(f"INFO 已更新 {updated_count} 个 assigned_locale 并保存到 config.json") except Exception as e: self.log(f"ERROR 保存 config 失败: {e}") import traceback self.log(f"DEBUG {traceback.format_exc()}") return # === Step C: 调用 convert() === try: self.generator = ExcelToCLMConverter( config_path=config_path_str, locale_display_name=main_locale ) self.log("INFO 调用 convert() 开始处理...") self.generator.convert(excel_path) self.log("✅ Excel 解析与代码生成完成!") except Exception as e: self.log(f"ERROR 执行 convert() 失败: {type(e).__name__}: {e}") import traceback self.log(f"DEBUG 详细堆栈:\n{traceback.format_exc()}") QMessageBox.critical(self, "解析失败", f"换过程中发生错误:\n{str(e)}") return self.update_button_states() except Exception as e: self.log(f"❌ 解析流程意外中断: {e}") import traceback self.log(f"TRACEBACK:\n{traceback.format_exc()}") finally: # ========== 清理 FileHandler begin ========== if file_handler: try: file_handler.flush() file_handler.close() logging.getLogger("excel_to_clm").removeHandler(file_handler) self.log("📁 解析日志文件已关闭") except Exception as e: print(f"[WARN] 关闭 parse 日志文件失败: {e}") if log_file_path: self.log(f"📄 完整日志已保存至:\n{log_file_path}") # ========== end ========== def select_excel(self): file, _ = QFileDialog.getOpenFileName( self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls)") if file: self.excel_path.setText(file) def select_config_file(self): file, _ = QFileDialog.getOpenFileName( self, "选择配置文件", "config/", "JSON 配置文件 (*.json);;All Files (*)") if file: self.config_path.setText(file) self.load_default_config_for_power_tables() def select_c_file(self): file, _ = QFileDialog.getOpenFileName( self, "选择 C 源文件", "input/", "C Source Files (*.c)") if file: self.c_file_path.setText(file) def log(self, msg): """发送日志消息到信号队列""" self.log_emitter.log_signal.emit(msg) def update_log_area(self, msg): """更新日志文本框""" self.log_area.append(f"> {msg}") print(f"[LOG] {msg}") # ← 关键:让开发者能看到 def clear_log(self): self.log_area.clear() def show_preview(self): try: output_file = get_output_dir() with open(output_file, "r", encoding="utf-8") as f: content = f.read() dialog = QDialog(self) dialog.setWindowTitle(f"预览: {output_file}") dialog.resize(700, 500) layout = QVBoxLayout() text_edit = QTextEdit() text_edit.setReadOnly(True) text_edit.setText(content[:2000] + "\n...(后续内容已截断)") close_btn = QPushButton("关闭") close_btn.clicked.connect(dialog.accept) layout.addWidget(text_edit) layout.addWidget(close_btn) dialog.setLayout(layout) dialog.exec() except FileNotFoundError: self.log(f" 无法预览: 文件未生成 '{output_file}',请先点击【开始生成】") except Exception as e: self.log(f" 预览失败: {str(e)}") def run_range_sync(self,): """运行同步:使用 generator 生成的 manifest 路径自动同步到 C 文件""" from pathlib import Path import logging from datetime import datetime if not hasattr(self.generator, 'last_config') or not self.generator.last_config: self.log(" 尚未生成任何 文件,请先点击【解析 Excel】") return c_file_path = self.c_file_path.text().strip() if not c_file_path: self.log(" 未指定目标 C 文件路径,请在下方输入或选择") return if not Path(c_file_path).exists(): self.log(f" 目标 C 文件不存在: {c_file_path}") return file_handler = None log_file_path = None dry_run = self.chk_dry_run.isChecked() if not dry_run: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_dir = Path(get_output_dir()) / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file_path = log_dir / f"range_sync_{timestamp}.log" file_handler = logging.FileHandler(log_file_path, encoding='utf-8') file_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S') ) target_logger = logging.getLogger("channel") target_logger.addHandler(file_handler) self.log(f" 日志文件已创建: {log_file_path}") try: mode = "预览模式" if dry_run else "执行模式" self.log(f" 开始同步... ({mode})") self.log(f" 修改文件: {c_file_path}") sync = CLMRangeSynchronizer( c_file_path=c_file_path, dry_run=dry_run ) was_modified = sync.run() if dry_run: if was_modified: self.log("发现需要更新的内容,确认无误后可点击【执行同步】") else: self.log("所有 RANGE 已存在,无需修改") else: if was_modified: self.log(f"同步成功!已更新 {c_file_path}") self.log(f"备份已保存为 {c_file_path}.bak") else: self.log("文件已是最新,无需修改") except Exception as e: self.log(f"同步失败: {str(e)}") import traceback tb_msg = traceback.format_exc() self.log(f"错误详情:\n{tb_msg}") raise finally: if file_handler: try: file_handler.flush() file_handler.close() logging.getLogger("channel").removeHandler(file_handler) self.log("日志文件已关闭") except Exception as e: print(f"Warning: failed to close file handler: {e}") if not dry_run and log_file_path: self.log(f"完整日志已保存至:\n{log_file_path}") def load_default_config_for_power_tables(self): """从当前 config.json 自动填充空的功率表名称""" config_path_str = self.config_path.text().strip() or "config/config.json" saved_names, _ = self._read_locales_from_config(config_path_str) if not saved_names: return filled_count = 0 for display_name, enum_key in self.LOCALE_UI_MAPPING: edit_widget = self.power_table_edits.get(display_name) if not edit_widget: continue if not edit_widget.text().strip() and enum_key in saved_names: edit_widget.setText(saved_names[enum_key]) filled_count += 1 if filled_count > 0: self.log(f" 已从 config.json 自动填充 {filled_count} 个功率表名称") def _read_locales_from_config(self, config_path_str): """从 config.json 读取 enum -> assigned_locale 映射""" try: if not os.path.exists(config_path_str): raise FileNotFoundError(f"Config 文件不存在: {config_path_str}") with open(config_path_str, 'r', encoding='utf-8') as f: full_config = json.load(f) saved_names = {} for tgt in full_config.get("locale_targets", []): enum = tgt.get("enum") loc = tgt.get("assigned_locale") if enum and loc: saved_names[enum] = loc.strip() return saved_names, full_config except Exception as e: self.log(f"读取配置失败: {e}") import traceback self.log(traceback.format_exc()) return {}, None def run_full_process(self): """一键完成:解析 Excel → 生成 manifest → 同步到 .c 文件""" reply = QMessageBox.question( self, "确认执行", "是否要执行【解析Excel + 更新C文件】全流程?\n\n" "建议先使用【预览同步】检查变更。", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return self.run_parse() if hasattr(self.generator, 'last_config') and self.generator.last_config: self.run_range_sync() else: self.log(" 解析失败,无法继续同步") if __name__ == '__main__': import sys import traceback from PyQt6.QtWidgets import QMessageBox def handle_exception(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return print("".join(traceback.format_exception(exc_type, exc_value, exc_traceback))) QMessageBox.critical(None, "未捕获异常", f"{exc_type.__name__}: {exc_value}") sys.excepthook = handle_exception app = QApplication([]) try: window = CLMGeneratorUI() window.show() sys.exit(app.exec()) except Exception as e: msg = QMessageBox() msg.setWindowTitle("严重错误") msg.setText(f"程序启动失败:\n\n{type(e).__name__}: {e}\n\n详情查看日志或联系开发者。") msg.setIcon(QMessageBox.Icon.Critical) msg.exec() print(f"Critical error: {e}") 打开打包后的文件双击就显示找不到配置文件退出,问题出在哪,我觉得配置文件要手动选择
11-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值