Skip to main content

comindly_engine/
run.rs

1//! Starting an engine: a team server when `COMINDLY_PUBLIC_URL` is set, else a local one for this
2//! machine's command line and app.
3
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9
10/// Where a running local engine listens, for the command line to find it.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Running {
13    pub url: String,
14    pub pid: u32,
15    pub version: String,
16}
17
18/// `<config dir>/engine.json`, written while a local engine runs.
19pub fn running_file() -> PathBuf {
20    crate::config::config_dir().join("engine.json")
21}
22
23/// The local engine recorded as running, if its process is still alive.
24pub fn running() -> Option<Running> {
25    let text = std::fs::read_to_string(running_file()).ok()?;
26    let r: Running = serde_json::from_str(&text).ok()?;
27    alive(r.pid).then_some(r)
28}
29
30fn alive(pid: u32) -> bool {
31    #[cfg(unix)]
32    {
33        std::process::Command::new("kill")
34            .args(["-0", &pid.to_string()])
35            .stderr(std::process::Stdio::null())
36            .status()
37            .is_ok_and(|s| s.success())
38    }
39    #[cfg(not(unix))]
40    {
41        let _ = pid;
42        true
43    }
44}
45
46/// Serve the index at `from` until the process ends; `port` 0 takes a free one.
47pub fn serve(from: Option<&str>, host: &str, port: u16) -> Result<()> {
48    let root = crate::config::Config::load().index_dir(from);
49    let data = crate::view::Data::load_or_empty(&root, &crate::config::local_verifier());
50    let addr = format!("{host}:{port}");
51    let listener = std::net::TcpListener::bind(&addr).with_context(|| format!("bind {addr}"))?;
52    let addr = listener.local_addr()?.to_string();
53    if let Some(cfg) = crate::server::ServerConfig::from_env()? {
54        let (url, sign_in) = (cfg.public_url.clone(), cfg.sign_in.clone());
55        let server = crate::server::Server::open(&root, cfg).context("open the server table")?;
56        migrate(&server, &root)?;
57        // What an admin switched in the app outlasts the deployment's `COMINDLY_LOG_SKIP`.
58        if let Some(skip) = server.settings.log_skip() {
59            let off = crate::telemetry::parse_skip(&skip);
60            for kind in ["access", "tool"] {
61                crate::telemetry::set_logs(kind, !off.iter().any(|k| k == kind));
62            }
63        }
64        crate::ui::ok(&format!("comindly server for {url}, listening on {addr}"));
65        let ways: Vec<&str> = sign_in
66            .iter()
67            .map(|m| match m {
68                crate::server::SignIn::Email => "emailed links",
69                crate::server::SignIn::Code => "codes admins hand out",
70                crate::server::SignIn::Oidc => "the OpenID Connect provider",
71            })
72            .collect();
73        crate::ui::note(&format!("members sign in with {}", ways.join(", ")));
74        return crate::web::serve(data, listener, crate::web::Access::Server(Arc::new(server)));
75    }
76    let token = crate::config::ui_token()?;
77    let bound = listener.local_addr()?.port();
78    let running = Running {
79        url: format!("http://{host}:{bound}"),
80        pid: std::process::id(),
81        version: env!("CARGO_PKG_VERSION").into(),
82    };
83    std::fs::write(running_file(), serde_json::to_string(&running)?)?;
84    crate::ui::ok(&format!(
85        "comindly engine on {} (Ctrl-C to stop)",
86        running.url
87    ));
88    let served = crate::web::serve(data, listener, crate::web::Access::Local(token));
89    let _ = std::fs::remove_file(running_file());
90    served
91}
92
93/// Moves a server's kept `config.toml` into its settings and the index's, once.
94fn migrate(server: &crate::server::Server, root: &str) -> Result<()> {
95    let Some(text) = server.settings.legacy_config() else {
96        return Ok(());
97    };
98    if server.settings.repos().is_empty() {
99        let old: crate::config::Config = toml::from_str(&text).unwrap_or_default();
100        server.settings.set_repos("upgrade", &old.repos)?;
101    }
102    crate::rt::block_on(async {
103        let stored = crate::team::load(root).await?;
104        let mut settings = stored.settings;
105        if crate::config::adopt_legacy(&text, &mut settings) {
106            crate::team::save_as(root, &settings, stored.version, "upgrade").await?;
107        }
108        anyhow::Ok(())
109    })?;
110    server.settings.drop_legacy_config()?;
111    crate::ui::note("moved the server's kept config.toml into its settings and the index's");
112    Ok(())
113}