Skip to content

Commit 421b4e6

Browse files
committed
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
1 parent 9d25f90 commit 421b4e6

File tree

2 files changed

+59
-10
lines changed

2 files changed

+59
-10
lines changed

lib/core/extension/ssh_client.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ extension SSHClientX on SSHClient {
108108
return (session, result.takeBytes().string);
109109
}
110110

111-
/// Executes a command.
111+
/// Executes a command with password error detection.
112112
///
113113
/// This method is used for executing commands where password has already been
114114
/// handled beforehand (e.g., via base64 pipe in container commands).
115-
///
116-
/// [stderr: false] ensures only stdout is included in the returned output,
117-
/// keeping it clean from sudo password prompts that appear in stderr.
115+
/// It captures stderr via [onStderr] callback to detect sudo password errors
116+
/// (e.g., "Sorry, try again." or "incorrect password attempt"), while
117+
/// excluding stderr from the returned output via [stderr: false].
118118
///
119119
/// Returns exitCode:
120120
/// - 0: success
@@ -135,7 +135,8 @@ extension SSHClientX on SSHClient {
135135
sess.stdin.add('$script\n'.uint8List);
136136
sess.stdin.close();
137137
},
138-
onStderr: (data, session) async { // stderr is in `data`
138+
onStderr: (data, session) async {
139+
print('[DEBUG] stderr: $data');
139140
onStderr?.call(data, session);
140141
if (data.contains('Sorry, try again.') ||
141142
data.contains('incorrect password attempt')) {
@@ -144,8 +145,13 @@ extension SSHClientX on SSHClient {
144145
},
145146
onStdout: onStdout,
146147
entry: entry,
148+
stderr: false,
147149
);
148150

151+
print('[DEBUG] output: $output');
152+
print('[DEBUG] exitCode: ${session.exitCode}');
153+
print('[DEBUG] hasPasswordError: $hasPasswordError');
154+
149155
if (hasPasswordError) {
150156
return (2, output);
151157
}

lib/data/provider/container.dart

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,18 @@ class ContainerNotifier extends _$ContainerNotifier {
115115
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats, password: password));
116116
int? code;
117117
String raw = '';
118+
var isPodmanEmulation = false;
118119
if (client != null) {
119-
(code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId);
120+
(code, raw) = await client!.execWithPwd(
121+
cmd,
122+
context: context,
123+
id: hostId,
124+
onStderr: (data, _) {
125+
if (data.contains(_podmanEmulationMsg)) {
126+
isPodmanEmulation = true;
127+
}
128+
},
129+
);
120130
} else {
121131
state = state.copyWith(
122132
isBusy: false,
@@ -144,7 +154,7 @@ class ContainerNotifier extends _$ContainerNotifier {
144154
}
145155

146156
/// Pre-parse Podman detection
147-
if (raw.contains(_podmanEmulationMsg)) {
157+
if (isPodmanEmulation) {
148158
state = state.copyWith(
149159
error: ContainerErr(
150160
type: ContainerErrType.podmanDetected,
@@ -349,7 +359,7 @@ const _jsonFmt = '--format "{{json .}}"';
349359

350360
String _buildSudoCmd(String baseCmd, String password) {
351361
final pwdBase64 = base64Encode(utf8.encode(password));
352-
return 'echo "$pwdBase64" | base64 -d | sudo -S $baseCmd 2>/dev/null'; // Discard stderr to avoid password prompt
362+
return 'echo "$pwdBase64" | base64 -d | sudo -S $baseCmd';
353363
}
354364

355365
enum ContainerCmdType {
@@ -389,9 +399,42 @@ enum ContainerCmdType {
389399
}
390400

391401
static String execAll(ContainerType type, {bool sudo = false, bool includeStats = false, String? password}) {
392-
return ContainerCmdType.values
393-
.map((e) => e.exec(type, sudo: sudo, includeStats: includeStats, password: password))
402+
final commands = ContainerCmdType.values
403+
.map((e) => e.exec(type, sudo: false, includeStats: includeStats))
394404
.join('\necho ${ScriptConstants.separator}\n');
405+
406+
print('[DEBUG] commands: $commands');
407+
408+
final needsShWrapper = commands.contains('\n') || commands.contains('echo ${ScriptConstants.separator}');
409+
410+
if (needsShWrapper) {
411+
if (sudo && password != null) {
412+
final pwdBase64 = base64Encode(utf8.encode(password));
413+
final cmd = 'echo "$pwdBase64" | base64 -d | sudo -S sh -c \'${commands.replaceAll("'", "'\\''")}\'';
414+
print('[DEBUG] final sudo cmd (with sh): $cmd');
415+
return cmd;
416+
}
417+
if (sudo) {
418+
final cmd = 'sudo -S sh -c \'${commands.replaceAll("'", "'\\''")}\'';
419+
print('[DEBUG] final sudo cmd: $cmd');
420+
return cmd;
421+
}
422+
final cmd = 'sh -c \'${commands.replaceAll("'", "'\\''")}\'';
423+
print('[DEBUG] final sh cmd: $cmd');
424+
return cmd;
425+
}
426+
427+
if (sudo && password != null) {
428+
final cmd = _buildSudoCmd(commands, password);
429+
print('[DEBUG] final sudo cmd: $cmd');
430+
return cmd;
431+
}
432+
if (sudo) {
433+
final cmd = 'sudo -S $commands';
434+
print('[DEBUG] final sudo cmd: $cmd');
435+
return cmd;
436+
}
437+
return commands;
395438
}
396439

397440
/// Find out the required segment from [segments]

0 commit comments

Comments
 (0)