Fix: Attempt to Fix Nix/Flatpak (Testing)

This commit is contained in:
Youwes09
2026-03-20 21:03:53 -05:00
parent e6b542cd6b
commit 7df7191799
33 changed files with 1953 additions and 620 deletions
+103 -105
View File
@@ -16,16 +16,20 @@ pub struct StorageInfo {
path: String,
}
#[derive(Serialize)]
#[serde(tag = "kind", content = "message")]
pub enum SpawnError {
NotConfigured(String),
SpawnFailed(String),
}
fn resolve_downloads_path(downloads_path: &str) -> PathBuf {
if !downloads_path.trim().is_empty() {
return PathBuf::from(downloads_path);
}
let base = std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("/"))
});
.unwrap_or_else(|_| dirs::data_dir().unwrap_or_else(|| PathBuf::from("/")));
base.join("Tachidesk/downloads")
}
@@ -45,7 +49,9 @@ fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
0
};
let stat_path = if path.exists() { path.clone() } else {
let stat_path = if path.exists() {
path.clone()
} else {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
};
@@ -56,20 +62,14 @@ fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
.max_by_key(|d| d.mount_point().as_os_str().len())
.ok_or_else(|| "Could not find disk for path".to_string())?;
let total_bytes = disk.total_space();
let free_bytes = disk.available_space();
Ok(StorageInfo {
manga_bytes,
total_bytes,
free_bytes,
total_bytes: disk.total_space(),
free_bytes: disk.available_space(),
path: path.to_string_lossy().into_owned(),
})
}
/// Returns the true OS-level scale factor for the main window.
/// On Linux this bypasses WebKitGTK's unreliable devicePixelRatio.
/// On macOS the value comes directly from the native window.
#[tauri::command]
fn get_scale_factor(window: tauri::Window) -> f64 {
window.scale_factor().unwrap_or(1.0)
@@ -77,26 +77,21 @@ fn get_scale_factor(window: tauri::Window) -> f64 {
fn kill_tachidesk(app: &tauri::AppHandle) {
let state = app.state::<ServerState>();
let mut guard = state.0.lock().unwrap();
if let Some(child) = guard.take() {
if let Some(child) = state.0.lock().unwrap().take() {
let _ = child.kill();
println!("Killed tracked server child.");
}
#[cfg(target_os = "windows")]
let _ = std::process::Command::new("taskkill")
.args(["/F", "/FI", "IMAGENAME eq tachidesk*"])
.args(["/F", "/FI", "IMAGENAME eq java*"])
.status();
#[cfg(not(target_os = "windows"))]
let _ = std::process::Command::new("pkill")
.arg("-f")
.arg("tachidesk")
.args(["-f", "tachidesk"])
.status();
}
/// The default server.conf we seed on first launch.
/// Mirrors the Flatpak wrapper: headless, no tray, no browser pop-up.
const DEFAULT_SERVER_CONF: &str = r#"server.ip = "127.0.0.1"
server.port = 4567
server.webUIEnabled = false
@@ -114,9 +109,6 @@ server.maxSourcesInParallel = 6
server.extensionRepos = []
"#;
/// Ensure the Suwayomi data dir and server.conf exist, and that the three
/// keys that cause GUI/JCEF crashes are always set to safe values.
/// This mirrors the shell-script logic in the Flatpak's tachidesk-server wrapper.
fn seed_server_conf(data_dir: &PathBuf) {
let conf_path = data_dir.join("server.conf");
@@ -131,97 +123,73 @@ fn seed_server_conf(data_dir: &PathBuf) {
return;
}
// Conf already exists — patch the three critical keys in-place.
let Ok(contents) = std::fs::read_to_string(&conf_path) else { return };
let patched = patch_conf_key(
patch_conf_key(
patch_conf_key(
contents,
"server.webUIEnabled",
"false",
),
"server.initialOpenInBrowserEnabled",
"false",
patch_conf_key(contents, "server.webUIEnabled", "false"),
"server.initialOpenInBrowserEnabled", "false",
),
"server.systemTrayEnabled",
"false",
"server.systemTrayEnabled", "false",
);
let _ = std::fs::write(&conf_path, patched);
}
/// Replace `key = <value>` in a HOCON/properties-style conf, or append it
/// if the key is absent.
fn patch_conf_key(mut text: String, key: &str, value: &str) -> String {
fn patch_conf_key(text: String, key: &str, value: &str) -> String {
let replacement = format!("{key} = {value}");
// Find a line that starts with the key (tolerant of surrounding whitespace)
if let Some(pos) = text.lines().position(|l| l.trim_start().starts_with(key)) {
let lines: Vec<&str> = text.lines().collect();
// We need an owned replacement; rebuild from scratch.
let owned: Vec<String> = lines
let lines: Vec<&str> = text.lines().collect();
if let Some(pos) = lines.iter().position(|l| l.trim_start().starts_with(key)) {
let mut out = lines
.iter()
.enumerate()
.map(|(i, l)| {
if i == pos { replacement.clone() } else { l.to_string() }
})
.collect();
return owned.join("\n");
.map(|(i, l)| if i == pos { replacement.as_str() } else { l })
.collect::<Vec<_>>()
.join("\n");
out.push('\n');
return out;
}
// Key absent — append.
if !text.ends_with('\n') { text.push('\n'); }
text.push_str(&replacement);
text.push('\n');
text
let mut out = text;
if !out.ends_with('\n') { out.push('\n'); }
out.push_str(&replacement);
out.push('\n');
out
}
/// Resolve the Suwayomi data directory.
///
/// - Linux: $XDG_DATA_HOME/moku/tachidesk (matches Flatpak path)
/// - macOS: ~/Library/Application Support/dev.moku.app/tachidesk
fn suwayomi_data_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
.join("moku\\tachidesk")
}
#[cfg(target_os = "macos")]
{
dirs::data_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("~")))
.join("dev.moku.app/tachidesk")
}
#[cfg(not(target_os = "macos"))]
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let base = std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp"))
});
.unwrap_or_else(|_| dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp")));
base.join("moku/tachidesk")
}
}
/// Everything needed to spawn the server process.
struct ServerInvocation {
/// Path to the executable (javaw.exe on Windows, the sidecar script on macOS/Linux).
bin: std::ffi::OsString,
/// Extra args prepended before the Suwayomi rootDir flag.
/// On Windows: ["-jar", "<path-to-jar>"]
/// Elsewhere: []
bin: std::ffi::OsString,
prefix_args: Vec<String>,
/// Working directory for the child process.
/// On Windows this must be the bundle folder so javaw can find the JRE and jar.
/// Elsewhere: None (inherit).
working_dir: Option<PathBuf>,
}
/// Resolve the server binary path.
///
/// If the frontend passes a non-empty `binary` string (user override in
/// Settings) we always use that — on Linux this is the nixpkgs/Flatpak path.
///
/// Otherwise we look for the Tauri-bundled sidecar inside the resource dir
/// and, on Windows, build the javaw + jar invocation from the suwayomi-bundle.
fn resolve_server_binary(
binary: &str,
app: &tauri::AppHandle,
) -> Result<ServerInvocation, String> {
) -> Result<ServerInvocation, SpawnError> {
if !binary.trim().is_empty() {
return Ok(ServerInvocation {
bin: std::ffi::OsString::from(binary),
@@ -233,18 +201,17 @@ fn resolve_server_binary(
let resource_dir = app
.path()
.resource_dir()
.map_err(|e| format!("Could not locate resource dir: {e}"))?;
.map_err(|e| SpawnError::SpawnFailed(format!("Could not locate resource dir: {e}")))?;
// ── Windows: invoke the bundled javaw.exe with -jar Suwayomi-Launcher.jar ──
#[cfg(target_os = "windows")]
{
let sidecar = resource_dir.join("suwayomi-server-x86_64-pc-windows-msvc.exe");
let bundle_dir = resource_dir.join("suwayomi-bundle");
let jar = bundle_dir.join("Suwayomi-Launcher.jar");
let javaw = bundle_dir.join("jre").join("bin").join("javaw.exe");
let jar = bundle_dir.join("Suwayomi-Launcher.jar");
if sidecar.exists() && jar.exists() {
if javaw.exists() && jar.exists() {
return Ok(ServerInvocation {
bin: sidecar.into_os_string(),
bin: javaw.into_os_string(),
prefix_args: vec![
"-jar".to_string(),
jar.to_string_lossy().into_owned(),
@@ -252,16 +219,22 @@ fn resolve_server_binary(
working_dir: Some(bundle_dir),
});
}
return Err(SpawnError::NotConfigured(
"No bundled server found. Set the server path in Settings.".to_string(),
));
}
// ── macOS / Linux: sidecar script is self-contained ──
#[cfg(target_os = "macos")]
let candidates = [
"suwayomi-server-aarch64-apple-darwin",
"suwayomi-server-x86_64-apple-darwin",
// plain name as a dev/Linux fallback
"suwayomi-server",
];
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
let candidates = ["suwayomi-server"];
#[cfg(not(target_os = "windows"))]
for name in &candidates {
let p = resource_dir.join(name);
if p.exists() {
@@ -273,58 +246,83 @@ fn resolve_server_binary(
}
}
Err("Suwayomi server binary not found. Please set the path in Settings.".to_string())
// Fall back to PATH — covers Nix, distro packages, and any system install.
{
#[cfg(target_os = "windows")]
let which_cmd = "where";
#[cfg(not(target_os = "windows"))]
let which_cmd = "which";
for name in &["tachidesk-server", "suwayomi-server"] {
if std::process::Command::new(which_cmd)
.arg(name)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
{
return Ok(ServerInvocation {
bin: std::ffi::OsString::from(name),
prefix_args: vec![],
working_dir: None,
});
}
}
}
Err(SpawnError::NotConfigured(
"Server binary not found. Set the path in Settings.".to_string(),
))
}
#[tauri::command]
fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), String> {
let state = app.state::<ServerState>();
fn spawn_server(binary: String, app: tauri::AppHandle) -> Result<(), SpawnError> {
{
let state = app.state::<ServerState>();
let guard = state.0.lock().unwrap();
if guard.is_some() {
println!("Server already running, skipping spawn.");
return Ok(());
}
}
// Seed server.conf before launching so Suwayomi starts in headless mode.
let data_dir = suwayomi_data_dir();
seed_server_conf(&data_dir);
let invocation = resolve_server_binary(&binary, &app)?;
let shell = app.shell();
let bin_display = invocation.bin.clone();
let rootdir_flag = format!(
"-Dsuwayomi.tachidesk.config.server.rootDir={}",
data_dir.to_string_lossy()
);
// Build the full arg list: prefix_args (e.g. -jar foo.jar) + rootDir flag.
let args: Vec<String> = invocation.prefix_args.into_iter().chain(std::iter::once(rootdir_flag)).collect();
let args: Vec<String> = invocation.prefix_args
.into_iter()
.chain(std::iter::once(rootdir_flag))
.collect();
// On Windows, set the working directory to the bundle folder so javaw.exe
// can resolve the JRE and jar relative paths correctly.
let cmd = shell
let cmd = app.shell()
.command(&invocation.bin)
.env("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true")
.args(&args)
.current_dir(invocation.working_dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default()));
.current_dir(
invocation.working_dir
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
);
match cmd.spawn() {
Ok((_rx, child)) => {
println!("Spawned server: {:?}", invocation.bin);
let mut guard = state.0.lock().unwrap();
*guard = Some(child);
println!("Spawned server: {:?}", bin_display);
let state = app.state::<ServerState>();
*state.0.lock().unwrap() = Some(child);
Ok(())
}
Err(e) => {
eprintln!("Failed to spawn {:?}: {}", invocation.bin, e);
Err(e.to_string())
eprintln!("Failed to spawn {:?}: {}", bin_display, e);
Err(SpawnError::SpawnFailed(e.to_string()))
}
}
}
#[tauri::command]
fn kill_server(app: tauri::AppHandle) -> Result<(), String> {
kill_tachidesk(&app);
@@ -350,4 +348,4 @@ pub fn run() {
})
.run(tauri::generate_context!())
.expect("error while running moku");
}
}