use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{anyhow, bail, Result};
use super::{DaemonHandle, DaemonHost};
use crate::tui::{self, LogSource, LogTx, ServiceStatus};
pub struct RemoteHost {
socket_path: PathBuf,
dev_dir_override: Option<PathBuf>,
log_tx: Option<LogTx>,
}
impl RemoteHost {
pub fn new(
socket_path: PathBuf,
dev_dir_override: Option<PathBuf>,
log_tx: Option<LogTx>,
) -> Self {
Self {
socket_path,
dev_dir_override,
log_tx,
}
}
}
impl DaemonHost for RemoteHost {
fn pre_start_dev_dir(&self) -> Option<PathBuf> {
self.dev_dir_override
.clone()
.or_else(|| Some(default_dev_dir()))
}
fn ensure_running(&self) -> Result<DaemonHandle> {
if !self.socket_path.exists() {
return Err(anyhow!(
"socket '{}' does not exist locally. \
Either start the daemon, or set up an SSH tunnel:\n \
ssh -fNL {}:/run/node/control.sock pi@pi.local",
self.socket_path.display(),
self.socket_path.display(),
));
}
let dev_dir = self
.dev_dir_override
.clone()
.unwrap_or_else(default_dev_dir);
std::fs::create_dir_all(&dev_dir).ok();
tui::update_status(
self.log_tx.as_ref(),
LogSource::Daemon,
ServiceStatus::Ready,
Some(format!("remote socket: {}", self.socket_path.display())),
);
Ok(DaemonHandle {
name: String::new(),
socket_path: self.socket_path.clone(),
dev_dir,
banner: format!(
"remote daemon at {} (you started it; we just sideload)",
self.socket_path.display()
),
api_base_url: None,
})
}
fn restart(&self) -> Result<()> {
tui::sys_log(self.log_tx.as_ref(), "→ sending app.restart over IPC…");
let result = ipc_call(&self.socket_path, "app.restart", serde_json::json!({}));
match result {
Ok(_) => {
tui::sys_log(self.log_tx.as_ref(), "✓ restart request sent to remote daemon");
tui::update_status(
self.log_tx.as_ref(),
LogSource::Daemon,
ServiceStatus::Starting,
Some("restart sent via IPC".into()),
);
}
Err(e) => {
tui::sys_log(
self.log_tx.as_ref(),
format!(
"⚠ app.restart IPC failed: {e}\n \
The remote daemon may not support IPC restart. \
Restart it manually."
),
);
}
}
Ok(())
}
}
fn ipc_call(socket_path: &std::path::Path, method: &str, params: serde_json::Value) -> Result<serde_json::Value> {
let stream = UnixStream::connect(socket_path).with_context(|| {
format!("connect to {}", socket_path.display())
})?;
stream.set_read_timeout(Some(Duration::from_secs(10))).ok();
stream.set_write_timeout(Some(Duration::from_secs(10))).ok();
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
});
let mut line = serde_json::to_string(&req)?;
line.push('\n');
let mut stream = stream;
stream.write_all(line.as_bytes())?;
stream.flush().ok();
let response_line = BufReader::new(&stream)
.lines()
.next()
.ok_or_else(|| anyhow!("daemon closed connection without responding"))??;
let v: serde_json::Value = serde_json::from_str(&response_line)?;
if let Some(err) = v.get("error") {
bail!("IPC error: {}", err);
}
Ok(v["result"].clone())
}
use anyhow::Context;
fn default_dev_dir() -> PathBuf {
if let Ok(env) = std::env::var("NODE_DEV_APPS_DIR") {
if !env.is_empty() {
return PathBuf::from(env);
}
}
let home = std::env::var_os("HOME").map(PathBuf::from).unwrap_or_default();
home.join(".local/share/node-app/dev-apps")
}