feat: AdaptiveConcurrencyController

This commit is contained in:
xkeyC 2025-12-05 22:06:55 +08:00
parent 3c60b5a2c1
commit 8898569067
13 changed files with 387 additions and 62 deletions

View File

@ -32,6 +32,18 @@ Future<void> downloaderInit({
bool downloaderIsInitialized() => bool downloaderIsInitialized() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderIsInitialized(); RustLib.instance.api.crateApiDownloaderApiDownloaderIsInitialized();
/// Check if there are pending tasks to restore from session file (without starting the downloader)
/// This reads the session.json file directly to check if there are any torrents saved.
///
/// Parameters:
/// - working_dir: The directory where session data is stored (same as passed to downloader_init)
///
/// Returns: true if there are tasks to restore, false otherwise
bool downloaderHasPendingSessionTasks({required String workingDir}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderHasPendingSessionTasks(
workingDir: workingDir,
);
/// Add a torrent from bytes (e.g., .torrent file content) /// Add a torrent from bytes (e.g., .torrent file content)
Future<BigInt> downloaderAddTorrent({ Future<BigInt> downloaderAddTorrent({
required List<int> torrentBytes, required List<int> torrentBytes,
@ -118,6 +130,17 @@ Future<void> downloaderStop() =>
Future<void> downloaderShutdown() => Future<void> downloaderShutdown() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderShutdown(); RustLib.instance.api.crateApiDownloaderApiDownloaderShutdown();
/// Get all completed tasks from cache (tasks removed by downloader_remove_completed_tasks)
/// This cache is cleared when the downloader is shutdown/restarted
List<DownloadTaskInfo> downloaderGetCompletedTasksCache() => RustLib
.instance
.api
.crateApiDownloaderApiDownloaderGetCompletedTasksCache();
/// Clear the completed tasks cache manually
void downloaderClearCompletedTasksCache() => RustLib.instance.api
.crateApiDownloaderApiDownloaderClearCompletedTasksCache();
/// Update global speed limits /// Update global speed limits
/// Note: rqbit Session doesn't support runtime limit changes, /// Note: rqbit Session doesn't support runtime limit changes,
/// this function is a placeholder that returns an error. /// this function is a placeholder that returns an error.
@ -131,6 +154,7 @@ Future<void> downloaderUpdateSpeedLimits({
); );
/// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior) /// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior)
/// Removed tasks are cached in memory and can be queried via downloader_get_completed_tasks_cache
Future<int> downloaderRemoveCompletedTasks() => Future<int> downloaderRemoveCompletedTasks() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderRemoveCompletedTasks(); RustLib.instance.api.crateApiDownloaderApiDownloaderRemoveCompletedTasks();

View File

@ -6,6 +6,7 @@
import '../frb_generated.dart'; import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `get_process_path`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt`
Future<void> sendNotify({ Future<void> sendNotify({
@ -20,21 +21,27 @@ Future<void> sendNotify({
appId: appId, appId: appId,
); );
/// Get system memory size in GB
Future<BigInt> getSystemMemorySizeGb() => Future<BigInt> getSystemMemorySizeGb() =>
RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb(); RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb();
/// Get number of logical processors
Future<int> getNumberOfLogicalProcessors() => Future<int> getNumberOfLogicalProcessors() =>
RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors(); RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors();
/// Get all system information at once
Future<SystemInfo> getSystemInfo() => Future<SystemInfo> getSystemInfo() =>
RustLib.instance.api.crateApiWin32ApiGetSystemInfo(); RustLib.instance.api.crateApiWin32ApiGetSystemInfo();
/// Get GPU info from registry (more accurate VRAM)
Future<String> getGpuInfoFromRegistry() => Future<String> getGpuInfoFromRegistry() =>
RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry(); RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry();
/// Resolve shortcut (.lnk) file to get target path
Future<String> resolveShortcut({required String lnkPath}) => Future<String> resolveShortcut({required String lnkPath}) =>
RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath); RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath);
/// Open file explorer and select file/folder
Future<void> openDirWithExplorer({ Future<void> openDirWithExplorer({
required String path, required String path,
required bool isFile, required bool isFile,
@ -58,16 +65,19 @@ Future<List<ProcessInfo>> getProcessListByName({required String processName}) =>
processName: processName, processName: processName,
); );
/// Kill processes by name
Future<int> killProcessByName({required String processName}) => RustLib Future<int> killProcessByName({required String processName}) => RustLib
.instance .instance
.api .api
.crateApiWin32ApiKillProcessByName(processName: processName); .crateApiWin32ApiKillProcessByName(processName: processName);
/// Get disk physical sector size for performance
Future<int> getDiskPhysicalSectorSize({required String driveLetter}) => RustLib Future<int> getDiskPhysicalSectorSize({required String driveLetter}) => RustLib
.instance .instance
.api .api
.crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter); .crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter);
/// Create a desktop shortcut
Future<void> createDesktopShortcut({ Future<void> createDesktopShortcut({
required String targetPath, required String targetPath,
required String shortcutName, required String shortcutName,
@ -76,12 +86,14 @@ Future<void> createDesktopShortcut({
shortcutName: shortcutName, shortcutName: shortcutName,
); );
/// Run a program with admin privileges (UAC)
Future<void> runAsAdmin({required String program, required String args}) => Future<void> runAsAdmin({required String program, required String args}) =>
RustLib.instance.api.crateApiWin32ApiRunAsAdmin( RustLib.instance.api.crateApiWin32ApiRunAsAdmin(
program: program, program: program,
args: args, args: args,
); );
/// Start a program (without waiting)
Future<void> startProcess({ Future<void> startProcess({
required String program, required String program,
required List<String> args, required List<String> args,
@ -90,12 +102,15 @@ Future<void> startProcess({
args: args, args: args,
); );
/// Check if NVME patch is applied
Future<bool> checkNvmePatchStatus() => Future<bool> checkNvmePatchStatus() =>
RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus(); RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus();
/// Add NVME patch to registry
Future<void> addNvmePatch() => Future<void> addNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiAddNvmePatch(); RustLib.instance.api.crateApiWin32ApiAddNvmePatch();
/// Remove NVME patch from registry
Future<void> removeNvmePatch() => Future<void> removeNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch(); RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch();

View File

@ -72,7 +72,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.11.1'; String get codegenVersion => '2.11.1';
@override @override
int get rustContentHash => -641930410; int get rustContentHash => -1482626931;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@ -118,8 +118,13 @@ abstract class RustLibApi extends BaseApi {
List<String>? trackers, List<String>? trackers,
}); });
void crateApiDownloaderApiDownloaderClearCompletedTasksCache();
Future<List<DownloadTaskInfo>> crateApiDownloaderApiDownloaderGetAllTasks(); Future<List<DownloadTaskInfo>> crateApiDownloaderApiDownloaderGetAllTasks();
List<DownloadTaskInfo>
crateApiDownloaderApiDownloaderGetCompletedTasksCache();
Future<DownloadGlobalStat> crateApiDownloaderApiDownloaderGetGlobalStats(); Future<DownloadGlobalStat> crateApiDownloaderApiDownloaderGetGlobalStats();
Future<DownloadTaskInfo> crateApiDownloaderApiDownloaderGetTaskInfo({ Future<DownloadTaskInfo> crateApiDownloaderApiDownloaderGetTaskInfo({
@ -128,6 +133,10 @@ abstract class RustLibApi extends BaseApi {
Future<bool> crateApiDownloaderApiDownloaderHasActiveTasks(); Future<bool> crateApiDownloaderApiDownloaderHasActiveTasks();
bool crateApiDownloaderApiDownloaderHasPendingSessionTasks({
required String workingDir,
});
Future<void> crateApiDownloaderApiDownloaderInit({ Future<void> crateApiDownloaderApiDownloaderInit({
required String workingDir, required String workingDir,
required String defaultDownloadDir, required String defaultDownloadDir,
@ -633,6 +642,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["url", "outputFolder", "trackers"], argNames: ["url", "outputFolder", "trackers"],
); );
@override
void crateApiDownloaderApiDownloaderClearCompletedTasksCache() {
return handler.executeSync(
SyncTask(
callFfi: () {
return wire
.wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache();
},
codec: DcoCodec(
decodeSuccessData: dco_decode_unit,
decodeErrorData: null,
),
constMeta:
kCrateApiDownloaderApiDownloaderClearCompletedTasksCacheConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateApiDownloaderApiDownloaderClearCompletedTasksCacheConstMeta =>
const TaskConstMeta(
debugName: "downloader_clear_completed_tasks_cache",
argNames: [],
);
@override @override
Future<List<DownloadTaskInfo>> crateApiDownloaderApiDownloaderGetAllTasks() { Future<List<DownloadTaskInfo>> crateApiDownloaderApiDownloaderGetAllTasks() {
return handler.executeNormal( return handler.executeNormal(
@ -657,6 +693,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiDownloaderApiDownloaderGetAllTasksConstMeta => TaskConstMeta get kCrateApiDownloaderApiDownloaderGetAllTasksConstMeta =>
const TaskConstMeta(debugName: "downloader_get_all_tasks", argNames: []); const TaskConstMeta(debugName: "downloader_get_all_tasks", argNames: []);
@override
List<DownloadTaskInfo>
crateApiDownloaderApiDownloaderGetCompletedTasksCache() {
return handler.executeSync(
SyncTask(
callFfi: () {
return wire
.wire__crate__api__downloader_api__downloader_get_completed_tasks_cache();
},
codec: DcoCodec(
decodeSuccessData: dco_decode_list_download_task_info,
decodeErrorData: null,
),
constMeta:
kCrateApiDownloaderApiDownloaderGetCompletedTasksCacheConstMeta,
argValues: [],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateApiDownloaderApiDownloaderGetCompletedTasksCacheConstMeta =>
const TaskConstMeta(
debugName: "downloader_get_completed_tasks_cache",
argNames: [],
);
@override @override
Future<DownloadGlobalStat> crateApiDownloaderApiDownloaderGetGlobalStats() { Future<DownloadGlobalStat> crateApiDownloaderApiDownloaderGetGlobalStats() {
return handler.executeNormal( return handler.executeNormal(
@ -742,6 +806,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: [], argNames: [],
); );
@override
bool crateApiDownloaderApiDownloaderHasPendingSessionTasks({
required String workingDir,
}) {
return handler.executeSync(
SyncTask(
callFfi: () {
var arg0 = cst_encode_String(workingDir);
return wire
.wire__crate__api__downloader_api__downloader_has_pending_session_tasks(
arg0,
);
},
codec: DcoCodec(
decodeSuccessData: dco_decode_bool,
decodeErrorData: null,
),
constMeta:
kCrateApiDownloaderApiDownloaderHasPendingSessionTasksConstMeta,
argValues: [workingDir],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateApiDownloaderApiDownloaderHasPendingSessionTasksConstMeta =>
const TaskConstMeta(
debugName: "downloader_has_pending_session_tasks",
argNames: ["workingDir"],
);
@override @override
Future<void> crateApiDownloaderApiDownloaderInit({ Future<void> crateApiDownloaderApiDownloaderInit({
required String workingDir, required String workingDir,

View File

@ -1321,6 +1321,19 @@ class RustLibWire implements BaseWire {
) )
>(); >();
WireSyncRust2DartDco
wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache() {
return _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache();
}
late final _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cachePtr =
_lookup<ffi.NativeFunction<WireSyncRust2DartDco Function()>>(
'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache',
);
late final _wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache =
_wire__crate__api__downloader_api__downloader_clear_completed_tasks_cachePtr
.asFunction<WireSyncRust2DartDco Function()>();
void wire__crate__api__downloader_api__downloader_get_all_tasks(int port_) { void wire__crate__api__downloader_api__downloader_get_all_tasks(int port_) {
return _wire__crate__api__downloader_api__downloader_get_all_tasks(port_); return _wire__crate__api__downloader_api__downloader_get_all_tasks(port_);
} }
@ -1333,6 +1346,19 @@ class RustLibWire implements BaseWire {
_wire__crate__api__downloader_api__downloader_get_all_tasksPtr _wire__crate__api__downloader_api__downloader_get_all_tasksPtr
.asFunction<void Function(int)>(); .asFunction<void Function(int)>();
WireSyncRust2DartDco
wire__crate__api__downloader_api__downloader_get_completed_tasks_cache() {
return _wire__crate__api__downloader_api__downloader_get_completed_tasks_cache();
}
late final _wire__crate__api__downloader_api__downloader_get_completed_tasks_cachePtr =
_lookup<ffi.NativeFunction<WireSyncRust2DartDco Function()>>(
'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_completed_tasks_cache',
);
late final _wire__crate__api__downloader_api__downloader_get_completed_tasks_cache =
_wire__crate__api__downloader_api__downloader_get_completed_tasks_cachePtr
.asFunction<WireSyncRust2DartDco Function()>();
void wire__crate__api__downloader_api__downloader_get_global_stats( void wire__crate__api__downloader_api__downloader_get_global_stats(
int port_, int port_,
) { ) {
@ -1383,6 +1409,33 @@ class RustLibWire implements BaseWire {
_wire__crate__api__downloader_api__downloader_has_active_tasksPtr _wire__crate__api__downloader_api__downloader_has_active_tasksPtr
.asFunction<void Function(int)>(); .asFunction<void Function(int)>();
WireSyncRust2DartDco
wire__crate__api__downloader_api__downloader_has_pending_session_tasks(
ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir,
) {
return _wire__crate__api__downloader_api__downloader_has_pending_session_tasks(
working_dir,
);
}
late final _wire__crate__api__downloader_api__downloader_has_pending_session_tasksPtr =
_lookup<
ffi.NativeFunction<
WireSyncRust2DartDco Function(
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
)
>
>(
'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_has_pending_session_tasks',
);
late final _wire__crate__api__downloader_api__downloader_has_pending_session_tasks =
_wire__crate__api__downloader_api__downloader_has_pending_session_tasksPtr
.asFunction<
WireSyncRust2DartDco Function(
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
)
>();
void wire__crate__api__downloader_api__downloader_init( void wire__crate__api__downloader_api__downloader_init(
int port_, int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir, ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir,
@ -2083,9 +2136,9 @@ class RustLibWire implements BaseWire {
void wire__crate__api__win32_api__resolve_shortcut( void wire__crate__api__win32_api__resolve_shortcut(
int port_, int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> _lnk_path, ffi.Pointer<wire_cst_list_prim_u_8_strict> lnk_path,
) { ) {
return _wire__crate__api__win32_api__resolve_shortcut(port_, _lnk_path); return _wire__crate__api__win32_api__resolve_shortcut(port_, lnk_path);
} }
late final _wire__crate__api__win32_api__resolve_shortcutPtr = late final _wire__crate__api__win32_api__resolve_shortcutPtr =

View File

@ -28,9 +28,10 @@ class S {
static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
static Future<S> load(Locale locale) { static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false) final name =
? locale.languageCode (locale.countryCode?.isEmpty ?? false)
: locale.toString(); ? locale.languageCode
: locale.toString();
final localeName = Intl.canonicalizedLocale(name); final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) { return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName; Intl.defaultLocale = localeName;

View File

@ -52,14 +52,14 @@ class DownloadManager extends _$DownloadManager {
// Lazy load init // Lazy load init
() async { () async {
await Future.delayed(const Duration(milliseconds: 16));
try { try {
// Check if there are existing tasks (check working dir for session data) // Check if there are pending tasks to restore (without starting the downloader)
final dir = Directory(workingDir); if (downloader_api.downloaderHasPendingSessionTasks(workingDir: workingDir)) {
if (await dir.exists()) { dPrint("Launch download manager - found pending session tasks");
dPrint("Launch download manager");
await initDownloader(); await initDownloader();
} else { } else {
dPrint("LazyLoad download manager"); dPrint("LazyLoad download manager - no pending tasks");
} }
} catch (e) { } catch (e) {
dPrint("DownloadManager.checkLazyLoad Error:$e"); dPrint("DownloadManager.checkLazyLoad Error:$e");
@ -246,4 +246,15 @@ class DownloadManager extends _$DownloadManager {
} }
return await downloader_api.downloaderHasActiveTasks(); return await downloader_api.downloaderHasActiveTasks();
} }
/// Get all completed tasks from cache (tasks that were removed by removeCompletedTasks)
/// This cache is cleared when the downloader is shutdown/restarted
List<downloader_api.DownloadTaskInfo> getCompletedTasksCache() {
return downloader_api.downloaderGetCompletedTasksCache();
}
/// Clear the completed tasks cache manually
void clearCompletedTasksCache() {
downloader_api.downloaderClearCompletedTasksCache();
}
} }

View File

@ -41,7 +41,7 @@ final class DownloadManagerProvider
} }
} }
String _$downloadManagerHash() => r'f12d3fb1d7c03fdfccff7d07903218f38a860437'; String _$downloadManagerHash() => r'55c92224a5eb6bb0f84f0a97fd0585b94f61f711';
abstract class _$DownloadManager extends $Notifier<DownloadManagerState> { abstract class _$DownloadManager extends $Notifier<DownloadManagerState> {
DownloadManagerState build(); DownloadManagerState build();

View File

@ -153,7 +153,7 @@ class HomeDownloaderUI extends HookConsumerWidget {
const SizedBox(width: 32), const SizedBox(width: 32),
if (type != "stopped") if (type != "stopped")
DropDownButton( DropDownButton(
closeAfterClick: false, closeAfterClick: true,
title: Padding( title: Padding(
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
child: Text(S.current.downloader_action_options), child: Text(S.current.downloader_action_options),

View File

@ -42,7 +42,7 @@ final class HomeDownloaderUIModelProvider
} }
String _$homeDownloaderUIModelHash() => String _$homeDownloaderUIModelHash() =>
r'567cf106d69ed24a5adb8d7f4ad9c422cf33dc1e'; r'bf7d095d761fff078de707562cf311c20db664d9';
abstract class _$HomeDownloaderUIModel abstract class _$HomeDownloaderUIModel
extends $Notifier<HomeDownloaderUIState> { extends $Notifier<HomeDownloaderUIState> {

22
rust/Cargo.lock generated
View File

@ -2994,7 +2994,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit" name = "librqbit"
version = "9.0.0-beta.1" version = "9.0.0-beta.1"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
@ -3058,7 +3058,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-bencode" name = "librqbit-bencode"
version = "3.1.0" version = "3.1.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arrayvec", "arrayvec",
@ -3074,7 +3074,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-buffers" name = "librqbit-buffers"
version = "4.2.0" version = "4.2.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"bytes", "bytes",
"librqbit-clone-to-owned", "librqbit-clone-to-owned",
@ -3085,7 +3085,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-clone-to-owned" name = "librqbit-clone-to-owned"
version = "3.0.1" version = "3.0.1"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"bytes", "bytes",
] ]
@ -3093,7 +3093,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-core" name = "librqbit-core"
version = "5.0.0" version = "5.0.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3122,7 +3122,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-dht" name = "librqbit-dht"
version = "5.3.0" version = "5.3.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backon", "backon",
@ -3169,7 +3169,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-lsd" name = "librqbit-lsd"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atoi", "atoi",
@ -3189,7 +3189,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-peer-protocol" name = "librqbit-peer-protocol"
version = "4.3.0" version = "4.3.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitvec", "bitvec",
@ -3209,7 +3209,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-sha1-wrapper" name = "librqbit-sha1-wrapper"
version = "4.1.0" version = "4.1.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"assert_cfg", "assert_cfg",
"aws-lc-rs", "aws-lc-rs",
@ -3218,7 +3218,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-tracker-comms" name = "librqbit-tracker-comms"
version = "3.0.0" version = "3.0.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -3246,7 +3246,7 @@ dependencies = [
[[package]] [[package]]
name = "librqbit-upnp" name = "librqbit-upnp"
version = "1.0.0" version = "1.0.0"
source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.2#7a9b4d7db84b7b9cccc424e294610cc800a9baa4" source = "git+https://github.com/StarCitizenToolBox/rqbit?rev=f8c0b0927904e1d8b0e28e708bd69fd8069d413a#f8c0b0927904e1d8b0e28e708bd69fd8069d413a"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bstr", "bstr",

View File

@ -35,7 +35,7 @@ unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0
uuid = { version = "1.19.0", features = ["v4"] } uuid = { version = "1.19.0", features = ["v4"] }
parking_lot = "0.12.5" parking_lot = "0.12.5"
crossbeam-channel = "0.5.15" crossbeam-channel = "0.5.15"
librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", tag = "webseed-v0.0.2" } librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", rev = "f8c0b0927904e1d8b0e28e708bd69fd8069d413a" }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
bytes = "1.10" bytes = "1.10"

View File

@ -26,9 +26,9 @@ static SESSION_INIT_LOCK: once_cell::sync::Lazy<Mutex<()>> =
static TORRENT_HANDLES: once_cell::sync::Lazy<RwLock<HashMap<usize, ManagedTorrentHandle>>> = static TORRENT_HANDLES: once_cell::sync::Lazy<RwLock<HashMap<usize, ManagedTorrentHandle>>> =
once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
// Store output folders for each task // Store completed tasks info (in-memory cache, cleared on restart)
static TASK_OUTPUT_FOLDERS: once_cell::sync::Lazy<RwLock<HashMap<usize, String>>> = static COMPLETED_TASKS_CACHE: once_cell::sync::Lazy<RwLock<Vec<DownloadTaskInfo>>> =
once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); once_cell::sync::Lazy::new(|| RwLock::new(Vec::new()));
/// Download task status /// Download task status
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -132,12 +132,15 @@ pub async fn downloader_init(
}, },
webseed_config: Some(WebSeedConfig{ webseed_config: Some(WebSeedConfig{
max_concurrent_per_source: 32, max_concurrent_per_source: 32,
max_total_concurrent: 128, max_total_concurrent: 64,
request_timeout_secs: 30, request_timeout_secs: 30,
prefer_for_large_gaps: true, prefer_for_large_gaps: true,
min_gap_for_webseed: 10, min_gap_for_webseed: 10,
max_errors_before_disable: 10, max_errors_before_disable: 10,
disable_cooldown_secs: 600, disable_cooldown_secs: 600,
adaptive_increase_threshold: 5,
adaptive_decrease_threshold: 10,
..Default::default()
}), }),
..Default::default() ..Default::default()
}, },
@ -156,6 +159,44 @@ pub fn downloader_is_initialized() -> bool {
SESSION.read().is_some() SESSION.read().is_some()
} }
/// Check if there are pending tasks to restore from session file (without starting the downloader)
/// This reads the session.json file directly to check if there are any torrents saved.
///
/// Parameters:
/// - working_dir: The directory where session data is stored (same as passed to downloader_init)
///
/// Returns: true if there are tasks to restore, false otherwise
#[frb(sync)]
pub fn downloader_has_pending_session_tasks(working_dir: String) -> bool {
let session_file = PathBuf::from(&working_dir)
.join("rqbit-session")
.join("session.json");
if !session_file.exists() {
return false;
}
// Try to read and parse the session file
match std::fs::read_to_string(&session_file) {
Ok(content) => {
// Parse as JSON to check if there are any torrents
// The structure is: { "torrents": { "0": {...}, "1": {...} } }
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(json) => {
if let Some(torrents) = json.get("torrents") {
if let Some(obj) = torrents.as_object() {
return !obj.is_empty();
}
}
false
}
Err(_) => false,
}
}
Err(_) => false,
}
}
/// Helper function to get session /// Helper function to get session
fn get_session() -> Result<Arc<Session>> { fn get_session() -> Result<Arc<Session>> {
SESSION.read() SESSION.read()
@ -195,17 +236,10 @@ pub async fn downloader_add_torrent(
match response { match response {
AddTorrentResponse::Added(id, handle) => { AddTorrentResponse::Added(id, handle) => {
// Store output folder
if let Some(folder) = output_folder.clone() {
TASK_OUTPUT_FOLDERS.write().insert(id, folder);
}
TORRENT_HANDLES.write().insert(id, handle); TORRENT_HANDLES.write().insert(id, handle);
Ok(id) Ok(id)
} }
AddTorrentResponse::AlreadyManaged(id, handle) => { AddTorrentResponse::AlreadyManaged(id, handle) => {
if let Some(folder) = output_folder.clone() {
TASK_OUTPUT_FOLDERS.write().insert(id, folder);
}
TORRENT_HANDLES.write().insert(id, handle); TORRENT_HANDLES.write().insert(id, handle);
Ok(id) Ok(id)
} }
@ -251,16 +285,10 @@ pub async fn downloader_add_magnet(
match response { match response {
AddTorrentResponse::Added(id, handle) => { AddTorrentResponse::Added(id, handle) => {
if let Some(folder) = output_folder.clone() {
TASK_OUTPUT_FOLDERS.write().insert(id, folder);
}
TORRENT_HANDLES.write().insert(id, handle); TORRENT_HANDLES.write().insert(id, handle);
Ok(id) Ok(id)
} }
AddTorrentResponse::AlreadyManaged(id, handle) => { AddTorrentResponse::AlreadyManaged(id, handle) => {
if let Some(folder) = output_folder.clone() {
TASK_OUTPUT_FOLDERS.write().insert(id, folder);
}
TORRENT_HANDLES.write().insert(id, handle); TORRENT_HANDLES.write().insert(id, handle);
Ok(id) Ok(id)
} }
@ -364,11 +392,13 @@ pub async fn downloader_get_task_info(task_id: usize) -> Result<DownloadTaskInfo
if let Some(handle) = handle { if let Some(handle) = handle {
let stats = handle.stats(); let stats = handle.stats();
let name = handle.name().unwrap_or_else(|| format!("Task {}", task_id)); let name = handle.name().unwrap_or_else(|| format!("Task {}", task_id));
let output_folder = TASK_OUTPUT_FOLDERS // Get output_folder from handle's shared options
.read() let output_folder = handle
.get(&task_id) .shared()
.cloned() .options
.unwrap_or_default(); .output_folder
.to_string_lossy()
.into_owned();
let status = get_task_status(&stats); let status = get_task_status(&stats);
let progress = if stats.total_bytes > 0 { let progress = if stats.total_bytes > 0 {
@ -440,11 +470,13 @@ pub async fn downloader_get_all_tasks() -> Result<Vec<DownloadTaskInfo>> {
for (id, handle) in torrents { for (id, handle) in torrents {
let stats = handle.stats(); let stats = handle.stats();
let name = handle.name().unwrap_or_else(|| format!("Task {}", id)); let name = handle.name().unwrap_or_else(|| format!("Task {}", id));
let output_folder = TASK_OUTPUT_FOLDERS // Get output_folder from handle's shared options
.read() let output_folder = handle
.get(&id) .shared()
.cloned() .options
.unwrap_or_default(); .output_folder
.to_string_lossy()
.into_owned();
let status = get_task_status(&stats); let status = get_task_status(&stats);
let progress = if stats.total_bytes > 0 { let progress = if stats.total_bytes > 0 {
@ -552,7 +584,6 @@ pub async fn downloader_stop() -> Result<()> {
session.stop().await; session.stop().await;
} }
TORRENT_HANDLES.write().clear(); TORRENT_HANDLES.write().clear();
TASK_OUTPUT_FOLDERS.write().clear();
Ok(()) Ok(())
} }
@ -568,10 +599,24 @@ pub async fn downloader_shutdown() -> Result<()> {
} }
TORRENT_HANDLES.write().clear(); TORRENT_HANDLES.write().clear();
TASK_OUTPUT_FOLDERS.write().clear(); // Clear completed tasks cache on shutdown
COMPLETED_TASKS_CACHE.write().clear();
Ok(()) Ok(())
} }
/// Get all completed tasks from cache (tasks removed by downloader_remove_completed_tasks)
/// This cache is cleared when the downloader is shutdown/restarted
#[frb(sync)]
pub fn downloader_get_completed_tasks_cache() -> Vec<DownloadTaskInfo> {
COMPLETED_TASKS_CACHE.read().clone()
}
/// Clear the completed tasks cache manually
#[frb(sync)]
pub fn downloader_clear_completed_tasks_cache() {
COMPLETED_TASKS_CACHE.write().clear();
}
/// Update global speed limits /// Update global speed limits
/// Note: rqbit Session doesn't support runtime limit changes, /// Note: rqbit Session doesn't support runtime limit changes,
/// this function is a placeholder that returns an error. /// this function is a placeholder that returns an error.
@ -586,6 +631,7 @@ pub async fn downloader_update_speed_limits(
} }
/// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior) /// Remove all completed tasks (equivalent to aria2's --seed-time=0 behavior)
/// Removed tasks are cached in memory and can be queried via downloader_get_completed_tasks_cache
pub async fn downloader_remove_completed_tasks() -> Result<u32> { pub async fn downloader_remove_completed_tasks() -> Result<u32> {
let session = get_session()?; let session = get_session()?;
@ -599,8 +645,10 @@ pub async fn downloader_remove_completed_tasks() -> Result<u32> {
if has_handle { if has_handle {
// Use TorrentIdOrHash::Id for deletion (TorrentId is just usize) // Use TorrentIdOrHash::Id for deletion (TorrentId is just usize)
if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() { if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() {
// Save task info to cache before removing
COMPLETED_TASKS_CACHE.write().push(task.clone());
TORRENT_HANDLES.write().remove(&task.id); TORRENT_HANDLES.write().remove(&task.id);
TASK_OUTPUT_FOLDERS.write().remove(&task.id);
removed_count += 1; removed_count += 1;
} }
} }

View File

@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom, default_rust_auto_opaque = RustAutoOpaqueNom,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -641930410; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1482626931;
// Section: executor // Section: executor
@ -304,6 +304,24 @@ fn wire__crate__api__downloader_api__downloader_add_url_impl(
}, },
) )
} }
fn wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::DcoCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "downloader_clear_completed_tasks_cache",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
transform_result_dco::<_, _, ()>((move || {
let output_ok = Result::<_, ()>::Ok({
crate::api::downloader_api::downloader_clear_completed_tasks_cache();
})?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl( fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
) { ) {
@ -327,6 +345,24 @@ fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl(
}, },
) )
} }
fn wire__crate__api__downloader_api__downloader_get_completed_tasks_cache_impl(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::DcoCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "downloader_get_completed_tasks_cache",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
transform_result_dco::<_, _, ()>((move || {
let output_ok = Result::<_, ()>::Ok(
crate::api::downloader_api::downloader_get_completed_tasks_cache(),
)?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__downloader_api__downloader_get_global_stats_impl( fn wire__crate__api__downloader_api__downloader_get_global_stats_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
) { ) {
@ -400,6 +436,28 @@ fn wire__crate__api__downloader_api__downloader_has_active_tasks_impl(
}, },
) )
} }
fn wire__crate__api__downloader_api__downloader_has_pending_session_tasks_impl(
working_dir: impl CstDecode<String>,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::DcoCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "downloader_has_pending_session_tasks",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
let api_working_dir = working_dir.cst_decode();
transform_result_dco::<_, _, ()>((move || {
let output_ok = Result::<_, ()>::Ok(
crate::api::downloader_api::downloader_has_pending_session_tasks(
api_working_dir,
),
)?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__downloader_api__downloader_init_impl( fn wire__crate__api__downloader_api__downloader_init_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
working_dir: impl CstDecode<String>, working_dir: impl CstDecode<String>,
@ -1204,7 +1262,7 @@ fn wire__crate__api__win32_api__remove_nvme_patch_impl(
} }
fn wire__crate__api__win32_api__resolve_shortcut_impl( fn wire__crate__api__win32_api__resolve_shortcut_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
_lnk_path: impl CstDecode<String>, lnk_path: impl CstDecode<String>,
) { ) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>( FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo { flutter_rust_bridge::for_generated::TaskInfo {
@ -1213,11 +1271,11 @@ fn wire__crate__api__win32_api__resolve_shortcut_impl(
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
}, },
move || { move || {
let api__lnk_path = _lnk_path.cst_decode(); let api_lnk_path = lnk_path.cst_decode();
move |context| { move |context| {
transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>(
(move || { (move || {
let output_ok = crate::api::win32_api::resolve_shortcut(api__lnk_path)?; let output_ok = crate::api::win32_api::resolve_shortcut(api_lnk_path)?;
Ok(output_ok) Ok(output_ok)
})(), })(),
) )
@ -4067,6 +4125,12 @@ mod io {
) )
} }
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
wire__crate__api__downloader_api__downloader_clear_completed_tasks_cache_impl()
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_all_tasks( pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_all_tasks(
port_: i64, port_: i64,
@ -4074,6 +4138,12 @@ mod io {
wire__crate__api__downloader_api__downloader_get_all_tasks_impl(port_) wire__crate__api__downloader_api__downloader_get_all_tasks_impl(port_)
} }
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_completed_tasks_cache(
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
wire__crate__api__downloader_api__downloader_get_completed_tasks_cache_impl()
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_global_stats( pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_global_stats(
port_: i64, port_: i64,
@ -4096,6 +4166,13 @@ mod io {
wire__crate__api__downloader_api__downloader_has_active_tasks_impl(port_) wire__crate__api__downloader_api__downloader_has_active_tasks_impl(port_)
} }
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_has_pending_session_tasks(
working_dir: *mut wire_cst_list_prim_u_8_strict,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco {
wire__crate__api__downloader_api__downloader_has_pending_session_tasks_impl(working_dir)
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_init( pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_init(
port_: i64, port_: i64,
@ -4378,9 +4455,9 @@ mod io {
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__resolve_shortcut( pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__resolve_shortcut(
port_: i64, port_: i64,
_lnk_path: *mut wire_cst_list_prim_u_8_strict, lnk_path: *mut wire_cst_list_prim_u_8_strict,
) { ) {
wire__crate__api__win32_api__resolve_shortcut_impl(port_, _lnk_path) wire__crate__api__win32_api__resolve_shortcut_impl(port_, lnk_path)
} }
#[unsafe(no_mangle)] #[unsafe(no_mangle)]