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    StableRepo,
13};
14use crate::script::{IntoScriptName, ScriptInfo, ScriptName};
15use crate::script_repo::{RepoEntry, ScriptRepo, Visibility};
16use crate::script_type::{iter_default_templates, ScriptFullType, ScriptType};
17use crate::tag::{Tag, TagSelector, TagSelectorGroup};
18use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
19use std::fs::{create_dir_all, read_dir};
20use std::path::{Path, PathBuf};
21use std::process::Command;
22
23pub struct EditTagArgs {
24    pub content: TagSelector,
25    /// 命令行參數裡帶著 tag 選項,例如 hs edit --tag some-tag edit
26    pub explicit_tag: bool,
27    /// 命令行參數裡帶著 select 選項,例如 hs --select some-tag edit
28    pub explicit_select: bool,
29}
30
31pub async fn mv(
32    entry: &mut RepoEntry<'_>,
33    new_name: Option<ScriptName>,
34    ty: Option<ScriptType>,
35    tags: Option<TagSelector>,
36) -> Result {
37    if ty.is_some() || new_name.is_some() {
38        let og_path = path::open_script(&entry.name, &entry.ty, Some(true))?;
39        let new_name = new_name.as_ref().unwrap_or(&entry.name);
40        let new_ty = ty.as_ref().unwrap_or(&entry.ty);
41        let new_path = path::open_script(new_name, new_ty, None)?; // NOTE: 不判斷存在性,因為接下來要對新舊腳本同路徑的狀況做特殊處理
42        if new_path != og_path {
43            log::debug!("改動腳本檔案:{:?} -> {:?}", og_path, new_path);
44            if new_path.exists() {
45                return Err(Error::PathExist(new_path).context("移動成既存腳本"));
46            }
47            super::mv(&og_path, &new_path)?;
48        } else {
49            log::debug!("相同的腳本檔案:{:?},不做檔案處理", og_path);
50        }
51    }
52
53    entry
54        .update(|info| {
55            if let Some(ty) = ty {
56                info.ty = ty;
57            }
58            if let Some(name) = new_name {
59                info.name = name.clone();
60            }
61            if let Some(tags) = tags {
62                info.append_tags(tags);
63            }
64            info.write();
65        })
66        .await?;
67    Ok(())
68}
69
70fn create<F: FnOnce(String) -> Error, R: StableRepo>(
71    query: ScriptQuery,
72    script_repo: &mut R,
73    ty: &ScriptType,
74    on_conflict: F,
75) -> Result<(ScriptName, PathBuf)> {
76    let name = query.into_script_name()?;
77    log::debug!("打開新命名腳本:{:?}", name);
78    if script_repo.get_mut(&name, Visibility::All).is_some() {
79        return Err(on_conflict(name.to_string()));
80    }
81
82    let p =
83        path::open_script(&name, ty, None).context(format!("打開新命名腳本失敗:{:?}", name))?;
84    if p.exists() {
85        if p.is_dir() {
86            return Err(Error::PathExist(p).context("與目錄撞路徑"));
87        }
88        check_path_collision(&p, script_repo)?;
89        log::warn!("編輯野生腳本!");
90    } else {
91        // NOTE: 創建資料夾
92        if let Some(parent) = p.parent() {
93            super::handle_fs_res(&[&p], create_dir_all(parent))?;
94        }
95    }
96    Ok((name, p))
97}
98
99struct EditListQueryHandler {
100    anonymous_cnt: u32,
101    named: HashMap<ScriptName, PathBuf>,
102    ty: Option<ScriptFullType>,
103}
104impl EditListQueryHandler {
105    fn has_new_script(&self) -> bool {
106        self.anonymous_cnt > 0 || !self.named.is_empty()
107    }
108    fn new(ty: Option<ScriptFullType>) -> Self {
109        EditListQueryHandler {
110            ty,
111            named: Default::default(),
112            anonymous_cnt: 0,
113        }
114    }
115    fn get_or_default_type(&mut self) -> &ScriptFullType {
116        if self.ty.is_none() {
117            self.ty = Some(Default::default());
118        }
119        self.ty.as_ref().unwrap()
120    }
121}
122impl ListQueryHandler for EditListQueryHandler {
123    type Item = EditQuery<ListQuery>;
124    async fn handle_query<'a, R: StableRepo>(
125        &mut self,
126        query: ScriptQuery,
127        repo: &'a mut R,
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    caution: bool,
317    dir: Option<PathBuf>,
318) -> Result {
319    log::info!("執行 {:?}", entry.name);
320    super::hijack_ctrlc_once();
321
322    let mut env_vec = vec![];
323    if use_previous {
324        let historian = &entry.get_env().historian;
325        match historian.previous_args(entry.id, dir.as_deref()).await? {
326            None if error_no_previous => {
327                return Err(Error::NoPreviousArgs);
328            }
329            None => log::warn!("無前一次參數,當作空的"),
330            Some((arg_str, envs_str)) => {
331                log::debug!("撈到前一次呼叫的參數 {}", arg_str);
332                let mut prev_arg_vec: Vec<String> =
333                    serde_json::from_str(&arg_str).context(format!("反序列失敗 {}", arg_str))?;
334                env_vec =
335                    serde_json::from_str(&envs_str).context(format!("反序列失敗 {}", envs_str))?;
336                prev_arg_vec.extend(args.into_iter());
337                args = prev_arg_vec;
338            }
339        }
340    }
341
342    let here = path::normalize_path(".").ok();
343    let script_path = path::open_script(&entry.name, &entry.ty, Some(true))?;
344    let content = super::read_file(&script_path)?;
345
346    if caution
347        && Config::get()
348            .caution_tags
349            .select(&entry.tags, &entry.ty)
350            .is_true()
351    {
352        let ty = super::get_display_type(&entry.ty);
353        let mut first_part = entry.name.to_string();
354        for arg in args.iter() {
355            first_part += " ";
356            first_part += arg;
357        }
358        let msg = format!(
359            "{} requires extra caution. Are you sure?",
360            first_part.stylize().color(ty.color()).bold()
361        );
362        let yes = super::prompt(msg, false)?;
363        if !yes {
364            return Err(Error::Caution);
365        }
366    }
367
368    let mut hs_env_desc = vec![];
369    for (need_save, line) in extract_env_from_content_help_aware(&content) {
370        hs_env_desc.push(line.to_owned());
371        if need_save {
372            EnvPair::process_line(line, &mut env_vec);
373        }
374    }
375    EnvPair::sort(&mut env_vec);
376    let env_record = serde_json::to_string(&env_vec)?;
377
378    let run_id = entry
379        .update(|info| info.exec(content, &args, env_record, here))
380        .await?;
381
382    if dummy {
383        log::info!("--dummy 不用真的執行,提早退出");
384        return Ok(());
385    }
386    // Start packing hs tmpl val
387    // SAFETY: 底下所有對 `entry` 的借用,都不會被更後面的 `entry.update` 影響
388    let mut hs_tmpl_val = super::TmplVal::new();
389    let hs_name = entry.name.key();
390    let hs_name = hs_name.as_ref() as *const str;
391    let hs_name = unsafe { &*hs_name };
392    let hs_tags = &entry.tags as *const HashSet<Tag>;
393    let content = entry.exec_time.as_ref().unwrap().data().unwrap().0.as_str() as *const str;
394    hs_tmpl_val.path = Some(&script_path);
395    hs_tmpl_val.run_id = Some(run_id);
396    hs_tmpl_val.tags = unsafe { &*hs_tags }.iter().map(|t| t.as_ref()).collect();
397    hs_tmpl_val.env_desc = hs_env_desc;
398    hs_tmpl_val.name = Some(hs_name);
399    hs_tmpl_val.content = Some(unsafe { &*content });
400    // End packing hs tmpl val
401
402    let mut lock = ProcessLockWrite::new(run_id, entry.id, hs_name, &args)?;
403    let guard = lock.try_write_info()?;
404    for _ in 0..repeat {
405        let run_res = run(&script_path, &*entry, &args, &hs_tmpl_val, &env_vec);
406        let ret_code: i32;
407        match run_res {
408            Err(Error::ScriptError(code)) => {
409                ret_code = code;
410                res.push(run_res.unwrap_err());
411            }
412            Err(e) => return Err(e),
413            Ok(_) => ret_code = 0,
414        }
415        entry
416            .update(|info| info.exec_done(ret_code, run_id))
417            .await?;
418    }
419    if res.is_empty() {
420        ProcessLockWrite::mark_sucess(guard);
421    }
422    Ok(())
423}
424
425pub async fn load_utils(
426    script_repo: &mut ScriptRepo,
427    selector: Option<&TagSelectorGroup>,
428) -> Result {
429    for u in hyper_scripter_util::get_all().iter() {
430        log::info!("載入小工具 {}", u.name);
431        let name = u.name.to_owned().into_script_name()?;
432        if script_repo.get_mut(&name, Visibility::All).is_some() {
433            log::warn!("已存在的小工具 {:?},跳過", name);
434            continue;
435        }
436        let ty = u.ty.parse()?;
437        let tags: Vec<Tag> = if u.is_hidden {
438            vec!["util".parse().unwrap(), "hide".parse().unwrap()]
439        } else {
440            vec!["util".parse().unwrap()]
441        };
442        let p = path::open_script(&name, &ty, Some(false))?;
443
444        // NOTE: 創建資料夾
445        if let Some(parent) = p.parent() {
446            super::handle_fs_res(&[&p], create_dir_all(parent))?;
447        }
448
449        let script = ScriptInfo::builder(
450            0,
451            super::compute_hash(&u.content),
452            name,
453            ty,
454            tags.into_iter(),
455        )
456        .build();
457        let hide = if let Some(selector) = selector {
458            !selector.select(&script.tags, &script.ty)
459        } else {
460            false
461        };
462
463        let entry = if hide {
464            script_repo
465                .entry_hidden(&script.name)
466                .or_insert(script)
467                .await?
468        } else {
469            script_repo.entry(&script.name).or_insert(script).await?
470        };
471        super::prepare_script(&p, &*entry, None, &[u.content])?;
472    }
473    Ok(())
474}
475
476pub fn prepare_pre_run(content: Option<&str>) -> Result<PathBuf> {
477    let p = path::get_home().join(path::HS_PRE_RUN);
478    if content.is_some() || !p.exists() {
479        let content = content.unwrap_or_else(|| include_str!("hs_prerun"));
480        log::info!("寫入預執行腳本 {:?} {}", p, content);
481        super::write_file(&p, content)?;
482    }
483    Ok(p)
484}
485
486pub fn load_templates() -> Result {
487    for (ty, tmpl) in iter_default_templates() {
488        let tmpl_path = path::get_template_path(&ty)?;
489        if tmpl_path.exists() {
490            continue;
491        }
492        super::write_file(&tmpl_path, tmpl)?;
493    }
494    Ok(())
495}
496
497/// 判斷是否需要寫入主資料庫(script_infos 表格)
498pub fn need_write(arg: &Subs) -> bool {
499    use Subs::*;
500    match arg {
501        Edit { .. } => true,
502        CP { .. } => true,
503        RM { .. } => true,
504        LoadUtils { .. } => true,
505        MV {
506            ty,
507            tags,
508            new,
509            origin: _,
510        } => {
511            // TODO: 好好測試這個
512            ty.is_some() || tags.is_some() || new.is_some()
513        }
514        _ => false,
515    }
516}
517
518pub async fn after_script(
519    entry: &mut RepoEntry<'_>,
520    path: &Path,
521    prepare_resp: Option<PrepareRespond>,
522) -> Result {
523    let mut record_write = true;
524    let new_hash = super::compute_file_hash(path)?;
525    match prepare_resp {
526        None => {
527            log::debug!("不執行後處理");
528        }
529        Some(PrepareRespond::New { create_time }) => {
530            let modified = super::file_modify_time(path)?;
531            if create_time >= modified {
532                log::info!("新腳本未變動,應刪除之");
533                return Err(Error::EmptyCreate);
534            }
535        }
536        Some(PrepareRespond::Old { last_hash }) => {
537            if last_hash == new_hash {
538                log::info!("舊腳本未變動,不記錄寫事件(只記讀事件)");
539                record_write = false;
540            }
541        }
542    }
543    if record_write {
544        entry
545            .update(|info| {
546                info.write();
547                info.hash = new_hash;
548            })
549            .await?;
550    }
551    Ok(())
552}
553
554fn check_path_collision<R: StableRepo>(p: &Path, script_repo: &mut R) -> Result {
555    for script in script_repo.iter_mut(Visibility::All) {
556        let script_p = path::open_script(&script.name, &script.ty, None)?;
557        if &script_p == p {
558            return Err(Error::PathExist(script_p).context("與既存腳本撞路徑"));
559        }
560    }
561    Ok(())
562}
563
564pub fn get_all_active_process_locks() -> Result<Vec<ProcessLockRead>> {
565    let dir_path = path::get_process_lock_dir()?;
566    let dir = super::handle_fs_res(&[&dir_path], read_dir(&dir_path))?;
567    let mut ret = vec![];
568    for entry in dir {
569        let file_name = entry?.file_name();
570
571        // TODO: concurrent?
572        let file_name = file_name
573            .to_str()
574            .ok_or_else(|| Error::msg("檔案實體為空...?"))?;
575
576        let inner = |file_name| -> Result<Option<ProcessLockRead>> {
577            let file_path = dir_path.join(file_name);
578            let mut builder = ProcessLockRead::builder(file_path, file_name)?;
579
580            if builder.get_can_write()? {
581                log::info!("remove inactive file lock {:?}", builder.path);
582                super::remove(&builder.path)?;
583                Ok(None)
584            } else {
585                log::info!("found active file lock {:?}", builder.path);
586                Ok(Some(builder.build()?))
587            }
588        };
589        let lock = match inner(file_name) {
590            Ok(None) => continue,
591            Ok(Some(l)) => l,
592            Err(e) => {
593                log::warn!("error building process lock for {}: {:?}", file_name, e);
594                continue;
595            }
596        };
597        ret.push(lock);
598    }
599
600    Ok(ret)
601}