hyper_scripter/util/
main_util.rs

1use super::PrepareRespond;
2use crate::args::Subs;
3use crate::color::Stylize;
4use crate::config::Config;
5use crate::env_pair::EnvPair;
6use crate::error::{Contextable, Error, RedundantOpt, Result};
7use crate::extract_msg::extract_env_from_content_help_aware;
8use crate::path;
9use crate::process_lock::{ProcessLockRead, ProcessLockWrite};
10use crate::query::{
11    self, do_list_query_with_handler, EditQuery, ListQuery, ListQueryHandler, ScriptQuery,
12};
13use crate::script::{IntoScriptName, ScriptInfo, ScriptName};
14use crate::script_repo::{RepoEntry, ScriptRepo, Visibility};
15use crate::script_type::{iter_default_templates, ScriptFullType, ScriptType};
16use crate::tag::{Tag, TagSelector};
17use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
18use std::fs::{create_dir_all, read_dir};
19use std::path::{Path, PathBuf};
20use std::process::Command;
21
22pub struct EditTagArgs {
23    pub content: TagSelector,
24    /// 命令行參數裡帶著 tag 選項,例如 hs edit --tag some-tag edit
25    pub explicit_tag: bool,
26    /// 命令行參數裡帶著 select 選項,例如 hs --select some-tag edit
27    pub explicit_select: bool,
28}
29
30pub async fn mv(
31    entry: &mut RepoEntry<'_>,
32    new_name: Option<ScriptName>,
33    ty: Option<ScriptType>,
34    tags: Option<TagSelector>,
35) -> Result {
36    if ty.is_some() || new_name.is_some() {
37        let og_path = path::open_script(&entry.name, &entry.ty, Some(true))?;
38        let new_name = new_name.as_ref().unwrap_or(&entry.name);
39        let new_ty = ty.as_ref().unwrap_or(&entry.ty);
40        let new_path = path::open_script(new_name, new_ty, None)?; // NOTE: 不判斷存在性,因為接下來要對新舊腳本同路徑的狀況做特殊處理
41        if new_path != og_path {
42            log::debug!("改動腳本檔案:{:?} -> {:?}", og_path, new_path);
43            if new_path.exists() {
44                return Err(Error::PathExist(new_path).context("移動成既存腳本"));
45            }
46            super::mv(&og_path, &new_path)?;
47        } else {
48            log::debug!("相同的腳本檔案:{:?},不做檔案處理", og_path);
49        }
50    }
51
52    entry
53        .update(|info| {
54            if let Some(ty) = ty {
55                info.ty = ty;
56            }
57            if let Some(name) = new_name {
58                info.name = name.clone();
59            }
60            if let Some(tags) = tags {
61                info.append_tags(tags);
62            }
63            info.write();
64        })
65        .await?;
66    Ok(())
67}
68
69fn create<F: FnOnce(String) -> Error>(
70    query: ScriptQuery,
71    script_repo: &mut ScriptRepo,
72    ty: &ScriptType,
73    on_conflict: F,
74) -> Result<(ScriptName, PathBuf)> {
75    let name = query.into_script_name()?;
76    log::debug!("打開新命名腳本:{:?}", name);
77    if script_repo.get_mut(&name, Visibility::All).is_some() {
78        return Err(on_conflict(name.to_string()));
79    }
80
81    let p =
82        path::open_script(&name, ty, None).context(format!("打開新命名腳本失敗:{:?}", name))?;
83    if p.exists() {
84        if p.is_dir() {
85            return Err(Error::PathExist(p).context("與目錄撞路徑"));
86        }
87        check_path_collision(&p, script_repo)?;
88        log::warn!("編輯野生腳本!");
89    } else {
90        // NOTE: 創建資料夾
91        if let Some(parent) = p.parent() {
92            super::handle_fs_res(&[&p], create_dir_all(parent))?;
93        }
94    }
95    Ok((name, p))
96}
97
98struct EditListQueryHandler {
99    anonymous_cnt: u32,
100    named: HashMap<ScriptName, PathBuf>,
101    ty: Option<ScriptFullType>,
102}
103impl EditListQueryHandler {
104    fn has_new_script(&self) -> bool {
105        self.anonymous_cnt > 0 || !self.named.is_empty()
106    }
107    fn new(ty: Option<ScriptFullType>) -> Self {
108        EditListQueryHandler {
109            ty,
110            named: Default::default(),
111            anonymous_cnt: 0,
112        }
113    }
114    fn get_or_default_type(&mut self) -> &ScriptFullType {
115        if self.ty.is_none() {
116            self.ty = Some(Default::default());
117        }
118        self.ty.as_ref().unwrap()
119    }
120}
121// SAFETY: 實作永不改動 repo 本身
122unsafe impl ListQueryHandler for EditListQueryHandler {
123    type Item = EditQuery<ListQuery>;
124    async fn handle_query<'a>(
125        &mut self,
126        query: ScriptQuery,
127        repo: &'a mut ScriptRepo,
128    ) -> Result<Option<RepoEntry<'a>>> {
129        match query::do_script_query(&query, repo, false, false).await {
130            Err(Error::DontFuzz) | Ok(None) => {
131                let ty = self.get_or_default_type();
132                let (name, path) = create(query, repo, &ty.ty, |name| {
133                    log::error!("與被篩掉的腳本撞名");
134                    Error::ScriptIsFiltered(name.to_string())
135                })?;
136                self.named.insert(name, path);
137                Ok(None)
138            }
139            Ok(Some(entry)) => {
140                log::debug!("打開既有命名腳本:{:?}", entry.name);
141                // FIXME: 一旦 NLL 進化就修掉這段雙重詢問
142                let n = entry.name.clone();
143                return Ok(Some(repo.get_mut(&n, Visibility::All).unwrap()));
144            }
145            Err(e) => Err(e),
146        }
147    }
148    fn handle_item(&mut self, item: Self::Item) -> Option<ListQuery> {
149        match item {
150            EditQuery::Query(query) => Some(query),
151            EditQuery::NewAnonimous => {
152                self.get_or_default_type();
153                self.anonymous_cnt += 1;
154                None
155            }
156        }
157    }
158    fn should_raise_dont_fuzz_on_empty() -> bool {
159        false
160    }
161    fn should_return_all_on_empty() -> bool {
162        false
163    }
164}
165
166#[derive(Debug)]
167pub struct EditResult<'a> {
168    pub existing: Vec<RepoEntry<'a>>,
169}
170#[derive(Debug)]
171pub struct CreateResult {
172    pub ty: ScriptFullType,
173    pub tags: Vec<Tag>,
174    pub to_create: HashMap<ScriptName, PathBuf>,
175}
176impl CreateResult {
177    pub fn new(
178        ty: ScriptFullType,
179        tags: Vec<Tag>,
180        anonymous_cnt: u32,
181        named: HashMap<ScriptName, PathBuf>,
182    ) -> Result<CreateResult> {
183        let iter = path::new_anonymous_name(
184            anonymous_cnt,
185            named.iter().filter_map(|(name, _)| {
186                if let ScriptName::Anonymous(id) = name {
187                    Some(*id)
188                } else {
189                    None
190                }
191            }),
192        )
193        .context("打開新匿名腳本失敗")?;
194
195        let mut to_create = named;
196        for name in iter {
197            let path = path::open_script(&name, &ty.ty, None)?; // NOTE: new_anonymous_name 的邏輯已足以確保不會產生衝突的檔案,不檢查了!
198            to_create.insert(name, path);
199        }
200        Ok(CreateResult {
201            ty,
202            tags,
203            to_create,
204        })
205    }
206    pub fn iter_path(&self) -> impl Iterator<Item = &Path> {
207        self.to_create.iter().map(|(_, path)| path.as_ref())
208    }
209}
210
211// XXX 到底幹嘛把新增和編輯的邏輯攪在一處呢…?
212pub async fn edit_or_create(
213    edit_query: Vec<EditQuery<ListQuery>>,
214    script_repo: &'_ mut ScriptRepo,
215    ty: Option<ScriptFullType>,
216    tags: EditTagArgs,
217) -> Result<(EditResult<'_>, Option<CreateResult>)> {
218    let explicit_type = ty.is_some();
219    let mut edit_query_handler = EditListQueryHandler::new(ty);
220    let existing =
221        do_list_query_with_handler(script_repo, edit_query, &mut edit_query_handler).await?;
222
223    if existing.is_empty() && tags.explicit_select {
224        return Err(RedundantOpt::Selector.into());
225    }
226    if !edit_query_handler.has_new_script() && tags.explicit_tag {
227        return Err(RedundantOpt::Tag.into());
228    }
229    if !edit_query_handler.has_new_script() && explicit_type {
230        return Err(RedundantOpt::Type.into());
231    }
232
233    let edit_result = EditResult { existing };
234    if edit_query_handler.has_new_script() {
235        let create_result = CreateResult::new(
236            edit_query_handler.ty.unwrap(),
237            tags.content.into_allowed_iter().collect(),
238            edit_query_handler.anonymous_cnt,
239            edit_query_handler.named,
240        )?;
241        Ok((edit_result, Some(create_result)))
242    } else {
243        Ok((edit_result, None))
244    }
245}
246
247fn run(
248    script_path: &Path,
249    info: &ScriptInfo,
250    remaining: &[String],
251    hs_tmpl_val: &super::TmplVal<'_>,
252    remaining_envs: &[EnvPair],
253) -> Result<()> {
254    let conf = Config::get();
255    let ty = &info.ty;
256
257    let script_conf = conf.get_script_conf(ty)?;
258    let cmd_str = if let Some(cmd) = &script_conf.cmd {
259        cmd
260    } else {
261        return Err(Error::PermissionDenied(vec![script_path.to_path_buf()]));
262    };
263
264    let env = conf.gen_env(hs_tmpl_val, true)?;
265    let ty_env = script_conf.gen_env(hs_tmpl_val)?;
266
267    let pre_run_script = prepare_pre_run(None)?;
268    let (cmd, shebang) = super::shebang_handle::handle(&pre_run_script)?;
269    let args = shebang
270        .iter()
271        .map(|s| s.as_ref())
272        .chain(std::iter::once(pre_run_script.as_os_str()))
273        .chain(remaining.iter().map(|s| s.as_ref()));
274
275    let set_cmd_envs = |cmd: &mut Command| {
276        cmd.envs(ty_env.iter().map(|(a, b)| (a, b)));
277        cmd.envs(env.iter().map(|(a, b)| (a, b)));
278        cmd.envs(remaining_envs.iter().map(|p| (&p.key, &p.val)));
279    };
280
281    let mut cmd = super::create_cmd(cmd, args);
282    set_cmd_envs(&mut cmd);
283
284    let code = super::run_cmd(cmd)?;
285    log::info!("預腳本執行結果:{:?}", code);
286    if let Some(code) = code {
287        // TODO: 根據返回值做不同表現
288        return Err(Error::PreRunError(code));
289    }
290
291    let args = script_conf.args(hs_tmpl_val)?;
292    let full_args = args
293        .iter()
294        .map(|s| s.as_str())
295        .chain(remaining.iter().map(|s| s.as_str()));
296
297    let mut cmd = super::create_cmd(&cmd_str, full_args);
298    set_cmd_envs(&mut cmd);
299
300    let code = super::run_cmd(cmd)?;
301    log::info!("程式執行結果:{:?}", code);
302    if let Some(code) = code {
303        Err(Error::ScriptError(code))
304    } else {
305        Ok(())
306    }
307}
308pub async fn run_n_times(
309    repeat: u64,
310    dummy: bool,
311    entry: &mut RepoEntry<'_>,
312    mut args: Vec<String>,
313    res: &mut Vec<Error>,
314    use_previous: bool,
315    error_no_previous: bool,
316    dir: Option<PathBuf>,
317) -> Result {
318    log::info!("執行 {:?}", entry.name);
319    super::hijack_ctrlc_once();
320
321    let mut env_vec = vec![];
322    if use_previous {
323        let historian = &entry.get_env().historian;
324        match historian.previous_args(entry.id, dir.as_deref()).await? {
325            None if error_no_previous => {
326                return Err(Error::NoPreviousArgs);
327            }
328            None => log::warn!("無前一次參數,當作空的"),
329            Some((arg_str, envs_str)) => {
330                log::debug!("撈到前一次呼叫的參數 {}", arg_str);
331                let mut prev_arg_vec: Vec<String> =
332                    serde_json::from_str(&arg_str).context(format!("反序列失敗 {}", arg_str))?;
333                env_vec =
334                    serde_json::from_str(&envs_str).context(format!("反序列失敗 {}", envs_str))?;
335                prev_arg_vec.extend(args.into_iter());
336                args = prev_arg_vec;
337            }
338        }
339    }
340
341    let here = path::normalize_path(".").ok();
342    let script_path = path::open_script(&entry.name, &entry.ty, Some(true))?;
343    let content = super::read_file(&script_path)?;
344
345    if !Config::get_no_caution()
346        && Config::get()
347            .caution_tags
348            .select(&entry.tags, &entry.ty)
349            .is_true()
350    {
351        let ty = super::get_display_type(&entry.ty);
352        let mut first_part = entry.name.to_string();
353        for arg in args.iter() {
354            first_part += " ";
355            first_part += arg;
356        }
357        let msg = format!(
358            "{} requires extra caution. Are you sure?",
359            first_part.stylize().color(ty.color()).bold()
360        );
361        let yes = super::prompt(msg, false)?;
362        if !yes {
363            return Err(Error::Caution);
364        }
365    }
366
367    let mut hs_env_desc = vec![];
368    for (need_save, line) in extract_env_from_content_help_aware(&content) {
369        hs_env_desc.push(line.to_owned());
370        if need_save {
371            EnvPair::process_line(line, &mut env_vec);
372        }
373    }
374    EnvPair::sort(&mut env_vec);
375    let env_record = serde_json::to_string(&env_vec)?;
376
377    let run_id = entry
378        .update(|info| info.exec(content, &args, env_record, here))
379        .await?;
380
381    if dummy {
382        log::info!("--dummy 不用真的執行,提早退出");
383        return Ok(());
384    }
385    // Start packing hs tmpl val
386    // SAFETY: 底下所有對 `entry` 的借用,都不會被更後面的 `entry.update` 影響
387    let mut hs_tmpl_val = super::TmplVal::new();
388    let hs_name = entry.name.key();
389    let hs_name = hs_name.as_ref() as *const str;
390    let hs_name = unsafe { &*hs_name };
391    let hs_tags = &entry.tags as *const HashSet<Tag>;
392    let content = entry.exec_time.as_ref().unwrap().data().unwrap().0.as_str() as *const str;
393    hs_tmpl_val.path = Some(&script_path);
394    hs_tmpl_val.run_id = Some(run_id);
395    hs_tmpl_val.tags = unsafe { &*hs_tags }.iter().map(|t| t.as_ref()).collect();
396    hs_tmpl_val.env_desc = hs_env_desc;
397    hs_tmpl_val.name = Some(hs_name);
398    hs_tmpl_val.content = Some(unsafe { &*content });
399    // End packing hs tmpl val
400
401    let mut lock = ProcessLockWrite::new(run_id, entry.id, hs_name, &args)?;
402    let guard = lock.try_write_info()?;
403    for _ in 0..repeat {
404        let run_res = run(&script_path, &*entry, &args, &hs_tmpl_val, &env_vec);
405        let ret_code: i32;
406        match run_res {
407            Err(Error::ScriptError(code)) => {
408                ret_code = code;
409                res.push(run_res.unwrap_err());
410            }
411            Err(e) => return Err(e),
412            Ok(_) => ret_code = 0,
413        }
414        entry
415            .update(|info| info.exec_done(ret_code, run_id))
416            .await?;
417    }
418    if res.is_empty() {
419        ProcessLockWrite::mark_sucess(guard);
420    }
421    Ok(())
422}
423
424pub async fn load_utils(script_repo: &mut ScriptRepo) -> Result {
425    for u in hyper_scripter_util::get_all().iter() {
426        log::info!("載入小工具 {}", u.name);
427        let name = u.name.to_owned().into_script_name()?;
428        if script_repo.get_mut(&name, Visibility::All).is_some() {
429            log::warn!("已存在的小工具 {:?},跳過", name);
430            continue;
431        }
432        let ty = u.ty.parse()?;
433        let tags: Vec<Tag> = if u.is_hidden {
434            vec!["util".parse().unwrap(), "hide".parse().unwrap()]
435        } else {
436            vec!["util".parse().unwrap()]
437        };
438        let p = path::open_script(&name, &ty, Some(false))?;
439
440        // NOTE: 創建資料夾
441        if let Some(parent) = p.parent() {
442            super::handle_fs_res(&[&p], create_dir_all(parent))?;
443        }
444
445        let entry = script_repo
446            .entry(&name)
447            .or_insert(ScriptInfo::builder(0, name, ty, tags.into_iter()).build())
448            .await?;
449        super::prepare_script(&p, &*entry, None, &[u.content])?;
450    }
451    Ok(())
452}
453
454pub fn prepare_pre_run(content: Option<&str>) -> Result<PathBuf> {
455    let p = path::get_home().join(path::HS_PRE_RUN);
456    if content.is_some() || !p.exists() {
457        let content = content.unwrap_or_else(|| include_str!("hs_prerun"));
458        log::info!("寫入預執行腳本 {:?} {}", p, content);
459        super::write_file(&p, content)?;
460    }
461    Ok(p)
462}
463
464pub fn load_templates() -> Result {
465    for (ty, tmpl) in iter_default_templates() {
466        let tmpl_path = path::get_template_path(&ty)?;
467        if tmpl_path.exists() {
468            continue;
469        }
470        super::write_file(&tmpl_path, tmpl)?;
471    }
472    Ok(())
473}
474
475/// 判斷是否需要寫入主資料庫(script_infos 表格)
476pub fn need_write(arg: &Subs) -> bool {
477    use Subs::*;
478    match arg {
479        Edit { .. } => true,
480        CP { .. } => true,
481        RM { .. } => true,
482        LoadUtils { .. } => true,
483        MV {
484            ty,
485            tags,
486            new,
487            origin: _,
488        } => {
489            // TODO: 好好測試這個
490            ty.is_some() || tags.is_some() || new.is_some()
491        }
492        _ => false,
493    }
494}
495
496pub async fn after_script(
497    entry: &mut RepoEntry<'_>,
498    path: &Path,
499    prepare_resp: Option<&PrepareRespond>,
500) -> Result {
501    let mut record_write = true;
502    match prepare_resp {
503        None => {
504            log::debug!("不執行後處理");
505        }
506        Some(PrepareRespond { is_new, time }) => {
507            let modified = super::file_modify_time(path)?;
508            if time >= &modified {
509                if *is_new {
510                    log::info!("新腳本未變動,應刪除之");
511                    return Err(Error::EmptyCreate);
512                } else {
513                    log::info!("舊腳本未變動,不記錄寫事件(只記讀事件)");
514                    record_write = false;
515                }
516            }
517        }
518    }
519    if record_write {
520        entry.update(|info| info.write()).await?;
521    }
522    Ok(())
523}
524
525fn check_path_collision(p: &Path, script_repo: &mut ScriptRepo) -> Result {
526    for script in script_repo.iter_mut(Visibility::All) {
527        let script_p = path::open_script(&script.name, &script.ty, None)?;
528        if &script_p == p {
529            return Err(Error::PathExist(script_p).context("與既存腳本撞路徑"));
530        }
531    }
532    Ok(())
533}
534
535pub fn get_all_active_process_locks() -> Result<Vec<ProcessLockRead>> {
536    let dir_path = path::get_process_lock_dir()?;
537    let dir = super::handle_fs_res(&[&dir_path], read_dir(&dir_path))?;
538    let mut ret = vec![];
539    for entry in dir {
540        let file_name = entry?.file_name();
541
542        // TODO: concurrent?
543        let file_name = file_name
544            .to_str()
545            .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
546
547        let inner = |file_name| -> Result<Option<ProcessLockRead>> {
548            let file_path = dir_path.join(file_name);
549            let mut builder = ProcessLockRead::builder(file_path, file_name)?;
550
551            if builder.get_can_write()? {
552                log::info!("remove inactive file lock {:?}", builder.path);
553                super::remove(&builder.path)?;
554                Ok(None)
555            } else {
556                log::info!("found active file lock {:?}", builder.path);
557                Ok(Some(builder.build()?))
558            }
559        };
560        let lock = match inner(file_name) {
561            Ok(None) => continue,
562            Ok(Some(l)) => l,
563            Err(e) => {
564                log::warn!("error building process lock for {}: {:?}", file_name, e);
565                continue;
566            }
567        };
568        ret.push(lock);
569    }
570
571    Ok(ret)
572}