Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 2 additions & 34 deletions lib/data/model/container/ps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import 'package:server_box/data/model/container/type.dart';
import 'package:server_box/data/res/misc.dart';

sealed class ContainerPs {
final String? id = null;
final String? image = null;
String? get id;
String? get image;
String? get name;
String? get cmd;
ContainerStatus get status;

String? cpu;
Expand All @@ -24,15 +23,12 @@ sealed class ContainerPs {
}

final class PodmanPs implements ContainerPs {
final List<String>? command;
final DateTime? created;
final bool? exited;
@override
final String? id;
@override
final String? image;
final List<String>? names;
final int? startedAt;

@override
String? cpu;
Expand All @@ -44,21 +40,15 @@ final class PodmanPs implements ContainerPs {
String? disk;

PodmanPs({
this.command,
this.created,
this.exited,
this.id,
this.image,
this.names,
this.startedAt,
});

@override
String? get name => names?.firstOrNull;

@override
String? get cmd => command?.firstOrNull;

@override
ContainerStatus get status => ContainerStatus.fromPodmanExited(exited);

Expand Down Expand Up @@ -104,33 +94,14 @@ final class PodmanPs implements ContainerPs {
factory PodmanPs.fromRawJson(String str) =>
PodmanPs.fromJson(json.decode(str));

String toRawJson() => json.encode(toJson());

factory PodmanPs.fromJson(Map<String, dynamic> json) => PodmanPs(
command: json['Command'] == null
? []
: List<String>.from(json['Command']!.map((x) => x)),
created: json['Created'] == null ? null : DateTime.parse(json['Created']),
exited: json['Exited'],
id: json['Id'],
image: json['Image'],
names: json['Names'] == null
? []
: List<String>.from(json['Names']!.map((x) => x)),
startedAt: json['StartedAt'],
);

Map<String, dynamic> toJson() => {
'Command': command == null
? []
: List<dynamic>.from(command!.map((x) => x)),
'Created': created?.toIso8601String(),
'Exited': exited,
'Id': id,
'Image': image,
'Names': names == null ? [] : List<dynamic>.from(names!.map((x) => x)),
'StartedAt': startedAt,
};
}

final class DockerPs implements ContainerPs {
Expand All @@ -155,9 +126,6 @@ final class DockerPs implements ContainerPs {
@override
String? get name => names;

@override
String? get cmd => null;

@override
ContainerStatus get status => ContainerStatus.fromDockerState(state);

Expand Down
109 changes: 80 additions & 29 deletions lib/data/provider/container.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ abstract class ContainerState with _$ContainerState {
class ContainerNotifier extends _$ContainerNotifier {
var sudoCompleter = Completer<bool>();
String? _cachedPassword;
var _refreshGeneration = 0;
var _pendingRefresh = false;

@override
ContainerState build(
Expand Down Expand Up @@ -70,55 +72,88 @@ class ContainerNotifier extends _$ContainerNotifier {
}

Future<void> setType(ContainerType type) async {
final refreshGeneration = _resetSudoProbe();
state = state.copyWith(
type: type,
error: null,
runLog: null,
items: null,
images: null,
version: null,
isBusy: false,
);
Stores.container.setType(type, hostId);
await refresh(generation: refreshGeneration);
}

void resetSudoProbe() {
_resetSudoProbe();
state = state.copyWith(isBusy: false);
}

int _resetSudoProbe() {
sudoCompleter = Completer<bool>();
await refresh();
return ++_refreshGeneration;
}

void _requiresSudo() async {
bool _isStaleRefresh(int generation) {
return generation != _refreshGeneration || !ref.mounted;
}

Future<void> _requiresSudo(
Completer<bool> completer,
ContainerType type,
) async {
/// Podman is rootless
if (state.type == ContainerType.podman) {
return sudoCompleter.complete(false);
if (type == ContainerType.podman) {
return completer.complete(false);
}
if (!Stores.setting.containerTrySudo.fetch()) {
return sudoCompleter.complete(false);
return completer.complete(false);
}

final res = await client?.run(
_wrap(ContainerCmdType.images.exec(state.type)),
);
if (res?.string.toLowerCase().contains('permission denied') ?? false) {
return sudoCompleter.complete(true);
try {
final res = await client?.run(
_wrap(ContainerCmdType.images.exec(type)),
);
if (res?.string.toLowerCase().contains('permission denied') ?? false) {
return completer.complete(true);
}
return completer.complete(false);
} catch (e, trace) {
Loggers.app.warning('Container sudo check failed', e, trace);
if (!completer.isCompleted) {
completer.complete(false);
}
}
return sudoCompleter.complete(false);
}

Future<void> refresh({bool isAuto = false}) async {
if (state.isBusy) return;
Future<void> refresh({bool isAuto = false, int? generation}) async {
if (state.isBusy) {
_pendingRefresh = true;
return;
}
final refreshGeneration = generation ?? _refreshGeneration;
final type = state.type;
state = state.copyWith(isBusy: true);

if (!sudoCompleter.isCompleted) _requiresSudo();
final sudo = sudoCompleter;
if (!sudo.isCompleted) unawaited(_requiresSudo(sudo, type));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

final sudo = await sudoCompleter.future;
final needSudo = await sudo.future;
if (_isStaleRefresh(refreshGeneration)) return;

/// If sudo is required and auto refresh is enabled, skip the refresh.
/// Or this will ask for pwd again and again.
if (sudo && isAuto) {
if (needSudo && isAuto) {
state = state.copyWith(isBusy: false);
return;
}

String? password;
if (sudo) {
if (needSudo) {
password = await _getSudoPassword();
if (_isStaleRefresh(refreshGeneration)) return;
if (password == null) {
state = state.copyWith(
isBusy: false,
Expand All @@ -135,8 +170,8 @@ class ContainerNotifier extends _$ContainerNotifier {

final cmd = _wrap(
ContainerCmdType.execAll(
state.type,
sudo: sudo,
type,
sudo: needSudo,
includeStats: includeStats,
password: password,
),
Expand All @@ -156,14 +191,15 @@ class ContainerNotifier extends _$ContainerNotifier {
},
);
} else {
if (_isStaleRefresh(refreshGeneration)) return;
state = state.copyWith(
isBusy: false,
error: ContainerErr(type: ContainerErrType.noClient),
);
return;
}

if (!ref.mounted) return;
if (_isStaleRefresh(refreshGeneration)) return;
state = state.copyWith(isBusy: false);

if (!context.mounted) return;
Expand Down Expand Up @@ -242,13 +278,16 @@ class ContainerNotifier extends _$ContainerNotifier {
final psRaw = ContainerCmdType.ps.find(segments);
try {
final lines = psRaw.split('\n');
if (state.type == ContainerType.docker) {
if (type == ContainerType.docker) {
/// Due to the fetched data is not in json format, skip table header
lines.removeWhere((element) => element.contains('CONTAINER ID'));
final headerIdx = lines.indexWhere((element) {
return element.trimLeft().startsWith('CONTAINER ID');
});
if (headerIdx != -1) lines.removeAt(headerIdx);
}
lines.removeWhere((element) => element.isEmpty);
final items = lines
.map((e) => ContainerPs.fromRaw(e, state.type))
.map((e) => ContainerPs.fromRaw(e, type))
.toList();
state = state.copyWith(items: items);
} catch (e, trace) {
Expand All @@ -267,13 +306,13 @@ class ContainerNotifier extends _$ContainerNotifier {
List<ContainerImg> images;
if (isEntireJson) {
images = (json.decode(imageRaw) as List)
.map((e) => ContainerImg.fromRawJson(json.encode(e), state.type))
.map((e) => ContainerImg.fromRawJson(json.encode(e), type))
.toList();
} else {
final lines = imageRaw.split('\n');
lines.removeWhere((element) => element.isEmpty);
images = lines
.map((e) => ContainerImg.fromRawJson(e, state.type))
.map((e) => ContainerImg.fromRawJson(e, type))
.toList();
}
state = state.copyWith(images: images);
Expand All @@ -295,7 +334,10 @@ class ContainerNotifier extends _$ContainerNotifier {
final statsLines = statsRaw.split('\n');
statsLines.removeWhere((element) => element.isEmpty);
final items = state.items;
if (items == null) return;
if (items == null) {
await _refreshPendingIfNeeded(refreshGeneration);
return;
}

for (var item in items) {
final id = item.id;
Expand All @@ -317,6 +359,13 @@ class ContainerNotifier extends _$ContainerNotifier {
}
Loggers.app.warning('Parse docker stats: $statsRaw', e, trace);
}
await _refreshPendingIfNeeded(refreshGeneration);
}

Future<void> _refreshPendingIfNeeded(int generation) async {
if (_isStaleRefresh(generation) || !_pendingRefresh || state.isBusy) return;
_pendingRefresh = false;
await refresh(generation: generation);
}

Future<ContainerErr?> stop(String id) async => await run('stop $id');
Expand Down Expand Up @@ -349,7 +398,7 @@ class ContainerNotifier extends _$ContainerNotifier {
return await run('system prune -a -f --volumes');
}

Future<ContainerErr?> run(String cmd, {bool autoRefresh = true}) async {
Future<ContainerErr?> run(String cmd) async {
if (client == null) {
return ContainerErr(type: ContainerErrType.noClient);
}
Expand Down Expand Up @@ -400,7 +449,7 @@ class ContainerNotifier extends _$ContainerNotifier {
message: 'Command execution failed',
);
}
if (autoRefresh) await refresh();
await refresh();
return null;
}

Expand All @@ -410,7 +459,7 @@ class ContainerNotifier extends _$ContainerNotifier {
cmd = 'export LANG=en_US.UTF-8 && $cmd';
final noDockerHost = dockerHost?.isEmpty ?? true;
if (!noDockerHost) {
cmd = 'export DOCKER_HOST=$dockerHost && $cmd';
cmd = 'export DOCKER_HOST=${_shellQuote(dockerHost!)} && $cmd';
}
return cmd;
}
Expand All @@ -423,6 +472,8 @@ String _buildSudoCmd(String baseCmd, String password) {
return 'echo "$pwdBase64" | base64 -d | sudo -S $baseCmd';
}

String _shellQuote(String value) => "'${value.replaceAll("'", "'\\''")}'";

enum ContainerCmdType {
version,
ps,
Expand Down
1 change: 1 addition & 0 deletions lib/view/page/container/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ extension on _ContainerPageState {
void _onSaveDockerHost(String val) {
context.pop();
Stores.container.put(widget.args.spi.id, val.trim());
_containerNotifier.resetSudoProbe();
_containerNotifier.refresh();
}

Expand Down