mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 23:34:24 +01:00
new: container stats (#272)
This commit is contained in:
@@ -40,7 +40,7 @@ enum ContainerErrType {
|
||||
invalidVersion,
|
||||
cmdNoPrefix,
|
||||
segmentsNotMatch,
|
||||
parsePsItem,
|
||||
parsePs,
|
||||
parseImages,
|
||||
parseStats,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/extension/numx.dart';
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
|
||||
abstract final class ContainerPs {
|
||||
@@ -9,7 +11,14 @@ abstract final class ContainerPs {
|
||||
String? get cmd;
|
||||
bool get running;
|
||||
|
||||
String? cpu;
|
||||
String? mem;
|
||||
String? net;
|
||||
String? disk;
|
||||
|
||||
factory ContainerPs.fromRawJson(String s, ContainerType typ) => typ.ps(s);
|
||||
|
||||
void parseStats(String s);
|
||||
}
|
||||
|
||||
final class PodmanPs implements ContainerPs {
|
||||
@@ -23,6 +32,11 @@ final class PodmanPs implements ContainerPs {
|
||||
final List<String>? names;
|
||||
final int? startedAt;
|
||||
|
||||
String? cpu;
|
||||
String? mem;
|
||||
String? net;
|
||||
String? disk;
|
||||
|
||||
PodmanPs({
|
||||
this.command,
|
||||
this.created,
|
||||
@@ -42,6 +56,23 @@ final class PodmanPs implements ContainerPs {
|
||||
@override
|
||||
bool get running => exited != true;
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
final stats = json.decode(s);
|
||||
final cpuD = (stats['CPU'] as double? ?? 0).toStringAsFixed(1);
|
||||
final cpuAvgD = (stats['AvgCPU'] as double? ?? 0).toStringAsFixed(1);
|
||||
cpu = '$cpuD% / ${l10n.pingAvg} $cpuAvgD%';
|
||||
final memLimit = (stats['MemLimit'] as int? ?? 0).bytes2Str;
|
||||
final memUsage = (stats['MemUsage'] as int? ?? 0).bytes2Str;
|
||||
mem = '$memUsage / $memLimit';
|
||||
final netIn = (stats['NetInput'] as int? ?? 0).bytes2Str;
|
||||
final netOut = (stats['NetOutput'] as int? ?? 0).bytes2Str;
|
||||
net = '↓ $netIn / ↑ $netOut';
|
||||
final diskIn = (stats['BlockInput'] as int? ?? 0).bytes2Str;
|
||||
final diskOut = (stats['BlockOutput'] as int? ?? 0).bytes2Str;
|
||||
disk = '${l10n.read} $diskOut / ${l10n.write} $diskIn';
|
||||
}
|
||||
|
||||
factory PodmanPs.fromRawJson(String str) =>
|
||||
PodmanPs.fromJson(json.decode(str));
|
||||
|
||||
@@ -84,6 +115,11 @@ final class DockerPs implements ContainerPs {
|
||||
final String? names;
|
||||
final String? state;
|
||||
|
||||
String? cpu;
|
||||
String? mem;
|
||||
String? net;
|
||||
String? disk;
|
||||
|
||||
DockerPs({
|
||||
this.command,
|
||||
this.createdAt,
|
||||
@@ -102,6 +138,15 @@ final class DockerPs implements ContainerPs {
|
||||
@override
|
||||
bool get running => state == 'running';
|
||||
|
||||
@override
|
||||
void parseStats(String s) {
|
||||
final stats = json.decode(s);
|
||||
cpu = stats['CPUPerc'];
|
||||
mem = stats['MemUsage'];
|
||||
net = stats['NetIO'];
|
||||
disk = stats['BlockIO'];
|
||||
}
|
||||
|
||||
factory DockerPs.fromRawJson(String str) =>
|
||||
DockerPs.fromJson(json.decode(str));
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Containerd {
|
||||
final ContainerdClient client;
|
||||
|
||||
Containerd({
|
||||
required this.client,
|
||||
});
|
||||
|
||||
factory Containerd.fromRawJson(String str) =>
|
||||
Containerd.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory Containerd.fromJson(Map<String, dynamic> json) => Containerd(
|
||||
client: ContainerdClient.fromJson(json["Client"]),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"Client": client.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class ContainerdClient {
|
||||
final String apiVersion;
|
||||
final String version;
|
||||
final String goVersion;
|
||||
final String gitCommit;
|
||||
final String builtTime;
|
||||
final String os;
|
||||
|
||||
ContainerdClient({
|
||||
required this.apiVersion,
|
||||
required this.version,
|
||||
required this.goVersion,
|
||||
required this.gitCommit,
|
||||
required this.builtTime,
|
||||
required this.os,
|
||||
});
|
||||
|
||||
factory ContainerdClient.fromRawJson(String str) =>
|
||||
ContainerdClient.fromJson(json.decode(str));
|
||||
|
||||
String toRawJson() => json.encode(toJson());
|
||||
|
||||
factory ContainerdClient.fromJson(Map<String, dynamic> json) =>
|
||||
ContainerdClient(
|
||||
apiVersion: json["ApiVersion"],
|
||||
version: json["Version"],
|
||||
goVersion: json["GoVersion"],
|
||||
gitCommit: json["GitCommit"],
|
||||
builtTime: json["BuildTime"],
|
||||
os: json["Os"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"ApiVersion": apiVersion,
|
||||
"Version": version,
|
||||
"GoVersion": goVersion,
|
||||
"GitCommit": gitCommit,
|
||||
"BuildTime": builtTime,
|
||||
"Os": os,
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/listx.dart';
|
||||
import 'package:toolbox/core/extension/ssh_client.dart';
|
||||
import 'package:toolbox/data/model/app/shell_func.dart';
|
||||
import 'package:toolbox/data/model/container/image.dart';
|
||||
import 'package:toolbox/data/model/container/ps.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
import 'package:toolbox/data/model/container/type.dart';
|
||||
import 'package:toolbox/data/model/container/version.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
@@ -74,14 +75,20 @@ class ContainerProvider extends ChangeNotifier {
|
||||
|
||||
final sudo =
|
||||
await _requiresSudo() && Stores.setting.containerTrySudo.fetch();
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
await client?.execWithPwd(
|
||||
_wrap(ContainerCmdType.execAll(type, sudo: sudo)),
|
||||
final code = await client?.execWithPwd(
|
||||
_wrap(ContainerCmdType.execAll(
|
||||
type,
|
||||
sudo: sudo,
|
||||
includeStats: includeStats,
|
||||
)),
|
||||
context: context,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
);
|
||||
|
||||
if (raw.contains(_dockerNotFound)) {
|
||||
/// Code 127 means command not found
|
||||
if (code == 127 || raw.contains(_dockerNotFound)) {
|
||||
error = ContainerErr(type: ContainerErrType.notInstalled);
|
||||
notifyListeners();
|
||||
return;
|
||||
@@ -99,12 +106,10 @@ class ContainerProvider extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse docker version
|
||||
// Parse version
|
||||
final verRaw = ContainerCmdType.version.find(segments);
|
||||
debugPrint('version raw = $verRaw\n');
|
||||
try {
|
||||
final containerVersion = Containerd.fromRawJson(verRaw);
|
||||
version = containerVersion.client.version;
|
||||
version = json.decode(verRaw)['Client']['Version'];
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.invalidVersion,
|
||||
@@ -115,14 +120,23 @@ class ContainerProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker ps
|
||||
// Parse ps
|
||||
final psRaw = ContainerCmdType.ps.find(segments);
|
||||
try {
|
||||
final lines = psRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parsePs,
|
||||
message: '$e',
|
||||
);
|
||||
Loggers.parse.warning('Container ps failed', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final lines = psRaw.split('\n');
|
||||
lines.removeWhere((element) => element.isEmpty);
|
||||
items = lines.map((e) => ContainerPs.fromRawJson(e, type)).toList();
|
||||
|
||||
// Parse docker images
|
||||
// Parse images
|
||||
final imageRaw = ContainerCmdType.images.find(segments);
|
||||
try {
|
||||
final imgLines = imageRaw.split('\n');
|
||||
@@ -138,29 +152,31 @@ class ContainerProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Parse docker stats
|
||||
// final statsRaw = DockerCmdType.stats.find(segments);
|
||||
// try {
|
||||
// final statsLines = statsRaw.split('\n');
|
||||
// statsLines.removeWhere((element) => element.isEmpty);
|
||||
// if (statsLines.isNotEmpty) statsLines.removeAt(0);
|
||||
// for (var item in items!) {
|
||||
// final statsLine = statsLines.firstWhere(
|
||||
// (element) => element.contains(item.containerId),
|
||||
// orElse: () => '',
|
||||
// );
|
||||
// if (statsLine.isEmpty) continue;
|
||||
// item.parseStats(statsLine);
|
||||
// }
|
||||
// } catch (e, trace) {
|
||||
// error = DockerErr(
|
||||
// type: DockerErrType.parseStats,
|
||||
// message: '$e',
|
||||
// );
|
||||
// _logger.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
// } finally {
|
||||
// notifyListeners();
|
||||
// }
|
||||
// Parse stats
|
||||
final statsRaw = ContainerCmdType.stats.find(segments);
|
||||
try {
|
||||
final statsLines = statsRaw.split('\n');
|
||||
statsLines.removeWhere((element) => element.isEmpty);
|
||||
for (var item in items!) {
|
||||
final id = item.id;
|
||||
if (id == null) continue;
|
||||
final statsLine = statsLines.firstWhereOrNull(
|
||||
/// Use 5 characters to match the container id, possibility of mismatch
|
||||
/// is very low.
|
||||
(element) => element.contains(id.substring(0, 5)),
|
||||
);
|
||||
if (statsLine == null) continue;
|
||||
item.parseStats(statsLine);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
error = ContainerErr(
|
||||
type: ContainerErrType.parseStats,
|
||||
message: '$e',
|
||||
);
|
||||
Loggers.parse.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<ContainerErr?> stop(String id) async => await run('stop $id');
|
||||
@@ -223,21 +239,32 @@ const _jsonFmt = '--format "{{json .}}"';
|
||||
enum ContainerCmdType {
|
||||
version,
|
||||
ps,
|
||||
//stats,
|
||||
stats,
|
||||
images,
|
||||
;
|
||||
|
||||
String exec(ContainerType type, {bool sudo = false}) {
|
||||
String exec(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
|
||||
return switch (this) {
|
||||
ContainerCmdType.version => '$prefix version $_jsonFmt',
|
||||
ContainerCmdType.ps => '$prefix ps -a $_jsonFmt',
|
||||
// DockerCmdType.stats => '$prefix stats --no-stream';
|
||||
ContainerCmdType.stats =>
|
||||
includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
|
||||
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
|
||||
};
|
||||
}
|
||||
|
||||
static String execAll(ContainerType type, {bool sudo = false}) => values
|
||||
.map((e) => e.exec(type, sudo: sudo))
|
||||
.join(' && echo $seperator && ');
|
||||
static String execAll(
|
||||
ContainerType type, {
|
||||
bool sudo = false,
|
||||
bool includeStats = false,
|
||||
}) {
|
||||
return ContainerCmdType.values
|
||||
.map((e) => e.exec(type, sudo: sudo))
|
||||
.join(' && echo $seperator && ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +233,9 @@ class SettingStore extends PersistentStore {
|
||||
/// Keep previous server status when err occurs
|
||||
late final keepStatusWhenErr = property('keepStatusWhenErr', false);
|
||||
|
||||
/// Parse container stat
|
||||
late final containerParseStat = property('containerParseStat', true);
|
||||
|
||||
// Never show these settings for users
|
||||
//
|
||||
// ------BEGIN------
|
||||
|
||||
Reference in New Issue
Block a user