rush_sync_server/commands/start/
command.rs1use crate::commands::command::Command;
2use crate::commands::parsing::{parse_bulk_args, BulkMode};
3use crate::core::prelude::*;
4use crate::server::types::{ServerContext, ServerStatus};
5use crate::server::utils::port::is_port_available;
6use crate::server::utils::validation::find_server;
7use opener;
8
9#[derive(Debug, Default)]
10pub struct StartCommand;
11
12impl StartCommand {
13 pub fn new() -> Self {
14 Self
15 }
16}
17
18impl Command for StartCommand {
19 fn name(&self) -> &'static str {
20 "start"
21 }
22 fn description(&self) -> &'static str {
23 "Start server(s) - supports ranges and bulk operations"
24 }
25 fn matches(&self, command: &str) -> bool {
26 command.trim().to_lowercase().starts_with("start")
27 }
28
29 fn execute_sync(&self, args: &[&str]) -> Result<String> {
30 if args.is_empty() {
31 return Err(AppError::Validation(get_translation(
32 "server.error.id_missing",
33 &[],
34 )));
35 }
36
37 let config = get_config()?;
38 let ctx = crate::server::shared::get_shared_context();
39
40 match parse_bulk_args(args) {
41 BulkMode::Single(identifier) => self.start_single_server(&config, ctx, &identifier),
42 BulkMode::Range(start, end) => self.start_range_servers(&config, ctx, start, end),
43 BulkMode::All => self.start_all_servers(&config, ctx),
44 BulkMode::Invalid(error) => Err(AppError::Validation(error)),
45 }
46 }
47
48 fn priority(&self) -> u8 {
49 66
50 }
51}
52
53impl StartCommand {
54 fn start_single_server(
56 &self,
57 config: &Config,
58 ctx: &ServerContext,
59 identifier: &str,
60 ) -> Result<String> {
61 let (server_info, existing_handle) =
62 {
63 let servers_guard = ctx.servers.read().map_err(|_| {
64 AppError::Validation("Server-Context lock poisoned".to_string())
65 })?;
66
67 let server_info = find_server(&servers_guard, identifier)?.clone();
68
69 if server_info.status == ServerStatus::Running {
70 let handles_guard = ctx.handles.read().map_err(|_| {
71 AppError::Validation("Handle-Context lock poisoned".to_string())
72 })?;
73
74 if handles_guard.contains_key(&server_info.id) {
75 return Ok(format!(
76 "Server '{}' is already running on http://{}:{}",
77 server_info.name, config.server.bind_address, server_info.port
78 ));
79 }
80 }
81
82 let handles_guard = ctx.handles.read().map_err(|_| {
83 AppError::Validation("Handle-Context lock poisoned".to_string())
84 })?;
85 let existing_handle = handles_guard.get(&server_info.id).cloned();
86
87 (server_info, existing_handle)
88 };
89
90 if let Some(_handle) = existing_handle {
91 if server_info.status != ServerStatus::Running {
92 self.update_server_status(ctx, &server_info.id, ServerStatus::Running);
93
94 let server_id = server_info.id.clone();
95 tokio::spawn(async move {
96 crate::server::shared::persist_server_update(&server_id, ServerStatus::Running)
97 .await;
98 });
99 }
100
101 return Ok(format!(
102 "Server '{}' is already running on http://{}:{} (status corrected)",
103 server_info.name, config.server.bind_address, server_info.port
104 ));
105 }
106
107 match self.validate_port_safely(&server_info, &config.server.bind_address) {
109 PortValidationResult::Available => {}
110 PortValidationResult::OccupiedByUs => {
111 return Ok(get_translation(
112 "server.error.port_used_by_us",
113 &[&server_info.port.to_string()],
114 ));
115 }
116 PortValidationResult::OccupiedByOther => {
117 return Ok(get_translation(
118 "server.error.port_used_by_other",
119 &[&server_info.port.to_string(), &server_info.name],
120 ));
121 }
122 }
123
124 let running_count = self.count_running_servers(ctx);
126 if running_count >= config.server.max_concurrent {
127 return Err(AppError::Validation(format!(
128 "Cannot start server: Running servers limit reached ({}/{})",
129 running_count, config.server.max_concurrent
130 )));
131 }
132
133 self.actually_start_server(config, ctx, server_info, running_count)
134 }
135
136 fn start_range_servers(
138 &self,
139 config: &Config,
140 ctx: &ServerContext,
141 start: u32,
142 end: u32,
143 ) -> Result<String> {
144 let mut results = Vec::new();
145 let mut started_count = 0;
146 let mut failed_count = 0;
147
148 for i in start..=end {
149 let identifier = format!("{}", i);
150
151 match self.start_single_server(config, ctx, &identifier) {
152 Ok(message) => {
153 if message.contains("successfully started") {
154 started_count += 1;
155 results.push(format!("Server {}: Started", i));
156 } else {
157 results.push(format!("Server {}: {}", i, message));
158 }
159 }
160 Err(e) => {
161 failed_count += 1;
162 results.push(format!("Server {}: Failed - {}", i, e));
163
164 if e.to_string().contains("limit reached") {
166 break;
167 }
168 }
169 }
170 }
171
172 let summary = format!(
173 "Range start completed: {} started, {} failed (Range: {}-{})",
174 started_count, failed_count, start, end
175 );
176
177 if results.is_empty() {
178 Ok(summary)
179 } else {
180 Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
181 }
182 }
183
184 fn start_all_servers(&self, config: &Config, ctx: &ServerContext) -> Result<String> {
186 let stopped_servers: Vec<_> = {
187 let servers = read_lock(&ctx.servers, "servers")?;
188 servers
189 .values()
190 .filter(|s| s.status == ServerStatus::Stopped)
191 .map(|s| (s.id.clone(), s.name.clone()))
192 .collect()
193 };
194
195 if stopped_servers.is_empty() {
196 return Ok("No stopped servers to start".to_string());
197 }
198
199 if stopped_servers.len() > 50 {
200 log::warn!(
201 "Starting {} servers - this may take a while",
202 stopped_servers.len()
203 );
204 }
205
206 let mut results = Vec::new();
207 let mut started_count = 0;
208 let mut failed_count = 0;
209
210 for (server_id, server_name) in stopped_servers {
211 match self.start_single_server(config, ctx, &server_id) {
212 Ok(message) => {
213 if message.contains("successfully started") {
214 started_count += 1;
215 results.push(format!("{}: Started", server_name));
216 } else {
217 results.push(format!("{}: {}", server_name, message));
218 }
219 }
220 Err(e) => {
221 failed_count += 1;
222 results.push(format!("{}: Failed - {}", server_name, e));
223
224 if e.to_string().contains("limit reached") {
225 break;
226 }
227 }
228 }
229 }
230
231 let summary = format!(
232 "Start all completed: {} started, {} failed",
233 started_count, failed_count
234 );
235
236 Ok(format!("{}\n\nResults:\n{}", summary, results.join("\n")))
237 }
238
239 fn actually_start_server(
241 &self,
242 config: &Config,
243 ctx: &ServerContext,
244 server_info: crate::server::types::ServerInfo,
245 current_running_count: usize,
246 ) -> Result<String> {
247 match self.spawn_server(config, ctx, server_info.clone()) {
248 Ok(handle) => {
249 {
250 let mut handles = write_lock(&ctx.handles, "handles")?;
251 handles.insert(server_info.id.clone(), handle);
252 }
253
254 self.update_server_status(ctx, &server_info.id, ServerStatus::Running);
255
256 let server_id = server_info.id.clone();
257 tokio::spawn(async move {
258 crate::server::shared::persist_server_update(&server_id, ServerStatus::Running)
259 .await;
260 });
261
262 let server_url =
263 format!("http://{}:{}", config.server.bind_address, server_info.port);
264
265 if config.server.auto_open_browser {
266 self.spawn_browser_opener(server_url.clone(), server_info.name.clone(), config);
267 }
268
269 Ok(format!(
270 "Server '{}' successfully started on {} [PERSISTENT] ({}/{} running){}",
271 server_info.name,
272 server_url,
273 current_running_count + 1,
274 config.server.max_concurrent,
275 if config.server.auto_open_browser {
276 " - Browser opening..."
277 } else {
278 ""
279 }
280 ))
281 }
282 Err(e) => {
283 self.update_server_status(ctx, &server_info.id, ServerStatus::Failed);
284
285 let server_id = server_info.id.clone();
286 tokio::spawn(async move {
287 crate::server::shared::persist_server_update(&server_id, ServerStatus::Failed)
288 .await;
289 });
290
291 Err(AppError::Validation(format!("Server start failed: {}", e)))
292 }
293 }
294 }
295
296 fn validate_port_safely(
298 &self,
299 server_info: &crate::server::types::ServerInfo,
300 bind_address: &str,
301 ) -> PortValidationResult {
302 if is_port_available(server_info.port, bind_address) {
303 PortValidationResult::Available
304 } else {
305 match crate::server::utils::port::check_port_status(server_info.port, bind_address) {
306 crate::server::utils::port::PortStatus::Available => {
307 PortValidationResult::Available
308 }
309 crate::server::utils::port::PortStatus::OccupiedByUs => {
310 PortValidationResult::OccupiedByUs
311 }
312 crate::server::utils::port::PortStatus::OccupiedByOther => {
313 PortValidationResult::OccupiedByOther
314 }
315 }
316 }
317 }
318
319 fn count_running_servers(&self, ctx: &ServerContext) -> usize {
320 let servers = match ctx.servers.read() {
321 Ok(s) => s,
322 Err(e) => {
323 log::error!("servers lock poisoned: {}", e);
324 return 0;
325 }
326 };
327 servers
328 .values()
329 .filter(|s| s.status == ServerStatus::Running)
330 .count()
331 }
332
333 fn spawn_server(
334 &self,
335 config: &Config,
336 ctx: &ServerContext,
337 server_info: crate::server::types::ServerInfo,
338 ) -> std::result::Result<actix_web::dev::ServerHandle, String> {
339 crate::server::handlers::web::create_web_server(ctx, server_info, config)
340 }
341
342 fn spawn_browser_opener(&self, url: String, name: String, config: &Config) {
343 let delay = config.server.startup_delay_ms.min(2000);
344 tokio::spawn(async move {
345 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
346 if let Err(e) = opener::open(&url) {
347 log::warn!("Failed to open browser for '{}': {}", name, e);
348 }
349 });
350 }
351
352 fn update_server_status(&self, ctx: &ServerContext, server_id: &str, status: ServerStatus) {
353 if let Ok(mut servers) = ctx.servers.write() {
354 if let Some(server) = servers.get_mut(server_id) {
355 server.status = status;
356 }
357 }
358 }
359}
360
361#[derive(Debug)]
362enum PortValidationResult {
363 Available,
364 OccupiedByUs,
365 OccupiedByOther,
366}