diff --git a/lib/data/model/container/ps.dart b/lib/data/model/container/ps.dart index 3833c467..e29858db 100644 --- a/lib/data/model/container/ps.dart +++ b/lib/data/model/container/ps.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/core/extension/context/locale.dart'; +import 'package:server_box/data/model/container/status.dart'; import 'package:server_box/data/model/container/type.dart'; import 'package:server_box/data/res/misc.dart'; @@ -10,7 +11,7 @@ sealed class ContainerPs { final String? image = null; String? get name; String? get cmd; - bool get running; + ContainerStatus get status; String? cpu; String? mem; @@ -51,7 +52,7 @@ final class PodmanPs implements ContainerPs { String? get cmd => command?.firstOrNull; @override - bool get running => exited != true; + ContainerStatus get status => ContainerStatus.fromPodmanExited(exited); @override void parseStats(String s) { @@ -121,10 +122,7 @@ final class DockerPs implements ContainerPs { String? get cmd => null; @override - bool get running { - if (state?.contains('Exited') == true) return false; - return true; - } + ContainerStatus get status => ContainerStatus.fromDockerState(state); @override void parseStats(String s) { diff --git a/lib/data/model/container/status.dart b/lib/data/model/container/status.dart new file mode 100644 index 00000000..9fe04802 --- /dev/null +++ b/lib/data/model/container/status.dart @@ -0,0 +1,70 @@ +import 'package:fl_lib/fl_lib.dart'; +import 'package:server_box/core/extension/context/locale.dart'; + +/// Represents the various states a container can be in. +/// Supports both Docker and Podman container status parsing. +enum ContainerStatus { + running, + exited, + created, + paused, + restarting, + removing, + dead, + unknown; + + /// Check if the container is actively running + bool get isRunning => this == ContainerStatus.running; + + /// Check if the container can be started + bool get canStart => + this == ContainerStatus.exited || + this == ContainerStatus.created || + this == ContainerStatus.dead; + + /// Check if the container can be stopped + bool get canStop => + this == ContainerStatus.running || this == ContainerStatus.paused; + + /// Check if the container can be restarted + bool get canRestart => + this != ContainerStatus.removing && this != ContainerStatus.unknown; + + /// Parse Docker container status string to ContainerStatus + static ContainerStatus fromDockerState(String? state) { + if (state == null || state.isEmpty) return ContainerStatus.unknown; + + final lowerState = state.toLowerCase(); + + if (lowerState.startsWith('up')) return ContainerStatus.running; + if (lowerState.contains('exited')) return ContainerStatus.exited; + if (lowerState.contains('created')) return ContainerStatus.created; + if (lowerState.contains('paused')) return ContainerStatus.paused; + if (lowerState.contains('restarting')) return ContainerStatus.restarting; + if (lowerState.contains('removing')) return ContainerStatus.removing; + if (lowerState.contains('dead')) return ContainerStatus.dead; + + return ContainerStatus.unknown; + } + + /// Parse Podman container status from exited boolean + static ContainerStatus fromPodmanExited(bool? exited) { + if (exited == true) return ContainerStatus.exited; + if (exited == false) return ContainerStatus.running; + return ContainerStatus.unknown; + } + + /// Get display string for the status + String get displayName { + return switch (this) { + ContainerStatus.running => l10n.running, + ContainerStatus.exited => libL10n.exit, + ContainerStatus.created => 'Created', + ContainerStatus.paused => 'Paused', + ContainerStatus.restarting => 'Restarting', + ContainerStatus.removing => 'Removing', + ContainerStatus.dead => 'Dead', + ContainerStatus.unknown => libL10n.unknown, + }; + } +} diff --git a/lib/view/page/container/container.dart b/lib/view/page/container/container.dart index ef284d60..cd54110b 100644 --- a/lib/view/page/container/container.dart +++ b/lib/view/page/container/container.dart @@ -188,7 +188,7 @@ class _ContainerPageState extends ConsumerState { Widget _buildPs(ContainerState containerState) { final items = containerState.items; if (items == null) return UIs.placeholder; - final running = items.where((e) => e.running).length; + final running = items.where((e) => e.status.isRunning).length; final stopped = items.length - running; final subtitle = stopped > 0 ? l10n.dockerStatusRunningAndStoppedFmt(running, stopped) @@ -219,8 +219,8 @@ class _ContainerPageState extends ConsumerState { ), Text( '${item.image ?? l10n.unknown} - ${switch (item) { - final PodmanPs ps => ps.running ? l10n.running : l10n.stopped, - final DockerPs ps => ps.state, + final PodmanPs ps => ps.status.displayName, + final DockerPs ps => ps.state ?? ps.status.displayName, }}', style: UIs.text13Grey, ), @@ -277,7 +277,7 @@ class _ContainerPageState extends ConsumerState { Widget _buildMoreBtn(ContainerPs dItem) { return PopupMenu( - items: ContainerMenu.items(dItem.running).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(), + items: ContainerMenu.items(dItem.status.isRunning).map((e) => PopMenu.build(e, e.icon, e.toStr)).toList(), onSelected: (item) => _onTapMoreBtn(item, dItem), ); } diff --git a/test/container_test.dart b/test/container_test.dart index 0c5d0a52..8089a70f 100644 --- a/test/container_test.dart +++ b/test/container_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:server_box/data/model/container/ps.dart'; +import 'package:server_box/data/model/container/status.dart'; void main() { test('docker ps parse', () { @@ -26,7 +27,92 @@ fa1215b4be74 Up 12 hours firefly expect(ps.names, names[idx - 1]); expect(ps.image, images[idx - 1]); expect(ps.state, states[idx - 1]); - expect(ps.running, true); + expect(ps.status, ContainerStatus.running); + expect(ps.status.isRunning, true); } }); + + test('docker ps status detection', () { + // Test various Docker container states + final testCases = [ + // Running states + {'state': 'Up 2 minutes', 'status': ContainerStatus.running}, + {'state': 'Up 1 hour', 'status': ContainerStatus.running}, + {'state': 'UP 30 seconds', 'status': ContainerStatus.running}, // Case insensitive + {'state': 'up 5 days', 'status': ContainerStatus.running}, // Case insensitive + + // Non-running states + {'state': 'Exited (0) 5 minutes ago', 'status': ContainerStatus.exited}, + {'state': 'Created', 'status': ContainerStatus.created}, + {'state': 'Paused', 'status': ContainerStatus.paused}, + {'state': 'Restarting', 'status': ContainerStatus.restarting}, + {'state': 'Removing', 'status': ContainerStatus.removing}, + {'state': 'Dead', 'status': ContainerStatus.dead}, + + // Edge cases + {'state': null, 'status': ContainerStatus.unknown}, + {'state': '', 'status': ContainerStatus.unknown}, + {'state': 'Some Unknown Status', 'status': ContainerStatus.unknown}, + ]; + + for (final testCase in testCases) { + final ps = DockerPs(id: 'test', state: testCase['state'] as String?); + final expectedStatus = testCase['status'] as ContainerStatus; + expect( + ps.status, + expectedStatus, + reason: 'State "${testCase['state']}" should be ${expectedStatus.name}' + ); + + // Test status.isRunning method + expect( + ps.status.isRunning, + expectedStatus.isRunning, + reason: 'State "${testCase['state']}" isRunning should match status.isRunning' + ); + } + }); + + test('podman ps status detection', () { + final testCases = [ + {'exited': false, 'status': ContainerStatus.running}, + {'exited': true, 'status': ContainerStatus.exited}, + {'exited': null, 'status': ContainerStatus.unknown}, + ]; + + for (final testCase in testCases) { + final ps = PodmanPs(id: 'test', exited: testCase['exited'] as bool?); + final expectedStatus = testCase['status'] as ContainerStatus; + expect( + ps.status, + expectedStatus, + reason: 'Exited "${testCase['exited']}" should be ${expectedStatus.name}' + ); + + // Test status.isRunning method + expect( + ps.status.isRunning, + expectedStatus.isRunning, + reason: 'Exited "${testCase['exited']}" isRunning should match status.isRunning' + ); + } + }); + + test('container status utility methods', () { + expect(ContainerStatus.running.isRunning, true); + expect(ContainerStatus.exited.isRunning, false); + expect(ContainerStatus.created.isRunning, false); + + expect(ContainerStatus.exited.canStart, true); + expect(ContainerStatus.created.canStart, true); + expect(ContainerStatus.running.canStart, false); + + expect(ContainerStatus.running.canStop, true); + expect(ContainerStatus.paused.canStop, true); + expect(ContainerStatus.exited.canStop, false); + + expect(ContainerStatus.running.canRestart, true); + expect(ContainerStatus.removing.canRestart, false); + expect(ContainerStatus.unknown.canRestart, false); + }); }