muxr_client/session/
start.rs1use std::fs;
2use std::os::unix::process::CommandExt;
3use std::path::Path;
4use std::path::PathBuf;
5use std::process::Command;
6use std::process::Stdio;
7
8use muxr_core::ServerRunnerArgs;
9use muxr_core::SessionName;
10use muxr_core::SessionPaths;
11use rootcause::prelude::ResultExt;
12use rootcause::report;
13
14pub struct SpawnedServer {
15 pub log_locator: ServerLogLocator,
16}
17
18pub struct ServerLogLocator {
19 pub file_pattern: String,
20 pub logs_dir: PathBuf,
21 pub pid: u32,
22}
23
24pub fn cleanup_stale_session_files(paths: &SessionPaths) -> rootcause::Result<()> {
25 self::remove_file_if_exists(&paths.socket)?;
27 self::remove_file_if_exists(&paths.pid)?;
28 Ok(())
29}
30
31pub fn spawn_server_process(
32 session: &SessionName,
33 paths: &SessionPaths,
34 server_executable: &Path,
35 external_layout: Option<&Path>,
36) -> rootcause::Result<SpawnedServer> {
37 if !server_executable.is_file() {
39 return Err(report!("missing muxr server runner")
40 .attach(format!("expected={}", server_executable.display()))
41 .attach("run the muxr install/build step so muxr-server is installed next to muxr"));
42 }
43 let logs_dir = paths.logs_root()?;
44 let mut cmd = self::server_cmd(session, server_executable, external_layout);
45
46 let child = cmd.spawn().context("failed to spawn muxr server")?;
47 let pid = child.id();
48 drop(child);
49 Ok(SpawnedServer {
50 log_locator: ServerLogLocator {
51 file_pattern: SessionPaths::server_log_file_pattern(session, pid),
52 logs_dir,
53 pid,
54 },
55 })
56}
57
58fn server_cmd(session: &SessionName, server_executable: &Path, external_layout: Option<&Path>) -> Command {
59 let mut cmd = Command::new(server_executable);
60 let runner_args = ServerRunnerArgs {
61 external_layout: external_layout.map(Path::to_path_buf),
62 session: session.clone(),
63 };
64 cmd.args(runner_args.argv())
65 .stdin(Stdio::null())
66 .stdout(Stdio::null())
67 .stderr(Stdio::null())
68 .process_group(0);
69 cmd
70}
71
72fn remove_file_if_exists(path: &Path) -> rootcause::Result<()> {
73 match fs::remove_file(path) {
74 Ok(()) => Ok(()),
75 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
76 Err(error) => Err(error).context("failed to remove stale muxr file")?,
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use std::ffi::OsStr;
83 use std::os::unix::net::UnixListener;
84 use std::path::Path;
85
86 use muxr_core::EXTERNAL_LAYOUT_ARG;
87 use muxr_core::SessionPaths;
88 use test_that::prelude::*;
89
90 use super::*;
91
92 #[test]
93 fn test_cleanup_stale_session_files_when_running_pid_has_missing_socket_removes_pid() -> rootcause::Result<()> {
94 let tempdir = tempfile::tempdir()?;
95 let (_, paths) = self::session_paths(tempdir.path(), "work")?;
96 fs::create_dir_all(&paths.root)?;
97 fs::write(&paths.pid, std::process::id().to_string())?;
98
99 cleanup_stale_session_files(&paths)?;
100
101 assert_that!(paths.pid.exists(), eq(false));
102 assert_that!(paths.socket.exists(), eq(false));
103 Ok(())
104 }
105
106 #[test]
107 fn test_cleanup_stale_session_files_when_socket_path_exists_removes_socket_path() -> rootcause::Result<()> {
108 let tempdir = tempfile::tempdir()?;
109 let (_, paths) = self::session_paths(tempdir.path(), "work")?;
110 fs::create_dir_all(&paths.root)?;
111 let _listener = UnixListener::bind(&paths.socket)?;
112
113 cleanup_stale_session_files(&paths)?;
114
115 assert_that!(paths.socket.exists(), eq(false));
116 Ok(())
117 }
118
119 #[test]
120 fn test_server_cmd_uses_supplied_executable_for_server() -> rootcause::Result<()> {
121 let session: SessionName = "work".parse()?;
122 let cmd = server_cmd(&session, Path::new("/tmp/custom-muxr"), None);
123 let args: Vec<_> = cmd.get_args().collect();
124
125 assert_that!(cmd.get_program(), eq(OsStr::new("/tmp/custom-muxr")));
126 assert_that!(args.as_slice(), eq([OsStr::new("work")]));
127 Ok(())
128 }
129
130 #[test]
131 fn test_server_cmd_when_external_layout_is_supplied_passes_layout_to_server() -> rootcause::Result<()> {
132 let session: SessionName = "work".parse()?;
133 let layout = Path::new("../.config/muxr/layouts/work.json");
134 let cmd = server_cmd(&session, Path::new("/tmp/custom-muxr"), Some(layout));
135 let args: Vec<_> = cmd.get_args().collect();
136
137 assert_that!(
138 args.as_slice(),
139 eq([
140 OsStr::new("work"),
141 OsStr::new(EXTERNAL_LAYOUT_ARG),
142 OsStr::new("../.config/muxr/layouts/work.json")
143 ])
144 );
145 Ok(())
146 }
147
148 #[test]
149 fn test_spawn_server_process_when_runner_is_missing_returns_error() -> rootcause::Result<()> {
150 let session: SessionName = "work".parse()?;
151 let tempdir = tempfile::tempdir()?;
152 let missing_runner = tempdir.path().join("muxr-server");
153 let (_, paths) = self::session_paths(tempdir.path(), "work")?;
154
155 assert_that!(
156 spawn_server_process(&session, &paths, &missing_runner, None).map(|_| ()),
157 err(displays_as(contains_substring("missing muxr server runner")))
158 );
159 Ok(())
160 }
161
162 fn session_paths(base: &Path, raw: &str) -> rootcause::Result<(SessionName, SessionPaths)> {
163 let session = raw.parse()?;
164 let root = base.join("sessions").join(raw);
165
166 Ok((
167 session,
168 SessionPaths {
169 socket: root.join("server.sock"),
170 pid: root.join("server.pid"),
171 layout: root.join("layout.json"),
172 panes: root.join("panes"),
173 root,
174 },
175 ))
176 }
177}