Skip to content

feat(client):support single instance #522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
74 changes: 64 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions aw-client-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
aw-models = { path = "../aw-models" }
tokio = { version = "1.28.2", features = ["rt"] }
libc = "0.2"
log = "0.4"
thiserror = "1.0"
dirs = "4.0"
fs4 = { version = "0.13", features = ["sync"] }

[dev-dependencies]
aw-datastore = { path = "../aw-datastore" }
Expand Down
8 changes: 8 additions & 0 deletions aw-client-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ extern crate serde_json;
extern crate tokio;

pub mod blocking;
pub mod single_instance;

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

use chrono::{DateTime, Utc};
use serde_json::{json, Map};
use single_instance::SingleInstance;
use std::net::TcpStream;
use std::time::Duration;

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

pub struct AwClient {
client: reqwest::Client,
#[allow(dead_code)]
single_instance: SingleInstance,
pub baseurl: reqwest::Url,
pub name: String,
pub hostname: String,
Expand All @@ -40,9 +44,13 @@ impl AwClient {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.build()?;
//TODO: change localhost string to 127.0.0.1 for feature parity
let single_instance_name = format!("{}-at-{}-on-{}", name, host, port);
let single_instance = single_instance::SingleInstance::new(single_instance_name.as_str())?;

Ok(AwClient {
client,
single_instance,
baseurl,
name: name.to_string(),
hostname,
Expand Down
84 changes: 84 additions & 0 deletions aw-client-rust/src/single_instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use dirs::cache_dir;
use fs4::fs_std::FileExt;
use log::{debug, error};
use std::fs::{File, OpenOptions};
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

#[derive(Debug)]
pub struct SingleInstance {
file: Option<File>,
locked: Arc<AtomicBool>,
}

#[derive(Debug, thiserror::Error)]
pub enum SingleInstanceError {
#[error("Another instance is already running")]
AlreadyRunning,
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Failed to create lock directory")]
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)?;

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 => {
error!("Another instance is already running");
Err(SingleInstanceError::AlreadyRunning)
}
Err(e) => Err(SingleInstanceError::Io(e)),
}
}

#[cfg(unix)]
{
// On Unix-like systems, use flock
match OpenOptions::new().write(true).create(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
self.file.take();
self.locked.store(false, Ordering::SeqCst);
}
}
}
Loading