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 72aaa173f5.

* 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:
GT610
2026-01-17 14:40:44 +08:00
committed by GitHub
parent cd3c094af0
commit 39a3e0800b
32 changed files with 181 additions and 54 deletions

View File

@@ -112,7 +112,7 @@ extension SSHClientX on SSHClient {
return (session, result.takeBytes().string);
}
Future<int?> execWithPwd(
Future<(int?, String)> execWithPwd(
String script, {
String? entry,
BuildContext? context,
@@ -121,7 +121,7 @@ extension SSHClientX on SSHClient {
required String id,
}) async {
var isRequestingPwd = false;
final (session, _) = await exec(
final (session, output) = await exec(
(sess) {
sess.stdin.add('$script\n'.uint8List);
sess.stdin.close();
@@ -147,7 +147,7 @@ extension SSHClientX on SSHClient {
onStdout: onStdout,
entry: entry,
);
return session.exitCode;
return (session.exitCode, output);
}
Future<String> execForOutput(

View File

@@ -26,6 +26,7 @@ enum ContainerErrType {
parsePs,
parseImages,
parseStats,
podmanDetected,
}
class ContainerErr extends Err<ContainerErrType> {

View File

@@ -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) {

View File

@@ -1933,6 +1933,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Logs'**
String get logs;
/// No description provided for @podmanDockerEmulationDetected.
///
/// In en, this message translates to:
/// **'Podman Docker emulation detected. Please switch to Podman in settings.'**
String get podmanDockerEmulationDetected;
}
class _AppLocalizationsDelegate

View File

@@ -1031,4 +1031,8 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get logs => 'Protokolle';
@override
String get podmanDockerEmulationDetected =>
'Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman.';
}

View File

@@ -1022,4 +1022,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get logs => 'Logs';
@override
String get podmanDockerEmulationDetected =>
'Podman Docker emulation detected. Please switch to Podman in settings.';
}

View File

@@ -1033,4 +1033,8 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get logs => 'Registros';
@override
String get podmanDockerEmulationDetected =>
'Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración.';
}

View File

@@ -1036,4 +1036,8 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get logs => 'Journaux';
@override
String get podmanDockerEmulationDetected =>
'Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres.';
}

View File

@@ -1022,4 +1022,8 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get logs => 'Log';
@override
String get podmanDockerEmulationDetected =>
'Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan.';
}

View File

@@ -992,4 +992,8 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get logs => 'ログ';
@override
String get podmanDockerEmulationDetected =>
'Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。';
}

View File

@@ -1029,4 +1029,8 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get logs => 'Logboeken';
@override
String get podmanDockerEmulationDetected =>
'Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen.';
}

View File

@@ -1024,4 +1024,8 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get logs => 'Logs';
@override
String get podmanDockerEmulationDetected =>
'Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações.';
}

View File

@@ -1028,4 +1028,8 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get logs => 'Журналы';
@override
String get podmanDockerEmulationDetected =>
'Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках.';
}

View File

@@ -1023,4 +1023,8 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get logs => 'Günlükler';
@override
String get podmanDockerEmulationDetected =>
'Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman\'a geçin.';
}

View File

@@ -1028,4 +1028,8 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get logs => 'Журнали';
@override
String get podmanDockerEmulationDetected =>
'Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях.';
}

View File

@@ -977,6 +977,10 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get logs => '日志';
@override
String get podmanDockerEmulationDetected =>
'检测到 Podman Docker 仿真。请在设置中切换到 Podman。';
}
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
@@ -1931,4 +1935,8 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
@override
String get logs => '日誌';
@override
String get podmanDockerEmulationDetected =>
'檢測到 Podman Docker 仿真。請在設定中切換到 Podman。';
}

View File

@@ -294,5 +294,6 @@
"write": "Schreiben",
"writeScriptFailTip": "Das Schreiben des Skripts ist fehlgeschlagen, möglicherweise aufgrund fehlender Berechtigungen oder das Verzeichnis existiert nicht.",
"writeScriptTip": "Nach der Verbindung mit dem Server wird ein Skript in `~/.config/server_box` \n | `/tmp/server_box` geschrieben, um den Systemstatus zu überwachen. Sie können den Skriptinhalt überprüfen.",
"logs": "Protokolle"
"logs": "Protokolle",
"podmanDockerEmulationDetected": "Podman Docker-Emulation erkannt. Bitte wechseln Sie in den Einstellungen zu Podman."
}

