1use crate::common::types::ShurikenState;
2use crate::manager::ShurikenManager;
3use crate::utils::get_port_owner;
4use crate::{common::types::FieldValue, scripting::NinjaEngine, scripting::templater::Templater};
5use anyhow::Result;
6use log::info;
7use serde::{Deserialize, Serialize};
8use serde_json::{Value as JsonValue, json};
9use std::sync::Arc;
10use std::{
11 collections::HashMap,
12 path::{Path, PathBuf},
13};
14use tokio::{fs, sync::Mutex};
15
16#[derive(Debug, Deserialize, Serialize, Clone)]
21pub struct Tool {
22 pub name: String,
24 pub script: PathBuf,
26 pub description: Option<String>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone)]
34pub struct ShurikenConfig {
35 #[serde(rename = "config-path")]
37 pub config_path: PathBuf,
38 pub options: Option<HashMap<String, FieldValue>>,
40}
41
42#[derive(Debug, Serialize, Deserialize, Clone)]
46pub struct ShurikenMetadata {
47 pub name: String,
49 pub id: String,
51 pub version: String,
53 pub ports: Option<Vec<u16>>,
55 #[serde(rename = "check-ports")]
57 pub check_ports: Option<bool>,
58 #[serde(rename = "script-path")]
60 pub script_path: Option<PathBuf>,
61 #[serde(rename = "type")]
63 pub shuriken_type: String,
64}
65
66#[derive(Debug, Serialize, Deserialize, Clone)]
70pub struct LogsConfig {
71 #[serde(rename = "log-path")]
73 pub log_path: PathBuf,
74}
75
76async fn atomic_write_json(path: &Path, value: &JsonValue) -> Result<(), String> {
77 use tokio::fs;
78
79 let tmp_path = path.with_extension("tmp");
80
81 let data = serde_json::to_vec(value).map_err(|e| e.to_string())?;
82 fs::write(&tmp_path, data)
83 .await
84 .map_err(|e| format!("Failed to write tmp lockfile: {e}"))?;
85
86 std::fs::rename(&tmp_path, path).map_err(|e| format!("Failed to replace lockfile: {e}"))?;
88
89 Ok(())
90}
91
92#[derive(Debug, Serialize, Deserialize, Clone)]
96pub struct ShurikenToml {
97 pub shuriken: ShurikenMetadata,
99 pub config: Option<ShurikenConfig>,
101 pub logs: Option<LogsConfig>,
103 pub tools: Option<Vec<Tool>>,
105}
106
107#[derive(Debug, Serialize, Deserialize, Clone)]
112pub struct Shuriken {
113 #[serde(rename = "shuriken")]
115 pub metadata: ShurikenMetadata,
116 pub config: Option<ShurikenConfig>,
118 pub logs: Option<LogsConfig>,
120 pub tools: Option<Vec<Tool>>,
122
123 #[serde(skip)]
125 pub state: Arc<Mutex<ShurikenState>>,
126 #[serde(skip)]
128 pub dirty: Arc<Mutex<bool>>,
129}
130
131impl Shuriken {
132 pub async fn start(
147 &self,
148 engine: &NinjaEngine,
149 shuriken_dir: &Path,
150 mgr: Option<ShurikenManager>,
151 ) -> Result<(), String> {
152 info!("Starting shuriken {}", self.metadata.name);
153
154 let lock_dir = shuriken_dir.join(".ninja");
155 tokio::fs::create_dir_all(&lock_dir)
156 .await
157 .map_err(|e| format!("Failed to create .ninja directory: {}", e))?;
158 let lock_path = lock_dir.join("shuriken.lck");
159
160 if self.metadata.check_ports.unwrap_or(false) {
161 info!("Checking ports for shuriken {}", self.metadata.name);
162 if let Some(ports) = &self.metadata.ports {
163 for port in ports {
164 let port_owner = get_port_owner(*port);
165 if let Some(owner) = port_owner {
166 let name = owner.name.unwrap_or_else(|| "Unknown".into());
167 return Err(format!(
168 "Port {} is already in use by process {}. Please free the port and try again.",
169 port, name
170 ));
171 }
172 else {
173 info!("Port {} is free", port);
174 }
175 }
176 }
177 }
178
179 if self.metadata.shuriken_type == "daemon"
180 && let Some(script_path) = &self.metadata.script_path
181 {
182 let full_script_path = self.resolve_script_path(script_path, shuriken_dir);
183
184 let stem = full_script_path
185 .file_stem()
186 .ok_or_else(|| "Invalid script path".to_string())?
187 .to_string_lossy()
188 .to_string();
189 let compiled_path = lock_dir.join(format!("{stem}.ns"));
190
191 if let Some(mgr) = mgr {
192 engine
193 .execute_function("start", &compiled_path, Some(shuriken_dir), Some(mgr))
194 .await
195 .map_err(|e| format!("Script start failed: {}", e))?;
196 }
197
198 let lockfile_data = json!({
199 "name": self.metadata.name,
200 "type": "Script",
201 });
202
203 atomic_write_json(&lock_path, &lockfile_data).await?;
204
205 let mut state = self.state.lock().await;
206 *state = ShurikenState::Running;
207 }
208 Ok(())
209 }
210
211 pub async fn lockpick(&self, root_path: &Path) -> anyhow::Result<()> {
222 let root_path = root_path
223 .join("shurikens")
224 .join(&self.metadata.name.to_lowercase())
225 .join(".ninja");
226
227 if root_path.join("shuriken.lck").exists() {
228 fs::remove_file(root_path.join("shuriken.lck")).await?;
229 }
230
231 Ok(())
232 }
233
234 pub async fn configure(
248 &self,
249 root_path: &Path,
250 engine: &NinjaEngine,
251 mgr: Option<ShurikenManager>,
252 ) -> anyhow::Result<()> {
253 if let Some(ctx) = &self.config
254 && let Some(script_path) = &self.metadata.script_path
255 {
256 let shuriken_fields = ctx.options.clone();
257 let mut fields = HashMap::new();
258 if let Some(partial_fields) = shuriken_fields {
259 for (name, value) in partial_fields {
260 fields.insert(name, value);
261 }
262 }
263
264 let shuriken_path = root_path
266 .join("shurikens")
267 .join(&self.metadata.name.to_lowercase());
268
269 fs::create_dir_all(&shuriken_path).await?;
271
272 let templater = Templater::new(fields, shuriken_path.clone())?;
274
275 let config_full_path = shuriken_path.join(&ctx.config_path);
277
278 if let Some(parent) = config_full_path.parent() {
280 fs::create_dir_all(parent).await?;
281 }
282
283 templater
284 .generate_config(config_full_path)
285 .await
286 .map_err(|e| anyhow::Error::msg(e.to_string()))?;
287
288 engine
289 .execute_function("post_config", script_path, Some(root_path), mgr)
290 .await?;
291
292 Ok(())
293 } else {
294 Err(anyhow::Error::msg(
295 "Shuriken does not have a config or script path",
296 ))
297 }
298 }
299
300 pub async fn import(
314 &self,
315 engine: &NinjaEngine,
316 shuriken_dir: &Path,
317 mgr: Option<ShurikenManager>,
318 ) -> Result<(), String> {
319 info!(
320 "Shuriken {} is importing data! (if there is any)",
321 &self.metadata.name
322 );
323
324 if let Some(script_path) = &self.metadata.script_path {
325 let full_script_path = self.resolve_script_path(script_path, shuriken_dir);
326
327 let stem = full_script_path
328 .file_stem()
329 .ok_or_else(|| "Invalid script".to_string())?
330 .to_string_lossy()
331 .to_string();
332 let lock_dir = shuriken_dir.join(".ninja");
333 let compiled_path = lock_dir.join(format!("{stem}.ns"));
334
335 if let Some(mgr) = mgr {
336 engine
337 .execute_function("import", &compiled_path, Some(shuriken_dir), Some(mgr))
338 .await
339 .map_err(|e| format!("Script import failed: {}", e))?;
340 }
341 Ok(())
342 } else {
343 Err("Shuriken does not have a script path".to_string())
344 }
345 }
346
347 fn resolve_script_path(&self, script_path: &Path, shuriken_dir: &Path) -> PathBuf {
359 if script_path.is_absolute() {
360 script_path.to_path_buf()
361 } else {
362 shuriken_dir.join(script_path)
363 }
364 }
365
366 pub async fn stop(
380 &mut self,
381 engine: &NinjaEngine,
382 shuriken_dir: &Path,
383 mgr: Option<ShurikenManager>,
384 ) -> Result<(), String> {
385 info!("Stopping shuriken {}", self.metadata.name);
386 let lock_path = shuriken_dir.join(".ninja").join("shuriken.lck");
387
388 if self.metadata.shuriken_type == "daemon"
389 && let Some(script_path) = &self.metadata.script_path
390 {
391 let full_script_path = self.resolve_script_path(script_path, shuriken_dir);
392
393 let stem = full_script_path
394 .file_stem()
395 .ok_or_else(|| "Invalid script".to_string())?
396 .to_string_lossy()
397 .to_string();
398 let lock_dir = shuriken_dir.join(".ninja");
399 let compiled_path = lock_dir.join(format!("{stem}.ns"));
400
401 if let Some(mgr) = mgr {
402 {
403 let mut state = self.state.lock().await;
404 engine
405 .execute_function("stop", &compiled_path, Some(shuriken_dir), Some(mgr))
406 .await
407 .map_err(|e| {
408 *state = ShurikenState::Error(e.to_string());
409 format!("Script stop failed: {}", e)
410 })?;
411 } }
413
414 if lock_path.exists() {
415 tokio::fs::remove_file(&lock_path)
416 .await
417 .map_err(|e| format!("Failed to remove lockfile: {}", e))?;
418 }
419
420 let mut state = self.state.lock().await;
421 *state = ShurikenState::Idle;
422 Ok(())
423 }
424 else {
425 return Err("Shuriken does not have a script path or is not a daemon".to_string());
426 }
427 }
428}