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 pub explicit_tag: bool,
27 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)?; 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 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 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)?; 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
211pub 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 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 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 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 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
497pub 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 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 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}