feat: Vendor pezkuwi-subxt and pezkuwi-zombienet-sdk into monorepo

- Add pezkuwi-subxt crates to vendor/pezkuwi-subxt
- Add pezkuwi-zombienet-sdk crates to vendor/pezkuwi-zombienet-sdk
- Convert git dependencies to path dependencies
- Add vendor crates to workspace members
- Remove test/example crates from vendor (not needed for SDK)
- Fix feature propagation issues detected by zepter
- Fix workspace inheritance for internal dependencies
- All 606 crates now in workspace
- All 6919 internal dependency links verified correct
- No git dependencies remaining
This commit is contained in:
2025-12-22 23:31:24 +03:00
parent 4c8f281051
commit 70ddb6516f
386 changed files with 76759 additions and 36 deletions
@@ -0,0 +1,374 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, Weak},
};
use async_trait::async_trait;
use support::fs::FileSystem;
use tokio::sync::RwLock;
use tracing::{trace, warn};
use uuid::Uuid;
use super::node::{NativeNode, NativeNodeOptions};
use crate::{
constants::NAMESPACE_PREFIX,
native::{node::DeserializableNativeNodeOptions, provider},
shared::helpers::extract_execution_result,
types::{
GenerateFileCommand, GenerateFilesOptions, ProviderCapabilities, RunCommandOptions,
SpawnNodeOptions,
},
DynNode, NativeProvider, ProviderError, ProviderNamespace, ProviderNode,
};
pub(super) struct NativeNamespace<FS>
where
FS: FileSystem + Send + Sync + Clone,
{
weak: Weak<NativeNamespace<FS>>,
name: String,
provider: Weak<NativeProvider<FS>>,
base_dir: PathBuf,
capabilities: ProviderCapabilities,
filesystem: FS,
pub(super) nodes: RwLock<HashMap<String, Arc<NativeNode<FS>>>>,
}
impl<FS> NativeNamespace<FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
pub(super) async fn new(
provider: &Weak<NativeProvider<FS>>,
tmp_dir: &PathBuf,
capabilities: &ProviderCapabilities,
filesystem: &FS,
custom_base_dir: Option<&Path>,
) -> Result<Arc<Self>, ProviderError> {
let name = format!("{}{}", NAMESPACE_PREFIX, Uuid::new_v4());
let base_dir = if let Some(custom_base_dir) = custom_base_dir {
if !filesystem.exists(custom_base_dir).await {
filesystem.create_dir_all(custom_base_dir).await?;
} else {
warn!(
"⚠️ Using and existing directory {} as base dir",
custom_base_dir.to_string_lossy()
);
}
PathBuf::from(custom_base_dir)
} else {
let base_dir = PathBuf::from_iter([tmp_dir, &PathBuf::from(&name)]);
filesystem.create_dir(&base_dir).await?;
base_dir
};
Ok(Arc::new_cyclic(|weak| NativeNamespace {
weak: weak.clone(),
provider: provider.clone(),
name,
base_dir,
capabilities: capabilities.clone(),
filesystem: filesystem.clone(),
nodes: RwLock::new(HashMap::new()),
}))
}
pub(super) async fn attach_to_live(
provider: &Weak<NativeProvider<FS>>,
capabilities: &ProviderCapabilities,
filesystem: &FS,
custom_base_dir: &Path,
name: &str,
) -> Result<Arc<Self>, ProviderError> {
let base_dir = custom_base_dir.to_path_buf();
Ok(Arc::new_cyclic(|weak| NativeNamespace {
weak: weak.clone(),
provider: provider.clone(),
name: name.to_string(),
base_dir,
capabilities: capabilities.clone(),
filesystem: filesystem.clone(),
nodes: RwLock::new(HashMap::new()),
}))
}
}
#[async_trait]
impl<FS> ProviderNamespace for NativeNamespace<FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
fn name(&self) -> &str {
&self.name
}
fn base_dir(&self) -> &PathBuf {
&self.base_dir
}
fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities
}
fn provider_name(&self) -> &str {
provider::PROVIDER_NAME
}
async fn nodes(&self) -> HashMap<String, DynNode> {
self.nodes
.read()
.await
.iter()
.map(|(name, node)| (name.clone(), node.clone() as DynNode))
.collect()
}
async fn get_node_available_args(
&self,
(command, _image): (String, Option<String>),
) -> Result<String, ProviderError> {
let temp_node = self
.spawn_node(
&SpawnNodeOptions::new(format!("temp-{}", Uuid::new_v4()), "bash".to_string())
.args(vec!["-c", "while :; do sleep 1; done"]),
)
.await?;
let available_args_output = temp_node
.run_command(RunCommandOptions::new(command.clone()).args(vec!["--help"]))
.await?
.map_err(|(_exit, status)| {
ProviderError::NodeAvailableArgsError("".to_string(), command, status)
})?;
temp_node.destroy().await?;
Ok(available_args_output)
}
async fn spawn_node(&self, options: &SpawnNodeOptions) -> Result<DynNode, ProviderError> {
trace!("spawn node options {options:?}");
let node = NativeNode::new(NativeNodeOptions {
namespace: &self.weak,
namespace_base_dir: &self.base_dir,
name: &options.name,
program: &options.program,
args: &options.args,
env: &options.env,
startup_files: &options.injected_files,
created_paths: &options.created_paths,
db_snapshot: options.db_snapshot.as_ref(),
filesystem: &self.filesystem,
node_log_path: options.node_log_path.as_ref(),
})
.await?;
self.nodes
.write()
.await
.insert(options.name.clone(), node.clone());
Ok(node)
}
async fn spawn_node_from_json(
&self,
json_value: &serde_json::Value,
) -> Result<DynNode, ProviderError> {
let deserializable: DeserializableNativeNodeOptions =
serde_json::from_value(json_value.clone())?;
let options = NativeNodeOptions::from_deserializable(
&deserializable,
&self.weak,
&self.base_dir,
&self.filesystem,
);
let pid = json_value
.get("process_handle")
.and_then(|v| v.as_i64())
.ok_or_else(|| ProviderError::InvalidConfig("Missing pid field".to_string()))?
as i32;
let node = NativeNode::attach_to_live(options, pid).await?;
self.nodes
.write()
.await
.insert(node.name().to_string(), node.clone());
Ok(node)
}
async fn generate_files(&self, options: GenerateFilesOptions) -> Result<(), ProviderError> {
let node_name = if let Some(name) = options.temp_name {
name
} else {
format!("temp-{}", Uuid::new_v4())
};
// we spawn a node doing nothing but looping so we can execute our commands
let temp_node = self
.spawn_node(
&SpawnNodeOptions::new(node_name, "bash".to_string())
.args(vec!["-c", "while :; do sleep 1; done"])
.injected_files(options.injected_files),
)
.await?;
for GenerateFileCommand {
program,
args,
env,
local_output_path,
} in options.commands
{
trace!(
"🏗 building file {:?} in path {} with command {} {}",
local_output_path.as_os_str(),
self.base_dir.to_string_lossy(),
program,
args.join(" ")
);
let local_output_full_path = format!(
"{}{}{}",
self.base_dir.to_string_lossy(),
if local_output_path.starts_with("/") {
""
} else {
"/"
},
local_output_path.to_string_lossy()
);
let contents = extract_execution_result(
&temp_node,
RunCommandOptions { program, args, env },
options.expected_path.as_ref(),
)
.await?;
self.filesystem
.write(local_output_full_path, contents)
.await
.map_err(|err| ProviderError::FileGenerationFailed(err.into()))?;
}
temp_node.destroy().await
}
async fn static_setup(&self) -> Result<(), ProviderError> {
// no static setup exists for native provider
todo!()
}
async fn destroy(&self) -> Result<(), ProviderError> {
let mut names = vec![];
for node in self.nodes.read().await.values() {
node.abort()
.await
.map_err(|err| ProviderError::DestroyNodeFailed(node.name().to_string(), err))?;
names.push(node.name().to_string());
}
let mut nodes = self.nodes.write().await;
for name in names {
nodes.remove(&name);
}
if let Some(provider) = self.provider.upgrade() {
provider.namespaces.write().await.remove(&self.name);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use support::fs::local::LocalFileSystem;
use super::*;
use crate::{
types::{GenerateFileCommand, GenerateFilesOptions},
NativeProvider, Provider,
};
fn unique_temp_dir() -> PathBuf {
let mut base = std::env::temp_dir();
base.push(format!("znet_native_ns_test_{}", uuid::Uuid::new_v4()));
base
}
#[tokio::test]
async fn generate_files_uses_expected_path_when_provided() {
let fs = LocalFileSystem;
let provider = NativeProvider::new(fs.clone());
let base_dir = unique_temp_dir();
// Namespace builder will create directory if needed
let ns = provider
.create_namespace_with_base_dir(&base_dir)
.await
.expect("namespace should be created");
// Create a unique on-host path that the native node will write to
let expected_path =
std::env::temp_dir().join(format!("znet_expected_{}.json", uuid::Uuid::new_v4()));
// Command will write JSON into expected_path; stdout will be something else to ensure we don't read it
let program = "bash".to_string();
let script = format!(
"echo -n '{{\"hello\":\"world\"}}' > {} && echo should_not_be_used",
expected_path.to_string_lossy()
);
let args: Vec<String> = vec!["-lc".into(), script];
let out_name = PathBuf::from("result_expected.json");
let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args);
let options = GenerateFilesOptions::new(vec![cmd], None, Some(expected_path.clone()));
ns.generate_files(options)
.await
.expect("generation should succeed");
// Read produced file from namespace base_dir
let produced_path = base_dir.join(out_name);
let produced = fs
.read_to_string(&produced_path)
.await
.expect("should read produced file");
assert_eq!(produced, "{\"hello\":\"world\"}");
}
#[tokio::test]
async fn generate_files_uses_stdout_when_expected_path_absent() {
let fs = LocalFileSystem;
let provider = NativeProvider::new(fs.clone());
let base_dir = unique_temp_dir();
let ns = provider
.create_namespace_with_base_dir(&base_dir)
.await
.expect("namespace should be created");
// Command prints to stdout only
let program = "bash".to_string();
let args: Vec<String> = vec!["-lc".into(), "echo -n 42".into()];
let out_name = PathBuf::from("result_stdout.txt");
let cmd = GenerateFileCommand::new(program, out_name.clone()).args(args);
let options = GenerateFilesOptions::new(vec![cmd], None, None);
ns.generate_files(options)
.await
.expect("generation should succeed");
let produced_path = base_dir.join(out_name);
let produced = fs
.read_to_string(&produced_path)
.await
.expect("should read produced file");
assert_eq!(produced, "42");
}
}
@@ -0,0 +1,734 @@
use std::{
collections::HashMap,
env,
path::{Path, PathBuf},
process::Stdio,
sync::{Arc, Weak},
time::Duration,
};
use anyhow::anyhow;
use async_trait::async_trait;
use configuration::types::AssetLocation;
use flate2::read::GzDecoder;
use futures::future::try_join_all;
use nix::{
sys::signal::{kill, Signal},
unistd::Pid,
};
use serde::{ser::Error, Deserialize, Serialize, Serializer};
use sha2::Digest;
use support::{constants::THIS_IS_A_BUG, fs::FileSystem};
use tar::Archive;
use tokio::{
fs,
io::{AsyncRead, AsyncReadExt, BufReader},
process::{Child, ChildStderr, ChildStdout, Command},
sync::{
mpsc::{self, Sender},
RwLock,
},
task::JoinHandle,
time::sleep,
try_join,
};
use tracing::trace;
use super::namespace::NativeNamespace;
use crate::{
constants::{NODE_CONFIG_DIR, NODE_DATA_DIR, NODE_RELAY_DATA_DIR, NODE_SCRIPTS_DIR},
native,
types::{ExecutionResult, RunCommandOptions, RunScriptOptions, TransferedFile},
ProviderError, ProviderNamespace, ProviderNode,
};
pub(super) struct NativeNodeOptions<'a, FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
pub(super) namespace: &'a Weak<NativeNamespace<FS>>,
pub(super) namespace_base_dir: &'a PathBuf,
pub(super) name: &'a str,
pub(super) program: &'a str,
pub(super) args: &'a [String],
pub(super) env: &'a [(String, String)],
pub(super) startup_files: &'a [TransferedFile],
pub(super) created_paths: &'a [PathBuf],
pub(super) db_snapshot: Option<&'a AssetLocation>,
pub(super) filesystem: &'a FS,
pub(super) node_log_path: Option<&'a PathBuf>,
}
impl<'a, FS> NativeNodeOptions<'a, FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
pub(super) fn from_deserializable(
deserializable: &'a DeserializableNativeNodeOptions,
namespace: &'a Weak<NativeNamespace<FS>>,
namespace_base_dir: &'a PathBuf,
filesystem: &'a FS,
) -> NativeNodeOptions<'a, FS> {
NativeNodeOptions {
namespace,
namespace_base_dir,
name: &deserializable.name,
program: &deserializable.program,
args: &deserializable.args,
env: &deserializable.env,
startup_files: &[],
created_paths: &[],
db_snapshot: None,
filesystem,
node_log_path: deserializable.node_log_path.as_ref(),
}
}
}
#[derive(Deserialize)]
pub(super) struct DeserializableNativeNodeOptions {
pub name: String,
pub program: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub node_log_path: Option<PathBuf>,
}
enum ProcessHandle {
Spawned(Child, Pid),
Attached(Pid),
}
#[derive(Serialize)]
pub(super) struct NativeNode<FS>
where
FS: FileSystem + Send + Sync + Clone,
{
#[serde(skip)]
namespace: Weak<NativeNamespace<FS>>,
name: String,
program: String,
args: Vec<String>,
env: Vec<(String, String)>,
base_dir: PathBuf,
config_dir: PathBuf,
data_dir: PathBuf,
relay_data_dir: PathBuf,
scripts_dir: PathBuf,
log_path: PathBuf,
#[serde(serialize_with = "serialize_process_handle")]
// using RwLock from std to serialize properly, generally using sync locks is ok in async code as long as they
// are not held across await points
process_handle: std::sync::RwLock<Option<ProcessHandle>>,
#[serde(skip)]
stdout_reading_task: RwLock<Option<JoinHandle<()>>>,
#[serde(skip)]
stderr_reading_task: RwLock<Option<JoinHandle<()>>>,
#[serde(skip)]
log_writing_task: RwLock<Option<JoinHandle<()>>>,
#[serde(skip)]
filesystem: FS,
provider_tag: String,
}
impl<FS> NativeNode<FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
pub(super) async fn new(
options: NativeNodeOptions<'_, FS>,
) -> Result<Arc<Self>, ProviderError> {
let filesystem = options.filesystem.clone();
let base_dir =
PathBuf::from_iter([options.namespace_base_dir, &PathBuf::from(options.name)]);
trace!("creating base_dir {:?}", base_dir);
options.filesystem.create_dir_all(&base_dir).await?;
trace!("created base_dir {:?}", base_dir);
let base_dir_raw = base_dir.to_string_lossy();
let config_dir = PathBuf::from(format!("{base_dir_raw}{NODE_CONFIG_DIR}"));
let data_dir = PathBuf::from(format!("{base_dir_raw}{NODE_DATA_DIR}"));
let relay_data_dir = PathBuf::from(format!("{base_dir_raw}{NODE_RELAY_DATA_DIR}"));
let scripts_dir = PathBuf::from(format!("{base_dir_raw}{NODE_SCRIPTS_DIR}"));
let log_path = options
.node_log_path
.cloned()
.unwrap_or_else(|| base_dir.join(format!("{}.log", options.name)));
trace!("creating dirs {:?}", config_dir);
try_join!(
filesystem.create_dir_all(&config_dir),
filesystem.create_dir_all(&data_dir),
filesystem.create_dir_all(&relay_data_dir),
filesystem.create_dir_all(&scripts_dir),
)?;
trace!("created!");
let node = Arc::new(NativeNode {
namespace: options.namespace.clone(),
name: options.name.to_string(),
program: options.program.to_string(),
args: options.args.to_vec(),
env: options.env.to_vec(),
base_dir,
config_dir,
data_dir,
relay_data_dir,
scripts_dir,
log_path,
process_handle: std::sync::RwLock::new(None),
stdout_reading_task: RwLock::new(None),
stderr_reading_task: RwLock::new(None),
log_writing_task: RwLock::new(None),
filesystem: filesystem.clone(),
provider_tag: native::provider::PROVIDER_NAME.to_string(),
});
node.initialize_startup_paths(options.created_paths).await?;
node.initialize_startup_files(options.startup_files).await?;
if let Some(db_snap) = options.db_snapshot {
node.initialize_db_snapshot(db_snap).await?;
}
let (stdout, stderr) = node.initialize_process().await?;
node.initialize_log_writing(stdout, stderr).await;
Ok(node)
}
pub(super) async fn attach_to_live(
options: NativeNodeOptions<'_, FS>,
pid: i32,
) -> Result<Arc<Self>, ProviderError> {
let filesystem = options.filesystem.clone();
let base_dir =
PathBuf::from_iter([options.namespace_base_dir, &PathBuf::from(options.name)]);
trace!("creating base_dir {:?}", base_dir);
options.filesystem.create_dir_all(&base_dir).await?;
trace!("created base_dir {:?}", base_dir);
let base_dir_raw = base_dir.to_string_lossy();
let config_dir = PathBuf::from(format!("{base_dir_raw}{NODE_CONFIG_DIR}"));
let data_dir = PathBuf::from(format!("{base_dir_raw}{NODE_DATA_DIR}"));
let relay_data_dir = PathBuf::from(format!("{base_dir_raw}{NODE_RELAY_DATA_DIR}"));
let scripts_dir = PathBuf::from(format!("{base_dir_raw}{NODE_SCRIPTS_DIR}"));
let log_path = options
.node_log_path
.cloned()
.unwrap_or_else(|| base_dir.join(format!("{}.log", options.name)));
let pid = Pid::from_raw(pid);
let node = Arc::new(NativeNode {
namespace: options.namespace.clone(),
name: options.name.to_string(),
program: options.program.to_string(),
args: options.args.to_vec(),
env: options.env.to_vec(),
base_dir,
config_dir,
data_dir,
relay_data_dir,
scripts_dir,
log_path,
process_handle: std::sync::RwLock::new(Some(ProcessHandle::Attached(pid))),
stdout_reading_task: RwLock::new(None),
stderr_reading_task: RwLock::new(None),
log_writing_task: RwLock::new(None),
filesystem: filesystem.clone(),
provider_tag: native::provider::PROVIDER_NAME.to_string(),
});
Ok(node)
}
async fn initialize_startup_paths(&self, paths: &[PathBuf]) -> Result<(), ProviderError> {
trace!("creating paths {:?}", paths);
let base_dir_raw = self.base_dir.to_string_lossy();
try_join_all(paths.iter().map(|file| {
let full_path = format!("{base_dir_raw}{}", file.to_string_lossy());
self.filesystem.create_dir_all(full_path)
}))
.await?;
trace!("paths created!");
Ok(())
}
async fn initialize_startup_files(
&self,
startup_files: &[TransferedFile],
) -> Result<(), ProviderError> {
trace!("creating files {:?}", startup_files);
try_join_all(
startup_files
.iter()
.map(|file| self.send_file(&file.local_path, &file.remote_path, &file.mode)),
)
.await?;
trace!("files created!");
Ok(())
}
async fn initialize_db_snapshot(
&self,
db_snapshot: &AssetLocation,
) -> Result<(), ProviderError> {
trace!("snap: {db_snapshot}");
// check if we need to get the db or is already in the ns
let ns_base_dir = self.namespace_base_dir();
let hashed_location = match db_snapshot {
AssetLocation::Url(location) => hex::encode(sha2::Sha256::digest(location.to_string())),
AssetLocation::FilePath(filepath) => {
hex::encode(sha2::Sha256::digest(filepath.to_string_lossy().to_string()))
},
};
let full_path = format!("{ns_base_dir}/{hashed_location}.tgz");
trace!("db_snap fullpath in ns: {full_path}");
if !self.filesystem.exists(&full_path).await {
// needs to download/copy
self.get_db_snapshot(db_snapshot, &full_path).await?;
}
let contents = self.filesystem.read(&full_path).await.unwrap();
let gz = GzDecoder::new(&contents[..]);
let mut archive = Archive::new(gz);
archive
.unpack(self.base_dir.to_string_lossy().as_ref())
.unwrap();
if std::env::var("ZOMBIE_RM_TGZ_AFTER_EXTRACT").is_ok() {
let res = fs::remove_file(&full_path).await;
trace!("removing {}, result {:?}", full_path, res);
}
Ok(())
}
async fn get_db_snapshot(
&self,
location: &AssetLocation,
full_path: &str,
) -> Result<(), ProviderError> {
trace!("getting db_snapshot from: {:?} to: {full_path}", location);
match location {
AssetLocation::Url(location) => {
let res = reqwest::get(location.as_ref())
.await
.map_err(|err| ProviderError::DownloadFile(location.to_string(), err.into()))?;
let contents: &[u8] = &res.bytes().await.unwrap();
trace!("writing: {full_path}");
self.filesystem.write(full_path, contents).await?;
},
AssetLocation::FilePath(filepath) => {
self.filesystem.copy(filepath, full_path).await?;
},
};
Ok(())
}
async fn initialize_process(&self) -> Result<(ChildStdout, ChildStderr), ProviderError> {
let filtered_env: HashMap<String, String> = env::vars()
.filter(|(k, _)| k == "TZ" || k == "LANG" || k == "PATH")
.collect();
let mut process = Command::new(&self.program)
.args(&self.args)
.env_clear()
.envs(&filtered_env) // minimal environment
.envs(self.env.to_vec())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.current_dir(&self.base_dir)
.spawn()
.map_err(|err| ProviderError::NodeSpawningFailed(self.name.to_string(), err.into()))?;
let stdout = process
.stdout
.take()
.expect(&format!("infaillible, stdout is piped {THIS_IS_A_BUG}"));
let stderr = process
.stderr
.take()
.expect(&format!("infaillible, stderr is piped {THIS_IS_A_BUG}"));
let pid = Pid::from_raw(
process
.id()
.ok_or_else(|| ProviderError::ProcessIdRetrievalFailed(self.name.to_string()))?
as i32,
);
self.process_handle
.write()
.map_err(|_e| ProviderError::FailedToAcquireLock(self.name.clone()))?
.replace(ProcessHandle::Spawned(process, pid));
Ok((stdout, stderr))
}
async fn initialize_log_writing(&self, stdout: ChildStdout, stderr: ChildStderr) {
let (stdout_tx, mut rx) = mpsc::channel(10);
let stderr_tx = stdout_tx.clone();
self.stdout_reading_task
.write()
.await
.replace(self.create_stream_polling_task(stdout, stdout_tx));
self.stderr_reading_task
.write()
.await
.replace(self.create_stream_polling_task(stderr, stderr_tx));
let filesystem = self.filesystem.clone();
let log_path = self.log_path.clone();
self.log_writing_task
.write()
.await
.replace(tokio::spawn(async move {
loop {
while let Some(Ok(data)) = rx.recv().await {
// TODO: find a better way instead of ignoring error ?
let _ = filesystem.append(&log_path, data).await;
}
sleep(Duration::from_millis(250)).await;
}
}));
}
fn create_stream_polling_task(
&self,
stream: impl AsyncRead + Unpin + Send + 'static,
tx: Sender<Result<Vec<u8>, std::io::Error>>,
) -> JoinHandle<()> {
tokio::spawn(async move {
let mut reader = BufReader::new(stream);
let mut buffer = vec![0u8; 1024];
loop {
match reader.read(&mut buffer).await {
Ok(0) => {
let _ = tx.send(Ok(Vec::new())).await;
break;
},
Ok(n) => {
let _ = tx.send(Ok(buffer[..n].to_vec())).await;
},
Err(e) => {
let _ = tx.send(Err(e)).await;
break;
},
}
}
})
}
fn process_id(&self) -> Result<Pid, ProviderError> {
let pid = self
.process_handle
.read()
.map_err(|_e| ProviderError::FailedToAcquireLock(self.name.clone()))?
.as_ref()
.map(|handle| match handle {
ProcessHandle::Spawned(_, pid) => *pid,
ProcessHandle::Attached(pid) => *pid,
})
.ok_or_else(|| ProviderError::ProcessIdRetrievalFailed(self.name.to_string()))?;
Ok(pid)
}
pub(crate) async fn abort(&self) -> anyhow::Result<()> {
if let Some(task) = self.log_writing_task.write().await.take() {
task.abort();
}
if let Some(task) = self.stdout_reading_task.write().await.take() {
task.abort();
}
if let Some(task) = self.stderr_reading_task.write().await.take() {
task.abort();
}
let process_handle = {
let mut guard = self
.process_handle
.write()
.map_err(|_e| ProviderError::FailedToAcquireLock(self.name.clone()))?;
guard
.take()
.ok_or_else(|| anyhow!("no process was attached for the node"))?
};
match process_handle {
ProcessHandle::Spawned(mut child, _pid) => {
child.kill().await?;
},
ProcessHandle::Attached(pid) => {
kill(pid, Signal::SIGKILL)
.map_err(|err| anyhow!("Failed to kill attached process {pid}: {err}"))?;
},
}
Ok(())
}
fn namespace_base_dir(&self) -> String {
self.namespace
.upgrade()
.map(|namespace| namespace.base_dir().to_string_lossy().to_string())
.unwrap_or_else(|| panic!("namespace shouldn't be dropped, {THIS_IS_A_BUG}"))
}
}
#[async_trait]
impl<FS> ProviderNode for NativeNode<FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
fn name(&self) -> &str {
&self.name
}
fn args(&self) -> Vec<&str> {
self.args.iter().map(|arg| arg.as_str()).collect()
}
fn base_dir(&self) -> &PathBuf {
&self.base_dir
}
fn config_dir(&self) -> &PathBuf {
&self.config_dir
}
fn data_dir(&self) -> &PathBuf {
&self.data_dir
}
fn relay_data_dir(&self) -> &PathBuf {
&self.relay_data_dir
}
fn scripts_dir(&self) -> &PathBuf {
&self.scripts_dir
}
fn log_path(&self) -> &PathBuf {
&self.log_path
}
fn log_cmd(&self) -> String {
format!("tail -f {}", self.log_path().to_string_lossy())
}
fn path_in_node(&self, file: &Path) -> PathBuf {
let full_path = format!(
"{}/{}",
self.base_dir.to_string_lossy(),
file.to_string_lossy()
);
PathBuf::from(full_path)
}
async fn logs(&self) -> Result<String, ProviderError> {
Ok(self.filesystem.read_to_string(&self.log_path).await?)
}
async fn dump_logs(&self, local_dest: PathBuf) -> Result<(), ProviderError> {
Ok(self.filesystem.copy(&self.log_path, local_dest).await?)
}
async fn run_command(
&self,
options: RunCommandOptions,
) -> Result<ExecutionResult, ProviderError> {
let result = Command::new(options.program.clone())
.args(options.args.clone())
.envs(options.env)
.current_dir(&self.base_dir)
.output()
.await
.map_err(|err| {
ProviderError::RunCommandError(
format!("{} {}", &options.program, &options.args.join(" ")),
"locally".to_string(),
err.into(),
)
})?;
if result.status.success() {
Ok(Ok(String::from_utf8_lossy(&result.stdout).to_string()))
} else {
Ok(Err((
result.status,
String::from_utf8_lossy(&result.stderr).to_string(),
)))
}
}
async fn run_script(
&self,
options: RunScriptOptions,
) -> Result<ExecutionResult, ProviderError> {
let local_script_path = PathBuf::from(&options.local_script_path);
if !self.filesystem.exists(&local_script_path).await {
return Err(ProviderError::ScriptNotFound(local_script_path));
}
// extract file name and build remote file path
let script_file_name = local_script_path
.file_name()
.map(|file_name| file_name.to_string_lossy().to_string())
.ok_or(ProviderError::InvalidScriptPath(anyhow!(
"Can't retrieve filename from script with path: {:?}",
options.local_script_path
)))?;
let remote_script_path = format!(
"{}/{}",
self.scripts_dir.to_string_lossy(),
script_file_name
);
// copy and set script's execute permission
self.filesystem
.copy(local_script_path, &remote_script_path)
.await?;
self.filesystem.set_mode(&remote_script_path, 0o744).await?;
// execute script
self.run_command(RunCommandOptions {
program: remote_script_path,
args: options.args,
env: options.env,
})
.await
}
async fn send_file(
&self,
local_file_path: &Path,
remote_file_path: &Path,
mode: &str,
) -> Result<(), ProviderError> {
let namespaced_remote_file_path = PathBuf::from(format!(
"{}{}",
&self.base_dir.to_string_lossy(),
remote_file_path.to_string_lossy()
));
self.filesystem
.copy(local_file_path, &namespaced_remote_file_path)
.await?;
self.run_command(
RunCommandOptions::new("chmod")
.args(vec![mode, &namespaced_remote_file_path.to_string_lossy()]),
)
.await?
.map_err(|(_, err)| {
ProviderError::SendFile(
self.name.clone(),
local_file_path.to_string_lossy().to_string(),
anyhow!("{err}"),
)
})?;
Ok(())
}
async fn receive_file(
&self,
remote_file_path: &Path,
local_file_path: &Path,
) -> Result<(), ProviderError> {
let namespaced_remote_file_path = PathBuf::from(format!(
"{}{}",
&self.base_dir.to_string_lossy(),
remote_file_path.to_string_lossy()
));
self.filesystem
.copy(namespaced_remote_file_path, local_file_path)
.await?;
Ok(())
}
async fn pause(&self) -> Result<(), ProviderError> {
let process_id = self.process_id()?;
kill(process_id, Signal::SIGSTOP)
.map_err(|err| ProviderError::PauseNodeFailed(self.name.clone(), err.into()))?;
Ok(())
}
async fn resume(&self) -> Result<(), ProviderError> {
let process_id = self.process_id()?;
nix::sys::signal::kill(process_id, Signal::SIGCONT)
.map_err(|err| ProviderError::ResumeNodeFailed(self.name.clone(), err.into()))?;
Ok(())
}
async fn restart(&self, after: Option<Duration>) -> Result<(), ProviderError> {
if let Some(duration) = after {
sleep(duration).await;
}
self.abort()
.await
.map_err(|err| ProviderError::RestartNodeFailed(self.name.clone(), err))?;
let (stdout, stderr) = self
.initialize_process()
.await
.map_err(|err| ProviderError::RestartNodeFailed(self.name.clone(), err.into()))?;
self.initialize_log_writing(stdout, stderr).await;
Ok(())
}
async fn destroy(&self) -> Result<(), ProviderError> {
self.abort()
.await
.map_err(|err| ProviderError::DestroyNodeFailed(self.name.clone(), err))?;
if let Some(namespace) = self.namespace.upgrade() {
namespace.nodes.write().await.remove(&self.name);
}
Ok(())
}
}
fn serialize_process_handle<S>(
process_handle: &std::sync::RwLock<Option<ProcessHandle>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let pid = process_handle
.read()
.map_err(|_e| S::Error::custom("failed to acquire read lock"))?
.as_ref()
.map(|handle| match handle {
ProcessHandle::Spawned(_, pid) => pid.as_raw(),
ProcessHandle::Attached(pid) => pid.as_raw(),
});
pid.serialize(serializer)
}
@@ -0,0 +1,142 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, Weak},
};
use async_trait::async_trait;
use support::fs::FileSystem;
use tokio::sync::RwLock;
use super::namespace::NativeNamespace;
use crate::{
shared::helpers::extract_namespace_info, types::ProviderCapabilities, DynNamespace, Provider,
ProviderError, ProviderNamespace,
};
pub const PROVIDER_NAME: &str = "native";
pub struct NativeProvider<FS>
where
FS: FileSystem + Send + Sync + Clone,
{
weak: Weak<NativeProvider<FS>>,
capabilities: ProviderCapabilities,
tmp_dir: PathBuf,
filesystem: FS,
pub(super) namespaces: RwLock<HashMap<String, Arc<NativeNamespace<FS>>>>,
}
impl<FS> NativeProvider<FS>
where
FS: FileSystem + Send + Sync + Clone,
{
pub fn new(filesystem: FS) -> Arc<Self> {
Arc::new_cyclic(|weak| NativeProvider {
weak: weak.clone(),
capabilities: ProviderCapabilities {
has_resources: false,
requires_image: false,
prefix_with_full_path: true,
use_default_ports_in_cmd: false,
},
// NOTE: temp_dir in linux return `/tmp` but on mac something like
// `/var/folders/rz/1cyx7hfj31qgb98d8_cg7jwh0000gn/T/`, having
// one `trailing slash` and the other no can cause issues if
// you try to build a fullpath by concatenate. Use Pathbuf to prevent the issue.
tmp_dir: std::env::temp_dir(),
filesystem,
namespaces: RwLock::new(HashMap::new()),
})
}
pub fn tmp_dir(mut self, tmp_dir: impl Into<PathBuf>) -> Self {
self.tmp_dir = tmp_dir.into();
self
}
}
#[async_trait]
impl<FS> Provider for NativeProvider<FS>
where
FS: FileSystem + Send + Sync + Clone + 'static,
{
fn name(&self) -> &str {
PROVIDER_NAME
}
fn capabilities(&self) -> &ProviderCapabilities {
&self.capabilities
}
async fn namespaces(&self) -> HashMap<String, DynNamespace> {
self.namespaces
.read()
.await
.iter()
.map(|(name, namespace)| (name.clone(), namespace.clone() as DynNamespace))
.collect()
}
async fn create_namespace(&self) -> Result<DynNamespace, ProviderError> {
let namespace = NativeNamespace::new(
&self.weak,
&self.tmp_dir,
&self.capabilities,
&self.filesystem,
None,
)
.await?;
self.namespaces
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace)
}
async fn create_namespace_with_base_dir(
&self,
base_dir: &Path,
) -> Result<DynNamespace, ProviderError> {
let namespace = NativeNamespace::new(
&self.weak,
&self.tmp_dir,
&self.capabilities,
&self.filesystem,
Some(base_dir),
)
.await?;
self.namespaces
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace)
}
async fn create_namespace_from_json(
&self,
json_value: &serde_json::Value,
) -> Result<DynNamespace, ProviderError> {
let (base_dir, name) = extract_namespace_info(json_value)?;
let namespace = NativeNamespace::attach_to_live(
&self.weak,
&self.capabilities,
&self.filesystem,
&base_dir,
&name,
)
.await?;
self.namespaces
.write()
.await
.insert(namespace.name().to_string(), namespace.clone());
Ok(namespace)
}
}