mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +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:
|
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:
|
||||||
|
|||||||
10
.github/workflows/claude.yml
vendored
10
.github/workflows/claude.yml
vendored
@@ -13,10 +13,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
claude:
|
claude:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
github.actor == 'lollipopkit' && (
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
(github.event_name == 'issue_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_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@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
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 => (
|
||||||
|
|||||||
@@ -41,38 +41,63 @@ final class _SystemdPageState extends State<SystemdPage> {
|
|||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _pro.isBusy.listenVal(
|
child: Column(
|
||||||
(isBusy) => AnimatedContainer(
|
children: [
|
||||||
duration: Durations.medium1,
|
_buildScopeFilterChips(),
|
||||||
curve: Curves.fastEaseInToSlowEaseOut,
|
_pro.isBusy.listenVal(
|
||||||
height: isBusy ? SizedLoading.medium.size : 0,
|
(isBusy) => AnimatedContainer(
|
||||||
child: isBusy ? SizedLoading.medium : const SizedBox.shrink(),
|
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) {
|
Widget _buildScopeFilterChips() {
|
||||||
return units.listenVal((units) {
|
return _pro.scopeFilter.listenVal((currentFilter) {
|
||||||
if (units.isEmpty) {
|
return Wrap(
|
||||||
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
|
spacing: 8,
|
||||||
}
|
children: SystemdScopeFilter.values.map((filter) {
|
||||||
return SliverList(
|
final isSelected = filter == currentFilter;
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
return FilterChip(
|
||||||
final unit = units[index];
|
selected: isSelected,
|
||||||
return ListTile(
|
label: Text(filter.displayName),
|
||||||
leading: _buildScopeTag(unit.scope),
|
onSelected: (_) => _pro.scopeFilter.value = filter,
|
||||||
title: unit.description != null ? TipText(unit.name, unit.description!) : Text(unit.name),
|
);
|
||||||
subtitle: Wrap(
|
}).toList(),
|
||||||
children: [_buildStateTag(unit.state), _buildTypeTag(unit.type)],
|
).paddingSymmetric(horizontal: 13, vertical: 8);
|
||||||
).paddingOnly(top: 7),
|
});
|
||||||
trailing: _buildUnitFuncs(unit),
|
}
|
||||||
).cardx.paddingSymmetric(horizontal: 13);
|
|
||||||
}, childCount: units.length),
|
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"
|
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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user