1e186369c9
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m56s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/446 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
147 lines
4.4 KiB
Rust
147 lines
4.4 KiB
Rust
//! 正式 Web 预览的短时可访问性观察;不影响预览业务生命周期。
|
|
use super::{
|
|
contract::{Context, EventData, PreviewReady, PreviewSource, Source},
|
|
store::AnalyticsWriter,
|
|
};
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
sync::{Arc, Weak},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
pub(crate) struct Lease {
|
|
_alive: Arc<()>,
|
|
}
|
|
|
|
pub(crate) struct Observation {
|
|
alive: Weak<()>,
|
|
instance_id: String,
|
|
root: PathBuf,
|
|
entry: PathBuf,
|
|
project_id: String,
|
|
revision: u64,
|
|
context: Context,
|
|
writer: AnalyticsWriter,
|
|
source: Source,
|
|
preview_source: PreviewSource,
|
|
started: Instant,
|
|
cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
|
|
}
|
|
|
|
fn nonempty_entry(entry: &Path) -> bool {
|
|
std::fs::metadata(entry).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
|
|
}
|
|
|
|
pub(crate) fn prepare(
|
|
root: &Path,
|
|
capture: Option<(Context, AnalyticsWriter)>,
|
|
source: Source,
|
|
preview_source: PreviewSource,
|
|
) -> Option<(Lease, Observation)> {
|
|
let (context, writer) = capture?;
|
|
let entry = crate::project_game_root(root).join("index.html");
|
|
if !nonempty_entry(&entry) {
|
|
return None;
|
|
}
|
|
let project_id = crate::read_existing_manifest_for_project(root)
|
|
.ok()?
|
|
.project_id;
|
|
let revision = crate::read_game_creator_agent_runtime_project_revision(root)
|
|
.ok()?
|
|
.revision;
|
|
let alive = Arc::new(());
|
|
let observation = Observation {
|
|
alive: Arc::downgrade(&alive),
|
|
instance_id: uuid::Uuid::new_v4().to_string(),
|
|
root: root.into(),
|
|
entry,
|
|
project_id,
|
|
revision,
|
|
context,
|
|
writer,
|
|
source,
|
|
preview_source,
|
|
started: Instant::now(),
|
|
cancellation: None,
|
|
};
|
|
Some((Lease { _alive: alive }, observation))
|
|
}
|
|
|
|
impl Observation {
|
|
pub(crate) fn with_cancellation(
|
|
mut self,
|
|
cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
|
|
) -> Self {
|
|
self.cancellation = cancellation;
|
|
self
|
|
}
|
|
|
|
fn current(&self) -> bool {
|
|
!self
|
|
.cancellation
|
|
.as_ref()
|
|
.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::Acquire))
|
|
&& self.alive.strong_count() > 0
|
|
&& self.entry == crate::project_game_root(&self.root).join("index.html")
|
|
&& nonempty_entry(&self.entry)
|
|
&& crate::read_game_creator_agent_runtime_project_revision(&self.root)
|
|
.is_ok_and(|revision| revision.revision == self.revision)
|
|
&& crate::read_existing_manifest_for_project(&self.root)
|
|
.is_ok_and(|manifest| manifest.project_id == self.project_id)
|
|
}
|
|
|
|
pub(crate) fn schedule(self, port: u16) {
|
|
tauri::async_runtime::spawn(self.observe(port));
|
|
}
|
|
|
|
pub(crate) async fn observe(self, port: u16) {
|
|
if !self.current() {
|
|
return;
|
|
}
|
|
let reachable = tokio::time::timeout(Duration::from_secs(2), async {
|
|
let client = reqwest::Client::builder()
|
|
.no_proxy()
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
.build()
|
|
.ok()?;
|
|
let mut response = client
|
|
.get(format!("http://127.0.0.1:{port}/"))
|
|
.send()
|
|
.await
|
|
.ok()?;
|
|
if !response.status().is_success() {
|
|
return None;
|
|
}
|
|
while let Some(chunk) = response.chunk().await.ok()? {
|
|
if !chunk.is_empty() {
|
|
return Some(());
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.await;
|
|
if !matches!(reachable, Ok(Some(()))) || !self.current() {
|
|
return;
|
|
}
|
|
let event = self.context.capture(
|
|
EventData::PreviewReady(PreviewReady {
|
|
preview_source: self.preview_source,
|
|
preview_version: self.revision.to_string(),
|
|
ready_duration_ms: u64::try_from(self.started.elapsed().as_millis()).ok(),
|
|
}),
|
|
Some(self.project_id.clone()),
|
|
self.source,
|
|
None,
|
|
);
|
|
if let Ok(event) = event {
|
|
if self.current() {
|
|
let key = format!(
|
|
"{}:{}:{}:preview_ready",
|
|
self.project_id, self.instance_id, self.revision
|
|
);
|
|
self.writer.try_record(self.context.route, event, key);
|
|
}
|
|
}
|
|
}
|
|
}
|