#64 new: process page

This commit is contained in:
lollipopkit
2023-06-23 14:30:35 +08:00
parent 06d6500ef6
commit 743e5bfbff
7 changed files with 290 additions and 23 deletions

View File

@@ -360,7 +360,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -368,7 +368,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -491,7 +491,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -499,7 +499,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@@ -516,7 +516,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
DEVELOPMENT_TEAM = BA88US33G6; DEVELOPMENT_TEAM = BA88US33G6;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist";
@@ -524,7 +524,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View File

@@ -0,0 +1,123 @@
// Models for `ps -aux`
// Each line
import 'dart:convert';
class Proc {
final String user;
final int pid;
final double cpu;
final double mem;
final String vsz;
final String rss;
final String tty;
final String stat;
final String start;
final String time;
final String command;
Proc({
required this.user,
required this.pid,
required this.cpu,
required this.mem,
required this.vsz,
required this.rss,
required this.tty,
required this.stat,
required this.start,
required this.time,
required this.command,
});
factory Proc.parse(String raw) {
final parts = raw.split(RegExp(r'\s+'));
return Proc(
user: parts[0],
pid: int.parse(parts[1]),
cpu: double.parse(parts[2]),
mem: double.parse(parts[3]),
vsz: parts[4],
rss: parts[5],
tty: parts[6],
stat: parts[7],
start: parts[8],
time: parts[9],
command: parts.sublist(10).join(' '),
);
}
Map toJson() {
return {
'user': user,
'pid': pid,
'cpu': cpu,
'mem': mem,
'vsz': vsz,
'rss': rss,
'tty': tty,
'stat': stat,
'start': start,
'time': time,
'command': command,
};
}
@override
String toString() {
return const JsonEncoder.withIndent(' ').convert(toJson());
}
String get binary {
final parts = command.split(' ');
return parts[0];
}
}
// `ps -aux` result
class PsResult {
final List<Proc> procs;
final String? error;
PsResult({
required this.procs,
this.error,
});
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
final lines = raw.split('\n');
final procs = <Proc>[];
var err = '';
for (var i = 1; i < lines.length; i++) {
final line = lines[i];
if (line.isEmpty) continue;
try {
procs.add(Proc.parse(line));
} catch (e) {
err += '$line: $e\n';
}
}
switch (sort) {
case ProcSortMode.cpu:
procs.sort((a, b) => b.cpu.compareTo(a.cpu));
break;
case ProcSortMode.mem:
procs.sort((a, b) => b.mem.compareTo(a.mem));
break;
case ProcSortMode.pid:
procs.sort((a, b) => a.pid.compareTo(b.pid));
break;
case ProcSortMode.user:
procs.sort((a, b) => a.user.compareTo(b.user));
break;
}
return PsResult(procs: procs, error: err.isEmpty ? null : err);
}
}
enum ProcSortMode {
cpu,
mem,
pid,
user;
}

View File

@@ -2,8 +2,8 @@
class BuildData { class BuildData {
static const String name = "ServerBox"; static const String name = "ServerBox";
static const int build = 357; static const int build = 361;
static const String engine = "3.10.0"; static const String engine = "3.10.0";
static const String buildAt = "2023-06-08 22:45:50.770980"; static const String buildAt = "2023-06-21 18:31:01.595350";
static const int modifications = 3; static const int modifications = 2;
} }

139
lib/view/page/process.dart Normal file
View File

