Skip to main content

phlow_runtime/loader/
mod.rs

1pub mod error;
2pub mod loader;
3use crate::MODULE_EXTENSION;
4use crate::RUNTIME_ARCH;
5use crate::scripts::run_script;
6use crate::settings::Settings;
7use error::{Error, ModuleError};
8use libloading::{Library, Symbol};
9use loader::{load_external_module_info, load_local_module_info, load_script};
10use log::debug;
11use log::info;
12use phlow_sdk::prelude::Value;
13use phlow_sdk::structs::{ApplicationData, ModuleData, ModuleSetup};
14use phlow_sdk::valu3::json;
15use reqwest::Client;
16use std::io::Write;
17use std::sync::{Mutex, OnceLock};
18use std::{
19    fs::File,
20    path::{Path, PathBuf},
21};
22
23pub async fn load_script_value(
24    script_absolute_path: &str,
25    print_yaml: bool,
26    print_output: crate::settings::PrintOutput,
27    analyzer: Option<&crate::analyzer::Analyzer>,
28) -> Result<(Value, String), Error> {
29    let script_loaded = load_script(script_absolute_path, print_yaml, print_output, analyzer).await?;
30    Ok((script_loaded.script, script_loaded.script_file_path))
31}
32
33enum ModuleType {
34    Binary,
35    Script,
36}
37
38struct ModuleTarget {
39    pub path: String,
40    pub module_type: ModuleType,
41}
42
43static LOADED_LIBRARIES: OnceLock<Mutex<Vec<Library>>> = OnceLock::new();
44
45fn retain_library(lib: Library) {
46    let libraries = LOADED_LIBRARIES.get_or_init(|| Mutex::new(Vec::new()));
47    let mut libraries = libraries.lock().unwrap_or_else(|err| err.into_inner());
48    libraries.push(lib);
49}
50
51#[derive(Debug, Clone)]
52pub struct Loader {
53    pub main: i32,
54    pub modules: Vec<ModuleData>,
55    pub steps: Value,
56    pub app_data: ApplicationData,
57    pub tests: Option<Value>,
58}
59
60impl Loader {
61    pub async fn load(
62        script_absolute_path: &str,
63        print_yaml: bool,
64        print_output: crate::settings::PrintOutput,
65        analyzer: Option<&crate::analyzer::Analyzer>,
66    ) -> Result<Self, Error> {
67        let script_loaded =
68            load_script(script_absolute_path, print_yaml, print_output, analyzer).await?;
69
70        let base_path = Path::new(&script_loaded.script_file_path)
71            .parent()
72            .map(|p| p.to_string_lossy().to_string())
73            .unwrap_or_else(|| "./".to_string());
74
75        Self::from_script_value(script_loaded.script, &base_path)
76    }
77
78    pub fn from_value(script: &Value, base_path: Option<&Path>) -> Result<Self, Error> {
79        let base_path = base_path
80            .map(|path| path.to_path_buf())
81            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")));
82        let base_path = base_path.to_string_lossy().to_string();
83
84        Self::from_script_value(script.clone(), &base_path)
85    }
86
87    fn from_script_value(script: Value, base_path: &str) -> Result<Self, Error> {
88        let (main, modules) = match script.get("modules") {
89            Some(modules) => {
90                if !modules.is_array() {
91                    return Err(Error::ModuleLoaderError("Modules not an array".to_string()));
92                }
93
94                let main_name = match script.get("main") {
95                    Some(main) => Some(main.to_string()),
96                    None => None,
97                };
98
99                let mut main = -1;
100
101                let mut modules_vec = Vec::new();
102                let modules_array = modules.as_array().unwrap();
103
104                for module in modules_array {
105                    let mut module = ModuleData::try_from(module.clone())
106                        .map_err(|_| Error::ModuleLoaderError("Module not found".to_string()))?;
107
108                    if Some(module.name.clone()) == main_name {
109                        main = modules_vec.len() as i32;
110                    }
111
112                    if let Some(local_path) = module.local_path {
113                        let local_path_fix = format!("{}/{}", base_path, &local_path);
114                        module.local_path = Some(local_path_fix);
115                    }
116
117                    modules_vec.push(module);
118                }
119
120                (main, modules_vec)
121            }
122            None => (-1, Vec::new()),
123        };
124
125        let steps = match script.get("steps") {
126            Some(steps) => steps.clone(),
127            None => return Err(Error::StepsNotDefined),
128        };
129
130        let name = script.get("name").map(|v| v.to_string());
131        let version = script.get("version").map(|v| v.to_string());
132        let environment = script.get("environment").map(|v| v.to_string());
133        let author = script.get("author").map(|v| v.to_string());
134        let description = script.get("description").map(|v| v.to_string());
135        let license = script.get("license").map(|v| v.to_string());
136        let repository = script.get("repository").map(|v| v.to_string());
137        let homepage = script.get("homepage").map(|v| v.to_string());
138
139        let app_data = ApplicationData {
140            name,
141            version,
142            environment,
143            author,
144            description,
145            license,
146            repository,
147            homepage,
148        };
149
150        // Extract tests if they exist
151        let tests = script.get("tests").cloned();
152
153        Ok(Self {
154            main,
155            modules,
156            steps,
157            app_data,
158            tests,
159        })
160    }
161
162    fn find_module_path(module_relative_path: &str) -> Result<ModuleTarget, Error> {
163        let path = format!("{}/module.{}", module_relative_path, MODULE_EXTENSION);
164
165        debug!("Find {}...", path);
166
167        if Path::new(&path).exists() {
168            Ok(ModuleTarget {
169                path,
170                module_type: ModuleType::Binary,
171            })
172        } else {
173            let path = format!("{}/module.{}", module_relative_path, "phlow");
174
175            debug!("Find {}...", path);
176
177            if Path::new(&path).exists() {
178                Ok(ModuleTarget {
179                    path,
180                    module_type: ModuleType::Script,
181                })
182            } else {
183                let path = format!("{}.{}", module_relative_path, "phlow");
184
185                debug!("Find {}...", path);
186
187                if Path::new(&path).exists() {
188                    Ok(ModuleTarget {
189                        path,
190                        module_type: ModuleType::Script,
191                    })
192                } else {
193                    debug!("Module not found: {}", module_relative_path);
194                    Err(Error::ModuleNotFound(format!(
195                        "Module not found at path: {}",
196                        module_relative_path
197                    )))
198                }
199            }
200        }
201    }
202
203    pub fn get_steps(&self) -> Value {
204        let steps = self.steps.clone();
205        json!({
206            "steps": steps
207        })
208    }
209
210    pub async fn download(&self, default_package_repository_url: &str) -> Result<(), Error> {
211        if !Path::new("phlow_packages").exists() {
212            std::fs::create_dir("phlow_packages").map_err(Error::FileCreateError)?;
213        }
214
215        info!("Downloading modules...");
216
217        let client = Client::new();
218
219        let mut downloads = Vec::new();
220
221        for module in &self.modules {
222            // Skip local path modules - they don't need to be downloaded
223            if module.local_path.is_some() {
224                info!(
225                    "Module {} is a local path module, skipping download",
226                    module.name
227                );
228                continue;
229            }
230
231            let module_so_path = format!(
232                "phlow_packages/{}/module.{}",
233                module.module, MODULE_EXTENSION
234            );
235            if Path::new(&module_so_path).exists() {
236                info!(
237                    "Module {} ({}) already exists at {}, skipping download",
238                    module.name, module.version, module_so_path
239                );
240                continue;
241            }
242
243            let base_url = match &module.repository {
244                Some(repo) => repo.clone(),
245                None => format!(
246                    "{}/{}",
247                    if regex::Regex::new(r"^(https?://|\.git|.*@.*)")
248                        .unwrap()
249                        .is_match(default_package_repository_url)
250                    {
251                        default_package_repository_url.to_string()
252                    } else {
253                        format!(
254                            "https://raw.githubusercontent.com/{}",
255                            default_package_repository_url
256                        )
257                    },
258                    module
259                        .repository_path
260                        .clone()
261                        .ok_or_else(|| Error::ModuleNotFound(module.name.clone()))?
262                ),
263            };
264
265            info!("Base URL: {}", base_url);
266
267            let version = if module.version == "latest" {
268                let metadata_url = format!("{}/metadata.json", base_url);
269                info!("Metadata URL: {}", metadata_url);
270
271                let res = client
272                    .get(&metadata_url)
273                    .send()
274                    .await
275                    .map_err(Error::GetFileError)?;
276                let metadata = {
277                    let content = res.text().await.map_err(Error::BufferError)?;
278                    Value::json_to_value(&content).map_err(Error::LoaderErrorJsonValu3)?
279                };
280
281                match metadata.get("latest") {
282                    Some(version) => version.to_string(),
283                    None => {
284                        return Err(Error::VersionNotFound(ModuleError {
285                            module: module.name.clone(),
286                        }));
287                    }
288                }
289            } else {
290                module.version.clone()
291            };
292
293            let handler = Self::download_and_extract_tarball(
294                base_url.clone(),
295                module.module.clone(),
296                version.clone(),
297            );
298
299            downloads.push(handler);
300        }
301
302        let results = futures::future::join_all(downloads).await;
303        for result in results {
304            if let Err(err) = result {
305                return Err(err);
306            }
307        }
308
309        info!("All modules downloaded and extracted successfully");
310        Ok(())
311    }
312
313    async fn download_and_extract_tarball(
314        base_url: String,
315        module: String,
316        version: String,
317    ) -> Result<(), Error> {
318        use flate2::read::GzDecoder;
319        use tar::Archive;
320
321        let tarball_name = format!("{}-{}-{}.tar.gz", module, version, RUNTIME_ARCH);
322        let target_url = format!("{}/{}", base_url.trim_end_matches('/'), tarball_name);
323        let target_path = format!("phlow_packages/{}/{}", module, tarball_name);
324
325        if Path::new(&format!(
326            "phlow_packages/{}/module.{}",
327            module, MODULE_EXTENSION
328        ))
329        .exists()
330        {
331            return Ok(());
332        }
333
334        info!(
335            "Downloading module tarball {} from {}",
336            tarball_name, target_url
337        );
338
339        if let Some(parent) = Path::new(&target_path).parent() {
340            std::fs::create_dir_all(parent).map_err(Error::FileCreateError)?;
341        }
342
343        let client = Client::new();
344        let response = client
345            .get(&target_url)
346            .send()
347            .await
348            .map_err(Error::GetFileError)?;
349        let content = response.bytes().await.map_err(Error::BufferError)?;
350
351        // Salva o tarball temporariamente
352        let mut file = File::create(&target_path).map_err(Error::FileCreateError)?;
353        file.write_all(&content).map_err(Error::CopyError)?;
354
355        // Extrai o conteúdo
356        let tar_gz = File::open(&target_path).map_err(Error::FileCreateError)?;
357        let decompressor = GzDecoder::new(tar_gz);
358        let mut archive = Archive::new(decompressor);
359        archive
360            .unpack(format!("phlow_packages/{}", module))
361            .map_err(Error::CopyError)?;
362
363        // Remove o tar.gz após extração
364        std::fs::remove_file(&target_path).map_err(Error::FileCreateError)?;
365
366        info!("Module extracted to phlow_packages/{}", module);
367
368        Ok(())
369    }
370
371    pub fn update_info(&mut self) {
372        debug!("update_info");
373
374        for module in &mut self.modules {
375            let value = if let Some(local_path) = &module.local_path {
376                load_local_module_info(local_path)
377            } else {
378                load_external_module_info(&module.module)
379            };
380
381            debug!("module info loaded");
382            module.set_info(value);
383        }
384    }
385}
386
387pub fn load_module(
388    setup: ModuleSetup,
389    module_name: &str,
390    module_version: &str,
391    local_path: Option<String>,
392    settings: Settings,
393) -> Result<(), Error> {
394    let target = {
395        let module_relative_path = match local_path {
396            Some(local_path) => local_path,
397            None => format!("phlow_packages/{}", module_name),
398        };
399
400        let target = Loader::find_module_path(&module_relative_path)?;
401
402        info!(
403            "🧪 Load Module: {} ({}), in {}",
404            module_name, module_version, target.path
405        );
406
407        target
408    };
409
410    match target.module_type {
411        ModuleType::Script => {
412            run_script(&target.path, setup, &settings);
413        }
414        ModuleType::Binary => unsafe {
415            info!("Loading binary module: {}", target.path);
416
417            let lib: Library = match Library::new(&target.path) {
418                Ok(lib) => lib,
419                Err(err) => return Err(Error::LibLoadingError(err)),
420            };
421
422            {
423                let func: Symbol<unsafe extern "C" fn(ModuleSetup)> = match lib.get(b"plugin") {
424                    Ok(func) => func,
425                    Err(err) => return Err(Error::LibLoadingError(err)),
426                };
427
428                func(setup);
429            }
430
431            retain_library(lib);
432        },
433    }
434
435    Ok(())
436}