Compare commits

...

14 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
0a0928e2f6 fix: treat empty jumpChainIds as no jump 2026-01-17 23:55:41 +08:00
lollipopkit🏳️‍⚧️
61f161d8a6 opt. 2026-01-15 20:52:11 +08:00
lollipopkit🏳️‍⚧️
52c80795f4 opt. 2026-01-15 20:21:28 +08:00
lollipopkit🏳️‍⚧️
09f1ab2cf2 fix 2026-01-15 20:09:33 +08:00
lollipopkit🏳️‍⚧️
2eeb55c1d8 fix 2026-01-15 13:46:53 +08:00
lollipopkit🏳️‍⚧️
6738ac94f8 opt. 2026-01-15 13:15:31 +08:00
lollipopkit🏳️‍⚧️
827d40b8b5 opt. 2026-01-15 13:02:17 +08:00
lollipopkit🏳️‍⚧️
928f2becf1 fix 2026-01-15 12:41:10 +08:00
lollipopkit🏳️‍⚧️
7d30af44d6 fix 2026-01-15 10:10:21 +08:00
lollipopkit🏳️‍⚧️
35349a90eb opt.: deduplicate & merge 2026-01-15 10:10:14 +08:00
lollipopkit🏳️‍⚧️
8be9b9b10b impl: jump logic
Fixes #356
2026-01-15 09:42:34 +08:00
lollipopkit🏳️‍⚧️
c51cf62015 feat: jump server chain
Fixes #356
2026-01-14 22:36:47 +08:00
lollipopkit🏳️‍⚧️
8589b3b4d7 opt.: add a btn to minimize ai dialog (#1004)
* opt.: add a btn to minimize ai dialog
Fixes #1003

* opt.

* opt.
2026-01-14 15:15:33 +08:00
GT610
7693e30cbf opt: Better performance on server refreshing (#999)
* refactor(server): Replace Future.wait with an explicit list of futures to enhance readability

Refactor the nested map and async functions into explicit for loops and future lists to make the code logic clearer

* fix(server): Fixed the auto-refresh logic and concurrency control issues

- Add `_refreshCompleter` to prevent concurrent refreshes
- Fixed the issue where the status was not updated after the automatic refresh timer was canceled
- Remove the invalid check for `duration == 1`

* refactor(server): Optimize the server refresh logic by filtering out servers that do not need to be refreshed in advance

Move the server filtering logic outside the loop and use the `where` method to filter the servers that need to be refreshed, avoiding repeated condition checks within the loop. This improves code readability and reduces redundant condition checks.

* refactor: Optimize server refresh logic to enhance readability

Break down complex conditional checks into clearer steps, separating the logic for server refresh and rate limiter reset. Replace chained calls with explicit loops to make the code easier to maintain and understand.

* refactor(server): Remove `updateFuture` from `ServerState` and use the `_isRefreshing` flag instead

Simplify the server refresh logic, replace Future state tracking with a boolean flag, and avoid unnecessary state updates

* refactor(server_detail): Extract the setting items as local variables to improve performance

Extract the globally set items that are accessed repeatedly as local variables, reduce unnecessary state retrieval operations, and optimize page performance

* refactor: Rename `_displayCpuIndexSetting` to `_displayCpuIndex` for consistency

* refactor(server): Fix the issue of parallel blocking in server refresh

The original code uses Future.wait to wait for all refresh operations to complete, but in fact, there is no need to wait for the results of these operations. Instead, directly calling ignore() to ignore the results can avoid blocking caused by the slowest server

* fix: Adjust the order of logging and default value settings

Ensure to set the default value after recording the invalid duration warning

* refactor(server): Rename _refreshCompleter to _refreshInProgress to enhance readability

Change the variable name from `_refreshCompleter` to `_refreshInProgress`, so that it more accurately reflects the actual purpose of the variable, which is to indicate whether the refresh operation is in progress

* refactor(server): Remove unnecessary refresh progress status management

Simplify the server refresh logic, remove the unused _refreshInProgress state variable and related Completer handling, making the code more concise and straightforward

* chore: Update dependent package versions

Update the following dependent package versions:
- camera_web has been upgraded from 0.3.5 to 0.3.5+3
- ffi has been upgraded from 2.1.4 to 2.1.5
- hive_ce_flutter is upgraded from 2.3.3 to 2.3.4
- watcher is upgraded from 1.1.4 to 1.2.1

* opt.

---------

Co-authored-by: lollipopkit🏳️‍⚧️ <10864310+lollipopkit@users.noreply.github.com>
2026-01-14 13:47:06 +08:00
25 changed files with 876 additions and 289 deletions

View File

@@ -38,6 +38,77 @@ String getPrivateKey(String id) {
return pki.key; return pki.key;
} }
List<Spi> resolveMergedJumpChain(
Spi target, {
List<Spi>? jumpChain,
}) {
final injectedSpiMap = <String, Spi>{};
if (jumpChain != null) {
for (final s in jumpChain) {
injectedSpiMap[s.id] = s;
if (s.oldId.isNotEmpty) {
injectedSpiMap[s.oldId] = s;
}
}
}
Spi resolveSpi(String id) {
final injected = injectedSpiMap[id];
if (injected != null) return injected;
if (jumpChain != null) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
}
final fromStore = Stores.server.box.get(id);
if (fromStore == null) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
}
return fromStore;
}
return _resolveMergedJumpChainInternal(target, resolveSpi: resolveSpi);
}
List<Spi> _resolveMergedJumpChainInternal(
Spi target, {
required Spi Function(String id) resolveSpi,
}) {
final roots = target.jumpChainIds ?? (target.jumpId == null ? const <String>[] : [target.jumpId!]);
if (roots.isEmpty) return const <Spi>[];
final seen = <String>{};
final stack = <String>{};
final out = <Spi>[];
String normId(Spi spi) => spi.id.isNotEmpty ? spi.id : spi.oldId;
void dfs(String id) {
final hop = resolveSpi(id);
final norm = normId(hop);
if (stack.contains(norm)) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at $norm');
}
if (seen.contains(norm)) return;
stack.add(norm);
final deps = hop.jumpChainIds ?? (hop.jumpId == null ? const <String>[] : [hop.jumpId!]);
for (final dep in deps) {
dfs(dep);
}
stack.remove(norm);
if (seen.add(norm)) {
out.add(hop);
}
}
for (final r in roots) {
dfs(r);
}
return out;
}
Future<SSHClient> genClient( Future<SSHClient> genClient(
Spi spi, { Spi spi, {
void Function(GenSSHClientStatus)? onStatus, void Function(GenSSHClientStatus)? onStatus,
@@ -45,14 +116,17 @@ Future<SSHClient> genClient(
/// Only pass this param if using multi-threading and key login /// Only pass this param if using multi-threading and key login
String? privateKey, String? privateKey,
/// Only pass this param if using multi-threading and key login /// Pre-resolved jump chain (in `spi.jumpId` order: immediate -> farthest).
String? jumpPrivateKey,
Duration timeout = const Duration(seconds: 5),
/// [Spi] of the jump server
/// ///
/// Must pass this param if using multi-threading and key login /// This is mainly used when `Stores` is unavailable (e.g. in an isolate).
Spi? jumpSpi, List<Spi>? jumpChain,
/// Private keys for [jumpChain], aligned by index.
///
/// If a jump server uses key auth (`keyId != null`), you must provide the
/// decrypted key pem here (or `genClient` will try to read from `Stores`).
List<String?>? jumpPrivateKeys,
Duration timeout = const Duration(seconds: 5),
/// Handle keyboard-interactive authentication /// Handle keyboard-interactive authentication
SSHUserInfoRequestHandler? onKeyboardInteractive, SSHUserInfoRequestHandler? onKeyboardInteractive,
@@ -60,6 +134,41 @@ Future<SSHClient> genClient(
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted, void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt, Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
}) async { }) async {
return _genClientInternal(
spi,
onStatus: onStatus,
privateKey: privateKey,
jumpChain: jumpChain,
jumpPrivateKeys: jumpPrivateKeys,
timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive,
knownHostFingerprints: knownHostFingerprints,
onHostKeyAccepted: onHostKeyAccepted,
onHostKeyPrompt: onHostKeyPrompt,
visited: <String>{},
);
}
Future<SSHClient> _genClientInternal(
Spi spi, {
void Function(GenSSHClientStatus)? onStatus,
String? privateKey,
List<Spi>? jumpChain,
List<String?>? jumpPrivateKeys,
Duration timeout = const Duration(seconds: 5),
SSHUserInfoRequestHandler? onKeyboardInteractive,
Map<String, String>? knownHostFingerprints,
void Function(String storageKey, String fingerprintHex)? onHostKeyAccepted,
Future<bool> Function(HostKeyPromptInfo info)? onHostKeyPrompt,
required Set<String> visited,
SSHSocket? socketOverride,
bool followJumpConfig = true,
}) async {
final identifier = _hostIdentifier(spi);
if (!visited.add(identifier)) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump loop detected at ${spi.name} ($identifier)');
}
onStatus?.call(GenSSHClientStatus.socket); onStatus?.call(GenSSHClientStatus.socket);
final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints()); final hostKeyCache = Map<String, String>.from(knownHostFingerprints ?? _loadKnownHostFingerprints());
@@ -68,37 +177,126 @@ Future<SSHClient> genClient(
String? alterUser; String? alterUser;
final socket = await () async { final (socket, hopClients) = await () async {
// Proxy if (socketOverride != null) return (socketOverride, <SSHClient>[]);
final jumpSpi_ = () {
// Multi-thread or key login
if (jumpSpi != null) return jumpSpi;
// Main thread
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
}();
if (jumpSpi_ != null) {
final jumpClient = await genClient(
jumpSpi_,
privateKey: jumpPrivateKey,
timeout: timeout,
knownHostFingerprints: hostKeyCache,
onHostKeyAccepted: hostKeyPersist,
onHostKeyPrompt: onHostKeyPrompt,
);
return await jumpClient.forwardLocal(spi.ip, spi.port); if (followJumpConfig) {
final injectedSpiMap = <String, Spi>{};
final injectedKeyMap = <String, String?>{};
if (jumpChain != null) {
for (var i = 0; i < jumpChain.length; i++) {
final s = jumpChain[i];
injectedSpiMap[s.id] = s;
if (s.oldId.isNotEmpty) injectedSpiMap[s.oldId] = s;
if (jumpPrivateKeys != null && i < jumpPrivateKeys.length) {
injectedKeyMap[s.id] = jumpPrivateKeys[i];
if (s.oldId.isNotEmpty) injectedKeyMap[s.oldId] = jumpPrivateKeys[i];
}
}
}
Spi resolveSpi(String id) {
final injected = injectedSpiMap[id];
if (injected != null) return injected;
if (jumpChain != null) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found in provided chain: $id');
}
final fromStore = Stores.server.box.get(id);
if (fromStore == null) {
throw SSHErr(type: SSHErrType.connect, message: 'Jump server not found: $id');
}
return fromStore;
}
String? resolveHopPrivateKey(Spi hop) {
final keyId = hop.keyId;
if (keyId == null) return null;
final injected = injectedKeyMap[hop.id] ?? injectedKeyMap[hop.oldId];
return injected ?? getPrivateKey(keyId);
}
final hops = _resolveMergedJumpChainInternal(spi, resolveSpi: resolveSpi);
if (hops.isNotEmpty) {
// Build multi-hop forward chain with dedup/merge.
final createdClients = <SSHClient>[];
SSHClient? currentClient;
try {
final firstHop = hops.first;
final firstKey = resolveHopPrivateKey(firstHop);
if (firstHop.keyId != null && firstKey == null) {
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(firstHop.keyId ?? ''));
}
currentClient = await _genClientInternal(
firstHop,
privateKey: firstKey,
jumpChain: jumpChain,
jumpPrivateKeys: jumpPrivateKeys,
timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive,
knownHostFingerprints: hostKeyCache,
onHostKeyAccepted: hostKeyPersist,
onHostKeyPrompt: hostKeyPrompt,
visited: visited,
followJumpConfig: false,
);
createdClients.add(currentClient);
for (var i = 1; i < hops.length; i++) {
final hop = hops[i];
final forwarded = await currentClient!.forwardLocal(hop.ip, hop.port);
final hopKey = resolveHopPrivateKey(hop);
if (hop.keyId != null && hopKey == null) {
throw SSHErr(type: SSHErrType.noPrivateKey, message: l10n.privateKeyNotFoundFmt(hop.keyId ?? ''));
}
currentClient = await _genClientInternal(
hop,
privateKey: hopKey,
jumpChain: jumpChain,
jumpPrivateKeys: jumpPrivateKeys,
timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive,
knownHostFingerprints: hostKeyCache,
onHostKeyAccepted: hostKeyPersist,
onHostKeyPrompt: hostKeyPrompt,
visited: visited,
socketOverride: forwarded,
followJumpConfig: false,
);
createdClients.add(currentClient);
}
final forwardedSocket = await currentClient!.forwardLocal(spi.ip, spi.port);
return (forwardedSocket, createdClients);
} catch (e) {
// Close all created clients on error to avoid leaks
for (final client in createdClients) {
try {
client.close();
} catch (_) {
// Ignore close errors during cleanup
}
}
rethrow;
}
// Note: On success, all intermediate clients must remain open
// because the returned socket tunnels through them.
}
} }
// Direct // Direct
try { try {
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout); return (await SSHSocket.connect(spi.ip, spi.port, timeout: timeout), <SSHClient>[]);
} catch (e) { } catch (e) {
Loggers.app.warning('genClient', e); Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow; if (spi.alterUrl == null) rethrow;
try { try {
final res = spi.parseAlterUrl(); final res = spi.parseAlterUrl();
alterUser = res.$2; alterUser = res.$2;
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout); return (await SSHSocket.connect(res.$1, res.$3, timeout: timeout), <SSHClient>[]);
} catch (e) { } catch (e) {
Loggers.app.warning('genClient alterUrl', e); Loggers.app.warning('genClient alterUrl', e);
rethrow; rethrow;
@@ -113,32 +311,52 @@ Future<SSHClient> genClient(
prompt: hostKeyPrompt, prompt: hostKeyPrompt,
); );
final keyId = spi.keyId; Future<SSHClient> buildClient(SSHSocket socket) async {
if (keyId == null) { final keyId = spi.keyId;
onStatus?.call(GenSSHClientStatus.pwd); if (keyId == null) {
onStatus?.call(GenSSHClientStatus.pwd);
return SSHClient(
socket,
username: alterUser ?? spi.user,
onPasswordRequest: () => spi.pwd,
onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call,
// printDebug: debugPrint,
// printTrace: debugPrint,
);
}
privateKey ??= getPrivateKey(keyId);
onStatus?.call(GenSSHClientStatus.key);
return SSHClient( return SSHClient(
socket, socket,
username: alterUser ?? spi.user, username: spi.user,
onPasswordRequest: () => spi.pwd, // Must use [compute] here, instead of [Computer.shared.start]
identities: await compute(loadIndentity, privateKey!),
onUserInfoRequest: onKeyboardInteractive, onUserInfoRequest: onKeyboardInteractive,
onVerifyHostKey: hostKeyVerifier.call, onVerifyHostKey: hostKeyVerifier.call,
// printDebug: debugPrint, // printDebug: debugPrint,
// printTrace: debugPrint, // printTrace: debugPrint,
); );
} }
privateKey ??= getPrivateKey(keyId);
onStatus?.call(GenSSHClientStatus.key); final client = await buildClient(socket);
return SSHClient(
socket, // Tie hop clients' lifetime to the final client: close all hop clients
username: spi.user, // when the target client disconnects to avoid leaking SSH connections.
// Must use [compute] here, instead of [Computer.shared.start] if (hopClients.isNotEmpty) {
identities: await compute(loadIndentity, privateKey), client.done.whenComplete(() {
onUserInfoRequest: onKeyboardInteractive, for (final hopClient in hopClients) {
onVerifyHostKey: hostKeyVerifier.call, try {
// printDebug: debugPrint, hopClient.close();
// printTrace: debugPrint, } catch (_) {
); // Ignore close errors during cleanup
}
}
});
}
return client;
} }
typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex); typedef _HostKeyPersistCallback = void Function(String storageKey, String fingerprintHex);
@@ -300,20 +518,53 @@ Future<void> ensureKnownHostKey(
Duration timeout = const Duration(seconds: 5), Duration timeout = const Duration(seconds: 5),
SSHUserInfoRequestHandler? onKeyboardInteractive, SSHUserInfoRequestHandler? onKeyboardInteractive,
}) async { }) async {
final cache = _loadKnownHostFingerprints(); var cache = _loadKnownHostFingerprints();
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
return;
}
final jumpSpi = spi.jumpId != null ? Stores.server.box.get(spi.jumpId) : null; final hops = resolveMergedJumpChain(spi);
if (jumpSpi != null && !_hasKnownHostFingerprintForSpi(jumpSpi, cache)) {
await ensureKnownHostKey( // Check each hop's host key, routing through preceding hops
jumpSpi, for (var i = 0; i < hops.length; i++) {
final hop = hops[i];
// Preceding hops needed to reach this hop
final precedingHops = i > 0 ? hops.sublist(0, i) : null;
final precedingKeys = precedingHops?.map((h) =>
h.keyId != null ? getPrivateKey(h.keyId!) : null
).toList();
cache = await _ensureKnownHostKeyForSingle(
hop,
cache: cache,
timeout: timeout, timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive, onKeyboardInteractive: onKeyboardInteractive,
jumpChain: precedingHops,
jumpPrivateKeys: precedingKeys,
); );
cache.addAll(_loadKnownHostFingerprints()); }
if (_hasKnownHostFingerprintForSpi(spi, cache)) return;
// Check the target's host key, routing through all hops
final allKeys = hops.isNotEmpty
? hops.map((h) => h.keyId != null ? getPrivateKey(h.keyId!) : null).toList()
: null;
await _ensureKnownHostKeyForSingle(
spi,
cache: cache,
timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive,
jumpChain: hops.isNotEmpty ? hops : null,
jumpPrivateKeys: allKeys,
);
}
Future<Map<String, String>> _ensureKnownHostKeyForSingle(
Spi spi, {
required Map<String, String> cache,
Duration timeout = const Duration(seconds: 5),
SSHUserInfoRequestHandler? onKeyboardInteractive,
List<Spi>? jumpChain,
List<String?>? jumpPrivateKeys,
}) async {
if (_hasKnownHostFingerprintForSpi(spi, cache)) {
return cache;
} }
final client = await genClient( final client = await genClient(
@@ -321,6 +572,8 @@ Future<void> ensureKnownHostKey(
timeout: timeout, timeout: timeout,
onKeyboardInteractive: onKeyboardInteractive, onKeyboardInteractive: onKeyboardInteractive,
knownHostFingerprints: cache, knownHostFingerprints: cache,
jumpChain: jumpChain,
jumpPrivateKeys: jumpPrivateKeys,
); );
try { try {
@@ -328,6 +581,9 @@ Future<void> ensureKnownHostKey(
} finally { } finally {
client.close(); client.close();
} }
cache.addAll(_loadKnownHostFingerprints());
return cache;
} }
bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) { bool _hasKnownHostFingerprintForSpi(Spi spi, Map<String, String> cache) {

View File

@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:server_box/data/model/app/error.dart'; import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/custom.dart';
@@ -35,8 +36,15 @@ abstract class Spi with _$Spi {
String? alterUrl, String? alterUrl,
@Default(true) bool autoConnect, @Default(true) bool autoConnect,
/// [id] of the jump server /// [id] of the jump server (legacy, single hop)
///
/// Migrated to [jumpChainIds].
String? jumpId, String? jumpId,
/// Jump chain hop ids (nearest -> farthest)
///
/// Preferred over [jumpId].
@JsonKey(includeIfNull: false) List<String>? jumpChainIds,
ServerCustom? custom, ServerCustom? custom,
WakeOnLanCfg? wolCfg, WakeOnLanCfg? wolCfg,
@@ -79,7 +87,10 @@ extension Spix on Spi {
String? migrateId() { String? migrateId() {
if (id.isNotEmpty) return null; if (id.isNotEmpty) return null;
ServerStore.instance.delete(oldId); ServerStore.instance.delete(oldId);
final newSpi = copyWith(id: ShortId.generate()); final newSpi = copyWith(
id: ShortId.generate(),
jumpChainIds: jumpChainIds ?? (jumpId == null ? null : [jumpId!]),
);
newSpi.save(); newSpi.save();
return newSpi.id; return newSpi.id;
} }
@@ -94,7 +105,8 @@ extension Spix on Spi {
port == other.port && port == other.port &&
pwd == other.pwd && pwd == other.pwd &&
keyId == other.keyId && keyId == other.keyId &&
jumpId == other.jumpId; jumpId == other.jumpId &&
listEquals(jumpChainIds, other.jumpChainIds);
} }
/// Returns true if the connection should be re-established. /// Returns true if the connection should be re-established.
@@ -137,7 +149,7 @@ extension Spix on Spi {
tags: ['tag1', 'tag2'], tags: ['tag1', 'tag2'],
alterUrl: 'user@ip:port', alterUrl: 'user@ip:port',
autoConnect: true, autoConnect: true,
jumpId: 'jump_server_id', jumpChainIds: ['jump_server_id'],
custom: ServerCustom( custom: ServerCustom(
pveAddr: 'http://localhost:8006', pveAddr: 'http://localhost:8006',
pveIgnoreCert: false, pveIgnoreCert: false,

View File

@@ -16,8 +16,13 @@ T _$identity<T>(T value) => value;
mixin _$Spi { mixin _$Spi {
String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key String get name; String get ip; int get port; String get user; String? get pwd;/// [id] of private key
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server @JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server (legacy, single hop)
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal. ///
/// Migrated to [jumpChainIds].
String? get jumpId;/// Jump chain hop ids (nearest -> farthest)
///
/// Preferred over [jumpId].
@JsonKey(includeIfNull: false) List<String>? get jumpChainIds; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection. Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server @JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes; @JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
@@ -33,12 +38,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes)); return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other.jumpChainIds, jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes)); int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
@@ -49,7 +54,7 @@ abstract mixin class $SpiCopyWith<$Res> {
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl; factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
}); });
@@ -66,7 +71,7 @@ class _$SpiCopyWithImpl<$Res>
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -78,7 +83,8 @@ as String?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable as String?,jumpChainIds: freezed == jumpChainIds ? _self.jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
@@ -169,10 +175,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _Spi() when $default != null: case _Spi() when $default != null:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _: return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
return orElse(); return orElse();
} }
@@ -190,10 +196,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Spi(): case _Spi():
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _: return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -210,10 +216,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, @JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Spi() when $default != null: case _Spi() when $default != null:
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _: return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.jumpChainIds,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
return null; return null;
} }
@@ -225,7 +231,7 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
@JsonSerializable(includeIfNull: false) @JsonSerializable(includeIfNull: false)
class _Spi extends Spi { class _Spi extends Spi {
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._(); const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, @JsonKey(includeIfNull: false) final List<String>? jumpChainIds, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_jumpChainIds = jumpChainIds,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json); factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
@override final String name; @override final String name;
@@ -246,8 +252,25 @@ class _Spi extends Spi {
@override final String? alterUrl; @override final String? alterUrl;
@override@JsonKey() final bool autoConnect; @override@JsonKey() final bool autoConnect;
/// [id] of the jump server /// [id] of the jump server (legacy, single hop)
///
/// Migrated to [jumpChainIds].
@override final String? jumpId; @override final String? jumpId;
/// Jump chain hop ids (nearest -> farthest)
///
/// Preferred over [jumpId].
final List<String>? _jumpChainIds;
/// Jump chain hop ids (nearest -> farthest)
///
/// Preferred over [jumpId].
@override@JsonKey(includeIfNull: false) List<String>? get jumpChainIds {
final value = _jumpChainIds;
if (value == null) return null;
if (_jumpChainIds is EqualUnmodifiableListView) return _jumpChainIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final ServerCustom? custom; @override final ServerCustom? custom;
@override final WakeOnLanCfg? wolCfg; @override final WakeOnLanCfg? wolCfg;
/// It only applies to SSH terminal. /// It only applies to SSH terminal.
@@ -289,12 +312,12 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&const DeepCollectionEquality().equals(other._jumpChainIds, _jumpChainIds)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes)); int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,const DeepCollectionEquality().hash(_jumpChainIds),custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
@@ -305,7 +328,7 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl; factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId,@JsonKey(includeIfNull: false) List<String>? jumpChainIds, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
}); });
@@ -322,7 +345,7 @@ class __$SpiCopyWithImpl<$Res>
/// Create a copy of Spi /// Create a copy of Spi
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? jumpChainIds = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
return _then(_Spi( return _then(_Spi(
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
@@ -334,7 +357,8 @@ as String?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_t
as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable as List<String>?,alterUrl: freezed == alterUrl ? _self.alterUrl : alterUrl // ignore: cast_nullable_to_non_nullable
as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable as String?,autoConnect: null == autoConnect ? _self.autoConnect : autoConnect // ignore: cast_nullable_to_non_nullable
as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable as bool,jumpId: freezed == jumpId ? _self.jumpId : jumpId // ignore: cast_nullable_to_non_nullable
as String?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable as String?,jumpChainIds: freezed == jumpChainIds ? _self._jumpChainIds : jumpChainIds // ignore: cast_nullable_to_non_nullable
as List<String>?,custom: freezed == custom ? _self.custom : custom // ignore: cast_nullable_to_non_nullable
as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable as ServerCustom?,wolCfg: freezed == wolCfg ? _self.wolCfg : wolCfg // ignore: cast_nullable_to_non_nullable
as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable

View File

@@ -17,6 +17,9 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
alterUrl: json['alterUrl'] as String?, alterUrl: json['alterUrl'] as String?,
autoConnect: json['autoConnect'] as bool? ?? true, autoConnect: json['autoConnect'] as bool? ?? true,
jumpId: json['jumpId'] as String?, jumpId: json['jumpId'] as String?,
jumpChainIds: (json['jumpChainIds'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
custom: json['custom'] == null custom: json['custom'] == null
? null ? null
: ServerCustom.fromJson(json['custom'] as Map<String, dynamic>), : ServerCustom.fromJson(json['custom'] as Map<String, dynamic>),
@@ -47,6 +50,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
'alterUrl': ?instance.alterUrl, 'alterUrl': ?instance.alterUrl,
'autoConnect': instance.autoConnect, 'autoConnect': instance.autoConnect,
'jumpId': ?instance.jumpId, 'jumpId': ?instance.jumpId,
'jumpChainIds': ?instance.jumpChainIds,
'custom': ?instance.custom, 'custom': ?instance.custom,
'wolCfg': ?instance.wolCfg, 'wolCfg': ?instance.wolCfg,
'envs': ?instance.envs, 'envs': ?instance.envs,

View File

@@ -6,8 +6,8 @@ class SftpReq {
final String localPath; final String localPath;
final SftpReqType type; final SftpReqType type;
String? privateKey; String? privateKey;
Spi? jumpSpi; List<Spi>? jumpChain;
String? jumpPrivateKey; List<String?>? jumpPrivateKeys;
Map<String, String>? knownHostFingerprints; Map<String, String>? knownHostFingerprints;
SftpReq(this.spi, this.remotePath, this.localPath, this.type) { SftpReq(this.spi, this.remotePath, this.localPath, this.type) {
@@ -15,9 +15,17 @@ class SftpReq {
if (keyId != null) { if (keyId != null) {
privateKey = getPrivateKey(keyId); privateKey = getPrivateKey(keyId);
} }
if (spi.jumpId != null) { if (spi.jumpChainIds != null || spi.jumpId != null) {
jumpSpi = Stores.server.box.get(spi.jumpId); // Use resolveMergedJumpChain to recursively expand nested hop chains
jumpPrivateKey = Stores.key.fetchOne(jumpSpi?.keyId)?.key; final chain = resolveMergedJumpChain(spi);
final keys = <String?>[];
for (final hop in chain) {
keys.add(hop.keyId != null ? getPrivateKey(hop.keyId!) : null);
}
// Always set when a jump is configured so the isolate won't fallback to Stores.
jumpChain = chain;
jumpPrivateKeys = keys;
} }
try { try {
knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get()); knownHostFingerprints = Map<String, String>.from(Stores.setting.sshKnownHostFingerprints.get());
@@ -90,4 +98,4 @@ class SftpReqStatus {
} }
} }
enum SftpWorkerStatus { preparing, sshConnectted, loading, finished } enum SftpWorkerStatus { preparing, sshConnected, loading, finished }

View File

@@ -63,11 +63,11 @@ Future<void> _download(SftpReq req, SendPort mainSendPort, SendErrorFunction sen
final client = await genClient( final client = await genClient(
req.spi, req.spi,
privateKey: req.privateKey, privateKey: req.privateKey,
jumpSpi: req.jumpSpi, jumpChain: req.jumpChain,
jumpPrivateKey: req.jumpPrivateKey, jumpPrivateKeys: req.jumpPrivateKeys,
knownHostFingerprints: req.knownHostFingerprints, knownHostFingerprints: req.knownHostFingerprints,
); );
mainSendPort.send(SftpWorkerStatus.sshConnectted); mainSendPort.send(SftpWorkerStatus.sshConnected);
/// Create the directory if not exists /// Create the directory if not exists
final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf(Pfs.seperator)); final dirPath = req.localPath.substring(0, req.localPath.lastIndexOf(Pfs.seperator));
@@ -120,11 +120,11 @@ Future<void> _upload(SftpReq req, SendPort mainSendPort, SendErrorFunction sendE
final client = await genClient( final client = await genClient(
req.spi, req.spi,
privateKey: req.privateKey, privateKey: req.privateKey,
jumpSpi: req.jumpSpi, jumpChain: req.jumpChain,
jumpPrivateKey: req.jumpPrivateKey, jumpPrivateKeys: req.jumpPrivateKeys,
knownHostFingerprints: req.knownHostFingerprints, knownHostFingerprints: req.knownHostFingerprints,
); );
mainSendPort.send(SftpWorkerStatus.sshConnectted); mainSendPort.send(SftpWorkerStatus.sshConnected);
final local = File(req.localPath); final local = File(req.localPath);
if (!await local.exists()) { if (!await local.exists()) {

View File

@@ -58,7 +58,7 @@ final class ContainerNotifierProvider
} }
} }
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd'; String _$containerNotifierHash() => r'85457ec75264199c284572ee45beeaccba2044a1';
final class ContainerNotifierFamily extends $Family final class ContainerNotifierFamily extends $Family
with with

View File

@@ -58,7 +58,7 @@ final class PveNotifierProvider
} }
} }
String _$pveNotifierHash() => r'ba5f2d6cb47c33735f7cc09b771b4a86501b86c6'; String _$pveNotifierHash() => r'1e71faadee074b9c07bee731ef4ae6505e791967';
final class PveNotifierFamily extends $Family final class PveNotifierFamily extends $Family
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> { with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {

View File

@@ -103,37 +103,44 @@ class ServersNotifier extends _$ServersNotifier {
return; return;
} }
await Future.wait( final serversToRefresh = <MapEntry<String, Spi>>[];
state.servers.entries.map((entry) async { final idsToResetLimiter = <String>[];
final serverId = entry.key;
final spi = entry.value;
if (onlyFailed) { for (final entry in state.servers.entries) {
final serverState = ref.read(serverProvider(serverId)); final serverId = entry.key;
if (serverState.conn != ServerConn.failed) return; final spi = entry.value;
TryLimiter.reset(serverId);
}
if (state.manualDisconnectedIds.contains(serverId)) return; if (state.manualDisconnectedIds.contains(serverId)) continue;
final serverState = ref.read(serverProvider(serverId)); final serverState = ref.read(serverProvider(serverId));
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) {
return;
}
final serverNotifier = ref.read(serverProvider(serverId).notifier); if (onlyFailed) {
await serverNotifier.refresh(); if (serverState.conn != ServerConn.failed) continue;
}), idsToResetLimiter.add(serverId);
); }
if (serverState.conn == ServerConn.disconnected && !spi.autoConnect) continue;
serversToRefresh.add(entry);
}
for (final id in idsToResetLimiter) {
TryLimiter.reset(id);
}
for (final entry in serversToRefresh) {
final serverNotifier = ref.read(serverProvider(entry.key).notifier);
serverNotifier.refresh().ignore();
}
} }
Future<void> startAutoRefresh() async { Future<void> startAutoRefresh() async {
var duration = Stores.setting.serverStatusUpdateInterval.fetch(); var duration = Stores.setting.serverStatusUpdateInterval.fetch();
stopAutoRefresh(); stopAutoRefresh();
if (duration == 0) return; if (duration == 0) return;
if (duration < 0 || duration > 10 || duration == 1) { if (duration <= 1 || duration > 10) {
duration = 3;
Loggers.app.warning('Invalid duration: $duration, use default 3'); Loggers.app.warning('Invalid duration: $duration, use default 3');
duration = 3;
} }
final timer = Timer.periodic(Duration(seconds: duration), (_) async { final timer = Timer.periodic(Duration(seconds: duration), (_) async {
await refresh(); await refresh();
@@ -145,8 +152,8 @@ class ServersNotifier extends _$ServersNotifier {
final timer = state.autoRefreshTimer; final timer = state.autoRefreshTimer;
if (timer != null) { if (timer != null) {
timer.cancel(); timer.cancel();
state = state.copyWith(autoRefreshTimer: null);
} }
state = state.copyWith(autoRefreshTimer: null);
} }
bool get isAutoRefreshOn => state.autoRefreshTimer != null; bool get isAutoRefreshOn => state.autoRefreshTimer != null;

View File

@@ -41,7 +41,7 @@ final class ServersNotifierProvider
} }
} }
String _$serversNotifierHash() => r'3292bdce7d602ff64687b05ff81d120e71761ec2'; String _$serversNotifierHash() => r'277d1b219235f14bcc1b82a1e16260c2f28decdb';
abstract class _$ServersNotifier extends $Notifier<ServersState> { abstract class _$ServersNotifier extends $Notifier<ServersState> {
ServersState build(); ServersState build();

View File

@@ -35,7 +35,6 @@ abstract class ServerState with _$ServerState {
required ServerStatus status, required ServerStatus status,
@Default(ServerConn.disconnected) ServerConn conn, @Default(ServerConn.disconnected) ServerConn conn,
SSHClient? client, SSHClient? client,
Future<void>? updateFuture,
}) = _ServerState; }) = _ServerState;
} }
@@ -81,19 +80,16 @@ class ServerNotifier extends _$ServerNotifier {
} }
// Refresh server status // Refresh server status
bool _isRefreshing = false;
Future<void> refresh() async { Future<void> refresh() async {
if (state.updateFuture != null) { if (_isRefreshing) return;
await state.updateFuture;
return;
}
final updateFuture = _updateServer();
state = state.copyWith(updateFuture: updateFuture);
_isRefreshing = true;
try { try {
await updateFuture; await _updateServer();
} finally { } finally {
state = state.copyWith(updateFuture: null); _isRefreshing = false;
} }
} }
@@ -139,7 +135,7 @@ class ServerNotifier extends _$ServerNotifier {
final time2 = DateTime.now(); final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds; final spentTime = time2.difference(time1).inMilliseconds;
if (spi.jumpId == null) { if ((spi.jumpChainIds?.isNotEmpty != true) && spi.jumpId == null) {
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.'); Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
} else { } else {
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.'); Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$ServerState { mixin _$ServerState {
Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client; Future<void>? get updateFuture; Spi get spi; ServerStatus get status; ServerConn get conn; SSHClient? get client;
/// Create a copy of ServerState /// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ServerStateCopyWith<ServerState> get copyWith => _$ServerStateCopyWithImpl<Serv
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture)); return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
} }
@override @override
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture); int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
@override @override
String toString() { String toString() {
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)'; return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
} }
@@ -45,7 +45,7 @@ abstract mixin class $ServerStateCopyWith<$Res> {
factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl; factory $ServerStateCopyWith(ServerState value, $Res Function(ServerState) _then) = _$ServerStateCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
}); });
@@ -62,14 +62,13 @@ class _$ServerStateCopyWithImpl<$Res>
/// Create a copy of ServerState /// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable as SSHClient?,
as Future<void>?,
)); ));
} }
/// Create a copy of ServerState /// Create a copy of ServerState
@@ -163,10 +162,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _ServerState() when $default != null: case _ServerState() when $default != null:
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
return orElse(); return orElse();
} }
@@ -184,10 +183,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _ServerState(): case _ServerState():
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -204,10 +203,10 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Spi spi, ServerStatus status, ServerConn conn, SSHClient? client)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _ServerState() when $default != null: case _ServerState() when $default != null:
return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFuture);case _: return $default(_that.spi,_that.status,_that.conn,_that.client);case _:
return null; return null;
} }
@@ -219,14 +218,13 @@ return $default(_that.spi,_that.status,_that.conn,_that.client,_that.updateFutur
class _ServerState implements ServerState { class _ServerState implements ServerState {
const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client, this.updateFuture}); const _ServerState({required this.spi, required this.status, this.conn = ServerConn.disconnected, this.client});
@override final Spi spi; @override final Spi spi;
@override final ServerStatus status; @override final ServerStatus status;
@override@JsonKey() final ServerConn conn; @override@JsonKey() final ServerConn conn;
@override final SSHClient? client; @override final SSHClient? client;
@override final Future<void>? updateFuture;
/// Create a copy of ServerState /// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -238,16 +236,16 @@ _$ServerStateCopyWith<_ServerState> get copyWith => __$ServerStateCopyWithImpl<_
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client)&&(identical(other.updateFuture, updateFuture) || other.updateFuture == updateFuture)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerState&&(identical(other.spi, spi) || other.spi == spi)&&(identical(other.status, status) || other.status == status)&&(identical(other.conn, conn) || other.conn == conn)&&(identical(other.client, client) || other.client == client));
} }
@override @override
int get hashCode => Object.hash(runtimeType,spi,status,conn,client,updateFuture); int get hashCode => Object.hash(runtimeType,spi,status,conn,client);
@override @override
String toString() { String toString() {
return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client, updateFuture: $updateFuture)'; return 'ServerState(spi: $spi, status: $status, conn: $conn, client: $client)';
} }
@@ -258,7 +256,7 @@ abstract mixin class _$ServerStateCopyWith<$Res> implements $ServerStateCopyWith
factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl; factory _$ServerStateCopyWith(_ServerState value, $Res Function(_ServerState) _then) = __$ServerStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
Spi spi, ServerStatus status, ServerConn conn, SSHClient? client, Future<void>? updateFuture Spi spi, ServerStatus status, ServerConn conn, SSHClient? client
}); });
@@ -275,14 +273,13 @@ class __$ServerStateCopyWithImpl<$Res>
/// Create a copy of ServerState /// Create a copy of ServerState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,Object? updateFuture = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? spi = null,Object? status = null,Object? conn = null,Object? client = freezed,}) {
return _then(_ServerState( return _then(_ServerState(
spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable spi: null == spi ? _self.spi : spi // ignore: cast_nullable_to_non_nullable
as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as Spi,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable as ServerStatus,conn: null == conn ? _self.conn : conn // ignore: cast_nullable_to_non_nullable
as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable as ServerConn,client: freezed == client ? _self.client : client // ignore: cast_nullable_to_non_nullable
as SSHClient?,updateFuture: freezed == updateFuture ? _self.updateFuture : updateFuture // ignore: cast_nullable_to_non_nullable as SSHClient?,
as Future<void>?,
)); ));
} }

