mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Attempt to Fix Nix/Flatpak (Testing)
This commit is contained in:
+103
-105
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user