import 'dart:async'; import 'dart:convert'; import 'package:dartssh2/dartssh2.dart'; import 'package:logging/logging.dart'; import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/data/model/server/cpu_2_status.dart'; import 'package:toolbox/data/model/server/cpu_status.dart'; import 'package:toolbox/data/model/server/memory.dart'; import 'package:toolbox/data/model/server/net_speed.dart'; import 'package:toolbox/data/model/server/server_connection_state.dart'; import 'package:toolbox/data/model/server/disk_info.dart'; import 'package:toolbox/data/model/server/server.dart'; import 'package:toolbox/data/model/server/server_private_info.dart'; import 'package:toolbox/data/model/server/server_status.dart'; import 'package:toolbox/data/model/server/snippet.dart'; import 'package:toolbox/data/model/server/tcp_status.dart'; import 'package:toolbox/data/store/server.dart'; import 'package:toolbox/data/store/setting.dart'; import 'package:toolbox/locator.dart'; class ServerProvider extends BusyProvider { List _servers = []; List get servers => _servers; Timer? _timer; final logger = Logger('ServerProvider'); Memory get emptyMemory => Memory(total: 1, used: 0, free: 1, shared: 0, cache: 0, avail: 1); NetSpeedPart get emptyNetSpeedPart => NetSpeedPart('', 0, 0, 0); NetSpeed get emptyNetSpeed => NetSpeed([emptyNetSpeedPart], [emptyNetSpeedPart]); CpuStatus get emptyCpuStatus => CpuStatus('cpu', 0, 0, 0, 0, 0, 0, 0); Cpu2Status get emptyCpu2Status => Cpu2Status([emptyCpuStatus], [emptyCpuStatus], ''); ServerStatus get emptyStatus => ServerStatus( emptyCpu2Status, emptyMemory, 'Loading...', '', [DiskInfo('/', '/', 0, '0', '0', '0')], TcpStatus(0, 0, 0, 0), emptyNetSpeed); Future loadLocalData() async { setBusyState(true); final infos = locator().fetch(); _servers = List.generate(infos.length, (index) => genInfo(infos[index])); setBusyState(false); notifyListeners(); } ServerInfo genInfo(ServerPrivateInfo spi) { return ServerInfo( spi, emptyStatus, null, ServerConnectionState.disconnected); } Future genClient(ServerPrivateInfo spi) async { final socket = await SSHSocket.connect(spi.ip, spi.port); if (spi.authorization is String) { return SSHClient(socket, username: spi.user, onPasswordRequest: () => spi.authorization as String); } final auth = spi.authorization as Map; return SSHClient(socket, username: spi.user, identities: SSHKeyPair.fromPem(auth['privateKey'], auth['passphrase'])); } Future refreshData({int? idx}) async { if (idx != null) { final singleData = await _getData(_servers[idx].info, idx); if (singleData != null) { _servers[idx].status = singleData; notifyListeners(); } return; } try { await Future.wait(_servers.map((s) async { final idx = _servers.indexOf(s); final status = await _getData(s.info, idx); if (status != null) { _servers[idx].status = status; notifyListeners(); } })); } catch (e) { rethrow; } } Future startAutoRefresh() async { final duration = locator().serverStatusUpdateInterval.fetch()!; if (duration == 0) return; stopAutoRefresh(); Future.delayed(const Duration(milliseconds: 677), () => refreshData()); _timer = Timer.periodic(Duration(seconds: duration), (_) async { await refreshData(); }); } void stopAutoRefresh() { if (_timer != null) { _timer!.cancel(); _timer = null; } } void addServer(ServerPrivateInfo info) { _servers.add(genInfo(info)); locator().put(info); notifyListeners(); refreshData(idx: _servers.length - 1); } void delServer(ServerPrivateInfo info) { _servers.removeWhere((e) => e.info == info); locator().delete(info); notifyListeners(); } Future updateServer( ServerPrivateInfo old, ServerPrivateInfo newInfo) async { final idx = _servers.indexWhere((e) => e.info == old); _servers[idx].info = newInfo; _servers[idx].client = await genClient(newInfo); locator().update(old, newInfo); notifyListeners(); refreshData(idx: idx); } Future _getData(ServerPrivateInfo info, int idx) async { final client = _servers[idx].client; final connected = client != null; final state = _servers[idx].connectionState; if (!connected || state == ServerConnectionState.failed || state == ServerConnectionState.disconnected) { _servers[idx].connectionState = ServerConnectionState.connecting; notifyListeners(); final time1 = DateTime.now(); try { _servers[idx].client = await genClient(info); final time2 = DateTime.now(); logger.info( 'Connected to [${info.name}] in [${time2.difference(time1).toString()}].'); _servers[idx].connectionState = ServerConnectionState.connected; notifyListeners(); } catch (e) { _servers[idx].connectionState = ServerConnectionState.failed; _servers[idx].status.failedInfo = e.toString(); notifyListeners(); logger.warning(e); } } try { if (!connected) return null; final cpu = utf8.decode(await client!.run("cat /proc/stat | grep cpu")); final cpuTemp = utf8.decode(await client.run( r"paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/'")); final mem = utf8.decode(await client.run('free -m')); final sysVer = utf8 .decode(await client.run('cat /etc/os-release | grep PRETTY_NAME')); final upTime = utf8.decode(await client.run('uptime')); final disk = utf8.decode(await client.run('df -h')); final tcp = utf8.decode(await client.run('cat /proc/net/snmp')); final netSpeed = utf8.decode(await client.run('cat /proc/net/dev && date +%s')); return ServerStatus( _getCPU(cpu, _servers[idx].status.cpu2Status, cpuTemp), _getMem(mem), _getSysVer(sysVer), _getUpTime(upTime), _getDisk(disk), _getTcp(tcp), _getNetSpeed(netSpeed, _servers[idx].status.netSpeed)); } catch (e) { _servers[idx].connectionState = ServerConnectionState.failed; notifyListeners(); logger.warning(e); return null; } } /// [raw] example: /// Inter-| Receive | Transmit /// face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed /// lo: 45929941 269112 0 0 0 0 0 0 45929941 269112 0 0 0 0 0 0 /// eth0: 48481023 505772 0 0 0 0 0 0 36002262 202307 0 0 0 0 0 0 /// 1635752901 NetSpeed _getNetSpeed(String raw, NetSpeed old) { final split = raw.split('\n'); final deviceCount = split.length - 3; if (deviceCount < 1) return emptyNetSpeed; final time = int.parse(split[split.length - 2]); final results = []; for (int idx = 2; idx < deviceCount; idx++) { final data = split[idx].trim().split(':'); final device = data.first; final bytes = data.last.trim().split(' '); bytes.removeWhere((element) => element == ''); final bytesIn = int.parse(bytes.first); final bytesOut = int.parse(bytes[8]); results.add(NetSpeedPart(device, bytesIn, bytesOut, time)); } return old.update(results); } String _getSysVer(String raw) { final s = raw.split('='); if (s.length == 2) { return s[1].replaceAll('"', '').replaceFirst('\n', ''); } return ''; } String _getCPUTemp(String raw) { final split = raw.split('\n'); for (var item in split) { if (item.contains('x86_pkg_temp') || item.contains('cpu_thermal')) { return item.split(' ').last; } } return ''; } Cpu2Status _getCPU(String raw, Cpu2Status old, String temp) { final List cpus = []; for (var item in raw.split('\n')) { if (item == '') break; final id = item.split(' ').first; final matches = item.replaceFirst(id, '').trim().split(' '); cpus.add(CpuStatus( id, int.parse(matches[0]), int.parse(matches[1]), int.parse(matches[2]), int.parse(matches[3]), int.parse(matches[4]), int.parse(matches[5]), int.parse(matches[6]))); } if (cpus.isEmpty) { return emptyCpu2Status; } return old.update(cpus, _getCPUTemp(temp)); } String _getUpTime(String raw) { return raw.split('up ')[1].split(', ')[0]; } TcpStatus _getTcp(String raw) { final lines = raw.split('\n'); int idx = 0; for (var item in lines) { if (item.contains('Tcp:')) { idx++; } if (idx == 2) { final vals = item.split(RegExp(r'\s{1,}')); return TcpStatus(vals[5].i, vals[6].i, vals[7].i, vals[8].i); } } return TcpStatus(0, 0, 0, 0); } List _getDisk(String disk) { final list = []; final items = disk.split('\n'); for (var item in items) { if (items.indexOf(item) == 0 || item.isEmpty) { continue; } final vals = item.split(RegExp(r'\s{1,}')); list.add(DiskInfo(vals[0], vals[5], int.parse(vals[4].replaceFirst('%', '')), vals[2], vals[1], vals[3])); } return list; } Memory _getMem(String mem) { for (var item in mem.split('\n')) { if (item.contains('Mem:')) { final split = item.replaceFirst('Mem:', '').split(' '); split.removeWhere((e) => e == ''); final memList = split.map((e) => int.parse(e)).toList(); return Memory( total: memList[0], used: memList[1], free: memList[2], shared: memList[3], cache: memList[4], avail: memList[5]); } } return emptyMemory; } Future runSnippet(int idx, Snippet snippet) async { final result = await _servers[idx].client!.run(snippet.script); return utf8.decode(result); } }