This commit is contained in:
lollipopkit
2023-08-28 19:26:17 +08:00
parent d06ffdacd0
commit d9c26e01f4
3 changed files with 130 additions and 51 deletions

View File

@@ -43,8 +43,7 @@ enum AppShellFuncType {
switch (this) {
case AppShellFuncType.status:
return '''
result=\$(uname 2>&1 | grep "Linux")
if [ "\$result" != "" ]; then
if [ "\$isLinux" != "" ]; then
\t${_statusCmds.join(_cmdDivider)}
else
\t${_bsdStatusCmd.join(_cmdDivider)}
@@ -59,23 +58,16 @@ else
fi''';
case AppShellFuncType.process:
return '''
# Try sequencially
# main Linux: `ps -aux`
# BSD: `ps -ax`
# alpine: `ps -o pid,user,time,args`
#
# If there is any error, try another one
result=\$(ps -aux 2>&1 | grep "ps: ")
if [ "\$result" = "" ]; then
ps -aux
if [ "\$isLinux" != "" ]; then
\tif [ "\$isBusybox" != "" ]; then
\t\tps w
\telse
\t\tps -aux
\tfi
else
result=\$(ps -ax 2>&1 | grep "ps: ")
if [ "\$result" = "" ]; then
ps -ax
else
ps -o pid,user,time,args
fi
fi''';
\tps -ax
fi
''';
}
}
@@ -201,6 +193,10 @@ final _shellCmd = """
export LANG=en_US.UTF-8
isLinux=\$(uname 2>&1 | grep "Linux")
# Link /bin/sh to busybox?
isBusybox=\$(ls -l /bin/sh | grep "busybox")
${AppShellFuncType.shellScript}
""";

View File

@@ -1,8 +1,40 @@
import 'package:logging/logging.dart';
import '../../../data/res/misc.dart';
final _logger = Logger('Proc');
class _ProcValIdxMap {
final int pid;
final int? user;
final int? cpu;
final int? mem;
final int? vsz;
final int? rss;
final int? tty;
final int? stat;
final int? start;
final int? time;
final int command;
_ProcValIdxMap({
required this.pid,
this.user,
this.cpu,
this.mem,
this.vsz,
this.rss,
this.tty,
this.stat,
this.start,
this.time,
required this.command,
});
}
/// Some field can be null due to incompatible format on `BSD` and `Alpine`
class Proc {
final String user;
final String? user;
final int pid;
final double? cpu;
final double? mem;
@@ -11,37 +43,37 @@ class Proc {
final String? tty;
final String? stat;
final String? start;
final String time;
final String? time;
final String command;
Proc({
required this.user,
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,
this.cpu,
this.mem,
this.vsz,
this.rss,
this.tty,
this.stat,
this.start,
this.time,
required this.command,
});
factory Proc.parse(String raw) {
factory Proc.parse(String raw, _ProcValIdxMap map) {
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(' '),
user: map.user == null ? null : parts[map.user!],
pid: int.parse(parts[map.pid]),
cpu: map.cpu == null ? null : double.parse(parts[map.cpu!]),
mem: map.mem == null ? null : double.parse(parts[map.mem!]),
vsz: map.vsz == null ? null : parts[map.vsz!],
rss: map.rss == null ? null : parts[map.rss!],
tty: map.tty == null ? null : parts[map.tty!],
stat: map.stat == null ? null : parts[map.stat!],
start: map.start == null ? null : parts[map.start!],
time: map.time == null ? null : parts[map.time!],
command: parts.sublist(map.command).join(' '),
);
}
@@ -84,17 +116,38 @@ class PsResult {
factory PsResult.parse(String raw, {ProcSortMode sort = ProcSortMode.cpu}) {
final lines = raw.split('\n');
if (lines.isEmpty) return PsResult(procs: [], error: null);
final header = lines[0];
final parts = header.split(RegExp(r'\s+'));
parts.removeWhere((element) => element.isEmpty);
final map = _ProcValIdxMap(
pid: parts.indexOfOrNull('PID')!,
user: parts.indexOfOrNull('USER'),
cpu: parts.indexOfOrNull('%CPU'),
mem: parts.indexOfOrNull('%MEM'),
vsz: parts.indexOfOrNull('VSZ'),
rss: parts.indexOfOrNull('RSS'),
tty: parts.indexOfOrNull('TTY'),
stat: parts.indexOfOrNull('STAT'),
start: parts.indexOfOrNull('START'),
time: parts.indexOfOrNull('TIME'),
command: parts.indexOfOrNull('COMMAND') ?? parts.indexOfOrNull('CMD')!,
);
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) {
procs.add(Proc.parse(line, map));
} catch (e, trace) {
err += '$line: $e\n';
_logger.warning(trace);
}
}
switch (sort) {
case ProcSortMode.cpu:
procs.sort((a, b) => b.cpu?.compareTo(a.cpu ?? 0) ?? 0);
@@ -106,7 +159,7 @@ class PsResult {
procs.sort((a, b) => a.pid.compareTo(b.pid));
break;
case ProcSortMode.user:
procs.sort((a, b) => a.user.compareTo(b.user));
procs.sort((a, b) => a.user?.compareTo(b.user ?? '') ?? 0);
break;
case ProcSortMode.name:
procs.sort((a, b) => a.binary.compareTo(b.binary));
@@ -124,3 +177,10 @@ enum ProcSortMode {
name,
;
}
extension _StrIndex on List<String> {
int? indexOfOrNull(String val) {
final idx = indexOf(val);
return idx == -1 ? null : idx;
}
}

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:toolbox/core/extension/context.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils/misc.dart';
import '../../core/utils/ui.dart';
import '../../data/model/app/shell_func.dart';
@@ -12,6 +13,7 @@ import '../../data/model/server/proc.dart';
import '../../data/model/server/server_private_info.dart';
import '../../data/provider/server.dart';
import '../../data/res/ui.dart';
import '../../data/store/setting.dart';
import '../../locator.dart';
import '../widget/custom_appbar.dart';
import '../widget/round_rect_card.dart';
@@ -30,6 +32,8 @@ class _ProcessPageState extends State<ProcessPage> {
late Timer _timer;
late MediaQueryData _media;
final _setting = locator<SettingStore>();
SSHClient? _client;
PsResult _result = PsResult(procs: []);
@@ -39,7 +43,7 @@ class _ProcessPageState extends State<ProcessPage> {
// In cpu mode, the process list will change in a high frequency.
// So user will easily know that the list is refreshed.
ProcSortMode _procSortMode = ProcSortMode.cpu;
List<ProcSortMode> _sortModes = ProcSortMode.values;
List<ProcSortMode> _sortModes = List.from(ProcSortMode.values);
final _serverProvider = locator<ServerProvider>();
@@ -47,7 +51,9 @@ class _ProcessPageState extends State<ProcessPage> {
void initState() {
super.initState();
_client = _serverProvider.servers[widget.spi.id]?.client;
_timer = Timer.periodic(const Duration(seconds: 3), (_) => _refresh());
final duration =
Duration(seconds: _setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());
}
@override
@@ -110,7 +116,13 @@ class _ProcessPageState extends State<ProcessPage> {
onPressed: () => showRoundDialog(
context: context,
title: Text(_s.error),
child: Text(_result.error!),
child: SingleChildScrollView(child: Text(_result.error!)),
actions: [
TextButton(
onPressed: () => copy2Clipboard(_result.error!),
child: Text(_s.copy),
),
],
),
));
}
@@ -135,11 +147,14 @@ class _ProcessPageState extends State<ProcessPage> {
}
Widget _buildListItem(Proc proc) {
final leading = proc.user == null
? Text(proc.pid.toString())
: TwoLineText(up: proc.pid.toString(), down: proc.user!);
return RoundRectCard(
ListTile(
leading: SizedBox(
width: _media.size.width / 6,
child: TwoLineText(up: proc.pid.toString(), down: proc.user),
child: leading,
),
title: Text(proc.binary),
subtitle: Text(
@@ -175,15 +190,23 @@ class _ProcessPageState extends State<ProcessPage> {
}
Widget? _buildItemTrail(Proc proc) {
if (proc.cpu == null || proc.mem == null) {
if (proc.cpu == null && proc.mem == null) {
return null;
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
if (proc.cpu != null)
TwoLineText(
up: proc.cpu!.toStringAsFixed(1),
down: 'cpu',
),
width13,
TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
if (proc.mem != null)
TwoLineText(
up: proc.mem!.toStringAsFixed(1),
down: 'mem',
),
],
);
}