new: detail status page

This commit is contained in:
lollipopkit
2023-10-31 19:41:54 +08:00
parent f2edd14117
commit 37df072711
9 changed files with 208 additions and 196 deletions

View File

@@ -199,7 +199,7 @@ enum StatusCmdType {
tempType, tempType,
tempVal, tempVal,
host, host,
; diskio;
} }
/// Cmds for linux server /// Cmds for linux server
@@ -216,6 +216,7 @@ const _statusCmds = [
'cat /sys/class/thermal/thermal_zone*/type', 'cat /sys/class/thermal/thermal_zone*/type',
'cat /sys/class/thermal/thermal_zone*/temp', 'cat /sys/class/thermal/thermal_zone*/temp',
'hostname', 'hostname',
'cat /proc/diskstats',
]; ];
enum DockerCmdType { enum DockerCmdType {

View File

@@ -1,16 +1,19 @@
import 'package:toolbox/core/extension/numx.dart';
import 'package:toolbox/data/model/server/time_seq.dart';
import '../../res/misc.dart'; import '../../res/misc.dart';
class Disk { class Disk {
final String path; final String dev;
final String loc; final String mount;
final int usedPercent; final int usedPercent;
final String used; final String used;
final String size; final String size;
final String avail; final String avail;
const Disk({ const Disk({
required this.path, required this.dev,
required this.loc, required this.mount,
required this.usedPercent, required this.usedPercent,
required this.used, required this.used,
required this.size, required this.size,
@@ -18,6 +21,80 @@ class Disk {
}); });
} }
class DiskIO extends TimeSeq<DiskIOPiece> {
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<DiskIOPiece> parse(String raw, int time) {
final lines = raw.split('\n');
if (lines.isEmpty) return [];
final items = <DiskIOPiece>[];
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<DiskIOPiece> {
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<Disk> parseDisk(String raw) { List<Disk> parseDisk(String raw) {
final list = <Disk>[]; final list = <Disk>[];
final items = raw.split('\n'); final items = raw.split('\n');
@@ -38,8 +115,8 @@ List<Disk> parseDisk(String raw) {
} }
try { try {
list.add(Disk( list.add(Disk(
path: vals[0], dev: vals[0],
loc: vals[5], mount: vals[5],
usedPercent: int.parse(vals[4].replaceFirst('%', '')), usedPercent: int.parse(vals[4].replaceFirst('%', '')),
used: vals[2], used: vals[2],
size: vals[1], size: vals[1],
@@ -62,9 +139,9 @@ List<Disk> parseDisk(String raw) {
/// the fps may lower than 60. /// the fps may lower than 60.
Disk? findRootDisk(List<Disk> disks) { Disk? findRootDisk(List<Disk> disks) {
if (disks.isEmpty) return null; if (disks.isEmpty) return null;
final roots = disks.where((element) => element.loc == '/'); final roots = disks.where((element) => element.mount == '/');
if (roots.isEmpty) { if (roots.isEmpty) {
final sysRoots = disks.where((element) => element.loc == '/sysroot'); final sysRoots = disks.where((element) => element.mount == '/sysroot');
if (sysRoots.isEmpty) { if (sysRoots.isEmpty) {
return disks.first; return disks.first;
} else { } else {

View File

@@ -19,6 +19,7 @@ class ServerStatus {
Temperatures temps; Temperatures temps;
SystemType system; SystemType system;
String? failedInfo; String? failedInfo;
DiskIO diskIO;
ServerStatus({ ServerStatus({
required this.cpu, required this.cpu,
@@ -31,6 +32,7 @@ class ServerStatus {
required this.swap, required this.swap,
required this.temps, required this.temps,
required this.system, required this.system,
required this.diskIO,
this.failedInfo, this.failedInfo,
}); });
} }

View File

@@ -35,8 +35,10 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async { Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
final segments = req.segments; final segments = req.segments;
final time = int.tryParse(StatusCmdType.time.find(segments)) ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
try { try {
final time = int.parse(StatusCmdType.time.find(segments));
final net = parseNetSpeed(StatusCmdType.net.find(segments), time); final net = parseNetSpeed(StatusCmdType.net.find(segments), time);
req.ss.netSpeed.update(net); req.ss.netSpeed.update(net);
} catch (e, s) { } catch (e, s) {
@@ -101,6 +103,13 @@ Future<ServerStatus> _getLinuxStatus(ServerStatusUpdateReq req) async {
} catch (e, s) { } catch (e, s) {
Loggers.parse.warning(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; return req.ss;
} }

View File

@@ -4,7 +4,7 @@ class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 618; static const int build = 618;
static const String engine = "3.13.8"; static const String engine = "3.13.8";
static const String buildAt = "2023-10-30 17:18:16"; static const String buildAt = "2023-10-31 15:59:28";
static const int modifications = 5; static const int modifications = 7;
static const int script = 23; static const int script = 23;
} }

View File

@@ -6,6 +6,8 @@ class Miscs {
/// RegExp for number /// RegExp for number
static final numReg = RegExp(r'\s{1,}'); static final numReg = RegExp(r'\s{1,}');
static final blankReg = RegExp(r'\s+');
/// RegExp for password request /// RegExp for password request
static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):'); static final pwdRequestWithUserReg = RegExp(r'\[sudo\] password for (.+):');

View File

@@ -46,8 +46,8 @@ class InitStatus {
uptime: '', uptime: '',
disk: [ disk: [
const Disk( const Disk(
path: '/', dev: '/',
loc: '/', mount: '/',
usedPercent: 0, usedPercent: 0,
used: '0', used: '0',
size: '0', size: '0',
@@ -63,5 +63,6 @@ class InitStatus {
), ),
system: SystemType.linux, system: SystemType.linux,
temps: Temperatures(), temps: Temperatures(),
diskIO: DiskIO([], []),
); );
} }

View File

@@ -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/context/locale.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/data/model/server/cpu.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/net_speed.dart';
import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/model/server/system.dart'; import 'package:toolbox/data/model/server/system.dart';
import 'package:toolbox/data/res/store.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/server_func_btns.dart';
import 'package:toolbox/view/widget/value_notifier.dart'; import 'package:toolbox/view/widget/value_notifier.dart';
@@ -306,64 +308,55 @@ class _ServerDetailPageState extends State<ServerDetailPage>
} }
Widget _buildDiskView(ServerStatus ss) { Widget _buildDiskView(ServerStatus ss) {
final disk = ss.disk; final disks = ss.disk;
disk.removeWhere((e) { disks.removeWhere((e) {
for (final ingorePath in Stores.setting.diskIgnorePath.fetch()) { for (final ingorePath in Stores.setting.diskIgnorePath.fetch()) {
if (e.path.startsWith(ingorePath)) return true; if (e.dev.startsWith(ingorePath)) return true;
} }
return false; return false;
}); });
final children = disk final children =
.map((disk) => Padding( List.generate(disks.length, (idx) => _buildDiskItem(disks[idx], ss));
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();
return CardX( return CardX(
Padding( ExpandTile(
padding: UIs.roundRectCardPadding, title: Text('Disk'),
child: Column( leading: Icon(Icons.storage, size: 17),
mainAxisAlignment: MainAxisAlignment.center, initiallyExpanded: children.length <= 7,
children: children, 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) { Widget _buildNetView(ServerStatus ss) {
return CardX(
Padding(
padding: UIs.roundRectCardPadding,
child: ValueBuilder(
listenable: _netSortType,
build: () {
final ns = ss.netSpeed; final ns = ss.netSpeed;
final children = <Widget>[ final children = <Widget>[];
_buildNetSpeedTop(),
const Divider(
height: 7,
)
];
if (ns.devices.isEmpty) { if (ns.devices.isEmpty) {
children.add(Center( children.add(Center(
child: Text( child: Text(
@@ -376,141 +369,68 @@ class _ServerDetailPageState extends State<ServerDetailPage>
devices.sort(_netSortType.value.getSortFunc(ns)); devices.sort(_netSortType.value.getSortFunc(ns));
children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e))); children.addAll(devices.map((e) => _buildNetSpeedItem(ns, e)));
} }
return Column( return ValueBuilder(
listenable: _netSortType,
build: () {
return CardX(
ExpandTile(
title: Text('Net'),
leading: Icon(Icons.device_hub, size: 17),
initiallyExpanded: children.length <= 7,
children: children, children: children,
),
); );
}, },
),
),
);
}
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) { Widget _buildNetSpeedItem(NetSpeed ns, String device) {
final width = (_media.size.width - 34 - 34) / 3; return ListTile(
return Padding( title: Text(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: width,
child: Text(
device, device,
style: UIs.textSize11, style: UIs.textSize13Bold,
textScaleFactor: _textFactor, textScaleFactor: _textFactor,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), subtitle: Text(
SizedBox( '${ns.sizeIn(device: device)} | ${ns.sizeOut(device: device)}',
width: width,
child: Text(
'${ns.speedIn(device: device)} | ${ns.sizeIn(device: device)}',
style: UIs.textSize11, style: UIs.textSize11,
textAlign: TextAlign.center, textScaleFactor: _textFactor,
textScaleFactor: 0.87 * _textFactor,
), ),
), trailing: SizedBox(
SizedBox( width: 170,
width: width,
child: Text( child: Text(
'${ns.speedOut(device: device)} | ${ns.sizeOut(device: device)}', '${ns.speedOut(device: device)}\n ${ns.speedIn(device: device)}',
style: UIs.textSize11, textAlign: TextAlign.end,
textAlign: TextAlign.right,
textScaleFactor: 0.87 * _textFactor,
), ),
)
],
), ),
); );
} }
Widget _buildTemperature(ServerStatus ss) { Widget _buildTemperature(ServerStatus ss) {
final temps = ss.temps; if (ss.temps.isEmpty) {
if (temps.isEmpty) {
return UIs.placeholder; return UIs.placeholder;
} }
final List<Widget> 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( return CardX(
Padding( ExpandTile(
padding: UIs.roundRectCardPadding, title: Text('Temperature'),
child: Column(children: children), 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) { Widget _buildAnimatedText(Key key, String text, TextStyle style) {
return AnimatedSwitcher( return AnimatedSwitcher(
duration: const Duration(milliseconds: 277), duration: const Duration(milliseconds: 277),

View File

@@ -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 3.0T 1.4T 1.6T 48% /mnt/snapshot/1/10016
'''; ''';
final disks = parseDisk(raw); final disks = parseDisk(raw);
print(disks.map((e) => '${e.loc} ${e.used}').join('\n')); print(disks.map((e) => '${e.mount} ${e.used}').join('\n'));
}); });
} }