@@ -0,0 +1,139 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/stringx.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils/ui.dart';
import 'package:toolbox/data/model/server/proc.dart';
import 'package:toolbox/data/model/server/server_private_info.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/round_rect_card.dart';
import 'package:toolbox/view/widget/two_line_text.dart';
import '../../data/provider/server.dart';
import '../../locator.dart';
class ProcessPage extends StatefulWidget {
final ServerPrivateInfo spi;
const ProcessPage({super.key, required this.spi});
@override
_ProcessPageState createState() => _ProcessPageState();
}
class _ProcessPageState extends State<ProcessPage> {
late S _s;
late Timer _timer;
PsResult _result = PsResult(procs: []);
int? _lastFocusId;
ProcSortMode _procSortMode = ProcSortMode.cpu;
final _serverProvider = locator<ServerProvider>();
@override
void initState() {
super.initState();
final client = _serverProvider.servers[widget.spi.id]?.client;
if (client == null) {
showSnackBar(context, Text(_s.noClient));
return;
}
_timer = Timer.periodic(const Duration(seconds: 3), (_) async {
if (mounted) {
final result = await client.run('ps -aux'.withLangExport).string;
if (result.isEmpty) {
showSnackBar(context, Text(_s.noResult));
return;
}
_result = PsResult.parse(result, sort: _procSortMode);
setState(() {});
} else {
_timer.cancel();
}
});
}
@override
void dispose() {
super.dispose();
_timer.cancel();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_s = S.of(context)!;
}
@override
Widget build(BuildContext context) {
final actions = <Widget>[
PopupMenuButton<ProcSortMode>(
onSelected: (value) {
setState(() {
_procSortMode = value;
});
},
icon: const Icon(Icons.sort),
initialValue: _procSortMode,
itemBuilder: (_) => ProcSortMode.values
.map(
(e) => PopupMenuItem(value: e, child: Text(e.name)),
)
.toList(),
),
];
if (_result.error != null) {
actions.add(IconButton(
icon: const Icon(Icons.error),
onPressed: () => showRoundDialog(
context: context,
child: Text(_result.error!),
),
));
}
Widget child;
if (_result.procs.isEmpty) {
child = centerLoading;
} else {
child = ListView.builder(
itemCount: _result.procs.length,
itemBuilder: (ctx, idx) {
final proc = _result.procs[idx];
return _buildListItem(proc);
},
);
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: TwoLineText(up: widget.spi.name, down: _s.process),
actions: actions,
),
body: child,
);
}
Widget _buildListItem(Proc proc) {
return RoundRectCard(ListTile(
leading: SizedBox(
width: 57,
child: TwoLineText(up: proc.pid.toString(), down: 'pid'),
),
title: Text(proc.binary),
subtitle: Text(proc.command, style: grey),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'),
width13,
TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'),
],
),
onTap: () => _lastFocusId = proc.pid,
autofocus: _lastFocusId == proc.pid,
));
}
}

View File

@@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/navigator.dart';
import 'package:toolbox/core/extension/order.dart'; import 'package:toolbox/core/extension/order.dart';
import 'package:toolbox/core/utils/misc.dart'; import 'package:toolbox/core/utils/misc.dart';
import 'package:toolbox/data/res/server_cmd.dart'; import 'package:toolbox/view/page/process.dart';
import '../../../core/route.dart'; import '../../../core/route.dart';
import '../../../core/utils/ui.dart'; import '../../../core/utils/ui.dart';
@@ -338,13 +338,14 @@ class _ServerPageState extends State<ServerPage>
AppRoute(DockerManagePage(spi), 'Docker manage').go(context); AppRoute(DockerManagePage(spi), 'Docker manage').go(context);
break; break;
case ServerTabMenuType.process: case ServerTabMenuType.process:
AppRoute( // AppRoute(
SSHPage( // SSHPage(
spi: spi, // spi: spi,
initCmd: 'sh $shellPath -${shellFuncProcess.flag}', // initCmd: 'sh $shellPath -${shellFuncProcess.flag}',
), // ),
'ssh page (process)', // 'ssh page (process)',
).go(context); // ).go(context);
AppRoute(ProcessPage(spi: spi), 'process page').go(context);
break; break;
} }
}, },

View File

@@ -10,14 +10,18 @@ class TwoLineText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
up, up,
style: textSize15, style: textSize15,
overflow: TextOverflow.ellipsis,
), ),
Text( Text(
down, down,
style: textSize11, style: textSize11,
overflow: TextOverflow.ellipsis,
) )
], ],
); );

View File

@@ -475,9 +475,9 @@
baseConfigurationReference = C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -490,9 +490,9 @@
baseConfigurationReference = 15AF97DF993E8968098D6EBE /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = 15AF97DF993E8968098D6EBE /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -505,9 +505,9 @@
baseConfigurationReference = 7CFA7DE7FABA75685DFB6948 /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = 7CFA7DE7FABA75685DFB6948 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 357; CURRENT_PROJECT_VERSION = 361;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0.357; MARKETING_VERSION = 1.0.361;
PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;