Files
flutter_server_box/lib/data/provider/server/single.dart
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

386 lines
13 KiB
Dart

import 'dart:async';
import 'package:computer/computer.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:server_box/core/extension/ssh_client.dart';
import 'package:server_box/core/utils/server.dart';
import 'package:server_box/core/utils/ssh_auth.dart';
import 'package:server_box/data/helper/ssh_decoder.dart';
import 'package:server_box/data/helper/system_detector.dart';
import 'package:server_box/data/model/app/error.dart';
import 'package:server_box/data/model/app/scripts/script_consts.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/connection_stat.dart';
import 'package:server_box/data/model/server/server.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/model/server/server_status_update_req.dart';
import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/try_limiter.dart';
import 'package:server_box/data/provider/server/all.dart';
import 'package:server_box/data/res/status.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/ssh/session_manager.dart';
part 'single.g.dart';
part 'single.freezed.dart';
// Individual server state, including connection and status information
@freezed
abstract class ServerState with _$ServerState {
const factory ServerState({
required Spi spi,
required ServerStatus status,
@Default(ServerConn.disconnected) ServerConn conn,
SSHClient? client,
}) = _ServerState;
}
// Individual server state management
@Riverpod(keepAlive: true)
class ServerNotifier extends _$ServerNotifier {
@override
ServerState build(String serverId) {
final serverNotifier = ref.read(serversProvider);
final spi = serverNotifier.servers[serverId];
if (spi == null) {
throw StateError('Server $serverId not found');
}
return ServerState(spi: spi, status: InitStatus.status);
}
// Update connection status
void updateConnection(ServerConn conn) {
state = state.copyWith(conn: conn);
}
// Update server status
void updateStatus(ServerStatus status) {
state = state.copyWith(status: status);
}
// Update SSH client
void updateClient(SSHClient? client) {
state = state.copyWith(client: client);
}
// Update SPI configuration
void updateSpi(Spi spi) {
state = state.copyWith(spi: spi);
}
// Close connection
void closeConnection() {
final client = state.client;
client?.close();
state = state.copyWith(client: null, conn: ServerConn.disconnected);
}
// Refresh server status
bool _isRefreshing = false;
Future<void> refresh() async {
if (_isRefreshing) return;
_isRefreshing = true;
try {
await _updateServer();
} finally {
_isRefreshing = false;
}
}
Future<void> _updateServer() async {
await _getData();
}
Future<void> _getData() async {
final spi = state.spi;
final sid = spi.id;
if (!TryLimiter.canTry(sid)) {
if (state.conn != ServerConn.failed) {
updateConnection(ServerConn.failed);
}
return;
}
final newStatus = state.status..err = null; // Clear previous error
updateStatus(newStatus);
if (state.conn < ServerConn.connecting || (state.client?.isClosed ?? true)) {
updateConnection(ServerConn.connecting);
// Wake on LAN
final wol = spi.wolCfg;
if (wol != null) {
try {
await wol.wake();
} catch (e) {
Loggers.app.warning('Wake on lan failed', e);
}
}
try {
final time1 = DateTime.now();
final client = await genClient(
spi,
timeout: Duration(seconds: Stores.setting.timeout.fetch()),
onKeyboardInteractive: (_) => KeybordInteractive.defaultHandle(spi),
);
updateClient(client);
final time2 = DateTime.now();
final spentTime = time2.difference(time1).inMilliseconds;
if (spi.jumpId == null) {
Loggers.app.info('Connected to ${spi.name} in $spentTime ms.');
} else {
Loggers.app.info('Jump to ${spi.name} in $spentTime ms.');
}
// Record successful connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: time1,
result: ConnectionResult.success,
durationMs: spentTime,
));
final sessionId = 'ssh_${spi.id}';
TermSessionManager.add(
id: sessionId,
spi: spi,
startTimeMs: time1.millisecondsSinceEpoch,
disconnect: () => ref.read(serversProvider.notifier).closeOneServer(spi.id),
status: TermSessionStatus.connecting,
);
TermSessionManager.setActive(sessionId, hasTerminal: false);
} catch (e) {
TryLimiter.inc(sid);
// Determine connection failure type
ConnectionResult failureResult;
if (e.toString().contains('timeout') || e.toString().contains('Timeout')) {
failureResult = ConnectionResult.timeout;
} else if (e.toString().contains('auth') || e.toString().contains('Authentication')) {
failureResult = ConnectionResult.authFailed;
} else if (e.toString().contains('network') || e.toString().contains('Network')) {
failureResult = ConnectionResult.networkError;
} else {
failureResult = ConnectionResult.unknownError;
}
// Record failed connection
Stores.connectionStats.recordConnection(ConnectionStat(
serverId: spi.id,
serverName: spi.name,
timestamp: DateTime.now(),
result: failureResult,
errorMessage: e.toString(),
durationMs: 0,
));
final newStatus = state.status..err = SSHErr(type: SSHErrType.connect, message: e.toString());
updateStatus(newStatus);
updateConnection(ServerConn.failed);
// Remove SSH session when connection fails
final sessionId = 'ssh_${spi.id}';
TermSessionManager.remove(sessionId);
Loggers.app.warning('Connect to ${spi.name} failed', e);
return;
}
updateConnection(ServerConn.connected);
// Update SSH session status to connected
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.connected);
try {
// Detect system type
final detectedSystemType = await SystemDetector.detect(state.client!, spi);
final newStatus = state.status..system = detectedSystemType;
updateStatus(newStatus);
Loggers.app.info('Writing script for ${spi.name} (${detectedSystemType.name})');
final (stdoutResult, writeScriptResult) = await state.client!.execSafe(
(session) async {
final scriptRaw = ShellFuncManager.allScript(
spi.custom?.cmds,
systemType: detectedSystemType,
disabledCmdTypes: spi.disabledCmdTypes,
).uint8List;
session.stdin.add(scriptRaw);
session.stdin.close();
},
entry: ShellFuncManager.getInstallShellCmd(
spi.id,
systemType: detectedSystemType,
customDir: spi.custom?.scriptDir,
),
systemType: detectedSystemType,
context: 'WriteScript<${spi.name}>',
);
if (stdoutResult.isNotEmpty) {
Loggers.app.info('Script write stdout for ${spi.name}: $stdoutResult');
}
if (writeScriptResult.isNotEmpty) {
Loggers.app.warning('Script write stderr for ${spi.name}: $writeScriptResult');
if (detectedSystemType != SystemType.windows) {
ShellFuncManager.switchScriptDir(spi.id, systemType: detectedSystemType);
throw writeScriptResult;
}
} else {
Loggers.app.info('Script written successfully for ${spi.name}');
}
} on SSHAuthAbortError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} on SSHAuthFailError catch (e) {
TryLimiter.inc(sid);
final err = SSHErr(type: SSHErrType.auth, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
} catch (e) {
final err = SSHErr(type: SSHErrType.writeScript, message: e.toString());
final newStatus = state.status..err = err;
updateStatus(newStatus);
Loggers.app.warning(err);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
}
}
if (state.conn == ServerConn.connecting) return;
// Keep finished status to prevent UI from refreshing to loading state
if (state.conn != ServerConn.finished) {
updateConnection(ServerConn.loading);
}
List<String>? segments;
String? raw;
try {
final statusCmd = ShellFunc.status.exec(spi.id, systemType: state.status.system, customDir: spi.custom?.scriptDir);
// Loggers.app.info('Running status command for ${spi.name} (${state.status.system.name}): $statusCmd');
final execResult = await state.client?.run(statusCmd);
if (execResult != null) {
raw = SSHDecoder.decode(
execResult,
isWindows: state.status.system == SystemType.windows,
context: 'GetStatus<${spi.name}>',
);
// Loggers.app.info('Status response length for ${spi.name}: ${raw.length} bytes');
} else {
raw = '';
Loggers.app.warning('No status result from ${spi.name}');
}
if (raw.isEmpty) {
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'Empty response from server');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
segments = raw.split(ScriptConstants.separator).map((e) => e.trim()).toList();
if (segments.isEmpty) {
if (Stores.setting.keepStatusWhenErr.fetch()) {
// Keep previous server status when error occurs
if (state.conn != ServerConn.failed && state.status.more.isNotEmpty) {
return;
}
}
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.segements, message: 'Separate segments failed, raw:\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
} catch (e) {
TryLimiter.inc(sid);
final newStatus = state.status..err = SSHErr(type: SSHErrType.getStatus, message: e.toString());
updateStatus(newStatus);
updateConnection(ServerConn.failed);
Loggers.app.warning('Get status from ${spi.name} failed', e);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
try {
// Parse script output into command-specific mappings
final parsedOutput = ScriptConstants.parseScriptOutput(raw);
final req = ServerStatusUpdateReq(
ss: state.status,
parsedOutput: parsedOutput,
system: state.status.system,
customCmds: spi.custom?.cmds ?? {},
);
final newStatus = await Computer.shared.start(getStatus, req, taskName: 'StatusUpdateReq<${spi.id}>');
updateStatus(newStatus);
} catch (e, trace) {
TryLimiter.inc(sid);
final newStatus = state.status
..err = SSHErr(type: SSHErrType.getStatus, message: 'Parse failed: $e\n\n$raw');
updateStatus(newStatus);
updateConnection(ServerConn.failed);
Loggers.app.warning('Server status', e, trace);
final sessionId = 'ssh_${spi.id}';
TermSessionManager.updateStatus(sessionId, TermSessionStatus.disconnected);
return;
}
// Set Server.isBusy to false each time this method is called
updateConnection(ServerConn.finished);
// Reset retry count only after successful preparation
TryLimiter.reset(sid);
}
}
extension IndividualServerStateExtension on ServerState {
bool get needGenClient => conn < ServerConn.connecting;
bool get canViewDetails => conn == ServerConn.finished;
String get id => spi.id;
}