use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use rightkit_daemon::{
host, Allowlist, CallContext, Client, Emit, Handler, HostError, Hosted, ServiceConfig,
ServiceError, Suite,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
pub const MESSAGE_METHOD: &str = "instance-message";
const CONNECT_ATTEMPTS: u32 = 20;
const CONNECT_RETRY: Duration = Duration::from_millis(100);
const HOST_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct InstanceMessage {
pub argv: Vec<String>,
pub cwd: String,
pub action: Option<String>,
#[serde(default)]
pub args: Map<String, Value>,
}
impl InstanceMessage {
pub fn from_process() -> Self {
Self {
argv: std::env::args().collect(),
cwd: std::env::current_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default(),
action: None,
args: Map::new(),
}
}
pub fn with_action(mut self, action: &str) -> Self {
self.action = Some(action.to_owned());
self
}
pub fn with_arg(mut self, key: &str, value: impl Into<Value>) -> Self {
self.args.insert(key.to_owned(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaimFailure {
Discovery(String),
Host(String),
Forward(String),
Legacy(String),
InvalidId(String),
}
impl fmt::Display for ClaimFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Discovery(m) => write!(f, "single-instance discovery failed: {m}"),
Self::Host(m) => write!(f, "single-instance host failed: {m}"),
Self::Forward(m) => write!(f, "single-instance forward failed: {m}"),
Self::Legacy(m) => write!(f, "single-instance legacy holder refused the launch: {m}"),
Self::InvalidId(m) => write!(f, "single-instance id is not usable: {m}"),
}
}
}
impl std::error::Error for ClaimFailure {}
static CLAIMS: Mutex<Vec<(u64, Hosted)>> = Mutex::new(Vec::new());
static NEXT_CLAIM: AtomicU64 = AtomicU64::new(1);
static LEGACY_HOLDS: Mutex<Vec<(u64, crate::legacy_hold::LegacyHold)>> = Mutex::new(Vec::new());
fn legacy_holds() -> std::sync::MutexGuard<'static, Vec<(u64, crate::legacy_hold::LegacyHold)>> {
LEGACY_HOLDS.lock().unwrap_or_else(|e| e.into_inner())
}
fn claims() -> std::sync::MutexGuard<'static, Vec<(u64, Hosted)>> {
CLAIMS.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn hold(hosted: Hosted) -> u64 {
let token = NEXT_CLAIM.fetch_add(1, Ordering::Relaxed);
claims().push((token, hosted));
token
}
pub(crate) fn release(token: u64) {
let removed = {
let mut held = claims();
held.iter()
.position(|(t, _)| *t == token)
.map(|i| held.remove(i))
};
drop(removed);
let legacy = {
let mut held = legacy_holds();
held.iter()
.position(|(t, _)| *t == token)
.map(|i| held.remove(i))
};
drop(legacy);
}
pub(crate) fn release_all() {
let released: Vec<(u64, Hosted)> = std::mem::take(&mut *claims());
drop(released);
let legacy = std::mem::take(&mut *legacy_holds());
drop(legacy);
}
type DestroyHook = Box<dyn FnOnce() + Send>;
static DESTROY_HOOKS: Mutex<Vec<DestroyHook>> = Mutex::new(Vec::new());
fn run_destroy_hooks() {
let hooks: Vec<DestroyHook> =
std::mem::take(&mut *DESTROY_HOOKS.lock().unwrap_or_else(|e| e.into_inner()));
for hook in hooks {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(hook));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LegacySingleInstanceNames {
pub windows_mutex: String,
pub windows_window_class: String,
pub windows_window_title: String,
pub macos_socket: std::path::PathBuf,
pub linux_dbus_name: String,
}
impl LegacySingleInstanceNames {
pub fn for_identifier(identifier: &str) -> Self {
Self {
windows_mutex: format!("{identifier}-sim"),
windows_window_class: format!("{identifier}-sic"),
windows_window_title: format!("{identifier}-siw"),
macos_socket: std::path::PathBuf::from(format!(
"/tmp/{}_si.sock",
crate::legacy_instance::socket_stem(identifier)
)),
linux_dbus_name: format!("{identifier}.SingleInstance"),
}
}
}
pub struct SingleInstanceGuard {
token: u64,
}
impl Drop for SingleInstanceGuard {
fn drop(&mut self) {
release(self.token);
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ClaimOptions {
pub legacy_identifier: Option<String>,
pub hold_legacy: bool,
}
impl ClaimOptions {
pub fn with_legacy(identifier: &str) -> Self {
Self {
legacy_identifier: Some(identifier.to_owned()),
hold_legacy: false,
}
}
pub fn holding_legacy(mut self) -> Self {
self.hold_legacy = true;
self
}
}
#[must_use = "a Forwarded or Failed launch must exit; ignoring the outcome continues the launch"]
pub enum ClaimOutcome {
Primary(SingleInstanceGuard),
Forwarded,
Failed(ClaimFailure),
}
impl ClaimOutcome {
pub fn is_primary(&self) -> bool {
matches!(self, Self::Primary(_))
}
}
pub struct SingleInstance;
impl SingleInstance {
pub fn destroy() {
run_destroy_hooks();
release_all();
}
pub fn on_destroy(hook: impl FnOnce() + Send + 'static) {
DESTROY_HOOKS
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(Box::new(hook));
}
pub fn claim(
id: &str,
on_message: impl Fn(InstanceMessage) + Send + Sync + 'static,
) -> ClaimOutcome {
Self::claim_with(id, InstanceMessage::from_process(), on_message)
}
pub fn claim_with(
id: &str,
message: InstanceMessage,
on_message: impl Fn(InstanceMessage) + Send + Sync + 'static,
) -> ClaimOutcome {
Self::claim_with_options(id, ClaimOptions::default(), message, on_message)
}
pub fn claim_with_options(
id: &str,
options: ClaimOptions,
message: InstanceMessage,
on_message: impl Fn(InstanceMessage) + Send + Sync + 'static,
) -> ClaimOutcome {
match Suite::resolve() {
Ok(suite) => Self::claim_in_with(&suite, id, options, message, on_message),
Err(e) => ClaimOutcome::Failed(ClaimFailure::Discovery(e.to_string())),
}
}
pub fn claim_in(
suite: &Suite,
id: &str,
message: InstanceMessage,
on_message: impl Fn(InstanceMessage) + Send + Sync + 'static,
) -> ClaimOutcome {
Self::claim_in_with(suite, id, ClaimOptions::default(), message, on_message)
}
pub fn claim_in_with(
suite: &Suite,
id: &str,
options: ClaimOptions,
message: InstanceMessage,
on_message: impl Fn(InstanceMessage) + Send + Sync + 'static,
) -> ClaimOutcome {
let service = match crate::instance::derive_service_name(id, "-instance") {
Ok(name) => name,
Err(e) => {
eprintln!("rightkit-tauri-shell: single-instance is NOT active, invalid id: {e}");
return ClaimOutcome::Failed(ClaimFailure::InvalidId(e.to_string()));
}
};
let client_id = crate::instance::normalize_app_id(id);
let hold_identifier = options
.legacy_identifier
.clone()
.filter(|_| options.hold_legacy);
if hold_identifier.is_none() {
if let Some(identifier) = options.legacy_identifier.as_deref() {
let argv = message.argv.clone();
match crate::legacy_instance::forward_checked(identifier, &argv, &message.cwd) {
Ok(Some(delivery)) => {
delivery.log_unconfirmed("SingleInstance::claim");
return ClaimOutcome::Forwarded;
}
Ok(None) => {}
Err(e) => return ClaimOutcome::Failed(ClaimFailure::Legacy(e)),
}
}
}
let mut cfg = ServiceConfig::new(
&service,
&format!("instance:{id}"),
100,
env!("CARGO_PKG_VERSION"),
Allowlist::new().allow(&client_id, [MESSAGE_METHOD]),
);
cfg.takeover_timeout = HOST_TIMEOUT;
let on_message: crate::legacy_hold::OnMessage = Arc::new(on_message);
let handler = Arc::new(Receive {
on_message: on_message.clone(),
});
match host(suite, cfg, handler) {
Ok(hosted) => {
let Some(identifier) = hold_identifier else {
return ClaimOutcome::Primary(SingleInstanceGuard {
token: hold(hosted),
});
};
match crate::legacy_hold::acquire(
&identifier,
&message.argv,
&message.cwd,
on_message,
) {
Ok(crate::legacy_hold::Acquired::Held(legacy)) => {
let token = hold(hosted);
legacy_holds().push((token, legacy));
ClaimOutcome::Primary(SingleInstanceGuard { token })
}
Ok(crate::legacy_hold::Acquired::Forwarded) => {
drop(hosted);
ClaimOutcome::Forwarded
}
Err(e) => {
drop(hosted);
ClaimOutcome::Failed(ClaimFailure::Legacy(e))
}
}
}
Err(HostError::Held(_)) => forward(suite, &client_id, &service, &message),
Err(e) => ClaimOutcome::Failed(ClaimFailure::Host(e.to_string())),
}
}
}
fn forward(suite: &Suite, id: &str, service: &str, message: &InstanceMessage) -> ClaimOutcome {
let params = match serde_json::to_value(message) {
Ok(params) => params,
Err(e) => return ClaimOutcome::Failed(ClaimFailure::Forward(e.to_string())),
};
let mut last = String::from("live primary did not accept a connection");
for _ in 0..CONNECT_ATTEMPTS {
match Client::connect(suite, service, id) {
Ok(client) => {
return match client.call(MESSAGE_METHOD, params) {
Ok(_) => ClaimOutcome::Forwarded,
Err(e) => ClaimOutcome::Failed(ClaimFailure::Forward(e.to_string())),
};
}
Err(e) => {
last = e.to_string();
std::thread::sleep(CONNECT_RETRY);
}
}
}
ClaimOutcome::Failed(ClaimFailure::Forward(last))
}
struct Receive {
on_message: crate::legacy_hold::OnMessage,
}
impl Handler for Receive {
fn call(
&self,
_cx: &CallContext,
method: &str,
params: &Value,
_emit: &Emit,
) -> Result<Value, ServiceError> {
if method != MESSAGE_METHOD {
return Err(ServiceError::new("unknown_method", method.to_string()));
}
let message: InstanceMessage = serde_json::from_value(params.clone())
.map_err(|e| ServiceError::new("invalid_message", e.to_string()))?;
(self.on_message)(message);
Ok(json!({"ok": true}))
}
}