From 305a95f5570d78164049474a82c54735f3d88957 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:19:42 +0800 Subject: update: Action.rs 1. Rename `insert` to `with_data` and `insert_arc` to `with_arc_data` 2. Add new `insert_data` and `insert_arc_data` methods that take &mut self --- crates/system_action/src/action.rs | 14 ++++++++++++-- crates/vcs_actions/src/connection/action_service.rs | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'crates') diff --git a/crates/system_action/src/action.rs b/crates/system_action/src/action.rs index 9eef1db..ef1bf11 100644 --- a/crates/system_action/src/action.rs +++ b/crates/system_action/src/action.rs @@ -130,17 +130,27 @@ impl ActionContext { } /// Insert arbitrary data in the context - pub fn insert(mut self, value: T) -> Self { + pub fn with_data(mut self, value: T) -> Self { self.data.insert(TypeId::of::(), Arc::new(value)); self } /// Insert arbitrary data as Arc in the context - pub fn insert_arc(mut self, value: Arc) -> Self { + pub fn with_arc_data(mut self, value: Arc) -> Self { self.data.insert(TypeId::of::(), value); self } + /// Insert arbitrary data in the context + pub fn insert_data(&mut self, value: T) { + self.data.insert(TypeId::of::(), Arc::new(value)); + } + + /// Insert arbitrary data as Arc in the context + pub fn insert_arc_data(&mut self, value: Arc) { + self.data.insert(TypeId::of::(), value); + } + /// Get arbitrary data from the context pub fn get(&self) -> Option<&T> { self.data diff --git a/crates/vcs_actions/src/connection/action_service.rs b/crates/vcs_actions/src/connection/action_service.rs index ca236e7..3786fc5 100644 --- a/crates/vcs_actions/src/connection/action_service.rs +++ b/crates/vcs_actions/src/connection/action_service.rs @@ -184,7 +184,7 @@ async fn process_connection(stream: TcpStream, vault: Arc, action_pool: A let ctx: ActionContext = ActionContext::remote().insert_instance(instance); // Insert vault into context - let ctx = ctx.insert_arc(vault); + let ctx = ctx.with_arc_data(vault); info!( "Process action `{}` with argument `{}`", -- cgit From 47e957ec3cb35c61d72b2d4e06e048a657920aa0 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:20:33 +0800 Subject: Add new error types and sort variants alphabetically - Add Authentication, Locked, NoResult, and NotFound error variants - Remove Crypto error variant - Reorder all variants in A-Z order for better maintainability --- crates/utils/tcp_connection/src/error.rs | 49 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 23 deletions(-) (limited to 'crates') diff --git a/crates/utils/tcp_connection/src/error.rs b/crates/utils/tcp_connection/src/error.rs index cfea060..3667a59 100644 --- a/crates/utils/tcp_connection/src/error.rs +++ b/crates/utils/tcp_connection/src/error.rs @@ -3,38 +3,32 @@ use thiserror::Error; #[derive(Error, Debug, Clone)] pub enum TcpTargetError { - #[error("I/O error: {0}")] - Io(String), - - #[error("Serialization error: {0}")] - Serialization(String), + #[error("Authentication failed: {0}")] + Authentication(String), #[error("Cryptographic error: {0}")] Crypto(String), - #[error("Protocol error: {0}")] - Protocol(String), - - #[error("Authentication failed: {0}")] - Authentication(String), - #[error("File operation error: {0}")] File(String), - #[error("Network error: {0}")] - Network(String), + #[error("I/O error: {0}")] + Io(String), #[error("Invalid configuration: {0}")] Config(String), - #[error("Timeout: {0}")] - Timeout(String), + #[error("Locked: {0}")] + Locked(String), - #[error("Unsupported operation: {0}")] - Unsupported(String), + #[error("Network error: {0}")] + Network(String), - #[error("Pool already exists: {0}")] - PoolAlreadyExists(String), + #[error("No result: {0}")] + NoResult(String), + + #[error("Not found: {0}")] + NotFound(String), #[error("Not local machine: {0}")] NotLocal(String), @@ -42,11 +36,20 @@ pub enum TcpTargetError { #[error("Not remote machine: {0}")] NotRemote(String), - #[error("Not found: {0}")] - NotFound(String), + #[error("Pool already exists: {0}")] + PoolAlreadyExists(String), - #[error("Locked: {0}")] - Locked(String), + #[error("Protocol error: {0}")] + Protocol(String), + + #[error("Serialization error: {0}")] + Serialization(String), + + #[error("Timeout: {0}")] + Timeout(String), + + #[error("Unsupported operation: {0}")] + Unsupported(String), } impl From for TcpTargetError { -- cgit From d0f214b6eceecbf444ef023bd1b406790aee384b Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:21:07 +0800 Subject: feat: Completed `set_upstream_vault_action` --- crates/vcs_actions/src/actions/local_actions.rs | 78 ++++++++++++++++++++----- 1 file changed, 62 insertions(+), 16 deletions(-) (limited to 'crates') diff --git a/crates/vcs_actions/src/actions/local_actions.rs b/crates/vcs_actions/src/actions/local_actions.rs index f705692..1cf5772 100644 --- a/crates/vcs_actions/src/actions/local_actions.rs +++ b/crates/vcs_actions/src/actions/local_actions.rs @@ -1,30 +1,76 @@ use std::net::SocketAddr; use action_system::{action::ActionContext, macros::action_gen}; -use log::info; +use cfg_file::config::ConfigFile; +use log::{info, warn}; +use serde::{Deserialize, Serialize}; use tcp_connection::error::TcpTargetError; +use vcs_data::data::{local::config::LocalConfig, vault::config::VaultUuid}; + +use crate::actions::{ + auth_member, check_connection_instance, try_get_local_workspace, try_get_vault, +}; + +#[derive(Serialize, Deserialize)] +pub enum SetUpstreamVaultActionResult { + // Success + DirectedAndStained, + + // Fail + AlreadyStained, + AuthorizeFailed(String), +} #[action_gen] pub async fn set_upstream_vault_action( ctx: ActionContext, - _upstream: SocketAddr, -) -> Result<(), TcpTargetError> { + upstream: SocketAddr, +) -> Result { // Ensure the instance is available - let Some(instance) = ctx.instance() else { - return Err(TcpTargetError::NotFound( - "Connection Instance Lost.".to_string(), - )); - }; + let instance = check_connection_instance(&ctx)?; + + // Step1: Auth Member + if let Err(e) = auth_member(&ctx, instance).await { + return Ok(SetUpstreamVaultActionResult::AuthorizeFailed(e.to_string())); + } + + // Step2: Direct + if ctx.is_proc_on_remote() { + let vault = try_get_vault(&ctx)?; + instance + .lock() + .await + .write(vault.config().vault_uuid().clone()) + .await?; + } if ctx.is_proc_on_local() { - // Invoke on local - // Send the message to the server - let _ = instance.lock().await.write_text("Hello World!").await; - } else if ctx.is_proc_on_remote() { - // Remote execution - read the message from the client - let read = instance.lock().await.read_text().await?; - info!("Received: {}", read) + info!("Authorize successful. directing to upstream vault."); + + // Read the vault UUID from the instance + let vault_uuid = instance.lock().await.read::().await?; + + let local_workspace = try_get_local_workspace(&ctx)?; + let local_config = local_workspace.config(); + + let mut mut_local_config = local_config.lock().await; + if !mut_local_config.stained() { + // Stain the local workspace + mut_local_config.stain(vault_uuid); + + // Set the upstream address + mut_local_config.set_vault_addr(upstream); + + // Store the updated config + LocalConfig::write(&mut_local_config).await?; + + info!("Workspace stained!"); + return Ok(SetUpstreamVaultActionResult::DirectedAndStained); + } else { + warn!("Workspace already stained!"); + return Ok(SetUpstreamVaultActionResult::AlreadyStained); + } } - Ok(()) + Err(TcpTargetError::NoResult("No result.".to_string())) } -- cgit From 20af0f0daaa67dbb34184b5f855a75c2eb1864d3 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:21:41 +0800 Subject: Update actions.rs --- crates/vcs_actions/src/actions.rs | 113 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) (limited to 'crates') diff --git a/crates/vcs_actions/src/actions.rs b/crates/vcs_actions/src/actions.rs index 20bd037..858695a 100644 --- a/crates/vcs_actions/src/actions.rs +++ b/crates/vcs_actions/src/actions.rs @@ -1,5 +1,118 @@ +use std::sync::Arc; + +use action_system::action::ActionContext; +use tcp_connection::{error::TcpTargetError, instance::ConnectionInstance}; +use tokio::sync::Mutex; +use vcs_data::{ + constants::SERVER_PATH_MEMBER_PUB, + data::{local::LocalWorkspace, user::UserDirectory, vault::Vault}, +}; + pub mod local_actions; pub mod sheet_actions; pub mod user_actions; pub mod vault_actions; pub mod virtual_file_actions; + +/// Check if the connection instance is valid in the given context. +/// This function is used to verify the connection instance in actions that require remote calls. +pub fn check_connection_instance( + ctx: &ActionContext, +) -> Result<&Arc>, TcpTargetError> { + let Some(instance) = ctx.instance() else { + return Err(TcpTargetError::NotFound( + "Connection instance lost.".to_string(), + )); + }; + Ok(instance) +} + +/// Try to get the Vault instance from the context. +pub fn try_get_vault(ctx: &ActionContext) -> Result, TcpTargetError> { + let Some(vault) = ctx.get_arc::() else { + return Err(TcpTargetError::NotFound( + "Vault instance not found".to_string(), + )); + }; + Ok(vault) +} + +/// Try to get the LocalWorkspace instance from the context. +pub fn try_get_local_workspace(ctx: &ActionContext) -> Result, TcpTargetError> { + let Some(local_workspace) = ctx.get_arc::() else { + return Err(TcpTargetError::NotFound( + "LocalWorkspace instance not found".to_string(), + )); + }; + Ok(local_workspace) +} + +/// Try to get the UserDirectory instance from the context. +pub fn try_get_user_directory(ctx: &ActionContext) -> Result, TcpTargetError> { + let Some(user_directory) = ctx.get_arc::() else { + return Err(TcpTargetError::NotFound( + "UserDirectory instance not found".to_string(), + )); + }; + Ok(user_directory) +} + +/// Authenticate member based on whether the process is running locally or remotely +pub async fn auth_member( + ctx: &ActionContext, + instance: &Arc>, +) -> Result<(), TcpTargetError> { + // Start Challenge (Remote) + if ctx.is_proc_on_remote() { + let vault = try_get_vault(ctx)?; + let result = instance + .lock() + .await + .challenge(vault.vault_path().join(SERVER_PATH_MEMBER_PUB)) + .await; + + return match result { + Ok(pass) => { + if !pass { + // Send false to inform the client that authentication failed + instance.lock().await.write(false).await?; + Err(TcpTargetError::Authentication( + "Authenticate failed.".to_string(), + )) + } else { + // Send true to inform the client that authentication was successful + instance.lock().await.write(true).await?; + Ok(()) + } + } + Err(e) => Err(e), + }; + } + + // Accept Challenge (Local) + if ctx.is_proc_on_local() { + let local_workspace = try_get_local_workspace(ctx)?; + let user_directory = try_get_user_directory(ctx)?; + + // Member name & Private key + let member_name = local_workspace.config().lock().await.current_account(); + let private_key = user_directory.account_private_key_path(&member_name); + let _ = instance + .lock() + .await + .accept_challenge(private_key, &member_name) + .await?; + + // Read result + let challenge_result = instance.lock().await.read::().await?; + if challenge_result { + return Ok(()); + } else { + return Err(TcpTargetError::Authentication( + "Authenticate failed.".to_string(), + )); + } + } + + Err(TcpTargetError::NoResult("Auth failed.".to_string())) +} -- cgit From b48073e4c9a2ca80dd0503efc5b6ab121d504028 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:23:00 +0800 Subject: feat: Add port override capability to server entry The server_entry function now accepts an optional port_override parameter that allows specifying a custom port instead of using the configured port from vault configuration. When port_override is greater than 0, it takes precedence over the configured port. --- .../vcs_actions/src/connection/action_service.rs | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'crates') diff --git a/crates/vcs_actions/src/connection/action_service.rs b/crates/vcs_actions/src/connection/action_service.rs index 3786fc5..d9ddaab 100644 --- a/crates/vcs_actions/src/connection/action_service.rs +++ b/crates/vcs_actions/src/connection/action_service.rs @@ -21,21 +21,23 @@ use crate::{ }; // Start the server with a Vault using the specified directory -pub async fn server_entry(vault_path: impl Into) -> Result<(), TcpTargetError> { +pub async fn server_entry( + vault_path: impl Into, + port_override: u16, +) -> Result<(), TcpTargetError> { // Read the vault cfg let vault_cfg = VaultConfig::read().await?; // Create TCPListener - let listener = create_tcp_listener(&vault_cfg).await?; + let listener = create_tcp_listener(&vault_cfg, port_override).await?; // Initialize the vault let vault: Arc = init_vault(vault_cfg, vault_path.into()).await?; // Lock the vault - vault.lock().map_err(|e| { - error!("{}", e); - TcpTargetError::Locked(e.to_string()) - })?; + vault + .lock() + .map_err(|e| TcpTargetError::Locked(e.to_string()))?; // Create ActionPool let action_pool: Arc = Arc::new(server_action_pool()); @@ -50,9 +52,17 @@ pub async fn server_entry(vault_path: impl Into) -> Result<(), TcpTarge Ok(()) } -async fn create_tcp_listener(cfg: &VaultConfig) -> Result { +async fn create_tcp_listener( + cfg: &VaultConfig, + port_override: u16, +) -> Result { let local_bind_addr = cfg.server_config().local_bind(); - let bind_port = cfg.server_config().port(); + let port = if port_override > 0 { + port_override // Override -> PORT > 0 + } else { + cfg.server_config().port() // Default -> Port = 0 + }; + let bind_port = port; let sock_addr = SocketAddr::new(*local_bind_addr, bind_port); let listener = TcpListener::bind(sock_addr).await?; -- cgit From 805a2e38b09267f57213310af602c4ad4b51a5ac Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:23:48 +0800 Subject: Add LocalWorkspace and UserDirectory to client ActionContext Initialize and insert Arc-wrapped LocalWorkspace and UserDirectory instances into the ActionContext for client environment actions. This provides workspace and user directory data to actions running in client mode. The LocalWorkspace is initialized from the current directory's local config, while UserDirectory uses the current document directory. Both are wrapped in Arc for efficient sharing across the action execution. --- crates/vcs_actions/src/registry/client_registry.rs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'crates') diff --git a/crates/vcs_actions/src/registry/client_registry.rs b/crates/vcs_actions/src/registry/client_registry.rs index 9769750..982a9f9 100644 --- a/crates/vcs_actions/src/registry/client_registry.rs +++ b/crates/vcs_actions/src/registry/client_registry.rs @@ -1,5 +1,12 @@ +use std::sync::Arc; + use action_system::{action::ActionContext, action_pool::ActionPool}; +use cfg_file::config::ConfigFile; use tcp_connection::error::TcpTargetError; +use vcs_data::data::{ + local::{LocalWorkspace, config::LocalConfig}, + user::UserDirectory, +}; use crate::{ actions::local_actions::register_set_upstream_vault_action, @@ -36,6 +43,33 @@ async fn on_proc_begin( let action_name = ctx.action_name().to_string(); let action_args_json = ctx.action_args_json().clone(); + // Insert LocalWorkspace Arc + let Ok(local_config) = LocalConfig::read().await else { + return Err(TcpTargetError::NotFound( + "The current directory does not have a local workspace".to_string(), + )); + }; + let local_workspace = match LocalWorkspace::init_current_dir(local_config) { + Some(workspace) => workspace, + None => { + return Err(TcpTargetError::NotFound(format!( + "Failed to initialize local workspace.", + ))); + } + }; + let local_workspace_arc = Arc::new(local_workspace); + ctx.insert_arc_data(local_workspace_arc); + + // Insert UserDirectory Arc + let Some(user_directory) = UserDirectory::current_doc_dir() else { + return Err(TcpTargetError::NotFound( + "The user directory does not exist.".to_string(), + )); + }; + + let user_directory_arc = Arc::new(user_directory); + ctx.insert_arc_data(user_directory_arc); + // Get instance let Some(instance) = ctx.instance() else { return Err(TcpTargetError::Unsupported( -- cgit From eb167d3792e6af987425508dc806595f6be1f79c Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:24:46 +0800 Subject: Make config fields thread-safe with Arc - Change LocalWorkspace config to Arc> - Change Vault config to Arc - Add config accessor methods for both structs - Update initialization methods to wrap config in Arc/Mutex --- crates/vcs_data/src/data/local.rs | 21 ++++++++++++++++----- crates/vcs_data/src/data/vault.rs | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) (limited to 'crates') diff --git a/crates/vcs_data/src/data/local.rs b/crates/vcs_data/src/data/local.rs index c93bd2b..fb43042 100644 --- a/crates/vcs_data/src/data/local.rs +++ b/crates/vcs_data/src/data/local.rs @@ -1,7 +1,7 @@ -use std::{env::current_dir, path::PathBuf}; +use std::{env::current_dir, path::PathBuf, sync::Arc}; use cfg_file::config::ConfigFile; -use tokio::fs; +use tokio::{fs, sync::Mutex}; use crate::{ constants::{CLIENT_FILE_README, CLIENT_FILE_WORKSPACE}, @@ -12,7 +12,7 @@ use crate::{ pub mod config; pub struct LocalWorkspace { - config: LocalConfig, + config: Arc>, local_path: PathBuf, } @@ -25,13 +25,19 @@ impl LocalWorkspace { /// Initialize local workspace. pub fn init(config: LocalConfig, local_path: impl Into) -> Option { let local_path = find_local_path(local_path)?; - Some(Self { config, local_path }) + Some(Self { + config: Arc::new(Mutex::new(config)), + local_path, + }) } /// Initialize local workspace in the current directory. pub fn init_current_dir(config: LocalConfig) -> Option { let local_path = current_local_path()?; - Some(Self { config, local_path }) + Some(Self { + config: Arc::new(Mutex::new(config)), + local_path, + }) } /// Setup local workspace @@ -92,6 +98,11 @@ Without these credentials, the server will reject all access requests. Ok(()) } + /// Get a reference to the local configuration. + pub fn config(&self) -> Arc> { + self.config.clone() + } + /// Setup local workspace in current directory pub async fn setup_local_workspace_current_dir() -> Result<(), std::io::Error> { Self::setup_local_workspace(current_dir()?).await?; diff --git a/crates/vcs_data/src/data/vault.rs b/crates/vcs_data/src/data/vault.rs index 80ebe1d..efb4eec 100644 --- a/crates/vcs_data/src/data/vault.rs +++ b/crates/vcs_data/src/data/vault.rs @@ -2,6 +2,7 @@ use std::{ env::current_dir, fs::{self, create_dir_all}, path::PathBuf, + sync::Arc, }; use cfg_file::config::ConfigFile; @@ -22,7 +23,7 @@ pub mod sheets; pub mod virtual_file; pub struct Vault { - config: VaultConfig, + config: Arc, vault_path: PathBuf, } @@ -35,13 +36,19 @@ impl Vault { /// Initialize vault pub fn init(config: VaultConfig, vault_path: impl Into) -> Option { let vault_path = find_vault_path(vault_path)?; - Some(Self { config, vault_path }) + Some(Self { + config: Arc::new(config), + vault_path, + }) } /// Initialize vault pub fn init_current_dir(config: VaultConfig) -> Option { let vault_path = current_vault_path()?; - Some(Self { config, vault_path }) + Some(Self { + config: Arc::new(config), + vault_path, + }) } /// Setup vault @@ -144,4 +151,9 @@ Thank you for using `JustEnoughVCS!` Self::setup_vault(current_dir()?).await?; Ok(()) } + + /// Get vault configuration + pub fn config(&self) -> &Arc { + &self.config + } } -- cgit From 05de2da565a32ab3b144f7b95860897a13c895f7 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:25:05 +0800 Subject: Improve vault lock error message and formatting - Use clearer error message when vault is already locked - Fix code formatting for consistency - Remove unnecessary line breaks in error formatting --- crates/vcs_data/src/data/vault/service.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'crates') diff --git a/crates/vcs_data/src/data/vault/service.rs b/crates/vcs_data/src/data/vault/service.rs index 22e91d5..3f59c30 100644 --- a/crates/vcs_data/src/data/vault/service.rs +++ b/crates/vcs_data/src/data/vault/service.rs @@ -9,7 +9,7 @@ impl Vault { } /// Check if the current Vault is locked - pub fn is_locked(&self) -> bool { + pub fn is_locked(&self) -> bool { self.lock_file_path().exists() } @@ -19,10 +19,7 @@ impl Vault { return Err(std::io::Error::new( std::io::ErrorKind::AlreadyExists, format!( - "Vault is already locked at {}. \ - To unlock, please stop any running services. \ - If you are certain no services are running, \ - please delete this file", + "Vault is locked! This indicates a service is already running here.\nPlease stop other services or delete the lock file at the vault root directory: {}", self.lock_file_path().display() ), )); @@ -34,9 +31,10 @@ impl Vault { /// Unlock the current Vault pub fn unlock(&self) -> Result<(), std::io::Error> { if let Err(e) = std::fs::remove_file(self.lock_file_path()) - && e.kind() != std::io::ErrorKind::NotFound { - return Err(e); - } + && e.kind() != std::io::ErrorKind::NotFound + { + return Err(e); + } Ok(()) } } -- cgit From 50945b098e3f6ff16f3f4cf25c2835ddf1e7b3a8 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:25:57 +0800 Subject: Apply clippy suggestions - Use dereferenced UUID instead of cloning - Simplify error message formatting --- crates/vcs_actions/src/actions/local_actions.rs | 2 +- crates/vcs_actions/src/registry/client_registry.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'crates') diff --git a/crates/vcs_actions/src/actions/local_actions.rs b/crates/vcs_actions/src/actions/local_actions.rs index 1cf5772..3027218 100644 --- a/crates/vcs_actions/src/actions/local_actions.rs +++ b/crates/vcs_actions/src/actions/local_actions.rs @@ -40,7 +40,7 @@ pub async fn set_upstream_vault_action( instance .lock() .await - .write(vault.config().vault_uuid().clone()) + .write(*vault.config().vault_uuid()) .await?; } diff --git a/crates/vcs_actions/src/registry/client_registry.rs b/crates/vcs_actions/src/registry/client_registry.rs index 982a9f9..6f820e6 100644 --- a/crates/vcs_actions/src/registry/client_registry.rs +++ b/crates/vcs_actions/src/registry/client_registry.rs @@ -52,9 +52,7 @@ async fn on_proc_begin( let local_workspace = match LocalWorkspace::init_current_dir(local_config) { Some(workspace) => workspace, None => { - return Err(TcpTargetError::NotFound(format!( - "Failed to initialize local workspace.", - ))); + return Err(TcpTargetError::NotFound("Failed to initialize local workspace.".to_string())); } }; let local_workspace_arc = Arc::new(local_workspace); -- cgit