diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d79ad877..2e84b352 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -368,7 +368,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -491,7 +491,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -499,7 +499,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -516,7 +516,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; DEVELOPMENT_TEAM = BA88US33G6; ENABLE_BITCODE = NO; INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; @@ -524,7 +524,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = com.lollipopkit.toolbox; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/lib/data/model/server/proc.dart b/lib/data/model/server/proc.dart new file mode 100644 index 00000000..5567a682 --- /dev/null +++ b/lib/data/model/server/proc.dart @@ -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 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 = []; + 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; +} diff --git a/lib/data/res/build_data.dart b/lib/data/res/build_data.dart index 5f2d768a..0740981a 100644 --- a/lib/data/res/build_data.dart +++ b/lib/data/res/build_data.dart @@ -2,8 +2,8 @@ class BuildData { 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 buildAt = "2023-06-08 22:45:50.770980"; - static const int modifications = 3; + static const String buildAt = "2023-06-21 18:31:01.595350"; + static const int modifications = 2; } diff --git a/lib/view/page/process.dart b/lib/view/page/process.dart new file mode 100644 index 00000000..58282774 --- /dev/null +++ b/lib/view/page/process.dart @@ -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 { + late S _s; + late Timer _timer; + + PsResult _result = PsResult(procs: []); + int? _lastFocusId; + ProcSortMode _procSortMode = ProcSortMode.cpu; + + final _serverProvider = locator(); + + @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 = [ + PopupMenuButton( + 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, + )); + } +} diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index aeb795f0..2aa87e98 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/navigator.dart'; import 'package:toolbox/core/extension/order.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/utils/ui.dart'; @@ -338,13 +338,14 @@ class _ServerPageState extends State AppRoute(DockerManagePage(spi), 'Docker manage').go(context); break; case ServerTabMenuType.process: - AppRoute( - SSHPage( - spi: spi, - initCmd: 'sh $shellPath -${shellFuncProcess.flag}', - ), - 'ssh page (process)', - ).go(context); + // AppRoute( + // SSHPage( + // spi: spi, + // initCmd: 'sh $shellPath -${shellFuncProcess.flag}', + // ), + // 'ssh page (process)', + // ).go(context); + AppRoute(ProcessPage(spi: spi), 'process page').go(context); break; } }, diff --git a/lib/view/widget/two_line_text.dart b/lib/view/widget/two_line_text.dart index d44639ef..8f314218 100644 --- a/lib/view/widget/two_line_text.dart +++ b/lib/view/widget/two_line_text.dart @@ -10,14 +10,18 @@ class TwoLineText extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( up, style: textSize15, + overflow: TextOverflow.ellipsis, ), Text( down, style: textSize11, + overflow: TextOverflow.ellipsis, ) ], ); diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a5a67314..f5aecbd3 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -475,9 +475,9 @@ baseConfigurationReference = C1C758C41C4E208965A68933 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -490,9 +490,9 @@ baseConfigurationReference = 15AF97DF993E8968098D6EBE /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -505,9 +505,9 @@ baseConfigurationReference = 7CFA7DE7FABA75685DFB6948 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 357; + CURRENT_PROJECT_VERSION = 361; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0.357; + MARKETING_VERSION = 1.0.361; PRODUCT_BUNDLE_IDENTIFIER = tech.lolli.serverBox.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0;