mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
new: detail status page
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 (.+):');
|
||||
|
||||
|
||||
@@ -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([], []),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user