Headless windows installer (#5664)

This commit is contained in:
Anthony Stirling
2026-02-06 18:06:01 +00:00
committed by GitHub
parent 94e517df3c
commit ba72a2a623
21 changed files with 557 additions and 34 deletions

View File

@@ -2,7 +2,7 @@ use tauri_plugin_shell::ShellExt;
use tauri::Manager;
use std::sync::Mutex;
use std::path::{Path, PathBuf};
use crate::utils::add_log;
use crate::utils::{add_log, app_data_dir};
use crate::state::connection_state::{AppConnectionState, ConnectionMode};
// Store backend process handle and port globally
@@ -168,16 +168,7 @@ fn copy_dir_recursive(src: &Path, dest: &Path) -> std::io::Result<()> {
// Create, configure and run the Java command to run Stirling-PDF JAR
fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &PathBuf) -> Result<(), String> {
// Get platform-specific application data directory for Tauri mode
let app_data_dir = if cfg!(target_os = "macos") {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join("Library").join("Application Support").join("Stirling-PDF")
} else if cfg!(target_os = "windows") {
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
PathBuf::from(appdata).join("Stirling-PDF")
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".config").join("Stirling-PDF")
};
let app_data_dir = app_data_dir();
// Create subdirectories for different purposes
let config_dir = app_data_dir.join("configs");

View File

@@ -3,19 +3,25 @@ use crate::state::connection_state::{
ConnectionMode,
ServerConfig,
};
use crate::utils::{add_log, app_data_dir, system_provisioning_dir};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, State};
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, State};
use tauri_plugin_store::StoreExt;
const STORE_FILE: &str = "connection.json";
const FIRST_LAUNCH_KEY: &str = "setup_completed";
const CONNECTION_MODE_KEY: &str = "connection_mode";
const SERVER_CONFIG_KEY: &str = "server_config";
const LOCK_CONNECTION_KEY: &str = "lock_connection_mode";
const PROVISIONING_FILE_NAME: &str = "stirling-provisioning.json";
#[derive(Debug, Serialize, Deserialize)]
pub struct ConnectionConfig {
pub mode: ConnectionMode,
pub server_config: Option<ServerConfig>,
pub lock_connection_mode: bool,
}
#[tauri::command]
@@ -37,15 +43,22 @@ pub async fn get_connection_config(
.get(SERVER_CONFIG_KEY)
.and_then(|v| serde_json::from_value(v.clone()).ok());
let lock_connection_mode = store
.get(LOCK_CONNECTION_KEY)
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Update in-memory state
if let Ok(mut conn_state) = state.0.lock() {
conn_state.mode = mode.clone();
conn_state.server_config = server_config.clone();
conn_state.lock_connection_mode = lock_connection_mode;
}
Ok(ConnectionConfig {
mode,
server_config,
lock_connection_mode,
})
}
@@ -55,6 +68,7 @@ pub async fn set_connection_mode(
state: State<'_, AppConnectionState>,
mode: ConnectionMode,
server_config: Option<ServerConfig>,
lock_connection_mode: Option<bool>,
) -> Result<(), String> {
log::info!("Setting connection mode: {:?}", mode);
@@ -62,6 +76,9 @@ pub async fn set_connection_mode(
if let Ok(mut conn_state) = state.0.lock() {
conn_state.mode = mode.clone();
conn_state.server_config = server_config.clone();
if let Some(lock) = lock_connection_mode {
conn_state.lock_connection_mode = lock;
}
}
// Save to store
@@ -84,6 +101,14 @@ pub async fn set_connection_mode(
store.delete(SERVER_CONFIG_KEY);
}
if let Some(lock) = lock_connection_mode {
store.set(
LOCK_CONNECTION_KEY,
serde_json::to_value(lock)
.map_err(|e| format!("Failed to serialize lock flag: {}", e))?,
);
}
// Mark setup as completed
store.set(FIRST_LAUNCH_KEY, serde_json::json!(true));
@@ -95,6 +120,109 @@ pub async fn set_connection_mode(
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProvisioningConfig {
server_url: Option<String>,
lock_connection_mode: Option<bool>,
}
fn provisioning_file_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
paths.push(app_data_dir().join(PROVISIONING_FILE_NAME));
if let Some(system_dir) = system_provisioning_dir() {
paths.push(system_dir.join(PROVISIONING_FILE_NAME));
}
paths
}
pub fn apply_provisioning_if_present(app_handle: &AppHandle) -> Result<(), String> {
let provisioning_paths = provisioning_file_paths();
let provisioning_path = provisioning_paths
.into_iter()
.find(|path| path.exists());
let provisioning_path = match provisioning_path {
Some(path) => path,
None => return Ok(()),
};
add_log(format!(
"🧩 Provisioning file detected: {}",
provisioning_path.display()
));
let raw = fs::read_to_string(&provisioning_path)
.map_err(|e| format!("Failed to read provisioning file: {}", e))?;
let parsed: ProvisioningConfig = serde_json::from_str(&raw)
.map_err(|e| format!("Failed to parse provisioning file: {}", e))?;
let server_url = parsed
.server_url
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if server_url.is_none() {
add_log("⚠️ Provisioning file missing serverUrl; skipping apply".to_string());
return Ok(());
}
let lock_flag = parsed.lock_connection_mode.unwrap_or(false);
let store = app_handle
.store(STORE_FILE)
.map_err(|e| format!("Failed to access store: {}", e))?;
store.set(
CONNECTION_MODE_KEY,
serde_json::to_value(&ConnectionMode::SelfHosted)
.map_err(|e| format!("Failed to serialize mode: {}", e))?,
);
let server_config = ServerConfig {
url: server_url.clone().unwrap(),
};
store.set(
SERVER_CONFIG_KEY,
serde_json::to_value(&server_config)
.map_err(|e| format!("Failed to serialize config: {}", e))?,
);
store.set(
LOCK_CONNECTION_KEY,
serde_json::to_value(lock_flag)
.map_err(|e| format!("Failed to serialize lock flag: {}", e))?,
);
store.set(FIRST_LAUNCH_KEY, serde_json::json!(true));
store
.save()
.map_err(|e| format!("Failed to save store: {}", e))?;
if let Ok(mut conn_state) = app_handle.state::<AppConnectionState>().0.lock() {
conn_state.mode = ConnectionMode::SelfHosted;
conn_state.server_config = Some(server_config);
conn_state.lock_connection_mode = lock_flag;
}
let user_app_data = app_data_dir();
if provisioning_path.starts_with(&user_app_data) {
match fs::remove_file(&provisioning_path) {
Ok(_) => add_log("✅ Provisioning file applied and removed".to_string()),
Err(err) => add_log(format!(
"⚠️ Provisioning applied but failed to remove file: {}",
err
)),
}
} else {
add_log(" Provisioning applied from system location; leaving file in place".to_string());
}
Ok(())
}
#[tauri::command]
pub async fn is_first_launch(app_handle: AppHandle) -> Result<bool, String> {

View File

@@ -29,6 +29,7 @@ use commands::{
start_backend,
start_oauth_login,
};
use commands::connection::apply_provisioning_if_present;
use state::connection_state::AppConnectionState;
use utils::{add_log, get_tauri_logs};
use tauri_plugin_deep_link::DeepLinkExt;
@@ -116,6 +117,10 @@ pub fn run() {
});
}
if let Err(err) = apply_provisioning_if_present(&app.handle()) {
add_log(format!("⚠️ Failed to apply provisioning file: {}", err));
}
// Start backend immediately, non-blocking
let app_handle = app.handle().clone();

View File

@@ -17,6 +17,7 @@ pub struct ServerConfig {
pub struct ConnectionState {
pub mode: ConnectionMode,
pub server_config: Option<ServerConfig>,
pub lock_connection_mode: bool,
}
impl Default for ConnectionState {
@@ -24,6 +25,7 @@ impl Default for ConnectionState {
Self {
mode: ConnectionMode::SaaS,
server_config: None,
lock_connection_mode: false,
}
}
}

View File

@@ -1,3 +1,5 @@
pub mod logging;
pub mod paths;
pub use logging::{add_log, get_tauri_logs};
pub use logging::{add_log, get_tauri_logs};
pub use paths::{app_data_dir, system_provisioning_dir};

View File

@@ -0,0 +1,31 @@
use std::path::PathBuf;
pub fn app_data_dir() -> PathBuf {
if cfg!(target_os = "macos") {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("Stirling-PDF")
} else if cfg!(target_os = "windows") {
let appdata = std::env::var("APPDATA")
.unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
PathBuf::from(appdata).join("Stirling-PDF")
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".config").join("Stirling-PDF")
}
}
pub fn system_provisioning_dir() -> Option<PathBuf> {
if cfg!(target_os = "windows") {
let program_data = std::env::var("PROGRAMDATA").ok()?;
Some(PathBuf::from(program_data).join("Stirling-PDF"))
} else if cfg!(target_os = "macos") {
Some(PathBuf::from("/Library").join("Application Support").join("Stirling-PDF"))
} else if cfg!(target_os = "linux") {
Some(PathBuf::from("/etc").join("stirling-pdf"))
} else {
None
}
}