mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
UI changes to update and support auto updating (#6075)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
919f0ade99
commit
256d1a86d2
Generated
+170
@@ -75,6 +75,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -899,6 +908,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -1253,6 +1273,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -2467,6 +2497,12 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -2762,6 +2798,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-pdf-kit"
|
||||
version = "0.3.2"
|
||||
@@ -2874,6 +2922,20 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3686,15 +3748,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -3863,6 +3930,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
@@ -4429,6 +4523,7 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tiny_http",
|
||||
"tokio",
|
||||
@@ -4640,6 +4735,17 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4979,6 +5085,39 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.2",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.4.1"
|
||||
@@ -5928,6 +6067,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
@@ -6625,6 +6773,16 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
@@ -6789,6 +6947,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
@@ -35,6 +35,7 @@ tauri-plugin-store = "2.4.2"
|
||||
tauri-plugin-opener = "2.5.3"
|
||||
tauri-plugin-deep-link = "2.4.6"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-window-state = "2.2.1"
|
||||
keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] }
|
||||
tokio = { version = "1.50", features = ["time", "sync"] }
|
||||
|
||||
@@ -58,6 +58,11 @@
|
||||
"notification:allow-is-permission-granted",
|
||||
"window-state:allow-filename",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state"
|
||||
"window-state:allow-save-window-state",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-install"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,14 +6,37 @@ use std::path::PathBuf;
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ProvisioningConfig<'a> {
|
||||
server_url: &'a str,
|
||||
lock_connection_mode: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_url: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
lock_connection_mode: Option<bool>,
|
||||
/// Optional headless-install update policy.
|
||||
/// One of `"prompt"` (default), `"auto"`, or `"disabled"`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
update_mode: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn parse_bool(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "y"
|
||||
)
|
||||
}
|
||||
|
||||
/// Normalise the `--update-mode` argument into the lowercase tokens the app
|
||||
/// understands. Empty / whitespace values are treated as "not supplied" so
|
||||
/// MSI installs that don't pass STIRLING_UPDATE_MODE behave identically to
|
||||
/// earlier builds.
|
||||
fn parse_update_mode(value: &str) -> Result<Option<&'static str>, String> {
|
||||
match value.trim().to_lowercase().as_str() {
|
||||
"1" | "true" | "yes" | "y" => true,
|
||||
_ => false,
|
||||
"" => Ok(None),
|
||||
"prompt" => Ok(Some("prompt")),
|
||||
"auto" => Ok(Some("auto")),
|
||||
"disabled" | "off" | "none" => Ok(Some("disabled")),
|
||||
other => Err(format!(
|
||||
"Invalid --update-mode value '{}': expected prompt, auto, or disabled",
|
||||
other
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +44,7 @@ fn main() -> Result<(), String> {
|
||||
let mut output: Option<PathBuf> = None;
|
||||
let mut url: Option<String> = None;
|
||||
let mut lock_value: Option<String> = None;
|
||||
let mut update_mode_arg: Option<String> = None;
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
@@ -43,6 +67,12 @@ fn main() -> Result<(), String> {
|
||||
.ok_or_else(|| "--lock requires a value".to_string())?;
|
||||
lock_value = Some(value);
|
||||
}
|
||||
"--update-mode" => {
|
||||
let value = args
|
||||
.next()
|
||||
.ok_or_else(|| "--update-mode requires a value".to_string())?;
|
||||
update_mode_arg = Some(value);
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unknown argument: {}", arg));
|
||||
}
|
||||
@@ -50,11 +80,28 @@ fn main() -> Result<(), String> {
|
||||
}
|
||||
|
||||
let output = output.ok_or_else(|| "Missing --output".to_string())?;
|
||||
|
||||
let url = url
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| "Missing --url".to_string())?;
|
||||
let lock = lock_value.as_deref().map(parse_bool).unwrap_or(false);
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
let update_mode = update_mode_arg
|
||||
.as_deref()
|
||||
.map(parse_update_mode)
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
// Nothing to write — avoid clobbering an existing provisioning file when
|
||||
// the MSI is invoked without any of STIRLING_SERVER_URL / STIRLING_UPDATE_MODE.
|
||||
if url.is_none() && update_mode.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lock = if url.is_some() {
|
||||
Some(lock_value.as_deref().map(parse_bool).unwrap_or(false))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(parent) = output.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
@@ -62,8 +109,9 @@ fn main() -> Result<(), String> {
|
||||
}
|
||||
|
||||
let config = ProvisioningConfig {
|
||||
server_url: url.as_str(),
|
||||
server_url: url.as_deref(),
|
||||
lock_connection_mode: lock,
|
||||
update_mode,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&config)
|
||||
|
||||
@@ -15,8 +15,48 @@ 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";
|
||||
pub(crate) const UPDATE_MODE_KEY: &str = "update_mode";
|
||||
/// When `true` the update mode was written by a provisioning file and cannot
|
||||
/// be changed from the UI. Only another provisioning file (from MDM) can
|
||||
/// override it. We track this separately from `lock_connection_mode` because
|
||||
/// an admin may want to lock updates without locking the connection URL,
|
||||
/// or vice versa.
|
||||
pub(crate) const UPDATE_MODE_LOCKED_KEY: &str = "update_mode_locked";
|
||||
const PROVISIONING_FILE_NAME: &str = "stirling-provisioning.json";
|
||||
|
||||
/// How the desktop auto-updater should behave on startup.
|
||||
///
|
||||
/// * `Prompt` – default. Show the update popup when a new version is available
|
||||
/// and let the user decide whether to install.
|
||||
/// * `Auto` – silently download and install updates on startup, then restart.
|
||||
/// Intended for managed deployments (Intune/MDM) where the user
|
||||
/// cannot (or should not) be prompted.
|
||||
/// * `Disabled` – never check for updates, never show the update UI. Administrators
|
||||
/// are expected to push updates through their normal packaging flow.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum UpdateMode {
|
||||
Prompt,
|
||||
Auto,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl Default for UpdateMode {
|
||||
fn default() -> Self {
|
||||
UpdateMode::Prompt
|
||||
}
|
||||
}
|
||||
|
||||
/// Current update mode plus whether the UI is allowed to change it. Returned
|
||||
/// by [`get_update_mode`] so the settings page can show a "managed by
|
||||
/// administrator" hint instead of silently ignoring clicks.
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateModeInfo {
|
||||
pub mode: UpdateMode,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
pub mode: ConnectionMode,
|
||||
@@ -142,6 +182,9 @@ pub async fn set_connection_mode(
|
||||
struct ProvisioningConfig {
|
||||
server_url: Option<String>,
|
||||
lock_connection_mode: Option<bool>,
|
||||
/// Optional headless-install update policy (`"prompt"`, `"auto"`, `"disabled"`).
|
||||
/// When omitted the existing stored mode is left unchanged.
|
||||
update_mode: Option<UpdateMode>,
|
||||
}
|
||||
|
||||
fn provisioning_file_paths() -> Vec<PathBuf> {
|
||||
@@ -155,6 +198,23 @@ fn provisioning_file_paths() -> Vec<PathBuf> {
|
||||
paths
|
||||
}
|
||||
|
||||
/// A provisioning file should only lock the update-mode UI when it was placed
|
||||
/// somewhere that requires administrator rights to write to — i.e. the
|
||||
/// system-wide provisioning dir written by MSI/Intune. A file in the per-user
|
||||
/// `app_data_dir()` is just a user dropping JSON in their own profile; locking
|
||||
/// the UI based on that would let any local user permanently disable the
|
||||
/// Settings selector for themselves with no way back, because the file is
|
||||
/// deleted after apply but the lock flag persists in the store.
|
||||
pub(crate) fn provisioning_path_is_admin_owned(
|
||||
provisioning_path: &std::path::Path,
|
||||
system_dir: Option<&std::path::Path>,
|
||||
) -> bool {
|
||||
match system_dir {
|
||||
Some(dir) => provisioning_path.starts_with(dir),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_provisioning_if_present(app_handle: &AppHandle) -> Result<(), String> {
|
||||
let provisioning_paths = provisioning_file_paths();
|
||||
let provisioning_path = provisioning_paths
|
||||
@@ -181,8 +241,10 @@ pub fn apply_provisioning_if_present(app_handle: &AppHandle) -> Result<(), Strin
|
||||
.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());
|
||||
if server_url.is_none() && parsed.update_mode.is_none() {
|
||||
add_log(
|
||||
"⚠️ Provisioning file has neither serverUrl nor updateMode; skipping apply".to_string(),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -192,36 +254,68 @@ pub fn apply_provisioning_if_present(app_handle: &AppHandle) -> Result<(), Strin
|
||||
.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))?,
|
||||
);
|
||||
// Apply server URL / connection settings only when a URL was supplied — a
|
||||
// provisioning file containing just `updateMode` should be allowed to configure
|
||||
// the headless update policy without forcing self-hosted mode.
|
||||
let server_config = if let Some(url) = server_url {
|
||||
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(),
|
||||
let cfg = ServerConfig { url };
|
||||
store.set(
|
||||
SERVER_CONFIG_KEY,
|
||||
serde_json::to_value(&cfg)
|
||||
.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));
|
||||
Some(cfg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
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));
|
||||
if let Some(mode) = parsed.update_mode {
|
||||
store.set(
|
||||
UPDATE_MODE_KEY,
|
||||
serde_json::to_value(&mode)
|
||||
.map_err(|e| format!("Failed to serialize update mode: {}", e))?,
|
||||
);
|
||||
// Only lock the UI when the provisioning file came from a path that
|
||||
// requires admin rights to write — i.e. the system provisioning dir
|
||||
// populated by MSI/Intune. A user dropping a file in their own
|
||||
// `app_data_dir` must NOT lock themselves out of the Settings
|
||||
// selector permanently (the file is deleted after apply, but the
|
||||
// lock flag persists in the store).
|
||||
let system_dir = system_provisioning_dir();
|
||||
let locked = provisioning_path_is_admin_owned(
|
||||
&provisioning_path,
|
||||
system_dir.as_deref(),
|
||||
);
|
||||
store.set(UPDATE_MODE_LOCKED_KEY, serde_json::json!(locked));
|
||||
add_log(format!(
|
||||
"🧩 Provisioning set update mode to {:?} (locked={})",
|
||||
mode, locked
|
||||
));
|
||||
}
|
||||
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save store: {}", e))?;
|
||||
|
||||
if let Ok(mut conn_state) = app_handle.state::<AppConnectionState>().0.lock() {
|
||||
if let (Some(cfg), Ok(mut conn_state)) =
|
||||
(server_config.as_ref(), app_handle.state::<AppConnectionState>().0.lock())
|
||||
{
|
||||
conn_state.mode = ConnectionMode::SelfHosted;
|
||||
conn_state.server_config = Some(server_config);
|
||||
conn_state.server_config = Some(cfg.clone());
|
||||
conn_state.lock_connection_mode = lock_flag;
|
||||
}
|
||||
|
||||
@@ -255,6 +349,78 @@ pub async fn is_first_launch(app_handle: AppHandle) -> Result<bool, String> {
|
||||
Ok(!setup_completed)
|
||||
}
|
||||
|
||||
/// Read the configured update mode from the tauri store.
|
||||
///
|
||||
/// Returns [`UpdateMode::Prompt`] when the store is unavailable or no mode
|
||||
/// has been set — the prompt-the-user flow is the safe default for normal,
|
||||
/// non-managed installs.
|
||||
pub(crate) fn read_update_mode(app_handle: &AppHandle) -> UpdateMode {
|
||||
read_update_mode_info(app_handle).mode
|
||||
}
|
||||
|
||||
/// Read the configured update mode AND whether it's locked by provisioning.
|
||||
pub(crate) fn read_update_mode_info(app_handle: &AppHandle) -> UpdateModeInfo {
|
||||
match app_handle.store(STORE_FILE) {
|
||||
Ok(store) => {
|
||||
let mode = store
|
||||
.get(UPDATE_MODE_KEY)
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
let locked = store
|
||||
.get(UPDATE_MODE_LOCKED_KEY)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
UpdateModeInfo { mode, locked }
|
||||
}
|
||||
Err(_) => UpdateModeInfo {
|
||||
mode: UpdateMode::default(),
|
||||
locked: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_update_mode(app_handle: AppHandle) -> Result<UpdateModeInfo, String> {
|
||||
Ok(read_update_mode_info(&app_handle))
|
||||
}
|
||||
|
||||
/// Update the stored update mode from the UI.
|
||||
///
|
||||
/// Refuses to overwrite a provisioned (locked) value so an MDM-managed
|
||||
/// deployment can't be subverted by a user clicking in Settings.
|
||||
#[tauri::command]
|
||||
pub async fn set_update_mode(
|
||||
app_handle: AppHandle,
|
||||
mode: UpdateMode,
|
||||
) -> Result<(), String> {
|
||||
let store = app_handle
|
||||
.store(STORE_FILE)
|
||||
.map_err(|e| format!("Failed to access store: {}", e))?;
|
||||
|
||||
let locked = store
|
||||
.get(UPDATE_MODE_LOCKED_KEY)
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
if locked {
|
||||
add_log(format!(
|
||||
"⚠️ set_update_mode({:?}) rejected — mode is locked by provisioning",
|
||||
mode
|
||||
));
|
||||
return Err("Update mode is locked by your administrator".to_string());
|
||||
}
|
||||
|
||||
store.set(
|
||||
UPDATE_MODE_KEY,
|
||||
serde_json::to_value(&mode)
|
||||
.map_err(|e| format!("Failed to serialize update mode: {}", e))?,
|
||||
);
|
||||
store
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save store: {}", e))?;
|
||||
add_log(format!("⚙️ User set update mode to {:?}", mode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reset_setup_completion(app_handle: AppHandle) -> Result<(), String> {
|
||||
log::info!("Resetting setup completion flag");
|
||||
@@ -273,3 +439,105 @@ pub async fn reset_setup_completion(app_handle: AppHandle) -> Result<(), String>
|
||||
log::info!("Setup completion flag reset successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Windows-path tests are cfg-gated because `Path::starts_with` is
|
||||
// component-wise: on Linux, `"C:\\foo\\bar"` is a SINGLE path component
|
||||
// (since backslash is a literal character there, not a separator) so
|
||||
// the prefix never matches the way it would on Windows.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn user_app_data_provisioning_does_not_lock_ui() {
|
||||
// A user dropping a provisioning file in their own roaming AppData
|
||||
// (per-user, user-writable) must NOT permanently lock the update-mode
|
||||
// selector. The file is deleted after apply but the lock flag would
|
||||
// persist in the store, locking the user out with no way back.
|
||||
let user_path = PathBuf::from(
|
||||
"C:\\Users\\alice\\AppData\\Roaming\\Stirling-PDF\\stirling-provisioning.json",
|
||||
);
|
||||
let system_dir = PathBuf::from("C:\\ProgramData\\Stirling-PDF");
|
||||
|
||||
assert!(!provisioning_path_is_admin_owned(
|
||||
&user_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn system_provisioning_dir_does_lock_ui() {
|
||||
// A provisioning file in ProgramData\Stirling-PDF (or /Library, /etc)
|
||||
// requires admin/root rights to write — those locations are how MSI
|
||||
// and Intune deliver policy — so locking the UI here is correct.
|
||||
let system_path = PathBuf::from(
|
||||
"C:\\ProgramData\\Stirling-PDF\\stirling-provisioning.json",
|
||||
);
|
||||
let system_dir = PathBuf::from("C:\\ProgramData\\Stirling-PDF");
|
||||
|
||||
assert!(provisioning_path_is_admin_owned(
|
||||
&system_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_etc_provisioning_does_lock_ui() {
|
||||
let system_path = PathBuf::from("/etc/stirling-pdf/stirling-provisioning.json");
|
||||
let system_dir = PathBuf::from("/etc/stirling-pdf");
|
||||
assert!(provisioning_path_is_admin_owned(
|
||||
&system_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_home_config_does_not_lock_ui() {
|
||||
let user_path =
|
||||
PathBuf::from("/home/alice/.config/Stirling-PDF/stirling-provisioning.json");
|
||||
let system_dir = PathBuf::from("/etc/stirling-pdf");
|
||||
assert!(!provisioning_path_is_admin_owned(
|
||||
&user_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_library_provisioning_does_lock_ui() {
|
||||
let system_path = PathBuf::from(
|
||||
"/Library/Application Support/Stirling-PDF/stirling-provisioning.json",
|
||||
);
|
||||
let system_dir =
|
||||
PathBuf::from("/Library/Application Support/Stirling-PDF");
|
||||
assert!(provisioning_path_is_admin_owned(
|
||||
&system_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn macos_user_library_does_not_lock_ui() {
|
||||
let user_path = PathBuf::from(
|
||||
"/Users/alice/Library/Application Support/Stirling-PDF/stirling-provisioning.json",
|
||||
);
|
||||
let system_dir =
|
||||
PathBuf::from("/Library/Application Support/Stirling-PDF");
|
||||
assert!(!provisioning_path_is_admin_owned(
|
||||
&user_path,
|
||||
Some(&system_dir),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_system_dir_means_no_lock() {
|
||||
// Defensive: when the platform has no defined system_provisioning_dir,
|
||||
// refuse to lock — the user-AppData file is the only thing we'd be
|
||||
// matching against, and that's the case we explicitly want to leave
|
||||
// unlocked.
|
||||
let user_path = PathBuf::from("/home/alice/.config/Stirling-PDF/stirling-provisioning.json");
|
||||
assert!(!provisioning_path_is_admin_owned(&user_path, None));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod auth;
|
||||
pub mod default_app;
|
||||
pub mod platform;
|
||||
pub mod print;
|
||||
pub mod updater;
|
||||
pub mod window;
|
||||
|
||||
pub use backend::{cleanup_backend, get_backend_port, start_backend};
|
||||
@@ -19,9 +20,11 @@ pub use window::{
|
||||
};
|
||||
pub use connection::{
|
||||
get_connection_config,
|
||||
get_update_mode,
|
||||
is_first_launch,
|
||||
reset_setup_completion,
|
||||
set_connection_mode,
|
||||
set_update_mode,
|
||||
};
|
||||
pub use auth::{
|
||||
clear_auth_token,
|
||||
@@ -39,3 +42,7 @@ pub use auth::{
|
||||
pub use default_app::{is_default_pdf_handler, set_as_default_pdf_handler};
|
||||
pub use platform::get_desktop_os;
|
||||
pub use print::print_pdf_file_native;
|
||||
pub use updater::{
|
||||
can_install_updates, check_for_update, download_and_install_update, get_app_version,
|
||||
restart_app,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
use crate::commands::connection::{read_update_mode, UpdateMode};
|
||||
use crate::utils::add_log;
|
||||
|
||||
/// Information about an available update.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateInfo {
|
||||
/// The new version string (e.g. "2.8.0").
|
||||
pub version: String,
|
||||
/// The currently installed version string.
|
||||
pub current_version: String,
|
||||
/// Release notes / changelog, if provided by the update endpoint.
|
||||
pub release_notes: Option<String>,
|
||||
}
|
||||
|
||||
/// Progress payload emitted as `update-download-progress` events during download.
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProgress {
|
||||
/// Total bytes downloaded so far.
|
||||
pub downloaded: u64,
|
||||
/// Total bytes to download, if known.
|
||||
pub total: Option<u64>,
|
||||
/// Download percentage (0–100). Zero when total is unknown.
|
||||
pub percent: f64,
|
||||
}
|
||||
|
||||
/// Check whether a newer version is available.
|
||||
///
|
||||
/// Returns `None` when the app is up-to-date or when the check cannot be
|
||||
/// performed (e.g. no network, endpoint misconfigured). Errors are logged
|
||||
/// internally so callers do not need to surface them to the user.
|
||||
#[tauri::command]
|
||||
pub async fn check_for_update(app: AppHandle) -> Result<Option<UpdateInfo>, String> {
|
||||
// Managed deployments may have updates disabled — never hit the endpoint in
|
||||
// that case so there's no network traffic and no way for the UI to discover
|
||||
// an update that would go unused.
|
||||
if read_update_mode(&app) == UpdateMode::Disabled {
|
||||
add_log("🔕 Update check skipped — update mode is disabled".to_string());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
add_log("🔍 Checking for updates...".to_string());
|
||||
|
||||
let current_version = app.package_info().version.to_string();
|
||||
|
||||
let updater = match app.updater() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
add_log(format!("⚠️ Updater not available (plugin not configured?): {}", e));
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
add_log(format!("✅ Update available: {} → {}", current_version, update.version));
|
||||
Ok(Some(UpdateInfo {
|
||||
version: update.version.clone(),
|
||||
current_version,
|
||||
release_notes: update.body.clone(),
|
||||
}))
|
||||
}
|
||||
Ok(None) => {
|
||||
add_log("✅ App is up to date".to_string());
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => {
|
||||
// Surface as Ok(None) so the frontend never shows a noisy error banner
|
||||
// on a transient network failure during startup.
|
||||
add_log(format!("⚠️ Update check failed: {}", e));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and install the latest available update.
|
||||
///
|
||||
/// Emits three events on the `AppHandle`:
|
||||
/// * `update-download-progress` – [`UpdateProgress`] payload, sent for each
|
||||
/// received chunk.
|
||||
/// * `update-download-finished` – empty payload once the download is complete
|
||||
/// and the installer has been written to disk.
|
||||
/// * `update-ready-to-restart` – empty payload once the in-process install
|
||||
/// step has completed and the app can be safely restarted.
|
||||
///
|
||||
/// Returns `Err` if the update cannot be found, downloaded, or installed so
|
||||
/// the frontend can fall back to opening the download page.
|
||||
#[tauri::command]
|
||||
pub async fn download_and_install_update(app: AppHandle) -> Result<(), String> {
|
||||
add_log("📥 Starting update download and install...".to_string());
|
||||
|
||||
let updater = app.updater().map_err(|e| {
|
||||
let msg = format!("Updater plugin unavailable: {}", e);
|
||||
add_log(format!("⚠️ {}", msg));
|
||||
msg
|
||||
})?;
|
||||
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "No update is currently available".to_string())?;
|
||||
|
||||
add_log(format!("📥 Downloading version {}...", update.version));
|
||||
|
||||
let downloaded = Arc::new(Mutex::new(0u64));
|
||||
let app_progress = app.clone();
|
||||
let app_finish = app.clone();
|
||||
|
||||
update
|
||||
.download_and_install(
|
||||
move |chunk_length, content_length| {
|
||||
let mut dl = downloaded.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*dl += chunk_length as u64;
|
||||
let current = *dl;
|
||||
let percent = content_length
|
||||
.map(|total| (current as f64 / total as f64) * 100.0)
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let _ = app_progress.emit(
|
||||
"update-download-progress",
|
||||
UpdateProgress {
|
||||
downloaded: current,
|
||||
total: content_length,
|
||||
percent,
|
||||
},
|
||||
);
|
||||
},
|
||||
move || {
|
||||
add_log("✅ Download finished, applying update...".to_string());
|
||||
let _ = app_finish.emit("update-download-finished", ());
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = format!("Update installation failed: {}", e);
|
||||
add_log(format!("❌ {}", msg));
|
||||
msg
|
||||
})?;
|
||||
|
||||
add_log("✅ Update installed — waiting for restart".to_string());
|
||||
let _ = app.emit("update-ready-to-restart", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restart the application to apply an already-installed update.
|
||||
///
|
||||
/// This function never returns — the process is replaced by the new version.
|
||||
#[tauri::command]
|
||||
pub fn restart_app(app: AppHandle) {
|
||||
add_log("🔄 Restarting app to apply update...".to_string());
|
||||
app.restart();
|
||||
}
|
||||
|
||||
/// Return the currently running application version string.
|
||||
#[tauri::command]
|
||||
pub fn get_app_version(app: AppHandle) -> String {
|
||||
app.package_info().version.to_string()
|
||||
}
|
||||
|
||||
/// Result of [`can_install_updates`] — does this process have the permissions
|
||||
/// needed to run the Tauri updater's MSI/NSIS installer without triggering
|
||||
/// UAC elevation (or another blocker)?
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CanInstallResult {
|
||||
/// `true` when the install directory is writable from the current
|
||||
/// process — the strongest signal that msiexec will succeed without
|
||||
/// needing elevation. `false` means auto-install is very likely to
|
||||
/// trigger a UAC prompt (or be blocked entirely) and should be
|
||||
/// skipped in silent/auto mode.
|
||||
pub can_install: bool,
|
||||
/// Machine-readable reason when `can_install` is `false`. One of
|
||||
/// `"install_dir_not_writable"`, `"install_dir_unknown"`. `None` when
|
||||
/// `can_install` is `true`.
|
||||
pub reason: Option<String>,
|
||||
/// Path we probed so the frontend can include it in the "contact your
|
||||
/// administrator" alert. Best-effort — may be empty on exotic layouts.
|
||||
pub install_dir: Option<String>,
|
||||
}
|
||||
|
||||
/// Check whether the Tauri updater is likely to be able to install a new
|
||||
/// version silently.
|
||||
///
|
||||
/// Tauri's Windows updater runs msiexec against the downloaded MSI, which on
|
||||
/// a per-machine install writes to `C:\Program Files\Stirling-PDF`. If the
|
||||
/// current user can write to that directory without elevation, msiexec will
|
||||
/// succeed silently (passive install mode). If they can't, the install
|
||||
/// triggers a UAC prompt that can't be auto-approved — and for a managed
|
||||
/// (Intune/MDM) deployment the user typically can't satisfy it at all.
|
||||
///
|
||||
/// We test this by writing (and immediately deleting) a zero-byte probe file
|
||||
/// next to the running executable. This avoids trying to reverse-engineer
|
||||
/// Windows token elevation state, which is surprisingly hard to do correctly
|
||||
/// on Windows when UAC filtered tokens are in play.
|
||||
///
|
||||
/// On macOS and Linux the probe-next-to-exe heuristic is misleading:
|
||||
/// `current_exe().parent()` on macOS is `Stirling-PDF.app/Contents/MacOS/`
|
||||
/// which is almost always user-writable even when `/Applications/` is not,
|
||||
/// so the probe returns a false positive. On Linux the AppImage/deb install
|
||||
/// story is different and not gated by this kind of check. We platform-gate
|
||||
/// to Windows and report `can_install: true` elsewhere so the existing
|
||||
/// interactive update flow runs unchanged.
|
||||
#[tauri::command]
|
||||
pub fn can_install_updates() -> CanInstallResult {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
return CanInstallResult {
|
||||
can_install: true,
|
||||
reason: None,
|
||||
install_dir: None,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
add_log(format!("⚠️ can_install_updates: current_exe failed: {}", e));
|
||||
return CanInstallResult {
|
||||
can_install: false,
|
||||
reason: Some("install_dir_unknown".to_string()),
|
||||
install_dir: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
let parent = match exe.parent() {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => {
|
||||
return CanInstallResult {
|
||||
can_install: false,
|
||||
reason: Some("install_dir_unknown".to_string()),
|
||||
install_dir: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let dir_display = parent.display().to_string();
|
||||
let probe = parent.join(".stirling-auto-update-probe.tmp");
|
||||
match std::fs::File::create(&probe) {
|
||||
Ok(_) => {
|
||||
let _ = std::fs::remove_file(&probe);
|
||||
add_log(format!("✅ can_install_updates: writable: {}", dir_display));
|
||||
CanInstallResult {
|
||||
can_install: true,
|
||||
reason: None,
|
||||
install_dir: Some(dir_display),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
add_log(format!(
|
||||
"⚠️ can_install_updates: install dir not writable ({}): {}",
|
||||
dir_display, e
|
||||
));
|
||||
CanInstallResult {
|
||||
can_install: false,
|
||||
reason: Some("install_dir_not_writable".to_string()),
|
||||
install_dir: Some(dir_display),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,16 @@ use commands::{
|
||||
set_connection_mode,
|
||||
set_as_default_pdf_handler,
|
||||
get_desktop_os,
|
||||
get_update_mode,
|
||||
print_pdf_file_native,
|
||||
set_update_mode,
|
||||
start_backend,
|
||||
start_oauth_login,
|
||||
can_install_updates,
|
||||
check_for_update,
|
||||
download_and_install_update,
|
||||
get_app_version,
|
||||
restart_app,
|
||||
target_window_label,
|
||||
MAIN_WINDOW_LABEL,
|
||||
};
|
||||
@@ -79,6 +86,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.manage(AppConnectionState::default())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
@@ -181,6 +189,13 @@ pub fn run() {
|
||||
start_oauth_login,
|
||||
get_desktop_os,
|
||||
print_pdf_file_native,
|
||||
can_install_updates,
|
||||
check_for_update,
|
||||
download_and_install_update,
|
||||
get_app_version,
|
||||
get_update_mode,
|
||||
set_update_mode,
|
||||
restart_app,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
||||
@@ -83,6 +83,16 @@
|
||||
"desktop": {
|
||||
"schemes": ["stirlingpdf"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/latest.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDZEREM2QTgwNjdGNzdCOUQKUldTZGUvZG5nR3JjYmZ4TlZ3YVpHTVIzd2hBT2c4c000RDNIN2tLZENEblE4ZXFhU3J5V0lQanEK",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
<Fragment>
|
||||
<Property Id="STIRLING_SERVER_URL" Secure="yes" />
|
||||
<Property Id="STIRLING_LOCK_CONNECTION" Secure="yes" />
|
||||
<!--
|
||||
STIRLING_UPDATE_MODE — headless auto-update policy for MDM/Intune deploys.
|
||||
Accepts "prompt" (default, user-interactive), "auto" (silent download+install on
|
||||
startup), or "disabled" (never check). Leaving it unset preserves whatever was
|
||||
configured on the previous install (or defaults to "prompt" on a fresh install).
|
||||
-->
|
||||
<Property Id="STIRLING_UPDATE_MODE" Secure="yes" />
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Directory Id="AppDataFolder" />
|
||||
@@ -64,20 +71,24 @@
|
||||
<CustomAction
|
||||
Id="SetWriteProvisioningFilePerUser"
|
||||
Property="WriteProvisioningFilePerUser"
|
||||
Value="--output "[AppDataFolder]Stirling-PDF\stirling-provisioning.json" --url "[STIRLING_SERVER_URL]" --lock "[STIRLING_LOCK_CONNECTION]""
|
||||
Value="--output "[AppDataFolder]Stirling-PDF\stirling-provisioning.json" --url "[STIRLING_SERVER_URL]" --lock "[STIRLING_LOCK_CONNECTION]" --update-mode "[STIRLING_UPDATE_MODE]""
|
||||
/>
|
||||
|
||||
<CustomAction
|
||||
Id="SetWriteProvisioningFileAllUsers"
|
||||
Property="WriteProvisioningFileAllUsers"
|
||||
Value="--output "[CommonAppDataFolder]Stirling-PDF\stirling-provisioning.json" --url "[STIRLING_SERVER_URL]" --lock "[STIRLING_LOCK_CONNECTION]""
|
||||
Value="--output "[CommonAppDataFolder]Stirling-PDF\stirling-provisioning.json" --url "[STIRLING_SERVER_URL]" --lock "[STIRLING_LOCK_CONNECTION]" --update-mode "[STIRLING_UPDATE_MODE]""
|
||||
/>
|
||||
|
||||
<!--
|
||||
Run the provisioner when EITHER STIRLING_SERVER_URL or STIRLING_UPDATE_MODE is set.
|
||||
The provisioner is a no-op if both values end up empty, so passing neither is safe.
|
||||
-->
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="SetWriteProvisioningFilePerUser" After="InstallFiles">STIRLING_SERVER_URL <> "" AND (NOT ALLUSERS OR ALLUSERS=0)</Custom>
|
||||
<Custom Action="WriteProvisioningFilePerUser" After="SetWriteProvisioningFilePerUser">STIRLING_SERVER_URL <> "" AND (NOT ALLUSERS OR ALLUSERS=0)</Custom>
|
||||
<Custom Action="SetWriteProvisioningFileAllUsers" After="InstallFiles">STIRLING_SERVER_URL <> "" AND (ALLUSERS=1 OR ALLUSERS=2)</Custom>
|
||||
<Custom Action="WriteProvisioningFileAllUsers" After="SetWriteProvisioningFileAllUsers">STIRLING_SERVER_URL <> "" AND (ALLUSERS=1 OR ALLUSERS=2)</Custom>
|
||||
<Custom Action="SetWriteProvisioningFilePerUser" After="InstallFiles">(STIRLING_SERVER_URL <> "" OR STIRLING_UPDATE_MODE <> "") AND (NOT ALLUSERS OR ALLUSERS=0)</Custom>
|
||||
<Custom Action="WriteProvisioningFilePerUser" After="SetWriteProvisioningFilePerUser">(STIRLING_SERVER_URL <> "" OR STIRLING_UPDATE_MODE <> "") AND (NOT ALLUSERS OR ALLUSERS=0)</Custom>
|
||||
<Custom Action="SetWriteProvisioningFileAllUsers" After="InstallFiles">(STIRLING_SERVER_URL <> "" OR STIRLING_UPDATE_MODE <> "") AND (ALLUSERS=1 OR ALLUSERS=2)</Custom>
|
||||
<Custom Action="WriteProvisioningFileAllUsers" After="SetWriteProvisioningFileAllUsers">(STIRLING_SERVER_URL <> "" OR STIRLING_UPDATE_MODE <> "") AND (ALLUSERS=1 OR ALLUSERS=2)</Custom>
|
||||
</InstallExecuteSequence>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
Reference in New Issue
Block a user