mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-16 23:04:22 +01:00
opt.: systemd page (#851)
This commit is contained in:
6
.github/workflows/claude-code-review.yml
vendored
6
.github/workflows/claude-code-review.yml
vendored
@@ -12,11 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# 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'
|
||||
if: github.event.pull_request.user.login == 'lollipopkit'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
10
.github/workflows/claude.yml
vendored
10
.github/workflows/claude.yml
vendored
@@ -13,10 +13,12 @@ on:
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(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' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
github.actor == 'lollipopkit' && (
|
||||
(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' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -132,8 +132,9 @@ extension SSHClientX on SSHClient {
|
||||
if (data.contains('[sudo] password for ')) {
|
||||
isRequestingPwd = true;
|
||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||
if (context == null) return;
|
||||
final pwd = context.mounted ? await context.showPwdDialog(title: user, id: id) : null;
|
||||
final ctx = context ?? WidgetsBinding.instance.focusManager.primaryFocus?.context;
|
||||
if (ctx == null) return;
|
||||
final pwd = ctx.mounted ? await ctx.showPwdDialog(title: user, id: id) : null;
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.stdin.close();
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
|
||||
enum SystemdUnitFunc {
|
||||
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 {
|
||||
active,
|
||||
inactive,
|
||||
|
||||
@@ -440,7 +440,7 @@ class ServerProvider extends Provider {
|
||||
|
||||
try {
|
||||
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();
|
||||
if (raw == null || raw.isEmpty || segments == null || segments.isEmpty) {
|
||||
if (Stores.setting.keepStatusWhenErr.fetch()) {
|
||||
|
||||
@@ -8,20 +8,31 @@ import 'package:server_box/data/provider/server.dart';
|
||||
|
||||
final class SystemdProvider {
|
||||
late final VNode<Server> _si;
|
||||
late final bool _isRoot;
|
||||
|
||||
SystemdProvider.init(Spi spi) {
|
||||
_isRoot = spi.isRoot;
|
||||
_si = ServerProvider.pick(spi: spi)!;
|
||||
getUnits();
|
||||
}
|
||||
|
||||
final isBusy = false.vn;
|
||||
final units = <SystemdUnit>[].vn;
|
||||
final scopeFilter = SystemdScopeFilter.all.vn;
|
||||
|
||||
void dispose() {
|
||||
isBusy.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 {
|
||||
@@ -35,43 +46,33 @@ final class SystemdProvider {
|
||||
final userUnits = <String>[];
|
||||
final systemUnits = <String>[];
|
||||
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);
|
||||
} else if (unit.startsWith('~/.config/systemd/user')) {
|
||||
} else {
|
||||
userUnits.add(unit);
|
||||
} else if (unit.trim().isNotEmpty) {
|
||||
Loggers.app.warning('Unknown unit: $unit');
|
||||
}
|
||||
}
|
||||
|
||||
final parsedUserUnits = await _parseUnitObj(
|
||||
userUnits,
|
||||
SystemdUnitScope.user,
|
||||
);
|
||||
final parsedSystemUnits = await _parseUnitObj(
|
||||
systemUnits,
|
||||
SystemdUnitScope.system,
|
||||
);
|
||||
final parsedUserUnits = await _parseUnitObj(userUnits, SystemdUnitScope.user);
|
||||
final parsedSystemUnits = await _parseUnitObj(systemUnits, SystemdUnitScope.system);
|
||||
this.units.value = [...parsedUserUnits, ...parsedSystemUnits];
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Parse systemd', e, s);
|
||||
dprint('Parse systemd', e, s);
|
||||
}
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<List<SystemdUnit>> _parseUnitObj(
|
||||
List<String> unitNames,
|
||||
SystemdUnitScope scope,
|
||||
) async {
|
||||
final unitNames_ = unitNames
|
||||
.map((e) => e.trim().split('/').last.split('.').first)
|
||||
.toList();
|
||||
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
|
||||
final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
|
||||
final script =
|
||||
'''
|
||||
for unit in ${unitNames_.join(' ')}; do
|
||||
state=\$(systemctl show --no-pager \$unit)
|
||||
echo -n "${ScriptConstants.separator}\n\$state"
|
||||
echo "\$state"
|
||||
echo -n "\n${ScriptConstants.separator}\n"
|
||||
done
|
||||
''';
|
||||
final client = _si.value.client!;
|
||||
@@ -79,21 +80,30 @@ done
|
||||
final units = result.split(ScriptConstants.separator);
|
||||
|
||||
final parsedUnits = <SystemdUnit>[];
|
||||
for (final unit in units) {
|
||||
final parts = unit.split('\n');
|
||||
for (final unit in units.where((e) => e.trim().isNotEmpty)) {
|
||||
final parts = unit
|
||||
.split('\n')
|
||||
.where((e) => e.trim().isNotEmpty)
|
||||
.toList();
|
||||
if (parts.isEmpty) continue;
|
||||
var name = '';
|
||||
var type = '';
|
||||
var state = '';
|
||||
String? description;
|
||||
for (final part in parts) {
|
||||
if (part.startsWith('Id=')) {
|
||||
final val = _getIniVal(part).split('.');
|
||||
name = val.first;
|
||||
type = val.last;
|
||||
final val = _getIniVal(part);
|
||||
if (val == null) continue;
|
||||
// Id=sshd.service
|
||||
final vals = val.split('.');
|
||||
name = vals.first;
|
||||
type = vals.last;
|
||||
continue;
|
||||
}
|
||||
if (part.startsWith('ActiveState=')) {
|
||||
state = _getIniVal(part);
|
||||
final val = _getIniVal(part);
|
||||
if (val == null) continue;
|
||||
state = val;
|
||||
continue;
|
||||
}
|
||||
if (part.startsWith('Description=')) {
|
||||
@@ -104,23 +114,17 @@ done
|
||||
|
||||
final unitType = SystemdUnitType.fromString(type);
|
||||
if (unitType == null) {
|
||||
Loggers.app.warning('Unit type: $type');
|
||||
dprint('Unit type: $type');
|
||||
continue;
|
||||
}
|
||||
final unitState = SystemdUnitState.fromString(state);
|
||||
if (unitState == null) {
|
||||
Loggers.app.warning('Unit state: $state');
|
||||
dprint('Unit state: $state');
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedUnits.add(
|
||||
SystemdUnit(
|
||||
name: name,
|
||||
type: unitType,
|
||||
scope: scope,
|
||||
state: unitState,
|
||||
description: description,
|
||||
),
|
||||
SystemdUnit(name: name, type: unitType, scope: scope, state: unitState, description: description),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,13 +154,16 @@ done
|
||||
|
||||
for type in \$types; do
|
||||
get_files \$type /etc/systemd/system
|
||||
get_files \$type /lib/systemd/system
|
||||
get_files \$type /usr/lib/systemd/system
|
||||
# Parsing these paths can lead to SSH transport closed errors
|
||||
# get_files \$type /lib/systemd/system
|
||||
# get_files \$type /usr/lib/systemd/system
|
||||
get_files \$type ~/.config/systemd/user
|
||||
done | sort
|
||||
''';
|
||||
}
|
||||
|
||||
String _getIniVal(String line) {
|
||||
return line.split('=').last;
|
||||
String? _getIniVal(String line) {
|
||||
final idx = line.indexOf('=');
|
||||
if (idx < 0) return null;
|
||||
return line.substring(idx + 1).trim();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ extension on _ServerPageState {
|
||||
Widget _buildTopRightWidget(Server s) {
|
||||
final (child, onTap) = switch (s.conn) {
|
||||
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,
|
||||
),
|
||||
ServerConn.failed => (
|
||||
|
||||
@@ -41,38 +41,63 @@ final class _SystemdPageState extends State<SystemdPage> {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: _pro.isBusy.listenVal(
|
||||
(isBusy) => AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? SizedLoading.medium.size : 0,
|
||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildScopeFilterChips(),
|
||||
_pro.isBusy.listenVal(
|
||||
(isBusy) => AnimatedContainer(
|
||||
duration: Durations.medium1,
|
||||
curve: Curves.fastEaseInToSlowEaseOut,
|
||||
height: isBusy ? SizedLoading.medium.size : 0,
|
||||
width: isBusy ? SizedLoading.medium.size : 0,
|
||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildUnitList(_pro.units),
|
||||
_buildUnitList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnitList(VNode<List<SystemdUnit>> units) {
|
||||
return units.listenVal((units) {
|
||||
if (units.isEmpty) {
|
||||
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
||||
}
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final unit = units[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||
subtitle: Wrap(
|
||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||
).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
}, childCount: units.length),
|
||||
);
|
||||
Widget _buildScopeFilterChips() {
|
||||
return _pro.scopeFilter.listenVal((currentFilter) {
|
||||
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 SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
final unit = filteredUnits[index];
|
||||
return ListTile(
|
||||
leading: _buildScopeTag(unit.scope),
|
||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
||||
subtitle: Wrap(
|
||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
||||
).paddingOnly(top: 7),
|
||||
trailing: _buildUnitFuncs(unit),
|
||||
).cardx.paddingSymmetric(horizontal: 13);
|
||||
}, childCount: filteredUnits.length),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -497,8 +497,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "v1.0.327"
|
||||
resolved-ref: "5075a679b814b10742f967066858ba4df92ea4ae"
|
||||
ref: "v1.0.329"
|
||||
resolved-ref: "1620fb055986dfcf7587d7a16eb994a39c0d5772"
|
||||
url: "https://github.com/lppcg/fl_lib"
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
|
||||
@@ -63,7 +63,7 @@ dependencies:
|
||||
fl_lib:
|
||||
git:
|
||||
url: https://github.com/lppcg/fl_lib
|
||||
ref: v1.0.327
|
||||
ref: v1.0.329
|
||||
|
||||
dependency_overrides:
|
||||
# webdav_client_plus:
|
||||
|
||||
Reference in New Issue
Block a user