diff --git a/lib/data/model/app/shell_func.dart b/lib/data/model/app/shell_func.dart index b6c56d7b..cfabccb3 100644 --- a/lib/data/model/app/shell_func.dart +++ b/lib/data/model/app/shell_func.dart @@ -2,14 +2,16 @@ import '../../res/build_data.dart'; import '../../res/server_cmd.dart'; import '../server/system.dart'; -const _cmdDivider = '\necho $seperator\n'; +const _cmdDivider = '\necho $seperator\n\t'; const _serverBoxDir = r'$HOME/.config/server_box'; const _shellPath = '$_serverBoxDir/mobile_app.sh'; enum AppShellFuncType { status, - docker; + docker, + process, + ; String get flag { switch (this) { @@ -17,6 +19,8 @@ enum AppShellFuncType { return 's'; case AppShellFuncType.docker: return 'd'; + case AppShellFuncType.process: + return 'p'; } } @@ -30,6 +34,8 @@ enum AppShellFuncType { // `dockeR` -> avoid conflict with `docker` command // 以防止循环递归 return 'dockeR'; + case AppShellFuncType.process: + return 'process'; } } @@ -39,17 +45,36 @@ enum AppShellFuncType { return ''' result=\$(uname 2>&1 | grep "Linux") if [ "\$result" != "" ]; then -${_statusCmds.join(_cmdDivider)} +\t${_statusCmds.join(_cmdDivider)} else -${_bsdStatusCmd.join(_cmdDivider)} +\t${_bsdStatusCmd.join(_cmdDivider)} fi'''; case AppShellFuncType.docker: return ''' result=\$(docker version 2>&1 | grep "permission denied") if [ "\$result" != "" ]; then -${_dockerCmds.join(_cmdDivider)} +\t${_dockerCmds.join(_cmdDivider)} else -${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)} +\t${_dockerCmds.map((e) => "sudo -S $e").join(_cmdDivider)} +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 +else + result=\$(ps -ax 2>&1 | grep "ps: ") + if [ "\$result" = "" ]; then + ps -ax + else + ps -o pid,user,time,args + fi fi'''; } } diff --git a/lib/data/model/server/proc.dart b/lib/data/model/server/proc.dart index 5567a682..1fedf397 100644 --- a/lib/data/model/server/proc.dart +++ b/lib/data/model/server/proc.dart @@ -1,18 +1,16 @@ -// Models for `ps -aux` - -// Each line -import 'dart:convert'; +import '../../../data/res/misc.dart'; +/// Some field can be null due to incompatible format on `BSD` and `Alpine` 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 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; @@ -65,7 +63,7 @@ class Proc { @override String toString() { - return const JsonEncoder.withIndent(' ').convert(toJson()); + return jsonEncoder.convert(toJson()); } String get binary { @@ -99,10 +97,10 @@ class PsResult { } switch (sort) { case ProcSortMode.cpu: - procs.sort((a, b) => b.cpu.compareTo(a.cpu)); + procs.sort((a, b) => b.cpu?.compareTo(a.cpu ?? 0) ?? 0); break; case ProcSortMode.mem: - procs.sort((a, b) => b.mem.compareTo(a.mem)); + procs.sort((a, b) => b.mem?.compareTo(a.mem ?? 0) ?? 0); break; case ProcSortMode.pid: procs.sort((a, b) => a.pid.compareTo(b.pid)); @@ -110,6 +108,9 @@ class PsResult { case ProcSortMode.user: procs.sort((a, b) => a.user.compareTo(b.user)); break; + case ProcSortMode.name: + procs.sort((a, b) => a.binary.compareTo(b.binary)); + break; } return PsResult(procs: procs, error: err.isEmpty ? null : err); } @@ -119,5 +120,7 @@ enum ProcSortMode { cpu, mem, pid, - user; + user, + name, + ; } diff --git a/lib/data/res/misc.dart b/lib/data/res/misc.dart index e15eabba..1fb0fe40 100644 --- a/lib/data/res/misc.dart +++ b/lib/data/res/misc.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/services.dart'; import '../model/app/github_id.dart'; @@ -52,3 +54,5 @@ const participants = { 'xgzxmytx', 'wind057', }; + +const jsonEncoder = JsonEncoder.withIndent(' '); diff --git a/lib/view/page/home.dart b/lib/view/page/home.dart index efb46c52..c6618861 100644 --- a/lib/view/page/home.dart +++ b/lib/view/page/home.dart @@ -254,7 +254,10 @@ class _HomePageState extends State /// Encode [map] to String with indent `\t` final text = const JsonEncoder.withIndent('\t').convert(map); - final result = await AppRoute.editor(text: text, langCode: 'json',).go(context); + final result = await AppRoute.editor( + text: text, + langCode: 'json', + ).go(context); if (result == null) { return; } diff --git a/lib/view/page/process.dart b/lib/view/page/process.dart index df592571..ba00f997 100644 --- a/lib/view/page/process.dart +++ b/lib/view/page/process.dart @@ -4,18 +4,18 @@ import 'package:dartssh2/dartssh2.dart'; 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/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 '../../core/utils/ui.dart'; +import '../../data/model/app/shell_func.dart'; +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 '../../locator.dart'; import '../widget/custom_appbar.dart'; +import '../widget/round_rect_card.dart'; +import '../widget/two_line_text.dart'; class ProcessPage extends StatefulWidget { final ServerPrivateInfo spi; @@ -34,7 +34,12 @@ class _ProcessPageState extends State { PsResult _result = PsResult(procs: []); int? _lastFocusId; - ProcSortMode _procSortMode = ProcSortMode.mem; + + // Issue #64 + // 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 _sortModes = ProcSortMode.values; final _serverProvider = locator(); @@ -54,12 +59,23 @@ class _ProcessPageState extends State { Future _refresh() async { if (mounted) { - final result = await _client?.run('ps -aux'.withLangExport).string; + final result = await _client?.run(AppShellFuncType.process.exec).string; if (result == null || result.isEmpty) { showSnackBar(context, Text(_s.noResult)); return; } _result = PsResult.parse(result, sort: _procSortMode); + + // If there are any [Proc]'s data is not complete, + // the option to sort by cpu/mem will not be available. + final isAnyProcDataNotComplete = + _result.procs.any((e) => e.cpu == null || e.mem == null); + if (isAnyProcDataNotComplete) { + _sortModes.removeWhere((e) => e == ProcSortMode.cpu); + _sortModes.removeWhere((e) => e == ProcSortMode.mem); + } else { + _sortModes = ProcSortMode.values; + } setState(() {}); } else { _timer.cancel(); @@ -83,10 +99,8 @@ class _ProcessPageState extends State { }, icon: const Icon(Icons.sort), initialValue: _procSortMode, - itemBuilder: (_) => ProcSortMode.values - .map( - (e) => PopupMenuItem(value: e, child: Text(e.name)), - ) + itemBuilder: (_) => _sortModes + .map((e) => PopupMenuItem(value: e, child: Text(e.name))) .toList(), ), ]; @@ -107,21 +121,7 @@ class _ProcessPageState extends State { child = ListView.builder( itemCount: _result.procs.length, padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7), - itemBuilder: (ctx, idx) { - final proc = _result.procs[idx]; - return AnimatedSwitcher( - duration: const Duration(milliseconds: 277), - switchInCurve: Curves.easeIn, - switchOutCurve: Curves.easeOut, - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: child, - ); - }, - child: _buildListItem(proc), - ); - }, + itemBuilder: (_, idx) => _buildListItem(_result.procs[idx]), ); } return Scaffold( @@ -148,14 +148,7 @@ class _ProcessPageState extends State { maxLines: 3, overflow: TextOverflow.fade, ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - TwoLineText(up: proc.cpu.toStringAsFixed(1), down: 'cpu'), - width13, - TwoLineText(up: proc.mem.toStringAsFixed(1), down: 'mem'), - ], - ), + trailing: _buildItemTrail(proc), onTap: () => _lastFocusId = proc.pid, onLongPress: () { showRoundDialog( @@ -180,4 +173,18 @@ class _ProcessPageState extends State { key: ValueKey(proc.pid), ); } + + Widget? _buildItemTrail(Proc proc) { + if (proc.cpu == null || proc.mem == null) { + return null; + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'), + width13, + TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'), + ], + ); + } }