From 7cd12cc29b45578fee1048199924f0b4533c06ca Mon Sep 17 00:00:00 2001 From: John Spahn <44337821+jspahn80134@users.noreply.github.com> Date: Thu, 26 Dec 2024 08:53:15 -0500 Subject: [PATCH] Added global capture and capture enum --- server/Cargo.toml | 53 +++++++++++------- server/src/capture.rs | 120 ++++++++++++++++++++++++++++++++++++---- server/src/webserver.rs | 31 +++++++---- 3 files changed, 160 insertions(+), 44 deletions(-) diff --git a/server/Cargo.toml b/server/Cargo.toml index 6b6ecf8..400540f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,7 +16,7 @@ # the CodeChat Editor. If not, see # [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). # -# # `Cargo.toml` -- Rust build/package management config for the server +# # `Cargo.toml` -- Rust build/package management config # # ## General package configurations [package] @@ -29,7 +29,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.5" +version = "0.1.3" # This library allows other packages to use core CodeChat Editor features. [lib] @@ -37,38 +37,46 @@ name = "code_chat_editor" # ## Dependencies [dependencies] +actix = "0.13.1" +async-once-cell = "0.5.0" actix-files = "0.6" actix-rt = "2.9.0" actix-web = "4" actix-ws = "0.3.0" bytes = { version = "1", features = ["serde"] } chrono = "0.4" -clap = { version = "4.5.19", features = ["derive"] } -dunce = "1.0.5" -futures-util = "0.3.29" -indoc = "2.0.5" lazy_static = "1" log = "0.4" log4rs = "1.3" -mime = "0.3.17" -mime_guess = "2.0.5" -minreq = "2.12.0" -normalize-line-endings = "0.3.0" notify-debouncer-full = "0.4" -open = "5.3.0" path-slash = "0.2.1" -pest = "2.7.14" -pest_derive = "2.7.14" -# Per the [docs](https://docs.rs/crate/pulldown-cmark/latest), skip building the -# binary. -pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] } regex = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } -url = "2.5.2" urlencoding = "2" +# Per the [docs](https://docs.rs/crate/pulldown-cmark/latest), skip building the +# binary. +pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] } +futures-util = "0.3.29" +async-trait = "0.1.81" +open = "5.3.0" +dunce = "1.0.5" +minreq = "2.12.0" +mime = "0.3.17" +mime_guess = "2.0.5" +url = "2.5.2" +clap = { version = "4.5.19", features = ["derive"] } +indoc = "2.0.5" +cmd_lib = "1.9.5" +tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } +simplelog = "0.12.0" +config = "0.13" +pest = "2.7.14" +pest_derive = "2.7.14" +normalize-line-endings = "0.3.0" +uuid = { version = "1", features = ["v4"] } +dirs = "5.0.1" # [Windows-only dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies). [target.'cfg(windows)'.dependencies] @@ -76,12 +84,15 @@ win_partitions = "0.3.0" # ### Development-only dependencies [dev-dependencies] +assertables = "9" +assert_fs = "1" +tokio-tungstenite = "0.24" actix-http = "3.9.0" assert_cmd = "2.0.16" -assert_fs = "1" -assertables = "9" predicates = "3.1.2" -tokio-tungstenite = "0.26" +# See the [docs](https://github.com/rust-lang/rust-clippy#usage) to install +# clippy; it can't be installed as a dev-dependency. See the +# [fmt docs](https://github.com/rust-lang/rustfmt#quick-start) to install fmt. # #### Use local packages for development [patch.crates-io] diff --git a/server/src/capture.rs b/server/src/capture.rs index 50f3a6e..f9c8a2c 100644 --- a/server/src/capture.rs +++ b/server/src/capture.rs @@ -21,37 +21,103 @@ // Standard library use indoc::indoc; use std::fs; -use std::io; +use std::io::{self, Write}; use std::path::Path; +use std::path::PathBuf; use std::sync::Arc; // Third-party +use async_once_cell::OnceCell; use chrono::Local; +use dirs::config_dir; use log::{error, info}; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tokio_postgres::{Client, NoTls}; +use uuid::Uuid; + +pub static GLOBAL_EVENT_CAPTURE: OnceCell> = OnceCell::new(); + +pub async fn get_event_capture() -> Arc { + GLOBAL_EVENT_CAPTURE + .get_or_init(async { + let capture = EventCapture::new("config.json") + .await + .expect("Failed to initialize EventCapture"); + Arc::new(capture) + }) + .await + .clone() +} // Local +/// Returns the path where the identifier file is stored. +fn get_uuid_file_path() -> Result { + let mut config_path = config_dir().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "Could not find configuration directory", + ) + })?; + config_path.push("vsc_extension"); + config_path.push("uuid.txt"); + Ok(config_path) +} + +/// Creates a new UUID and writes it to the file if it does not already exist. +fn create_uuid() -> Result<(), io::Error> { + let path = self::get_uuid_file_path()?; + + // Check if the file already exists + if path.exists() { + return Ok(()); // UUID already exists, no need to create + } + + // Generate a new UUID + let new_uuid = Uuid::new_v4().to_string(); + + // Ensure the parent directory exists + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + // Write the new UUID to the file + let mut file = fs::File::create(&path)?; + file.write_all(new_uuid.as_bytes())?; + Ok(()) +} + +/// Retrieves the UUID from the file. If the file does not exist, it creates one. +pub fn get_uuid() -> Result { + let path = get_uuid_file_path()?; + + // If the file does not exist, create it + if !path.exists() { + self::create_uuid()?; + } + + // Read and return the UUID + let uuid = fs::read_to_string(&path)?; + Ok(uuid.trim().to_string()) +} + /* ## The Event Structure: TheĀ `Event` struct represents an event to be stored in the database. - Fields: - `user_id`: The ID of the user associated with the event. - + Fields: - `event_type`: The type of event (e.g., "keystroke", "file_open"). - `data`: Optional additional data associated with the event. ### Example - let event = Event { user_id: "user123".to_string(), event_type: - "keystroke".to_string(), data: Some("Pressed key A".to_string()), }; + let event = Event {event_type:"keystroke".to_string(), data: Some("Pressed key A".to_string()), }; */ #[derive(Deserialize, Debug)] pub struct Event { - pub user_id: String, - pub event_type: String, + pub event_type: EventType, // Use the EventType enum pub data: Option, } @@ -95,7 +161,6 @@ holds a `tokio_postgres::Client` for database operations. // Create an event let event = Event { - user_id: "user123".to_string(), event_type: "keystroke".to_string(), data: Some("Pressed key A".to_string()), }; @@ -111,6 +176,31 @@ pub struct EventCapture { db_client: Arc>, } +// Define a globally available EventType enum +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum EventType { + KeyStroke, + FileOpen, + FileSave, + ApplicationStart, + ApplicationExit, + System, +} + +impl EventType { + /// Convert the EventType enum to a string representation + pub fn as_str(&self) -> &'static str { + match self { + EventType::KeyStroke => "KeyStroke", + EventType::FileOpen => "FileOpen", + EventType::FileSave => "FileSave", + EventType::ApplicationStart => "ApplicationStart", + EventType::ApplicationExit => "ApplicationExit", + EventType::System => "System Event", + } + } +} + /* ## The EventCapture Implementation */ @@ -181,7 +271,6 @@ impl EventCapture { let event_capture = EventCapture::new("config.json").await?; let event = Event { - user_id: "user123".to_string(), event_type: "keystroke".to_string(), data: Some("Pressed key A".to_string()), }; @@ -201,6 +290,15 @@ impl EventCapture { VALUES ($1, $2, $3, $4) "}; + // Retrieve the UUID, creating it if necessary + let uuid = match get_uuid() { + Ok(uuid) => uuid, + Err(err) => { + error!("Error obtaining UUID: {:?}", err); + String::new() // Provide a fallback value, e.g., an empty string + } + }; + // Acquire a lock on the database client for thread-safe access let client = self.db_client.lock().await; @@ -209,8 +307,8 @@ impl EventCapture { .execute( stmt, &[ - &event.user_id, - &event.event_type, + &uuid, + &event.event_type.as_str(), &formatted_time, &event.data, ], @@ -232,7 +330,7 @@ CREATE TABLE events ( id SERIAL PRIMARY KEY, user_id TEXT NOT NULL, event_type TEXT NOT NULL, timestamp TEXT NOT NULL, data TEXT ); - **`id SERIAL PRIMARY KEY`**: Auto-incrementing primary key. -- **`user_id TEXT NOT NULL`**: The ID of the user associated with the event. +- **`user_id TEXT NOT NULL`**: The ID of the user associated with the event (UUID). - **`event_type TEXT NOT NULL`**: The type of event. - **`timestamp TEXT NOT NULL`**: The timestamp of the event. - **`data TEXT`**: Optional additional data associated with the event. diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 2b295cd..e7eee62 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -71,7 +71,10 @@ use vscode::{ }; // ### Local -//use crate::capture::EventCapture; +use crate::capture::get_event_capture; +use crate::capture::Event; +use crate::capture::EventCapture; +use crate::capture::EventType; use crate::processing::{ source_to_codechat_for_web_string, CodeChatForWeb, TranslationResultsString, }; @@ -80,10 +83,10 @@ use filewatcher::{ filewatcher_websocket, }; -// ## Data structures -// -// ### Data structures supporting a websocket connection between the IDE, this server, and the CodeChat Editor Client -// +/// ## Data structures +/// +/// ### Data structures supporting a websocket connection between the IDE, this server, and the CodeChat Editor Client +/// /// Provide queues which send data to the IDE and the CodeChat Editor Client. #[derive(Debug)] struct WebsocketQueues { @@ -318,9 +321,6 @@ lazy_static! { static ref ROOT_PATH: PathBuf = { let exe_path = env::current_exe().unwrap(); let exe_dir = exe_path.parent().unwrap(); - #[cfg(not(any(test, debug_assertions)))] - let root_path = PathBuf::from(exe_dir); - #[cfg(any(test, debug_assertions))] let mut root_path = PathBuf::from(exe_dir); // When in debug or running tests, use the layout of the Git repo to // find client files. In release mode, we assume the static folder is a @@ -1077,12 +1077,22 @@ async fn client_websocket( // ## Webserver core #[actix_web::main] pub async fn main(port: u16) -> std::io::Result<()> { + // Initialize EventCapture + let event_capture = get_event_capture().await; + + // Example usage + let event = Event { + event_type: EventType::System, + data: Some("Server Started".to_string()), + }; + + event_capture.insert_event(event).await?; run_server(port).await } pub async fn run_server(port: u16) -> std::io::Result<()> { // Connect to the Capture Database - //let _event_capture = EventCapture::new("config.json").await?; + let _event_capture = EventCapture::new("config.json").await?; // Pre-load the bundled files before starting the webserver. let _ = &*BUNDLED_FILES_MAP; @@ -1104,9 +1114,6 @@ pub async fn run_server(port: u16) -> std::io::Result<()> { } pub fn configure_logger(level: LevelFilter) { - #[cfg(not(debug_assertions))] - let l4rs = ROOT_PATH.clone(); - #[cfg(debug_assertions)] let mut l4rs = ROOT_PATH.clone(); #[cfg(debug_assertions)] l4rs.push("server");