Support APT/Docker

This commit is contained in:
Junyuan Feng
2022-03-10 15:25:14 +08:00
parent e6e08dc407
commit f0081e0587
10 changed files with 143 additions and 93 deletions

View File

@@ -1,6 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
extension Uint8ListX on Future<Uint8List> { extension FutureUint8ListX on Future<Uint8List> {
Future<String> get string async => utf8.decode(await this); Future<String> get string async => utf8.decode(await this);
} }
extension Uint8ListX on Uint8List {
String get string => utf8.decode(this);
}

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/provider_base.dart'; import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart'; import 'package:toolbox/data/model/apt/upgrade_pkg_info.dart';
import 'package:toolbox/data/model/distribution.dart'; import 'package:toolbox/data/model/distribution.dart';
@@ -8,41 +7,68 @@ import 'package:toolbox/data/model/distribution.dart';
class AptProvider extends BusyProvider { class AptProvider extends BusyProvider {
SSHClient? client; SSHClient? client;
Distribution? dist; Distribution? dist;
String? whoami;
List<AptUpgradePkgInfo>? upgradeable; List<AptUpgradePkgInfo>? upgradeable;
String? error;
String? updateLog;
AptProvider(); AptProvider();
void init(SSHClient client, Distribution dist) { Future<void> init(SSHClient client, Distribution dist) async {
this.client = client; this.client = client;
this.dist = dist; this.dist = dist;
whoami = (await client.run('whoami').string).trim();
} }
bool get isSU => whoami == 'root';
void clear() { void clear() {
client = null; client = null;
dist = null; dist = null;
upgradeable = null; upgradeable = null;
} error = null;
updateLog = null;
bool get isReady { whoami = null;
return upgradeable != null && !isBusy;
} }
Future<void> refreshInstalled() async { Future<void> refreshInstalled() async {
if (client == null) {
error = 'No client';
return;
}
await update(); await update();
final result = utf8.decode(await client!.run('apt list --upgradeable')); final result = await client!.run('apt list --upgradeable').string;
final list = result.split('\n').sublist(4); try {
list.removeWhere((element) => element.isEmpty); final list = result.split('\n').sublist(4);
upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList(); list.removeWhere((element) => element.isEmpty);
notifyListeners(); upgradeable = list.map((e) => AptUpgradePkgInfo(e, dist!)).toList();
} catch (e) {
error = e.toString();
} finally {
notifyListeners();
}
} }
Future<void> update() async { Future<void> update() async {
if (client == null) {
error = 'No client';
return;
}
await client!.run('apt update'); await client!.run('apt update');
} }
// Future<void> upgrade() async { Future<void> upgrade() async {
// setBusyState(); if (client == null) {
// await client!.run('apt upgrade -y'); error = 'No client';
// refreshInstalled(); return;
// } }
updateLog = null;
final session = await client!.execute('apt upgrade -y');
session.stdout.listen((data) {
updateLog = (updateLog ?? '') + data.string;
notifyListeners();
});
refreshInstalled();
}
} }

View File

