surfer/
main.rs

1#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
2
3#[cfg(not(target_arch = "wasm32"))]
4mod main_impl {
5    use camino::Utf8PathBuf;
6    use clap::Parser;
7    use color_eyre::eyre::Context;
8    use color_eyre::Result;
9    use egui::Vec2;
10    use libsurfer::{
11        batch_commands::read_command_file,
12        file_watcher::FileWatcher,
13        logs,
14        message::Message,
15        run_egui,
16        wave_source::{string_to_wavesource, WaveSource},
17        StartupParams, SystemState,
18    };
19    use log::error;
20
21    #[derive(clap::Subcommand)]
22    enum Commands {
23        #[cfg(not(target_arch = "wasm32"))]
24        /// starts surfer in headless mode so that a user can connect to it
25        Server {
26            /// port on which server will listen
27            #[clap(long)]
28            port: Option<u16>,
29            /// token used by the client to authenticate to the server
30            #[clap(long)]
31            token: Option<String>,
32            /// waveform file that we want to serve
33            #[arg(long)]
34            file: String,
35        },
36    }
37
38    #[derive(clap::Parser, Default)]
39    #[command(version, about)]
40    struct Args {
41        /// Waveform file in VCD, FST, or GHW format.
42        wave_file: Option<String>,
43        #[cfg(feature = "spade")]
44        #[clap(long)]
45        /// Load Spade state file
46        spade_state: Option<Utf8PathBuf>,
47        #[cfg(feature = "spade")]
48        #[clap(long)]
49        /// Specify Spade top-level entity
50        spade_top: Option<String>,
51        /// Path to a file containing 'commands' to run after a waveform has been loaded.
52        /// The commands are the same as those used in the command line interface inside the program.
53        /// Commands are separated by lines or ;. Empty lines are ignored. Line comments starting with
54        /// `#` are supported
55        /// NOTE: This feature is not permanent, it will be removed once a solid scripting system
56        /// is implemented.
57        #[clap(long, short, verbatim_doc_comment)]
58        command_file: Option<Utf8PathBuf>,
59        /// Alias for --command_file to support VUnit
60        #[clap(long)]
61        script: Option<Utf8PathBuf>,
62
63        #[clap(long, short)]
64        /// Load previously saved state file
65        state_file: Option<Utf8PathBuf>,
66
67        #[clap(long, action)]
68        /// Port for WCP to connect to
69        wcp_initiate: Option<u16>,
70
71        #[command(subcommand)]
72        command: Option<Commands>,
73    }
74
75    impl Args {
76        pub fn command_file(&self) -> &Option<Utf8PathBuf> {
77            if self.script.is_some() && self.command_file.is_some() {
78                error!("At most one of --command_file and --script can be used");
79                return &None;
80            }
81            if self.command_file.is_some() {
82                &self.command_file
83            } else {
84                &self.script
85            }
86        }
87    }
88
89    #[allow(dead_code)] // NOTE: Only used in desktop version
90    fn startup_params_from_args(args: Args) -> StartupParams {
91        let startup_commands = if let Some(cmd_file) = args.command_file() {
92            read_command_file(cmd_file)
93        } else {
94            vec![]
95        };
96        StartupParams {
97            #[cfg(feature = "spade")]
98            spade_state: args.spade_state,
99            #[cfg(feature = "spade")]
100            spade_top: args.spade_top,
101            #[cfg(not(feature = "spade"))]
102            spade_state: None,
103            #[cfg(not(feature = "spade"))]
104            spade_top: None,
105            waves: args.wave_file.map(|s| string_to_wavesource(&s)),
106            wcp_initiate: args.wcp_initiate,
107            startup_commands,
108        }
109    }
110
111    #[cfg(not(target_arch = "wasm32"))]
112    pub(crate) fn main() -> Result<()> {
113        use libsurfer::{
114            state::UserState, translation::wasm_translator::discover_wasm_translators,
115        };
116
117        logs::start_logging()?;
118
119        // https://tokio.rs/tokio/topics/bridging
120        // We want to run the gui in the main thread, but some long running tasks like
121        // loading VCDs should be done asynchronously. We can't just use std::thread to
122        // do that due to wasm support, so we'll start a tokio runtime
123        let runtime = tokio::runtime::Builder::new_current_thread()
124            .worker_threads(1)
125            .enable_all()
126            .build()
127            .unwrap();
128
129        // parse arguments
130        let args = Args::parse();
131        #[cfg(not(target_arch = "wasm32"))]
132        if let Some(Commands::Server { port, token, file }) = args.command {
133            let default_port = 8911; // FIXME: make this more configurable
134            let res = runtime.block_on(surver::server_main(
135                port.unwrap_or(default_port),
136                token,
137                file,
138                None,
139            ));
140            return res;
141        }
142
143        let _enter = runtime.enter();
144
145        std::thread::spawn(move || {
146            runtime.block_on(async {
147                loop {
148                    tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
149                }
150            });
151        });
152
153        let state_file = args.state_file.clone();
154        let startup_params = startup_params_from_args(args);
155        let waves = startup_params.waves.clone();
156
157        let state = match &state_file {
158            Some(file) => std::fs::read_to_string(file)
159                .with_context(|| format!("Failed to read state from {file}"))
160                .and_then(|content| {
161                    ron::from_str::<UserState>(&content)
162                        .with_context(|| format!("Failed to decode state from {file}"))
163                })
164                .map(SystemState::from)
165                .map(|mut s| {
166                    s.user.state_file = Some(file.into());
167                    s
168                })
169                .or_else(|e| {
170                    error!("Failed to read state file. Opening fresh session\n{e:#?}");
171                    SystemState::new()
172                })?,
173            None => SystemState::new()?,
174        }
175        .with_params(startup_params);
176
177        // Not using batch commands here as we want to start processing wasm plugins
178        // as soon as we start up, no need to wait for the waveform to load
179        let sender = state.channels.msg_sender.clone();
180        for message in discover_wasm_translators() {
181            sender.send(message).unwrap();
182        }
183
184        // install a file watcher that emits a `SuggestReloadWaveform` message
185        // whenever the user-provided file changes.
186        let _watcher = match waves {
187            Some(WaveSource::File(path)) => {
188                let sender = state.channels.msg_sender.clone();
189                FileWatcher::new(&path, move || {
190                    match sender.send(Message::SuggestReloadWaveform) {
191                        Ok(_) => {}
192                        Err(err) => {
193                            error!("Message ReloadWaveform did not send:\n{err}")
194                        }
195                    }
196                })
197                .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
198                .ok()
199            }
200            _ => None,
201        };
202
203        let options = eframe::NativeOptions {
204            viewport: egui::ViewportBuilder::default()
205                .with_app_id("org.surfer-project.surfer")
206                .with_title("Surfer")
207                .with_inner_size(Vec2::new(
208                    state.user.config.layout.window_width as f32,
209                    state.user.config.layout.window_height as f32,
210                )),
211            ..Default::default()
212        };
213
214        eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
215
216        Ok(())
217    }
218}
219
220#[cfg(target_arch = "wasm32")]
221mod main_impl {
222    use eframe::wasm_bindgen::JsCast;
223    use eframe::web_sys;
224    use libsurfer::wasm_api::WebHandle;
225
226    // Calling main is not the intended way to start surfer, instead, it should be
227    // started by `wasm_api::WebHandle`
228    pub(crate) fn main() -> color_eyre::Result<()> {
229        let document = web_sys::window()
230            .expect("No window")
231            .document()
232            .expect("No document");
233        let canvas = document
234            .get_element_by_id("the_canvas_id")
235            .expect("Failed to find the_canvas_id")
236            .dyn_into::<web_sys::HtmlCanvasElement>()
237            .expect("the_canvas_id was not a HtmlCanvasElement");
238
239        wasm_bindgen_futures::spawn_local(async {
240            let wh = WebHandle::new();
241            wh.start(canvas).await.expect("Failed to start surfer");
242        });
243
244        Ok(())
245    }
246}
247
248fn main() -> color_eyre::Result<()> {
249    main_impl::main()
250}