netsblox_vm/
cli.rs

1//! Access to the standard `netsblox-vm` CLI.
2//!
3//! Access to this submodule requires the `cli` feature flag, which is enabled by default.
4//!
5//! This submodule acts as a full-fledged implementation of a usable `netsblox-vm` CLI front-end.
6//! This includes being able to compile and run individual project files locally,
7//! as well as a server mode where a user can connect to the server from the browser
8//! and use the block-based interface to write, upload, and run code on the server.
9//! Note that server mode does not yet support multiple simultaneous.
10
11use alloc::rc::Rc;
12use alloc::vec::Vec;
13use alloc::boxed::Box;
14use alloc::string::String;
15use alloc::collections::VecDeque;
16
17use core::time::Duration;
18use core::cell::{Cell, RefCell};
19use core::{mem, fmt};
20
21use std::fs::File;
22use std::io::{self, Read, Write as IoWrite, stdout};
23use std::sync::{Arc, Mutex};
24use std::sync::mpsc::{channel, Sender, TryRecvError};
25use std::sync::atomic::{AtomicBool, Ordering as MemoryOrder};
26use std::thread;
27
28use clap::Subcommand;
29use actix_web::{get, post, web, App, HttpServer, Responder, HttpResponse};
30use actix_cors::Cors;
31
32use crossterm::{cursor, execute, queue};
33use crossterm::tty::IsTty;
34use crossterm::event::{self, Event, KeyCode as RawKeyCode, KeyModifiers as RawKeyModifiers};
35use crossterm::terminal::{self, ClearType};
36use crossterm::style::{ResetColor, SetForegroundColor, Color, Print};
37
38use crate::*;
39use crate::gc::*;
40use crate::json::*;
41use crate::real_time::*;
42use crate::std_system::*;
43use crate::std_util::*;
44use crate::bytecode::*;
45use crate::runtime::*;
46use crate::process::*;
47use crate::project::*;
48use crate::template::*;
49use crate::compact_str::*;
50
51const DEFAULT_BASE_URL: &str = "https://cloud.netsblox.org";
52const DEFAULT_EDITOR_URL: &str = "https://editor.netsblox.org";
53
54const STEPS_PER_IO_ITER: usize = 64;
55const MAX_REQUEST_SIZE_BYTES: usize = 1024 * 1024 * 1024;
56const YIELDS_BEFORE_IDLE_SLEEP: usize = 256;
57const IDLE_SLEEP_TIME: Duration = Duration::from_micros(500);
58const CLOCK_INTERVAL: Duration = Duration::from_millis(10);
59const COLLECT_INTERVAL: Duration = Duration::from_secs(60);
60
61macro_rules! crash {
62    ($ret:literal : $($tt:tt)*) => {{
63        eprint!($($tt)*);
64        eprint!("\r\n");
65        std::process::exit($ret);
66    }}
67}
68
69struct AtExit<F: FnOnce()>(Option<F>);
70impl<F: FnOnce()> AtExit<F> {
71    fn new(f: F) -> Self { Self(Some(f)) }
72}
73impl<F: FnOnce()> Drop for AtExit<F> {
74    fn drop(&mut self) {
75        self.0.take().unwrap()()
76    }
77}
78
79#[derive(Collect)]
80#[collect(no_drop, bound = "")]
81struct Env<'gc, C: CustomTypes<StdSystem<C>>> {
82                               proj: Gc<'gc, RefLock<Project<'gc, C, StdSystem<C>>>>,
83    #[collect(require_static)] locs: Locations,
84}
85type EnvArena<S> = Arena<Rootable![Env<'_, S>]>;
86
87fn get_env<C: CustomTypes<StdSystem<C>>>(role: &ast::Role, system: Rc<StdSystem<C>>) -> Result<EnvArena<C>, FromAstError> {
88    let (bytecode, init_info, locs, _) = ByteCode::compile(role)?;
89    Ok(EnvArena::new(|mc| {
90        let proj = Project::from_init(mc, &init_info, Rc::new(bytecode), Settings::default(), system);
91        Env { proj: Gc::new(mc, RefLock::new(proj)), locs }
92    }))
93}
94
95/// Standard NetsBlox VM project actions that can be performed
96#[derive(Subcommand)]
97pub enum Mode {
98    /// Compiles and runs a single project file
99    Run {
100        /// Path to the (xml) project file
101        src: CompactString,
102        /// The specific role to run, or none if not ambiguous
103        #[clap(long)]
104        role: Option<CompactString>,
105
106        /// Address of the NetsBlox server
107        #[clap(long, default_value_t = CompactString::from(DEFAULT_BASE_URL))]
108        server: CompactString,
109    },
110    /// Compiles a single project file and dumps its disassembly to stdout
111    Dump {
112        /// Path to the (xml) project file
113        src: CompactString,
114        /// The specific role to compile, or none if not ambiguous
115        #[clap(long)]
116        role: Option<CompactString>,
117    },
118    /// Starts an execution server which you can connect to from the browser
119    Start {
120        /// Address of the NetsBlox server
121        #[clap(long, default_value_t = CompactString::from(DEFAULT_BASE_URL))]
122        server: CompactString,
123
124        /// The address of this machine, which others use to send HTTP requests
125        #[clap(long, default_value_t = CompactString::from("127.0.0.1"))]
126        addr: CompactString,
127        /// The port to bind for the web server
128        #[clap(long, default_value_t = 6286)]
129        port: u16,
130    },
131}
132
133#[derive(Debug)]
134enum OpenProjectError<'a> {
135    ParseError { error: Box<ast::Error> },
136    RoleNotFound { role: &'a str },
137    NoRoles,
138    MultipleRoles { count: usize },
139}
140impl fmt::Display for OpenProjectError<'_> {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            OpenProjectError::ParseError { error } => write!(f, "failed to parse project: {error:?}"),
144            OpenProjectError::RoleNotFound { role } => write!(f, "no role named '{role}'"),
145            OpenProjectError::NoRoles => write!(f, "project had no roles"),
146            OpenProjectError::MultipleRoles { count } => write!(f, "project had multiple ({count}) roles, but a specific role was not specified"),
147        }
148    }
149}
150
151fn read_file(src: &str) -> io::Result<String> {
152    let mut file = File::open(src)?;
153    let mut s = String::new();
154    file.read_to_string(&mut s)?;
155    Ok(s)
156}
157fn open_project<'a>(content: &str, role: Option<&'a str>) -> Result<(CompactString, ast::Role), OpenProjectError<'a>> {
158    let parsed = match ast::Parser::default().parse(content) {
159        Ok(x) => x,
160        Err(error) => return Err(OpenProjectError::ParseError { error }),
161    };
162    let role = match role {
163        Some(role) => match parsed.roles.into_iter().find(|x| x.name == role) {
164            Some(x) => x,
165            None => return Err(OpenProjectError::RoleNotFound { role }),
166        }
167        None => match parsed.roles.len() {
168            0 => return Err(OpenProjectError::NoRoles),
169            1 => parsed.roles.into_iter().next().unwrap(),
170            count => return Err(OpenProjectError::MultipleRoles { count }),
171        }
172    };
173    Ok((parsed.name, role))
174}
175
176fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: CompactString, role: &ast::Role, overrides: Config<C, StdSystem<C>>, clock: Arc<Clock>) {
177    terminal::enable_raw_mode().unwrap();
178    execute!(stdout(), cursor::Hide).unwrap();
179    let _tty_mode_guard = AtExit::new(|| {
180        terminal::disable_raw_mode().unwrap();
181        execute!(stdout(), cursor::Show).unwrap()
182    });
183
184    let old_panic_hook = std::panic::take_hook();
185    std::panic::set_hook(Box::new(move |ctx| {
186        let _ = terminal::disable_raw_mode();
187        old_panic_hook(ctx);
188    }));
189
190    let update_flag = Rc::new(Cell::new(false));
191    let input_queries = Rc::new(RefCell::new(VecDeque::new()));
192    let mut term_size = terminal::size().unwrap();
193    let mut input_value = CompactString::default();
194
195    let config = overrides.fallback(&Config {
196        command: {
197            let update_flag = update_flag.clone();
198            Some(Rc::new(move |_, key, command, proc| match command {
199                Command::Print { style: _, value } => {
200                    let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
201                    if let Some(value) = value {
202                        print!("{entity:?} > {value:?}\r\n");
203                        update_flag.set(true);
204                    }
205                    key.complete(Ok(()));
206                    CommandStatus::Handled
207                }
208                _ => CommandStatus::UseDefault { key, command },
209            }))
210        },
211        request: {
212            let update_flag = update_flag.clone();
213            let input_queries = input_queries.clone();
214            Some(Rc::new(move |_, key, request, proc| match request {
215                Request::Input { prompt } => {
216                    let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
217                    input_queries.borrow_mut().push_back((format!("{entity:?} {prompt:?} > "), key));
218                    update_flag.set(true);
219                    RequestStatus::Handled
220                }
221                _ => RequestStatus::UseDefault { key, request },
222            }))
223        },
224    });
225
226    let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone()));
227    let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
228    print!("public id: {}\r\n", system.get_public_id());
229
230    let mut env = match get_env(role, system) {
231        Ok(x) => x,
232        Err(e) => {
233            print!("error loading project: {e:?}\r\n");
234            return;
235        }
236    };
237    env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start));
238
239    let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
240    let mut input_sequence = Vec::with_capacity(16);
241    let in_input_mode = || !input_queries.borrow().is_empty();
242    'program: loop {
243        debug_assert_eq!(input_sequence.len(), 0);
244        while event::poll(Duration::from_secs(0)).unwrap() {
245            match event::read().unwrap() {
246                Event::Key(key) => match key.code {
247                    RawKeyCode::Char('c') if key.modifiers == RawKeyModifiers::CONTROL => break 'program,
248                    RawKeyCode::Esc => input_sequence.push(Input::Stop),
249                    RawKeyCode::Char(ch) => match in_input_mode() {
250                        true => { input_value.push(ch); update_flag.set(true); }
251                        false => input_sequence.push(Input::KeyDown { key: KeyCode::Char(ch.to_ascii_lowercase()) }),
252                    }
253                    RawKeyCode::Backspace => if in_input_mode() && input_value.pop().is_some() { update_flag.set(true) }
254                    RawKeyCode::Enter => if let Some((_, res_key)) = input_queries.borrow_mut().pop_front() {
255                        res_key.complete(Ok(SimpleValue::Text(mem::take(&mut input_value)).into()));
256                        update_flag.set(true);
257                    }
258                    RawKeyCode::Up => if !in_input_mode() { input_sequence.push(Input::KeyDown { key: KeyCode::Up }) }
259                    RawKeyCode::Down => if !in_input_mode() { input_sequence.push(Input::KeyDown { key: KeyCode::Down }) }
260                    RawKeyCode::Left => if !in_input_mode() { input_sequence.push(Input::KeyDown { key: KeyCode::Left }) }
261                    RawKeyCode::Right => if !in_input_mode() { input_sequence.push(Input::KeyDown { key: KeyCode::Right }) }
262                    _ => (),
263                }
264                Event::Resize(c, r) => {
265                    term_size = (c, r);
266                    update_flag.set(true);
267                }
268                _ => (),
269            }
270        }
271
272        env.mutate(|mc, env| {
273            let mut proj = env.proj.borrow_mut(mc);
274            for input in input_sequence.drain(..) { proj.input(mc, input); }
275            for _ in 0..STEPS_PER_IO_ITER {
276                let res = proj.step(mc);
277                if let ProjectStep::Error { error, proc: _ } = &res {
278                    print!("\r\n>>> runtime error: {:?}\r\n\r\n", error.cause);
279                }
280                idle_sleeper.consume(&res);
281            }
282        });
283        if clock.read(Precision::Low) > next_collect {
284            env.collect_all();
285            next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
286        }
287
288        if update_flag.get() {
289            update_flag.set(false);
290
291            queue!(stdout(),
292                cursor::SavePosition,
293                cursor::MoveTo(0, term_size.1 - 1),
294                terminal::Clear(ClearType::CurrentLine)).unwrap();
295            let queries = input_queries.borrow();
296            if let Some((query, _)) = queries.front() {
297                queue!(stdout(),
298                    SetForegroundColor(Color::Blue),
299                    Print(query),
300                    ResetColor,
301                    Print(&input_value)).unwrap();
302            }
303            queue!(stdout(), cursor::RestorePosition).unwrap();
304            stdout().flush().unwrap();
305        }
306    }
307
308    execute!(stdout(), terminal::Clear(ClearType::CurrentLine)).unwrap();
309}
310fn run_proj_non_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: CompactString, role: &ast::Role, overrides: Config<C, StdSystem<C>>, clock: Arc<Clock>) {
311    let config = overrides.fallback(&Config {
312        request: None,
313        command: Some(Rc::new(move |_, key, command, proc| match command {
314            Command::Print { style: _, value } => {
315                let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
316                if let Some(value) = value { println!("{entity:?} > {value:?}") }
317                key.complete(Ok(()));
318                CommandStatus::Handled
319            }
320            _ => CommandStatus::UseDefault { key, command },
321        })),
322    });
323
324    let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone()));
325    let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
326    println!(">>> public id: {}\n", system.get_public_id());
327
328    let mut env = match get_env(role, system) {
329        Ok(x) => x,
330        Err(e) => {
331            println!(">>> error loading project: {e:?}");
332            return;
333        }
334    };
335    env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start));
336
337    let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
338    loop {
339        env.mutate(|mc, env| {
340            let mut proj = env.proj.borrow_mut(mc);
341            for _ in 0..STEPS_PER_IO_ITER {
342                let res = proj.step(mc);
343                if let ProjectStep::Error { error, proc: _ } = &res {
344                    println!("\n>>> runtime error: {:?}\n", error.cause);
345                }
346                idle_sleeper.consume(&res);
347            }
348        });
349        if clock.read(Precision::Low) > next_collect {
350            env.collect_all();
351            next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
352        }
353    }
354}
355fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: CompactString, addr: CompactString, port: u16, overrides: Config<C, StdSystem<C>>, clock: Arc<Clock>, syscalls: &[SyscallMenu]) {
356    println!(r#"connect from {DEFAULT_EDITOR_URL}/?extensions=["http://{addr}:{port}/extension.js"]"#);
357
358    let extension = ExtensionArgs {
359        server: &format!("http://{addr}:{port}"),
360        syscalls,
361        omitted_elements: &["thumbnail", "pentrails", "history", "replay"],
362        pull_interval: Duration::from_millis(250),
363    }.render();
364
365    enum ServerCommand {
366        SetProject(String),
367        Input(Input),
368    }
369
370    let (proj_sender, proj_receiver) = channel();
371
372    struct State {
373        extension: String,
374        running: AtomicBool,
375        current_proj: Mutex<String>,
376        proj_sender: Mutex<Sender<ServerCommand>>,
377        output: Mutex<String>,
378        errors: Mutex<Vec<ErrorSummary>>,
379    }
380    let state = web::Data::new(State {
381        extension,
382        running: AtomicBool::new(true),
383        current_proj: Mutex::new(EMPTY_PROJECT.into()),
384        proj_sender: Mutex::new(proj_sender),
385        output: Mutex::new(String::with_capacity(1024)),
386        errors: Mutex::new(Vec::with_capacity(8)),
387    });
388
389    macro_rules! tee_println {
390        ($state:expr => $($t:tt)*) => {{
391            let content = format!($($t)*);
392            if let Some(state) = $state {
393                let mut output = state.output.lock().unwrap();
394                output.push_str(&content);
395                output.push('\n');
396            }
397            println!("{content}");
398        }}
399    }
400
401    let weak_state = Arc::downgrade(&state);
402    let config = overrides.fallback(&Config {
403        request: None,
404        command: Some(Rc::new(move |_, key, command, proc| match command {
405            Command::Print { style: _, value } => {
406                let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
407                if let Some(value) = value { tee_println!(weak_state.upgrade() => "{entity:?} > {value:?}") }
408                key.complete(Ok(()));
409                CommandStatus::Handled
410            }
411            _ => CommandStatus::UseDefault { key, command },
412        })),
413    });
414    let system = Rc::new(StdSystem::new_sync(nb_server, Some("native-server"), config, clock.clone()));
415    let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
416    println!("public id: {}", system.get_public_id());
417
418    #[tokio::main(flavor = "multi_thread", worker_threads = 1)]
419    async fn run_http(state: web::Data<State>, port: u16) {
420        #[get("/extension.js")]
421        async fn get_extension(state: web::Data<State>) -> impl Responder {
422            HttpResponse::Ok().content_type("text/javascript").body(state.extension.clone())
423        }
424
425        #[post("/pull")]
426        async fn pull_status(state: web::Data<State>) -> impl Responder {
427            let running = state.running.load(MemoryOrder::Relaxed);
428            let output = mem::take(&mut *state.output.lock().unwrap());
429            let errors = mem::take(&mut *state.errors.lock().unwrap());
430
431            HttpResponse::Ok().content_type("application/json").body(serde_json::to_string(&Status { running, output, errors }).unwrap())
432        }
433
434        #[post("/project")]
435        async fn set_project(state: web::Data<State>, body: web::Bytes) -> impl Responder {
436            match String::from_utf8(body.to_vec()) {
437                Ok(content) => {
438                    state.proj_sender.lock().unwrap().send(ServerCommand::SetProject(content)).unwrap();
439                    HttpResponse::Ok().content_type("text/plain").body("loaded project")
440                }
441                Err(_) => HttpResponse::BadRequest().content_type("text/plain").body("project was not valid utf8"),
442            }
443        }
444
445        #[get("/project")]
446        async fn get_project(state: web::Data<State>) -> impl Responder {
447            let proj = state.current_proj.lock().unwrap().clone();
448            HttpResponse::Ok().content_type("text/xml").append_header(("Content-Disposition", "attachment; filename=\"project.xml\"")).body(proj)
449        }
450
451        #[post("/input")]
452        async fn send_input(state: web::Data<State>, input: web::Bytes) -> impl Responder {
453            let input = match String::from_utf8(input.to_vec()) {
454                Ok(input) => match input.as_str() {
455                    "start" => Input::Start,
456                    "stop" => Input::Stop,
457                    _ => return HttpResponse::BadRequest().content_type("text/plain").body(format!("unknown input: {input:?}")),
458                }
459                Err(_) => return HttpResponse::BadRequest().content_type("text/plain").body("input was not valid utf8")
460            };
461            state.proj_sender.lock().unwrap().send(ServerCommand::Input(input)).unwrap();
462            HttpResponse::Ok().content_type("text/plain").body("sent input")
463        }
464
465        #[post("/toggle-paused")]
466        async fn toggle_paused(state: web::Data<State>) -> impl Responder {
467            state.running.fetch_xor(true, MemoryOrder::Relaxed);
468            HttpResponse::Ok().content_type("text/plain").body("toggled pause state")
469        }
470
471        HttpServer::new(move || {
472            App::new()
473                .wrap(Cors::permissive())
474                .app_data(web::PayloadConfig::new(MAX_REQUEST_SIZE_BYTES))
475                .app_data(state.clone())
476                .service(get_extension)
477                .service(pull_status)
478                .service(set_project)
479                .service(get_project)
480                .service(send_input)
481                .service(toggle_paused)
482        })
483        .workers(1)
484        .bind(("localhost", port)).unwrap().run().await.unwrap();
485    }
486    let weak_state = Arc::downgrade(&state);
487    thread::spawn(move || run_http(state, port));
488
489    let (_, empty_role) = open_project(EMPTY_PROJECT, None).unwrap_or_else(|_| crash!(666: "default project failed to load"));
490    let mut env = get_env(&empty_role, system.clone()).unwrap();
491
492    let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
493    'program: loop {
494        'input: loop {
495            match proj_receiver.try_recv() {
496                Ok(command) => match command {
497                    ServerCommand::SetProject(content) => match open_project(&content, None) {
498                        Ok((proj_name, role)) => {
499                            let mut state = weak_state.upgrade().unwrap();
500                            tee_println!(Some(&mut state) => "\n>>> loaded project '{proj_name}'\n");
501                            match get_env(&role, system.clone()) {
502                                Ok(x) => {
503                                    env = x;
504                                    *state.current_proj.lock().unwrap() = content;
505                                }
506                                Err(e) => tee_println!(Some(&mut state) => "\n>>> project load error: {e:?}\n>>> keeping previous project...\n"),
507                            }
508                        }
509                        Err(e) => match e {
510                            OpenProjectError::ParseError { error } if error.location.collab_id.is_some() => {
511                                let mut state = weak_state.upgrade().unwrap();
512                                let cause = format_compact!("{:?}", error.kind);
513                                state.errors.lock().unwrap().push(ErrorSummary {
514                                    cause: cause.clone(),
515                                    entity: error.location.entity.unwrap_or_default(),
516                                    globals: vec![],
517                                    fields: vec![],
518                                    trace: vec![TraceEntry { location: error.location.collab_id.unwrap(), locals: vec![] }], // unwrap safe because of branch guard condition
519                                });
520                                tee_println!(Some(&mut state) => "\n>>> project load error: {cause:?}\n>>> see red error comments...\n>>> keeping previous project...\n");
521                            }
522                            _ => tee_println!(weak_state.upgrade() => "\n>>> project load error: {e:?}\n>>> keeping previous project...\n"),
523                        }
524                    }
525                    ServerCommand::Input(input) => {
526                        if let Input::Start = &input {
527                            if let Some(state) = weak_state.upgrade() {
528                                state.running.store(true, MemoryOrder::Relaxed);
529                            }
530                        }
531                        env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, input));
532                    }
533                }
534                Err(TryRecvError::Disconnected) => break 'program,
535                Err(TryRecvError::Empty) => break 'input,
536            }
537        }
538        if !weak_state.upgrade().map(|state| state.running.load(MemoryOrder::Relaxed)).unwrap_or(true) {
539            idle_sleeper.trigger();
540            continue;
541        }
542
543        env.mutate(|mc, env| {
544            let mut proj = env.proj.borrow_mut(mc);
545            for _ in 0..STEPS_PER_IO_ITER {
546                let res = proj.step(mc);
547                match &res {
548                    ProjectStep::Error { error, proc } => if let Some(state) = weak_state.upgrade() {
549                        let summary = ErrorSummary::extract(error, proc, &env.locs);
550
551                        tee_println!(Some(&state) => "\n>>> runtime error in entity {:?}: {:?}\n>>> see red error comments...\n", summary.entity, summary.cause);
552
553                        state.errors.lock().unwrap().push(summary);
554                    }
555                    ProjectStep::Pause => if let Some(state) = weak_state.upgrade() {
556                        state.running.store(false, MemoryOrder::Relaxed);
557                        break
558                    }
559                    _ => (),
560                }
561                idle_sleeper.consume(&res);
562            }
563        });
564        if clock.read(Precision::Low) > next_collect {
565            env.collect_all();
566            next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
567        }
568    }
569}
570
571/// Runs a CLI client using the given [`Mode`] configuration.
572pub fn run<C: CustomTypes<StdSystem<C>>>(mode: Mode, config: Config<C, StdSystem<C>>, syscalls: &[SyscallMenu]) {
573    let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
574    let clock = Arc::new(Clock::new(utc_offset, Some(Precision::Medium)));
575    let clock_clone = clock.clone();
576    thread::spawn(move || loop {
577        thread::sleep(CLOCK_INTERVAL);
578        clock_clone.update();
579    });
580
581    match mode {
582        Mode::Run { src, role, server } => {
583            let content = read_file(&src).unwrap_or_else(|_| crash!(1: "failed to read file '{src}'"));
584            let (project_name, role) = open_project(&content, role.as_deref()).unwrap_or_else(|e| crash!(2: "{e}"));
585
586            if stdout().is_tty() {
587                run_proj_tty(&project_name, server, &role, config, clock);
588            } else {
589                run_proj_non_tty(&project_name, server, &role, config, clock);
590            }
591        }
592        Mode::Dump { src, role } => {
593            let content = read_file(&src).unwrap_or_else(|_| crash!(1: "failed to read file '{src}'"));
594            let (_, role) = open_project(&content, role.as_deref()).unwrap_or_else(|e| crash!(2: "{e}"));
595
596            let (bytecode, _, _, _) = ByteCode::compile(&role).unwrap();
597            println!("instructions:");
598            bytecode.dump_code(&mut std::io::stdout().lock()).unwrap();
599            println!("\ndata:");
600            bytecode.dump_data(&mut std::io::stdout().lock()).unwrap();
601            println!("\ntotal size: {}", bytecode.total_size());
602        }
603        Mode::Start { server, addr, port } => {
604            run_server(server, addr, port, config, clock, syscalls);
605        }
606    }
607}