Skip to content

Commit 095dc7f

Browse files
authored
Parse Docker ps output with tab-separated fields (#1227)
* Use tab-delimited docker ps output and add parse tests * Handle malformed container ps rows and use Docker Status field * Fix docker ps formatting test expectation
1 parent 17b429c commit 095dc7f

3 files changed

Lines changed: 58 additions & 19 deletions

File tree

lib/data/model/container/ps.dart

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import 'package:fl_lib/fl_lib.dart';
44
import 'package:server_box/core/extension/context/locale.dart';
55
import 'package:server_box/data/model/container/status.dart';
66
import 'package:server_box/data/model/container/type.dart';
7-
import 'package:server_box/data/res/misc.dart';
87

98
sealed class ContainerPs {
109
String? get id;
@@ -146,15 +145,21 @@ final class DockerPs implements ContainerPs {
146145
'${l10n.read} ${blockParts.firstOrNull ?? '0B'} / ${l10n.write} ${blockParts.length > 1 ? blockParts[1] : '0B'}';
147146
}
148147

149-
/// CONTAINER ID NAMES IMAGE STATUS
150-
/// a049d689e7a1 aria2-pro p3terx/aria2-pro Up 3 weeks
148+
/// CONTAINER ID\tSTATUS\tNAMES\tIMAGE
149+
/// a049d689e7a1\tUp 3 weeks\taria2-pro\tp3terx/aria2-pro
151150
factory DockerPs.parse(String raw) {
152-
final parts = raw.split(Miscs.multiBlankReg);
151+
final parts = raw.split('\t');
152+
if (parts.length < 4) {
153+
throw FormatException(
154+
'Docker ps row has ${parts.length} fields, expected 4',
155+
raw,
156+
);
157+
}
153158
return DockerPs(
154159
id: parts[0],
155160
state: parts[1],
156161
names: parts[2],
157-
image: parts[3].trim(),
162+
image: parts[3],
158163
);
159164
}
160165
}

lib/data/provider/container.dart

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,14 @@ class ContainerNotifier extends _$ContainerNotifier {
286286
if (headerIdx != -1) lines.removeAt(headerIdx);
287287
}
288288
lines.removeWhere((element) => element.isEmpty);
289-
final items = lines
290-
.map((e) => ContainerPs.fromRaw(e, type))
291-
.toList();
289+
final items = <ContainerPs>[];
290+
for (final line in lines) {
291+
try {
292+
items.add(ContainerPs.fromRaw(line, type));
293+
} on FormatException catch (e, trace) {
294+
Loggers.app.warning('Skip malformed container ps row', e, trace);
295+
}
296+
}
292297
state = state.copyWith(items: items);
293298
} catch (e, trace) {
294299
if (state.error == null) {
@@ -487,14 +492,8 @@ enum ContainerCmdType {
487492
final baseCmd = switch (this) {
488493
ContainerCmdType.version => '${type.name} version $_jsonFmt',
489494
ContainerCmdType.ps => switch (type) {
490-
/// TODO: Rollback to json format when performance recovers.
491-
/// Use [_jsonFmt] in Docker will cause the operation to slow down.
492495
ContainerType.docker =>
493-
'${type.name} ps -a --format "table {{printf \\"'
494-
'%-15.15s '
495-
'%-30.30s '
496-
'${"%-50.50s " * 2}\\"'
497-
' .ID .Status .Names .Image}}"',
496+
'${type.name} ps -a --format "{{.ID}}\\t{{.Status}}\\t{{.Names}}\\t{{.Image}}"',
498497
ContainerType.podman => '${type.name} ps -a $_jsonFmt',
499498
},
500499
ContainerCmdType.stats =>

test/container_test.dart

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import 'package:flutter_test/flutter_test.dart';
22
import 'package:server_box/data/model/container/ps.dart';
33
import 'package:server_box/data/model/container/status.dart';
4+
import 'package:server_box/data/model/container/type.dart';
5+
import 'package:server_box/data/provider/container.dart';
46

57
void main() {
68
test('docker ps parse', () {
79
const raw = '''
8-
CONTAINER ID STATUS NAMES IMAGE
9-
0e9e2ef860d2 Up 2 hours hbbs rustdesk/rustdesk-server:latest
10-
9a4df3ed340c Up 41 minutes hbbr rustdesk/rustdesk-server:latest
11-
fa1215b4be74 Up 12 hours firefly uusec/firefly:latest
10+
CONTAINER ID\tSTATUS\tNAMES\tIMAGE
11+
0e9e2ef860d2\tUp 2 hours\thbbs\trustdesk/rustdesk-server:latest
12+
9a4df3ed340c\tUp 41 minutes\thbbr\trustdesk/rustdesk-server:latest
13+
fa1215b4be74\tUp 12 hours\tfirefly\tuusec/firefly:latest
1214
''';
1315
final lines = raw.split('\n');
1416
const ids = ['0e9e2ef860d2', '9a4df3ed340c', 'fa1215b4be74'];
@@ -32,6 +34,39 @@ fa1215b4be74 Up 12 hours firefly
3234
}
3335
});
3436

37+
test('docker ps parse handles long swarm container names', () {
38+
const name =
39+
'apps-all-stack_komari-agent.zdngp1z1t23llz9l30s86tq3g.fjmkg9amn0u76tbln96mmzlq2';
40+
const image = 'registry.example.com/team/komari-agent:2026.07.10';
41+
final ps = DockerPs.parse('0e9e2ef860d2\tUp 2 hours\t$name\t$image');
42+
43+
expect(name.length, greaterThan(50));
44+
expect(ps.id, '0e9e2ef860d2');
45+
expect(ps.state, 'Up 2 hours');
46+
expect(ps.names, name);
47+
expect(ps.image, image);
48+
});
49+
50+
test('docker ps parse reports malformed rows', () {
51+
expect(
52+
() => DockerPs.parse('0e9e2ef860d2\tUp 2 hours\thbbs'),
53+
throwsA(
54+
isA<FormatException>()
55+
.having((e) => e.message, 'message', contains('Docker ps row'))
56+
.having((e) => e.message, 'message', contains('expected 4')),
57+
),
58+
);
59+
});
60+
61+
test('docker ps command uses human-readable status', () {
62+
final cmd = ContainerCmdType.ps.exec(ContainerType.docker);
63+
64+
expect(
65+
cmd,
66+
'docker ps -a --format "{{.ID}}\\t{{.Status}}\\t{{.Names}}\\t{{.Image}}"',
67+
);
68+
});
69+
3570
test('docker ps status detection', () {
3671
// Test various Docker container states
3772
final testCases = [

0 commit comments

Comments
 (0)