View File

@@ -58,7 +58,7 @@ final class ServerNotifierProvider
} }
} }
String _$serverNotifierHash() => r'185c6b4546c3bc526f5b2ca79d16aed665818863'; String _$serverNotifierHash() => r'52e806bcc32a7818d1ec2b07a3c683b06885c9f8';
final class ServerNotifierFamily extends $Family final class ServerNotifierFamily extends $Family
with with

View File

@@ -89,15 +89,12 @@ class ServerStore extends HiveStore {
// Replace ids in jump server settings. // Replace ids in jump server settings.
final spi = get<Spi>(newId); final spi = get<Spi>(newId);
if (spi != null) { if (spi != null) {
final jumpId = spi.jumpId; // This could be an oldId. final jumpChainIds = spi.jumpChainIds ?? (spi.jumpId == null ? null : [spi.jumpId!]);
// Check if this jumpId corresponds to a server that was also migrated. if (jumpChainIds == null || jumpChainIds.isEmpty) continue;
if (jumpId != null && idMap.containsKey(jumpId)) {
final newJumpId = idMap[jumpId]; final newChain = jumpChainIds.map((e) => idMap[e] ?? e).toList();
if (spi.jumpId != newJumpId) { final newSpi = spi.copyWith(jumpId: null, jumpChainIds: newChain);
final newSpi = spi.copyWith(jumpId: newJumpId); update(spi, newSpi);
update(spi, newSpi);
}
}
} }
// Replace ids in [Snippet] // Replace ids in [Snippet]

View File

@@ -107,6 +107,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
alterUrl: fields[7] as String?, alterUrl: fields[7] as String?,
autoConnect: fields[8] == null ? true : fields[8] as bool, autoConnect: fields[8] == null ? true : fields[8] as bool,
jumpId: fields[9] as String?, jumpId: fields[9] as String?,
jumpChainIds: (fields[16] as List?)?.cast<String>(),
custom: fields[10] as ServerCustom?, custom: fields[10] as ServerCustom?,
wolCfg: fields[11] as WakeOnLanCfg?, wolCfg: fields[11] as WakeOnLanCfg?,
envs: (fields[12] as Map?)?.cast<String, String>(), envs: (fields[12] as Map?)?.cast<String, String>(),
@@ -119,7 +120,7 @@ class SpiAdapter extends TypeAdapter<Spi> {
@override @override
void write(BinaryWriter writer, Spi obj) { void write(BinaryWriter writer, Spi obj) {
writer writer
..writeByte(16) ..writeByte(17)
..writeByte(0) ..writeByte(0)
..write(obj.name) ..write(obj.name)
..writeByte(1) ..writeByte(1)
@@ -151,7 +152,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
..writeByte(14) ..writeByte(14)
..write(obj.customSystemType) ..write(obj.customSystemType)
..writeByte(15) ..writeByte(15)
..write(obj.disabledCmdTypes); ..write(obj.disabledCmdTypes)
..writeByte(16)
..write(obj.jumpChainIds);
} }
@override @override

View File

@@ -27,7 +27,7 @@ types:
index: 4 index: 4
Spi: Spi:
typeId: 3 typeId: 3
nextIndex: 16 nextIndex: 17
fields: fields:
name: name:
index: 0 index: 0
@@ -61,6 +61,8 @@ types:
index: 14 index: 14
disabledCmdTypes: disabledCmdTypes:
index: 15 index: 15
jumpChainIds:
index: 16
VirtKey: VirtKey:
typeId: 4 typeId: 4
nextIndex: 45 nextIndex: 45

View File

@@ -63,6 +63,9 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
final _netSortType = ValueNotifier(_NetSortType.device); final _netSortType = ValueNotifier(_NetSortType.device);
late final _collapse = _settings.collapseUIDefault.fetch(); late final _collapse = _settings.collapseUIDefault.fetch();
late final _textFactor = TextScaler.linear(_settings.textFactor.fetch()); late final _textFactor = TextScaler.linear(_settings.textFactor.fetch());
late final _cpuViewAsProgress = _settings.cpuViewAsProgress.fetch();
late final _moveServerFuncs = _settings.moveServerFuncs.fetch();
late final _displayCpuIndex = _settings.displayCpuIndex.fetch();
@override @override
void dispose() { void dispose() {
@@ -97,7 +100,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
} }
Widget _buildMainPage(ServerState si) { Widget _buildMainPage(ServerState si) {
final buildFuncs = !Stores.setting.moveServerFuncs.fetch(); final buildFuncs = !_moveServerFuncs;
final logo = _buildLogo(si); final logo = _buildLogo(si);
final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)]; final children = <Widget>[if (logo != null) logo, if (buildFuncs) ServerFuncBtns(spi: si.spi)];
for (final card in _cardsOrder) { for (final card in _cardsOrder) {
@@ -197,7 +200,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
]); ]);
} }
final List<Widget> children = Stores.setting.cpuViewAsProgress.fetch() final List<Widget> children = _cpuViewAsProgress
? _buildCPUProgress(ss.cpu) ? _buildCPUProgress(ss.cpu)
: [_buildCPUChart(ss)]; : [_buildCPUChart(ss)];
@@ -258,7 +261,7 @@ class _ServerDetailPageState extends ConsumerState<ServerDetailPage> with Single
const kRowThreshold = 4; const kRowThreshold = 4;
const kCoresCountThreshold = kMaxColumn * kRowThreshold; const kCoresCountThreshold = kMaxColumn * kRowThreshold;
final children = <Widget>[]; final children = <Widget>[];
final displayCpuIndexSetting = Stores.setting.displayCpuIndex.fetch(); final displayCpuIndexSetting = _displayCpuIndex;
if (cs.coresCount > kCoresCountThreshold) { if (cs.coresCount > kCoresCountThreshold) {
final numCoresToDisplay = cs.coresCount - 1; final numCoresToDisplay = cs.coresCount - 1;

View File

@@ -222,6 +222,30 @@ extension _Actions on _ServerEditPageState {
return; return;
} }
final oldSpi = this.spi;
if (oldSpi != null) {
final originalJumpChain = oldSpi.jumpChainIds ?? (oldSpi.jumpId == null ? const <String>[] : [oldSpi.jumpId!]);
final currentJumpChain = _jumpChain.value;
final jumpChainChanged = () {
if (originalJumpChain.isEmpty && currentJumpChain.isEmpty) return false;
if (originalJumpChain.length != currentJumpChain.length) return true;
for (var i = 0; i < originalJumpChain.length; i++) {
if (originalJumpChain[i] != currentJumpChain[i]) return true;
}
return false;
}();
if (jumpChainChanged) {
final ok = await context.showRoundDialog<bool>(
title: libL10n.attention,
child: Text(libL10n.askContinue('${l10n.jumpServer} ${libL10n.setting}')),
actions: Btnx.cancelOk,
);
if (ok != true) return;
}
}
if (_keyIdx.value == null && _passwordController.text.isEmpty) { if (_keyIdx.value == null && _passwordController.text.isEmpty) {
final ok = await context.showRoundDialog<bool>( final ok = await context.showRoundDialog<bool>(
title: libL10n.attention, title: libL10n.attention,
@@ -277,7 +301,8 @@ extension _Actions on _ServerEditPageState {
tags: _tags.value.isEmpty ? null : _tags.value.toList(), tags: _tags.value.isEmpty ? null : _tags.value.toList(),
alterUrl: _altUrlController.text.selfNotEmptyOrNull, alterUrl: _altUrlController.text.selfNotEmptyOrNull,
autoConnect: _autoConnect.value, autoConnect: _autoConnect.value,
jumpId: _jumpServer.value, jumpId: null,
jumpChainIds: _jumpChain.value.isEmpty ? null : _jumpChain.value,
custom: custom, custom: custom,
wolCfg: wol, wolCfg: wol,
envs: _env.value.isEmpty ? null : _env.value, envs: _env.value.isEmpty ? null : _env.value,
@@ -421,7 +446,7 @@ extension _Utils on _ServerEditPageState {
_altUrlController.text = spi.alterUrl ?? ''; _altUrlController.text = spi.alterUrl ?? '';
_autoConnect.value = spi.autoConnect; _autoConnect.value = spi.autoConnect;
_jumpServer.value = spi.jumpId; _jumpChain.value = spi.jumpChainIds ?? (spi.jumpId == null ? const <String>[] : [spi.jumpId!]);
final custom = spi.custom; final custom = spi.custom;
if (custom != null) { if (custom != null) {

View File

@@ -25,6 +25,7 @@ import 'package:server_box/view/page/private_key/edit.dart';
import 'package:server_box/view/page/server/discovery/discovery.dart'; import 'package:server_box/view/page/server/discovery/discovery.dart';
part 'actions.dart'; part 'actions.dart';
part 'jump_chain.dart';
part 'widget.dart'; part 'widget.dart';
class ServerEditPage extends ConsumerStatefulWidget { class ServerEditPage extends ConsumerStatefulWidget {
@@ -66,7 +67,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
/// -1: non selected, null: password, others: index of private key /// -1: non selected, null: password, others: index of private key
final _keyIdx = ValueNotifier<int?>(null); final _keyIdx = ValueNotifier<int?>(null);
final _autoConnect = ValueNotifier(true); final _autoConnect = ValueNotifier(true);
final _jumpServer = nvn<String?>(); final _jumpChain = <String>[].vn;
final _pveIgnoreCert = ValueNotifier(false); final _pveIgnoreCert = ValueNotifier(false);
final _env = <String, String>{}.vn; final _env = <String, String>{}.vn;
final _customCmds = <String, String>{}.vn; final _customCmds = <String, String>{}.vn;
@@ -100,7 +101,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
_keyIdx.dispose(); _keyIdx.dispose();
_autoConnect.dispose(); _autoConnect.dispose();
_jumpServer.dispose(); _jumpChain.dispose();
_pveIgnoreCert.dispose(); _pveIgnoreCert.dispose();
_env.dispose(); _env.dispose();
_customCmds.dispose(); _customCmds.dispose();
@@ -199,7 +200,6 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
), ),
_buildAuth(), _buildAuth(),
_buildSystemType(), _buildSystemType(),
_buildJumpServer(),
_buildMore(), _buildMore(),
]; ];
return AutoMultiList(children: children); return AutoMultiList(children: children);

View File

@@ -0,0 +1,176 @@
part of 'edit.dart';
extension _JumpChain on _ServerEditPageState {
Widget _buildJumpChain() {
final serversState = ref.watch(serversProvider);
final servers = serversState.servers;
final selfId = spi?.id;
if (selfId == null) {
return ListTile(
leading: const Icon(Icons.map),
title: Text(l10n.jumpServer),
subtitle: Text(libL10n.empty, style: UIs.textGrey),
).cardx;
}
String serverNameOrId(String id) {
return servers[id]?.name ?? id;
}
List<String> flattenHopIds(String id, {required Set<String> visited}) {
if (!visited.add(id)) return const <String>[];
final spi = servers[id];
if (spi == null) return const <String>[];
final hops = spi.jumpChainIds;
if (hops == null || hops.isEmpty) return const <String>[];
final flat = <String>[];
for (final hopId in hops) {
flat.add(hopId);
flat.addAll(flattenHopIds(hopId, visited: visited));
}
return flat;
}
bool containsCycleWithCandidate(String candidateId) {
final queue = [..._jumpChain.value, candidateId];
final directVisited = <String>{selfId};
for (final hopId in queue) {
if (hopId == selfId) return true;
if (!directVisited.add(hopId)) return true;
}
for (final hopId in queue) {
final extra = flattenHopIds(hopId, visited: <String>{selfId});
for (final id in extra) {
if (id == selfId) return true;
}
}
return false;
}
String? buildTextNearToFar() {
if (_jumpChain.value.isEmpty) return null;
final flat = <String>[];
final visited = <String>{selfId};
for (final hopId in _jumpChain.value) {
flat.add(hopId);
flat.addAll(flattenHopIds(hopId, visited: visited));
}
final names = flat.map(serverNameOrId).toList();
if (names.isEmpty) return null;
return names.join('');
}
String? buildTextFarToNear() {
final text = buildTextNearToFar();
if (text == null) return null;
return text.split('').reversed.join('');
}
return _jumpChain.listenVal((_) {
final nearToFar2 = buildTextNearToFar();
final farToNear2 = buildTextFarToNear();
return ListTile(
leading: const Icon(Icons.map),
title: Text(l10n.jumpServer),
subtitle: (nearToFar2 == null)
? Text(libL10n.empty, style: UIs.textGrey)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('${l10n.route}: $nearToFar2', style: UIs.textGrey),
Text('${libL10n.path}: $farToNear2', style: UIs.textGrey),
],
),
trailing: const Icon(Icons.keyboard_arrow_right),
onTap: () async {
if (serversState.serverOrder.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
}
final candidates = serversState.serverOrder.where((e) => e != selfId).toList();
if (candidates.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
}
// Add a hop
final nextHop = await context.showPickSingleDialog<String>(
title: '${l10n.jumpServer} (+1)',
items: candidates.where((id) => !containsCycleWithCandidate(id)).toList(),
display: serverNameOrId,
clearable: true,
);
if (nextHop == null) return;
_jumpChain.value = [..._jumpChain.value, nextHop];
// If user wants to manage order/remove, offer a simple editor dialog
await context.showRoundDialog<void>(
title: l10n.jumpServer,
child: SizedBox(
width: 320,
child: _jumpChain.listenVal((hops) {
return ListView.builder(
shrinkWrap: true,
itemCount: hops.length,
itemBuilder: (context, index) {
final id = hops[index];
return ListTile(
title: Text(serverNameOrId(id)),
subtitle: Text(id, style: UIs.textGrey),
trailing: Wrap(
spacing: 4,
children: [
IconButton(
icon: const Icon(Icons.arrow_upward, size: 18),
onPressed: index == 0
? null
: () {
final list = [..._jumpChain.value];
final tmp = list[index - 1];
list[index - 1] = list[index];
list[index] = tmp;
_jumpChain.value = list;
},
),
IconButton(
icon: const Icon(Icons.arrow_downward, size: 18),
onPressed: index == hops.length - 1
? null
: () {
final list = [..._jumpChain.value];
final tmp = list[index + 1];
list[index + 1] = list[index];
list[index] = tmp;
_jumpChain.value = list;
},
),
IconButton(
icon: const Icon(Icons.delete, size: 18),
onPressed: () {
final list = [..._jumpChain.value]..removeAt(index);
_jumpChain.value = list;
},
),
],
),
);
},
);
}),
),
actions: Btnx.oks,
);
},
).cardx;
});
}
}

View File

@@ -132,6 +132,7 @@ extension _Widgets on _ServerEditPageState {
return ExpandTile( return ExpandTile(
title: Text(l10n.more), title: Text(l10n.more),
children: [ children: [
_buildJumpChain(),
Input( Input(
controller: _logoUrlCtrl, controller: _logoUrlCtrl,
type: TextInputType.url, type: TextInputType.url,
@@ -347,48 +348,6 @@ extension _Widgets on _ServerEditPageState {
); );
} }
Widget _buildJumpServer() {
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
final srvs = ref
.watch(serversProvider)
.servers
.values
.where((e) => e.jumpId == null)
.where((e) => e.id != spi?.id)
.toList();
final choice = _jumpServer.listenVal((val) {
final srv = srvs.firstWhereOrNull((e) => e.id == _jumpServer.value);
return Choice<Spi>(
multiple: false,
clearable: true,
value: srv != null ? [srv] : [],
builder: (state, _) => Wrap(
children: List<Widget>.generate(srvs.length, (index) {
final item = srvs[index];
return ChoiceChipX<Spi>(
label: item.name,
state: state,
value: item,
onSelected: (srv, on) {
if (on) {
_jumpServer.value = srv.id;
} else {
_jumpServer.value = null;
}
},
);
}),
),
);
});
return ExpandTile(
leading: const Icon(Icons.map),
initiallyExpanded: _jumpServer.value != null,
childrenPadding: padding,
title: Text(l10n.jumpServer),
children: [choice],
).cardx;
}
Widget _buildWriteScriptTip() { Widget _buildWriteScriptTip() {
return Btn.tile( return Btn.tile(

View File

@@ -84,6 +84,7 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
String? _streamingContent; String? _streamingContent;
String? _error; String? _error;
bool _isStreaming = false; bool _isStreaming = false;
bool _isMinimized = false;
@override @override
void initState() { void initState() {
@@ -387,12 +388,23 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final bottomPadding = MediaQuery.viewInsetsOf(context).bottom; final bottomPadding = MediaQuery.viewInsetsOf(context).bottom;
final heightFactor = _isMinimized ? 0.18 : 0.85;
return FractionallySizedBox( return TweenAnimationBuilder<double>(
heightFactor: 0.85, tween: Tween<double>(end: heightFactor),
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
builder: (context, animatedHeightFactor, child) {
return ClipRect(
child: FractionallySizedBox(
heightFactor: animatedHeightFactor,
child: child,
),
);
},
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row( child: Row(
@@ -402,83 +414,96 @@ class _AskAiSheetState extends ConsumerState<_AskAiSheet> {
if (_isStreaming) if (_isStreaming)
const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)), const SizedBox(height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2)),
const Spacer(), const Spacer(),
IconButton(
icon: Icon(_isMinimized ? Icons.unfold_more : Icons.unfold_less),
tooltip: libL10n.fold,
onPressed: () {
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_isMinimized = !_isMinimized;
});
},
),
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()), IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()),
], ],
), ),
), ),
Expanded( if (!_isMinimized) ...[
child: Scrollbar( Expanded(
controller: _scrollController, child: Scrollbar(
child: ListView(
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), child: ListView(
children: [ controller: _scrollController,
Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium), padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
const SizedBox(height: 6), children: [
CardX( Text(context.l10n.askAiSelectedContent, style: theme.textTheme.titleMedium),
child: Padding( const SizedBox(height: 6),
padding: const EdgeInsets.all(12),
child: SelectableText(
widget.selection,
style: const TextStyle(fontFamily: 'monospace'),
),
),
),
const SizedBox(height: 16),
Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),
const SizedBox(height: 6),
..._buildConversationWidgets(context, theme),
if (_error != null) ...[
const SizedBox(height: 16),
CardX( CardX(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)), child: SelectableText(
widget.selection,
style: const TextStyle(fontFamily: 'monospace'),
),
), ),
), ),
const SizedBox(height: 16),
Text(context.l10n.askAiConversation, style: theme.textTheme.titleMedium),
const SizedBox(height: 6),
..._buildConversationWidgets(context, theme),
if (_error != null) ...[
const SizedBox(height: 16),
CardX(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(_error!, style: TextStyle(color: theme.colorScheme.error)),
),
),
],
if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()],
const SizedBox(height: 16),
], ],
if (_isStreaming) ...[const SizedBox(height: 16), const LinearProgressIndicator()], ),
const SizedBox(height: 16),
],
), ),
), ),
), Padding(
Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: Text(
child: Text( context.l10n.askAiDisclaimer,
context.l10n.askAiDisclaimer, style: theme.textTheme.bodySmall?.copyWith(
style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.error,
color: theme.colorScheme.error, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
), textAlign: TextAlign.center,
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),
child: Row(
children: [
Expanded(
child: Input(
controller: _inputController,
minLines: 1,
maxLines: 4,
hint: context.l10n.askAiFollowUpHint,
action: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 12),
Btn.icon(
onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,
icon: const Icon(Icons.send, size: 18),
), ),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 16 + bottomPadding),
child: Row(
children: [
Expanded(
child: Input(
controller: _inputController,
minLines: 1,
maxLines: 4,
hint: context.l10n.askAiFollowUpHint,
action: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 12),
Btn.icon(
onTap: _isStreaming || _inputController.text.trim().isEmpty ? null : _sendMessage,
icon: const Icon(Icons.send, size: 18),
),
],
).cardx,
),
] else
const SizedBox(height: 8),
], ],
).cardx, ),
), ),
],
),
),
); );
} }
} }

