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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -948,9 +948,7 @@ class _MainWindowState extends State<MainWindow> with WindowListener, Loggable {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Chip(
label: Text(
l10n.totalSpeed(formatSpeed(totalDownloadSpeed)),
),
label: Text(l10n.totalSpeed(formatSpeed(totalDownloadSpeed))),
avatar: const Icon(Icons.speed, size: 16),
backgroundColor: colorScheme.surfaceContainerHighest,
padding: const EdgeInsets.symmetric(
Expand Down
51 changes: 39 additions & 12 deletions lib/models/settings.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'dart:convert' show jsonDecode, jsonEncode;
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../utils/logging.dart';
import 'package:path/path.dart' as p;

import '../utils/app_data_dir.dart';
import 'dart:convert' show jsonDecode, jsonEncode;
import '../utils/default_download_directory.dart';
import '../utils/logging.dart';

enum AppRunMode { standard, tray, hideTray }

Expand Down Expand Up @@ -90,7 +94,11 @@ class Settings extends ChangeNotifier with Loggable {
return getAppDataDirectory();
}

void _assignDefaultSettings() {
Future<String> _defaultDownloadDirectory() async {
return getDefaultDownloadDirectory();
}

void _assignDefaultSettings({required String defaultDownloadDir}) {
_autoStart = false;
_minimizeToTray = true;
_runMode = AppRunMode.tray;
Expand Down Expand Up @@ -119,7 +127,7 @@ class Settings extends ChangeNotifier with Loggable {
_maxConnectionPerServer = 16;
_split = 16;
_continueDownloads = true;
_downloadDir = '';
_downloadDir = defaultDownloadDir;

// Speed settings
_maxOverallDownloadLimit = 0;
Expand Down Expand Up @@ -158,11 +166,11 @@ class Settings extends ChangeNotifier with Loggable {
/// Get settings file path
String _getSettingsFilePath() {
final dataDir = _getDataDirectory();
final configDir = Directory('${dataDir.path}/config');
final configDir = Directory(p.join(dataDir.path, 'config'));
if (!configDir.existsSync()) {
configDir.createSync(recursive: true);
}
return '${configDir.path}/$_settingsFileName';
return p.join(configDir.path, _settingsFileName);
}

String _normalizeBtTracker(String trackers) {
Expand Down Expand Up @@ -244,6 +252,7 @@ class Settings extends ChangeNotifier with Loggable {
if (file.existsSync()) {
final jsonString = await file.readAsString();
final settingsMap = jsonDecode(jsonString);
var needsSave = false;

// Global settings
_autoStart = settingsMap['autoStart'] ?? false;
Expand Down Expand Up @@ -306,7 +315,14 @@ class Settings extends ChangeNotifier with Loggable {
_maxConnectionPerServer = settingsMap['maxConnectionPerServer'] ?? 16;
_split = settingsMap['split'] ?? 16;
_continueDownloads = settingsMap['continueDownloads'] ?? true;
_downloadDir = settingsMap['downloadDir'] ?? '';
final configuredDownloadDir =
(settingsMap['downloadDir'] as String? ?? '').trim();
if (configuredDownloadDir.isEmpty) {
_downloadDir = await _defaultDownloadDirectory();
needsSave = true;
} else {
_downloadDir = p.normalize(configuredDownloadDir);
}

// Speed settings
_maxOverallDownloadLimit = settingsMap['maxOverallDownloadLimit'] ?? 0;
Expand Down Expand Up @@ -344,8 +360,12 @@ class Settings extends ChangeNotifier with Loggable {
_userAgent =
settingsMap['userAgent'] ??
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

if (needsSave) {
await _saveAllSettings();
}
} else {
_applyDefaultSettings();
await _applyDefaultSettings(save: true);
}

_isLoaded = true;
Expand All @@ -357,14 +377,19 @@ class Settings extends ChangeNotifier with Loggable {
} catch (e) {
this.e('Failed to load settings', error: e);
// Apply default settings
_applyDefaultSettings();
await _applyDefaultSettings(save: true);
_isLoaded = true;
}
}

// Apply default settings
void _applyDefaultSettings() {
_assignDefaultSettings();
Future<void> _applyDefaultSettings({bool save = false}) async {
_assignDefaultSettings(
defaultDownloadDir: await _defaultDownloadDirectory(),
);
if (save) {
await _saveAllSettings();
}
// Schedule notifyListeners to run after the current frame is built
SchedulerBinding.instance.addPostFrameCallback((_) {
notifyListeners();
Expand Down Expand Up @@ -448,7 +473,9 @@ class Settings extends ChangeNotifier with Loggable {
}

Future<void> resetToDefaults() async {
_assignDefaultSettings();
_assignDefaultSettings(
defaultDownloadDir: await _defaultDownloadDirectory(),
);
_isLoaded = true;
notifyListeners();
await _saveAllSettings();
Expand Down
38 changes: 28 additions & 10 deletions lib/services/aria2_rpc_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,16 @@ class Aria2RpcClient with Loggable {
} on ConnectionFailedException catch (e) {
w('Connection test failed: $e');
return false;
} on UnauthorizedException {
rethrow;
} catch (err, stackTrace) {
e(
'Unexpected error during connection test',
error: err,
stackTrace: stackTrace,
);
rethrow;
}
} on UnauthorizedException {
rethrow;
} catch (err, stackTrace) {
e(
'Unexpected error during connection test',
error: err,
stackTrace: stackTrace,
);
rethrow;
}
}

/// Pause a download task
Expand Down Expand Up @@ -547,6 +547,24 @@ class Aria2RpcClient with Loggable {
}
}

/// Shut down aria2 through RPC so it can flush its session state.
Future<bool> shutdown({bool force = false}) async {
try {
final response = await callRpc(
force ? 'aria2.forceShutdown' : 'aria2.shutdown',
[],
);
return response['result'] == 'OK';
} catch (e, stackTrace) {
this.e(
'Failed to shut down ${instance.name} through RPC',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
}

/// Purge all stopped download results from aria2.
Future<bool> purgeDownloadResult() async {
try {
Expand Down
75 changes: 63 additions & 12 deletions lib/services/builtin_instance_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;

import '../models/aria2_instance.dart';
import '../utils/app_data_dir.dart';
import 'builtin_upnp_service.dart';
import '../utils/default_download_directory.dart';
import '../utils/logging.dart';
import 'aria2_rpc_client.dart';
import 'builtin_upnp_service.dart';

enum BuiltinInstanceApplyMode { none, liveApply, restartRequired }

/// Service class for managing the built-in Aria2 instance
class BuiltinInstanceService with Loggable {
static const Duration _rpcShutdownTimeout = Duration(seconds: 5);

static BuiltinInstanceService? _instance;
Process? _aria2Process;
String? _aria2cPath;
Expand All @@ -36,27 +41,30 @@ class BuiltinInstanceService with Loggable {

void _initializePaths() {
final dataDir = getAppDataDirectory();
final coreDirPath = '${dataDir.path}/core';
final coreDirPath = p.join(dataDir.path, 'core');
final coreDir = Directory(coreDirPath);

if (!coreDir.existsSync()) {
this.w('Core directory does not exist: $coreDirPath, creating it...');
coreDir.createSync(recursive: true);
}

_aria2cPath = '$coreDirPath/aria2c${Platform.isWindows ? '.exe' : ''}';
_aria2ConfPath = '$coreDirPath/aria2.conf';
_sessionPath = '$coreDirPath/aria2.session';
_logPath = '$coreDirPath/aria2.log';
_aria2cPath = p.join(
coreDirPath,
'aria2c${Platform.isWindows ? '.exe' : ''}',
);
_aria2ConfPath = p.join(coreDirPath, 'aria2.conf');
_sessionPath = p.join(coreDirPath, 'aria2.session');
_logPath = p.join(coreDirPath, 'aria2.log');
}

String _getSettingsFilePath() {
final dataDir = getAppDataDirectory();
final configDir = Directory('${dataDir.path}/config');
final configDir = Directory(p.join(dataDir.path, 'config'));
if (!configDir.existsSync()) {
configDir.createSync(recursive: true);
}
return '${configDir.path}/settings.json';
return p.join(configDir.path, 'settings.json');
}

Map<String, dynamic> _readSettingsSnapshot() {
Expand Down Expand Up @@ -101,8 +109,7 @@ class BuiltinInstanceService with Loggable {
}

String _defaultDownloadDir() {
final dataDir = getAppDataDirectory();
return '${dataDir.path}/downloads';
return getDefaultDownloadDirectorySync();
}

String _resolveEffectiveBtListenPort(Map<String, dynamic> settings) {
Expand Down Expand Up @@ -328,6 +335,8 @@ class BuiltinInstanceService with Loggable {
'--enable-dht6=${settings['enableDht6'] ?? true}',
'--conf-path=$_aria2ConfPath',
'--save-session=$sessionPath',
'--save-session-interval=30',
'--force-save=false',
'--log-level=info',
'--log=$logPath',
];
Expand Down Expand Up @@ -427,8 +436,26 @@ class BuiltinInstanceService with Loggable {
await _stdoutSubscription?.cancel();
await _stderrSubscription?.cancel();

_aria2Process!.kill();
await _aria2Process!.exitCode.timeout(const Duration(seconds: 5));
try {
await _shutdownThroughRpcIfPossible().timeout(_rpcShutdownTimeout);
} on TimeoutException {
this.w(
'Timed out waiting for built-in Aria2 RPC shutdown, terminating process',
);
_aria2Process?.kill();
}

if (_aria2Process != null) {
try {
await _aria2Process!.exitCode.timeout(const Duration(seconds: 5));
} on TimeoutException {
this.w(
'Built-in Aria2 did not exit after RPC shutdown, terminating process',
);
_aria2Process!.kill();
await _aria2Process!.exitCode.timeout(const Duration(seconds: 5));
}
}

_aria2Process = null;
_isConnected = false;
Expand All @@ -448,6 +475,30 @@ class BuiltinInstanceService with Loggable {
return _aria2Process != null;
}

int? get pid => _aria2Process?.pid;

Future<void> _shutdownThroughRpcIfPossible() async {
final client = Aria2RpcClient(getBuiltinInstanceConfig());
try {
await client.saveSession().timeout(_rpcShutdownTimeout);
await client.shutdown(force: true).timeout(_rpcShutdownTimeout);
} on TimeoutException {
this.w(
'Timed out during built-in Aria2 RPC shutdown; falling back to process termination',
);
_aria2Process?.kill();
} catch (e, stackTrace) {
this.w(
'Failed to stop built-in Aria2 through RPC; falling back to process termination',
error: e,
stackTrace: stackTrace,
);
_aria2Process?.kill();
} finally {
client.close();
}
}

void _monitorProcessOutput() {
if (_aria2Process == null) return;

Expand Down
Loading