fix(container): Parsing results in sudo mode (#1031)

* docs(l10n): fix un-updated English translation

* feat(container): Add support for requiring a sudo password

Add support for sudo password verification for Docker container operations, including:
1. Added ContainerErrType.sudoPasswordRequired error type
2. Add password prompt text in multi-language files
3. Modify the SSH execution logic to correctly handle the input of sudo password
4. Implement password caching and verification mechanism

* feat(container): Add sudo password error handling logic

Add a new error type `sudoPasswordIncorrect` to handle situations where the sudo password is incorrect

Modify the password verification logic in the SSH client, and return a specific error code when a password error is detected

Update multilingual files to support password error prompt information

* fix(ssh): Remove unnecessary stderr parameter and improve sudo command handling

Clean up the no longer needed stderr parameter in the SSH client, which was originally used to handle sudo password prompts

Unify the sudo command construction logic, always use the _buildSudoCmd method, and add stderr redirection
Clear cached passwords when passwords are incorrect

* fix(container): Improved sudo command handling and Podman simulation detection

Fix the sudo command processing logic, remove the masking of stderr to capture password errors

Override the detection logic simulated by Podman

Refactor the command building logic to support sh wrapping of multi-line commands

* fix(container): Improve the prompt message for sudo password errors

Update the sudo password error prompt messages for all languages to more accurately reflect situations of incorrect password or lack of permission

Fix the password error detection logic for both the SSH client and container providers simultaneously

* refactor(container): Remove unused sudo and password parameters in exec method

Simplify the exec method signature by removing the sudo and password parameters that are no longer needed, as these functions are no longer in use

* feat: Add new contributors and optimize container command handling

Add two new contributors to the GithubIds list and refactor the container command processing logic:
1. Simplify the command wrapping logic and uniformly use `sh -c` for processing
2. Specific error handling when adding a sudo password incorrectly
3. Remove redundant conditional checks and temporary variables
This commit is contained in:
GT610
2026-01-29 18:07:20 +08:00
committed by GitHub
parent d5e1d89394
commit 9281a578e7
30 changed files with 278 additions and 52 deletions

View File

@@ -27,6 +27,8 @@ enum ContainerErrType {
parseImages,
parseStats,
podmanDetected,
sudoPasswordRequired,
sudoPasswordIncorrect,
}
class ContainerErr extends Err<ContainerErrType> {

View File

@@ -37,6 +37,7 @@ abstract class ContainerState with _$ContainerState {
@riverpod
class ContainerNotifier extends _$ContainerNotifier {
var sudoCompleter = Completer<bool>();
String? _cachedPassword;
@override
ContainerState build(SSHClient? client, String userName, String hostId, BuildContext context) {
@@ -49,6 +50,18 @@ class ContainerNotifier extends _$ContainerNotifier {
return initialState;
}
Future<String?> _getSudoPassword() async {
if (_cachedPassword != null) return _cachedPassword;
if (!context.mounted) return null;
final pwd = await context.showPwdDialog(title: userName, id: hostId);
if (pwd != null && pwd.isNotEmpty) {
_cachedPassword = pwd;
}
return pwd;
}
Future<void> setType(ContainerType type) async {
state = state.copyWith(type: type, error: null, runLog: null, items: null, images: null, version: null);
Stores.container.setType(type, hostId);
@@ -84,14 +97,39 @@ class ContainerNotifier extends _$ContainerNotifier {
state = state.copyWith(isBusy: false);
return;
}
String? password;
if (sudo) {
password = await _getSudoPassword();
if (password == null) {
state = state.copyWith(
isBusy: false,
error: ContainerErr(
type: ContainerErrType.sudoPasswordRequired,
message: l10n.containerSudoPasswordRequired,
),
);
return;
}
}
final includeStats = Stores.setting.containerParseStat.fetch();
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats));
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats, password: password));
int? code;
String raw = '';
final errs = <String>[];
var isPodmanEmulation = false;
if (client != null) {
(code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId);
(code, raw) = await client!.execWithPwd(
cmd,
context: context,
id: hostId,
onStderr: (data, _) {
if (data.contains(_podmanEmulationMsg)) {
isPodmanEmulation = true;
}
},
);
} else {
state = state.copyWith(
isBusy: false,
@@ -106,13 +144,23 @@ class ContainerNotifier extends _$ContainerNotifier {
if (!context.mounted) return;
/// Code 127 means command not found
if (code == 127 || raw.contains(_dockerNotFound) || errs.join().contains(_dockerNotFound)) {
if (code == 127 || raw.contains(_dockerNotFound)) {
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
return;
}
/// Sudo password error (exitCode = 2)
if (code == 2) {
_cachedPassword = null;
state = state.copyWith(error: ContainerErr(
type: ContainerErrType.sudoPasswordIncorrect,
message: l10n.containerSudoPasswordIncorrect,
));
return;
}
/// Pre-parse Podman detection
if (raw.contains(_podmanEmulationMsg)) {
if (isPodmanEmulation) {
state = state.copyWith(
error: ContainerErr(
type: ContainerErrType.podmanDetected,
@@ -122,15 +170,8 @@ class ContainerNotifier extends _$ContainerNotifier {
return;
}
/// Filter out sudo password prompt from output
if (errs.any((e) => e.contains('[sudo] password'))) {
raw = raw.split('\n').where((line) => !line.contains('[sudo] password')).join('\n');
}
/// Detect Podman not installed when using Podman mode
if (state.type == ContainerType.podman &&
(errs.any((e) => e.contains('podman: not found')) ||
raw.contains('podman: not found'))) {
if (state.type == ContainerType.podman && raw.contains('podman: not found')) {
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
return;
}
@@ -276,21 +317,43 @@ class ContainerNotifier extends _$ContainerNotifier {
ContainerType.podman => 'podman $cmd',
};
final needSudo = await sudoCompleter.future;
String? password;
if (needSudo) {
password = await _getSudoPassword();
if (password == null) {
return ContainerErr(
type: ContainerErrType.sudoPasswordRequired,
message: l10n.containerSudoPasswordRequired,
);
}
}
if (needSudo) {
cmd = _buildSudoCmd(cmd, password!);
}
state = state.copyWith(runLog: '');
final errs = <String>[];
final (code, _) = await client?.execWithPwd(
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
final (code, _) = await client!.execWithPwd(
_wrap(cmd),
context: context,
onStdout: (data, _) {
state = state.copyWith(runLog: '${state.runLog}$data');
},
onStderr: (data, _) => errs.add(data),
id: hostId,
) ?? (null, null);
);
state = state.copyWith(runLog: null);
if (code == 2) {
_cachedPassword = null;
return ContainerErr(
type: ContainerErrType.sudoPasswordIncorrect,
message: l10n.containerSudoPasswordIncorrect,
);
}
if (code != 0) {
return ContainerErr(type: ContainerErrType.unknown, message: errs.join('\n').trim());
return ContainerErr(type: ContainerErrType.unknown, message: 'Command execution failed');
}
if (autoRefresh) await refresh();
return null;
@@ -310,6 +373,11 @@ class ContainerNotifier extends _$ContainerNotifier {
const _jsonFmt = '--format "{{json .}}"';
String _buildSudoCmd(String baseCmd, String password) {
final pwdBase64 = base64Encode(utf8.encode(password));
return 'echo "$pwdBase64" | base64 -d | sudo -S $baseCmd';
}
enum ContainerCmdType {
version,
ps,
@@ -319,30 +387,41 @@ enum ContainerCmdType {
// and don't require splitting output with ScriptConstants.separator
;
String exec(ContainerType type, {bool sudo = false, bool includeStats = false}) {
final prefix = sudo ? 'sudo -S ${type.name}' : type.name;
return switch (this) {
ContainerCmdType.version => '$prefix version $_jsonFmt',
String exec(ContainerType type, {bool includeStats = false}) {
final baseCmd = switch (this) {
ContainerCmdType.version => '${type.name} version $_jsonFmt',
ContainerCmdType.ps => switch (type) {
/// TODO: Rollback to json format when performance recovers.
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
ContainerType.docker =>
'$prefix ps -a --format "table {{printf \\"'
'${type.name} ps -a --format "table {{printf \\"'
'%-15.15s '
'%-30.30s '
'${"%-50.50s " * 2}\\"'
' .ID .Status .Names .Image}}"',
ContainerType.podman => '$prefix ps -a $_jsonFmt',
ContainerType.podman => '${type.name} ps -a $_jsonFmt',
},
ContainerCmdType.stats => includeStats ? '$prefix stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '$prefix image ls $_jsonFmt',
ContainerCmdType.stats => includeStats ? '${type.name} stats --no-stream $_jsonFmt' : 'echo PASS',
ContainerCmdType.images => '${type.name} image ls $_jsonFmt',
};
return baseCmd;
}
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false}) {
return ContainerCmdType.values
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats))
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false, String? password}) {
final commands = ContainerCmdType.values
.map((e) => e.exec(type, includeStats: includeStats))
.join('\necho ${ScriptConstants.separator}\n');
final wrappedCommands = 'sh -c \'${commands.replaceAll("'", "'\\''")}\'';
if (sudo && password != null) {
return _buildSudoCmd(wrappedCommands, password);
}
if (sudo) {
return 'sudo -S $wrappedCommands';
}
return wrappedCommands;
}
/// Find out the required segment from [segments]

View File

@@ -138,7 +138,9 @@ abstract final class GithubIds {
'itmagpro',
'atikattar1104',
'coldboy404',
'puskyer'
'puskyer',
'wanababy',
'toarujs'
};
}