View File

@@ -304,5 +304,6 @@
"menuGitHubRepository": "GitHub Repository",
"menuWiki": "Wiki",
"menuHelp": "Help",
"logs": "Logs"
"logs": "Logs",
"podmanDockerEmulationDetected": "Podman Docker emulation detected. Please switch to Podman in settings."
}

View File

@@ -294,5 +294,6 @@
"write": "Escribir",
"writeScriptFailTip": "La escritura en el script falló, posiblemente por falta de permisos o porque el directorio no existe.",
"writeScriptTip": "Después de conectarse al servidor, se escribirá un script en `~/.config/server_box` \n | `/tmp/server_box` para monitorear el estado del sistema. Puedes revisar el contenido del script.",
"logs": "Registros"
"logs": "Registros",
"podmanDockerEmulationDetected": "Detectada emulación de Podman Docker. Por favor, cambie a Podman en la configuración."
}

View File

@@ -294,5 +294,6 @@
"write": "Écrire",
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script.",
"logs": "Journaux"
"logs": "Journaux",
"podmanDockerEmulationDetected": "Émulation Podman Docker détectée. Veuillez passer à Podman dans les paramètres."
}

View File

@@ -294,5 +294,6 @@
"write": "Tulis",
"writeScriptFailTip": "Penulisan ke skrip gagal, mungkin karena tidak ada izin atau direktori tidak ada.",
"writeScriptTip": "Setelah terhubung ke server, sebuah skrip akan ditulis ke `~/.config/server_box` \n | `/tmp/server_box` untuk memantau status sistem. Anda dapat meninjau konten skrip tersebut.",
"logs": "Log"
"logs": "Log",
"podmanDockerEmulationDetected": "Emulasi Podman Docker terdeteksi. Silakan beralih ke Podman di pengaturan."
}

View File

@@ -294,5 +294,6 @@
"write": "書き込み",
"writeScriptFailTip": "スクリプトの書き込みに失敗しました。権限がないかディレクトリが存在しない可能性があります。",
"writeScriptTip": "サーバーへの接続後、システムステータスを監視するスクリプトが `~/.config/server_box` \n | `/tmp/server_box` に書き込まれます。スクリプトの内容を確認できます。",
"logs": "ログ"
"logs": "ログ",
"podmanDockerEmulationDetected": "Podman Docker エミュレーションが検出されました。設定で Podman に切り替えてください。"
}

View File

@@ -294,5 +294,6 @@
"write": "Schrijven",
"writeScriptFailTip": "Het schrijven naar het script is mislukt, mogelijk door gebrek aan rechten of omdat de map niet bestaat.",
"writeScriptTip": "Na het verbinden met de server wordt een script geschreven naar `~/.config/server_box` \n | `/tmp/server_box` om de systeemstatus te monitoren. U kunt de inhoud van het script controleren.",
"logs": "Logboeken"
"logs": "Logboeken",
"podmanDockerEmulationDetected": "Podman Docker-emulatie gedetecteerd. Schakel over naar Podman in de instellingen."
}

View File

@@ -294,5 +294,6 @@
"write": "Escrita",
"writeScriptFailTip": "Falha ao escrever no script, possivelmente devido à falta de permissões ou o diretório não existe.",
"writeScriptTip": "Após conectar ao servidor, um script será escrito em `~/.config/server_box` \n | `/tmp/server_box` para monitorar o status do sistema. Você pode revisar o conteúdo do script.",
"logs": "Logs"
"logs": "Logs",
"podmanDockerEmulationDetected": "Emulação Podman Docker detectada. Por favor, alterne para Podman nas configurações."
}

View File

@@ -294,5 +294,6 @@
"write": "Запись",
"writeScriptFailTip": "Запись скрипта не удалась, возможно, из-за отсутствия прав или потому что, директории не существует.",
"writeScriptTip": "После подключения к серверу скрипт будет записан в `~/.config/server_box` \n | `/tmp/server_box` для мониторинга состояния системы. Вы можете проверить содержимое скрипта.",
"logs": "Журналы"
"logs": "Журналы",
"podmanDockerEmulationDetected": "Обнаружена эмуляция Podman Docker. Пожалуйста, переключитесь на Podman в настройках."
}

View File

