opt.: systemd page (#851)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-13 22:16:55 +08:00
committed by GitHub
parent e4a9875620
commit ed8a1d18b9
10 changed files with 131 additions and 84 deletions

View File

@@ -12,11 +12,7 @@ on:
jobs: jobs:
claude-review: claude-review:
# Optional: Filter by PR author if: github.event.pull_request.user.login == 'lollipopkit'
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:

View File

@@ -13,10 +13,12 @@ on:
jobs: jobs:
claude: claude:
if: | if: |
github.actor == 'lollipopkit' && (
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
)
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read

View File

@@ -132,8 +132,9 @@ extension SSHClientX on SSHClient {
if (data.contains('[sudo] password for ')) { if (data.contains('[sudo] password for ')) {
isRequestingPwd = true; isRequestingPwd = true;
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1); final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
if (context == null) return; final ctx = context ?? WidgetsBinding.instance.focusManager.primaryFocus?.context;
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null; if (ctx == null) return;
final pwd = ctx.mounted ? await ctx.showPwdDialog(title: user, id: id) : null;
if (pwd == null || pwd.isEmpty) { if (pwd == null || pwd.isEmpty) {
session.stdin.close(); session.stdin.close();
} else { } else {

View File

@@ -1,5 +1,6 @@
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:server_box/core/extension/context/locale.dart';
enum SystemdUnitFunc { enum SystemdUnitFunc {
start, start,
@@ -49,6 +50,18 @@ enum SystemdUnitScope {
} }
} }
enum SystemdScopeFilter {
all,
system,
user;
String get displayName => switch (this) {
all => libL10n.all,
system => l10n.system,
user => libL10n.user,
};
}
enum SystemdUnitState { enum SystemdUnitState {
active, active,
inactive, inactive,

View File

@@ -440,7 +440,7 @@ class ServerProvider extends Provider {
try { try {
raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string; raw = await sv.client?.run(ShellFunc.status.exec(spi.id, systemType: sv.status.system)).string;
dprint('Get status from ${spi.name}:\n$raw'); //dprint('Get status from ${spi.name}:\n$raw');
segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList(); segments = raw?.split(ScriptConstants.separator).map((e) => e.trim()).toList();
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) { if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) { if (Stores.setting.keepStatusWhenErr.fetch()) {

View File

@@ -8,20 +8,31 @@ import 'package:server_box/data/provider/server.dart';
final class SystemdProvider { final class SystemdProvider {
late final VNode<Server> _si; late final VNode<Server> _si;
late final bool _isRoot;
SystemdProvider.init(Spi spi) { SystemdProvider.init(Spi spi) {
_isRoot = spi.isRoot;
_si = ServerProvider.pick(spi: spi)!; _si = ServerProvider.pick(spi: spi)!;
getUnits(); getUnits();
} }
final isBusy = false.vn; final isBusy = false.vn;
final units = <SystemdUnit>[].vn; final units = <SystemdUnit>[].vn;
final scopeFilter = SystemdScopeFilter.all.vn;
void dispose() { void dispose() {
isBusy.dispose(); isBusy.dispose();
units.dispose(); units.dispose();
scopeFilter.dispose();
}
List<SystemdUnit> get filteredUnits {
switch (scopeFilter.value) {
case SystemdScopeFilter.all:
return units.value;
case SystemdScopeFilter.system:
return units.value.where((unit) => unit.scope == SystemdUnitScope.system).toList();
case SystemdScopeFilter.user:
return units.value.where((unit) => unit.scope == SystemdUnitScope.user).toList();
}
} }
Future<void> getUnits() async { Future<void> getUnits() async {
@@ -35,43 +46,33 @@ final class SystemdProvider {
final userUnits = <String>[]; final userUnits = <String>[];
final systemUnits = <String>[]; final systemUnits = <String>[];
for (final unit in units) { for (final unit in units) {
if (unit.startsWith('/etc/systemd/system')) { final maybeSystem = unit.contains('/systemd/system');
final maybeUser = unit.contains('/.config/systemd/user');
if (maybeSystem && !maybeUser) {
systemUnits.add(unit); systemUnits.add(unit);
} else if (unit.startsWith('~/.config/systemd/user')) { } else {
userUnits.add(unit); userUnits.add(unit);
} else if (unit.trim().isNotEmpty) {
Loggers.app.warning('Unknown unit: $unit');
} }
} }
final parsedUserUnits = await _parseUnitObj( final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
userUnits, final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
SystemdUnitScope.user,
);
final parsedSystemUnits = await _parseUnitObj(
systemUnits,
SystemdUnitScope.system,
);
this.units.value = [...parsedUserUnits, ...parsedSystemUnits]; this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
} catch (e, s) { } catch (e, s) {
Loggers.app.warning('Parse systemd', e, s); dprint('Parse systemd', e, s);
} }
isBusy.value = false; isBusy.value = false;
} }
Future<List<SystemdUnit>> _parseUnitObj( Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
List<String> unitNames, final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
SystemdUnitScope scope,
) async {
final unitNames_ = unitNames
.map((e) => e.trim().split('/').last.split('.').first)
.toList();
final script = final script =
''' '''
for unit in ${unitNames_.join(' ')}; do for unit in ${unitNames_.join(' ')}; do
state=\$(systemctl show --no-pager \$unit) state=\$(systemctl show --no-pager \$unit)
echo -n "${ScriptConstants.separator}\n\$state" echo "\$state"
echo -n "\n${ScriptConstants.separator}\n"
done done
'''; ''';
final client = _si.value.client!; final client = _si.value.client!;
@@ -79,21 +80,30 @@ done
final units = result.split(ScriptConstants.separator); final units = result.split(ScriptConstants.separator);
final parsedUnits = <SystemdUnit>[]; final parsedUnits = <SystemdUnit>[];
for (final unit in units) { for (final unit in units.where((e) => e.trim().isNotEmpty)) {
final parts = unit.split('\n'); final parts = unit
.split('\n')
.where((e) => e.trim().isNotEmpty)
.toList();
if (parts.isEmpty) continue;
var name = ''; var name = '';
var type = ''; var type = '';
var state = ''; var state = '';
String? description; String? description;
for (final part in parts) { for (final part in parts) {
if (part.startsWith('Id=')) { if (part.startsWith('Id=')) {
final val = _getIniVal(part).split('.'); final val = _getIniVal(part);
name = val.first; if (val == null) continue;
type = val.last; // Id=sshd.service
final vals = val.split('.');
name = vals.first;
type = vals.last;
continue; continue;
} }
if (part.startsWith('ActiveState=')) { if (part.startsWith('ActiveState=')) {
state = _getIniVal(part); final val = _getIniVal(part);
if (val == null) continue;
state = val;
continue; continue;
} }
if (part.startsWith('Description=')) { if (part.startsWith('Description=')) {
@@ -104,23 +114,17 @@ done
final unitType = SystemdUnitType.fromString(type); final unitType = SystemdUnitType.fromString(type);
if (unitType == null) { if (unitType == null) {
Loggers.app.warning('Unit type: $type'); dprint('Unit type: $type');
continue; continue;
} }
final unitState = SystemdUnitState.fromString(state); final unitState = SystemdUnitState.fromString(state);
if (unitState == null) { if (unitState == null) {
Loggers.app.warning('Unit state: $state'); dprint('Unit state: $state');
continue; continue;
} }
parsedUnits.add( parsedUnits.add(
SystemdUnit( SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
name: name,
type: unitType,
scope: scope,
state: unitState,
description: description,
),
); );
} }
@@ -150,13 +154,16 @@ done
for type in \$types; do for type in \$types; do
get_files \$type /etc/systemd/system get_files \$type /etc/systemd/system
get_files \$type /lib/systemd/system # Parsing these paths can lead to SSH transport closed errors
get_files \$type /usr/lib/systemd/system # get_files \$type /lib/systemd/system
# get_files \$type /usr/lib/systemd/system
get_files \$type ~/.config/systemd/user get_files \$type ~/.config/systemd/user
done | sort done | sort
'''; ''';
} }
String _getIniVal(String line) { String? _getIniVal(String line) {
return line.split('=').last; final idx = line.indexOf('=');
if (idx < 0) return null;
return line.substring(idx + 1).trim();
} }

View File

@@ -20,7 +20,10 @@ extension on _ServerPageState {
Widget _buildTopRightWidget(Server s) { Widget _buildTopRightWidget(Server s) {
final (child, onTap) = switch (s.conn) { final (child, onTap) = switch (s.conn) {
ServerConn.connecting || ServerConn.loading || ServerConn.connected => ( ServerConn.connecting || ServerConn.loading || ServerConn.connected => (
SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 5), SizedBox.square(
dimension: _ServerPageState._kCardHeightMin,
child: SizedLoading(_ServerPageState._kCardHeightMin, strokeWidth: 3, padding: 3),
),
null, null,
), ),
ServerConn.failed => ( ServerConn.failed => (

View File

@@ -41,28 +41,52 @@ final class _SystemdPageState extends State<SystemdPage> {
return CustomScrollView( return CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
SliverToBoxAdapter( SliverToBoxAdapter(
child: _pro.isBusy.listenVal( child: Column(
children: [
_buildScopeFilterChips(),
_pro.isBusy.listenVal(
(isBusy) => AnimatedContainer( (isBusy) => AnimatedContainer(
duration: Durations.medium1, duration: Durations.medium1,
curve: Curves.fastEaseInToSlowEaseOut, curve: Curves.fastEaseInToSlowEaseOut,
height: isBusy ? SizedLoading.medium.size : 0, height: isBusy ? SizedLoading.medium.size : 0,
width: isBusy ? SizedLoading.medium.size : 0,
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(), child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
), ),
), ),
],
), ),
_buildUnitList(_pro.units), ),
_buildUnitList(),
], ],
); );
} }
Widget _buildUnitList(VNode<List<SystemdUnit>> units) { Widget _buildScopeFilterChips() {
return units.listenVal((units) { return _pro.scopeFilter.listenVal((currentFilter) {
if (units.isEmpty) { return Wrap(
spacing: 8,
children: SystemdScopeFilter.values.map((filter) {
final isSelected = filter == currentFilter;
return FilterChip(
selected: isSelected,
label: Text(filter.displayName),
onSelected: (_) => _pro.scopeFilter.value = filter,
);
}).toList(),
).paddingSymmetric(horizontal: 13, vertical: 8);
});
}
Widget _buildUnitList() {
return _pro.units.listenVal((allUnits) {
return _pro.scopeFilter.listenVal((filter) {
final filteredUnits = _pro.filteredUnits;
if (filteredUnits.isEmpty) {
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13)); return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
} }
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate((context, index) { delegate: SliverChildBuilderDelegate((context, index) {
final unit = units[index]; final unit = filteredUnits[index];
return ListTile( return ListTile(
leading: _buildScopeTag(unit.scope), leading: _buildScopeTag(unit.scope),
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name), title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
@@ -71,9 +95,10 @@ final class _SystemdPageState extends State<SystemdPage> {
).paddingOnly(top: 7), ).paddingOnly(top: 7),
trailing: _buildUnitFuncs(unit), trailing: _buildUnitFuncs(unit),
).cardx.paddingSymmetric(horizontal: 13); ).cardx.paddingSymmetric(horizontal: 13);
}, childCount: units.length), }, childCount: filteredUnits.length),
); );
}); });
});
} }
Widget _buildUnitFuncs(SystemdUnit unit) { Widget _buildUnitFuncs(SystemdUnit unit) {

View File

@@ -497,8 +497,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "v1.0.327" ref: "v1.0.329"
resolved-ref: "5075a679b814b10742f967066858ba4df92ea4ae" resolved-ref: "1620fb055986dfcf7587d7a16eb994a39c0d5772"
url: "https://github.com/lppcg/fl_lib" url: "https://github.com/lppcg/fl_lib"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -63,7 +63,7 @@ dependencies:
fl_lib: fl_lib:
git: git:
url: https://github.com/lppcg/fl_lib url: https://github.com/lppcg/fl_lib
ref: v1.0.327 ref: v1.0.329
dependency_overrides: dependency_overrides:
# webdav_client_plus: # webdav_client_plus: