use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::{bail, Context, Result};
use super::dev::host::monorepo::monorepo_dev_dir;
const PICKUP_TIMEOUT: Duration = Duration::from_secs(180);
pub fn run(project_path: &Path, instances: &[String], build: bool, timeout_secs: u64) -> Result<()> {
let project_path = project_path
.canonicalize()
.with_context(|| format!("canonicalize {}", project_path.display()))?;
let instances: Vec<String> = if instances.is_empty() {
vec!["alice".to_string()]
} else {
instances.to_vec()
};
for instance in &instances {
let env_dir = env_dir_for(&project_path, instance)?;
if !env_dir.is_dir() {
bail!(
"no dev environment for instance '{instance}' at {} — has `node-app dev` ever run here?",
env_dir.display()
);
}
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let control_dir = env_dir.join("control");
fs::create_dir_all(&control_dir)
.with_context(|| format!("mkdir -p {}", control_dir.display()))?;
let request_path = control_dir.join("request.json");
let body = serde_json::json!({
"action": "restart",
"build": if build { serde_json::Value::from("system") } else { serde_json::Value::Null },
"ts": ts,
});
let tmp = control_dir.join("request.json.tmp");
fs::write(&tmp, body.to_string()).with_context(|| format!("write {}", tmp.display()))?;
fs::rename(&tmp, &request_path)
.with_context(|| format!("rename → {}", request_path.display()))?;
println!(
"→ requested restart of '{instance}'{}",
if build { " with node-server rebuild" } else { "" }
);
let started = Instant::now();
loop {
if !request_path.exists() {
break;
}
if started.elapsed() > PICKUP_TIMEOUT {
let _ = fs::remove_file(&request_path);
bail!(
"dev session for '{instance}' didn't pick the request up within {}s. \
Either no `node-app dev` session is running, or it is still booting \
(staging + sideloading deps) — wait for it to finish and retry.",
PICKUP_TIMEOUT.as_secs()
);
}
std::thread::sleep(Duration::from_millis(200));
}
let result_path = control_dir.join("last-result.json");
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
loop {
if let Ok(text) = fs::read_to_string(&result_path) {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
if v.get("ts").and_then(|t| t.as_u64()) == Some(ts) {
let ok = v.get("ok").and_then(|b| b.as_bool()).unwrap_or(false);
let msg = v
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("(no message)");
if ok {
println!(
"✓ '{instance}' restarted ({:.1}s): {msg}",
started.elapsed().as_secs_f32()
);
break;
}
bail!("restart of '{instance}' failed: {msg}");
}
}
}
if Instant::now() > deadline {
bail!(
"no result from '{instance}' within {timeout_secs}s — check the dev session logs \
(the restart may still be in progress; a --build of a cold workspace can take longer: \
retry with --timeout 1200)"
);
}
std::thread::sleep(Duration::from_millis(300));
}
}
Ok(())
}
fn env_dir_for(project_path: &Path, instance: &str) -> Result<PathBuf> {
let dev_dir = monorepo_dev_dir(project_path, instance, None)?;
dev_dir
.parent()
.map(|p| p.to_path_buf())
.ok_or_else(|| anyhow::anyhow!("dev dir {} has no parent", dev_dir.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_dir_is_parent_of_dev_apps_dir() {
let tmp = tempfile::TempDir::new().unwrap();
let env_dir = env_dir_for(tmp.path(), "alice").unwrap();
let dev_dir = monorepo_dev_dir(tmp.path(), "alice", None).unwrap();
assert_eq!(dev_dir.parent().unwrap(), env_dir);
assert_eq!(dev_dir, env_dir.join("dev-apps"));
}
#[test]
fn env_dir_differs_per_instance() {
let tmp = tempfile::TempDir::new().unwrap();
let alice = env_dir_for(tmp.path(), "alice").unwrap();
let bob = env_dir_for(tmp.path(), "bob").unwrap();
assert_ne!(alice, bob);
}
}