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 pub explicit_tag: bool,
26 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)?; 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 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}
121unsafe 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 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 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 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 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 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
475pub 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 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 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}