hyper_scripter/script_repo/
mod.rs

1use crate::config::Recent;
2use crate::error::Result;
3use crate::script::{IntoScriptName, ScriptInfo, ScriptName};
4use crate::script_type::ScriptType;
5use crate::tag::{Tag, TagSelectorGroup};
6use chrono::{Duration, NaiveDateTime, Utc};
7use fxhash::FxHashMap as HashMap;
8use hyper_scripter_historian::{Event, EventData, Historian, LastTimeRecord};
9use sqlx::SqlitePool;
10use std::collections::hash_map::Entry::{self, *};
11
12pub mod helper;
13pub use helper::RepoEntry;
14
15#[derive(Clone, Debug, Default)]
16pub struct RecentFilter {
17    pub recent: Recent,
18    pub archaeology: bool,
19}
20enum TimeBound {
21    Timeless,
22    Bound(Option<NaiveDateTime>),
23}
24impl TimeBound {
25    fn new(recent: Recent) -> Self {
26        match recent {
27            Recent::Timeless => TimeBound::Timeless,
28            Recent::NoNeglect => TimeBound::Bound(None),
29            Recent::Days(d) => {
30                let mut time = Utc::now().naive_utc();
31                time -= Duration::days(d.into());
32                TimeBound::Bound(Some(time))
33            }
34        }
35    }
36}
37
38#[derive(Debug, Clone, Copy, Eq, PartialEq)]
39pub enum Visibility {
40    Normal,
41    All,
42    Inverse,
43}
44impl Visibility {
45    pub fn is_normal(&self) -> bool {
46        matches!(self, Self::Normal)
47    }
48    pub fn is_all(&self) -> bool {
49        matches!(self, Self::All)
50    }
51    pub fn is_inverse(&self) -> bool {
52        matches!(self, Self::Inverse)
53    }
54    pub fn invert(self) -> Self {
55        match self {
56            Self::Normal => Self::Inverse,
57            Self::Inverse => Self::Normal,
58            Self::All => {
59                log::warn!("無效的可見度反轉:all => all");
60                Self::All
61            }
62        }
63    }
64}
65
66#[derive(Debug)]
67enum TraceOption {
68    Normal,
69    // record nothing
70    NoTrace,
71    // don't affect last time, only record history
72    Humble,
73}
74
75#[derive(Debug)]
76pub struct DBEnv {
77    info_pool: SqlitePool,
78    pub historian: Historian,
79    trace_opt: TraceOption,
80    modifies_script: bool,
81}
82
83pub struct RepoEntryOptional<'b> {
84    entry: Entry<'b, String, ScriptInfo>,
85    env: &'b DBEnv,
86}
87impl<'b> RepoEntryOptional<'b> {
88    pub async fn or_insert(self, info: ScriptInfo) -> Result<RepoEntry<'b>> {
89        let exist = matches!(&self.entry, Occupied(_));
90        let info = self.entry.or_insert(info);
91        if !exist {
92            log::debug!("往資料庫塞新腳本 {:?}", info);
93            let id = self.env.handle_insert(info).await?;
94            log::debug!("往資料庫新增腳本成功,得 id = {}", id);
95            info.set_id(id as i64);
96        }
97        Ok(RepoEntry::new(info, self.env))
98    }
99}
100
101impl DBEnv {
102    pub async fn close(self) {
103        futures::join!(self.info_pool.close(), self.historian.close());
104
105        // FIXME: sqlx bug: 這邊可能不會正確關閉,導致有些記錄遺失,暫時解是關閉後加一個 `sleep`
106        #[cfg(debug_assertions)]
107        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
108    }
109    pub fn new(info_pool: SqlitePool, historian: Historian, modifies_script: bool) -> Self {
110        Self {
111            info_pool,
112            historian,
113            modifies_script,
114            trace_opt: TraceOption::Normal,
115        }
116    }
117    pub async fn handle_neglect(&self, id: i64) -> Result {
118        let time = Utc::now().naive_utc();
119        sqlx::query!(
120            "
121            INSERT OR IGNORE INTO last_events (script_id) VALUES(?);
122            UPDATE last_events SET neglect = ? WHERE script_id = ?
123            ",
124            id,
125            time,
126            id
127        )
128        .execute(&self.info_pool)
129        .await?;
130        Ok(())
131    }
132
133    pub async fn update_last_time_directly(&self, last_time: LastTimeRecord) -> Result {
134        let LastTimeRecord {
135            script_id,
136            exec_time,
137            exec_done_time,
138            humble_time,
139        } = last_time;
140        sqlx::query!(
141            "UPDATE last_events set humble = ?, exec = ?, exec_done = ? WHERE script_id = ?",
142            humble_time,
143            exec_time,
144            exec_done_time,
145            script_id
146        )
147        .execute(&self.info_pool)
148        .await?;
149        Ok(())
150    }
151    async fn update_last_time(&self, info: &ScriptInfo) -> Result {
152        let exec_count = info.exec_count as i32;
153        match self.trace_opt {
154            TraceOption::NoTrace => return Ok(()),
155            TraceOption::Normal => (),
156            TraceOption::Humble => {
157                // FIXME: what if a script is created with humble?
158                let humble_time = info.last_major_time();
159                sqlx::query!(
160                    "UPDATE last_events set humble = ?, exec_count = ? WHERE script_id = ?",
161                    humble_time,
162                    exec_count,
163                    info.id,
164                )
165                .execute(&self.info_pool)
166                .await?;
167                return Ok(());
168            }
169        }
170
171        let exec_time = info.exec_time.as_ref().map(|t| **t);
172        let exec_done_time = info.exec_done_time.as_ref().map(|t| **t);
173        let neglect_time = info.neglect_time.as_ref().map(|t| **t);
174        sqlx::query!(
175            "
176            INSERT OR REPLACE INTO last_events
177            (script_id, read, write, exec, exec_done, neglect, humble, exec_count)
178            VALUES(?, ?, ?, ?, ?, ?, ?, ?)
179            ",
180            info.id,
181            *info.read_time,
182            *info.write_time,
183            exec_time,
184            exec_done_time,
185            neglect_time,
186            info.humble_time,
187            exec_count
188        )
189        .execute(&self.info_pool)
190        .await?;
191        Ok(())
192    }
193
194    async fn handle_delete(&self, id: i64) -> Result {
195        assert!(self.modifies_script);
196        self.historian.remove(id).await?;
197        log::debug!("清理腳本 {:?} 的最新事件", id);
198        sqlx::query!("DELETE FROM last_events WHERE script_id = ?", id)
199            .execute(&self.info_pool)
200            .await?;
201        sqlx::query!("DELETE from script_infos where id = ?", id)
202            .execute(&self.info_pool)
203            .await?;
204        Ok(())
205    }
206
207    async fn handle_insert(&self, info: &ScriptInfo) -> Result<i64> {
208        assert!(self.modifies_script);
209        let name_cow = info.name.key();
210        let name = name_cow.as_ref();
211        let ty = info.ty.as_ref();
212        let tags = join_tags(info.tags.iter());
213        let res = sqlx::query!(
214            "
215            INSERT INTO script_infos (name, ty, tags, hash)
216            VALUES(?, ?, ?, ?)
217            RETURNING id
218            ",
219            name,
220            ty,
221            tags,
222            info.hash,
223        )
224        .fetch_one(&self.info_pool)
225        .await?;
226        Ok(res.id)
227    }
228
229    async fn handle_change(&self, info: &ScriptInfo) -> Result<i64> {
230        log::debug!("開始修改資料庫 {:?}", info);
231        if info.changed {
232            assert!(self.modifies_script);
233            let name = info.name.key();
234            let name = name.as_ref();
235            let tags = join_tags(info.tags.iter());
236            let ty = info.ty.as_ref();
237            sqlx::query!(
238                "UPDATE script_infos SET name = ?, tags = ?, ty = ?, hash = ? where id = ?",
239                name,
240                tags,
241                ty,
242                info.hash,
243                info.id,
244            )
245            .execute(&self.info_pool)
246            .await?;
247        }
248
249        if matches!(self.trace_opt, TraceOption::NoTrace) {
250            return Ok(0);
251        }
252
253        let mut last_event_id = 0;
254        macro_rules! record_event {
255            ($time:expr, $data:expr) => {
256                self.historian.record(&Event {
257                    script_id: info.id,
258                    humble: matches!(self.trace_opt, TraceOption::Humble),
259                    time: $time,
260                    data: $data,
261                })
262            };
263        }
264
265        if let Some(time) = info.exec_done_time.as_ref() {
266            if let Some(&(code, main_event_id)) = time.data() {
267                log::debug!("{:?} 的執行完畢事件", info.name);
268                last_event_id = record_event!(
269                    **time,
270                    EventData::ExecDone {
271                        code,
272                        main_event_id,
273                    }
274                )
275                .await?;
276
277                if last_event_id != 0 {
278                    self.update_last_time(info).await?;
279                } else {
280                    log::info!("{:?} 的執行完畢事件被忽略了", info.name);
281                }
282                return Ok(last_event_id); // XXX: 超級醜的作法,為了避免重復記錄其它的事件
283            }
284        }
285
286        self.update_last_time(info).await?;
287
288        if info.read_time.has_changed() {
289            log::debug!("{:?} 的讀取事件", info.name);
290            last_event_id = record_event!(*info.read_time, EventData::Read).await?;
291        }
292        if info.write_time.has_changed() {
293            log::debug!("{:?} 的寫入事件", info.name);
294            last_event_id = record_event!(*info.write_time, EventData::Write).await?;
295        }
296        if let Some(time) = info.exec_time.as_ref() {
297            if let Some((content, args, envs, dir)) = time.data() {
298                log::debug!("{:?} 的執行事件", info.name);
299                last_event_id = record_event!(
300                    **time,
301                    EventData::Exec {
302                        content,
303                        args,
304                        envs,
305                        dir: dir.as_deref(),
306                    }
307                )
308                .await?;
309            }
310        }
311
312        Ok(last_event_id)
313    }
314}
315
316fn join_tags<'a, I: Iterator<Item = &'a Tag>>(tags: I) -> String {
317    let tags_arr: Vec<&str> = tags.map(|t| t.as_ref()).collect();
318    tags_arr.join(",")
319}
320
321#[derive(Debug)]
322pub struct ScriptRepo {
323    map: HashMap<String, ScriptInfo>,
324    hidden_map: HashMap<String, ScriptInfo>,
325    latest_name: Option<String>,
326    db_env: DBEnv,
327    pub time_hidden_count: u32,
328}
329
330macro_rules! iter_by_vis {
331    ($self:expr, $vis:expr) => {{
332        let (iter, iter2) = match $vis {
333            Visibility::Normal => ($self.map.iter_mut(), None),
334            Visibility::All => ($self.map.iter_mut(), Some($self.hidden_map.iter_mut())),
335            Visibility::Inverse => ($self.hidden_map.iter_mut(), None),
336        };
337        iter.chain(iter2.into_iter().flatten()).map(|(_, v)| v)
338    }};
339}
340
341impl ScriptRepo {
342    pub async fn close(self) {
343        self.db_env.close().await;
344    }
345    pub fn iter(&self) -> impl Iterator<Item = &ScriptInfo> {
346        self.map.iter().map(|(_, info)| info)
347    }
348    pub fn iter_mut(&mut self, visibility: Visibility) -> impl Iterator<Item = RepoEntry<'_>> {
349        iter_by_vis!(self, visibility).map(|info| RepoEntry::new(info, &self.db_env))
350    }
351    pub fn historian(&self) -> &Historian {
352        &self.db_env.historian
353    }
354    pub async fn new(
355        recent: RecentFilter,
356        db_env: DBEnv,
357        selector: &TagSelectorGroup,
358    ) -> Result<ScriptRepo> {
359        let mut hidden_map = HashMap::<String, ScriptInfo>::default();
360        let mut map: HashMap<String, ScriptInfo> = Default::default();
361        let time_bound = TimeBound::new(recent.recent);
362
363        let scripts = sqlx::query!(
364            "SELECT * FROM script_infos si LEFT JOIN last_events le ON si.id = le.script_id"
365        )
366        .fetch_all(&db_env.info_pool)
367        .await?;
368        let mut time_hidden_count = 0;
369        for record in scripts.into_iter() {
370            let name = record.name;
371            log::trace!("載入腳本:{} {} {}", name, record.ty, record.tags);
372            let script_name = name.clone().into_script_name_unchecked()?; // NOTE: 從資料庫撈出來就別檢查了吧
373
374            let mut builder = ScriptInfo::builder(
375                record.id,
376                record.hash,
377                script_name,
378                ScriptType::new_unchecked(record.ty),
379                record.tags.split(',').filter_map(|s| {
380                    if s.is_empty() {
381                        None
382                    } else {
383                        Some(Tag::new_unchecked(s.to_string()))
384                    }
385                }),
386            );
387
388            builder.created_time(record.created_time);
389            builder.exec_count(record.exec_count.unwrap_or_default() as u64);
390            if let Some(time) = record.write {
391                builder.write_time(time);
392            }
393            if let Some(time) = record.read {
394                builder.read_time(time);
395            }
396            if let Some(time) = record.exec {
397                builder.exec_time(time);
398            }
399            if let Some(time) = record.exec_done {
400                builder.exec_done_time(time);
401            }
402            if let Some(time) = record.neglect {
403                builder.neglect_time(time);
404            }
405            if let Some(time) = record.humble {
406                builder.humble_time(time);
407            }
408            let script = builder.build();
409
410            let mut hide = !selector.select(&script.tags, &script.ty);
411            if !hide {
412                if let Some(neglect) = record.neglect {
413                    log::debug!("腳本 {} 曾於 {} 被忽略", script.name, neglect);
414                }
415
416                let overtime = match time_bound {
417                    TimeBound::Timeless => false,
418                    TimeBound::Bound(time_bound) => {
419                        let time_bound = std::cmp::max(time_bound, record.neglect);
420                        if let Some(time_bound) = time_bound {
421                            time_bound > script.last_major_time()
422                        } else {
423                            false
424                        }
425                    }
426                };
427                hide = recent.archaeology ^ overtime;
428                if hide {
429                    time_hidden_count += 1;
430                }
431            }
432
433            if hide {
434                hidden_map.insert(name, script);
435            } else {
436                log::trace!("腳本 {:?} 通過篩選", name);
437                map.insert(name, script);
438            }
439        }
440        Ok(ScriptRepo {
441            map,
442            hidden_map,
443            latest_name: None,
444            time_hidden_count,
445            db_env,
446        })
447    }
448    pub fn no_trace(&mut self) {
449        self.db_env.trace_opt = TraceOption::NoTrace;
450    }
451    pub fn humble(&mut self) {
452        self.db_env.trace_opt = TraceOption::Humble;
453    }
454    // fn latest_mut_no_cache(&mut self) -> Option<&mut ScriptInfo<'a>> {
455    //     let latest = self.map.iter_mut().max_by_key(|(_, info)| info.last_time());
456    //     if let Some((name, info)) = latest {
457    //         self.latest_name = Some(name.clone());
458    //         Some(info)
459    //     } else {
460    //         None
461    //     }
462    // }
463    pub fn latest_mut(&mut self, n: usize, visibility: Visibility) -> Option<RepoEntry<'_>> {
464        // if let Some(name) = &self.latest_name {
465        //     // FIXME: 一旦 rust nll 進化就修掉這段
466        //     if self.map.contains_key(name) {
467        //         return self.map.get_mut(name);
468        //     }
469        //     log::warn!("快取住的最新資訊已經不見了…?重找一次");
470        // }
471        // self.latest_mut_no_cache()
472        let mut v: Vec<_> = iter_by_vis!(self, visibility).collect();
473        v.sort_by_key(|s| s.last_time());
474        if v.len() >= n {
475            let t = v.remove(v.len() - n);
476            Some(RepoEntry::new(t, &self.db_env))
477        } else {
478            None
479        }
480    }
481    pub fn get_mut(&mut self, name: &ScriptName, visibility: Visibility) -> Option<RepoEntry<'_>> {
482        // FIXME: 一旦 NLL 進化就修掉這個 unsafe
483        let map = &mut self.map as *mut HashMap<String, ScriptInfo>;
484        let map = unsafe { &mut *map };
485        let key = name.key();
486        let info = match visibility {
487            Visibility::Normal => map.get_mut(&*key),
488            Visibility::Inverse => self.hidden_map.get_mut(&*key),
489            Visibility::All => {
490                let info = map.get_mut(&*key);
491                // 用 Option::or 有一些生命週期的怪問題…
492                if info.is_some() {
493                    info
494                } else {
495                    self.hidden_map.get_mut(&*key)
496                }
497            }
498        };
499        let env = &self.db_env;
500        info.map(move |info| RepoEntry::new(info, env))
501    }
502    pub fn get_mut_by_id(&mut self, id: i64) -> Option<RepoEntry<'_>> {
503        // XXX: 複雜度很瞎
504        self.iter_mut(Visibility::All).find(|e| e.id == id)
505    }
506
507    pub async fn remove(&mut self, id: i64) -> Result {
508        // TODO: 從 map 中刪掉?但如果之後沒其它用途似乎也未必需要...
509        log::debug!("從資料庫刪除腳本 {:?}", id);
510        self.db_env.handle_delete(id).await?;
511        Ok(())
512    }
513    pub fn entry(&mut self, name: &ScriptName) -> RepoEntryOptional<'_> {
514        let entry = self.map.entry(name.key().into_owned());
515        RepoEntryOptional {
516            entry,
517            env: &self.db_env,
518        }
519    }
520    pub fn entry_hidden(&mut self, name: &ScriptName) -> RepoEntryOptional<'_> {
521        let entry = self.hidden_map.entry(name.key().into_owned());
522        RepoEntryOptional {
523            entry,
524            env: &self.db_env,
525        }
526    }
527}