ref(systemd): Fix safety bugs and improve performance (#1020)

* fix(systemd): Fix the issue of special characters in unit names

In systemd unit processing, filtering of special characters in unit names has been added to prevent command injection and security issues. Additionally, the rendering performance of the unit list has been optimized by merging unnecessary watch calls and removing confirmation dialogs to simplify the operation process.

* feat(Systemd): Add a confirmation dialog for systemd unit operations

Display a confirmation dialog when stopping or restarting systemd units to prevent accidental operations. For other operations, directly navigate to the SSH page to execute commands.

* fix(systemd): Fix the range of characters allowed in unit names

Extend the regular expression to allow more valid characters, including dots, @, and colons, in system unit names, to support a broader range of unit naming conventions

* fix(systemd): Fix the issue of parsing service names with dots

When dealing with service IDs containing multiple dots (such as org.cups.cupsd.service), correctly extract the service name and type. When there are no dots in the service ID, set the type to an empty string.
This commit is contained in:
GT610
2026-01-22 17:47:06 +08:00
committed by GitHub
parent 84a1bd5519
commit 87d7feb823
3 changed files with 52 additions and 29 deletions

View File

@@ -96,7 +96,7 @@ final class SystemdUnit {
String getCmd({required SystemdUnitFunc func, required bool isRoot}) {
final prefix = scope.getCmdPrefix(isRoot);
return '$prefix ${func.name} $name';
return '$prefix ${func.name} ${name.replaceAll(RegExp(r'[^a-zA-Z0-9\-_.@:]'), '')}';
}
List<SystemdUnitFunc> get availableFuncs {

View File

@@ -77,11 +77,16 @@ class SystemdNotifier extends _$SystemdNotifier {
}
Future<List<SystemdUnit>> _parseUnitObj(List<String> unitNames, SystemdUnitScope scope) async {
final unitNames_ = unitNames.map((e) => e.trim().split('/').last.split('.').first).toList();
final unitNames_ = unitNames.map((e) {
final fullName = e.trim().split('/').last;
final lastDot = fullName.lastIndexOf('.');
final name = lastDot > 0 ? fullName.substring(0, lastDot) : fullName;
return name.replaceAll(RegExp(r'[^a-zA-Z0-9\-_.@:]'), '');
}).toList();
final script =
'''
for unit in ${unitNames_.join(' ')}; do
state=\$(systemctl show --no-pager \$unit)
for unit in ${unitNames_.map((e) => '"$e"').join(' ')}; do
state=\$(systemctl show --no-pager -- "\$unit")
echo "\$state"
echo -n "\n${ScriptConstants.separator}\n"
done
@@ -102,10 +107,15 @@ done
if (part.startsWith('Id=')) {
final val = _getIniVal(part);
if (val == null) continue;
// Id=sshd.service
final vals = val.split('.');
name = vals.first;
type = vals.last;
// Id=org.cups.cupsd.service
final lastDot = val.lastIndexOf('.');
if (lastDot > 0) {
name = val.substring(0, lastDot);
type = val.substring(lastDot + 1);
} else {
name = val;
type = '';
}
continue;
}
if (part.startsWith('ActiveState=')) {

View File

@@ -73,8 +73,7 @@ final class _SystemdPageState extends ConsumerState<SystemdPage> {
}
Widget _buildUnitList() {
ref.watch(_pro.select((p) => p.units));
ref.watch(_pro.select((p) => p.scopeFilter));
ref.watch(_pro.select((p) => (p.units, p.scopeFilter)));
final filteredUnits = _notifier.filteredUnits;
if (filteredUnits.isEmpty) {
return SliverToBoxAdapter(child: CenterGreyTitle(libL10n.empty).paddingSymmetric(horizontal: 13));
@@ -97,28 +96,42 @@ final class _SystemdPageState extends ConsumerState<SystemdPage> {
Widget _buildUnitFuncs(SystemdUnit unit) {
return PopupMenu(
items: unit.availableFuncs.map(_buildUnitFuncBtn).toList(),
onSelected: (val) async {
final cmd = unit.getCmd(func: val, isRoot: widget.args.spi.isRoot);
final sure = await context.showRoundDialog(
title: libL10n.attention,
child: SimpleMarkdown(data: '```shell\n$cmd\n```'),
actions: [
CountDownBtn(
seconds: 1,
onTap: () => context.pop(true),
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
if (sure != true) return;
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
SSHPage.route.go(context, args);
},
onSelected: (val) => _handleUnitFuncSelected(unit, val),
);
}
void _handleUnitFuncSelected(SystemdUnit unit, SystemdUnitFunc func) {
final cmd = unit.getCmd(func: func, isRoot: widget.args.spi.isRoot);
if (func == SystemdUnitFunc.stop || func == SystemdUnitFunc.restart) {
_showConfirmDialog(cmd);
} else {
_navigateToSsh(cmd);
}
}
void _showConfirmDialog(String cmd) async {
final sure = await context.showRoundDialog(
title: libL10n.attention,
child: SimpleMarkdown(data: '```shell\n$cmd\n```'),
actions: [
Btn.cancel(),
CountDownBtn(
seconds: 3,
onTap: () => context.pop(true),
text: libL10n.ok,
afterColor: Colors.red,
),
],
);
if (sure == true) _navigateToSsh(cmd);
}
void _navigateToSsh(String cmd) {
final args = SshPageArgs(spi: widget.args.spi, initCmd: cmd);
SSHPage.route.go(context, args);
}
PopupMenuEntry _buildUnitFuncBtn(SystemdUnitFunc func) {
return PopupMenuItem<SystemdUnitFunc>(
value: func,