hyper_scripter/script_repo/
mod.rs

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