From cdb32839d576053684f5397281dd81228f63f495 Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.com> Date: Wed, 11 Feb 2026 02:38:52 +0800 Subject: [PATCH] fix --- AGENTS.md | 126 +++++++++ __pycache__/backend.cpython-36.pyc | Bin 0 -> 12582 bytes __pycache__/backend.cpython-39.pyc | Bin 0 -> 12750 bytes __pycache__/frontend.cpython-36.pyc | Bin 0 -> 10374 bytes backend.py | 394 ++++++++++++++++++++++------ frontend.py | 156 ++++++----- 6 files changed, 525 insertions(+), 151 deletions(-) create mode 100644 AGENTS.md create mode 100644 __pycache__/backend.cpython-36.pyc create mode 100644 __pycache__/backend.cpython-39.pyc create mode 100644 __pycache__/frontend.cpython-36.pyc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2f59a96 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,126 @@ +# BootRepairTool - 项目说明 + +## 项目概述 + +**BootRepairTool** 是一个用于修复 Linux 系统 GRUB 引导的图形化工具。适用于 Live USB/CD 环境,帮助用户修复损坏的 GRUB 引导加载器。 + +## 项目结构 + +``` +BootRepairTool/ +├── backend.py # 后端逻辑 - 分区扫描、挂载、GRUB修复 +├── frontend.py # 前端界面 - tkinter GUI +├── README.md # 项目简介 +└── AGENTS.md # 本文件 +``` + +## 技术栈 + +- **语言**: Python 3 +- **GUI 框架**: tkinter +- **运行环境**: Linux Live USB/CD (需要 root/sudo 权限) + +## 核心功能 + +### 1. 分区扫描 (`backend.py:scan_partitions`) +- 使用 `lsblk -J` 获取磁盘和分区信息 +- 识别 EFI 系统分区 (ESP) +- 过滤 Live 系统自身的分区 + +### 2. 系统挂载 (`backend.py:mount_target_system`) +- 挂载根分区 (/) +- 挂载独立 /boot 分区(可选) +- 挂载 EFI 分区(UEFI 模式) +- 绑定伪文件系统 (/dev, /proc, /sys, /run) + +### 3. 发行版检测 (`backend.py:detect_distro_type`) +- 支持: Arch, CentOS/RHEL, Debian, Ubuntu +- 通过读取 `/etc/os-release` 识别 + +### 4. GRUB 修复 (`backend.py:chroot_and_repair_grub`) +- **BIOS 模式**: `grub-install /dev/sdX` +- **UEFI 模式**: `grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB` +- 更新 GRUB 配置: + - Debian/Ubuntu: `update-grub` + - Arch: `grub-mkconfig -o /boot/grub/grub.cfg` + - CentOS: `grub2-mkconfig -o /boot/grub2/grub.cfg` + +### 5. 卸载清理 (`backend.py:unmount_target_system`) +- 逆序卸载绑定的文件系统 +- 卸载 EFI/boot/根分区 +- 清理临时挂载点 + +## 前端界面 (`frontend.py`) + +### 主要组件 + +1. **分区选择区域** + - 根分区 (/) - 必选 + - 独立 /boot 分区 - 可选 + - EFI 系统分区 (ESP) - UEFI 模式下推荐 + +2. **目标磁盘选择** + - 物理磁盘选择 (如 /dev/sda) + - UEFI 模式复选框(自动检测 Live 环境) + +3. **日志窗口** + - 彩色日志输出(信息/警告/错误/成功) + - 自动滚动 + +### 工作流程 + +``` +扫描分区 → 用户选择分区 → 点击修复 → 挂载系统 → +检测发行版 → chroot修复GRUB → 卸载分区 → 完成 +``` + +### 线程安全 + +修复过程在独立线程中运行,避免 UI 卡死。使用 `master.after()` 进行线程安全的日志更新。 + +## 代码规范 + +### 后端函数返回值 +所有后端函数统一返回 `(success: bool, ..., error_message: str)` 格式: +```python +# 示例 +success, stdout, stderr = run_command([...]) +success, disks, partitions, efi_parts, err = scan_partitions() +``` + +### 错误处理 +- 命令执行失败时自动清理已挂载的分区 +- 日志记录使用 `[Backend]` 前缀 +- GUI 使用弹窗显示关键错误 + +## 使用限制 + +1. **需要 root 权限**: 所有磁盘操作需要 `sudo` +2. **Live 环境**: 设计为在 Live USB/CD 中运行 +3. **单线程修复**: 同时只能进行一个修复任务 +4. **UEFI 自动检测**: 根据 Live 环境自动设置 UEFI 模式 + +## 测试 + +直接运行 `backend.py` 可进行基础测试(仅扫描分区): +```bash +sudo python backend.py +``` + +完整修复测试需要取消注释 `__main__` 块中的测试代码并设置实际分区。 + +## 潜在改进点 + +1. **添加 requirements.txt**: 当前无第三方依赖 +2. **国际化**: 当前仅支持中文界面 +3. **配置文件**: 支持保存/加载常用配置 +4. **日志持久化**: 将日志保存到文件 +5. **多语言发行版支持**: 扩展 detect_distro_type +6. **Btrfs/LVM 支持**: 当前仅支持标准分区 + +## 注意事项 + +- **数据风险**: 操作磁盘有数据丢失风险,建议备份 +- **EFI 分区警告**: UEFI 模式下未选择 ESP 会弹出警告 +- **临时挂载点**: 使用 `/mnt_grub_repair_` 格式 +- **依赖命令**: 需要系统中存在 `lsblk`, `mount`, `umount`, `grub-install`, `grub-mkconfig`/`update-grub` diff --git a/__pycache__/backend.cpython-36.pyc b/__pycache__/backend.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1db805ea4e54ef5011dffc5fc05cf91e7c9204c1 GIT binary patch literal 12582 zcmb_iTW}oJneOhH>A7e$l4V(zjW82Kz*;2BTo#B5NPJUbV#^K|feeAkSf?eAG@22o zr;R<;!y06R9c&N;V}i{k;SxfEO%}q<7-#dc`;dn`eJ`)pa0zd|NH;bx3sp##(w|(Y~+4T`=i$M7eoAOcnWYnM#|uKAQj+uFcsu?C>6rn$eB~&RCp?qicCdQ(WzJ}rfVXQY7s#Z8aGmLtJO*f z^RywtB6290Y7naW9F@7hkb}MWpkZv6dtkOiwX;Z{S>!DC;nH2>s zeyEu^9TIJ#9h^Ftn)6X3gc{3E>!L#}N7-^}6+dnPwK`F2h3G;_r&wQaQMKFL=E~DS z(JfY?%@x!Su^RopO7x&!ml&WN?jP3}GtlB1@V!PPLBA4ZjeM&2C9xLux~)~>+DT(p zm%p)Ai|b}|UBmm!{9ZnzeK(Z4D%F#^I<+Qsjdc~MevQ)W#SLQJATJMHSwA91BK`%7oOy?{wxIJqZz2KJYXwi%8 zoGxYy`Aja`hqW&)AJ|hU6bHtovxkHAqhsTx&PL|V_1s>=3r=T>dp%QHMMvgGQJwz` zgh4z3Jofbnj%YR(Z8x@w2#gzLy{J8hr*FUd({%rN7vz6JmE3-$cADsBp=SMER^v~7zeq4F({Ka!`RzG;B za`>%3eSY+h2cKXbgVPzAnMzU@2b0aLP_gp%6UhN#?HjN~=B7Rsoyts4TXJAD*Do;x zV5GRH!3`#xo92?C(u#+f$Ag*v(#HPOhW@Xl`yaaTx>AqI_W3O;y<(Pbp;=mkz||h= zqPdySUHynQgX1dVu-GC~w9=S??5LHdDO!JLt}vR(*>|t6 z*Xh;0Kqh~n*I?GEh~Jbg<6!1DC|eQtI+x_oNpXeN=cMd#{ztdEsJq$|^WT8AkQd5CZz61To!= z$G#go@W5~po8Hw=@ovLh;_#f(UJ24sD;g64SHr%;&o$hDiye48Tn?dzNi|+Ehzez) z33@_ff!)~1sAW#Z%=Pozum}W45EOknZ8W~>f=#8rCRM6(Xv$1I2>+K(eh9%b_mk(ZkR0o5XLQuE?Y^X46xhN1wxsM$V*GP&epWp) zTlxNLe?0hj_4TJJPd%w7w4PMc$~`E>9^9l3_e05&F_^rpCr5hCRAMwU4FPPW!F$?) zTrXPti>c7)UTbtx(kYak6s$ntbR%dmQ4^`!A51P;Sb6go^B=x-MWO1I>gr%}Xn4uG zYHkLTp62wRUB5Lx`KkyLhRFPdr;-jG35R*BzO zrBuh}OfF}MU5aL_lm(eej3{BWb^ml$TEbg4l+9Vgh2l`b$qOd&qFeWmTD};wi?TN? zsoS!Hf`yAh3e$rd)kO7ls?m>Lh3PuVo5z9J+3c z_E1OJEE+E4FiGZ_AoSkX?$>6G{1(&>Gjw{J=_BA6cEj>lZdgPo4f`cGJP{DF8QqVy z)MN1(UH*W8Fm02+2p8s#YB9jmliy>P2osp3>)(k#c2TFxcIT!Oe?(t?ss)bB?9 zxEqBYc+7=1u#0XBhjGp!eZZ~lziz=?b}pXFS^be?_iOn-V=j}E=By!KZJtZ;`cE~R z;EkA?&w3m)T90E+nYzB#2t-N_?ZuZH_@e|N(l~+zGbO=3i z>EQE~^UwRG@6A?@JXtyR8AS2+Z|}Ht@Z9_t&sC0pJojE5VwOS-0KH&7Gi6DVtq{Hk zrY+b;I9931=-zBjNGnf3Sr)z66vRVu8d9ftSsy6H25`CurYw=ol+1xqAq!Jrn9J_7 zhCOr4CPrS^$xr4BkAj1pEdjVYPF8qP;+D(ov2tEWy@1VRkt*~?B*_WcPr(2MH&U>H zf}1EH0U`-=rh@y%GR0J2>(I8+b(?RxdE@Q3+&>c4o_1|>UmYe%GZM=0;|Aq}4 zH*EgOEnD$y@&dViQ=TEFF6;fJfSBqp#d-8T&K>EE{hymQvvwNHQeiQbrl|wOyp{U( zj|d>90^?TEGYO6Z5O$<}>D-V-kl96y*M2k$>m%e4Z0~K8*AWN8xsq%Sc>{t}1PGT~ zP%&N`C`MWpnwJ#MWICH4E2P5c6VFKN-i*z%kEb>juO+FYsTNR?Tx3q+oX@;P%>e`H zMIu~&1?}v@W9JcQp$T&n|WA%dD6I=2m=&WM>>r@ts3QbH`bJw$*HA(cgdP4$p4XN>O# z%34`3Lk`+~B0R3q&o#zEyR`$~DC!f23;A6RjBDi}o)8`rQZ|6IAF0doa53nHAyY#@ znk38b#TksMw1^QW;9-3t1U&pUaB2j&y0g|2aB+muET}}h9C<7P{Ja75gu;J!1$FBZ z)P!c^K(h(w6b8sAzIya$K#K~;Ui{=e0*^-zR!<+Dd*kuBQ!fF&s+N|l&zT`svi8Min^b~+bX53Qf7RMT( zHx**YTQd6Z!BZ&RJ-l(p*1n+P^~C$t<7WVLD@%c@FhdMLD!Y(}P%31EEpG=0-h!#Ndv@;}-eQdwgvH=UUW20E zs2BE;u_SmXuSVbnVVGtX9bC` zst4(i8Fcj402|4Tpn|YzVWFI6sqDo$VTm+3LHr`5u##TKo@`$53AfWe)j~)_r>qW% z{(=M@ESC*4hC4SGbpRW@J4r__@Jx96>SxVTQtU^GNsoICOHF zDNPu8p&0|Fs;U=4y=lK*2=#Di0XcN}YMnX^Y5*#31;!pqR4XQ^BSh_&!913q`DGf) zqV+OBAC&#MK^NELpARIEj!=oN0Z??}Si;|q>P4dn z1vLZSGx|!c95~QX3{HgH0BFO6f`+OBc3JEH!)Xsb&>O?Z1XgT;;5XuO9oD`qX z%O;+1F)U(GTACXe4~n>d;z@}{%29&&_J5(R2%UZM$KtHFx-nL8JTAcLJt&V(#L$}r zPHhXPT3iLYd$^y7_CfgzeNW3_A}ff;`J5{H;xvUs~Kf@;ExO~{+3#FF%L~Gb}nc!Og?w9l053;p>pf~ z7u;6Jz+>e^tq*Qu0zP)~ahKctp0vTtFzjyb3F5V$z|=^%ZQ~))<0jxLyV`BTJO&r^ z$guaS9sz!Y>T{sz*KogrykkNW;6$8Z8qEWoEW^C91FiQO$Emr%d}(#+3_LRmIuLHC z9dXzNO`-r0qAob#rnvaYha|_?-L%Ljw(z_RCPB0$|7M?cvj=M7`MRS1+|xgw``Ob; z?pjg_qh?OYb(Lq&UOISmfqQMi5xs&V#^pNz83`=K4Y=vzxx@E?(&t-8NF&m*28Q5S zw)&DyQff&!G^CITNyO^O*Q+OvG*7p>2Y~yBz(T#aX$9&83+mSwrB+;8HZYYhrs;+w zEv@NHR;J zTYeoC9P-7fS__sa$zHD|3Tm}Snlu*Scb^o)^A|bEi0+E)TK!UxthG{v!hz|c4O5d0 zD`eXTY=p2yWgqyK%>I6YI_#12m8C=Ob~N*T(joI9S>p+-izY!P5qJUisgZkBZliQ$ zDl-Y&R@#jAl&&KKoOK{;J^4*ak{a>M9DHQiX?X_{z3uW|D%efI2nDPI3D&XtYtn{a zMV_ap89GX`vgA(6i8ZjAYHgT`7GcNbYB&0-6q8W2rv3wT_80;!7GW@XMp!XUkmUuD4l&8MM7YaiT!nDlFazghjjb!b`VmJ_v>%fUwNVq_u;l^Mi4HWL7>rF6WWHr)`E6y3qJ zh}Ic>A`Zo<=MB*Y&!35sHMn0{ToQo?E69Gr4KymG;By4+-83$wR>Mc7%k83_Q&({+ zMdL$RgemvZ2$AaHvVCF&#vXJdoI2#sluA9%bJgyby-qwaV_ar`fjO+jB0j~hc^pPz zzcPRJMD>k>)n7i{tVCd;;pS%U=#jZ!oU6S0^dj}sB!m}A`ntd(sjG1M6Q3%@QCo;0&wZOe(W%d;uSs>sI&-elIiBI8nH1F*9 zBDKC>Zmq@o(beh8%j?OaGqopYJ!)AZ1w-Dp9P+(2nJCa{Ai;a<1UwrD~ zxsPxW5s(7p(xQN5PH#|tkK!2$#wpl~0AyK;Pf##P83~rJi^QlGq+VfutPHM9bA?CY zZ6E<6BM5MpVe#0bJMP;)vTgge;jLb1To#;Z+^(|RB~t|!3kjlBTfv56T50d1CL#*9^n=C+f{u0lE{56UO zHTh>4L?EoC%TDsB2g=Rn9)6msBOX_3jy)skmE&|`aiDKX! zlW`9N8}}lR_izQ^?pc`hIm~!)B~Cem*ihLlhx6U4)YZogl*Ae(ZphAoYl|CZ^8s98 zhQP28*FZiW6p3-Y9LfJ4={DT9ydc`eGSPt+Bl+JTZ#nWV){J@430)Dbr&b^prk^ua7!mv^92+eJo zfIMcvA79Waj1InE=%T*E%t7CG5kT*P=BST`X2*?SX0B?U8JHqRFgHDoxe;H2v|Q_K z#FDVY*}Rv4H$cn+QHh&uzL5O-z4vXpc;QUt&Ex8t^6wxtVFKg(6CYpDb{b4lE4T@- zoH;uG#$oEp%ZKKU{uJI3<%U$kR@L)&-Xamr-@m9^Y69Dp*|U}Jztdc;X`e2vx=1Og zIyJezt{J5-;invO$T?RB-Tn-+>$C=|eXq+X&vGeF@VY;nhoe51D=qKuSJQCU{yT0< z-*#(1j#Oz?KfU^a5Fr8z^1xjTwIjWhXq4Qie8C->O&}&&!y$m zH~tK&@iA1Xqn=s+K&q4=t~dAB*j`@pGb*`<`o$ui;J4F-fWBCG@qHS@rNhVP&Yw{# zgD69oJ(w9fLCL0@9$|XAH z@I7>K?_#jt<+p}Ugla|`F>py+5bh_up1`FC-HMyg4~YPIeQ`O080ic8L5J`yc_;Ge z+JztJhEBZ;DO{PrQ*5FP_}!3Y@T`BUMQW>lXVS%JT)B3I@rrWm1j_iHt!&g}x`@Kn z5!erj2+8*_+`T~P^zD%woCwlgHRNrG^Kdr~Ionq>MGNpRxpG@w^(_%OcAyO)-%Z@Y zB+9rKS7mVh;C@83zY@A#D;tmW-A^|u%v~&FsZOa7NpSfGpKla=JA7Q(%=ZsIUoKn7 zx6X4p>juRWJqLy!^tr~EE4#8-68pV{Bp$5uMd=cJL*(QGr$as`ay)l7InElK4*8t8 zbO}!Eqy~ac__GPVeEXa@&_gzba#X_+zoOxYhpTTG$UDZ!I}YUC0_42`G+ji4d-xW1 zDgl3E#=9GNwHd!3mX25@R+U>GYf-qIzF8Q9Htt>RJWX4Xu{k@N)g6-$rs31}?~@mN zp!m1o@xLaw9YS4Afb_jLDyQF3esI;7vP@VwaE8x2b8a@N|A+Vc( z#l0K?dxsFv0^j=3LN9@*$C`W>WacXBq;iOm2SH`j4UiS5ij__wzx57$Po8m9BaZ3dg#{J4X0-&IFo9+d0u$tC|4N00PFLLKpK{z?b{j)z(sjaP4*Z$Z_JR`YwYm>@mQ@g4P z-tXMo^N?(^kL*Z&``&x*J@0bYN>0xQQ^9;F70QQG;d~?&ktHLLYB5wp8&*(7Er4TwdNb$pmZ6_>n)0Q zo7=qVgla4>7NX7h)DPoo^m~!981=f1UdrM9<28m9w0JG}UT4Hn{wmNK`Bd*q#uC(9 zU@kPSA5q3-_IKvh#?pPVEaClS{_fl-eM?I%N-a)Zle#u_ow*35egf(Z#*N0Z{gSzO zt+Y?xr|=pH<10Mxnx?#)j2@nMZ7r`5LG7e@9n~@9EmC5|MOy3rgz9Mn&S=i`)D2n7 z_SCi6LE8&$8nv^{s zc35E$)$2b6F%?e$k97lrgOY`X+k&lP1csHeY)el{Lo#iT{UKN8U8k<<&f6#)iQ{+i zox{_oAD=#Tu5$9p>5ooV&h4*Gd{+7F+3I&sSH|D0j32DNfAm+M9lZG6A64Idt@7O2 z3ujJN-+!xe;Ei8>cIcOT_HrI8M>8ywk5e~S#+zB8V&%=f@m|B+)oU4A-nQHX%ffIk?)se=@G;*LvlYRUSX!E1>(J?Q4 z=df8YcaO4FUj~DJ!y}b$+=XpnWNb5ydCCr&X`0C8cjk(NnVhw1dA&wL_5zv0SVG~A zdm+Cw7Hb0+7NejYf!B2)V&mz2IOm zpU)Hwuf;H}L6+sa*9#0jYIvHp!_4Kppq#9&IS_0M;Uu$ z0A-Gu3Oy*yb&is3%P|!>HNs_k9^! z>u~i(UyZ08arJ%j7xi@odW{|0=^Vq<^7Ao$ejJN;;p06QK7MEF{WDW%UKI=ZtIrNs zKKZbEA5{)~IDPIrl_x)~Jb(D&3*VbQeR|I6^%oom_|3<~ZkT#$zu;3jcx-y| zt@yGF=bonH{Lm81F^Wri;!ASY_T0#lZHZ`6H~`eRn8u5F2eU+O*~vUdmgNKR`cF%F7I#9 zS}&$QJWxIMA^tBO`v8Jz>if@MCV7@w&fuVFSv_&fHn6w(+hi;e$M~mCep)>^UU}-( zU+#IlI`L%X$tT2w){|mdc@IjFht`Ngzb!tatc=gd@qvV%iVbE)A;irzc#k^J4Ys-4 zPHBTX%)t@10LO(bq~K}<4ka5*#6*hrSH`bMtepJe^apQTMijjgU0oUP>%U@MF*hsY zp5!b>yMAk2CI}KH43X*cPsSZO-f=N|&Y~-I^29sUL+2_7-$PgCHJCMW8L2RbgmBaN zo_%iWg;%R5-kEy!4bkJ3@kEQKjk0V3^R%;=EqD>j**?mOv_M{fIR#G_tHj?}rBuf~ znOx2^Hj6Ur7%Q?=Y(R*qb-PEi%rv}tec7DZU$py*PQlMWJ!+EFlWZ>!7qblx@lrGMSJ; z?MEGL!NXyd{Iw_YmS2MH`h28JU6SASWYF3_v4CZuw7`k$; zw5_A8+lmVbO)`6*3ca~%Tq)d!nn4c0UgD)g;1+a)>|-}*ghv$X1vfYmFd~z(A8V<{ zqLVU8QIEd3g$bi|i{y%a#*DT}g_m#Vu^^Ann-rt!@ZX*OyK7P&{Vn9PIw=|RPlU=L zSAncX?=Y&65d*mF9#P6+#5)#B<8rabxJtyS#xAKGL8_Be`Zxt~pV9(K7wRuS{iqAM z&UU$xaoNhbEj*0!9MTM;wdN6a&7H$>`H{Va-(U_G?vln8cDQ*C!RHrZ7Qq)WF^}~) zX0jf~T=LTO^(_NjyRDpOyW5JIi%>$b+Kt6-D_{FqL3Pz~%#HCD|JQRnu8iL2&+avD zD}Z`r)(rpQF)NSkqIFGN=WM|KbYki1nfy>`y4TodUw6><$XPM!`x7{uDt< z-8Gc4Q-O7T>q|@Tx%rmWw}17PJCc2O+}xME`R28^B-gCIbxm@`iq$Lb`Px_4;aQVV zQi0sAJnYB(jGQb5jC`^b<>U9!@jxQ-KW*I8vsRjz1&w@~CJ*8$Fl^eMPQV?Yup{lu z;fBnCbha@{>(MN170n@lpJ-z@(Rg|&NK&wzMiv6fRGrC~_z*C3JiDc=f| zrLtUx1hj5|r3cEe0==vY5wL&S|9tz%W6Tk{-IjH zwBV75ILB$!ar~->egMoU(Cmee-z5lnXixRTp{ds&pE~|sfLL+*1;m{`|3c;XD^q8_ zQ+;KBUFDuLgW z#-VRXN!~l7B#Wh0{i`>w>*?Dv@ZjckJzLhN*7a=Mw6%X=^QQIv13g=}u3y`;dG&n* z8&E#eOoc{jn)mO%O>pq_MVPC(K0NEFg zTryUVzE?eR3V^qAC2$qwKm*XqDi$E9iW$RVtH6P8$W+_CTQ>EtH3y4^$w3k$??NK% z1$~6fuBW0)5qK(0*UX$lE)G6*XwK2|+N{A$p&>S`w^7C-K9Z)$I;_%&rs62laf6*I zt2mVH3jX9}z)0*~l*1ada8aG+vf7LCglVM7rQ(y2!fJXQ+p`6OO8_hFm%1Dp5uPP$ zo}Sy4+vt(;#=LoJ=kyE1#JUOI@@yr%9lN z>omY1(0<)0XZ^Yqvz3Vu<;&6oQlSH;b9n4uh1vR`R8V|#vZbMZs~;bL`n|aCE$qeW z;l0ym&*DH$|9F4(({Zw!gaOC%g~@>(h5|T%)iW0 zg&cBJh_aOb62 z>4$o}j`|T|<}Q<2Fcp-dB|I%qL%eVN#IQJ((#_p)?pdD3@DD-bUW_+i7ldBxEIBC7O8`}Czjj3GifJ!Q z2^*%tKG|9T-!GjA-`N4EF0)%flR>+c(^TP9847@=FgS%=oG@!Mr)e%XLx^VRwrndl z6xJm9WaX=*a-h^y$a9Mp!1xNl~ zA+3O#ENg~b*6{@Gpb>$iM{)zhsuA^1JgL@DIZTj$T(W*=t2jFWIy;=bj$VMA?fvh@$tqKlbmYO!lpi$MU6 z`LD_4R<{-MZ@e68-oLFq1%S} zQ)l%=v381{0FY?)v5WFo^Zuwq0R?V3%(*rJZp0m?Qq`D;8G~C5z+sW&)SO|y#5#To zJ{kcZ2s_k{Ijn^yaR3-m7anj}T=@6{l4acSG{-wO`^>D2qtF%kH+#REeNnT|*k$#n zp8l(;A3PoBU5g8W)XXWqtn%#Xi+c{ua>a>GQ3l zqygrby?yXLn?3O+Np(d?)F-eB`K+qPCaOmdHcz*>QGg?eyh?R)(+boJ%&K2slp47- zuQy+?({x{wX56u!hBrLKg>bq!!bN`N;PJ|tPyOZJ`$^3m*3Z>TN)y_JgqJ8%-x6`5 zFt|diXVz-7KSu?pt6po?62-ayOR$?osMQ`|o2hz7qdG?BuWy{gIlpovyH+)!&8TF) z&4eiI9ks0)t(UAWWLskvLYSwFoJ=J>nIxRU9do|6bTkmq(0lQJCp#iu=i0_y{_I9% zdI9cQBQGi2Lg`RGGXis$Ssd&MWp_UaTnTcu$F@?E^oXbD;3~_GvOhy2(ayd_gl!Z& zOaWJd9R891Yf^{bK+==b967QS(H^9nNCT*e*8YM>+`HCrv2}{A=<8BUf^Fvi8Re`3 z0x1%Lod$i!^*A$RhjWO* z;)?;|p+#K8@Nv=%&$?_P?O4~4PjC&nlZ-M*bx2HHIw2Jx6Ak)>;zUtEQGAL54ELMT zJ226pPxmR$9xex9zG;)vggyyvM|g9W#CALYe5vB+KZUZsnq7Q;y4}`!EKX zk6}S=3}tl|zmA(B%dMr&9PQ9V7j>+fII%W!Kou^+z8uNp6I~-&(`w`Ofc)Ek9e}Xh-r{;QN zKSLUGf^;XR{{rbQ%%19oc=|U;iW`mp)@3jiag{W6=-|{3&s1J{dX5%s(gm{>PF-%2@E0b- z-cN-7t0|Qw88*0sE{z7wf?c#IW|-TvnF7U}?Qp$06dN*)BFj*$2v?KkSSF_nKid+{ zvJ^Az!QP^kWF}y-WtPH}&RUjZdH_heU@0RFuLGuU~uD zvgJ1=mX>0OrZ=qr<~otSNS72PWyy_Py(I#kO8v*j7n#D&3@fHu;0H4^yNV8jm0+W6 zLS>_Pu}2W3GzjXEu@t=adED(5oZVm$stu^rT8kyo|It!sJ!w1n?K$&N(=?bvY~X9K zOoy%$n(Qbj+4ql5pFUH0_Ql2=R9}4Z!kG_oXA-*Pv+Au3RqqyqjGM?-rm`5b+Qf);G8ko;JH)m22KJmh| zk%v;aI4;;&;Z!JVr1{->sui(ZcEt3PVYpswR!nCN`1b^baaA!#+;p{iI zyLUSl#Tr64BpnuwLRO7}2a+7po}PC8z7;t-Tn8%N%iv>cSKy}$M38rdXIB2Tq6{-)!AfR14krq{{}n?`0@VJY`7uJ#KPv8fJU(~7gsmt!dL|Px7b;Y>E|-!o*jhf z0gz@1KU|jcJI}A*w{^{h^QS5&kBFPl{|7M$`x>9f_}GQE&&oKpg3I>GsYBDRAE2(h zw14W*+i<-IAEywXqMpCg<_KN>iH^7kClp^9KV5n1t>$V?`*L>GIbvSasY&W(&G7#U zPS8H*8f@n}n)hdr`(@>x!(Gf_hB$!r041i4(PGno_GYZ8a3 zv@l7pB=9E=eUXjb#jS*1B6R`aVztOeX|iP2xQk}aA#ow*jt0+XlkG!Z6JgEzO#CBb+34RY>%}Oe^nqegr~oC_Bv7QDL0|67{7{>J|S| ztMR>7siU6R_=YPm6C*~qBxiFXsgMY;1H`bwno*L(KYGX8qGBBBv;DP_4&6}G*Ag$j zN29uU;KGA!3_Mj)TMEc?>6~aewLXU)p z9Dc`b@0e=?bo;I0)1;aqNDN$33pW21brd&9)<&1-l+ik;}&~DHQ?ffaMgGo?%W|%m)bggy6KX8IP8k=mq@@v z@I#dA)>3@0gd2B314PE1iV=NTyIm?Pk1Vb61}G*0PAzF9tl7_fu3>PE`B1T$>(70z zoHmicxw7%S6o;}+ z{R5|x&xup7z=^xP0k*~b3ciY^0vyA15Uy`H_2)I5`Y>8H14u^zq~S~~w*W}r#=pZt z>u{H~a3>Yk5p&%|wM1TRt~tn_Uv7Do9K3)ru_kw9TzTaadYM4 zVtC5^d+S*rPyR8``o9y{&Jy&e2|>R5dga7hqHCfr@gdx9$4i0b%a{9-L1PB$ut5s8 zQ;?zH+Z2$`fy;Nc2{DJ@%`8uSA2X5V$r0iFAn7IOap6oM7mPnIHLqNpYm}xQ@*9X| zcU;`L9@Uyka1>lQIC1fNCufyoi%?FuR=%i6f1-uE`sX5L5^{`$h!BY6>i{<1;n>++ zN@sbSnI^+$-BKbd1R;sJRM=OL)-W5Psv!!{OcrCV=#30ATU2+e;D4~9Z(g5PwLOSh` z&n=lu#>Mx8mGNh$esCK0APmDevbl3BLUeH#E4E4#omN&rm6fxkBjh2hR9`xFA%eSU zX^Jl%7)_fq{%qXnR58@*RqfB&sJHbH#n+&K#gMKC2pCrGPCXPZ}FXz@1&9 zNB@V$*)z4K->x-1I;Uyg3f#`3O^2jCK`efB|ELz(bG5cbi*y`#V`{yzPGmLDxv#eq zNQq}uM;v~lC={Va=;5uQT2SIY^kK-QNyA9qviUJA$| z$US1hU?O!{-)aOO(rIztXfJyjfnUpu)GwUyMH^596o2g|-V5MNH7y4LJ@I98=+1o6 zaB}7<8jwZqL!r+_i0d!tZG6|Px6fB_5TNy;*JGbYm*H4kiaxmbS{-h{=u$YXF5ysI dif(A{lP*ESUy80>a3^5`av|zK0l)#ue*vOaVa5Oe literal 0 HcmV?d00001 diff --git a/__pycache__/frontend.cpython-36.pyc b/__pycache__/frontend.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79f542aeebf5b594b3480e970dcaa21c975b812c GIT binary patch literal 10374 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 bool: + """ + 验证设备路径格式是否合法(防止命令注入)。 + :param path: 设备路径(如 /dev/sda1, /dev/mapper/cl-root) + :return: 是否合法 + """ + if not path: + return False + # 允许的格式: + # - /dev/sda, /dev/sda1 + # - /dev/nvme0n1, /dev/nvme0n1p1 + # - /dev/mmcblk0, /dev/mmcblk0p1 + # - /dev/mapper/xxx (LVM逻辑卷) + # - /dev/dm-0 等 + patterns = [ + r'^/dev/[a-zA-Z0-9_-]+$', + r'^/dev/mapper/[a-zA-Z0-9_-]+$', + ] + return any(re.match(pattern, path) is not None for pattern in patterns) + + +def run_command(command: List[str], description: str = "执行命令", + cwd: Optional[str] = None, shell: bool = False, + timeout: int = COMMAND_TIMEOUT) -> Tuple[bool, str, str]: """ 运行一个系统命令,并捕获其输出和错误。 :param command: 要执行的命令列表 (例如 ["sudo", "lsblk"]) :param description: 命令的描述,用于日志 :param cwd: 更改工作目录 - :param shell: 是否使用shell执行命令 (通常不推荐,除非命令需要shell特性) - :return: (True/False, stdout, stderr) 表示成功、标准输出、标准错误 + :param shell: 是否使用shell执行命令 + :param timeout: 命令超时时间(秒) + :return: (success, stdout, stderr) 表示成功、标准输出、标准错误 """ try: print(f"[Backend] {description}: {' '.join(command)}") @@ -20,9 +55,10 @@ def run_command(command, description="执行命令", cwd=None, shell=False): command, capture_output=True, text=True, - check=True, # 如果命令返回非零退出码,将抛出CalledProcessError + check=True, cwd=cwd, - shell=shell + shell=shell, + timeout=timeout ) print(f"[Backend] 命令成功: {description}") return True, result.stdout, result.stderr @@ -30,6 +66,9 @@ def run_command(command, description="执行命令", cwd=None, shell=False): print(f"[Backend] 命令失败: {description}") print(f"[Backend] 错误输出: {e.stderr}") return False, e.stdout, e.stderr + except subprocess.TimeoutExpired: + print(f"[Backend] 命令超时: {description}") + return False, "", f"命令执行超时(超过 {timeout} 秒)" except FileNotFoundError: print(f"[Backend] 命令未找到: {' '.join(command)}") return False, "", f"命令未找到: {command[0]}" @@ -37,16 +76,103 @@ def run_command(command, description="执行命令", cwd=None, shell=False): print(f"[Backend] 发生未知错误: {e}") return False, "", str(e) -def scan_partitions(): + +def _process_partition(block_device: Dict, all_disks: List, all_partitions: List, + all_efi_partitions: List) -> None: + """ + 处理单个块设备,递归处理子分区、LVM逻辑卷等。 + """ + dev_name = f"/dev/{block_device.get('name', '')}" + dev_type = block_device.get("type") + + if dev_type == "disk": + all_disks.append({"name": dev_name}) + # 递归处理子分区 + for child in block_device.get("children", []): + _process_partition(child, all_disks, all_partitions, all_efi_partitions) + + elif dev_type == "part": + # 过滤掉Live系统自身的分区 + mountpoint = block_device.get("mountpoint") + if mountpoint and (mountpoint == "/" or + mountpoint.startswith("/run/media") or + mountpoint.startswith("/cdrom") or + mountpoint.startswith("/live")): + # 继续处理子设备(如LVM),但自己不被标记为可用分区 + for child in block_device.get("children", []): + _process_partition(child, all_disks, all_partitions, all_efi_partitions) + return + + part_info = { + "name": dev_name, + "fstype": block_device.get("fstype", "unknown"), + "size": block_device.get("size", "unknown"), + "mountpoint": mountpoint, + "uuid": block_device.get("uuid"), + "partlabel": block_device.get("partlabel"), + "label": block_device.get("label"), + "parttype": (block_device.get("parttype") or "").upper() # EFI分区类型GUID + } + all_partitions.append(part_info) + + # 识别EFI系统分区 (FAT32文件系统, 且满足以下条件之一) + is_vfat = part_info["fstype"] == "vfat" + has_efi_label = part_info["partlabel"] and "EFI" in part_info["partlabel"].upper() + has_efi_name = part_info["label"] and "EFI" in part_info["label"].upper() + is_efi_type = part_info["parttype"] == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" + + if is_vfat and (has_efi_label or has_efi_name or is_efi_type): + all_efi_partitions.append(part_info) + + # 递归处理子设备(如LVM逻辑卷) + for child in block_device.get("children", []): + _process_partition(child, all_disks, all_partitions, all_efi_partitions) + + elif dev_type in ["lvm", "dm"]: + # 处理LVM逻辑卷和Device Mapper设备 + mountpoint = block_device.get("mountpoint") + + # 过滤Live系统自身的挂载点 + if mountpoint and (mountpoint == "/" or + mountpoint.startswith("/run/media") or + mountpoint.startswith("/cdrom") or + mountpoint.startswith("/live")): + pass # 仍然添加到分区列表,但标记一下 + + # 尝试获取mapper路径(如 /dev/mapper/cl-root) + lv_name = block_device.get("name", "") + # 如果名称中包含'-',可能是mapper设备 + if "-" in lv_name and not lv_name.startswith("dm-"): + mapper_path = f"/dev/mapper/{lv_name}" + else: + mapper_path = dev_name + + part_info = { + "name": mapper_path, + "fstype": block_device.get("fstype", "unknown"), + "size": block_device.get("size", "unknown"), + "mountpoint": mountpoint, + "uuid": block_device.get("uuid"), + "partlabel": block_device.get("partlabel"), + "label": block_device.get("label"), + "parttype": (block_device.get("parttype") or "").upper(), + "is_lvm": True, + "dm_name": lv_name + } + all_partitions.append(part_info) + + # 递归处理可能的子设备 + for child in block_device.get("children", []): + _process_partition(child, all_disks, all_partitions, all_efi_partitions) + + +def scan_partitions() -> Tuple[bool, List, List, List, str]: """ 扫描系统中的所有磁盘和分区,并返回结构化的信息。 :return: (success, disks, partitions, efi_partitions, error_message) - disks: list of {"name": "/dev/sda"} - partitions: list of {"name": "/dev/sda1", "fstype": "ext4", "size": "100G", "mountpoint": "/"} - efi_partitions: list of {"name": "/dev/sda1", "fstype": "vfat", "size": "512M"} """ success, stdout, stderr = run_command( - ["sudo", "lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID,PARTLABEL,LABEL,TYPE"], + ["sudo", "lsblk", "-J", "-o", "NAME,FSTYPE,SIZE,MOUNTPOINT,UUID,PARTLABEL,LABEL,TYPE,PARTTYPE"], "扫描分区" ) @@ -60,31 +186,7 @@ def scan_partitions(): all_efi_partitions = [] for block_device in data.get("blockdevices", []): - dev_name = f"/dev/{block_device['name']}" - dev_type = block_device.get("type") - - if dev_type == "disk": - all_disks.append({"name": dev_name}) - elif dev_type == "part": - # 过滤掉Live系统自身的分区(基于常见的Live环境挂载点) - mountpoint = block_device.get("mountpoint") - if mountpoint and (mountpoint == "/" or mountpoint.startswith("/run/media") or mountpoint.startswith("/cdrom")): - continue - - part_info = { - "name": dev_name, - "fstype": block_device.get("fstype", "unknown"), - "size": block_device.get("size", "unknown"), - "mountpoint": mountpoint, - "uuid": block_device.get("uuid"), - "partlabel": block_device.get("partlabel"), - "label": block_device.get("label") - } - all_partitions.append(part_info) - - # 识别EFI系统分区 (FAT32文件系统, 且通常有特定标签或名称) - if part_info["fstype"] == "vfat" and (part_info["partlabel"] == "EFI System Partition" or part_info["label"] == "EFI"): - all_efi_partitions.append(part_info) + _process_partition(block_device, all_disks, all_partitions, all_efi_partitions) return True, all_disks, all_partitions, all_efi_partitions, "" @@ -93,21 +195,77 @@ def scan_partitions(): except Exception as e: return False, [], [], [], f"处理分区数据时发生未知错误: {e}" -def mount_target_system(root_partition, boot_partition=None, efi_partition=None): + +def _cleanup_partial_mount(mount_point: str, mounted_boot: bool, mounted_efi: bool, + bind_paths_mounted: List[str]) -> None: + """ + 清理部分挂载的资源(用于挂载失败时的回滚)。 + """ + print(f"[Backend] 清理部分挂载资源: {mount_point}") + + # 逆序卸载已绑定的路径 + for target_path in reversed(bind_paths_mounted): + if os.path.ismount(target_path): + run_command(["sudo", "umount", target_path], f"清理卸载绑定 {target_path}") + + # 卸载EFI分区 + if mounted_efi: + efi_mount_point = os.path.join(mount_point, "boot/efi") + if os.path.ismount(efi_mount_point): + run_command(["sudo", "umount", efi_mount_point], "清理卸载EFI分区") + + # 卸载/boot分区 + if mounted_boot: + boot_mount_point = os.path.join(mount_point, "boot") + if os.path.ismount(boot_mount_point): + run_command(["sudo", "umount", boot_mount_point], "清理卸载/boot分区") + + # 卸载根分区 + if os.path.ismount(mount_point): + run_command(["sudo", "umount", mount_point], "清理卸载根分区") + + # 删除临时目录 + if os.path.exists(mount_point) and not os.path.ismount(mount_point): + try: + os.rmdir(mount_point) + except OSError: + pass + + +def mount_target_system(root_partition: str, boot_partition: Optional[str] = None, + efi_partition: Optional[str] = None) -> Tuple[bool, str, str]: """ 挂载目标系统的根分区、/boot分区和EFI分区到临时目录。 - :param root_partition: 目标系统的根分区设备路径 (例如 /dev/sda1) + :param root_partition: 目标系统的根分区设备路径 :param boot_partition: 目标系统的独立 /boot 分区设备路径 (可选) :param efi_partition: 目标系统的EFI系统分区设备路径 (可选,仅UEFI) :return: (True/False, mount_point, error_message) """ - mount_point = "/mnt_grub_repair_" + str(int(time.time())) # 确保唯一性 - if not os.path.exists(mount_point): - os.makedirs(mount_point) + # 验证输入 + if not validate_device_path(root_partition): + return False, "", f"无效的根分区路径: {root_partition}" + if boot_partition and not validate_device_path(boot_partition): + return False, "", f"无效的/boot分区路径: {boot_partition}" + if efi_partition and not validate_device_path(efi_partition): + return False, "", f"无效的EFI分区路径: {efi_partition}" + + # 创建临时挂载点 + mount_point = "/mnt_grub_repair_" + str(int(time.time())) + try: + os.makedirs(mount_point, exist_ok=False) + except Exception as e: + return False, "", f"创建挂载点失败: {e}" + + # 跟踪已挂载的资源,用于失败时清理 + bind_paths_mounted = [] + mounted_boot = False + mounted_efi = False # 1. 挂载根分区 - success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point], f"挂载根分区 {root_partition}") + success, _, stderr = run_command(["sudo", "mount", root_partition, mount_point], + f"挂载根分区 {root_partition}") if not success: + os.rmdir(mount_point) return False, "", f"挂载根分区失败: {stderr}" # 2. 如果有独立 /boot 分区 @@ -115,20 +273,24 @@ def mount_target_system(root_partition, boot_partition=None, efi_partition=None) boot_mount_point = os.path.join(mount_point, "boot") if not os.path.exists(boot_mount_point): os.makedirs(boot_mount_point) - success, _, stderr = run_command(["sudo", "mount", boot_partition, boot_mount_point], f"挂载 /boot 分区 {boot_partition}") + success, _, stderr = run_command(["sudo", "mount", boot_partition, boot_mount_point], + f"挂载 /boot 分区 {boot_partition}") if not success: - unmount_target_system(mount_point) # 挂载失败,尝试清理 + _cleanup_partial_mount(mount_point, False, False, []) return False, "", f"挂载 /boot 分区失败: {stderr}" + mounted_boot = True # 3. 如果有EFI分区 (UEFI系统) if efi_partition: efi_mount_point = os.path.join(mount_point, "boot/efi") if not os.path.exists(efi_mount_point): os.makedirs(efi_mount_point) - success, _, stderr = run_command(["sudo", "mount", efi_partition, efi_mount_point], f"挂载 EFI 分区 {efi_partition}") + success, _, stderr = run_command(["sudo", "mount", efi_partition, efi_mount_point], + f"挂载 EFI 分区 {efi_partition}") if not success: - unmount_target_system(mount_point) # 挂载失败,尝试清理 + _cleanup_partial_mount(mount_point, mounted_boot, False, []) return False, "", f"挂载 EFI 分区失败: {stderr}" + mounted_efi = True # 4. 绑定必要的伪文件系统 bind_paths = ["/dev", "/dev/pts", "/proc", "/sys", "/run"] @@ -136,63 +298,108 @@ def mount_target_system(root_partition, boot_partition=None, efi_partition=None) target_path = os.path.join(mount_point, path.lstrip('/')) if not os.path.exists(target_path): os.makedirs(target_path) - success, _, stderr = run_command(["sudo", "mount", "--bind", path, target_path], f"绑定 {path} 到 {target_path}") + success, _, stderr = run_command(["sudo", "mount", "--bind", path, target_path], + f"绑定 {path} 到 {target_path}") if not success: - unmount_target_system(mount_point) # 绑定失败,尝试清理 + _cleanup_partial_mount(mount_point, mounted_boot, mounted_efi, bind_paths_mounted) return False, "", f"绑定 {path} 失败: {stderr}" + bind_paths_mounted.append(target_path) return True, mount_point, "" -def detect_distro_type(mount_point): + +def detect_distro_type(mount_point: str) -> str: """ 尝试检测目标系统发行版类型。 :param mount_point: 目标系统根分区的挂载点 - :return: "arch", "centos", "debian", "ubuntu", "unknown" + :return: "arch", "centos", "debian", "ubuntu", "fedora", "opensuse", "unknown" """ os_release_path = os.path.join(mount_point, "etc/os-release") if not os.path.exists(os_release_path): + # 尝试 /etc/issue 作为备选 + issue_path = os.path.join(mount_point, "etc/issue") + if os.path.exists(issue_path): + try: + with open(issue_path, "r") as f: + content = f.read().lower() + if "ubuntu" in content: + return "ubuntu" + elif "debian" in content: + return "debian" + elif "centos" in content or "rhel" in content: + return "centos" + elif "fedora" in content: + return "fedora" + except Exception: + pass return "unknown" try: with open(os_release_path, "r") as f: content = f.read() - if "ID=arch" in content: - return "arch" - elif "ID=centos" in content or "ID=rhel" in content: - return "centos" - elif "ID=debian" in content: - return "debian" - elif "ID=ubuntu" in content: - return "ubuntu" - else: - return "unknown" + + # 解析 ID 和 ID_LIKE 字段 + id_match = re.search(r'^ID=(.+)$', content, re.MULTILINE) + id_like_match = re.search(r'^ID_LIKE=(.+)$', content, re.MULTILINE) + + distro_id = id_match.group(1).strip('"\'') if id_match else "" + id_like = id_like_match.group(1).strip('"\'') if id_like_match else "" + + # 直接匹配 + if distro_id == "ubuntu": + return "ubuntu" + elif distro_id == "debian": + return "debian" + elif distro_id in ["arch", "manjaro", "endeavouros"]: + return "arch" + elif distro_id in ["centos", "rhel", "rocky", "almalinux"]: + return "centos" + elif distro_id == "fedora": + return "fedora" + elif distro_id in ["opensuse", "opensuse-leap", "opensuse-tumbleweed"]: + return "opensuse" + + # 通过 ID_LIKE 推断 + if "ubuntu" in id_like: + return "ubuntu" + elif "debian" in id_like: + return "debian" + elif "arch" in id_like: + return "arch" + elif "rhel" in id_like or "centos" in id_like or "fedora" in id_like: + return "centos" # 使用centos命令集 + elif "suse" in id_like: + return "opensuse" + + return "unknown" except Exception as e: print(f"[Backend] 无法读取os-release文件: {e}") return "unknown" -def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type="unknown"): + +def chroot_and_repair_grub(mount_point: str, target_disk: str, + is_uefi: bool = False, distro_type: str = "unknown") -> Tuple[bool, str]: """ Chroot到目标系统并执行GRUB修复命令。 :param mount_point: 目标系统根分区的挂载点 - :param target_disk: GRUB要安装到的物理磁盘 (例如 /dev/sda) + :param target_disk: GRUB要安装到的物理磁盘 :param is_uefi: 目标系统是否使用UEFI启动 - :param distro_type: 目标系统发行版类型 (用于选择正确的GRUB命令) + :param distro_type: 目标系统发行版类型 :return: (True/False, error_message) """ + if not validate_device_path(target_disk): + return False, f"无效的目标磁盘路径: {target_disk}" + chroot_cmd_prefix = ["sudo", "chroot", mount_point] # 1. 安装GRUB到目标磁盘 if is_uefi: - # UEFI系统,需要确保EFI分区已挂载到 /boot/efi - # grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB - # 注意:某些系统可能需要指定 --removable 选项,以便在某些固件上更容易启动 - # 统一使用 --bootloader-id=GRUB,如果已存在会更新 success, _, stderr = run_command( - chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--bootloader-id=GRUB"], + chroot_cmd_prefix + ["grub-install", "--target=x86_64-efi", + "--efi-directory=/boot/efi", "--bootloader-id=GRUB"], "安装UEFI GRUB" ) else: - # BIOS系统,安装到MBR success, _, stderr = run_command( chroot_cmd_prefix + ["grub-install", target_disk], "安装BIOS GRUB" @@ -203,14 +410,30 @@ def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type= # 2. 更新GRUB配置文件 grub_update_cmd = [] if distro_type in ["debian", "ubuntu"]: - grub_update_cmd = ["update-grub"] # 这是一个脚本,实际是调用grub-mkconfig + grub_update_cmd = ["update-grub"] elif distro_type == "arch": grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"] elif distro_type == "centos": - grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] # CentOS 8/9 + # 检测实际存在的配置文件路径 + grub2_path = os.path.join(mount_point, "boot/grub2/grub.cfg") + grub_path = os.path.join(mount_point, "boot/grub/grub.cfg") + if os.path.exists(os.path.dirname(grub2_path)): + grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] + else: + grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub/grub.cfg"] + elif distro_type == "fedora": + grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] + elif distro_type == "opensuse": + grub_update_cmd = ["grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"] else: - # 尝试通用命令 - grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"] + # 尝试通用命令,先检测配置文件路径 + for cfg_path in ["/boot/grub/grub.cfg", "/boot/grub2/grub.cfg"]: + full_path = os.path.join(mount_point, cfg_path.lstrip('/')) + if os.path.exists(os.path.dirname(full_path)): + grub_update_cmd = ["grub-mkconfig", "-o", cfg_path] + break + else: + grub_update_cmd = ["grub-mkconfig", "-o", "/boot/grub/grub.cfg"] success, _, stderr = run_command( chroot_cmd_prefix + grub_update_cmd, @@ -221,7 +444,8 @@ def chroot_and_repair_grub(mount_point, target_disk, is_uefi=False, distro_type= return True, "" -def unmount_target_system(mount_point): + +def unmount_target_system(mount_point: str) -> Tuple[bool, str]: """ 卸载所有挂载的分区。 :param mount_point: 目标系统根分区的挂载点 @@ -231,17 +455,21 @@ def unmount_target_system(mount_point): success = True error_msg = "" - # 逆序卸载绑定 + # 按照挂载的逆序卸载: + # 挂载顺序: /dev, /dev/pts, /proc, /sys, /run, /boot/efi, /boot, 根分区 + # 卸载顺序: /run, /sys, /proc, /dev/pts, /dev, /boot/efi, /boot, 根分区 + + # 1. 卸载绑定路径(先卸载子目录) bind_paths = ["/run", "/sys", "/proc", "/dev/pts", "/dev"] - for path in reversed(bind_paths): # 逆序 + for path in bind_paths: target_path = os.path.join(mount_point, path.lstrip('/')) - if os.path.ismount(target_path): # 检查是否真的挂载了 + if os.path.ismount(target_path): s, _, stderr = run_command(["sudo", "umount", target_path], f"卸载绑定 {target_path}") if not s: success = False error_msg += f"卸载绑定 {target_path} 失败: {stderr}\n" - # 卸载 /boot/efi (如果存在) + # 2. 卸载 /boot/efi (如果存在) efi_mount_point = os.path.join(mount_point, "boot/efi") if os.path.ismount(efi_mount_point): s, _, stderr = run_command(["sudo", "umount", efi_mount_point], f"卸载 {efi_mount_point}") @@ -249,7 +477,7 @@ def unmount_target_system(mount_point): success = False error_msg += f"卸载 {efi_mount_point} 失败: {stderr}\n" - # 卸载 /boot (如果存在) + # 3. 卸载 /boot (如果存在且是独立挂载的) boot_mount_point = os.path.join(mount_point, "boot") if os.path.ismount(boot_mount_point): s, _, stderr = run_command(["sudo", "umount", boot_mount_point], f"卸载 {boot_mount_point}") @@ -257,25 +485,25 @@ def unmount_target_system(mount_point): success = False error_msg += f"卸载 {boot_mount_point} 失败: {stderr}\n" - # 卸载根分区 + # 4. 卸载根分区 if os.path.ismount(mount_point): s, _, stderr = run_command(["sudo", "umount", mount_point], f"卸载根分区 {mount_point}") if not s: success = False error_msg += f"卸载根分区 {mount_point} 失败: {stderr}\n" - # 清理临时挂载点目录 + # 5. 清理临时挂载点目录 if os.path.exists(mount_point) and not os.path.ismount(mount_point): try: - os.rmdir(mount_point) + shutil.rmtree(mount_point) print(f"[Backend] 清理临时目录 {mount_point}") except OSError as e: print(f"[Backend] 无法删除临时目录 {mount_point}: {e}") error_msg += f"无法删除临时目录 {mount_point}: {e}\n" - return success, error_msg + # 示例:如果直接运行backend.py,可以进行一些测试 if __name__ == "__main__": print("--- 运行后端测试 ---") diff --git a/frontend.py b/frontend.py index 5de3f97..d643950 100644 --- a/frontend.py +++ b/frontend.py @@ -3,6 +3,8 @@ import tkinter as tk from tkinter import ttk, messagebox import threading +import os +import re import backend # 导入后端逻辑 class GrubRepairApp: @@ -153,18 +155,31 @@ class GrubRepairApp: self.selected_root_partition_info = self.all_partitions_data.get(selected_display) if self.selected_root_partition_info: - self.log_message(f"已选择根分区: {self.selected_root_partition_info['name']}", "info") - # 尝试根据根分区所在的磁盘自动选择目标磁盘 - # /dev/sda1 -> /dev/sda - disk_name_from_root = "/".join(self.selected_root_partition_info['name'].split('/')[:3]) - if disk_name_from_root in self.target_disk_combo['values']: - self.target_disk_combo.set(disk_name_from_root) - self.selected_target_disk_info = self.all_disks_data.get(disk_name_from_root) - self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info") - else: + root_part_name = self.selected_root_partition_info['name'] + self.log_message(f"已选择根分区: {root_part_name}", "info") + + # 检查是否是 LVM 逻辑卷 + is_lvm = self.selected_root_partition_info.get('is_lvm', False) + + if is_lvm: + # LVM 逻辑卷无法直接从路径推断父磁盘,提示用户手动选择 self.target_disk_combo.set("") self.selected_target_disk_info = None - self.log_message("无法自动选择目标磁盘,请手动选择。", "warning") + self.log_message("检测到 LVM 逻辑卷,请手动选择 GRUB 安装的目标磁盘。", "warning") + else: + # 尝试根据根分区所在的磁盘自动选择目标磁盘 + # /dev/sda1 -> /dev/sda + # /dev/nvme0n1p1 -> /dev/nvme0n1 + # /dev/mmcblk0p1 -> /dev/mmcblk0 + disk_name_from_root = re.sub(r'(\d+)?p?\d+$', '', root_part_name) + if disk_name_from_root in self.target_disk_combo['values']: + self.target_disk_combo.set(disk_name_from_root) + self.selected_target_disk_info = self.all_disks_data.get(disk_name_from_root) + self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info") + else: + self.target_disk_combo.set("") + self.selected_target_disk_info = None + self.log_message("无法自动选择目标磁盘,请手动选择。", "warning") else: self.selected_root_partition_info = None self.target_disk_combo.set("") @@ -215,71 +230,76 @@ class GrubRepairApp: repair_thread.start() def _run_repair_process(self): - root_part_path = self.selected_root_partition_info['name'] - boot_part_path = self.selected_boot_partition_info['name'] if self.selected_boot_partition_info else None - efi_part_path = self.selected_efi_partition_info['name'] if self.selected_efi_partition_info else None - target_disk_path = self.selected_target_disk_info['name'] + try: + root_part_path = self.selected_root_partition_info['name'] + boot_part_path = self.selected_boot_partition_info['name'] if self.selected_boot_partition_info else None + efi_part_path = self.selected_efi_partition_info['name'] if self.selected_efi_partition_info else None + target_disk_path = self.selected_target_disk_info['name'] - self.log_message(f"选择的根分区: {root_part_path}", "info") - self.log_message(f"选择的独立 /boot 分区: {boot_part_path if boot_part_path else '无'}", "info") - self.log_message(f"选择的EFI分区: {efi_part_path if efi_part_path else '无'}", "info") - self.log_message(f"GRUB安装目标磁盘: {target_disk_path}", "info") - self.log_message(f"UEFI模式: {'启用' if self.is_uefi_mode else '禁用'}", "info") + self.log_message(f"选择的根分区: {root_part_path}", "info") + self.log_message(f"选择的独立 /boot 分区: {boot_part_path if boot_part_path else '无'}", "info") + self.log_message(f"选择的EFI分区: {efi_part_path if efi_part_path else '无'}", "info") + self.log_message(f"GRUB安装目标磁盘: {target_disk_path}", "info") + self.log_message(f"UEFI模式: {'启用' if self.is_uefi_mode else '禁用'}", "info") - # 1. 挂载目标系统 - self.log_message("正在挂载目标系统分区...", "info") - mount_ok, self.mount_point, mount_err = backend.mount_target_system( - root_part_path, - boot_part_path, - efi_part_path if self.is_uefi_mode else None # 只有UEFI模式才挂载EFI分区 - ) - if not mount_ok: - self.log_message(f"挂载失败,修复中止: {mount_err}", "error") - messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}") - self.start_button.config(state="normal") - return + # 1. 挂载目标系统 + self.log_message("正在挂载目标系统分区...", "info") + mount_ok, self.mount_point, mount_err = backend.mount_target_system( + root_part_path, + boot_part_path, + efi_part_path if self.is_uefi_mode else None # 只有UEFI模式才挂载EFI分区 + ) + if not mount_ok: + self.log_message(f"挂载失败,修复中止: {mount_err}", "error") + self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}")) + return - # 2. 尝试识别发行版类型 - self.log_message("正在检测目标系统发行版...", "info") - distro_type = backend.detect_distro_type(self.mount_point) - self.log_message(f"检测到目标系统发行版: {distro_type}", "info") + # 2. 尝试识别发行版类型 + self.log_message("正在检测目标系统发行版...", "info") + distro_type = backend.detect_distro_type(self.mount_point) + self.log_message(f"检测到目标系统发行版: {distro_type}", "info") - # 3. Chroot并修复GRUB - self.log_message("正在进入Chroot环境并修复GRUB...", "info") - repair_ok, repair_err = backend.chroot_and_repair_grub( - self.mount_point, - target_disk_path, - self.is_uefi_mode, - distro_type - ) - if not repair_ok: - self.log_message(f"GRUB修复失败: {repair_err}", "error") - messagebox.showerror("修复失败", f"GRUB修复过程中发生错误。\n错误: {repair_err}") - # 即使失败,也要尝试卸载 - else: - self.log_message("GRUB修复命令执行成功!", "success") + # 3. Chroot并修复GRUB + self.log_message("正在进入Chroot环境并修复GRUB...", "info") + repair_ok, repair_err = backend.chroot_and_repair_grub( + self.mount_point, + target_disk_path, + self.is_uefi_mode, + distro_type + ) + if not repair_ok: + self.log_message(f"GRUB修复失败: {repair_err}", "error") + self.master.after(0, lambda: messagebox.showerror("修复失败", f"GRUB修复过程中发生错误。\n错误: {repair_err}")) + # 即使失败,也要尝试卸载 + else: + self.log_message("GRUB修复命令执行成功!", "success") - # 4. 卸载分区 - self.log_message("正在卸载目标系统分区...", "info") - unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point) - if not unmount_ok: - self.log_message(f"卸载分区失败: {unmount_err}", "error") - messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}") - else: - self.log_message("所有分区已成功卸载。", "success") + # 4. 卸载分区 + self.log_message("正在卸载目标系统分区...", "info") + unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point) + if not unmount_ok: + self.log_message(f"卸载分区失败: {unmount_err}", "error") + self.master.after(0, lambda: messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}")) + else: + self.log_message("所有分区已成功卸载。", "success") + + self.log_message("--- GRUB 修复过程结束 ---", "info") + + # 最终结果提示 + if repair_ok and unmount_ok: + self.master.after(0, lambda: messagebox.showinfo("修复成功", "GRUB引导修复已完成!请重启系统以验证。")) + elif repair_ok and not unmount_ok: + self.master.after(0, lambda: messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成,但部分分区卸载失败。请重启系统以验证。")) + else: + self.master.after(0, lambda: messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。")) - self.log_message("--- GRUB 修复过程结束 ---", "info") - - # 最终结果提示 - if repair_ok and unmount_ok: - messagebox.showinfo("修复成功", "GRUB引导修复已完成!请重启系统以验证。") - elif repair_ok and not unmount_ok: - messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成,但部分分区卸载失败。请重启系统以验证。") - else: - messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。") - - self.start_button.config(state="normal") + except Exception as e: + self.log_message(f"发生未预期的错误: {str(e)}", "error") + self.master.after(0, lambda err=str(e): messagebox.showerror("错误", f"修复过程中发生错误:\n{err}")) + finally: + # 确保按钮状态被恢复 + self.master.after(0, lambda: self.start_button.config(state="normal")) if __name__ == "__main__":