From c42c701ffc5cc65adf2600c968f63ec92d2ae950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?= =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:45:25 +0800 Subject: [PATCH] bug: incorrect disk smart info (#789) --- README.md | 7 +- README_zh.md | 21 +++-- lib/data/model/app/shell_func.dart | 2 +- lib/data/model/server/disk_smart.dart | 91 +++++++++++++++++- lib/view/page/server/detail/view.dart | 131 ++++++++++++++++++++++---- pubspec.lock | 4 +- pubspec.yaml | 2 +- 7 files changed, 223 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index aa43afe8..752a3ccc 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ Especially thanks to dartss ## 📥 Install -| Platform | From | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|Platform| From| +|--|--| | iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) | | Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) | | Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) | @@ -38,13 +38,14 @@ Please only download pkgs from the source that **you trust**! ## 🔖 Feature -- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`... +- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`... - Platform specific: `Bio auth`、`Msg push`、`Home widget`、`watchOS App`... - English, 简体中文; Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); Español, Русский язык, Português, 日本語 (Generated by GPT) ## 🆘 Help
+ qq donate discord
diff --git a/README_zh.md b/README_zh.md index a471d0fb..0bfeddfc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -15,8 +15,8 @@ 特别感谢 dartssh2 & xterm.dart

- ## 🏙️ 截屏 + @@ -26,20 +26,19 @@
- ## 📥 安装 -平台 | 下载 -----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +平台|下载 +--|-- iOS / macOS | [AppStore](https://apps.apple.com/app/id1586449703) Android | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) / [F-Droid](https://f-droid.org/packages/tech.lolli.toolbox) / [OpenAPK](https://www.openapk.net/serverbox/tech.lolli.toolbox/) Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) / [CDN](https://cdn.lpkt.cn/serverbox/pkg/?sort=time&order=desc&layout=grid) 请从 **信任** 的来源下载! - ## 🔖 特点 -- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理... + +- `状态图表`(CPU、传感器、GPU 等), `SSH` 终端, `SFTP`, `Docker & 进程 & Systemd` 管理,`S.M.A.R.T`... - 特殊支持:`生物认证`、`推送`、`桌面小部件`、`watchOS App`、`跟随系统颜色`... - 本地化 - English, 简体中文 @@ -47,10 +46,10 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel - Deutsch [@its-tom](https://github.com/its-tom), 繁體中文 [@kalashnikov](https://github.com/kalashnikov), Indonesian [@azkadev](https://github.com/azkadev), Français [@FrancXPT](https://github.com/FrancXPT), Dutch [@QazCetelic](https://github.com/QazCetelic), Türkçe [@mikropsoft](https://github.com/mikropsoft), Українська мова [@CakesTwix](https://github.com/CakesTwix); - 感谢贡献者们! - ## 🆘 帮助
+ qq donate discord
@@ -59,26 +58,30 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel - **常见问题** 可以在 [app wiki](https://github.com/lollipopkit/flutter_server_box/wiki/主页) 查看。 反馈前须知: + 1. 反馈问题请附带 log(点击首页右上角),并以 bug 模版提交。 2. 反馈问题前请检查是否是 serverbox 的问题。 3. 欢迎所有有效、正面的反馈,主观(比如你觉得其他UI更好看)的反馈不一定会接受 - ## 🧱 贡献 + 任何正面的贡献都欢迎。 ### 开发 + 1. 安装 [Flutter](https://flutter.dev/docs/get-started/install) 2. 克隆这个仓库, 运行 `flutter run` 启动应用 3. 运行 `dart run fl_build -p PLATFORM` 构建应用 ### 翻译 + [指南](https://blog.lpkt.cn/faq/) 可在我的博客中找到。 ## 💡 我的其它 Apps + - [GPT Box](https://github.com/lollipopkit/flutter_gpt_box) - 支持 OpenAI API 的 第三方全平台客户端。 - [更多](https://github.com/lollipopkit) - 工具 & etc. - ## 📝 协议 + `GPL v3 lollipopkit` diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index bbdf4e2a..2fca8db6 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -225,7 +225,7 @@ enum StatusCmdType { ), nvidia._('nvidia-smi -q -x'), sensors._('sensors'), - diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -j /dev/\$d; echo; done'), + diskSmart._('for d in \$(lsblk -dn -o KNAME); do smartctl -a -j /dev/\$d; echo; done'), cpuBrand._('cat /proc/cpuinfo | grep "model name"'); final String cmd; diff --git a/lib/data/model/server/disk_smart.dart b/lib/data/model/server/disk_smart.dart index d2cc065a..78595454 100644 --- a/lib/data/model/server/disk_smart.dart +++ b/lib/data/model/server/disk_smart.dart @@ -35,7 +35,10 @@ abstract class DiskSmart with _$DiskSmart { // Basic final device = data['device']?['name']?.toString() ?? ''; - final healthy = data['smart_status']?['passed'] as bool?; + + if (!_isPhysicalDisk(device)) continue; + + final healthy = _parseHealthStatus(data); // Model and Serial final model = @@ -72,6 +75,92 @@ abstract class DiskSmart with _$DiskSmart { return results; } + static bool _isPhysicalDisk(String device) { + if (device.isEmpty) return false; + + // Common patterns for physical disks + final patterns = [ + RegExp(r'^/dev/sd[a-z]$'), // SATA/SCSI: /dev/sda, /dev/sdb + RegExp(r'^/dev/hd[a-z]$'), // IDE: /dev/hda, /dev/hdb + RegExp(r'^/dev/nvme\d+n\d+$'), // NVMe: /dev/nvme0n1, /dev/nvme1n1 + RegExp(r'^/dev/mmcblk\d+$'), // MMC: /dev/mmcblk0 + RegExp(r'^/dev/vd[a-z]$'), // VirtIO: /dev/vda, /dev/vdb + RegExp(r'^/dev/xvd[a-z]$'), // Xen: /dev/xvda, /dev/xvdb + ]; + + return patterns.any((pattern) => pattern.hasMatch(device)); + } + + static bool? _parseHealthStatus(Map data) { + // smart_status.passed + final smartStatus = data['smart_status']; + if (smartStatus is Map) { + final passed = smartStatus['passed']; + if (passed is bool) return passed; + } + + // smart_status.status + if (smartStatus is Map) { + final status = smartStatus['status']?.toString().toLowerCase(); + if (status != null) { + if (status.contains('pass') || status.contains('ok')) return true; + if (status.contains('fail')) return false; + } + } + + // smart_status + final rootSmartStatus = data['smart_status']?.toString().toLowerCase(); + if (rootSmartStatus != null) { + if (rootSmartStatus.contains('pass') || rootSmartStatus.contains('ok')) return true; + if (rootSmartStatus.contains('fail')) return false; + } + + // health attrs + final attrTable = data['ata_smart_attributes']?['table'] as List?; + if (attrTable != null) { + var hasFailingAttributes = false; + + for (final attr in attrTable) { + if (attr is Map) { + final whenFailed = attr['when_failed']?.toString(); + if (whenFailed != null && whenFailed.isNotEmpty && whenFailed != 'never') { + hasFailingAttributes = true; + break; + } + + // Whether the attribute is critical + final name = attr['name']?.toString(); + final value = attr['value'] as int?; + final thresh = attr['thresh'] as int?; + + if (name != null && value != null && thresh != null && thresh > 0) { + const criticalAttrs = [ + 'Reallocated_Sector_Ct', + 'Reallocated_Event_Count', + 'Current_Pending_Sector', + 'Offline_Uncorrectable', + 'UDMA_CRC_Error_Count', + ]; + + if (criticalAttrs.contains(name) && value < thresh) { + hasFailingAttributes = true; + break; + } + } + } + } + + if (hasFailingAttributes) return false; + } + + if (attrTable != null && attrTable.isNotEmpty) { + return true; + } + + // Uncertain status, assume healthy + return true; + } + static Map _parseSmartAttributes(Map data) { final attributes = {}; diff --git a/lib/view/page/server/detail/view.dart b/lib/view/page/server/detail/view.dart index 3b17ba1a..8c2d2916 100644 --- a/lib/view/page/server/detail/view.dart +++ b/lib/view/page/server/detail/view.dart @@ -580,38 +580,133 @@ class _ServerDetailPageState extends State with SingleTickerPr } Widget _buildDiskSmartItem(DiskSmart smart) { - final isPass = smart.healthy ?? false; - final statusText = isPass ? 'PASS' : 'FAIL'; - final statusColor = isPass ? Colors.green : Colors.red; - final statusIcon = isPass - ? Icon(Icons.check_circle, color: Colors.green, size: 18) - : Icon(Icons.error, color: Colors.red, size: 18); + final healthStatus = _getDiskHealthStatus(smart); return ListTile( dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), - leading: statusIcon, + leading: healthStatus.icon, title: Text(smart.device, style: UIs.text13, textScaler: _textFactor), trailing: Text( - statusText, - style: UIs.text13.copyWith(color: statusColor, fontWeight: FontWeight.bold), + healthStatus.text, + style: UIs.text13.copyWith(fontWeight: FontWeight.bold), textScaler: _textFactor, ), subtitle: _buildDiskSmartDetails(smart), + onTap: () => _onTapDiskSmartItem(smart), ); } + ({String text, Color color, Widget icon}) _getDiskHealthStatus(DiskSmart smart) { + if (smart.healthy == null) { + return ( + text: libL10n.unknown, + color: Colors.orange, + icon: const Icon(Icons.help_outline, color: Colors.orange, size: 18), + ); + } else if (smart.healthy!) { + return ( + text: 'PASS', + color: Colors.green, + icon: const Icon(Icons.check_circle, color: Colors.green, size: 18), + ); + } else { + return (text: 'FAIL', color: Colors.red, icon: const Icon(Icons.error, color: Colors.red, size: 18)); + } + } + Widget? _buildDiskSmartDetails(DiskSmart smart) { final details = []; - - if (smart.model != null) details.add(smart.model!); - if (smart.serial != null) details.add('S/N: ${smart.serial}'); - if (smart.temperature != null) details.add('${smart.temperature!.toStringAsFixed(1)}°C'); - if (smart.powerOnHours != null) details.add('${smart.powerOnHours} hours'); - + + if (smart.model != null) { + details.add(smart.model!); + } + + if (smart.temperature != null) { + details.add('${smart.temperature!.toStringAsFixed(1)}°C'); + } + + if (smart.powerOnHours != null) { + final hours = smart.powerOnHours!; + details.add('$hours ${libL10n.hour}'); + } + + if (smart.ssdLifeLeft != null) { + details.add('Life left: ${smart.ssdLifeLeft}%'); + } + if (details.isEmpty) return null; - - return Text(details.join(' | '), style: UIs.text12, textScaler: _textFactor); + + return Text( + details.join(' | '), + style: UIs.text12Grey, + textScaler: _textFactor, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ); + } + + void _onTapDiskSmartItem(DiskSmart smart) { + final details = []; + + if (smart.model != null) details.add('Model: ${smart.model}'); + if (smart.serial != null) details.add('Serial: ${smart.serial}'); + if (smart.temperature != null) details.add('Temperature: ${smart.temperature!.toStringAsFixed(1)}°C'); + + if (smart.powerOnHours != null) { + details.add('Power On: ${smart.powerOnHours} ${libL10n.hour}'); + } + if (smart.powerCycleCount != null) { + details.add('Power Cycle: ${smart.powerCycleCount}'); + } + + if (smart.ssdLifeLeft != null) { + details.add('Life Left: ${smart.ssdLifeLeft}%'); + } + if (smart.lifetimeWritesGiB != null) { + details.add('Lifetime Write: ${smart.lifetimeWritesGiB} GiB'); + } + if (smart.lifetimeReadsGiB != null) { + details.add('Lifetime Read: ${smart.lifetimeReadsGiB} GiB'); + } + if (smart.averageEraseCount != null) { + details.add('Avg. Erase: ${smart.averageEraseCount}'); + } + if (smart.unsafeShutdownCount != null) { + details.add('Unsafe Shutdown: ${smart.unsafeShutdownCount}'); + } + + final criticalAttrs = [ + 'Reallocated_Sector_Ct', + 'Current_Pending_Sector', + 'Offline_Uncorrectable', + 'UDMA_CRC_Error_Count', + ]; + + for (final attrName in criticalAttrs) { + final attr = smart.getAttribute(attrName); + if (attr != null && attr.rawValue != null) { + final value = attr.rawValue.toString(); + details.add('${attrName.replaceAll('_', ' ')}: $value'); + } + } + + if (details.isEmpty) { + return; + } + + final markdown = details.join('\n\n- '); + context.showRoundDialog( + title: smart.device, + child: MarkdownBody( + data: '- $markdown', + selectable: true, + styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( + p: UIs.text13Grey, + h2: UIs.text15, + ), + ), + actions: Btnx.oks, + ); } Widget? _buildNetView(Server si) { diff --git a/pubspec.lock b/pubspec.lock index 47366ee3..ceadc601 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -480,8 +480,8 @@ packages: dependency: "direct dev" description: path: "." - ref: "v1.0.49" - resolved-ref: "830d1f4c95a35f440881b6ff9c57e24711002fa0" + ref: "v1.0.50" + resolved-ref: c7b53e0e6b40e4a021175695fdc9e7a54a9b5321 url: "https://github.com/lppcg/fl_build.git" source: git version: "1.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6bcac6f8..4675b05b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,7 +91,7 @@ dev_dependencies: fl_build: git: url: https://github.com/lppcg/fl_build.git - ref: v1.0.49 + ref: v1.0.50 flutter: generate: true