Skip to content

Commit cea060e

Browse files
committed
feat(client):support single instance
1 parent f14470a commit cea060e

File tree

4 files changed

+131
-7
lines changed

4 files changed

+131
-7
lines changed

Cargo.lock

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

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" }

aw-client-rust/src/lib.rs

+8
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()?;
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())?;
4350

4451
Ok(AwClient {
4552
client,
53+
single_instance,
4654
baseurl,
4755
name: name.to_string(),
4856
hostname,

aw-client-rust/src/single_instance.rs

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

0 commit comments

Comments
 (0)