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,
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 {

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';
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<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) {
final list = <Disk>[];
final items = raw.split('\n');
@@ -38,8 +115,8 @@ List<Disk> 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<Disk> parseDisk(String raw) {
/// the fps may lower than 60.
Disk? findRootDisk(List<Disk> 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 {

View File

@@ -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,
});
}

View File

@@ -35,8 +35,10 @@ Future<ServerStatus> getStatus(ServerStatusUpdateReq req) async {
Future<ServerStatus> _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<ServerStatus> _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;
}

View File

@@ -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;
}

View File

@@ -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 (.+):');

View File

@@ -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([], []),
);
}

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/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<ServerDetailPage>
}
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 = <Widget>[
_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 = <Widget>[];
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<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(
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),

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
''';
final disks = parseDisk(raw);
print(disks.map((e) => '${e.loc} ${e.used}').join('\n'));
print(disks.map((e) => '${e.mount} ${e.used}').join('\n'));
});
}