opt: Improve container parsing and error handling (#1001)
* fix(ssh): Modify the return type of execWithPwd to include the output content Adjust the return type of the `execWithPwd` method to `(int?, String)` so that it can simultaneously return the exit code and output content Fix the issue in ContainerNotifier where the return result of execWithPwd is not handled correctly Ensure that server operations (shutdown/restart/suspend) are correctly pending until the command execution is completed * refactor(container): Change single error handling to multiple error lists Support the simultaneous display of multiple container operation errors, enhancing error handling capabilities * fix(container): Adjust the layout width and optimize the handling of text overflow Adjust the width calculation for the container page layout, changing from subtracting a fixed value to subtracting a smaller value to improve the layout Add overflow ellipsis processing to the text to prevent anomalies when the text is too long * Revert "refactor(container): Change single error handling to multiple error lists" This reverts commit 72aaa173f5ceabc952ab5c28e024451ac1309920. * feat(container): Add Podman Docker emulation detection function Add detection for Podman Docker emulation in the container module. When detected, a prompt message will be displayed and users will be advised to switch to Podman settings. Updated the multilingual translation files to support the new features. * fix: Fix error handling in SSH client and container operations Fix the issue where the SSH client does not handle stderr when executing commands Error handling for an empty client in the container addition operation Fix the issue where null may be returned during server page operations * fix(container): Check if client is empty before running the command When the client is null, directly return an error to avoid null pointer exception * fix: Revert `stderr` ignore * fix(container): Detect Podman simulation in advance and optimize error handling Move the Podman simulation detection to the initial parsing stage to avoid redundant checks Remove duplicated error handling code and simplify the logic * fix(container): Fix the error handling logic during container command execution Increase the inspection of error outputs, including handling situations such as sudo password prompts and Podman not being installed * refactor(macOS): Remove unused path_provider_foundation plugin
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/core/extension/context/locale.dart';
|
||||
import 'package:server_box/core/extension/ssh_client.dart';
|
||||
import 'package:server_box/data/model/app/error.dart';
|
||||
import 'package:server_box/data/model/app/scripts/script_consts.dart';
|
||||
@@ -18,6 +19,7 @@ part 'container.freezed.dart';
|
||||
part 'container.g.dart';
|
||||
|
||||
final _dockerNotFound = RegExp(r"command not found|Unknown command|Command '\w+' not found");
|
||||
final _podmanEmulationMsg = 'Emulate Docker CLI using podman';
|
||||
|
||||
@freezed
|
||||
abstract class ContainerState with _$ContainerState {
|
||||
@@ -84,21 +86,51 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
final includeStats = Stores.setting.containerParseStat.fetch();
|
||||
|
||||
var raw = '';
|
||||
final cmd = _wrap(ContainerCmdType.execAll(state.type, sudo: sudo, includeStats: includeStats));
|
||||
final code = await client?.execWithPwd(
|
||||
cmd,
|
||||
context: context,
|
||||
onStdout: (data, _) => raw = '$raw$data',
|
||||
id: hostId,
|
||||
);
|
||||
int? code;
|
||||
String raw = '';
|
||||
final errs = <String>[];
|
||||
if (client != null) {
|
||||
(code, raw) = await client!.execWithPwd(cmd, context: context, id: hostId);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isBusy: false,
|
||||
error: ContainerErr(type: ContainerErrType.noClient),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ref.mounted) return;
|
||||
state = state.copyWith(isBusy: false);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
/// Code 127 means command not found
|
||||
if (code == 127 || raw.contains(_dockerNotFound)) {
|
||||
if (code == 127 || raw.contains(_dockerNotFound) || errs.join().contains(_dockerNotFound)) {
|
||||
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
|
||||
return;
|
||||
}
|
||||
|
||||
/// Pre-parse Podman detection
|
||||
if (raw.contains(_podmanEmulationMsg)) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(
|
||||
type: ContainerErrType.podmanDetected,
|
||||
message: l10n.podmanDockerEmulationDetected,
|
||||
),
|
||||
);
|
||||
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'))) {
|
||||
state = state.copyWith(error: ContainerErr(type: ContainerErrType.notInstalled));
|
||||
return;
|
||||
}
|
||||
@@ -122,9 +154,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
final version = json.decode(verRaw)['Client']['Version'];
|
||||
state = state.copyWith(version: version, error: null);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.invalidVersion, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container version failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -140,9 +174,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
final items = lines.map((e) => ContainerPs.fromRaw(e, state.type)).toList();
|
||||
state = state.copyWith(items: items);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parsePs, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container ps failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -162,9 +198,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
state = state.copyWith(images: images);
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseImages, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Container images failed', e, trace);
|
||||
}
|
||||
|
||||
@@ -189,9 +227,11 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
item.parseStats(statsLine, state.version);
|
||||
}
|
||||
} catch (e, trace) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
|
||||
);
|
||||
if (state.error == null) {
|
||||
state = state.copyWith(
|
||||
error: ContainerErr(type: ContainerErrType.parseStats, message: '$e'),
|
||||
);
|
||||
}
|
||||
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +267,10 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
}
|
||||
|
||||
Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
|
||||
if (client == null) {
|
||||
return ContainerErr(type: ContainerErrType.noClient);
|
||||
}
|
||||
|
||||
cmd = switch (state.type) {
|
||||
ContainerType.docker => 'docker $cmd',
|
||||
ContainerType.podman => 'podman $cmd',
|
||||
@@ -234,7 +278,7 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
|
||||
state = state.copyWith(runLog: '');
|
||||
final errs = <String>[];
|
||||
final code = await client?.execWithPwd(
|
||||
final (code, _) = await client?.execWithPwd(
|
||||
_wrap((await sudoCompleter.future) ? 'sudo -S $cmd' : cmd),
|
||||
context: context,
|
||||
onStdout: (data, _) {
|
||||
@@ -242,7 +286,7 @@ class ContainerNotifier extends _$ContainerNotifier {
|
||||
},
|
||||
onStderr: (data, _) => errs.add(data),
|
||||
id: hostId,
|
||||
);
|
||||
) ?? (null, null);
|
||||
state = state.copyWith(runLog: null);
|
||||
|
||||
if (code != 0) {
|
||||
|
||||
Reference in New Issue
Block a user