@@ -33,8 +33,12 @@ class DockerProvider extends BusyProvider {
error = 'invalid version'; error = 'invalid version';
notifyListeners(); notifyListeners();
} else { } else {
version = verSplit[1].split(' ').last; try {
edition = verSplit[0].split(': ')[1]; version = verSplit[1].split(' ').last;
edition = verSplit[0].split(': ')[1];
} catch (e) {
error = e.toString();
}
} }
final raw = await client!.run('docker ps -a').string; final raw = await client!.run('docker ps -a').string;
@@ -43,11 +47,17 @@ class DockerProvider extends BusyProvider {
notifyListeners(); notifyListeners();
return; return;
} }
final lines = raw.split('\n');
lines.removeAt(0); try {
lines.removeWhere((element) => element.isEmpty); final lines = raw.split('\n');
running = lines.map((e) => DockerPsItem.fromRawString(e)).toList(); lines.removeAt(0);
notifyListeners(); lines.removeWhere((element) => element.isEmpty);
running = lines.map((e) => DockerPsItem.fromRawString(e)).toList();
} catch (e) {
error = e.toString();
} finally {
notifyListeners();
}
} }
Future<bool> stop(String id) async { Future<bool> stop(String id) async {

View File

@@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:dartssh2/dartssh2.dart'; import 'package:dartssh2/dartssh2.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:toolbox/core/extension/stringx.dart'; import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/provider_base.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_2_status.dart';
import 'package:toolbox/data/model/server/cpu_status.dart'; import 'package:toolbox/data/model/server/cpu_status.dart';
@@ -178,8 +178,10 @@ class ServerProvider extends BusyProvider {
final si = _servers[idx]; final si = _servers[idx];
try { try {
if (si.client == null) return; if (si.client == null) return;
final raw = utf8.decode(await si.client!.run( final raw = await si.client!
r"cat /proc/net/dev && date +%s && echo 'A====A' && cat /etc/os-release | grep PRETTY_NAME && echo 'A====A' && cat /proc/stat | grep cpu && echo 'A====A' && paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/' && echo 'A====A' && uptime && echo 'A====A' && cat /proc/net/snmp && echo 'A====A' && df -h && echo 'A====A' && free -m")); .run(
r"cat /proc/net/dev && date +%s && echo 'A====A' && cat /etc/os-release | grep PRETTY_NAME && echo 'A====A' && cat /proc/stat | grep cpu && echo 'A====A' && paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/' && echo 'A====A' && uptime && echo 'A====A' && cat /proc/net/snmp && echo 'A====A' && df -h && echo 'A====A' && free -m")
.string;
final lines = raw.split('A====A').map((e) => e.trim()).toList(); final lines = raw.split('A====A').map((e) => e.trim()).toList();
_getCPU(spi, lines[2], lines[3]); _getCPU(spi, lines[2], lines[3]);
_getMem(spi, lines[7]); _getMem(spi, lines[7]);
@@ -321,10 +323,10 @@ class ServerProvider extends BusyProvider {
} }
Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async { Future<String?> runSnippet(ServerPrivateInfo spi, Snippet snippet) async {
final result = await _servers return await _servers
.firstWhere((element) => element.info == spi) .firstWhere((element) => element.info == spi)
.client! .client!
.run(snippet.script); .run(snippet.script)
return utf8.decode(result); .string;
} }
} }

View File

@@ -2,9 +2,9 @@
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 106; static const int build = 107;
static const String engine = static const String engine =
"Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 7e9793dee1 (6 days ago) • 2022-03-02 11:23:12 -0600\nEngine • revision bd539267b4\nTools • Dart 2.16.1 • DevTools 2.9.2\n"; "Flutter 2.10.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision 7e9793dee1 (8 days ago) • 2022-03-02 11:23:12 -0600\nEngine • revision bd539267b4\nTools • Dart 2.16.1 • DevTools 2.9.2\n";
static const String buildAt = "2022-03-08 18:06:40.014600"; static const String buildAt = "2022-03-10 13:25:24.362670";
static const int modifications = 8; static const int modifications = 11;
} }

View File

@@ -7,6 +7,7 @@ import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/provider/apt.dart'; import 'package:toolbox/data/provider/apt.dart';
import 'package:toolbox/data/provider/server.dart'; import 'package:toolbox/data/provider/server.dart';
import 'package:toolbox/locator.dart'; import 'package:toolbox/locator.dart';
import 'package:toolbox/view/widget/center_loading.dart';
import 'package:toolbox/view/widget/round_rect_card.dart'; import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/two_line_text.dart'; import 'package:toolbox/view/widget/two_line_text.dart';
@@ -55,61 +56,75 @@ class _AptManagePageState extends State<AptManagePage>
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'Apt', down: widget.spi.ip), title: TwoLineText(up: 'Apt', down: widget.spi.name),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
locator<AptProvider>().refreshInstalled();
},
),
],
), ),
body: Consumer<AptProvider>(builder: (_, apt, __) { body: Consumer<AptProvider>(builder: (_, apt, __) {
if (apt.upgradeable == null) { if (apt.upgradeable == null) {
apt.refreshInstalled(); apt.refreshInstalled();
return centerLoading;
}
if (!apt.isSU) {
return Center(
child: Text(
'Only supported as root. Not "${apt.whoami}".',
style: greyStyle,
),
);
} }
return ListView( return ListView(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(13),
children: [_buildUpdatePanel(apt)], children:
[_buildUpdatePanel(apt)].map((e) => RoundRectCard(e)).toList(),
); );
}), }),
); );
} }
Widget _buildUpdatePanel(AptProvider apt) { Widget _buildUpdatePanel(AptProvider apt) {
if (apt.upgradeable!.isEmpty) {
return const ListTile(
title: Text(
'No update available',
textAlign: TextAlign.center,
),
subtitle: Text('>_<', textAlign: TextAlign.center),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
RoundRectCard(ExpansionTile( ExpansionTile(
title: Text(!apt.isReady title: Text('Found ${apt.upgradeable!.length} update'),
? 'Loading...' subtitle: Text(
: 'Found ${apt.upgradeable!.length} update'), apt.upgradeable!.map((e) => e.package).join(', '),
subtitle: !apt.isReady maxLines: 1,
? null overflow: TextOverflow.ellipsis,
: Text( style: greyStyle,
apt.upgradeable!.map((e) => e.package).join(', '), ),
maxLines: 1, children: apt.updateLog == null
overflow: TextOverflow.ellipsis, ? [
style: greyStyle, TextButton(
), child: const Text('Update all'),
children: [ onPressed: () {
// TextButton( apt.upgrade();
// child: Text('Update all'), }),
// onPressed: () { SizedBox(
// apt.upgrade(); height: _media.size.height * 0.73,
// }),
!apt.isReady
? const SizedBox()
: SizedBox(
height: _media.size.height * 0.23,
child: ListView( child: ListView(
children: apt.upgradeable! children: apt.upgradeable!
.map((e) => _buildUpdateItem(e, apt)) .map((e) => _buildUpdateItem(e, apt))
.toList()), .toList()),
) )
], ]
)) : [
SizedBox(
height: _media.size.height * 0.73,
child: ListView(
padding: const EdgeInsets.all(18),
children: [Text(apt.updateLog!)],
))
],
)
], ],
); );
} }