@@ -294,5 +294,6 @@
"write": "Yaz",
"writeScriptFailTip": "Betik yazma başarısız oldu, muhtemelen izin eksikliği veya dizin mevcut değil.",
"writeScriptTip": "Sunucuya bağlandıktan sonra, sistem durumunu izlemek için `~/.config/server_box` \n | `/tmp/server_box` dizinine bir betik yazılacak. Betik içeriğini inceleyebilirsiniz.",
"logs": "Günlükler"
"logs": "Günlükler",
"podmanDockerEmulationDetected": "Podman Docker emülasyonu tespit edildi. Lütfen ayarlarda Podman'a geçin."
}

View File

@@ -294,5 +294,6 @@
"write": "Записати",
"writeScriptFailTip": "Запис у скрипт не вдався, можливо, через брак дозволів або каталог не існує.",
"writeScriptTip": "Після підключення до сервера скрипт буде записано у `~/.config/server_box` \n | `/tmp/server_box` для моніторингу стану системи. Ви можете переглянути вміст скрипта.",
"logs": "Журнали"
"logs": "Журнали",
"podmanDockerEmulationDetected": "Виявлено емуляцію Podman Docker. Будь ласка, переключіться на Podman у налаштуваннях."
}

View File

@@ -301,5 +301,6 @@
"menuGitHubRepository": "GitHub 仓库",
"menuWiki": "Wiki",
"menuHelp": "帮助",
"logs": "日志"
"logs": "日志",
"podmanDockerEmulationDetected": "检测到 Podman Docker 仿真。请在设置中切换到 Podman。"
}

View File

@@ -294,5 +294,6 @@
"write": "寫入",
"writeScriptFailTip": "寫入腳本失敗,可能是沒有權限/目錄不存在等。",
"writeScriptTip": "連線到伺服器後,將會在 `~/.config/server_box` \n | `/tmp/server_box` 中寫入一個腳本來監測系統狀態。你可以審查腳本內容。",
"logs": "日誌"
"logs": "日誌",
"podmanDockerEmulationDetected": "檢測到 Podman Docker 仿真。請在設定中切換到 Podman。"
}

View File

@@ -234,7 +234,7 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
if (item.cpu == null || item.mem == null) return UIs.placeholder;
return LayoutBuilder(
builder: (_, cons) {
final width = cons.maxWidth / 2 - 41;
final width = cons.maxWidth / 2 - 6.5;
return Column(
children: [
UIs.height13,
@@ -264,10 +264,17 @@ class _ContainerPageState extends ConsumerState<ContainerPage> {
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: Colors.grey),
UIs.width7,
Text(value ?? l10n.unknown, style: UIs.text11Grey),
Expanded(
child: Text(
value ?? l10n.unknown,
style: UIs.text11Grey,
overflow: TextOverflow.ellipsis,
),
),
],
),
],

View File

@@ -49,11 +49,12 @@ extension _Operation on _ServerPageState {
await context.showRoundDialog(title: libL10n.attention, child: Text(l10n.suspendTip));
Stores.setting.showSuspendTip.put(false);
}
srv.client?.execWithPwd(
await srv.client?.execWithPwd(
ShellFunc.suspend.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
context: context,
id: srv.id,
);
) ??
(null, '');
},
typ: l10n.suspend,
name: srv.spi.name,
@@ -62,11 +63,13 @@ extension _Operation on _ServerPageState {
void _onTapShutdown(ServerState srv) {
_askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
context: context,
id: srv.id,
),
func: () async {
await srv.client?.execWithPwd(
ShellFunc.shutdown.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
context: context,
id: srv.id,
);
},
typ: l10n.shutdown,
name: srv.spi.name,
);
@@ -74,11 +77,14 @@ extension _Operation on _ServerPageState {
void _onTapReboot(ServerState srv) {
_askFor(
func: () => srv.client?.execWithPwd(
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
context: context,
id: srv.id,
),
func: () async {
await srv.client?.execWithPwd(
ShellFunc.reboot.exec(srv.spi.id, systemType: srv.status.system, customDir: null),
context: context,
id: srv.id,
) ??
(null, '');
},
typ: l10n.reboot,
name: srv.spi.name,
);

View File

@@ -12,7 +12,6 @@ import flutter_secure_storage_macos
import icloud_storage
import local_auth_darwin
import package_info_plus
import path_provider_foundation
import screen_retriever_macos
import share_plus
import shared_preferences_foundation
@@ -28,7 +27,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin"))
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))