Merge branch 'main' of http://192.168.1.41:30061/admin/ODIAN_Launcher
This commit is contained in:
@@ -199,6 +199,19 @@ struct Inner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Inner {
|
impl Inner {
|
||||||
|
/// Take `url` as the DHP's Engine API, and keep it in launcher.json so a launcher started
|
||||||
|
/// offline next time still arms live mode with it. Err is only the save failing: the
|
||||||
|
/// address is in use either way.
|
||||||
|
fn adopt_engine(&mut self, url: String) -> Result<(), String> {
|
||||||
|
let site = self.settings.site_url.clone();
|
||||||
|
let changed = self.state.remember_engine(&site, &url);
|
||||||
|
self.engine_url = Some(url);
|
||||||
|
if changed {
|
||||||
|
self.state.save(&self.settings.install_path())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn status(&self) -> Status {
|
fn status(&self) -> Status {
|
||||||
let installed = self.state.installed_version.clone();
|
let installed = self.state.installed_version.clone();
|
||||||
// Online means "update before launch"; offline launches whatever is installed.
|
// Online means "update before launch"; offline launches whatever is installed.
|
||||||
@@ -387,6 +400,8 @@ impl Launcher {
|
|||||||
pub fn load(app: &AppHandle) -> Result<Launcher, String> {
|
pub fn load(app: &AppHandle) -> Result<Launcher, String> {
|
||||||
let settings = Settings::load(app)?;
|
let settings = Settings::load(app)?;
|
||||||
let state = LauncherState::load(&settings.install_path())?;
|
let state = LauncherState::load(&settings.install_path())?;
|
||||||
|
// Where live mode forwards, known before the first probe: offline, there is no probe.
|
||||||
|
let engine_url = state.engine_url(&settings.site_url);
|
||||||
let token = tokens::load(&settings.site_url);
|
let token = tokens::load(&settings.site_url);
|
||||||
let http = reqwest::Client::builder()
|
let http = reqwest::Client::builder()
|
||||||
.user_agent(concat!("ODIAN-Launcher/", env!("CARGO_PKG_VERSION")))
|
.user_agent(concat!("ODIAN-Launcher/", env!("CARGO_PKG_VERSION")))
|
||||||
@@ -394,7 +409,7 @@ impl Launcher {
|
|||||||
.map_err(|e| format!("cannot start the HTTP client: {e}"))?;
|
.map_err(|e| format!("cannot start the HTTP client: {e}"))?;
|
||||||
Ok(Launcher {
|
Ok(Launcher {
|
||||||
http,
|
http,
|
||||||
inner: Mutex::new(Inner { settings, state, online: false, available: None, token, phase: Phase::Checking, error: None, sync: None, engine_url: None, live: None, map_from_site: None, map_problem: None, tiles_waiting: None, unfinished: None, dev: None, dev_refusal: None, market_checked: None, portable: crate::settings::portable_drive().is_some(), updating_self: false, offline_reason: None, drive_extras: None }),
|
inner: Mutex::new(Inner { settings, state, online: false, available: None, token, phase: Phase::Checking, error: None, sync: None, engine_url, live: None, map_from_site: None, map_problem: None, tiles_waiting: None, unfinished: None, dev: None, dev_refusal: None, market_checked: None, portable: crate::settings::portable_drive().is_some(), updating_self: false, offline_reason: None, drive_extras: None }),
|
||||||
stack: Mutex::new(None),
|
stack: Mutex::new(None),
|
||||||
stack_states: std::sync::Mutex::new(Vec::new()),
|
stack_states: std::sync::Mutex::new(Vec::new()),
|
||||||
sign_in_cancel: Notify::new(),
|
sign_in_cancel: Notify::new(),
|
||||||
@@ -562,14 +577,15 @@ impl Launcher {
|
|||||||
false => self.log(app, "warn", format!("Your DHP did not answer. {why}")),
|
false => self.log(app, "warn", format!("Your DHP did not answer. {why}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
let kept = {
|
||||||
let mut inner = self.inner.lock().await;
|
let mut inner = self.inner.lock().await;
|
||||||
inner.online = probe.online;
|
inner.online = probe.online;
|
||||||
inner.available = probe.version;
|
inner.available = probe.version;
|
||||||
// Keep the last known address while offline: it is where live mode resumes.
|
// Keep the last known address while offline: it is where live mode resumes.
|
||||||
if engine.is_some() {
|
engine.map(|url| inner.adopt_engine(url)).unwrap_or(Ok(()))
|
||||||
inner.engine_url = engine;
|
};
|
||||||
}
|
if let Err(e) = kept {
|
||||||
|
self.log(app, "warn", format!("Could not remember the DHP's Engine API address: {e}"));
|
||||||
}
|
}
|
||||||
// Offline nothing is checked or forgotten: the remembered operator still launches.
|
// Offline nothing is checked or forgotten: the remembered operator still launches.
|
||||||
if let (true, Some(token)) = (probe.online, token) {
|
if let (true, Some(token)) = (probe.online, token) {
|
||||||
@@ -903,6 +919,9 @@ impl Launcher {
|
|||||||
if next.site_url != inner.settings.site_url {
|
if next.site_url != inner.settings.site_url {
|
||||||
inner.token = tokens::load(&next.site_url);
|
inner.token = tokens::load(&next.site_url);
|
||||||
inner.available = None;
|
inner.available = None;
|
||||||
|
// Another DHP's Engine API is not this one's: only what this PC learned from the
|
||||||
|
// new site, if it was ever on it.
|
||||||
|
inner.engine_url = inner.state.engine_url(&next.site_url);
|
||||||
}
|
}
|
||||||
inner.settings = next;
|
inner.settings = next;
|
||||||
}
|
}
|
||||||
@@ -1859,6 +1878,12 @@ impl Launcher {
|
|||||||
|
|
||||||
/// Start the local stack. Returns once launching has begun; `stack` and `status` events follow.
|
/// Start the local stack. Returns once launching has begun; `stack` and `status` events follow.
|
||||||
pub async fn launch(&self, app: &AppHandle) -> Result<(), String> {
|
pub async fn launch(&self, app: &AppHandle) -> Result<(), String> {
|
||||||
|
// The DHP is looked at on start, on focus and on request. A PC that was offline then and
|
||||||
|
// has connected since would otherwise launch on that stale answer - no live mode for the
|
||||||
|
// whole session, and no update check. Offline it costs one probe (2 s at most).
|
||||||
|
if !self.inner.lock().await.online {
|
||||||
|
self.refresh(app).await;
|
||||||
|
}
|
||||||
let config = {
|
let config = {
|
||||||
let mut inner = self.inner.lock().await;
|
let mut inner = self.inner.lock().await;
|
||||||
let status = inner.status();
|
let status = inner.status();
|
||||||
@@ -2428,12 +2453,13 @@ impl Launcher {
|
|||||||
}
|
}
|
||||||
let probe = site::probe(&self.http, &site_url).await;
|
let probe = site::probe(&self.http, &site_url).await;
|
||||||
let engine = self.pick_engine(app, &probe).await;
|
let engine = self.pick_engine(app, &probe).await;
|
||||||
{
|
let kept = {
|
||||||
let mut inner = self.inner.lock().await;
|
let mut inner = self.inner.lock().await;
|
||||||
inner.online = probe.online;
|
inner.online = probe.online;
|
||||||
if engine.is_some() {
|
engine.map(|url| inner.adopt_engine(url)).unwrap_or(Ok(()))
|
||||||
inner.engine_url = engine;
|
};
|
||||||
}
|
if let Err(e) = kept {
|
||||||
|
self.log(app, "warn", format!("Could not remember the DHP's Engine API address: {e}"));
|
||||||
}
|
}
|
||||||
if !probe.online {
|
if !probe.online {
|
||||||
if was_online {
|
if was_online {
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ pub struct LauncherState {
|
|||||||
/// while it was away. None until the first sync.
|
/// while it was away. None until the first sync.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tile_sync: Option<TileSync>,
|
pub tile_sync: Option<TileSync>,
|
||||||
|
/// The DHP's Engine API this PC last reached, so a launcher started OFFLINE still arms
|
||||||
|
/// live mode: the local API then switches to the DHP by itself once the network comes.
|
||||||
|
/// Held only in memory before, so a PC that started the launcher offline ran the whole
|
||||||
|
/// session on its own data even after it connected.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub engine: Option<KnownEngine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whose Engine API, and where. The site is named for the same reason as `TileSync`: an
|
||||||
|
/// address learned from one DHP means nothing to a PC pointed at another.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct KnownEngine {
|
||||||
|
pub site: String,
|
||||||
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whose journal, and how far through it. The site is named because a PC moved to another
|
/// Whose journal, and how far through it. The site is named because a PC moved to another
|
||||||
@@ -60,6 +74,21 @@ impl LauncherState {
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Engine API this PC last reached on `site`, None for a site it never reached.
|
||||||
|
pub fn engine_url(&self, site: &str) -> Option<String> {
|
||||||
|
self.engine.as_ref().filter(|known| known.site == site && !site.is_empty()).map(|known| known.url.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remember `url` as `site`'s Engine API. True when that changed what is on disk.
|
||||||
|
pub fn remember_engine(&mut self, site: &str, url: &str) -> bool {
|
||||||
|
let known = KnownEngine { site: site.to_string(), url: url.to_string() };
|
||||||
|
if site.is_empty() || self.engine.as_ref() == Some(&known) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.engine = Some(known);
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
@@ -105,3 +134,42 @@ pub fn cached_notes(install_dir: &Path, version: &str) -> Option<String> {
|
|||||||
pub fn cache_notes(install_dir: &Path, version: &str, markdown: &str) -> Result<(), String> {
|
pub fn cache_notes(install_dir: &Path, version: &str, markdown: &str) -> Result<(), String> {
|
||||||
write_atomic(&state_dir(install_dir).join("notes").join(format!("{version}.md")), markdown.as_bytes())
|
write_atomic(&state_dir(install_dir).join("notes").join(format!("{version}.md")), markdown.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const SITE: &str = "https://engine.odian.fr/dhp";
|
||||||
|
const ENGINE: &str = "https://engine-api.odian.fr";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_engine_api_is_remembered_per_site() {
|
||||||
|
let mut state = LauncherState::default();
|
||||||
|
assert_eq!(state.engine_url(SITE), None, "a PC that never reached the DHP knows no address");
|
||||||
|
assert!(state.remember_engine(SITE, ENGINE));
|
||||||
|
assert_eq!(state.engine_url(SITE).as_deref(), Some(ENGINE));
|
||||||
|
// Another DHP's address is not this one's.
|
||||||
|
assert_eq!(state.engine_url("http://192.168.7.2:30097"), None);
|
||||||
|
// Unchanged is not a change: no write for every probe that finds the same address.
|
||||||
|
assert!(!state.remember_engine(SITE, ENGINE));
|
||||||
|
// And a PC set offline on purpose (no DHP address) remembers nothing.
|
||||||
|
assert!(!state.remember_engine("", ENGINE));
|
||||||
|
assert_eq!(state.engine_url(""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_engine_api_survives_a_restart_and_old_files_still_load() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("odian-state-{}", uuid::Uuid::new_v4()));
|
||||||
|
let mut state = LauncherState::default();
|
||||||
|
state.remember_engine(SITE, ENGINE);
|
||||||
|
state.save(&dir).unwrap();
|
||||||
|
// What a launcher started offline reads: the address it had before it was closed.
|
||||||
|
assert_eq!(LauncherState::load(&dir).unwrap().engine_url(SITE).as_deref(), Some(ENGINE));
|
||||||
|
|
||||||
|
// A launcher.json from before this field loads, with no address.
|
||||||
|
std::fs::write(dir.join("state").join("launcher.json"), br#"{"install_id":"abc","operator":"jimmy"}"#).unwrap();
|
||||||
|
let old = LauncherState::load(&dir).unwrap();
|
||||||
|
assert_eq!((old.install_id.as_str(), old.engine_url(SITE)), ("abc", None));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user