View File

@@ -58,7 +58,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'Docker', down: widget.spi.ip), title: TwoLineText(up: 'Docker', down: widget.spi.name),
), ),
body: _buildMain(), body: _buildMain(),
); );
@@ -110,7 +110,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
Widget _buildVersion(String edition, String version) { Widget _buildVersion(String edition, String version) {
return Padding( return Padding(
padding: const EdgeInsets.all(13), padding: const EdgeInsets.all(17),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(edition), Text(version)], children: [Text(edition), Text(version)],
@@ -172,7 +172,7 @@ class _DockerManagePageState extends State<DockerManagePage> {
}, },
itemHeight: 37, itemHeight: 37,
itemPadding: const EdgeInsets.only(left: 17, right: 17), itemPadding: const EdgeInsets.only(left: 17, right: 17),
dropdownWidth: 160, dropdownWidth: 133,
dropdownDecoration: BoxDecoration( dropdownDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
), ),

View File

@@ -245,12 +245,7 @@ class _ServerPageState extends State<ServerPage>
AppRoute(AptManagePage(spi), 'apt manage page').go(context); AppRoute(AptManagePage(spi), 'apt manage page').go(context);
break; break;
case ServerTabMenuItems.sftp: case ServerTabMenuItems.sftp:
AppRoute( AppRoute(SFTPPage(spi), 'SFTP').go(context);
SFTPPage(
spi: spi,
),
'SFTP')
.go(context);
break; break;
case ServerTabMenuItems.snippet: case ServerTabMenuItems.snippet:
AppRoute( AppRoute(

View File

@@ -11,8 +11,8 @@ import 'package:toolbox/view/widget/fade_in.dart';
import 'package:toolbox/view/widget/two_line_text.dart'; import 'package:toolbox/view/widget/two_line_text.dart';
class SFTPPage extends StatefulWidget { class SFTPPage extends StatefulWidget {
final ServerPrivateInfo? spi; final ServerPrivateInfo spi;
const SFTPPage({this.spi, Key? key}) : super(key: key); const SFTPPage(this.spi, {Key? key}) : super(key: key);
@override @override
_SFTPPageState createState() => _SFTPPageState(); _SFTPPageState createState() => _SFTPPageState();
@@ -34,10 +34,8 @@ class _SFTPPageState extends State<SFTPPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.spi != null) { _status.spi = widget.spi;
_status.spi = widget.spi!; _status.selected = true;
_status.selected = true;
}
} }
@override @override
@@ -45,7 +43,7 @@ class _SFTPPageState extends State<SFTPPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: TwoLineText(up: 'SFTP', down: _status.spi?.ip ?? '...'), title: TwoLineText(up: 'SFTP', down: widget.spi.name),
), ),
body: _buildFileView(), body: _buildFileView(),
); );
@@ -98,6 +96,13 @@ class _SFTPPageState extends State<SFTPPage> {
return ListTile( return ListTile(
leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file), leading: Icon(isDir ? Icons.folder : Icons.insert_drive_file),
title: Text(file.filename), title: Text(file.filename),
trailing: Text(
DateTime.fromMillisecondsSinceEpoch(
(file.attr.modifyTime ?? 0) * 1000)
.toString()
.replaceFirst('.000', ''),
style: const TextStyle(color: Colors.grey),
),
subtitle: subtitle:
isDir ? null : Text(convertBytes(file.attr.size ?? 0)), isDir ? null : Text(convertBytes(file.attr.size ?? 0)),
onTap: () { onTap: () {

View File

@@ -335,13 +335,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -528,7 +521,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.8" version: "0.4.3"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: