diff --git a/Cargo.lock b/Cargo.lock
index 1bfa58d..cb90930 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -304,7 +304,7 @@ dependencies = [
"tungstenite 0.24.0",
"uuid",
"walkdir",
- "widestring",
+ "widestring 1.2.1",
"windows-sys 0.59.0",
"x509",
"x509-certificate",
@@ -750,6 +750,18 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "auto-launcher"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa871af263fe9f0150d234d1e265a1117e0348ff6409ffcced0d387bdf087ac9"
+dependencies = [
+ "dirs 6.0.0",
+ "thiserror 2.0.17",
+ "windows-registry",
+ "windows-result 0.3.4",
+]
+
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -2387,7 +2399,7 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
- "memoffset",
+ "memoffset 0.9.1",
"rustc_version",
]
@@ -4243,6 +4255,18 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "mac-notification-sys"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621"
+dependencies = [
+ "cc",
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.2",
+ "time",
+]
+
[[package]]
name = "mach2"
version = "0.4.3"
@@ -4305,6 +4329,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "memoffset"
version = "0.9.1"
@@ -4522,6 +4555,19 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+[[package]]
+name = "nix"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
+dependencies = [
+ "bitflags 1.3.2",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset 0.6.5",
+]
+
[[package]]
name = "nix"
version = "0.30.1"
@@ -4532,7 +4578,7 @@ dependencies = [
"cfg-if",
"cfg_aliases",
"libc",
- "memoffset",
+ "memoffset 0.9.1",
]
[[package]]
@@ -4560,6 +4606,20 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+[[package]]
+name = "notify-rust"
+version = "4.11.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400"
+dependencies = [
+ "futures-lite",
+ "log",
+ "mac-notification-sys",
+ "serde",
+ "tauri-winrt-notification",
+ "zbus",
+]
+
[[package]]
name = "ns-keyed-archive"
version = "0.1.4"
@@ -5580,7 +5640,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64 0.22.1",
"indexmap",
- "quick-xml",
+ "quick-xml 0.38.3",
"serde",
"time",
]
@@ -5658,6 +5718,7 @@ dependencies = [
name = "plumeimpactor"
version = "1.4.1"
dependencies = [
+ "auto-launcher",
"chrono",
"env_logger",
"futures",
@@ -5666,6 +5727,7 @@ dependencies = [
"idevice",
"image",
"log",
+ "notify-rust",
"open",
"plist",
"plume_core",
@@ -5674,6 +5736,7 @@ dependencies = [
"plume_utils",
"rfd",
"rustls 0.23.32",
+ "single-instance",
"thiserror 2.0.17",
"tokio",
"tray-icon",
@@ -5942,6 +6005,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+[[package]]
+name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quick-xml"
version = "0.38.3"
@@ -7031,6 +7103,19 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd19be0257552dd56d1bb6946f89f193c6e5b9f13cc9327c4bc84a357507c74"
+[[package]]
+name = "single-instance"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4637485391f8545c9d3dbf60f9d9aab27a90c789a700999677583bcb17c8795d"
+dependencies = [
+ "libc",
+ "nix 0.23.2",
+ "thiserror 1.0.69",
+ "widestring 0.4.3",
+ "winapi",
+]
+
[[package]]
name = "skrifa"
version = "0.37.0"
@@ -7433,6 +7518,18 @@ version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
+dependencies = [
+ "quick-xml 0.37.5",
+ "thiserror 2.0.17",
+ "windows 0.61.3",
+ "windows-version",
+]
+
[[package]]
name = "tempfile"
version = "3.22.0"
@@ -7989,7 +8086,7 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
dependencies = [
- "memoffset",
+ "memoffset 0.9.1",
"tempfile",
"winapi",
]
@@ -8405,7 +8502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [
"proc-macro2",
- "quick-xml",
+ "quick-xml 0.38.3",
"quote",
]
@@ -8622,6 +8719,12 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "widestring"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
+
[[package]]
name = "widestring"
version = "1.2.1"
@@ -8683,16 +8786,38 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections 0.2.0",
+ "windows-core 0.61.2",
+ "windows-future 0.2.1",
+ "windows-link 0.1.3",
+ "windows-numerics 0.2.0",
+]
+
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
- "windows-collections",
+ "windows-collections 0.3.2",
"windows-core 0.62.2",
- "windows-future",
- "windows-numerics",
+ "windows-future 0.3.2",
+ "windows-numerics 0.3.1",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
]
[[package]]
@@ -8717,6 +8842,19 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement 0.60.2",
+ "windows-interface 0.59.3",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -8730,6 +8868,17 @@ dependencies = [
"windows-strings 0.5.1",
]
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading 0.1.0",
+]
+
[[package]]
name = "windows-future"
version = "0.3.2"
@@ -8738,7 +8887,7 @@ checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
- "windows-threading",
+ "windows-threading 0.2.1",
]
[[package]]
@@ -8797,6 +8946,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
[[package]]
name = "windows-numerics"
version = "0.3.1"
@@ -8807,6 +8966,17 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
[[package]]
name = "windows-result"
version = "0.2.0"
@@ -8816,6 +8986,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -8835,6 +9014,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
[[package]]
name = "windows-strings"
version = "0.5.1"
@@ -8961,6 +9149,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
[[package]]
name = "windows-threading"
version = "0.2.1"
@@ -8970,6 +9167,15 @@ dependencies = [
"windows-link 0.2.1",
]
+[[package]]
+name = "windows-version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@@ -9232,11 +9438,11 @@ dependencies = [
[[package]]
name = "winres"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
+version = "0.1.11"
+source = "git+https://github.com/Nilstrieb/winres?branch=linking-flags#c839134b5f78d7dd0f5c8211ec6c7b675b0026fc"
dependencies = [
"toml 0.5.11",
+ "version_check",
]
[[package]]
@@ -9482,7 +9688,7 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
- "nix",
+ "nix 0.30.1",
"ordered-stream",
"serde",
"serde_repr",
diff --git a/README.md b/README.md
index 44573bd..27e6bf9 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# PlumeImpactor
+#
Impactor
[](https://github.com/khcrysalis/PlumeImpactor/releases)
[](https://github.com/khcrysalis/PlumeImpactor/releases)
@@ -11,24 +11,25 @@ Open-source, cross-platform, and feature rich iOS sideloading application. Suppo
[^2]: On Windows, [iTunes](https://support.apple.com/en-us/106372) must be downloaded so Impactor is able to use the drivers for interacting with Apple devices.
-|  |
-| :--------------------------------------------------------------------------------------------------: |
-| Screenshot of Impactor after importing [Feather](https://github.com/khcrysalis/Feather). |
+
### Features
- User friendly and clean UI.
- Supports Linux.
- Sign and sideload applications on iOS 9.0+ & Mac with your Apple ID.
- - Installing with AppSync is supported.
+ - Installing with [AppSync](https://github.com/akemin-dayo/AppSync) is supported.
- Installing with ipatool gotten ipa's is supported.
- Automatically disables updates from the App Store.
- Simple customization options for the app.
- Tweak support for advanced users, using [ElleKit](https://github.com/tealbathingsuit/ellekit) for injection.
- Supports injecting `.deb` and `.dylib` files.
- Supports adding `.framework`, `.bundle`, and `.appex` directories.
-- Generates P12 for SideStore/AltStore to use, similar to how Altserver works.
+ - Supports replacing Cydia Substrate with ElleKit for 26.0 compatibility.
+- Generates P12 for SideStore/AltStore to use, similar to Altserver.
- Automatically populate pairing files for apps like SideStore, Antrag, and Protokolle.
+- Comes with simple device utilities for retrusting/placing pairing file.
+- Export P12 for use with LiveContainer.
- Almost *proper* entitlement handling and can register app plugins.
- Able to request entitlements like `increased-memory-limit`, for emulators like MelonX or UTM.
@@ -50,6 +51,21 @@ Lastly, we do all of the necessary modifications we need to the app you're tryin
That's the entire gist of how this works! Of course its very short and brief, however feel free to look how it works since its open source :D
+### Pairing File
+
+Impactor also allows the user to generate a pairing file for applications to talk directly to the device remotely. This pairing file is device specific and will become invalid if you ever re-trust/update/reset.
+
+Supported apps:
+- `SideStore`
+- `Feather`
+- `SparseBox`
+- `LiveContainer + SideStore`
+- `Antrag`
+- `Protokolle`
+- `StikDebug`
+
+You can retrieve this file by either sideloading the supported app of your choice, or going to the `Utilities` page when a device is connected and press install for the supported app. Head over to the [downloads](https://github.com/khcrysalis/PlumeImpactor/releases).
+
## Structure
The project is seperated in multiple modules, all serve single or multiple uses depending on their importance.
@@ -67,7 +83,6 @@ The project is seperated in multiple modules, all serve single or multiple uses
Building is going to be a bit convoluted for each platform, each having their own unique specifications, but the best reference for building should be looking at how [GitHub actions](./.github/workflows/build.yml) does it.
-
You need:
- [Rust](https://rustup.rs/).
- [CMake](https://cmake.org/download/) (and a c++ compiler).
@@ -107,6 +122,16 @@ sudo dnf install clang-devel pkg-config gtk3-devel libpng-devel libjpeg-devel me
|
|
| _**"samara is cute" - Vendicated**_ |
+## Star History
+
+
+
+
+
+
+
+
+
## Acknowledgements
- [SAMSAM](https://github.com/khcrysalis) – The maker.
@@ -117,6 +142,10 @@ sudo dnf install clang-devel pkg-config gtk3-devel libpng-devel libjpeg-devel me
- [idevice](https://github.com/jkcoxson/idevice) – Used for communication with `installd`, specifically for sideloading the apps to your devices.
- [apple-codesign-rs](https://github.com/indygreg/apple-platform-rs) – Codesign alternative, modified and extended upon to work for Impactor.
+
+
+
+
## License
Project is licensed under the MIT license. You can see the full details of the license [here](https://github.com/khcrysalis/PlumeImpactor/blob/main/LICENSE). Some components may be licensed under different licenses, see their respective directories for details.
diff --git a/apps/plumeimpactor/Cargo.toml b/apps/plumeimpactor/Cargo.toml
index 8f92b0a..2dca710 100644
--- a/apps/plumeimpactor/Cargo.toml
+++ b/apps/plumeimpactor/Cargo.toml
@@ -29,12 +29,15 @@ tray-icon = { version = "0.21.2", default-features = false }
image = "0.25"
rfd = "0.16.0"
open = "5.3.3"
+notify-rust = "4.11.7"
+single-instance = "0.3.3"
+auto-launcher = "0.6.1"
[target.'cfg(target_os = "macos")'.dependencies]
plume_gestalt = { path = "../../crates/plume_gestalt" }
-[target.'cfg(target_os = "windows")'.build-dependencies]
-winres = "0.1"
+[build-dependencies]
+winres = { git = "https://github.com/Nilstrieb/winres", branch = "linking-flags" }
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
diff --git a/apps/plumeimpactor/build.rs b/apps/plumeimpactor/build.rs
index 97fad26..f2eb1ee 100644
--- a/apps/plumeimpactor/build.rs
+++ b/apps/plumeimpactor/build.rs
@@ -1,8 +1,19 @@
-fn main() {
- #[cfg(windows)]
- {
- let mut res = winres::WindowsResource::new();
+use std::io;
+use winres::WindowsResource;
+
+fn main() -> io::Result<()> {
+ if std::env::var("CARGO_CFG_TARGET_FAMILY").unwrap() == "windows" {
+ let mut res = WindowsResource::new();
+ match std::env::var("CARGO_CFG_TARGET_ENV").unwrap().as_str() {
+ "gnu" => {
+ res.set_ar_path("x86_64-w64-mingw32-ar")
+ .set_windres_path("x86_64-w64-mingw32-windres");
+ }
+ "msvc" => {}
+ _ => panic!("unsupported env"),
+ };
res.set_icon("../../package/windows/icon.ico");
- res.compile().unwrap();
+ res.compile()?;
}
+ Ok(())
}
diff --git a/apps/plumeimpactor/src/main.rs b/apps/plumeimpactor/src/main.rs
index c93fc46..22a8323 100644
--- a/apps/plumeimpactor/src/main.rs
+++ b/apps/plumeimpactor/src/main.rs
@@ -2,11 +2,15 @@
use crate::refresh::spawn_refresh_daemon;
+#[cfg(any(target_os = "linux", target_os = "windows"))]
+use single_instance::SingleInstance;
+
mod appearance;
mod defaults;
mod refresh;
mod screen;
mod subscriptions;
+mod startup;
mod tray;
pub const APP_NAME: &str = "Impactor";
@@ -16,11 +20,32 @@ fn main() -> iced::Result {
env_logger::init();
let _ = rustls::crypto::ring::default_provider().install_default();
+ #[cfg(any(target_os = "linux", target_os = "windows"))]
+ let _single_instance = match SingleInstance::new(APP_NAME) {
+ Ok(instance) => {
+ if !instance.is_single() {
+ log::info!("Another instance is already running; exiting.");
+ return Ok(());
+ }
+ Some(instance)
+ }
+ Err(err) => {
+ log::warn!("Failed to acquire single-instance lock: {err}");
+ None
+ }
+ };
+
#[cfg(target_os = "linux")]
{
gtk::init().expect("GTK init failed");
}
+ #[cfg(target_os = "macos")]
+ {
+ notify_rust::get_bundle_identifier_or_default("Impactor");
+ let _ = notify_rust::set_application("dev.khcrysalis.PlumeImpactor");
+ }
+
let (_daemon_handle, connected_devices) = spawn_refresh_daemon();
screen::set_refresh_daemon_devices(connected_devices);
diff --git a/apps/plumeimpactor/src/refresh.rs b/apps/plumeimpactor/src/refresh.rs
index 73ef0d4..e9e3067 100644
--- a/apps/plumeimpactor/src/refresh.rs
+++ b/apps/plumeimpactor/src/refresh.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
@@ -14,9 +14,24 @@ use crate::defaults::get_data_path;
pub type ConnectedDevices = Arc>>;
+struct RefreshGuard {
+ udid: String,
+ tasks: Arc>>,
+}
+
+impl Drop for RefreshGuard {
+ fn drop(&mut self) {
+ if let Ok(mut tasks) = self.tasks.lock() {
+ tasks.remove(&self.udid);
+ log::debug!("Released lock for device {}", self.udid);
+ }
+ }
+}
+
pub struct RefreshDaemon {
store_path: std::path::PathBuf,
connected_devices: ConnectedDevices,
+ active_tasks: Arc>>,
check_interval: Duration,
}
@@ -25,6 +40,7 @@ impl RefreshDaemon {
Self {
store_path: get_data_path().join("accounts.json"),
connected_devices: Arc::new(Mutex::new(HashMap::new())),
+ active_tasks: Arc::new(Mutex::new(HashSet::new())),
check_interval: Duration::from_secs(60 * 30), // Check every 30 minutes
}
}
@@ -43,6 +59,11 @@ impl RefreshDaemon {
loop {
if let Err(e) = rt.block_on(self.check_and_refresh()) {
log::error!("Refresh daemon error: {}", e);
+ notify_rust::Notification::new()
+ .summary("Impactor")
+ .body(&format!("Failed to refresh: {}", e))
+ .show()
+ .ok();
}
thread::sleep(self.check_interval);
@@ -60,12 +81,25 @@ impl RefreshDaemon {
for (udid, refresh_device) in store.refreshes() {
for app in &refresh_device.apps {
if app.scheduled_refresh <= now {
+ // We check for active tasks here to prevent the background loop
+ // from even starting a wait if a manual refresh is already running.
+ if self.is_busy(udid) {
+ log::info!(
+ "Device {} is already being processed. Skipping this app for now.",
+ udid
+ );
+ continue;
+ }
+
log::info!("App at {:?} needs refresh for device {}", app.path, udid);
+ // Note: wait_for_device might take a long time.
+ // refresh_app will double-check the lock once the device is found.
let device = self.wait_for_device(udid).await?;
- self.refresh_app(&store, refresh_device, app, &device)
- .await?;
+ if let Err(e) = self.refresh_app(&store, refresh_device, app, &device).await {
+ log::error!("Error refreshing app: {}", e);
+ }
}
}
}
@@ -73,16 +107,16 @@ impl RefreshDaemon {
Ok(())
}
+ fn is_busy(&self, udid: &str) -> bool {
+ self.active_tasks
+ .lock()
+ .map(|t| t.contains(udid))
+ .unwrap_or(false)
+ }
+
async fn wait_for_device(&self, udid: &str) -> Result {
log::info!("Waiting for device {} to connect...", udid);
- if let Ok(devices) = self.connected_devices.lock() {
- if let Some(device) = devices.get(udid) {
- log::info!("Device {} is already connected", udid);
- return Ok(device.clone());
- }
- }
-
let timeout = Duration::from_secs(60 * 60); // 1 hour timeout
let start = std::time::Instant::now();
@@ -109,8 +143,40 @@ impl RefreshDaemon {
app: &plume_store::RefreshApp,
device: &Device,
) -> Result<(), String> {
+ // Try to acquire the lock for this UDID.
+ {
+ let mut tasks = self
+ .active_tasks
+ .lock()
+ .map_err(|_| "Failed to lock task registry")?;
+ if tasks.contains(&device.udid) {
+ log::warn!(
+ "Refresh already in progress for {}. Aborting duplicate.",
+ device.udid
+ );
+ return Ok(());
+ }
+ tasks.insert(device.udid.clone());
+ }
+
+ // lock is released when this function returns
+ let _guard = RefreshGuard {
+ udid: device.udid.clone(),
+ tasks: self.active_tasks.clone(),
+ };
+
log::info!("Starting refresh for app at {:?}", app.path);
+ notify_rust::Notification::new()
+ .summary("Impactor")
+ .body(&format!(
+ "Started refreshing {} for {}",
+ app.name.as_deref().unwrap_or("???"),
+ &refresh_device.name
+ ))
+ .show()
+ .ok();
+
let account = store
.get_account(&refresh_device.account)
.ok_or_else(|| format!("Account {} not found", refresh_device.account))?;
@@ -139,10 +205,15 @@ impl RefreshDaemon {
};
let identity_is_new = {
- let identity =
- CertificateIdentity::new_with_session(&session, get_data_path(), None, team_id)
- .await
- .map_err(|e| format!("Failed to create identity: {}", e))?;
+ let identity = CertificateIdentity::new_with_session(
+ &session,
+ get_data_path(),
+ None,
+ team_id,
+ false,
+ )
+ .await
+ .map_err(|e| format!("Failed to create identity: {}", e))?;
identity.new
};
@@ -155,6 +226,11 @@ impl RefreshDaemon {
false
};
+ // Determine if we need to reinstall:
+ // - Mac devices always need reinstalling
+ // - If the identity is new, we need to reinstall
+ // - If the app is not installed, we need to reinstall
+ // - If the app is installed and identity is not new, we can just update profiles
let needs_reinstall = device.is_mac || identity_is_new || !is_installed;
if needs_reinstall {
@@ -173,6 +249,16 @@ impl RefreshDaemon {
log::info!("Successfully refreshed app at {:?}", app.path);
+ notify_rust::Notification::new()
+ .summary("Impactor")
+ .body(&format!(
+ "Successfully refreshed {} for {}",
+ app.name.as_deref().unwrap_or("???"),
+ &refresh_device.name
+ ))
+ .show()
+ .ok();
+
Ok(())
}
@@ -198,10 +284,15 @@ impl RefreshDaemon {
};
let team_id_string = team_id.to_string();
- let signing_identity =
- CertificateIdentity::new_with_session(session, get_data_path(), None, &team_id_string)
- .await
- .map_err(|e| format!("Failed to create signing identity: {}", e))?;
+ let signing_identity = CertificateIdentity::new_with_session(
+ session,
+ get_data_path(),
+ None,
+ &team_id_string,
+ false,
+ )
+ .await
+ .map_err(|e| format!("Failed to create signing identity: {}", e))?;
let mut signer = Signer::new(Some(signing_identity), options);
diff --git a/apps/plumeimpactor/src/screen/mod.rs b/apps/plumeimpactor/src/screen/mod.rs
index 69a9a9b..05f7626 100644
--- a/apps/plumeimpactor/src/screen/mod.rs
+++ b/apps/plumeimpactor/src/screen/mod.rs
@@ -108,7 +108,13 @@ impl Impactor {
let mut tray = ImpactorTray::new();
let store = Self::init_account_store_sync();
tray.update_refresh_apps(&store);
- let (id, open_task) = window::open(defaults::default_window_settings());
+ let start_in_tray = crate::startup::start_in_tray_from_args();
+ let (main_window, open_task) = if start_in_tray {
+ (None, Task::none())
+ } else {
+ let (id, open_task) = window::open(defaults::default_window_settings());
+ (Some(id), open_task.discard())
+ };
(
Self {
@@ -117,12 +123,12 @@ impl Impactor {
devices: Vec::new(),
selected_device: None,
tray: Some(tray),
- main_window: Some(id),
+ main_window,
account_store: Some(store),
login_windows: std::collections::HashMap::new(),
pending_installation: false,
},
- open_task.discard(),
+ open_task,
)
}
@@ -424,6 +430,12 @@ impl Impactor {
}
Task::none()
}
+ settings::Message::ToggleAutoStart(enabled) => {
+ if let Err(err) = crate::startup::set_auto_start_enabled(enabled) {
+ log::error!("Failed to update auto-start: {err}");
+ }
+ Task::none()
+ }
settings::Message::FetchTeams(ref email) => {
if let Some(account_store) = &self.account_store {
if let Some(account) = account_store.accounts().get(email) {
@@ -576,12 +588,22 @@ impl Impactor {
.refresh_app(&store, refresh_device, app, &device)
.await
{
- log::error!("Failed to refresh app: {}", e);
- } else {
- log::info!(
- "Successfully refreshed app at {:?}",
- app.path
+ log::error!(
+ "Failed to refresh app at {:?} on device {}: {}",
+ app.path,
+ udid,
+ e
);
+ notify_rust::Notification::new()
+ .summary("Impactor")
+ .body(&format!(
+ "Failed to refresh {} for {}: {}",
+ app.name.as_deref().unwrap_or("???"),
+ &refresh_device.name,
+ e
+ ))
+ .show()
+ .ok();
}
}
}
diff --git a/apps/plumeimpactor/src/screen/settings.rs b/apps/plumeimpactor/src/screen/settings.rs
index fdfcbd7..6fa6bc2 100644
--- a/apps/plumeimpactor/src/screen/settings.rs
+++ b/apps/plumeimpactor/src/screen/settings.rs
@@ -1,6 +1,6 @@
use std::collections::HashMap;
-use iced::widget::{button, column, container, pick_list, row, scrollable, text};
+use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text};
use iced::{Alignment, Element, Fill, Task};
use plume_store::AccountStore;
@@ -27,6 +27,7 @@ pub enum Message {
SelectTeam(String, String),
FetchTeams(String),
TeamsLoaded(String, Vec),
+ ToggleAutoStart(bool),
}
#[derive(Debug)]
@@ -54,6 +55,7 @@ impl SettingsScreen {
self.loading_teams = None;
Task::none()
}
+ Message::ToggleAutoStart(_) => Task::none(),
Message::SelectTeam(_, _) => Task::none(),
_ => Task::none(),
}
@@ -151,11 +153,20 @@ impl SettingsScreen {
content = content.push(text("No accounts added yet"));
}
+ let auto_start_enabled = crate::startup::auto_start_enabled();
+ content = content.push(self.view_auto_start_toggle(auto_start_enabled));
content = content.push(self.view_account_buttons(selected_index));
content.into()
}
+ fn view_auto_start_toggle(&self, auto_start_enabled: bool) -> Element<'_, Message> {
+ checkbox(auto_start_enabled)
+ .label("Launch on Startup")
+ .on_toggle(Message::ToggleAutoStart)
+ .into()
+ }
+
fn view_account_buttons(&self, selected_index: Option) -> Element<'_, Message> {
let mut buttons = row![
button(appearance::icon_text(appearance::PLUS, "Add Account", None))
diff --git a/apps/plumeimpactor/src/startup.rs b/apps/plumeimpactor/src/startup.rs
new file mode 100644
index 0000000..61116ef
--- /dev/null
+++ b/apps/plumeimpactor/src/startup.rs
@@ -0,0 +1,57 @@
+use std::path::PathBuf;
+
+use auto_launcher::AutoLaunchBuilder;
+
+#[cfg(target_os = "linux")]
+use auto_launcher::LinuxLaunchMode;
+#[cfg(target_os = "macos")]
+use auto_launcher::MacOSLaunchMode;
+#[cfg(target_os = "windows")]
+use auto_launcher::WindowsEnableMode;
+
+pub(crate) const TRAY_ONLY_ARG: &str = "--tray";
+
+pub(crate) fn start_in_tray_from_args() -> bool {
+ std::env::args().any(|arg| arg == "--tray")
+}
+
+pub(crate) fn auto_start_enabled() -> bool {
+ build_auto_launcher()
+ .and_then(|launcher| launcher.is_enabled().map_err(|e| e.to_string()))
+ .unwrap_or(false)
+}
+
+pub(crate) fn set_auto_start_enabled(enabled: bool) -> Result<(), String> {
+ let launcher = build_auto_launcher()?;
+
+ if enabled {
+ launcher.enable().map_err(|e| e.to_string())
+ } else {
+ launcher.disable().map_err(|e| e.to_string())
+ }
+}
+
+fn build_auto_launcher() -> Result {
+ let app_path = resolve_app_path().map_err(|e| format!("Failed to resolve app path: {e}"))?;
+ let app_path_string = app_path.to_string_lossy().to_string();
+
+ let mut builder = AutoLaunchBuilder::new();
+ builder.set_app_name(crate::APP_NAME);
+ builder.set_app_path(&app_path_string);
+ builder.set_args(&[TRAY_ONLY_ARG]);
+
+ #[cfg(target_os = "macos")]
+ builder.set_macos_launch_mode(MacOSLaunchMode::LaunchAgent);
+
+ #[cfg(target_os = "windows")]
+ builder.set_windows_enable_mode(WindowsEnableMode::CurrentUser);
+
+ #[cfg(target_os = "linux")]
+ builder.set_linux_launch_mode(LinuxLaunchMode::XdgAutostart);
+
+ builder.build().map_err(|e| e.to_string())
+}
+
+fn resolve_app_path() -> Result {
+ std::env::current_exe()
+}
diff --git a/apps/plumeimpactor/src/subscriptions.rs b/apps/plumeimpactor/src/subscriptions.rs
index fbdaeb7..c083355 100644
--- a/apps/plumeimpactor/src/subscriptions.rs
+++ b/apps/plumeimpactor/src/subscriptions.rs
@@ -255,6 +255,7 @@ pub(crate) async fn run_installation(
crate::defaults::get_data_path(),
None,
team_id,
+ false,
)
.await
.map_err(|e| e.to_string())?;
@@ -514,6 +515,7 @@ pub(crate) async fn export_certificate(account: plume_store::GsaAccount) -> Resu
crate::defaults::get_data_path(),
None,
team_id,
+ true,
)
.await
.map_err(|e| e.to_string())?;
diff --git a/apps/plumesign/src/commands/sign.rs b/apps/plumesign/src/commands/sign.rs
index 1e8cf94..e8ed20d 100644
--- a/apps/plumesign/src/commands/sign.rs
+++ b/apps/plumesign/src/commands/sign.rs
@@ -94,7 +94,7 @@ pub async fn execute(args: SignArgs) -> Result<()> {
let session = get_authenticated_account().await?;
let team_id = teams(&session).await?;
let cert_identity =
- CertificateIdentity::new_with_session(&session, get_data_path(), None, &team_id)
+ CertificateIdentity::new_with_session(&session, get_data_path(), None, &team_id, false)
.await?;
options.mode = SignerMode::Pem;
diff --git a/crates/plume_core/src/utils/certificate.rs b/crates/plume_core/src/utils/certificate.rs
index fb508a5..74deac7 100644
--- a/crates/plume_core/src/utils/certificate.rs
+++ b/crates/plume_core/src/utils/certificate.rs
@@ -58,6 +58,7 @@ impl CertificateIdentity {
config_path: PathBuf,
machine_name: Option,
team_id: &String,
+ is_export: bool,
) -> Result {
let machine_name = machine_name.unwrap_or_else(|| MACHINE_NAME.to_string());
@@ -126,7 +127,7 @@ impl CertificateIdentity {
};
// TODO: this may be horrendious
- if let Some(p12_data) = identity.create_pkcs12(&key_pair) {
+ if let Some(p12_data) = identity.create_pkcs12(&key_pair, is_export) {
identity.p12_data = Some(p12_data);
}
@@ -159,7 +160,7 @@ impl CertificateIdentity {
// just another unnecessary dependency, but the p12 crate that applecodesign-rs
// uses has no support for modern encryption, hopefully this doesn't add that
// much more bloat
- pub fn create_pkcs12(&self, data: &[Vec; 2]) -> Option> {
+ pub fn create_pkcs12(&self, data: &[Vec; 2], is_export: bool) -> Option> {
let cert_der = pem::parse(&data[0]).ok()?.contents().to_vec();
let key_der = pem::parse(&data[1]).ok()?.contents().to_vec();
@@ -181,7 +182,16 @@ impl CertificateIdentity {
p12_keystore::KeyStoreEntry::PrivateKeyChain(key_chain),
);
- let writer = keystore.writer(self.machine_id.as_deref().unwrap_or(""));
+ // when exporting the user has no idea what the password is, just dont set one
+ // otherwise, when not exporting (used for SideStore/AltStore) we use the
+ // machine_id since it needs it to locate a matching certificate
+ let password = if is_export {
+ "".to_string()
+ } else {
+ self.machine_id.as_deref().unwrap_or("").to_string()
+ };
+
+ let writer = keystore.writer(&password);
writer.write().ok()
}
diff --git a/demo.png b/demo.png
index ff0e82f..44d3bf1 100644
Binary files a/demo.png and b/demo.png differ