Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a7e3049

Browse files
committedApr 12, 2025··
feat(client):support single instance
1 parent f14470a commit a7e3049

File tree

4 files changed

+136
-8
lines changed

4 files changed

+136
-8
lines changed
 

Diff for: ‎Cargo.lock

+37-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎aw-client-rust/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ libc = "0.2"
1616
log = "0.4"
1717
thiserror = "1.0"
1818
dirs = "4.0"
19+
fs4 = { version = "0.13", features = ["sync"] }
20+
# fs4 = { version = "0.13", features = ["tokio"] }
1921

2022
[dev-dependencies]
2123
aw-datastore = { path = "../aw-datastore" }

Diff for: ‎aw-client-rust/src/lib.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ extern crate serde_json;
66
extern crate tokio;
77

88
pub mod blocking;
9+
pub mod single_instance;
910

1011
use std::{collections::HashMap, error::Error};
1112

1213
use chrono::{DateTime, Utc};
1314
use serde_json::{json, Map};
15+
use single_instance::SingleInstance;
1416
use std::net::TcpStream;
1517
use std::time::Duration;
1618

1719
pub use aw_models::{Bucket, BucketMetadata, Event};
1820

1921
pub struct AwClient {
2022
client: reqwest::Client,
23+
#[allow(dead_code)]
24+
single_instance: SingleInstance,
2125
pub baseurl: reqwest::Url,
2226
pub name: String,
2327
pub hostname: String,
@@ -40,9 +44,13 @@ impl AwClient {
4044
let client = reqwest::Client::builder()
4145
.timeout(std::time::Duration::from_secs(120))
4246
.build()?;
43-
47+
//TODO: change localhost string to 127.0.0.1 for feature parity
48+
let single_instance_name = format!("{}-at-{}-on-{}",name,host,port);
49+
let single_instance = single_instance::SingleInstance::new(single_instance_name.as_str())?;
50+
4451
Ok(AwClient {
4552
client,
53+
single_instance,
4654
baseurl,
4755
name: name.to_string(),
4856
hostname,

Diff for: ‎aw-client-rust/src/single_instance.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use fs4::fs_std::FileExt;
2+
use log::{debug, error};
3+
use std::fs::{File, OpenOptions};
4+
use std::io;
5+
use std::sync::atomic::{AtomicBool, Ordering};
6+
use std::sync::Arc;
7+
use dirs::cache_dir;
8+
9+
#[derive(Debug)]
10+
pub struct SingleInstance {
11+
file: Option<File>,
12+
locked: Arc<AtomicBool>,
13+
}
14+
15+
#[derive(Debug, thiserror::Error)]
16+
pub enum SingleInstanceError {
17+
#[error("Another instance is already running")]
18+
AlreadyRunning,
19+
#[error("IO error: {0}")]
20+
Io(#[from] io::Error),
21+
#[error("Failed to create lock directory")]
22+
LockDirCreation,
23+
}
24+
25+
impl SingleInstance {
26+
pub fn new(
27+
client_name: &str,
28+
) -> Result<SingleInstance, SingleInstanceError> {
29+
let cache_dir = cache_dir().ok_or(SingleInstanceError::LockDirCreation)?;
30+
let lock_dir = cache_dir.join("activitywatch").join("client_locks");
31+
std::fs::create_dir_all(&lock_dir).map_err(|_| SingleInstanceError::LockDirCreation)?;
32+
33+
let lockfile = lock_dir.join(client_name);
34+
debug!("SingleInstance lockfile: {:?}", lockfile);
35+
36+
#[cfg(windows)]
37+
{
38+
// On Windows, try to create an exclusive file
39+
// Remove existing file if it exists (in case of previous crash)
40+
let _ = std::fs::remove_file(&lockfile);
41+
42+
match OpenOptions::new()
43+
.write(true)
44+
.create(true)
45+
.create_new(true)
46+
.open(&lockfile)
47+
{
48+
Ok(file) => Ok(SingleInstance {
49+
file: Some(file),
50+
locked: Arc::new(AtomicBool::new(true)),
51+
}),
52+
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
53+
error!("Another instance is already running");
54+
Err(SingleInstanceError::AlreadyRunning)
55+
}
56+
Err(e) => Err(SingleInstanceError::Io(e)),
57+
}
58+
}
59+
60+
#[cfg(unix)]
61+
{
62+
// On Unix-like systems, use flock
63+
match OpenOptions::new().write(true).create(true).open(&lockfile) {
64+
Ok(file) => {
65+
match file.try_lock_exclusive() {
66+
Ok(true) => Ok(SingleInstance {
67+
file: Some(file),
68+
locked: Arc::new(AtomicBool::new(true)),
69+
}),
70+
Ok(false) => Err(SingleInstanceError::AlreadyRunning),
71+
Err(e) => Err(SingleInstanceError::Io(e)),
72+
}
73+
}
74+
Err(e) => Err(SingleInstanceError::Io(e)),
75+
}
76+
}
77+
}
78+
}
79+
80+
impl Drop for SingleInstance {
81+
fn drop(&mut self) {
82+
if self.locked.load(Ordering::SeqCst) {
83+
//drop the file handle and lock on Unix and Windows
84+
self.file.take();
85+
self.locked.store(false, Ordering::SeqCst);
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)