Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 106 additions & 35 deletions aw-client-rust/src/single_instance.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use dirs::cache_dir;
use fs4::fs_std::FileExt;
use log::debug;
use std::fs::{File, OpenOptions};
Expand All @@ -22,43 +21,54 @@ pub enum SingleInstanceError {
LockDirCreation,
}

impl SingleInstance {
pub fn new(client_name: &str) -> Result<SingleInstance, SingleInstanceError> {
let cache_dir = cache_dir().ok_or(SingleInstanceError::LockDirCreation)?;
let lock_dir = cache_dir.join("activitywatch").join("client_locks");
std::fs::create_dir_all(&lock_dir).map_err(|_| SingleInstanceError::LockDirCreation)?;
/// The running app's cache dir, as set by [`set_android_cache_dir`].
#[cfg(target_os = "android")]
static ANDROID_CACHE_DIR: std::sync::Mutex<Option<std::path::PathBuf>> =
std::sync::Mutex::new(None);

let lockfile = lock_dir.join(client_name);
debug!("SingleInstance lockfile: {:?}", lockfile);
/// Set the Android app's cache dir (`Context.getCacheDir()`), where lock files
/// are kept. The app's private dirs depend on its package (debug builds add a
/// `.debug` suffix) and on the Android user (work profiles are not user 0), so
/// they can't be hardcoded.
#[cfg(target_os = "android")]
pub fn set_android_cache_dir(path: &std::path::Path) {
*ANDROID_CACHE_DIR.lock().unwrap() = Some(path.to_path_buf());
}

/// The configured cache dir, else `/data/data/<package>/cache` for the running
/// package (the package name is the process name in `/proc/self/cmdline`).
#[cfg(target_os = "android")]
fn android_cache_dir() -> Option<std::path::PathBuf> {
if let Some(dir) = ANDROID_CACHE_DIR.lock().unwrap().clone() {
return Some(dir);
}
let cmdline = std::fs::read("/proc/self/cmdline").ok()?;
let process = cmdline.split(|b| *b == 0).next()?;
let process = std::str::from_utf8(process).ok()?;
// Service processes are named "<package>:<process>".
let package = process.split(':').next()?;
if package.is_empty() || package.contains('/') {
return None;
}
Some(
std::path::Path::new("/data/data")
.join(package)
.join("cache"),
)
}

#[cfg(windows)]
impl SingleInstance {
pub fn new(client_name: &str) -> Result<SingleInstance, SingleInstanceError> {
// On Android, use the app-specific cache directory with flock (Android is Unix-based).
#[cfg(target_os = "android")]
{
// On Windows, try to create an exclusive file
// Remove existing file if it exists (in case of previous crash)
let _ = std::fs::remove_file(&lockfile);
let cache_dir = android_cache_dir().ok_or(SingleInstanceError::LockDirCreation)?;
let lock_dir = cache_dir.join("activitywatch").join("client_locks");
std::fs::create_dir_all(&lock_dir).map_err(|_| SingleInstanceError::LockDirCreation)?;

match OpenOptions::new()
.write(true)
.create(true)
.create_new(true)
.open(&lockfile)
{
Ok(file) => Ok(SingleInstance {
file: Some(file),
locked: Arc::new(AtomicBool::new(true)),
}),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
// Qualified so the unix build doesn't carry an unused `error` import
log::error!("Another instance is already running");
Err(SingleInstanceError::AlreadyRunning)
}
Err(e) => Err(SingleInstanceError::Io(e)),
}
}
let lockfile = lock_dir.join(client_name);
debug!("SingleInstance lockfile: {:?}", lockfile);

#[cfg(unix)]
{
// On Unix-like systems, use flock
match OpenOptions::new()
.write(true)
.create(true)
Expand All @@ -70,19 +80,80 @@ impl SingleInstance {
file: Some(file),
locked: Arc::new(AtomicBool::new(true)),
}),
Ok(false) => Err(SingleInstanceError::AlreadyRunning),
Ok(false) => {
// Qualified so non-Android unix builds don't carry an unused `error` import
log::error!("Another instance is already running");
Err(SingleInstanceError::AlreadyRunning)
}
Err(e) => Err(SingleInstanceError::Io(e)),
},
Err(e) => Err(SingleInstanceError::Io(e)),
}
}

#[cfg(not(target_os = "android"))]
{
use dirs::cache_dir;

let cache_dir = cache_dir().ok_or(SingleInstanceError::LockDirCreation)?;
let lock_dir = cache_dir.join("activitywatch").join("client_locks");
std::fs::create_dir_all(&lock_dir).map_err(|_| SingleInstanceError::LockDirCreation)?;

let lockfile = lock_dir.join(client_name);
debug!("SingleInstance lockfile: {:?}", lockfile);

#[cfg(windows)]
{
// On Windows, try to create an exclusive file.
// Remove existing file if it exists (in case of previous crash).
let _ = std::fs::remove_file(&lockfile);

match OpenOptions::new()
.write(true)
.create(true)
.create_new(true)
.open(&lockfile)
{
Ok(file) => Ok(SingleInstance {
file: Some(file),
locked: Arc::new(AtomicBool::new(true)),
}),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
log::error!("Another instance is already running");
Err(SingleInstanceError::AlreadyRunning)
}
Err(e) => Err(SingleInstanceError::Io(e)),
}
}

#[cfg(unix)]
{
// On Unix-like systems (Linux, macOS), use flock.
match OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&lockfile)
{
Ok(file) => match file.try_lock_exclusive() {
Ok(true) => Ok(SingleInstance {
file: Some(file),
locked: Arc::new(AtomicBool::new(true)),
}),
Ok(false) => Err(SingleInstanceError::AlreadyRunning),
Err(e) => Err(SingleInstanceError::Io(e)),
},
Err(e) => Err(SingleInstanceError::Io(e)),
}
}
}
}
}

impl Drop for SingleInstance {
fn drop(&mut self) {
if self.locked.load(Ordering::SeqCst) {
//drop the file handle and lock on Unix and Windows
// Drop the file handle, releasing the lock on Unix and Windows.
self.file.take();
self.locked.store(false, Ordering::SeqCst);
}
Expand Down
10 changes: 10 additions & 0 deletions aw-sync/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ fn apply_android_data_dir_from_env() {
let path = files_dir.to_string_lossy();
info!("android data dir from XDG_DATA_HOME: {}", path);
aw_server::dirs::set_android_data_dir(&path);
set_client_cache_dir(&files_dir);
}

/// Point aw-client-rust's single-instance locks at this app's cache dir, the
/// `cache` sibling of `filesDir` (`/data/user/<user>/<package>/{files,cache}`).
fn set_client_cache_dir(files_dir: &Path) {
if let Some(app_dir) = files_dir.parent() {
aw_client_rust::single_instance::set_android_cache_dir(&app_dir.join("cache"));
}
}

/// Mirror of `RustInterface.setDataDir`. Prefer this explicit path; the XDG
Expand All @@ -218,6 +227,7 @@ pub extern "C" fn Java_net_activitywatch_android_SyncInterface_setDataDir(
let path: String = s.into();
info!("Setting android data dir as {}", path);
aw_server::dirs::set_android_data_dir(&path);
set_client_cache_dir(Path::new(&path));
}
Err(e) => {
error!("setDataDir: failed to read path: {}", e);
Expand Down
Loading