mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-10 17:55:43 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90e35ed54e |
@@ -332,16 +332,18 @@ async function terminateChild() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childExited = await waitForChildExit(5000);
|
if (await waitForChildExit(5000) && await waitForProcessGroupExit(child.pid, 5000)) {
|
||||||
const processGroupExited = await waitForProcessGroupExit(child.pid, 5000);
|
|
||||||
if (childExited && processGroupExited) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!childWasRunning || childExit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.kill(-child.pid, 'SIGKILL');
|
process.kill(-child.pid, 'SIGKILL');
|
||||||
} catch {
|
} catch {
|
||||||
if (!childExited) {
|
if (!childExit) {
|
||||||
child.kill('SIGKILL');
|
child.kill('SIGKILL');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2523,57 +2523,6 @@ mod tests {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn migrates_v1_database_and_creates_backup() {
|
|
||||||
let temp = TempDir::new().unwrap();
|
|
||||||
let path = temp.path().join(DATABASE_NAME);
|
|
||||||
let connection = Connection::open(&path).unwrap();
|
|
||||||
connection
|
|
||||||
.execute_batch(
|
|
||||||
"
|
|
||||||
CREATE TABLE downloads (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
queue_id TEXT,
|
|
||||||
data TEXT NOT NULL
|
|
||||||
);
|
|
||||||
CREATE TABLE settings (id INTEGER PRIMARY KEY, data TEXT NOT NULL);
|
|
||||||
CREATE TABLE queues (id TEXT PRIMARY KEY, data TEXT NOT NULL);
|
|
||||||
CREATE TABLE download_ownership (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
primary_path TEXT NOT NULL
|
|
||||||
);
|
|
||||||
INSERT INTO download_ownership VALUES ('download-1', '/downloads/file.bin');
|
|
||||||
PRAGMA user_version = 1;
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
drop(connection);
|
|
||||||
|
|
||||||
let state = init_at_path(temp.path()).unwrap();
|
|
||||||
let connection = state.lock().unwrap();
|
|
||||||
let version: i64 = connection
|
|
||||||
.pragma_query_value(None, "user_version", |row| row.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(version, CURRENT_SCHEMA_VERSION);
|
|
||||||
assert!(table_exists(&connection, "download_owned_paths").unwrap());
|
|
||||||
assert!(table_exists(&connection, "download_removal_paths").unwrap());
|
|
||||||
assert_eq!(
|
|
||||||
load_ownership(&connection).unwrap(),
|
|
||||||
vec![(
|
|
||||||
"download-1".to_string(),
|
|
||||||
"/downloads/file.bin".to_string(),
|
|
||||||
vec!["/downloads/file.bin".to_string()]
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
assert!(fs::read_dir(temp.path()).unwrap().flatten().any(|entry| {
|
|
||||||
entry
|
|
||||||
.file_name()
|
|
||||||
.to_string_lossy()
|
|
||||||
.starts_with("firelink.sqlite.backup-schema-v1-")
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn refuses_to_open_a_database_symlink() {
|
fn refuses_to_open_a_database_symlink() {
|
||||||
|
|||||||
@@ -308,16 +308,18 @@ async fn download_handler(
|
|||||||
None => return Err(StatusCode::BAD_REQUEST),
|
None => return Err(StatusCode::BAD_REQUEST),
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_hidden = state
|
if let Some(window) = state.app_handle.get_webview_window("main") {
|
||||||
.app_handle
|
let is_visible = window.is_visible().unwrap_or(true);
|
||||||
.get_webview_window("main")
|
if !is_visible {
|
||||||
.and_then(|window| window.is_visible().ok())
|
let _ = window.show();
|
||||||
.is_some_and(|is_visible| !is_visible);
|
let _ = window.set_focus();
|
||||||
crate::restore_main_window(&state.app_handle);
|
// Sleep briefly to let the webview wake up from macOS App Nap
|
||||||
if is_hidden {
|
// otherwise the IPC event emitted immediately after is dropped.
|
||||||
// Sleep briefly to let the webview wake up from macOS App Nap
|
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
||||||
// otherwise the IPC event emitted immediately after is dropped.
|
} else {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !wait_for_frontend(&state.frontend_ready).await {
|
if !wait_for_frontend(&state.frontend_ready).await {
|
||||||
|
|||||||
+49
-336
@@ -3328,21 +3328,6 @@ fn metadata_is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn path_has_symlink_component(path: &std::path::Path) -> bool {
|
pub(crate) fn path_has_symlink_component(path: &std::path::Path) -> bool {
|
||||||
path_has_component_matching(path, metadata_is_link_or_reparse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect only symbolic-link components, without treating every Windows
|
|
||||||
/// reparse point as a link. Trusted application-data directories may use
|
|
||||||
/// junctions for Windows folder redirection; user-selected download and
|
|
||||||
/// recovery paths continue to use the stricter helper above.
|
|
||||||
pub(crate) fn path_has_symbolic_link_component(path: &std::path::Path) -> bool {
|
|
||||||
path_has_component_matching(path, |metadata| metadata.file_type().is_symlink())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path_has_component_matching(
|
|
||||||
path: &std::path::Path,
|
|
||||||
matches: impl Fn(&std::fs::Metadata) -> bool,
|
|
||||||
) -> bool {
|
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
|
|
||||||
let mut current = std::path::PathBuf::new();
|
let mut current = std::path::PathBuf::new();
|
||||||
@@ -3354,7 +3339,7 @@ fn path_has_component_matching(
|
|||||||
Component::Normal(name) => {
|
Component::Normal(name) => {
|
||||||
current.push(name);
|
current.push(name);
|
||||||
if std::fs::symlink_metadata(¤t)
|
if std::fs::symlink_metadata(¤t)
|
||||||
.is_ok_and(|metadata| matches(&metadata))
|
.is_ok_and(|metadata| metadata_is_link_or_reparse(&metadata))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -3659,7 +3644,6 @@ pub struct AppState {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct MainWindowRestoreState {
|
struct MainWindowRestoreState {
|
||||||
requested: AtomicBool,
|
requested: AtomicBool,
|
||||||
startup_complete: AtomicBool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -3955,26 +3939,7 @@ where
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn restore_main_window(app_handle: &tauri::AppHandle) {
|
fn restore_main_window(app_handle: &tauri::AppHandle) {
|
||||||
let startup_complete = app_handle
|
|
||||||
.try_state::<MainWindowRestoreState>()
|
|
||||||
.is_none_or(|state| state.startup_complete.load(Ordering::Acquire));
|
|
||||||
if !startup_complete {
|
|
||||||
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
|
|
||||||
// The first window can be requested by a single-instance callback
|
|
||||||
// or an opened .torrent path while the native host is still being
|
|
||||||
// constructed. Do not touch the HWND until RunEvent::Ready.
|
|
||||||
state.requested.store(true, Ordering::Release);
|
|
||||||
// Ready may have won the transition between the first load and
|
|
||||||
// the request store. Re-check before returning so that request is
|
|
||||||
// serviced by this call instead of being left pending forever.
|
|
||||||
if !state.startup_complete.load(Ordering::Acquire) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(window) = app_handle.get_webview_window("main") else {
|
let Some(window) = app_handle.get_webview_window("main") else {
|
||||||
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
|
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
|
||||||
state.requested.store(true, Ordering::Release);
|
state.requested.store(true, Ordering::Release);
|
||||||
@@ -3987,24 +3952,6 @@ pub(crate) fn restore_main_window(app_handle: &tauri::AppHandle) {
|
|||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mark_main_window_startup_complete(app_handle: &tauri::AppHandle) {
|
|
||||||
if let Some(state) = app_handle.try_state::<MainWindowRestoreState>() {
|
|
||||||
state.startup_complete.store(true, Ordering::Release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn reveal_main_window(app_handle: &tauri::AppHandle) {
|
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
|
||||||
if let Err(error) = window.set_focusable(true) {
|
|
||||||
log::warn!("Could not make the main window focusable: {error}");
|
|
||||||
}
|
|
||||||
if let Err(error) = window.show() {
|
|
||||||
eprintln!("Failed to reveal the main window: {error}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_pending_main_window(app_handle: &tauri::AppHandle) {
|
fn restore_pending_main_window(app_handle: &tauri::AppHandle) {
|
||||||
if app_handle
|
if app_handle
|
||||||
.try_state::<MainWindowRestoreState>()
|
.try_state::<MainWindowRestoreState>()
|
||||||
@@ -18200,12 +18147,9 @@ fn toggle_log_pause(caller: tauri::WebviewWindow, pause: bool) -> Result<(), Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn is_log_paused() -> bool {
|
fn is_log_paused(caller: tauri::WebviewWindow) -> bool {
|
||||||
// This read is needed during renderer bootstrap, including by standalone
|
properties_window::ensure_main_window(&caller).is_ok()
|
||||||
// Properties windows. Avoid extracting a WebviewWindow here: on Windows,
|
&& LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
|
||||||
// WebView2 can invoke the command while its host is still initializing.
|
|
||||||
// Mutating logging commands remain restricted to the main window below.
|
|
||||||
LOG_PAUSED.load(std::sync::atomic::Ordering::Relaxed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -18251,7 +18195,7 @@ pub fn run() {
|
|||||||
let aria2_port = Arc::new(std::sync::atomic::AtomicU16::new(initial_aria2_port));
|
let aria2_port = Arc::new(std::sync::atomic::AtomicU16::new(initial_aria2_port));
|
||||||
let aria2_port_clone = Arc::clone(&aria2_port);
|
let aria2_port_clone = Arc::clone(&aria2_port);
|
||||||
let aria2_secret = uuid::Uuid::new_v4().to_string();
|
let aria2_secret = uuid::Uuid::new_v4().to_string();
|
||||||
let builder = tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(MainWindowRestoreState::default())
|
.manage(MainWindowRestoreState::default())
|
||||||
.manage(properties_window::PropertiesWindowRegistry::default())
|
.manage(properties_window::PropertiesWindowRegistry::default())
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
@@ -18553,17 +18497,6 @@ pub fn run() {
|
|||||||
main_window_builder = main_window_builder
|
main_window_builder = main_window_builder
|
||||||
.inner_size(startup_size.width as f64, startup_size.height as f64)
|
.inner_size(startup_size.width as f64, startup_size.height as f64)
|
||||||
.prevent_overflow();
|
.prevent_overflow();
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
// Wry installs its parent WM_SETFOCUS handler while WebView2
|
|
||||||
// is still being initialized. Keep the host hidden and
|
|
||||||
// non-activatable until RunEvent::Ready so Windows cannot
|
|
||||||
// re-enter that handler during native construction.
|
|
||||||
main_window_builder = main_window_builder
|
|
||||||
.visible(false)
|
|
||||||
.focused(false)
|
|
||||||
.focusable(false);
|
|
||||||
}
|
|
||||||
main_window_builder
|
main_window_builder
|
||||||
.build()
|
.build()
|
||||||
.map_err(|error| format!("failed to create main window: {error}"))?;
|
.map_err(|error| format!("failed to create main window: {error}"))?;
|
||||||
@@ -18575,6 +18508,13 @@ pub fn run() {
|
|||||||
collect_opened_torrent_paths(std::env::args_os().skip(1)),
|
collect_opened_torrent_paths(std::env::args_os().skip(1)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
window
|
||||||
|
.set_decorations(false)
|
||||||
|
.map_err(|error| format!("failed to disable Windows native frame: {error}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
let deep_link_app = app.handle().clone();
|
let deep_link_app = app.handle().clone();
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if let Err(error) = app.deep_link().register_all() {
|
if let Err(error) = app.deep_link().register_all() {
|
||||||
@@ -19859,273 +19799,46 @@ pub fn run() {
|
|||||||
let _ = window.hide();
|
let _ = window.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
// Keep each generated handler below the Windows stack-overflow threshold.
|
|
||||||
// Tauri expands generate_handler! into one command match whose arms include
|
|
||||||
// argument extraction for every command. A single 1.4-era table was large
|
|
||||||
// enough to overflow the Windows main-thread stack on the first invoke.
|
|
||||||
let startup_handler: Box<dyn Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync> =
|
|
||||||
Box::new(tauri::generate_handler![
|
|
||||||
get_engine_status,
|
|
||||||
get_aria2_engine_status,
|
|
||||||
get_ytdlp_engine_status,
|
|
||||||
get_ffmpeg_engine_status,
|
|
||||||
get_deno_engine_status,
|
|
||||||
test_ytdlp,
|
|
||||||
test_aria2c,
|
|
||||||
test_ffmpeg,
|
|
||||||
test_deno,
|
|
||||||
pause_download,
|
|
||||||
resume_download,
|
|
||||||
fetch_metadata,
|
|
||||||
inspect_torrent,
|
|
||||||
rekey_torrent_metadata,
|
|
||||||
remove_torrent_metadata,
|
|
||||||
fetch_media_metadata,
|
|
||||||
fetch_media_playlist_metadata,
|
|
||||||
begin_dock_badge_session,
|
|
||||||
update_dock_badge,
|
|
||||||
get_platform_info,
|
|
||||||
approve_download_root,
|
|
||||||
set_prevent_sleep,
|
|
||||||
set_power_preferences,
|
|
||||||
get_free_space,
|
|
||||||
perform_system_action,
|
|
||||||
ack_schedule_trigger,
|
|
||||||
check_automation_permission,
|
|
||||||
request_automation_permission,
|
|
||||||
open_automation_settings,
|
|
||||||
]);
|
|
||||||
let account_and_extension_handler: Box<dyn Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync> =
|
|
||||||
Box::new(tauri::generate_handler![
|
|
||||||
set_keychain_password,
|
|
||||||
get_keychain_password,
|
|
||||||
delete_keychain_password,
|
|
||||||
save_site_login,
|
|
||||||
delete_site_login,
|
|
||||||
hydrate_extension_pairing_token,
|
|
||||||
get_session_pairing_token,
|
|
||||||
regenerate_pairing_token,
|
|
||||||
grant_keychain_access,
|
|
||||||
get_keychain_grant_status,
|
|
||||||
accept_keychain_grant,
|
|
||||||
abandon_keychain_grant,
|
|
||||||
authorize_keychain_access,
|
|
||||||
acknowledge_pairing_token_change,
|
|
||||||
inspect_download_target,
|
|
||||||
toggle_tray_icon,
|
|
||||||
set_extension_pairing_token,
|
|
||||||
get_extension_server_port,
|
|
||||||
set_extension_frontend_ready,
|
|
||||||
ack_frontend_exit,
|
|
||||||
ack_extension_download,
|
|
||||||
]);
|
|
||||||
let torrent_handler: Box<dyn Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync> =
|
|
||||||
Box::new(tauri::generate_handler![
|
|
||||||
set_concurrent_limit,
|
|
||||||
set_queue_concurrency_limits,
|
|
||||||
set_download_speed_limit,
|
|
||||||
set_torrent_upload_limit,
|
|
||||||
set_torrent_peer_options,
|
|
||||||
get_torrent_peers,
|
|
||||||
get_torrent_availability,
|
|
||||||
get_torrent_file_progress,
|
|
||||||
get_torrent_piece_progress,
|
|
||||||
get_torrent_file_selection,
|
|
||||||
set_torrent_file_selection,
|
|
||||||
get_torrent_details,
|
|
||||||
get_torrent_magnet_link,
|
|
||||||
export_torrent_metadata,
|
|
||||||
move_torrent_data,
|
|
||||||
cancel_torrent_move_data,
|
|
||||||
verify_torrent_data,
|
|
||||||
get_torrent_web_seeds,
|
|
||||||
set_torrent_web_seeds,
|
|
||||||
set_torrent_max_open_files,
|
|
||||||
set_torrent_overall_upload_limit,
|
|
||||||
set_global_speed_limit,
|
|
||||||
remove_download,
|
|
||||||
get_download_primary_path,
|
|
||||||
detach_download_for_reconfigure,
|
|
||||||
]);
|
|
||||||
let queue_and_properties_handler: Box<dyn Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync> =
|
|
||||||
Box::new(tauri::generate_handler![
|
|
||||||
enqueue_download,
|
|
||||||
enqueue_many,
|
|
||||||
cancel_enqueue_generation,
|
|
||||||
move_in_queue,
|
|
||||||
move_many_in_queue,
|
|
||||||
remove_from_queue,
|
|
||||||
get_pending_order,
|
|
||||||
commands::reveal_in_file_manager,
|
|
||||||
commands::open_downloaded_file,
|
|
||||||
properties_window::open_download_properties_window,
|
|
||||||
properties_window::get_properties_window_download_id,
|
|
||||||
properties_window::properties_window_send_ready,
|
|
||||||
properties_window::properties_window_reveal,
|
|
||||||
properties_window::properties_window_send_action,
|
|
||||||
properties_window::validate_properties_window_request,
|
|
||||||
properties_window::close_download_properties_window,
|
|
||||||
properties_window::properties_window_registry_remove_for_download,
|
|
||||||
]);
|
|
||||||
let persistence_and_logs_handler: Box<dyn Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync> =
|
|
||||||
Box::new(tauri::generate_handler![
|
|
||||||
parity::get_system_proxy,
|
|
||||||
parity::get_file_category,
|
|
||||||
parity::check_for_updates,
|
|
||||||
parity::is_supported_media,
|
|
||||||
parity::get_supported_media_domains,
|
|
||||||
parity::create_category_directories,
|
|
||||||
db_save_settings,
|
|
||||||
db_load_settings,
|
|
||||||
canonicalize_torrent_network_setting,
|
|
||||||
db_get_all_downloads,
|
|
||||||
db_replace_downloads,
|
|
||||||
db_commit_download_state,
|
|
||||||
clear_torrent_removal_paths,
|
|
||||||
reconcile_torrent_removal_reservations,
|
|
||||||
db_get_all_queues,
|
|
||||||
db_replace_queues,
|
|
||||||
read_logs,
|
|
||||||
export_logs,
|
|
||||||
toggle_log_pause,
|
|
||||||
is_log_paused,
|
|
||||||
clear_logs,
|
|
||||||
set_log_stream_active,
|
|
||||||
]);
|
|
||||||
|
|
||||||
builder
|
|
||||||
.invoke_handler(move |invoke| {
|
|
||||||
let command = invoke.message.command().to_owned();
|
|
||||||
match command.as_str() {
|
|
||||||
"get_engine_status"
|
|
||||||
| "get_aria2_engine_status"
|
|
||||||
| "get_ytdlp_engine_status"
|
|
||||||
| "get_ffmpeg_engine_status"
|
|
||||||
| "get_deno_engine_status"
|
|
||||||
| "test_ytdlp"
|
|
||||||
| "test_aria2c"
|
|
||||||
| "test_ffmpeg"
|
|
||||||
| "test_deno"
|
|
||||||
| "pause_download"
|
|
||||||
| "resume_download"
|
|
||||||
| "fetch_metadata"
|
|
||||||
| "inspect_torrent"
|
|
||||||
| "rekey_torrent_metadata"
|
|
||||||
| "remove_torrent_metadata"
|
|
||||||
| "fetch_media_metadata"
|
|
||||||
| "fetch_media_playlist_metadata"
|
|
||||||
| "begin_dock_badge_session"
|
|
||||||
| "update_dock_badge"
|
|
||||||
| "get_platform_info"
|
|
||||||
| "approve_download_root"
|
|
||||||
| "set_prevent_sleep"
|
|
||||||
| "set_power_preferences"
|
|
||||||
| "get_free_space"
|
|
||||||
| "perform_system_action"
|
|
||||||
| "ack_schedule_trigger"
|
|
||||||
| "check_automation_permission"
|
|
||||||
| "request_automation_permission"
|
|
||||||
| "open_automation_settings" => startup_handler(invoke),
|
|
||||||
"set_keychain_password"
|
|
||||||
| "get_keychain_password"
|
|
||||||
| "delete_keychain_password"
|
|
||||||
| "save_site_login"
|
|
||||||
| "delete_site_login"
|
|
||||||
| "hydrate_extension_pairing_token"
|
|
||||||
| "get_session_pairing_token"
|
|
||||||
| "regenerate_pairing_token"
|
|
||||||
| "grant_keychain_access"
|
|
||||||
| "get_keychain_grant_status"
|
|
||||||
| "accept_keychain_grant"
|
|
||||||
| "abandon_keychain_grant"
|
|
||||||
| "authorize_keychain_access"
|
|
||||||
| "acknowledge_pairing_token_change"
|
|
||||||
| "inspect_download_target"
|
|
||||||
| "toggle_tray_icon"
|
|
||||||
| "set_extension_pairing_token"
|
|
||||||
| "get_extension_server_port"
|
|
||||||
| "set_extension_frontend_ready"
|
|
||||||
| "ack_frontend_exit"
|
|
||||||
| "ack_extension_download" => account_and_extension_handler(invoke),
|
|
||||||
"set_concurrent_limit"
|
|
||||||
| "set_queue_concurrency_limits"
|
|
||||||
| "set_download_speed_limit"
|
|
||||||
| "set_torrent_upload_limit"
|
|
||||||
| "set_torrent_peer_options"
|
|
||||||
| "get_torrent_peers"
|
|
||||||
| "get_torrent_availability"
|
|
||||||
| "get_torrent_file_progress"
|
|
||||||
| "get_torrent_piece_progress"
|
|
||||||
| "get_torrent_file_selection"
|
|
||||||
| "set_torrent_file_selection"
|
|
||||||
| "get_torrent_details"
|
|
||||||
| "get_torrent_magnet_link"
|
|
||||||
| "export_torrent_metadata"
|
|
||||||
| "move_torrent_data"
|
|
||||||
| "cancel_torrent_move_data"
|
|
||||||
| "verify_torrent_data"
|
|
||||||
| "get_torrent_web_seeds"
|
|
||||||
| "set_torrent_web_seeds"
|
|
||||||
| "set_torrent_max_open_files"
|
|
||||||
| "set_torrent_overall_upload_limit"
|
|
||||||
| "set_global_speed_limit"
|
|
||||||
| "remove_download"
|
|
||||||
| "get_download_primary_path"
|
|
||||||
| "detach_download_for_reconfigure" => torrent_handler(invoke),
|
|
||||||
"enqueue_download"
|
|
||||||
| "enqueue_many"
|
|
||||||
| "cancel_enqueue_generation"
|
|
||||||
| "move_in_queue"
|
|
||||||
| "move_many_in_queue"
|
|
||||||
| "remove_from_queue"
|
|
||||||
| "get_pending_order"
|
|
||||||
| "reveal_in_file_manager"
|
|
||||||
| "open_downloaded_file"
|
|
||||||
| "open_download_properties_window"
|
|
||||||
| "get_properties_window_download_id"
|
|
||||||
| "properties_window_send_ready"
|
|
||||||
| "properties_window_reveal"
|
|
||||||
| "properties_window_send_action"
|
|
||||||
| "validate_properties_window_request"
|
|
||||||
| "close_download_properties_window"
|
|
||||||
| "properties_window_registry_remove_for_download" => {
|
|
||||||
queue_and_properties_handler(invoke)
|
|
||||||
}
|
|
||||||
"get_system_proxy"
|
|
||||||
| "get_file_category"
|
|
||||||
| "check_for_updates"
|
|
||||||
| "is_supported_media"
|
|
||||||
| "get_supported_media_domains"
|
|
||||||
| "create_category_directories"
|
|
||||||
| "db_save_settings"
|
|
||||||
| "db_load_settings"
|
|
||||||
| "canonicalize_torrent_network_setting"
|
|
||||||
| "db_get_all_downloads"
|
|
||||||
| "db_replace_downloads"
|
|
||||||
| "db_commit_download_state"
|
|
||||||
| "clear_torrent_removal_paths"
|
|
||||||
| "reconcile_torrent_removal_reservations"
|
|
||||||
| "db_get_all_queues"
|
|
||||||
| "db_replace_queues"
|
|
||||||
| "read_logs"
|
|
||||||
| "export_logs"
|
|
||||||
| "toggle_log_pause"
|
|
||||||
| "is_log_paused"
|
|
||||||
| "clear_logs"
|
|
||||||
| "set_log_stream_active" => persistence_and_logs_handler(invoke),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_engine_status, get_aria2_engine_status, get_ytdlp_engine_status, get_ffmpeg_engine_status,
|
||||||
|
get_deno_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno,
|
||||||
|
pause_download, resume_download, fetch_metadata, inspect_torrent, rekey_torrent_metadata, remove_torrent_metadata, fetch_media_metadata, fetch_media_playlist_metadata,
|
||||||
|
begin_dock_badge_session, update_dock_badge, get_platform_info, approve_download_root, set_prevent_sleep, set_power_preferences, get_free_space, perform_system_action,
|
||||||
|
ack_schedule_trigger,
|
||||||
|
check_automation_permission, request_automation_permission, open_automation_settings,
|
||||||
|
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||||
|
save_site_login, delete_site_login,
|
||||||
|
hydrate_extension_pairing_token, get_session_pairing_token, regenerate_pairing_token, grant_keychain_access,
|
||||||
|
get_keychain_grant_status, accept_keychain_grant, abandon_keychain_grant,
|
||||||
|
authorize_keychain_access,
|
||||||
|
acknowledge_pairing_token_change,
|
||||||
|
inspect_download_target, toggle_tray_icon, set_extension_pairing_token,
|
||||||
|
get_extension_server_port, set_extension_frontend_ready, ack_frontend_exit, ack_extension_download, set_concurrent_limit, set_queue_concurrency_limits, set_download_speed_limit, set_torrent_upload_limit, set_torrent_peer_options, get_torrent_peers, get_torrent_availability, get_torrent_file_progress, get_torrent_piece_progress, get_torrent_file_selection, set_torrent_file_selection, get_torrent_details, get_torrent_magnet_link, export_torrent_metadata, move_torrent_data, cancel_torrent_move_data, verify_torrent_data, get_torrent_web_seeds, set_torrent_web_seeds, set_torrent_max_open_files, set_torrent_overall_upload_limit, set_global_speed_limit, remove_download, get_download_primary_path,
|
||||||
|
detach_download_for_reconfigure,
|
||||||
|
enqueue_download, enqueue_many, cancel_enqueue_generation, move_in_queue, move_many_in_queue, remove_from_queue, get_pending_order,
|
||||||
|
commands::reveal_in_file_manager, commands::open_downloaded_file,
|
||||||
|
properties_window::open_download_properties_window,
|
||||||
|
properties_window::get_properties_window_download_id,
|
||||||
|
properties_window::properties_window_send_ready,
|
||||||
|
properties_window::properties_window_reveal,
|
||||||
|
properties_window::properties_window_send_action,
|
||||||
|
properties_window::validate_properties_window_request,
|
||||||
|
properties_window::close_download_properties_window,
|
||||||
|
properties_window::properties_window_registry_remove_for_download,
|
||||||
|
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
|
||||||
|
parity::create_category_directories,
|
||||||
|
db_save_settings, db_load_settings, canonicalize_torrent_network_setting,
|
||||||
|
db_get_all_downloads, db_replace_downloads,
|
||||||
|
db_commit_download_state,
|
||||||
|
clear_torrent_removal_paths, reconcile_torrent_removal_reservations,
|
||||||
|
db_get_all_queues, db_replace_queues,
|
||||||
|
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs,
|
||||||
|
set_log_stream_active
|
||||||
|
])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| match event {
|
.run(|app_handle, event| match event {
|
||||||
tauri::RunEvent::Ready => {
|
|
||||||
mark_main_window_startup_complete(app_handle);
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
reveal_main_window(app_handle);
|
|
||||||
restore_pending_main_window(app_handle);
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
tauri::RunEvent::Opened { urls } => {
|
tauri::RunEvent::Opened { urls } => {
|
||||||
let paths = collect_opened_torrent_paths(
|
let paths = collect_opened_torrent_paths(
|
||||||
|
|||||||
@@ -437,9 +437,6 @@ pub fn open_download_properties_window(
|
|||||||
// native window becomes visible. Showing an opaque native surface
|
// native window becomes visible. Showing an opaque native surface
|
||||||
// here exposes the webview's unpainted white background.
|
// here exposes the webview's unpainted white background.
|
||||||
.visible(false)
|
.visible(false)
|
||||||
// A hidden WebView2 must not request focus during construction. The
|
|
||||||
// native reveal path focuses it after the window is visible.
|
|
||||||
.focused(false)
|
|
||||||
.transparent(true);
|
.transparent(true);
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||||
let builder = builder.decorations(false);
|
let builder = builder.decorations(false);
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ fn aria2_server_stat_is_valid(contents: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
fn canonicalize_storage_path(path: &Path) -> Result<PathBuf, String> {
|
||||||
if crate::path_has_symbolic_link_component(path) {
|
if crate::path_has_symlink_component(path) {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"storage path contains a symlinked component: '{}'",
|
"storage path contains a symlinked component: '{}'",
|
||||||
path.display()
|
path.display()
|
||||||
@@ -455,32 +455,4 @@ mod tests {
|
|||||||
|
|
||||||
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
assert!(canonicalize_storage_path(Path::new(&redirected)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
#[test]
|
|
||||||
fn accepts_windows_junctions_for_redirected_storage_paths() {
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
let parent = TempDir::new().unwrap();
|
|
||||||
let spaced_parent = parent.path().join("firelink test data");
|
|
||||||
fs::create_dir(&spaced_parent).unwrap();
|
|
||||||
let root = TempDir::new_in(&spaced_parent).unwrap();
|
|
||||||
let target = TempDir::new_in(&spaced_parent).unwrap();
|
|
||||||
let redirected = root.path().join("redirected");
|
|
||||||
let target_storage = target.path().join("firelink");
|
|
||||||
fs::create_dir(&target_storage).unwrap();
|
|
||||||
|
|
||||||
let status = Command::new("cmd")
|
|
||||||
.args(["/D", "/C", "mklink", "/J"])
|
|
||||||
.arg(&redirected)
|
|
||||||
.arg(target.path())
|
|
||||||
.status()
|
|
||||||
.expect("Windows junction creation command should start");
|
|
||||||
assert!(status.success(), "mklink /J failed with status {status}");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
canonicalize_storage_path(&redirected.join("firelink")).unwrap(),
|
|
||||||
fs::canonicalize(target_storage).unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-59
@@ -16,29 +16,7 @@ import { invokeCommand as invoke } from './ipc';
|
|||||||
|
|
||||||
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
|
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
|
||||||
|
|
||||||
// WebView2 can overflow its native call stack when the renderer sends IPC
|
void initLogger();
|
||||||
// during document bootstrapping. Keep all renderer-to-native startup work
|
|
||||||
// behind the document load boundary, not just the first logger query. This is
|
|
||||||
// also needed for Zustand persistence, whose module initialization reads the
|
|
||||||
// native database before React mounts.
|
|
||||||
const documentLoaded = new Promise<void>((resolve) => {
|
|
||||||
const releaseAfterNativeLoad = () => {
|
|
||||||
// The load event is dispatched from WebView2's navigation callback. Move
|
|
||||||
// renderer startup to the next task so its first IPC cannot re-enter that
|
|
||||||
// native callback stack.
|
|
||||||
window.setTimeout(resolve, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
releaseAfterNativeLoad();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.addEventListener('load', releaseAfterNativeLoad, { once: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
void documentLoaded.then(() => {
|
|
||||||
void initLogger();
|
|
||||||
});
|
|
||||||
|
|
||||||
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
|
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
|
||||||
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
|
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
|
||||||
@@ -58,13 +36,11 @@ const originalConsoleError = console.error.bind(console);
|
|||||||
const originalConsoleWarn = console.warn.bind(console);
|
const originalConsoleWarn = console.warn.bind(console);
|
||||||
console.error = (...values: unknown[]) => {
|
console.error = (...values: unknown[]) => {
|
||||||
originalConsoleError(...values);
|
originalConsoleError(...values);
|
||||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
void logError(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
|
||||||
void documentLoaded.then(() => logError(message)).catch(() => undefined);
|
|
||||||
};
|
};
|
||||||
console.warn = (...values: unknown[]) => {
|
console.warn = (...values: unknown[]) => {
|
||||||
originalConsoleWarn(...values);
|
originalConsoleWarn(...values);
|
||||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
void logWarn(redactConsoleMessage(serializeConsoleArguments(values))).catch(() => undefined);
|
||||||
void documentLoaded.then(() => logWarn(message)).catch(() => undefined);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootElement = document.getElementById("root");
|
const rootElement = document.getElementById("root");
|
||||||
@@ -99,46 +75,20 @@ const PropertiesStartupFailure = () => (
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
const MainStartupFailure = () => (
|
|
||||||
<main className="flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
|
|
||||||
<p role="alert">Firelink could not be loaded.</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="app-button app-button-primary px-3 text-xs"
|
|
||||||
onClick={() => {
|
|
||||||
void getCurrentWindow().close().catch(error => {
|
|
||||||
console.error('[MainStartupFailure] close failed', error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</button>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderMainApp = async () => {
|
const renderMainApp = async () => {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
await documentLoaded;
|
// Keep the child entrypoint isolated from the main application module. App
|
||||||
|
// imports the persistent Zustand stores, whose module initialization issues
|
||||||
try {
|
// main-window-only IPC commands. Loading it in a Properties child creates a
|
||||||
// Keep the child entrypoint isolated from the main application module. App
|
// second persistence owner and can race the bridge handshake.
|
||||||
// imports the persistent Zustand stores, whose module initialization issues
|
const RootComponent = (await import('./App')).default;
|
||||||
// main-window-only IPC commands. Loading it in a Properties child creates a
|
renderRoot(RootComponent);
|
||||||
// second persistence owner and can race the bridge handshake.
|
|
||||||
const RootComponent = (await import('./App')).default;
|
|
||||||
renderRoot(RootComponent);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize Firelink:', error);
|
|
||||||
renderRoot(MainStartupFailure);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPropertiesApp = async () => {
|
const renderPropertiesApp = async () => {
|
||||||
if (!rootElement) return;
|
if (!rootElement) return;
|
||||||
|
|
||||||
await documentLoaded;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Properties starts with the synchronous English catalog and changes locale
|
// Properties starts with the synchronous English catalog and changes locale
|
||||||
// after its first paint. Waiting for a lazy locale chunk here delays the
|
// after its first paint. Waiting for a lazy locale chunk here delays the
|
||||||
|
|||||||
Reference in New Issue
Block a user