roblox_studio_utils/
opener.rs1use std::{
2 ffi::OsString,
3 fs, io,
4 net::Ipv4Addr,
5 path::Path,
6 process::{Command, Stdio},
7};
8
9use crate::paths::RobloxStudioPaths;
10use crate::result::{RobloxStudioError, RobloxStudioResult};
11use crate::task::RobloxStudioTask;
12
13const DEFAULT_SERVER_ADDR: Ipv4Addr = Ipv4Addr::LOCALHOST;
14const DEFAULT_SERVER_PORT: u16 = 50608;
15
16#[derive(Debug, Clone)]
21pub struct RobloxStudioOpener {
22 args: Vec<OsString>,
23 task: Option<RobloxStudioTask>,
24 server_addr: Ipv4Addr,
25 server_port: u16,
26}
27
28impl RobloxStudioOpener {
29 #[must_use]
33 pub fn new() -> Self {
34 Self {
35 args: Vec::new(),
36 task: None,
37 server_addr: DEFAULT_SERVER_ADDR,
38 server_port: DEFAULT_SERVER_PORT,
39 }
40 }
41
42 #[must_use]
49 #[doc(hidden)]
50 #[allow(clippy::needless_pass_by_value)]
51 pub fn with_arg<K, V>(mut self, key: K, value: V) -> Self
52 where
53 K: Into<OsString>,
54 V: Into<OsString>,
55 {
56 let key: OsString = key.into();
57 let value: OsString = value.into();
58 if key == "-task"
59 && let Some(task_str) = value.to_str()
60 {
61 self.task = RobloxStudioTask::parse(task_str);
62 }
63 self.args.push(key);
64 self.args.push(value);
65 self
66 }
67
68 fn with_zeros(self) -> Self {
72 self.with_arg("-creatorType", "0")
75 .with_arg("-creatorId", "0")
76 .with_arg("-universeId", "0")
77 .with_arg("-placeId", "0")
78 }
79
80 #[must_use]
87 #[allow(clippy::needless_pass_by_value)]
88 pub fn with_server_addr<A>(mut self, server_addr: A) -> Self
89 where
90 A: Into<Ipv4Addr>,
91 {
92 self.server_addr = server_addr.into();
93 self
94 }
95
96 #[must_use]
103 pub fn with_server_port(mut self, server_port: u16) -> Self {
104 self.server_port = server_port;
105 self
106 }
107
108 #[must_use]
114 pub fn open_place(mut self, universe_id: u64, place_id: u64) -> Self {
115 self.task = Some(RobloxStudioTask::EditPlace);
116 self.with_arg("-task", RobloxStudioTask::EditPlace)
117 .with_arg("-universeId", universe_id.to_string())
118 .with_arg("-placeId", place_id.to_string())
119 }
120
121 pub fn open_file<P>(mut self, file_path: P) -> RobloxStudioResult<Self>
132 where
133 P: AsRef<Path>,
134 {
135 self.task = Some(RobloxStudioTask::EditFile);
136 let file_path_full = file_path
137 .as_ref()
138 .canonicalize()
139 .map_err(|e| RobloxStudioError::PathCanonicalize(e.to_string()))?;
140 let file_path_str = file_path_full
141 .to_str()
142 .ok_or(RobloxStudioError::PathToString(file_path_full.clone()))?;
143 Ok(self
144 .with_arg("-task", RobloxStudioTask::EditFile)
145 .with_arg("-localPlaceFile", file_path_str))
146 }
147
148 pub fn start_server<P>(mut self, file_path: P) -> RobloxStudioResult<Self>
160 where
161 P: AsRef<Path>,
162 {
163 self.task = Some(RobloxStudioTask::StartServer);
164 let file_path_source = file_path
165 .as_ref()
166 .canonicalize()
167 .map_err(|e| RobloxStudioError::PathCanonicalize(e.to_string()))?;
168 let file_path_target = dirs::data_local_dir()
169 .ok_or(RobloxStudioError::LocalDataDirMissing)?
170 .join("Roblox")
171 .join("server.rbxl");
172
173 fs::copy(file_path_source, file_path_target)
174 .map_err(|e| RobloxStudioError::LocalDataDirCopyPlace(e.to_string()))?;
175
176 let server_addr = self.server_addr.to_string();
177 let server_port = self.server_port.to_string();
178 Ok(self
179 .with_arg("-task", RobloxStudioTask::StartServer)
180 .with_arg("-server", server_addr)
181 .with_arg("-port", server_port)
182 .with_zeros())
183 }
184
185 #[allow(clippy::missing_errors_doc)]
193 pub fn start_server_with_clients<P>(
194 self,
195 file_path: P,
196 num_clients: u8,
197 ) -> RobloxStudioResult<Self>
198 where
199 P: AsRef<Path>,
200 {
201 Ok(self
202 .start_server(file_path)?
203 .with_arg("-numtestserverplayersuponstartup", num_clients.to_string()))
204 }
205
206 #[must_use]
212 pub fn start_client(mut self) -> Self {
213 self.task = Some(RobloxStudioTask::StartClient);
214 let server_addr = self.server_addr.to_string();
215 let server_port = self.server_port.to_string();
216 self.with_arg("-task", RobloxStudioTask::StartClient)
217 .with_arg("-server", server_addr)
218 .with_arg("-port", server_port)
219 .with_zeros()
220 }
221
222 #[allow(clippy::zombie_processes)]
234 pub fn run(self) -> RobloxStudioResult<()> {
235 let paths = RobloxStudioPaths::new()?;
236 let exe = paths.exe_for_task(self.task);
237
238 spawn_studio_process(exe, &self.args)?;
239
240 Ok(())
241 }
242}
243
244impl Default for RobloxStudioOpener {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250fn create_studio_command(exe: &Path, args: &[OsString]) -> Command {
251 let mut cmd = Command::new(exe);
252 cmd.args(args);
253 cmd.stdin(Stdio::null());
254 cmd.stdout(Stdio::null());
255 cmd.stderr(Stdio::null());
256 cmd
257}
258
259#[cfg(not(target_os = "windows"))]
260#[allow(clippy::zombie_processes)]
261fn spawn_studio_process(exe: &Path, args: &[OsString]) -> RobloxStudioResult<()> {
262 let mut cmd = create_studio_command(exe, args);
263
264 configure_detached_spawn(&mut cmd)?;
271
272 cmd.spawn()?;
273
274 Ok(())
275}
276
277#[cfg(target_os = "windows")]
278#[allow(clippy::zombie_processes)]
279fn spawn_studio_process(exe: &Path, args: &[OsString]) -> RobloxStudioResult<()> {
280 const DETACHED_PROCESS: u32 = 0x0000_0008;
288 const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
289 const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;
290
291 const WINDOWS_FLAGS_FULL: u32 =
292 DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB;
293 const WINDOWS_FLAGS_FALLBACK: u32 = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
294
295 spawn_studio_process_with_flags(exe, args, WINDOWS_FLAGS_FULL).or_else(|error| match error {
305 RobloxStudioError::Io(io_error) if io_error.kind() == io::ErrorKind::PermissionDenied => {
306 spawn_studio_process_with_flags(exe, args, WINDOWS_FLAGS_FALLBACK)
307 .or_else(|_| spawn_studio_process_with_flags(exe, args, 0))
308 }
309 other => Err(other),
310 })
311}
312
313#[cfg(target_os = "windows")]
314#[allow(clippy::zombie_processes)]
315fn spawn_studio_process_with_flags(
316 exe: &Path,
317 args: &[OsString],
318 flags: u32,
319) -> RobloxStudioResult<()> {
320 let mut cmd = create_studio_command(exe, args);
321
322 configure_detached_spawn(&mut cmd, flags)?;
323
324 cmd.spawn()?;
325
326 Ok(())
327}
328
329#[cfg(target_family = "unix")]
330fn configure_detached_spawn(cmd: &mut Command) -> io::Result<()> {
331 use std::os::unix::process::CommandExt;
332
333 unsafe {
341 cmd.pre_exec(|| {
342 if libc::setsid() == -1 {
343 Err(io::Error::last_os_error())
344 } else {
345 Ok(())
346 }
347 });
348 }
349
350 Ok(())
351}
352
353#[cfg(target_os = "windows")]
354fn configure_detached_spawn(cmd: &mut Command, flags: u32) -> io::Result<()> {
355 use std::os::windows::process::CommandExt;
356
357 if flags != 0 {
358 cmd.creation_flags(flags);
359 }
360
361 Ok(())
362}
363
364#[cfg(not(any(target_family = "unix", target_os = "windows")))]
365fn configure_detached_spawn(_cmd: &mut Command) -> io::Result<()> {
366 Ok(())
367}