From 3eecfc4db06b31d37e0e1b8da1d89e14ba822212 Mon Sep 17 00:00:00 2001 From: zj <1052308357@qq.comm> Date: Mon, 2 Feb 2026 22:31:02 +0800 Subject: [PATCH] fix bug45 --- .qtcreator/pyproject.toml.user | 138 +++++++- __pycache__/dialogs.cpython-314.pyc | Bin 27948 -> 27998 bytes __pycache__/disk_operations.cpython-314.pyc | Bin 25274 -> 25600 bytes __pycache__/raid_operations.cpython-314.pyc | Bin 12211 -> 15559 bytes __pycache__/system_info.cpython-314.pyc | Bin 22581 -> 31357 bytes __pycache__/ui_form.cpython-314.pyc | Bin 9479 -> 9647 bytes dialogs.py | 28 +- disk_operations.py | 362 +++++++++++--------- form.ui | 20 +- mainwindow.py | 8 +- raid_operations.py | 139 ++++++-- system_info.py | 216 +++++++++--- ui_form.py | 30 +- 13 files changed, 651 insertions(+), 290 deletions(-) diff --git a/.qtcreator/pyproject.toml.user b/.qtcreator/pyproject.toml.user index f646c9c..effd638 100644 --- a/.qtcreator/pyproject.toml.user +++ b/.qtcreator/pyproject.toml.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -99,13 +99,15 @@ {2b4075d3-005b-46f6-8fd5-3bd8a94071db} 0 0 - 0 + 2 /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2 true Python.PysideBuildStep + /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-project + /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/pyside6-uic 1 构建 @@ -126,7 +128,7 @@ Python 3.14.2 Virtual Environment Python.PySideBuildConfiguration 0 - 0 + 2 0 @@ -224,7 +226,70 @@ /home/jing/qtpj/diskmanager :0 - 4 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + dialogs.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/dialogs.py + false + + /home/jing/qtpj/diskmanager/dialogs.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + raid_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/raid_operations.py + false + + /home/jing/qtpj/diskmanager/raid_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + lvm_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/lvm_operations.py + false + + /home/jing/qtpj/diskmanager/lvm_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + 7 /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2/bin/python /home/jing/qtpj/diskmanager/.qtcreator/Python_3_14_2venv_2 @@ -326,7 +391,70 @@ /home/jing/qtpj/diskmanager :0 - 4 + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + dialogs.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/dialogs.py + false + + /home/jing/qtpj/diskmanager/dialogs.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + raid_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/raid_operations.py + false + + /home/jing/qtpj/diskmanager/raid_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + + true + true + 0 + true + + 2 + + false + -e cpu-cycles --call-graph dwarf,4096 -F 250 + lvm_operations.py + PythonEditor.RunConfiguration. + /home/jing/qtpj/diskmanager/lvm_operations.py + false + + /home/jing/qtpj/diskmanager/lvm_operations.py + true + true + /home/jing/qtpj/diskmanager + :0 + + 7 diff --git a/__pycache__/dialogs.cpython-314.pyc b/__pycache__/dialogs.cpython-314.pyc index 5f4a612b225ebdc425b1d04ce5c05ca41bc4af18..9a467ca858b74ac1beffbab25542bc73d1e54957 100644 GIT binary patch delta 2420 zcmai0YiyHM81DJLc9eC!47Tppj=>7$TLHT90O!q*R>y`(3Nu9!6aOU z7y@`<_=hJE&P-NAzk&zQ(4A}N+pm&CLt#?odbp8X0TekjaoAU-9U zqY5OVc=C6Nh6vg>AW%5nIQDGeVl8Umzm#frFxk|_u z*k6|iHn)>(gBx|(%1ZYf-0jEZHRLT&%S*`Hpqo}g-T^nmcVQL7eXwg~WgHJ$o`0k5)hdVx^cg8{ji+?^|mi%EsXB_0<%p65w=7CncPbPqc1 zf%_G1vIn{ylazUtnF6^0o_re|tskLmttt}8Cvdc;h+G_ycFft@o|Lk+T-7g_pk1@d zi!s-dmR`h&tq6lsNJ8qIMBw{bJ6#vh_T-3g|8tCc-afuT|Kav=9-Yp^rP*$Bp6$b> zOe=`1nuEWWA1)Z+Irp0b@g~^zwTweUOkd{QYU)fPV zMr-&H+bY6s4k6wH1CcjnHQNu~~!-tEa=U zrkSwRn*$G;3bYY%c`+E8_X<+!PhNv8p{V_S<-Po=gI(%K;XmGRaZEsUE{hgG`;h480I5F%E1v`Q< z)#MNPIwH&&9oyTYIu>q15&Kf*?H-i*CX;$-^_At@$0lv=xXqokRm5!-3ERvY`n-OF zwa-ujpZiKn|4h^5+YdD)?WJ*hY0_R9w^t_YRTowz^5*Y0_3JZ|dRts?OX{6*y|dTU zr=J0&%~?9-Rz~H6G>s)IX)cbNi<4$4Zk7^eS8uq_T;0!Tiz9AvBrTKUmdU;0Tb3Hw z)wY(DL1gs|AtJ!1O)jHQPs*_*vo9iodtr4>qc})t8%dKXWz>L0%pO^>f@P;tqp?A0 zQ4Wk3t?;Yg2z!@hv5by}C37>xdTsH5EgSYWo4BpkI~s}tBh$5fID0h)hPPya$7qEm z3mq9_G;ARMlXTD*2yUmC$Kg5{#@HJ z<6&bmSBmFKy{^;IL~hk?<8yM^oXA_a+XP?AZY|~w9?DMbwmx$WoNAjOSZbAq)!VS- zB-We=@G6md_u1@B0)9z%vNq*4h~Ldt>~ab401r9TgGGlnFvGxmL?5Elkic*^SVhh# z_O&BP9lzmEDmb5wN9H$$&xY4Z5a3C z=OK?OEDRq{%+sYm(lDUq+|EGCrJeB4x=n8W9|M2m_ycj}KZoO*lyey)RDE^F`aow$ XuBDIRvrt#&7DCc{mj9)}U+(rV>$q+2 delta 2146 zcmai#e@s(X6vuntqgYxf)XGYMVHHMwrL>(=9aEvA5%LzIb>&K4{-h_{`%KUADQ zwhFb!4cF0QZN^G{>sHVlkTjQ!W7N?Ou8bh}LA`8*U zpz34sAdScRBk>KANZn|E6R`_)#pCn%h(-9m5HYcsB>3rkW|0guj2&HwZo~#Km6*%k z!e#`~gV>0OGQ-Uo>uuTb z9-B_p(qM0Qm>%No^_yPrjng4q#*Z)|UdFEUU|(OjH$-=%WkM>-!wRB<7!CKT(`dLa z8eAV%#R!{+hr5b(N?NX>N+Nu2RZoMFP(W^*Um#Q-#0j~a+?{KR!K9|f5Kjnx|C2 zB_48~xw4UK(Exo67MR$@+J7PPm?i$u2ac4icX`@-{c{qOh&7GscA$=zlPT9 z^3LX&X$BSBHxM`FQ>{xxazp;T?J~*dred^n@7vgNAj;SwoB=~({1#BG7YCbQ6Pg2sX(xYJ&!Zxt%=CSVU_bu0%O zs0CY~0QQB9kP)!J&?+A*%h7rv^cyh>GUa1U(#8pIO`J5hAS%) zbJmRM+TfF*-CUA%R4I&2oXshR{`E%qE$6!ou~^g0eo`8I6pB=~ z)cPgecU?iGP{(|qLTTPS@{ejhgYw({B{~rq9CMW;^ diff --git a/__pycache__/disk_operations.cpython-314.pyc b/__pycache__/disk_operations.cpython-314.pyc index 5c5d56a6639c73060d5f3dd0a56eb1a1d13e45c1..9ce4637cf64e8d1048755dc0d79631ed72da19ea 100644 GIT binary patch delta 8572 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 AzW@LL delta 8245 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 diff --git a/__pycache__/system_info.cpython-314.pyc b/__pycache__/system_info.cpython-314.pyc index f892df5db729bd1552b04ee9883ab4479d635a76..fcb119b3dc2f2983bb8bebf5e489f82c8faa4080 100644 GIT binary patch delta 10270 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 XxHX6Vl)v3ts@NSy$NqsrDyaG&MTF2p delta 3109 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 XJ*18A1NV_FZdC;(K>Ha(kx%t6El@%^ diff --git a/__pycache__/ui_form.cpython-314.pyc b/__pycache__/ui_form.cpython-314.pyc index 332f16e4f21713c2a67fcd53aac8dd0b1977e41a..e79d5a713e3f234965b27ce306b996019f4764c6 100644 GIT binary patch delta 1659 zcmZ`(O>7%g5cXSp?Oofuo3LL0tbY@yj+2IvCJt_$;KWTRDox1dhg2Xp#njs-s7chV z0|hR*KskUEbyO}~0;eDWNtHv@Udjz54%DipMLtwHwC(|f5)MT`5WKgJ6IW{YF#GMi zneUtTcII7ub^R1~nCI*!{P@95)L>4(__@T@_P;*g<(oK!8<*iAc(v)z0)Sm%nvCHq;_ zUMKRmJqW+c?po~|tU$@chN8#z^-*oB_}J^KN|9q1#M-KH&cr9SzEsOScxMM{c*Zninm{w5gavjS zBFYd&l!6;UC;F^o>amt0fzXP$zH&jr774#!9> zO?eFvKN4wpSJeu0=W-WEa_=uL70z~-IZZvUsh4MuFBJ-l3y|f*0{>(Xe9C_SlF)|# z(|~X^6~&ZVH#TK9m(QP`%U@6nT8ub*h)EFBOH7=Y7qKZ@ao|+$ZTLoz;U^*BuwPaS zlLbvJoSTJ;@EavqpwuwMMGmhG>mBK0NBUl509G6QkP|!H!6FmXnbsoHx@OVisbV}; zV)`C{(m2G0+SXe1_Q7KN;Jr`=cAt~scH{GszsUG?=1`G2w3^prL&exoi5cG9Z5&5p zJ=Z$_HDAI_{gs$GUhmiA>0&%xVg|Mtr_NMY;zcID*0`1_F`d6W-Mjgh_y&S)R}`7y z`+y5iuwplQmBY;~!lIv{scA}K;Vmf%k0dW*VFRP28>4IPBwQr`6pVBOtzy*bA--*l z-ZRjkCka0oXaysWm#l7KbiqJf-Xz>K&@x6gA6dPN(P^LCOoSu8Bp}&~cv2R8>vLDp z-gvx&edI#8?~B4mvbWkt2eq50&3hCt_@70AZzUuBOu&oCbtIx%z7v;1 zv|*XF7;ZTdQMS!3pAY+5;Wo<)K^m?ra2%|%X^~5#Iv*vONVJ4xxWM`0fh@z4gGFx2DTKrM zZ&)O=J#B(_Sp{CRrjY}XB?voK*_ z4Ril42kYK!jF?frk2dTbK9a~79YFdPa9^4Y zmdW>%zKD|UrEMpibl`v<_3%BB)7O*CBvU|>!L+5$?9!wBLHcw0;l9wEoEX$4#ob$o z8!VF_B3po)f(s2(;Sd%^(UahD#T$S7D2(wEJm=z_$8=qO6c${gqU+^~TAi6LogOWn zn_sNL9aq%j-pft(fKzfhi7AmSSg?Db@|RCa^Q-CbMpJ&Rx> zARs60;Ol`TC;=WmmK|nOzRrDvxs^bu5upT=aNj_)7!3w>-`5yjFpv;R!uJMx9;5D% z9{D*&Zy4xrj5GrsZ%RVEsUGQrPnr_2&_ELWihVZ>lnW=p6Ar+BK7d3z(iYwa%i#dM zSHiFpPCzLfLiCn^pZpU3Qafudnj2q6^iDZM56d1s{N<3T`aSIZg7q0%OVRSb+OcMG zVmn}0~)hRZ8WGdbC=Z-L##pRL3i?RMj516=~|AgE)Ydbk@`?S?wo=oWFgndV;LGu-S}3 zcQgiF(J|{u!i~UN(HNS7OVLNx9z=OF`y0d9kHdO2$$rPps|64f{v1i7K0(aGsi)b= zVAH9uSEL*0CLBEz 0 else 0.01) # 至少0.01GB else: self.lv_size_spinbox.setMinimum(0.1) # 恢复正常最小值 - self.lv_size_spinbox.setMaximum(max(self.lv_size_spinbox.minimum(), max_size_gb)) # 设置最大值,确保不小于最小值 + # 计算并设置 spinbox 的最大值 + clamped_max_gb = max(self.lv_size_spinbox.minimum(), max_size_gb) + self.lv_size_spinbox.setMaximum(clamped_max_gb) # 如果选中了“使用最大可用空间”,则将 spinbox 值设置为最大值 if self.use_max_space_checkbox.isChecked(): - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) + self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值 else: # 如果当前值超过了新的最大值,则调整为新的最大值 - if self.lv_size_spinbox.value() > self.lv_size_spinbox.maximum(): - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) + if self.lv_size_spinbox.value() > clamped_max_gb: + self.lv_size_spinbox.setValue(clamped_max_gb) # 如果当前值小于新的最小值,则调整 elif self.lv_size_spinbox.value() < self.lv_size_spinbox.minimum(): self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum()) - def _toggle_size_input(self, state): """根据“使用最大可用空间”复选框的状态切换大小输入框的启用/禁用状态。""" + # 获取当前选中的卷组的实际最大可用空间(GB) + selected_vg = self.vg_combo_box.currentText() + actual_max_gb = self.vg_sizes.get(selected_vg, 0.0) + + # 确保计算出的最大值不小于spinbox的最小值 + clamped_max_gb = max(self.lv_size_spinbox.minimum(), actual_max_gb) + if state == Qt.Checked: self.lv_size_spinbox.setDisabled(True) - self.lv_size_spinbox.setValue(self.lv_size_spinbox.maximum()) # 设置为最大值 + self.lv_size_spinbox.setValue(clamped_max_gb) # 使用计算出的最大值 else: self.lv_size_spinbox.setDisabled(False) # 如果之前是最大值,取消勾选后,恢复到最小值或一个合理值 - if self.lv_size_spinbox.value() == self.lv_size_spinbox.maximum(): + if self.lv_size_spinbox.value() == clamped_max_gb: # 比较时也使用计算出的最大值 self.lv_size_spinbox.setValue(self.lv_size_spinbox.minimum()) + # 否则,保持当前值(用户手动输入的值) def get_lv_info(self): diff --git a/disk_operations.py b/disk_operations.py index 803ad09..6c4007d 100644 --- a/disk_operations.py +++ b/disk_operations.py @@ -3,12 +3,13 @@ import logging import re import os from PySide6.QtWidgets import QMessageBox, QInputDialog +from system_info import SystemInfoManager logger = logging.getLogger(__name__) class DiskOperations: - def __init__(self): - pass + def __init__(self, system_manager: SystemInfoManager): # NEW: 接收 system_manager 实例 + self.system_manager = system_manager def _execute_shell_command(self, command_list, error_message, root_privilege=True, suppress_critical_dialog_on_stderr_match=None, input_data=None): @@ -18,6 +19,7 @@ class DiskOperations: :param error_message: 命令失败时显示给用户的错误消息。 :param root_privilege: 如果为 True,则使用 sudo 执行命令。 :param suppress_critical_dialog_on_stderr_match: 如果 stderr 包含此字符串,则不显示关键错误对话框。 + 可以是字符串或字符串元组。 :param input_data: 传递给命令stdin的数据 (str)。 :return: (True/False, stdout_str, stderr_str) """ @@ -50,10 +52,17 @@ class DiskOperations: logger.error(f"标准输出: {e.stdout.strip()}") logger.error(f"标准错误: {stderr_output}") - if suppress_critical_dialog_on_stderr_match and \ - (suppress_critical_dialog_on_stderr_match in stderr_output or \ - (isinstance(suppress_critical_dialog_on_stderr_match, tuple) and \ - any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match))): + # Determine if the error dialog should be suppressed by this function + should_suppress_dialog_here = False + if suppress_critical_dialog_on_stderr_match: + if isinstance(suppress_critical_dialog_on_stderr_match, str): + if suppress_critical_dialog_on_stderr_match in stderr_output: + should_suppress_dialog_here = True + elif isinstance(suppress_critical_dialog_on_stderr_match, tuple): + if any(s in stderr_output for s in suppress_critical_dialog_on_stderr_match): + should_suppress_dialog_here = True + + if should_suppress_dialog_here: logger.info(f"错误信息 '{stderr_output}' 匹配抑制条件,不显示关键错误对话框。") else: QMessageBox.critical(None, "错误", f"{error_message}\n错误详情: {stderr_output}") @@ -68,162 +77,173 @@ class DiskOperations: return False, "", str(e) def _add_to_fstab(self, device_path, mount_point, fstype, uuid): - """ - 将设备的挂载信息添加到 /etc/fstab。 - 使用 UUID 识别设备以提高稳定性。 - """ - if not uuid or not mount_point or not fstype: - logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") - QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") - return False + """ + 将设备的挂载信息添加到 /etc/fstab。 + 使用 UUID 识别设备以提高稳定性。 + """ + if not uuid or not mount_point or not fstype: + logger.error(f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + QMessageBox.warning(None, "警告", f"无法将设备 {device_path} 添加到 fstab:缺少 UUID/挂载点/文件系统类型。") + return False - fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" - fstab_path = "/etc/fstab" + fstab_entry = f"UUID={uuid} {mount_point} {fstype} defaults 0 2" + fstab_path = "/etc/fstab" - try: - # 检查 fstab 中是否已存在相同 UUID 或挂载点的条目 - with open(fstab_path, 'r') as f: - fstab_content = f.readlines() + try: + # 检查 fstab 中是否已存在相同 UUID 的条目 + # 使用 _execute_shell_command 来读取 fstab,尽管通常不需要 sudo + # 这里为了简化,直接读取文件,但写入时使用 _execute_shell_command + with open(fstab_path, 'r') as f: + fstab_content = f.readlines() - for line in fstab_content: - if f"UUID={uuid}" in line: - logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。") - QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。") - return True # 认为成功,因为目标已达成 - # 检查挂载点是否已存在 (可能被其他设备使用) - if mount_point in line.split(): - logger.warning(f"挂载点 {mount_point} 已存在于 fstab 中。跳过添加。") - QMessageBox.information(None, "信息", f"挂载点 {mount_point} 已存在于 fstab 中。") - return True # 认为成功,因为目标已达成 + for line in fstab_content: + if f"UUID={uuid}" in line: + logger.warning(f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。跳过添加。") + QMessageBox.information(None, "信息", f"设备 {device_path} (UUID={uuid}) 已存在于 fstab 中。") + return True # 认为成功,因为目标已达成 - # 如果不存在,则追加到 fstab - with open(fstab_path, 'a') as f: - f.write(fstab_entry + '\n') - logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。") - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。") - return True - except Exception as e: - logger.error(f"写入 {fstab_path} 失败: {e}") - QMessageBox.critical(None, "错误", f"写入 {fstab_path} 失败: {e}") - return False + # 如果不存在,则追加到 fstab + # 使用 _execute_shell_command for sudo write + success, _, stderr = self._execute_shell_command( + ["sh", "-c", f"echo '{fstab_entry}' >> {fstab_path}"], + f"将 {device_path} 添加到 {fstab_path} 失败", + root_privilege=True + ) + if success: + logger.info(f"已将 {fstab_entry} 添加到 {fstab_path}。") + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功添加到 /etc/fstab。") + return True + else: + return False # Error handled by _execute_shell_command + except Exception as e: + logger.error(f"处理 {fstab_path} 失败: {e}") + QMessageBox.critical(None, "错误", f"处理 {fstab_path} 失败: {e}") + return False def _remove_fstab_entry(self, device_path): - """ - 从 /etc/fstab 中移除指定设备的条目。 - 此方法需要 SystemInfoManager 来获取设备的 UUID。 - """ - success, stdout, stderr = self._execute_shell_command( - ["lsblk", "-no", "UUID", device_path], - f"获取设备 {device_path} 的 UUID 失败", - root_privilege=False, # lsblk 通常不需要 sudo - suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") # 匹配中英文错误 - ) - if not success: - logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。错误: {stderr}") - return False + """ + 从 /etc/fstab 中移除指定设备的条目。 + 此方法需要 SystemInfoManager 来获取设备的 UUID。 + """ + # NEW: 使用 system_manager 获取 UUID,它会处理路径解析 + device_details = self.system_manager.get_device_details_by_path(device_path) + if not device_details or not device_details.get('uuid'): + logger.warning(f"无法获取设备 {device_path} 的 UUID,无法从 fstab 中移除。") + return False + uuid = device_details.get('uuid') - uuid = stdout.strip() - if not uuid: - logger.warning(f"设备 {device_path} 没有 UUID,无法从 fstab 中移除。") - return False - - fstab_path = "/etc/fstab" - try: - with open(fstab_path, 'r') as f: - lines = f.readlines() - - new_lines = [] - removed = False - for line in lines: - if f"UUID={uuid}" in line: - logger.info(f"从 {fstab_path} 中移除了条目: {line.strip()}") - removed = True - else: - new_lines.append(line) - - if removed: - # 写入临时文件,然后替换原文件 - with open(fstab_path + ".tmp", 'w') as f_tmp: - f_tmp.writelines(new_lines) - os.rename(fstab_path + ".tmp", fstab_path) - logger.info(f"已从 {fstab_path} 中移除 UUID={uuid} 的条目。") + fstab_path = "/etc/fstab" + # Use sed for robust removal with sudo + # suppress_critical_dialog_on_stderr_match is added to handle cases where fstab might not exist + # or sed reports no changes, which is not a critical error for removal. + command = ["sed", "-i", f"/UUID={uuid}/d", fstab_path] + success, _, stderr = self._execute_shell_command( + command, + f"从 {fstab_path} 中删除 UUID={uuid} 的条目失败", + root_privilege=True, + suppress_critical_dialog_on_stderr_match=( + f"sed: {fstab_path}: No such file or directory", # English + f"sed: {fstab_path}: 没有那个文件或目录", # Chinese + "no changes were made" # if sed finds nothing to delete + ) + ) + if success: + logger.info(f"已从 {fstab_path} 中删除 UUID={uuid} 的条目。") return True else: - logger.info(f"fstab 中未找到 UUID={uuid} 的条目。") - return False - except Exception as e: - logger.error(f"修改 {fstab_path} 失败: {e}") - QMessageBox.critical(None, "错误", f"修改 {fstab_path} 失败: {e}") - return False + # If sed failed, it might be because the entry wasn't found, which is fine. + # _execute_shell_command would have suppressed the dialog if it matched the suppress_critical_dialog_on_stderr_match. + # So, if we reach here, it's either a real error (dialog shown by _execute_shell_command) + # or a suppressed "no changes" type of error. In both cases, if no real error, we return True. + if any(s in stderr for s in ( + f"sed: {fstab_path}: No such file or directory", + f"sed: {fstab_path}: 没有那个文件或目录", + "no changes were made", + "No such file or directory" # more general check + )): + logger.info(f"fstab 中未找到 UUID={uuid} 的条目,无需删除。") + return True # Consider it a success if the entry is not there + return False # Other errors are already handled by _execute_shell_command def mount_partition(self, device_path, mount_point, add_to_fstab=False): - """ - 挂载指定设备到指定挂载点。 - :param device_path: 要挂载的设备路径。 - :param mount_point: 挂载点。 - :param add_to_fstab: 是否添加到 /etc/fstab。 - :return: True 如果成功,否则 False。 - """ - if not os.path.exists(mount_point): - try: - os.makedirs(mount_point) - logger.info(f"创建挂载点目录: {mount_point}") - except OSError as e: - QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}") - logger.error(f"创建挂载点目录 {mount_point} 失败: {e}") + """ + 挂载指定设备到指定挂载点。 + :param device_path: 要挂载的设备路径。 + :param mount_point: 挂载点。 + :param add_to_fstab: 是否添加到 /etc/fstab。 + :return: True 如果成功,否则 False。 + """ + if not os.path.exists(mount_point): + try: + os.makedirs(mount_point) + logger.info(f"创建挂载点目录: {mount_point}") + except OSError as e: + QMessageBox.critical(None, "错误", f"创建挂载点目录 {mount_point} 失败: {e}") + logger.error(f"创建挂载点目录 {mount_point} 失败: {e}") + return False + + logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") + success, _, stderr = self._execute_shell_command( + ["mount", device_path, mount_point], + f"挂载设备 {device_path} 失败" + ) + if success: + QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") + if add_to_fstab: + # NEW: 使用 self.system_manager 获取 fstype 和 UUID + device_details = self.system_manager.get_device_details_by_path(device_path) + if device_details: + fstype = device_details.get('fstype') + uuid = device_details.get('uuid') + if fstype and uuid: + self._add_to_fstab(device_path, mount_point, fstype, uuid) + else: + logger.error(f"无法获取设备 {device_path} 的文件系统类型或 UUID 以添加到 fstab。") + QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取文件系统类型或 UUID 以添加到 fstab。") + else: + logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab。") + QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。") + return True + else: return False - logger.info(f"尝试挂载设备 {device_path} 到 {mount_point}。") - success, _, stderr = self._execute_shell_command( - ["mount", device_path, mount_point], - f"挂载设备 {device_path} 失败" - ) - if success: - QMessageBox.information(None, "成功", f"设备 {device_path} 已成功挂载到 {mount_point}。") - if add_to_fstab: - # 为了获取 fstype 和 UUID,需要再次调用 lsblk - success_details, stdout_details, stderr_details = self._execute_shell_command( - ["lsblk", "-no", "FSTYPE,UUID", device_path], - f"获取设备 {device_path} 的文件系统类型和 UUID 失败", - root_privilege=False, - suppress_critical_dialog_on_stderr_match=("找不到或无法访问", "No such device or address") - ) - if success_details: - fstype, uuid = stdout_details.strip().split() - self._add_to_fstab(device_path, mount_point, fstype, uuid) - else: - logger.error(f"无法获取设备 {device_path} 的详细信息以添加到 fstab: {stderr_details}") - QMessageBox.warning(None, "警告", f"设备 {device_path} 已挂载,但无法获取详细信息以添加到 fstab。") - return True - else: - return False - def unmount_partition(self, device_path, show_dialog_on_error=True): - """ - 卸载指定设备。 - :param device_path: 要卸载的设备路径。 - :param show_dialog_on_error: 是否在发生错误时显示对话框。 - :return: True 如果成功,否则 False。 - """ - logger.info(f"尝试卸载设备 {device_path}。") - try: + """ + 卸载指定设备。 + :param device_path: 要卸载的设备路径。 + :param show_dialog_on_error: 是否在发生错误时显示对话框。 + :return: True 如果成功,否则 False。 + """ + logger.info(f"尝试卸载设备 {device_path}。") + # 定义表示设备已未挂载的错误信息(中英文) + # 增加了 "未指定挂载点" + already_unmounted_errors = ("not mounted", "未挂载", "未指定挂载点") + + # 调用 _execute_shell_command,并告诉它在遇到“已未挂载”错误时,不要弹出其自身的关键错误对话框。 success, _, stderr = self._execute_shell_command( ["umount", device_path], f"卸载设备 {device_path} 失败", - suppress_critical_dialog_on_stderr_match="not mounted" if not show_dialog_on_error else None + suppress_critical_dialog_on_stderr_match=already_unmounted_errors ) + if success: + # 如果命令成功执行,则卸载成功。 QMessageBox.information(None, "成功", f"设备 {device_path} 已成功卸载。") return True else: - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 失败。\n错误详情: {stderr}") - return False - except Exception as e: - logger.error(f"卸载设备 {device_path} 时发生异常: {e}") - if show_dialog_on_error: - QMessageBox.critical(None, "错误", f"卸载设备 {device_path} 时发生异常: {e}") - return False + # 如果命令失败,检查是否是因为设备已经未挂载或挂载点未指定。 + is_already_unmounted_error = any(s in stderr for s in already_unmounted_errors) + + if is_already_unmounted_error: + logger.info(f"设备 {device_path} 已经处于未挂载状态(或挂载点未指定),无需重复卸载。") + # 这种情况下,操作结果符合预期(设备已是未挂载),不弹出对话框,并返回 True。 + return True + else: + # 对于其他类型的卸载失败(例如设备忙、权限不足等), + # _execute_shell_command 应该已经弹出了关键错误对话框 + # (因为它没有匹配到 `already_unmounted_errors` 进行抑制)。 + # 所以,这里我们不需要再次弹出对话框,直接返回 False。 + return False def get_disk_free_space_info_mib(self, disk_path, total_disk_mib): """ @@ -234,13 +254,19 @@ class DiskOperations: 如果磁盘有分区表但没有空闲空间,返回 (None, None)。 """ logger.debug(f"尝试获取磁盘 {disk_path} 的空闲空间信息 (MiB)。") + # suppress_critical_dialog_on_stderr_match is added to handle cases where parted might complain about + # an unrecognized disk label, which is expected for a fresh disk. success, stdout, stderr = self._execute_shell_command( ["parted", "-s", disk_path, "unit", "MiB", "print", "free"], f"获取磁盘 {disk_path} 分区信息失败", - root_privilege=True + root_privilege=True, + suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label") ) if not success: + # If parted failed and it wasn't due to an unrecognized label (handled by suppress_critical_dialog_on_stderr_match), + # then _execute_shell_command would have shown a dialog. + # We just log and return None. logger.error(f"获取磁盘 {disk_path} 空闲空间信息失败: {stderr}") return None, None @@ -294,23 +320,31 @@ class DiskOperations: # 1. 检查磁盘是否有分区表 has_partition_table = False - try: - success_check, stdout_check, stderr_check = self._execute_shell_command( - ["parted", "-s", disk_path, "print"], - f"检查磁盘 {disk_path} 分区表失败", - suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"), - root_privilege=True - ) - if success_check: - if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check: - has_partition_table = True - else: - logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。") + # Use suppress_critical_dialog_on_stderr_match for "unrecognized disk label" when checking + success_check, stdout_check, stderr_check = self._execute_shell_command( + ["parted", "-s", disk_path, "print"], + f"检查磁盘 {disk_path} 分区表失败", + suppress_critical_dialog_on_stderr_match=("无法辨识的磁盘卷标", "unrecognized disk label"), + root_privilege=True + ) + # If success_check is False, it means _execute_shell_command encountered an error. + # If that error was "unrecognized disk label", it was suppressed, and we treat it as no partition table. + # If it was another error, a dialog was shown by _execute_shell_command, and we should stop. + if not success_check: + # Check if the failure was due to "unrecognized disk label" (which means no partition table) + if any(s in stderr_check for s in ("无法辨识的磁盘卷标", "unrecognized disk label")): + logger.info(f"磁盘 {disk_path} 没有可识别的分区表。") + has_partition_table = False # Explicitly set to False else: - logger.info(f"parted print 命令失败,但可能不是因为没有分区表,错误信息: {stderr_check}") - except Exception as e: - logger.error(f"检查磁盘 {disk_path} 分区表时发生异常: {e}") - pass + logger.error(f"检查磁盘 {disk_path} 分区表时发生非预期的错误: {stderr_check}") + return False # Other critical error, stop operation. + else: # success_check is True + if "Partition Table: unknown" not in stdout_check and "分区表:unknown" not in stdout_check: + has_partition_table = True + else: + logger.info(f"parted print 报告磁盘 {disk_path} 的分区表为 'unknown'。") + has_partition_table = False + actual_start_mib_for_parted = 0.0 @@ -374,12 +408,12 @@ class DiskOperations: create_cmd, f"在 {disk_path} 上创建分区失败" ) - if not success: + if success: + QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。") + return True + else: return False - QMessageBox.information(None, "成功", f"在 {disk_path} 上成功创建了 {size_for_log} 的分区。") - return True - def delete_partition(self, device_path): """ 删除指定分区。 @@ -394,9 +428,12 @@ class DiskOperations: return False # 尝试卸载分区 - self.unmount_partition(device_path, show_dialog_on_error=False) # 静默卸载 + # The unmount_partition method now handles "not mounted" gracefully without dialog and returns True. + # So, we just call it. + self.unmount_partition(device_path, show_dialog_on_error=False) # show_dialog_on_error=False means no dialog for other errors too. # 从 fstab 中移除条目 + # _remove_fstab_entry also handles "not found" gracefully. self._remove_fstab_entry(device_path) # 获取父磁盘和分区号 @@ -472,4 +509,3 @@ class DiskOperations: return True else: return False - diff --git a/form.ui b/form.ui index 4080348..7269374 100644 --- a/form.ui +++ b/form.ui @@ -14,7 +14,14 @@ Linux 存储管理工具 - + + + + + 刷新数据 + + + @@ -24,7 +31,7 @@ 块设备概览 - + @@ -130,13 +137,6 @@ - - - - 刷新数据 - - - @@ -152,7 +152,7 @@ 0 0 1000 - 23 + 30 diff --git a/mainwindow.py b/mainwindow.py index 394e7f9..58cfa40 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -35,7 +35,7 @@ class MainWindow(QMainWindow): # 初始化管理器和操作类 self.system_manager = SystemInfoManager() - self.disk_ops = DiskOperations() + self.disk_ops = DiskOperations(self.system_manager) self.raid_ops = RaidOperations() self.lvm_ops = LvmOperations() @@ -208,12 +208,12 @@ class MainWindow(QMainWindow): if dev_data.get('children'): logger.debug(f"磁盘 {disk_path} 存在现有分区,尝试计算下一个分区起始位置。") - calculated_start_mib = self.disk_ops.get_disk_next_partition_start_mib(disk_path) - if calculated_start_mib is None: + calculated_start_mib, largest_free_space_mib = self.disk_ops.get_disk_free_space_info_mib(disk_path, total_disk_mib) + if calculated_start_mib is None or largest_free_space_mib is None: QMessageBox.critical(self, "错误", f"无法确定磁盘 {disk_path} 的分区起始位置。") return start_position_mib = calculated_start_mib - max_available_mib = total_disk_mib - start_position_mib + max_available_mib = largest_free_space_mib if max_available_mib < 0: max_available_mib = 0.0 else: diff --git a/raid_operations.py b/raid_operations.py index 3d05cce..a8f4e54 100644 --- a/raid_operations.py +++ b/raid_operations.py @@ -39,17 +39,18 @@ class RaidOperations: return True, stdout, stderr except subprocess.CalledProcessError as e: + stderr_output = e.stderr.strip() # 获取标准错误输出 logger.error(f"{error_msg_prefix} 命令: {full_cmd_str}") logger.error(f"退出码: {e.returncode}") logger.error(f"标准输出: {e.stdout.strip()}") - logger.error(f"标准错误: {e.stderr.strip()}") + logger.error(f"标准错误: {stderr_output}") # 根据 suppress_critical_dialog_on_stderr_match 参数决定是否弹出错误对话框 - if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in e.stderr.strip(): + if suppress_critical_dialog_on_stderr_match and suppress_critical_dialog_on_stderr_match in stderr_output: logger.info(f"特定错误 '{suppress_critical_dialog_on_stderr_match}' 匹配,已抑制错误对话框。") else: - QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {e.stderr.strip()}") - return False, e.stdout, e.stderr + QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n详细信息: {stderr_output}") + return False, e.stdout, stderr_output # 返回 stderr_output except FileNotFoundError: logger.error(f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。") QMessageBox.critical(None, "错误", f"命令 '{command_list[0]}' 未找到。请确保已安装相关工具。") @@ -59,13 +60,50 @@ class RaidOperations: QMessageBox.critical(None, "错误", f"{error_msg_prefix}\n未知错误: {e}") return False, "", str(e) + def _get_next_available_md_device_name(self): + """ + 查找下一个可用的 /dev/mdX 设备名称(例如 /dev/md0, /dev/md1, ...)。 + """ + existing_md_numbers = set() + try: + raid_arrays = self.system_manager.get_mdadm_arrays() + for array in raid_arrays: + device_path = array.get('device') + if device_path and device_path.startswith('/dev/md'): + # 匹配 /dev/mdX 形式的设备名 + match = re.match(r'/dev/md(\d+)', device_path) + if match: + existing_md_numbers.add(int(match.group(1))) + except Exception as e: + logger.warning(f"获取现有 RAID 阵列信息失败,可能无法找到最优的下一个设备名: {e}") + + next_md_num = 0 + while next_md_num in existing_md_numbers: + next_md_num += 1 + + return f"/dev/md{next_md_num}" + def create_raid_array(self, devices, level, chunk_size): """ 创建 RAID 阵列。 :param devices: 成员设备列表,例如 ['/dev/sdb1', '/dev/sdc1'] - :param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' + :param level: RAID 级别,例如 'raid0', 'raid1', 'raid5' (也可以是整数 0, 1, 5) :param chunk_size: Chunk 大小 (KB) """ + # --- 标准化 RAID 级别输入 --- + if isinstance(level, int): + level_str = f"raid{level}" + elif isinstance(level, str): + if level in ["0", "1", "5", "6", "10"]: # 增加 "6", "10" 确保全面 + level_str = f"raid{level}" + else: + level_str = level + else: + QMessageBox.critical(None, "错误", f"不支持的 RAID 级别类型: {type(level)}") + return False + level = level_str + # --- 标准化 RAID 级别输入结束 --- + if not devices or len(devices) < 2: QMessageBox.critical(None, "错误", "创建 RAID 阵列至少需要两个成员设备。") return False @@ -74,6 +112,17 @@ class RaidOperations: if level == "raid5" and len(devices) < 3: QMessageBox.critical(None, "错误", "RAID5 至少需要三个设备。") return False + # 补充其他 RAID 级别设备数量检查,与 dialogs.py 保持一致 + if level == "raid1" and len(devices) < 2: + QMessageBox.critical(None, "错误", "RAID1 至少需要两个设备。") + return False + if level == "raid6" and len(devices) < 4: + QMessageBox.critical(None, "错误", "RAID6 至少需要四个设备。") + return False + if level == "raid10" and len(devices) < 2: + QMessageBox.critical(None, "错误", "RAID10 至少需要两个设备。") + return False + # 确认操作 reply = QMessageBox.question(None, "确认创建 RAID 阵列", @@ -90,69 +139,80 @@ class RaidOperations: for dev in devices: # 使用 --force 选项,避免交互式提示 clear_cmd = ["mdadm", "--zero-superblock", "--force", dev] + + # 定义表示设备上没有超级块的错误信息,根据日志调整 + no_superblock_error_match = "Unrecognised md component device" + success, _, stderr = self._execute_shell_command( clear_cmd, f"清除设备 {dev} 上的旧 RAID 超级块失败", - suppress_critical_dialog_on_stderr_match="No superblocks" # 抑制“没有超级块”的错误提示 + suppress_critical_dialog_on_stderr_match=no_superblock_error_match ) - if success: + + # 检查 stderr 是否包含“未识别的 MD 组件设备”信息 + if no_superblock_error_match in stderr: + logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。") + elif success: logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。") else: - # 如果清除失败,但不是因为“没有超级块”,则可能需要进一步处理 - if "No superblocks" not in stderr: - QMessageBox.warning(None, "警告", f"清除设备 {dev} 上的旧 RAID 超级块可能失败,但尝试继续。") + logger.error(f"清除设备 {dev} 上的旧 RAID 超级块失败,中断 RAID 创建。") + return False # 2. 创建 RAID 阵列 - # 默认阵列名为 /dev/md/new_raid,可以考虑让用户输入 - array_name = "/dev/md/new_raid" + # --- 修改点:自动生成唯一的阵列名称 --- + array_name = self._get_next_available_md_device_name() + logger.info(f"将使用自动生成的 RAID 阵列名称: {array_name}") + # --- 修改点结束 --- + if level == "raid0": - # RAID0 至少2个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid0", f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices elif level == "raid1": - # RAID1 至少2个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid1", f"--raid-devices={len(devices)}"] + devices elif level == "raid5": - # RAID5 至少3个设备 create_cmd = ["mdadm", "--create", array_name, "--level=raid5", f"--raid-devices={len(devices)}"] + devices + elif level == "raid6": # 增加 RAID6 + create_cmd = ["mdadm", "--create", array_name, "--level=raid6", + f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices + elif level == "raid10": # 增加 RAID10 + create_cmd = ["mdadm", "--create", array_name, "--level=raid10", + f"--raid-devices={len(devices)}", f"--chunk={chunk_size}K"] + devices else: QMessageBox.critical(None, "错误", f"不支持的 RAID 级别: {level}") return False # 在 --create 命令中添加 --force 选项 - create_cmd.insert(2, "--force") # 插入到 --create 后面 + create_cmd.insert(2, "--force") - # <--- 关键修改:通过 input_to_command 参数传入 'y\n' 来强制 mdadm 接受 - if not self._execute_shell_command(create_cmd, f"创建 RAID 阵列失败", input_to_command='y\n')[0]: + success_create, stdout_create, stderr_create = self._execute_shell_command( + create_cmd, + f"创建 RAID 阵列失败", + input_to_command='y\n' # 尝试通过 stdin 传递 'y' + ) + if not success_create: + # 检查是否是由于 "Array name ... is in use already." 导致的失败 + if "Array name" in stderr_create and "is in use already" in stderr_create: + QMessageBox.critical(None, "错误", f"创建 RAID 阵列失败:阵列名称 {array_name} 已被占用。请尝试停止或删除现有阵列。") return False logger.info(f"成功创建 RAID {level} 阵列 {array_name}。") QMessageBox.information(None, "成功", f"成功创建 RAID {level} 阵列 {array_name}。") # 3. 刷新 mdadm 配置并等待阵列激活 - # 注意:这里使用 '>>' 重定向,subprocess.run 无法直接处理 shell 重定向符号 - # 需要改成先读取,再写入,或者使用 bash -c "..." - # 暂时先用 bash -c 的方式,更简单 - # 旧代码:self._execute_shell_command(["mdadm", "--examine", "--scan", ">>", "/etc/mdadm/mdadm.conf"], "更新 mdadm.conf 失败") - - # 获取新的 mdadm.conf 内容 examine_scan_cmd = ["mdadm", "--examine", "--scan"] success_scan, scan_stdout, _ = self._execute_shell_command(examine_scan_cmd, "扫描 mdadm 配置失败") if success_scan: - # 将扫描结果追加到 mdadm.conf append_to_conf_cmd = ["bash", "-c", f"echo '{scan_stdout.strip()}' >> /etc/mdadm/mdadm.conf"] if not self._execute_shell_command(append_to_conf_cmd, "更新 /etc/mdadm/mdadm.conf 失败")[0]: logger.warning("更新 /etc/mdadm/mdadm.conf 失败。") else: logger.warning("未能扫描到 mdadm 配置,跳过更新 mdadm.conf。") - - # self._execute_shell_command(["update-initramfs", "-u"], "更新 initramfs 失败") - return True + def stop_raid_array(self, array_path): """ 停止一个 RAID 阵列。 @@ -173,7 +233,9 @@ class RaidOperations: # 暂时先直接调用 umount 命令,不处理 fstab # 或者,更好的方式是让 MainWindow 调用 disk_ops.unmount_partition # 此处简化处理,只执行 umount 命令 - self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", suppress_critical_dialog_on_stderr_match="not mounted") + # 这里的 suppress_critical_dialog_on_stderr_match 应该与 DiskOperations 中的定义保持一致 + self._execute_shell_command(["umount", array_path], f"尝试卸载 {array_path} 失败", + suppress_critical_dialog_on_stderr_match="not mounted") # 仅抑制英文,因为这里没有访问 DiskOperations 的 already_unmounted_errors if not self._execute_shell_command(["mdadm", "--stop", array_path], f"停止 RAID 阵列 {array_path} 失败")[0]: return False @@ -209,9 +271,24 @@ class RaidOperations: for dev in member_devices: # 使用 --force 选项,避免交互式提示 clear_cmd = ["mdadm", "--zero-superblock", "--force", dev] - if not self._execute_shell_command(clear_cmd, f"清除设备 {dev} 上的 RAID 超级块失败")[0]: + + # 定义表示设备上没有超级块的错误信息,根据日志调整 + no_superblock_error_match = "Unrecognised md component device" + + success, _, stderr = self._execute_shell_command( + clear_cmd, + f"清除设备 {dev} 上的 RAID 超级块失败", + suppress_critical_dialog_on_stderr_match=no_superblock_error_match + ) + + if no_superblock_error_match in stderr: + logger.info(f"设备 {dev} 上未找到旧 RAID 超级块,已确保其干净。") + elif success: + logger.info(f"已清除设备 {dev} 上的旧 RAID 超级块。") + else: success_all_cleared = False - QMessageBox.warning(None, "警告", f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。") + # 错误对话框已由 _execute_shell_command 弹出,这里只记录警告 + logger.warning(f"未能清除设备 {dev} 上的 RAID 超级块,请手动检查。") if success_all_cleared: logger.info(f"成功删除 RAID 阵列 {array_path} 并清除了成员设备超级块。") diff --git a/system_info.py b/system_info.py index f9bf584..b264e40 100644 --- a/system_info.py +++ b/system_info.py @@ -3,7 +3,7 @@ import subprocess import json import logging import re -import os # 导入 os 模块 +import os logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ class SystemInfoManager: Helper to find device data by its path recursively. Added type check for target_path. """ - if not isinstance(target_path, str): # 添加类型检查 + if not isinstance(target_path, str): logger.warning(f"传入 _find_device_by_path_recursive 的 target_path 不是字符串: {target_path} (类型: {type(target_path)})") return None for dev in dev_list: @@ -94,42 +94,109 @@ class SystemInfoManager: return found return None + def _find_device_by_maj_min_recursive(self, dev_list, target_maj_min): + """ + Helper to find device data by its major:minor number recursively. + """ + if not isinstance(target_maj_min, str): + logger.warning(f"传入 _find_device_by_maj_min_recursive 的 target_maj_min 不是字符串: {target_maj_min} (类型: {type(target_maj_min)})") + return None + for dev in dev_list: + if dev.get('maj:min') == target_maj_min: + return dev + if 'children' in dev: + found = self._find_device_by_maj_min_recursive(dev['children'], target_maj_min) + if found: + return found + return None + def get_mountpoint_for_device(self, device_path): """ 根据设备路径获取其挂载点。 - 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。 - Added type check for device_path. + 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。 """ - if not isinstance(device_path, str): # 添加类型检查 + if not isinstance(device_path, str): logger.warning(f"传入 get_mountpoint_for_device 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})") return None + original_device_path = device_path + logger.debug(f"get_mountpoint_for_device: 正在处理原始设备路径: {original_device_path}") + devices = self.get_block_devices() + logger.debug(f"get_mountpoint_for_device: 从 lsblk 获取到 {len(devices)} 个块设备信息。") + + target_maj_min = None + current_search_path = original_device_path + + # 1. 解析符号链接以获取实际路径,并尝试获取 maj:min + if original_device_path.startswith('/dev/') and os.path.exists(original_device_path): + try: + # 先尝试解析真实路径,因为 stat 可能对符号链接返回链接本身的 maj:min + resolved_path = os.path.realpath(original_device_path) + logger.debug(f"get_mountpoint_for_device: 原始设备路径 {original_device_path} 解析为 {resolved_path}") + current_search_path = resolved_path # 使用解析后的路径进行 stat 和后续查找 + + # 获取解析后路径的 maj:min + stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True) + raw_maj_min = stat_output.strip() # e.g., "fc:0" + + # MODIFIED: Convert hex major to decimal + if ':' in raw_maj_min: + major_hex, minor_dec = raw_maj_min.split(':') + try: + major_dec = str(int(major_hex, 16)) # Convert hex to decimal string + target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0" + logger.debug(f"get_mountpoint_for_device: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}") + except ValueError: + logger.warning(f"get_mountpoint_for_device: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。") + target_maj_min = None # Fallback if conversion fails + else: + logger.warning(f"get_mountpoint_for_device: stat 输出 '{raw_maj_min}' 格式不符合预期。") + target_maj_min = None + + except subprocess.CalledProcessError as e: + logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}") + except Exception as e: + logger.debug(f"get_mountpoint_for_device: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}") + elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path): + logger.warning(f"get_mountpoint_for_device: 设备路径 {original_device_path} 不存在,无法获取挂载点。") + return None + + # 2. 尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找 + dev_info = self._find_device_by_path_recursive(devices, current_search_path) + logger.debug(f"get_mountpoint_for_device: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}): {dev_info}") - # 1. 尝试直接使用提供的 device_path 在 lsblk 输出中查找 - dev_info = self._find_device_by_path_recursive(devices, device_path) if dev_info: - logger.debug(f"直接从 lsblk 获取到 {device_path} 的挂载点: {dev_info.get('mountpoint')}") - return dev_info.get('mountpoint') + mountpoint = dev_info.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的挂载点: {mountpoint}") + return mountpoint - # 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) - if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查 - logger.debug(f"处理 RAID 阵列 {device_path} 以获取挂载点...") + # 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找 + if target_maj_min: + logger.debug(f"get_mountpoint_for_device: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。") + dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min) + logger.debug(f"get_mountpoint_for_device: 通过 maj:min 查找结果: {dev_info_by_maj_min}") + if dev_info_by_maj_min: + mountpoint = dev_info_by_maj_min.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 通过 maj:min 找到 {original_device_path} 的挂载点: {mountpoint}") + return mountpoint - # 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127) - actual_md_device_path = self._get_actual_md_device_path(device_path) - logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}") + # 4. 如果仍然查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) + if original_device_path.startswith('/dev/md'): + logger.debug(f"get_mountpoint_for_device: 正在处理 RAID 阵列 {original_device_path} 以获取挂载点...") + actual_md_device_path = self._get_actual_md_device_path(original_device_path) + logger.debug(f"get_mountpoint_for_device: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}") if actual_md_device_path: - # 现在,使用实际的内核设备路径从 lsblk 中查找挂载点 actual_dev_info = self._find_device_by_path_recursive(devices, actual_md_device_path) - logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}") + logger.debug(f"get_mountpoint_for_device: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_dev_info}") if actual_dev_info: - logger.debug(f"在实际设备 {actual_md_device_path} 上找到了挂载点: {actual_dev_info.get('mountpoint')}") - return actual_dev_info.get('mountpoint') + mountpoint = actual_dev_info.get('mountpoint') + logger.debug(f"get_mountpoint_for_device: 在实际设备 {actual_md_device_path} 上找到了挂载点: {mountpoint}") + return mountpoint - logger.debug(f"未能获取到 {device_path} 的挂载点。") + logger.debug(f"get_mountpoint_for_device: 未能获取到 {original_device_path} 的挂载点。") return None def get_mdadm_arrays(self): @@ -269,9 +336,10 @@ class SystemInfoManager: except Exception as e: logger.error(f"获取LVM卷组信息失败: {e}") - # Get LVs + # Get LVs (MODIFIED: added -o lv_path) try: - stdout, _ = self._run_command(["lvs", "--reportformat", "json"]) + # 明确请求 lv_path,因为默认的 --reportformat json 不包含它 + stdout, _ = self._run_command(["lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", "--reportformat", "json"]) data = json.loads(stdout) if 'report' in data and data['report']: for lv_data in data['report'][0].get('lv', []): @@ -283,7 +351,7 @@ class SystemInfoManager: 'lv_attr': lv_data.get('lv_attr'), 'origin': lv_data.get('origin'), 'snap_percent': lv_data.get('snap_percent'), - 'lv_path': lv_data.get('lv_path') + 'lv_path': lv_data.get('lv_path') # 现在应该能正确获取到路径了 }) except subprocess.CalledProcessError as e: if "No logical volume found" in e.stderr or "No logical volumes found" in e.stdout: @@ -358,7 +426,7 @@ class SystemInfoManager: 通过检查 /dev/md/ 目录下的符号链接来获取。 Added type check for array_path. """ - if not isinstance(array_path, str): # 添加类型检查 + if not isinstance(array_path, str): logger.warning(f"传入 _get_actual_md_device_path 的 array_path 不是字符串: {array_path} (类型: {type(array_path)})") return None @@ -378,63 +446,103 @@ class SystemInfoManager: def get_device_details_by_path(self, device_path): """ 根据设备路径获取设备的 UUID 和文件系统类型 (fstype)。 - 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名。 + 此方法现在可以正确处理 /dev/md/new_raid 这样的 RAID 阵列别名,以及 LVM 逻辑卷的符号链接。 Added type check for device_path. :param device_path: 设备的路径,例如 '/dev/sdb1' 或 '/dev/md0' 或 '/dev/md/new_raid' :return: 包含 'uuid' 和 'fstype' 的字典,如果未找到则返回 None。 """ - if not isinstance(device_path, str): # 添加类型检查 + if not isinstance(device_path, str): logger.warning(f"传入 get_device_details_by_path 的 device_path 不是字符串: {device_path} (类型: {type(device_path)})") return None + original_device_path = device_path + logger.debug(f"get_device_details_by_path: 正在处理原始设备路径: {original_device_path}") + devices = self.get_block_devices() # 获取所有块设备信息 + logger.debug(f"get_device_details_by_path: 从 lsblk 获取到 {len(devices)} 个块设备信息。") - # 1. 首先,尝试直接使用提供的 device_path 在 lsblk 输出中查找 - # 对于 /dev/md/new_raid 这样的别名,这一步通常会返回 None - lsblk_details = self._find_device_by_path_recursive(devices, device_path) - logger.debug(f"lsblk_details for {device_path} (direct lookup): {lsblk_details}") + target_maj_min = None + current_search_path = original_device_path - if lsblk_details and lsblk_details.get('fstype'): # 即使没有UUID,有fstype也算找到部分信息 - # 如果直接找到了 fstype,就返回 lsblk 提供的 UUID 和 fstype - # 这里的 UUID 应该是文件系统 UUID - logger.debug(f"直接从 lsblk 获取到 {device_path} 的详情: {lsblk_details}") + # 1. 解析符号链接以获取实际路径,并尝试获取 maj:min + if original_device_path.startswith('/dev/') and os.path.exists(original_device_path): + try: + resolved_path = os.path.realpath(original_device_path) + logger.debug(f"get_device_details_by_path: 原始设备路径 {original_device_path} 解析为 {resolved_path}") + current_search_path = resolved_path + + stat_output, _ = self._run_command(["stat", "-c", "%t:%T", resolved_path], check_output=True) + raw_maj_min = stat_output.strip() # e.g., "fc:0" + + # MODIFIED: Convert hex major to decimal + if ':' in raw_maj_min: + major_hex, minor_dec = raw_maj_min.split(':') + try: + major_dec = str(int(major_hex, 16)) # Convert hex to decimal string + target_maj_min = f"{major_dec}:{minor_dec}" # e.g., "252:0" + logger.debug(f"get_device_details_by_path: 解析后设备 {resolved_path} 的 maj:min (hex) 为 {raw_maj_min},转换为 decimal 为 {target_maj_min}") + except ValueError: + logger.warning(f"get_device_details_by_path: 无法将 maj:min 的主要部分 '{major_hex}' 从十六进制转换为十进制。") + target_maj_min = None + else: + logger.warning(f"get_device_details_by_path: stat 输出 '{raw_maj_min}' 格式不符合预期。") + target_maj_min = None + + except subprocess.CalledProcessError as e: + logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (命令失败): {e.stderr.strip()}") + except Exception as e: + logger.debug(f"get_device_details_by_path: 无法获取 {resolved_path} 的 maj:min (未知错误): {e}") + elif original_device_path.startswith('/dev/') and not os.path.exists(original_device_path): + logger.warning(f"get_device_details_by_path: 设备路径 {original_device_path} 不存在,无法获取详情。") + return None + + # 2. 首先,尝试使用 (可能已解析的) current_search_path 在 lsblk 输出中查找 + lsblk_details = self._find_device_by_path_recursive(devices, current_search_path) + logger.debug(f"get_device_details_by_path: _find_device_by_path_recursive 查找结果 (使用 {current_search_path}, 直接查找): {lsblk_details}") + + if lsblk_details and lsblk_details.get('fstype'): + logger.debug(f"get_device_details_by_path: 直接从 lsblk 获取到 {original_device_path} (解析为 {current_search_path}) 的详情: {lsblk_details}") return { 'uuid': lsblk_details.get('uuid'), 'fstype': lsblk_details.get('fstype') } - # 2. 如果直接查找失败,并且是 RAID 阵列(例如 /dev/md/new_raid) - if device_path.startswith('/dev/md'): # 此处 device_path 已通过类型检查 - logger.debug(f"处理 RAID 阵列 {device_path}...") + # 3. 如果直接路径查找失败,并且我们有 maj:min,尝试通过 maj:min 查找 + if target_maj_min: + logger.debug(f"get_device_details_by_path: 直接路径查找失败,尝试通过 maj:min ({target_maj_min}) 查找。") + dev_info_by_maj_min = self._find_device_by_maj_min_recursive(devices, target_maj_min) + logger.debug(f"get_device_details_by_path: 通过 maj:min 查找结果: {dev_info_by_maj_min}") + if dev_info_by_maj_min and dev_info_by_maj_min.get('fstype'): + logger.debug(f"get_device_details_by_path: 通过 maj:min 找到 {original_device_path} 的文件系统详情: {dev_info_by_maj_min}") + return { + 'uuid': dev_info_by_maj_min.get('uuid'), + 'fstype': dev_info_by_maj_min.get('fstype') + } - # 获取 RAID 阵列的实际内核设备路径(例如 /dev/md127) - actual_md_device_path = self._get_actual_md_device_path(device_path) - logger.debug(f"RAID 阵列 {device_path} 的实际设备路径: {actual_md_device_path}") + # 4. 如果仍然没有找到,并且是 RAID 阵列(例如 /dev/md/new_raid) + if original_device_path.startswith('/dev/md'): + logger.debug(f"get_device_details_by_path: 正在处理 RAID 阵列 {original_device_path}...") + + actual_md_device_path = self._get_actual_md_device_path(original_device_path) + logger.debug(f"get_device_details_by_path: RAID 阵列 {original_device_path} 的实际设备路径: {actual_md_device_path}") if actual_md_device_path: - # 现在,使用实际的内核设备路径从 lsblk 中查找 fstype 和 UUID (文件系统 UUID) actual_device_lsblk_details = self._find_device_by_path_recursive(devices, actual_md_device_path) - logger.debug(f"实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}") + logger.debug(f"get_device_details_by_path: 实际设备路径 {actual_md_device_path} 的 lsblk 详情: {actual_device_lsblk_details}") if actual_device_lsblk_details and actual_device_lsblk_details.get('fstype'): - # 找到了实际 RAID 设备上的文件系统信息 - # 此时的 UUID 是文件系统 UUID,fstype 是文件系统类型 - logger.debug(f"在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}") + logger.debug(f"get_device_details_by_path: 在实际设备 {actual_md_device_path} 上找到了文件系统详情: {actual_device_lsblk_details}") return { 'uuid': actual_device_lsblk_details.get('uuid'), 'fstype': actual_device_lsblk_details.get('fstype') } else: - # RAID 设备存在,但 lsblk 没有报告文件系统 (例如,尚未格式化) - # 此时 fstype 为 None。如果需要,我们可以返回 RAID 阵列本身的 UUID,但 fstype 仍为 None - logger.warning(f"RAID 阵列 {device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。") - # 对于 fstab,如果没有 fstype,就无法创建条目。 - # 此时返回 None,让调用者知道无法写入 fstab。 + logger.warning(f"get_device_details_by_path: RAID 阵列 {original_device_path} (实际设备 {actual_md_device_path}) 未找到文件系统类型。") return None else: - logger.warning(f"无法确定 RAID 阵列 {device_path} 的实际内核设备路径。") - return None # 无法解析实际设备路径,也无法获取 fstype + logger.warning(f"get_device_details_by_path: 无法确定 RAID 阵列 {original_device_path} 的实际内核设备路径。") + return None - # 3. 如果仍然没有找到,返回 None - logger.debug(f"未能获取到 {device_path} 的任何详情。") + # 5. 如果仍然没有找到,返回 None + logger.debug(f"get_device_details_by_path: 未能获取到 {original_device_path} 的任何详情。") return None diff --git a/ui_form.py b/ui_form.py index 3efa696..783dd25 100644 --- a/ui_form.py +++ b/ui_form.py @@ -27,18 +27,23 @@ class Ui_MainWindow(object): MainWindow.resize(1000, 700) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") - self.verticalLayout = QVBoxLayout(self.centralwidget) - self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout_2 = QVBoxLayout(self.centralwidget) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.refreshButton = QPushButton(self.centralwidget) + self.refreshButton.setObjectName(u"refreshButton") + + self.verticalLayout_2.addWidget(self.refreshButton) + self.tabWidget = QTabWidget(self.centralwidget) self.tabWidget.setObjectName(u"tabWidget") self.tab_block_devices = QWidget() self.tab_block_devices.setObjectName(u"tab_block_devices") - self.verticalLayout_2 = QVBoxLayout(self.tab_block_devices) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout = QVBoxLayout(self.tab_block_devices) + self.verticalLayout.setObjectName(u"verticalLayout") self.treeWidget_block_devices = QTreeWidget(self.tab_block_devices) self.treeWidget_block_devices.setObjectName(u"treeWidget_block_devices") - self.verticalLayout_2.addWidget(self.treeWidget_block_devices) + self.verticalLayout.addWidget(self.treeWidget_block_devices) self.tabWidget.addTab(self.tab_block_devices, "") self.tab_raid = QWidget() @@ -62,23 +67,20 @@ class Ui_MainWindow(object): self.tabWidget.addTab(self.tab_lvm, "") - self.verticalLayout.addWidget(self.tabWidget) - - self.refreshButton = QPushButton(self.centralwidget) - self.refreshButton.setObjectName(u"refreshButton") - - self.verticalLayout.addWidget(self.refreshButton) + self.verticalLayout_2.addWidget(self.tabWidget) self.logOutputTextEdit = QTextEdit(self.centralwidget) self.logOutputTextEdit.setObjectName(u"logOutputTextEdit") self.logOutputTextEdit.setReadOnly(True) - self.verticalLayout.addWidget(self.logOutputTextEdit) + self.verticalLayout_2.addWidget(self.logOutputTextEdit) + self.verticalLayout_2.setStretch(1, 4) + self.verticalLayout_2.setStretch(2, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 1000, 23)) + self.menubar.setGeometry(QRect(0, 0, 1000, 30)) MainWindow.setMenuBar(self.menubar) self.statusbar = QStatusBar(MainWindow) self.statusbar.setObjectName(u"statusbar") @@ -94,6 +96,7 @@ class Ui_MainWindow(object): def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"Linux \u5b58\u50a8\u7ba1\u7406\u5de5\u5177", None)) + self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None)) ___qtreewidgetitem = self.treeWidget_block_devices.headerItem() ___qtreewidgetitem.setText(12, QCoreApplication.translate("MainWindow", u"\u7236\u8bbe\u5907\u540d", None)); ___qtreewidgetitem.setText(11, QCoreApplication.translate("MainWindow", u"\u4e3b\u6b21\u53f7", None)); @@ -115,6 +118,5 @@ class Ui_MainWindow(object): ___qtreewidgetitem2 = self.treeWidget_lvm.headerItem() ___qtreewidgetitem2.setText(0, QCoreApplication.translate("MainWindow", u"1", None)); self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_lvm), QCoreApplication.translate("MainWindow", u"LVM \u7ba1\u7406", None)) - self.refreshButton.setText(QCoreApplication.translate("MainWindow", u"\u5237\u65b0\u6570\u636e", None)) # retranslateUi