From 37df0727110592a66cda8ca4ebaf104f9da23467 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Tue, 31 Oct 2023 19:41:54 +0800 Subject: [PATCH] new: detail status page --- lib/data/model/app/shell_func.dart | 3 +- lib/data/model/server/disk.dart | 93 +++++- lib/data/model/server/server_status.dart | 2 + .../server/server_status_update_req.dart | 11 +- lib/data/res/build_data.dart | 4 +- lib/data/res/misc.dart | 2 + lib/data/res/status.dart | 5 +- lib/view/page/server/detail.dart | 282 +++++++----------- test/status_prase_test.dart | 2 +- 9 files changed, 208 insertions(+), 196 deletions(-) diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index 9a05b44c..f841b549 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -199,7 +199,7 @@ enum StatusCmdType { tempType, tempVal, host, - ; + diskio; } /// Cmds for linux server @@ -216,6 +216,7 @@ const _statusCmds = [ 'cat /sys/class/thermal/thermal_zone*/type', 'cat /sys/class/thermal/thermal_zone*/temp', 'hostname', + 'cat /proc/diskstats', ]; enum DockerCmdType { diff --git a/lib/data/model/server/disk.dart b/lib/data/model/server/disk.dart index 84d77268..a6925894 100644 --- a/lib/data/model/server/disk.dart +++ b/lib/data/model/server/disk.dart @@ -1,16 +1,19 @@ +import 'package:toolbox/core/extension/numx.dart'; +import 'package:toolbox/data/model/server/time_seq.dart'; + import '../../res/misc.dart'; class Disk { - final String path; - final String loc; + final String dev; + final String mount; final int usedPercent; final String used; final String size; final String avail; const Disk({ - required this.path, - required this.loc, + required this.dev, + required this.mount, required this.usedPercent, required this.used, required this.size, @@ -18,6 +21,80 @@ class Disk { }); } +class DiskIO extends TimeSeq { + DiskIO(super.pre, super.now); + + (String?, String?) getReadSpeed(String dev) { + final pres = this.pre.where( + (element) => element.dev == dev.replaceFirst('/dev/', ''), + ); + final nows = this.now.where( + (element) => element.dev == dev.replaceFirst('/dev/', ''), + ); + if (pres.isEmpty || nows.isEmpty) return (null, null); + final pre = pres.first; + final now = nows.first; + final sectorsRead = now.sectorsRead - pre.sectorsRead; + final sectorsWrite = now.sectorsWrite - pre.sectorsWrite; + final time = now.time - pre.time; + final read = '${(sectorsRead / time * 512).convertBytes}/s'; + final write = '${(sectorsWrite / time * 512).convertBytes}/s'; + return (read, write); + } + + // Raw: + // 254 0 vda 584193 186416 40419294 845790 5024458 2028159 92899586 6997559 0 5728372 8143590 0 0 0 0 2006112 300240 + // 254 1 vda1 584029 186416 40412734 845668 5024453 2028159 92899586 6997558 0 5728264 7843226 0 0 0 0 0 0 + // 11 0 sr0 36 0 280 49 0 0 0 0 0 56 49 0 0 0 0 0 0 + // 7 0 loop0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 1 loop1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 2 loop2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 3 loop3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 4 loop4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 5 loop5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 6 loop6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 7 7 loop7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + static List parse(String raw, int time) { + final lines = raw.split('\n'); + if (lines.isEmpty) return []; + final items = []; + for (var item in lines) { + item = item.trim(); + if (item.isEmpty) continue; + final vals = item.split(Miscs.blankReg); + if (vals.length < 10) continue; + try { + items.add(DiskIOPiece( + dev: vals[2], + sectorsRead: int.parse(vals[5]), + sectorsWrite: int.parse(vals[9]), + time: time, + )); + } catch (e) { + continue; + } + } + return items; + } +} + +class DiskIOPiece extends TimeSeqIface { + final String dev; + final int sectorsRead; + final int sectorsWrite; + final int time; + + DiskIOPiece({ + required this.dev, + required this.sectorsRead, + required this.sectorsWrite, + required this.time, + }); + + @override + bool same(DiskIOPiece other) => dev == other.dev; +} + List parseDisk(String raw) { final list = []; final items = raw.split('\n'); @@ -38,8 +115,8 @@ List parseDisk(String raw) { } try { list.add(Disk( - path: vals[0], - loc: vals[5], + dev: vals[0], + mount: vals[5], usedPercent: int.parse(vals[4].replaceFirst('%', '')), used: vals[2], size: vals[1], @@ -62,9 +139,9 @@ List parseDisk(String raw) { /// the fps may lower than 60. Disk? findRootDisk(List disks) { if (disks.isEmpty) return null; - final roots = disks.where((element) => element.loc == '/'); + final roots = disks.where((element) => element.mount == '/'); if (roots.isEmpty) { - final sysRoots = disks.where((element) => element.loc == '/sysroot'); + final sysRoots = disks.where((element) => element.mount == '/sysroot'); if (sysRoots.isEmpty) { return disks.first; } else { diff --git a/lib/data/model/server/server_status.dart b/lib/data/model/server/server_status.dart index a93424bc..a4223d8f 100644 --- a/lib/data/model/server/server_status.dart +++ b/lib/data/model/server/server_status.dart @@ -19,6 +19,7 @@ class ServerStatus { Temperatures temps; SystemType system; String? failedInfo; + DiskIO diskIO; ServerStatus({ required this.cpu, @@ -31,6 +32,7 @@ class ServerStatus { required this.swap, required this.temps, required this.system, + required this.diskIO, this.failedInfo, }); } diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart index f0384134..bbf6d6c0 100644 --- a/lib/data/model/server/server_status_update_req.dart +++ b/lib/data/model/server/server_status_update_req.dart @@ -35,8 +35,10 @@ Future getStatus(ServerStatusUpdateReq req) async { Future _getLinuxStatus(ServerStatusUpdateReq req) async { final segments = req.segments; + final time = int.tryParse(StatusCmdType.time.find(segments)) ?? + DateTime.now().millisecondsSinceEpoch ~/ 1000; + try { - final time = int.parse(StatusCmdType.time.find(segments)); final net = parseNetSpeed(StatusCmdType.net.find(segments), time); req.ss.netSpeed.update(net); } catch (e, s) { @@ -101,6 +103,13 @@ Future _getLinuxStatus(ServerStatusUpdateReq req) async { } catch (e, s) { Loggers.parse.warning(e, s); } + + try { + final diskio = DiskIO.parse(StatusCmdType.diskio.find(segments), time); + req.ss.diskIO.update(diskio); + } catch (e, s) { + Loggers.parse.warning(e, s); + } return req.ss; } diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 405ce3bb..06291e53 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -4,7 +4,7 @@ class BuildData { static const String name = "ServerBox"; static const int build = 618; static const String engine = "3.13.8"; - static const String buildAt = "2023-10-30 17:18:16"; - static const int modifications = 5; + static const String buildAt = "2023-10-31 15:59:28"; + static const int modifications = 7; static const int script = 23; } diff --git a/lib/data/res/misc.dart b/lib/data/res/misc.dart index 5b2bb3c0..e373a46e 100644 --- a/lib/data/res/misc.dart +++ b/lib/data/res/misc.dart @@ -6,6 +6,8 @@ class Miscs { /// RegExp for number static final numReg = RegExp(r'\s{1,}'); + static final blankReg = RegExp(r'\s+'); + /// RegExp for password request static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):'); diff --git a/lib/data/res/status.dart b/lib/data/res/status.dart index a52df3f8..22178195 100644 --- a/lib/data/res/status.dart +++ b/lib/data/res/status.dart @@ -46,8 +46,8 @@ class InitStatus { uptime: '', disk: [ const Disk( - path: '/', - loc: '/', + dev: '/', + mount: '/', usedPercent: 0, used: '0', size: '0', @@ -63,5 +63,6 @@ class InitStatus { ), system: SystemType.linux, temps: Temperatures(), + diskIO: DiskIO([], []), ); } diff --git a/lib/view/page/server/detail.dart b/lib/view/page/server/detail.dart index 62df8270..a01c3b5b 100644 --- a/lib/view/page/server/detail.dart +++ b/lib/view/page/server/detail.dart @@ -4,10 +4,12 @@ import 'package:toolbox/core/extension/context/common.dart'; import 'package:toolbox/core/extension/context/locale.dart'; import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/data/model/server/cpu.dart'; +import 'package:toolbox/data/model/server/disk.dart'; import 'package:toolbox/data/model/server/net_speed.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/system.dart'; import 'package:toolbox/data/res/store.dart'; +import 'package:toolbox/view/widget/expand_tile.dart'; import 'package:toolbox/view/widget/server_func_btns.dart'; import 'package:toolbox/view/widget/value_notifier.dart'; @@ -306,211 +308,129 @@ class _ServerDetailPageState extends State } Widget _buildDiskView(ServerStatus ss) { - final disk = ss.disk; - disk.removeWhere((e) { + final disks = ss.disk; + disks.removeWhere((e) { for (final ingorePath in Stores.setting.diskIgnorePath.fetch()) { - if (e.path.startsWith(ingorePath)) return true; + if (e.dev.startsWith(ingorePath)) return true; } return false; }); - final children = disk - .map((disk) => Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${disk.usedPercent}% of ${disk.size}', - style: UIs.textSize11, - textScaleFactor: _textFactor, - ), - Text( - disk.path, - style: UIs.textSize11, - textScaleFactor: _textFactor, - ) - ], - ), - _buildProgress(disk.usedPercent.toDouble()) - ], - ), - )) - .toList(); + final children = + List.generate(disks.length, (idx) => _buildDiskItem(disks[idx], ss)); return CardX( - Padding( - padding: UIs.roundRectCardPadding, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, + ExpandTile( + title: Text('Disk'), + leading: Icon(Icons.storage, size: 17), + initiallyExpanded: children.length <= 7, + children: children, + ), + ); + } + + Widget _buildDiskItem(Disk disk, ServerStatus ss) { + final (read, write) = ss.diskIO.getReadSpeed(disk.dev); + return ListTile( + title: Text( + disk.dev, + style: UIs.textSize13Bold, + textScaleFactor: _textFactor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17), + subtitle: Text( + '${disk.usedPercent}% of ${disk.size}\n↑ $read | ↓ $write', + style: UIs.textSize11, + textScaleFactor: _textFactor, + ), + trailing: SizedBox( + height: 37, + width: 37, + child: CircularProgressIndicator( + value: disk.usedPercent / 100, + strokeWidth: 7, + backgroundColor: DynamicColors.progress.resolve(context), + valueColor: AlwaysStoppedAnimation(primaryColor), ), ), ); } Widget _buildNetView(ServerStatus ss) { - return CardX( - Padding( - padding: UIs.roundRectCardPadding, - child: ValueBuilder( - listenable: _netSortType, - build: () { - final ns = ss.netSpeed; - final children = [ - _buildNetSpeedTop(), - const Divider( - height: 7, - ) - ]; - if (ns.devices.isEmpty) { - children.add(Center( - child: Text( - l10n.noInterface, - style: const TextStyle(color: Colors.grey, fontSize: 13), - ), - )); - } else { - final devices = ns.devices; - devices.sort(_netSortType.value.getSortFunc(ns)); - children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e))); - } - return Column( - children: children, - ); - }, + final ns = ss.netSpeed; + final children = []; + if (ns.devices.isEmpty) { + children.add(Center( + child: Text( + l10n.noInterface, + style: const TextStyle(color: Colors.grey, fontSize: 13), + ), + )); + } else { + final devices = ns.devices; + devices.sort(_netSortType.value.getSortFunc(ns)); + children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e))); + } + return ValueBuilder( + listenable: _netSortType, + build: () { + return CardX( + ExpandTile( + title: Text('Net'), + leading: Icon(Icons.device_hub, size: 17), + initiallyExpanded: children.length <= 7, + children: children, + ), + ); + }, + ); + } + + Widget _buildNetSpeedItem(NetSpeed ns, String device) { + return ListTile( + title: Text( + device, + style: UIs.textSize13Bold, + textScaleFactor: _textFactor, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + '${ns.sizeIn(device: device)} | ${ns.sizeOut(device: device)}', + style: UIs.textSize11, + textScaleFactor: _textFactor, + ), + trailing: SizedBox( + width: 170, + child: Text( + '↑ ${ns.speedOut(device: device)}\n↓ ${ns.speedIn(device: device)}', + textAlign: TextAlign.end, ), ), ); } - Widget _buildNetSpeedTop() { - const icon = Icon(Icons.arrow_downward, size: 13); - return Padding( - padding: const EdgeInsets.only(bottom: 3), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - child: _netSortType.value.isDevice - ? const Row( - children: [ - Text('Iface'), - icon, - ], - ) - : const Text('Iface'), - onTap: () => _netSortType.value = _NetSortType.device, - ), - GestureDetector( - child: _netSortType.value.isIn - ? const Row( - children: [ - Text('Recv'), - icon, - ], - ) - : const Text('Recv'), - onTap: () => _netSortType.value = _NetSortType.recv, - ), - GestureDetector( - child: _netSortType.value.isOut - ? const Row( - children: [ - Text('Trans'), - icon, - ], - ) - : const Text('Trans'), - onTap: () => _netSortType.value = _NetSortType.trans, - ), - ], - ), - ); - } - - Widget _buildNetSpeedItem(NetSpeed ns, String device) { - final width = (_media.size.width - 34 - 34) / 3; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: width, - child: Text( - device, - style: UIs.textSize11, - textScaleFactor: _textFactor, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - width: width, - child: Text( - '${ns.speedIn(device: device)} | ${ns.sizeIn(device: device)}', - style: UIs.textSize11, - textAlign: TextAlign.center, - textScaleFactor: 0.87 * _textFactor, - ), - ), - SizedBox( - width: width, - child: Text( - '${ns.speedOut(device: device)} | ${ns.sizeOut(device: device)}', - style: UIs.textSize11, - textAlign: TextAlign.right, - textScaleFactor: 0.87 * _textFactor, - ), - ) - ], - ), - ); - } - Widget _buildTemperature(ServerStatus ss) { - final temps = ss.temps; - if (temps.isEmpty) { + if (ss.temps.isEmpty) { return UIs.placeholder; } - final List children = [ - const Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon(Icons.device_hub, size: 17), - Icon(Icons.ac_unit, size: 17), - ], - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 3), - child: Divider(height: 7), - ), - ]; - children.addAll(temps.devices.map((key) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - key, - style: UIs.textSize11, - textScaleFactor: _textFactor, - ), - Text( - '${temps.get(key)}°C', - style: UIs.textSize11, - textScaleFactor: _textFactor, - ), - ], - ))); return CardX( - Padding( - padding: UIs.roundRectCardPadding, - child: Column(children: children), + ExpandTile( + title: Text('Temperature'), + leading: const Icon(Icons.ac_unit, size: 17), + initiallyExpanded: ss.temps.devices.length <= 7, + children: ss.temps.devices + .map((key) => _buildTemperatureItem(key, ss.temps.get(key))) + .toList(), ), ); } + Widget _buildTemperatureItem(String key, double? val) { + return ListTile( + title: Text(key, style: UIs.textSize13Bold), + trailing: Text('${val?.toStringAsFixed(1)}°C'), + ); + } + Widget _buildAnimatedText(Key key, String text, TextStyle style) { return AnimatedSwitcher( duration: const Duration(milliseconds: 277), diff --git a/test/status_prase_test.dart b/test/status_prase_test.dart index 120af09c..70b46c8f 100644 --- a/test/status_prase_test.dart +++ b/test/status_prase_test.dart @@ -82,6 +82,6 @@ Overlay 3.0T 1.4t 1.6T 48%/Share/CacheDev1_data/Container/Container-SATA/LIB/DOC 3.0T 1.4T 1.6T 48% /mnt/snapshot/1/10016 '''; final disks = parseDisk(raw); - print(disks.map((e) => '${e.loc} ${e.used}').join('\n')); + print(disks.map((e) => '${e.mount} ${e.used}').join('\n')); }); }