Skip to main content

kernel/cli/
cli_executing.rs

1use crate::debug_panic_or_return_log;
2use std::collections::HashMap;
3use std::io;
4use std::io::Write;
5use crate::r_log;
6use crate::networking::announce_shutdown;
7use common::cli_actions::{CliAction, CliActionResponse};
8use common::cli_actions::CliActionResponse::{Ack, FixtureError, FixtureTypeInfo, UnsupportedCommand};
9use common::logging::LogLevel;
10use common::logging::LogLevel::{Info, UserError, UserSuccess, Warning};
11use common::fixture::{ChannelIndex, ChannelValue, ChannelParameter, PropertyType, FixtureType, ColorPropertyType};
12use common::fixture::ChannelError::{ChannelAlreadyInUse, ChannelOutOfRange, UniverseOutOfRange};
13use common::fixture::FixtureError::{ChannelError, DmxStateDesync, FixtureNameAlreadyInUse, FixtureTypeNameAlreadyInUse, InvalidFixture, InvalidFixtureType, InvalidPropertyType, MissingProperty, MultipleColorOutputTypes};
14use common::r_debug_log;
15use crate::fixture;
16
17/// Executes a structured [`CliAction`], returning a tuple containing the resulting [`LogLevel`] and a message string.
18///
19/// # Arguments
20///
21/// * `is_kernel`   - Flag indicating whether the action is executed from the kernel console
22/// * `cli_action`  - Reference to the [`CliAction`] to execute
23pub(super) fn execute_cli_action(is_kernel: bool, cli_action: &CliAction) -> (LogLevel, String) {
24    match cli_action {
25        CliAction::Help => {
26            const HELP_TEXT: &str = include_str!("../../../common/help.txt");
27            (UserSuccess, HELP_TEXT.to_string())
28        },
29
30        CliAction::FixtureNew { name, channels } => {
31            new_fixture_type(name.clone(), channels.clone())
32        },
33
34        CliAction::FixtureAdd { name, fixture_type_name, universe, channel } => {
35            new_fixture(name.clone(), fixture_type_name.clone(), *universe, *channel)
36        },
37        
38        CliAction::FixtureMove {fixture_name, new_universe, new_channel} => {
39            move_fixture(fixture_name.clone(), *new_universe, *new_channel)
40        }
41        
42        CliAction::FixtureRemove { fixture_name } => {
43            remove_fixture(fixture_name.clone())
44        }
45
46        CliAction::FixtureSet {name, property_type, value} => {
47            set_property_value(name.clone(), property_type.clone(), *value)
48        },
49
50        CliAction::FixtureGetType { fixture_name } => {
51            get_fixture_type(fixture_name.clone())
52        },
53
54        CliAction::Exit {save_changes} => {
55            if !is_kernel {
56                (UserError, "Only kernel can exit a session".to_string())
57            } else {
58                shutdown_kernel(save_changes);
59                (Info, "Shutdown aborted".to_string())
60                //If the programm exits the previous methode, the shutdown is aborted.
61            }
62
63        },
64
65        CliAction::OtherCommands {command} => {
66            execute_debug_command(command.clone())
67        },
68
69        //_ => (Error, "Not yet implemented".to_string())
70    }
71}
72
73/// Checks if the given string is a valid debug command and executes it.
74/// Returns a tuple containing the resulting [`LogLevel`] and a message string.
75///
76/// # Arguments
77///
78/// * `line` - The raw debug command string
79fn execute_debug_command(line: String) -> (LogLevel, String) {
80    let mut line_iter = line.split_ascii_whitespace();
81    //We want to check the arg count, we don't want the command counted
82    let arg_count = line_iter.clone().count().saturating_sub(1);
83    match line_iter.next() {
84
85        Some("create_debug") if cfg!(all(debug_assertions, not(test))) => {
86            let fixture_type_name = "rgb".to_string();
87            let universe = 0;
88
89            let mut channels = HashMap::new();
90            channels.insert(
91                PropertyType::Color(ColorPropertyType::Red), ChannelParameter::new(0, universe)
92            );
93            channels.insert(
94                PropertyType::Color(ColorPropertyType::Green), ChannelParameter::new(1, universe)
95            );
96            channels.insert(
97                PropertyType::Color(ColorPropertyType::Blue), ChannelParameter::new(2, universe)
98            );
99
100            let new_command = CliAction::FixtureNew {
101                name: fixture_type_name.clone(),
102                channels,
103            };
104
105            if let (UserError,error) = execute_cli_action(false,&new_command) {
106                return (UserError,error.to_string());
107            }
108            for i in 0..50 {
109                let name = i.to_string();
110                let start_channel = i * 3;
111
112                let add_command = CliAction::FixtureAdd {
113                    name,
114                    fixture_type_name: fixture_type_name.clone(),
115                    universe,
116                    channel: start_channel
117                };
118
119                match execute_cli_action(false, &add_command) {
120                    (UserSuccess, _) => continue,
121                    x => return x
122                }
123            }
124            (UserSuccess,"Created the debug-fixtures".to_string())
125        }
126
127        Some("set_all") if arg_count == 2 && cfg!(all(debug_assertions, not(test))) => {
128            let property_name = line_iter.next().unwrap().to_string();
129            let value = line_iter.next().unwrap().to_string();
130
131            let property_type = match PropertyType::from_str(&*property_name) {
132                Ok(property_type) => property_type,
133                Err(InvalidPropertyType(property_type)) => {
134                    return (UserError,format!("Error: \"{property_type}\" is not a valid PropertyType"))
135                }
136                Err(_) => unreachable!() //All possible Errors have been handles
137            };
138
139            let value = match crate::cli::command_parsing::parse_cli_value(&*value) {
140                Ok(value) => value,
141                Err(e) => return (UserError,e.to_string()),
142            };
143
144            for i in 0..50 {
145                let name = i.to_string();
146                let set_command = CliAction::FixtureSet {
147                    name,
148                    property_type: property_type.clone(),
149                    value,
150                };
151
152                match execute_cli_action(false, &set_command) {
153                    (UserSuccess, _) => continue,
154                    x => return x
155                }
156            }
157
158            (UserSuccess,format!("Set {} to {} in all debug-fixtures", property_type, value))
159        }
160
161        Some("break") if cfg!(all(debug_assertions, not(test))) => {
162            (Info,"Add a breakpoint at this point in the code to check the datastructures".to_string())
163        }
164
165        Some(command) => {
166            (UserError,format!("Unknown command \"{command}\". Please enter help, to get a list of commands."))
167        }
168
169        None => (UserError,"Unknown command. Please enter help, to get a list of commands.".to_string()),
170    }
171}
172
173/// Executes an implicit CLI action sent from a client, returning a structured [`CliActionResponse`].
174///
175/// # Arguments
176///
177/// * `cli_action` - Reference to the [`CliAction`] to process implicitly
178pub(crate) fn execute_implicit_cli_action(cli_action: &CliAction) -> CliActionResponse {
179    match cli_action {
180        CliAction::Help => {
181            r_debug_log!(Warning, "A Client has sent an implicit Help-Command. Please dont let him do that.");
182            UnsupportedCommand
183        }
184
185        CliAction::FixtureNew { name, channels } => {
186            if let Err(e) = FixtureType::new(name.clone(), channels.clone()) {
187                r_debug_log!(Warning, "Implicit FixtureNew threw an (User-)Error: {:?}", e);
188                FixtureError(e)
189            } else {
190                Ack
191            }
192        }
193
194        CliAction::FixtureAdd { name, fixture_type_name, universe, channel } => {
195            if let Err(e) = fixture::new_fixture(name.clone(), fixture_type_name.clone(), *channel, *universe) {
196                r_debug_log!(Warning, "Implicit FixtureAdd threw an (User-)Error: {:?}", e);
197                FixtureError(e)
198            } else {
199                Ack
200            }
201        }
202
203        CliAction::FixtureMove { fixture_name, new_universe, new_channel } => {
204            if let Err(e) = fixture::move_fixture(fixture_name.clone(), *new_channel, *new_universe) {
205                r_debug_log!(Warning, "Implicit FixtureMove threw an (User-)Error: {:?}", e);
206                FixtureError(e)
207            } else {
208                Ack
209            }
210        }
211
212        CliAction::FixtureRemove { fixture_name } => {
213            if let Err(e) = fixture::remove_fixture(fixture_name.clone()) {
214                r_debug_log!(Warning, "Implicit FixtureRemove threw an (User-)Error: {:?}", e);
215                FixtureError(e)
216            } else {
217                Ack
218            }
219        }
220
221        CliAction::FixtureSet {name, property_type, value} => {
222            if let Err(e) = fixture::set_property(name.clone(), property_type.clone(), *value) {
223                r_debug_log!(Warning, "Implicit FixtureSet threw an (User-)Error: {:?}", e);
224                FixtureError(e)
225            } else {
226                Ack
227            }
228        }
229
230        CliAction::FixtureGetType { fixture_name } => {
231            match fixture::get_fixture_type(fixture_name.clone()) {
232                Ok(fixture_type) => FixtureTypeInfo(fixture_type),
233                Err(e) => {
234                    r_debug_log!(Warning, "Implicit FixtureGetType threw an (User-)Error: {:?}", e);
235                    FixtureError(e)
236                },
237            }
238        }
239
240        CliAction::Exit {..} => {
241            r_debug_log!(Warning, "{}", "A client has sent an implicit Exit-Command. Only kernel can exit a session");
242            UnsupportedCommand
243        }
244
245        CliAction::OtherCommands { command } => {
246            r_debug_log!(Warning, "A client has sent an implicit Debug-Command : {}. Please dont him do that.", command);
247            UnsupportedCommand
248        }
249    }
250}
251
252/// Creates a new fixture type definition with the given name and channel mappings.
253///
254/// # Arguments
255///
256/// * `name`     - The name of the new fixture type
257/// * `channels` - A map linking property types to their channel parameters
258fn new_fixture_type(name: String, channels: HashMap<PropertyType, ChannelParameter>) -> (LogLevel, String) {
259    match FixtureType::new(name.clone(), channels) {
260
261        Err(ChannelError(ChannelAlreadyInUse(channel_type))) => {
262            (UserError,format!("Error: The channel {channel_type} overlaps with another channel."))
263        }
264
265        Err(ChannelError(ChannelOutOfRange)) => {
266            (UserError,"Error: A Channel is higher than the size of the Universe. This is not yet supported".into())
267        }
268
269        Err(FixtureTypeNameAlreadyInUse(name)) => {
270            (UserError, format!("Error: The Fixture type name {name} is already used."))
271        }
272
273        Err(InvalidPropertyType(property_type)) => {
274            (UserError,format!("Error: \"{property_type}\" is not a valid PropertyType"))
275        }
276
277        Err(MultipleColorOutputTypes(error_message)) => {
278            (UserError,error_message)
279        }
280
281        Err(_) => {
282            debug_panic_or_return_log!("new_fixture_type() threw an Error it shouldn't")
283        }
284
285        Ok(()) => {
286            (UserSuccess,format!("{} created successfully", name))
287        }
288    }
289}
290
291/// Spawns a new fixture instance based on an existing fixture type at the specified universe and channel.
292///
293/// # Arguments
294///
295/// * `name`              - The name/identifier for the new fixture instance
296/// * `fixture_type_name` - The name of the fixture type to instantiate
297/// * `universe`          - The target universe index
298/// * `channel`           - The starting channel index
299fn new_fixture(name: String, fixture_type_name: String, universe: usize, channel: ChannelIndex,)
300    -> (LogLevel, String) {
301    match fixture::new_fixture(name.clone(), fixture_type_name, channel, universe) {
302
303        Err(FixtureNameAlreadyInUse(name)) => {
304            (UserError,format!("Error: The Fixture name {name} is already used."))
305        }
306
307        Err(InvalidFixtureType(fixture_type_name)) => {
308            (UserError,format!("Error: There is no fixture-type named \"{fixture_type_name}\"."))
309        }
310
311        Err(ChannelError(ChannelAlreadyInUse(overlapping_fixture))) => {
312            (UserError,format!(
313                "Error: At least one Channel of this fixture is overlapping with {}. Fixture has not been created.",
314                overlapping_fixture
315            ))
316        }
317
318        Err(ChannelError(ChannelOutOfRange)) => {
319            (UserError,"Error: fixture overflows out of this remaining universe".to_string())
320        }
321
322        Err(ChannelError(UniverseOutOfRange)) => {
323            debug_panic_or_return_log!(
324                "Fatal Error: Fixture created in Universe that does not exist. Normally, the programm should \
325                automatically create an universe, but somehow, this hasn't happened"
326            )
327        }
328
329        Err(DmxStateDesync) => {
330            debug_panic_or_return_log!(
331                "Fatal Error: Dmx-State has desynced from real Fixture-Positions"
332            )
333        }
334
335        Err(_) => {
336            debug_panic_or_return_log!("new_fixture_type() threw an Error it shouldn't")
337        }
338
339        Ok(_) => {
340            (UserSuccess,format!("{} created successfully", name))
341        }
342    }
343}
344
345/// Relocates an existing fixture instance to a new universe and channel position.
346///
347/// # Arguments
348///
349/// * `fixture_name`  - The name of the fixture to move
350/// * `new_universe`  - The destination universe index
351/// * `new_channel`   - The destination starting channel index
352fn move_fixture(fixture_name: String, new_universe: usize, new_channel: ChannelIndex) -> (LogLevel, String) {
353    match fixture::move_fixture(fixture_name.clone(), new_channel, new_universe) {
354
355        Err(InvalidFixture(fixture_name)) => {
356            (UserError,format!("Error: There is no fixture named \"{fixture_name}\"."))
357        }
358
359        Err(ChannelError(ChannelAlreadyInUse(overlapping_fixture))) => {
360            (UserError,format!(
361                "Error: At least one Channel of this fixture is overlapping with {}. Fixture has not been created.",
362                overlapping_fixture
363            ))
364        }
365
366        Err(ChannelError(ChannelOutOfRange)) => {
367            (UserError,"Error: fixture overflows out of this remaining universe".to_string())
368        }
369
370        Err(ChannelError(UniverseOutOfRange)) => {
371            debug_panic_or_return_log!(
372                "Fatal Error: Fixture created in Universe that does not exist. Normally, the programm should \
373                automatically create an universe, but somehow, this hasn't happened"
374            )
375        }
376
377        Err(DmxStateDesync) => {
378            debug_panic_or_return_log!(
379                "Fatal Error: Dmx-State has desynced from real Fixture-Positions"
380            )
381        }
382
383        Err(_) => {
384            debug_panic_or_return_log!("new_fixture_type() threw an Error it shouldn't")
385        }
386
387        Ok(_) => {
388            (UserSuccess,format!("{} moved successfully", fixture_name))
389        }
390    }
391}
392
393/// Removes an existing fixture instance.
394///
395/// # Arguments
396///
397/// * `fixture_name` - The name of the fixture to remove
398fn remove_fixture(fixture_name: String) -> (LogLevel, String) {
399    match fixture::remove_fixture(fixture_name.clone()) {
400
401        Err(InvalidFixture(fixture_name)) => {
402            (UserError,format!("Error: There is no fixture named \"{fixture_name}\""))
403        }
404
405        Err(ChannelError(UniverseOutOfRange)) => {
406            debug_panic_or_return_log!(
407                "Fatal Error: Fixture {} is in a non-existent Universe", fixture_name
408            )
409        }
410
411        Err(DmxStateDesync) => {
412            debug_panic_or_return_log!(
413                "Fatal Error: Dmx-State has desynced from real Fixture-Positions"
414            )
415        }
416
417        Err(_) => {
418            debug_panic_or_return_log!("new_fixture_type() threw an Error it shouldn't")
419        }
420
421        Ok(_) => {
422            (UserSuccess,format!("{} removed successfully", fixture_name))
423        }
424    }
425}
426
427/// Updates a specific property value on a target fixture instance.
428///
429/// # Arguments
430///
431/// * `fixture_name`  - The name of the fixture to update
432/// * `property_type` - The property type to modify
433/// * `value`         - The new channel value to assign
434fn set_property_value(fixture_name: String, property_type: PropertyType, value: ChannelValue) -> (LogLevel, String) {
435    match fixture::set_property(fixture_name.clone(), property_type.clone(), value) {
436        Err(InvalidFixture(name)) => {
437            (UserError,format!("Error: \"{name}\" is not a valid Fixture"))
438        }
439
440        Err(MissingProperty(_)) => {
441            (UserError,format!("Error: \"{fixture_name}\" has no property \"{property_type}\""))
442        }
443
444        Err(_) => {
445            debug_panic_or_return_log!("new_fixture_type() threw an Error it shouldn't")
446        }
447
448        Ok(_) => {
449            (UserSuccess,format!("Value {property_type} of {fixture_name} changed successfully to {value}"))
450        }
451    }
452}
453
454/// Queries and retrieves the type name associated with a specific fixture instance.
455///
456/// # Arguments
457///
458/// * `fixture_name` - The name of the fixture to query
459fn get_fixture_type(fixture_name: String) -> (LogLevel, String) {
460    match fixture::get_fixture_type(fixture_name.clone()) {
461
462        Err(InvalidFixture(fixture)) => {
463            (UserError,format!("Error: \"{fixture}\" is not a valid Fixture"))
464        }
465
466        Err(_) => {
467            debug_panic_or_return_log!("get_fixture_type_from_string() threw an Error it shouldn't")
468        }
469
470        Ok(fixture_type) => {
471            (Info,format!("\"{fixture_name}\" is a fixture of the type \"{fixture_type}\""))
472        }
473    }
474}
475
476
477/// Handles kernel session shutdown procedures, optionally prompting the user to save changes if unspecified.
478///
479/// # Arguments
480///
481/// * `save_changes` - Optional boolean flag indicating whether to save changes (`Some(true)` / `Some(false)`) or prompt the user (`None`)
482fn shutdown_kernel(save_changes: &Option<bool>) {
483    r_log!(Info,"Shutting down Kernel");
484
485    let (should_exit, save_changes) = match save_changes {
486        Some(save_changes) => (true, *save_changes),
487        None => {
488            let stdout = io::stdout();
489            let mut handle = stdout.lock();
490
491            write!(handle, "\r\x1B[2K").unwrap();
492            write!(handle, "\x1b[33m[System] Do you want to save before exiting?\n\
493            (1) Save and exit (Warning: Not yet implemented. Its impossible to save right now)\n\
494            (2) Discard and exit\n\
495            (0) Cancel\n>\
496            \x1b[0m").unwrap();
497
498            handle.flush().unwrap();
499
500            let mut exit_choice = String::new();
501            io::stdin().read_line(&mut exit_choice).unwrap();
502            match exit_choice.trim() {
503                "1" => (true, true),
504                "2" => (true, false),
505                _ => {
506                    writeln!(handle, "\x1b[32m[System] Exit canceled. Resuming kernel...\x1b[0m").unwrap();
507                    (false, false)
508                }
509            }
510        }
511    };
512
513    if should_exit {
514        if save_changes {
515            r_log!(Warning, "Couldnt save changes ... not yet implemented");
516        } else {
517            r_log!(Warning, "Exiting without saving changes");
518        }
519
520        announce_shutdown();
521
522        // Let in "lock-que" waiting logging-messages get to their turn, and the TCP have his last Fun.
523        std::thread::sleep(std::time::Duration::from_millis(500));
524
525        std::process::exit(0);
526    }
527}
528
529/// Macro that either triggers a panic in debug mode (outside tests) or returns an error log tuple.
530#[macro_export]
531macro_rules! debug_panic_or_return_log {
532    ($($arg:tt)*) => {{
533        #[cfg(all(debug_assertions, not(test)))]
534        panic!("{}", format!($($arg)*));
535
536        #[cfg(not(all(debug_assertions, not(test))))]
537        (LogLevel::Error, format!($($arg)*))
538    }};
539}