View File

@@ -53,7 +53,7 @@ class _SftpMissionPageState extends ConsumerState<SftpMissionPage> {
return switch (status.status) { return switch (status.status) {
const (SftpWorkerStatus.finished) => _buildFinished(status), const (SftpWorkerStatus.finished) => _buildFinished(status),
const (SftpWorkerStatus.loading) => _buildLoading(status), const (SftpWorkerStatus.loading) => _buildLoading(status),
const (SftpWorkerStatus.sshConnectted) => _buildConnected(status), const (SftpWorkerStatus.sshConnected) => _buildConnected(status),
const (SftpWorkerStatus.preparing) => _buildPreparing(status), const (SftpWorkerStatus.preparing) => _buildPreparing(status),
_ => _buildDefault(status), _ => _buildDefault(status),
}; };

View File

@@ -205,10 +205,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: camera_web name: camera_web
sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.5" version: "0.3.5+3"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -440,10 +440,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.5"
file: file:
dependency: transitive dependency: transitive
description: description:
@@ -727,18 +727,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: hive_ce name: hive_ce
sha256: "81d39a03c4c0ba5938260a8c3547d2e71af59defecea21793d57fc3551f0d230" sha256: "29f8791bf13fa6cf7435a58f1f82a7c9706973c867affa77c34d91e105762664"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.15.1" version: "2.17.0"
hive_ce_flutter: hive_ce_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: hive_ce_flutter name: hive_ce_flutter
sha256: "26d656c9e8974f0732f1d09020e2d7b08ba841b8961a02dbfb6caf01474b0e9a" sha256: "2677e95a333ff15af43ccd06af7eb7abbf1a4f154ea071997f3de4346cae913a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.3.4"
hive_ce_generator: hive_ce_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -831,10 +831,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: isolate_channel name: isolate_channel
sha256: f3d36f783b301e6b312c3450eeb2656b0e7d1db81331af2a151d9083a3f6b18d sha256: "68191008e3a219bc87cc8cddbcd1e29810bd9f3a0fdc2108b574ccbd9aafda08"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.2+1" version: "0.3.0"
isolate_contactor: isolate_contactor:
dependency: transitive dependency: transitive
description: description:
@@ -1750,10 +1750,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.4" version: "1.2.1"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@@ -0,0 +1,93 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
void main() {
group('Jump server', () {
test('resolveMergedJumpChain throws when injected chain misses jump server', () {
const spi = Spi(
name: 'target',
ip: '10.0.0.10',
port: 22,
user: 'root',
id: 't',
jumpId: 'missing',
);
expect(
() => resolveMergedJumpChain(spi, jumpChain: const <Spi>[]),
throwsA(
isA<SSHErr>().having(
(e) => e.type,
'type',
SSHErrType.connect,
),
),
);
});
test('resolveMergedJumpChain merges and dedups', () {
const c = Spi(name: 'c', ip: '10.0.0.30', port: 22, user: 'root', id: 'c');
const d = Spi(name: 'd', ip: '10.0.0.40', port: 22, user: 'root', id: 'd');
const b = Spi(
name: 'b',
ip: '10.0.0.20',
port: 22,
user: 'root',
id: 'b',
jumpChainIds: ['c', 'd'],
);
const target = Spi(
name: 'target',
ip: '10.0.0.10',
port: 22,
user: 'root',
id: 't',
jumpChainIds: ['b', 'c'],
);
final chain = resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c, d]);
expect(chain.map((e) => e.id).toList(), ['c', 'd', 'b']);
});
test('resolveMergedJumpChain detects jump loop', () {
const b = Spi(
name: 'b',
ip: '10.0.0.20',
port: 22,
user: 'root',
id: 'b',
jumpChainIds: ['c'],
);
const c = Spi(
name: 'c',
ip: '10.0.0.30',
port: 22,
user: 'root',
id: 'c',
jumpChainIds: ['b'],
);
const target = Spi(
name: 'target',
ip: '10.0.0.10',
port: 22,
user: 'root',
id: 't',
jumpChainIds: ['b'],
);
expect(
() => resolveMergedJumpChain(target, jumpChain: const <Spi>[b, c]),
throwsA(
isA<SSHErr>().having(
(e) => e.type,
'type',
SSHErrType.connect,
),
),
);
});
});
}