Skip to main content

surfer/
main.rs

1#![cfg_attr(not(target_arch = "wasm32"), deny(unused_crate_dependencies))]
2#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
4#[cfg(not(target_arch = "wasm32"))]
5mod main_impl {
6    use camino::Utf8PathBuf;
7    use clap::Parser;
8    use emath::Vec2;
9    use eyre::Result;
10    use eyre::WrapErr as _;
11    use libsurfer::{
12        EGUI_CONTEXT, StartupParams, SystemState,
13        batch_commands::read_command_file,
14        file_watcher::FileWatcher,
15        logs,
16        message::Message,
17        run_egui,
18        wave_source::{WaveSource, string_to_wavesource},
19    };
20    use tracing::error;
21
22    #[derive(clap::Subcommand)]
23    enum Commands {
24        #[cfg(not(target_arch = "wasm32"))]
25        /// starts surfer in headless mode so that a user can connect to it
26        Server {
27            /// port on which server will listen
28            #[clap(long)]
29            port: Option<u16>,
30            /// IP address to bind the server to
31            #[clap(long)]
32            bind_address: Option<String>,
33            /// token used by the client to authenticate to the server
34            #[clap(long)]
35            token: Option<String>,
36            /// waveform file that we want to serve
37            #[arg(long)]
38            file: String,
39        },
40    }
41
42    #[derive(clap::Parser, Default)]
43    #[command(version = concat!(env!("CARGO_PKG_VERSION"), " (git: ", env!("VERGEN_GIT_DESCRIBE"), ")"), about)]
44    struct Args {
45        /// Waveform file in VCD, FST, or GHW format.
46        wave_file: Option<String>,
47        /// Path to a file containing SUCL commands to run after a waveform has been loaded.
48        /// The commands are the same as those used in the command line interface inside the program.
49        /// Commands are separated by lines or ;. Empty lines are ignored. Line comments starting with
50        /// `#` are supported
51        /// NOTE: This feature is not permanent, it will be removed once a solid scripting system
52        /// is implemented.
53        #[clap(long, short, verbatim_doc_comment)]
54        command_file: Option<Utf8PathBuf>,
55        /// Alias for --`command_file` to let `VUnit` use the same argument for both Surfer and GTKWave.
56        #[clap(long)]
57        script: Option<Utf8PathBuf>,
58        /// SUCL commands to run after a waveform has been loaded, given directly on the
59        /// command line instead of via --command-file. Multiple commands are
60        /// separated by ;.
61        #[clap(long = "command", short = 'C', verbatim_doc_comment)]
62        command_string: Option<String>,
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            match (&self.command_file, &self.script) {
78                (Some(_), Some(_)) => {
79                    error!("At most one of --command_file and --script can be used");
80                    None
81                }
82                (Some(cf), None) => Some(cf),
83                (None, Some(sc)) => Some(sc),
84                (None, None) => None,
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 mut startup_commands = Vec::new();
92        if let Some(command_string) = &args.command_string {
93            startup_commands.push(command_string.clone());
94        }
95        startup_commands.extend(
96            args.command_file()
97                .map(read_command_file)
98                .unwrap_or_default(),
99        );
100        StartupParams {
101            waves: args.wave_file.map(|s| string_to_wavesource(&s)),
102            wcp_initiate: args.wcp_initiate,
103            startup_commands,
104        }
105    }
106
107    #[cfg(not(target_arch = "wasm32"))]
108    pub(crate) fn main() -> Result<()> {
109        use egui::Pos2;
110        use libsurfer::state::UserState;
111        #[cfg(feature = "wasm_plugins")]
112        use libsurfer::translation::wasm_translator::discover_wasm_translators;
113        simple_eyre::install()?;
114
115        logs::start_logging()?;
116
117        std::panic::set_hook(Box::new(panic_handler));
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 {
133            port,
134            bind_address,
135            token,
136            file,
137        }) = args.command
138        {
139            let config = SystemState::new()?.user.config;
140
141            // Use CLI override if provided, otherwise use config setting
142            let bind_addr = bind_address.unwrap_or(config.server.bind_address);
143            let port = port.unwrap_or(config.server.port);
144
145            let res = runtime.block_on(surver::surver_main(port, bind_addr, token, &[file], None));
146            return res;
147        }
148
149        let _enter = runtime.enter();
150
151        std::thread::spawn(move || {
152            runtime.block_on(async {
153                loop {
154                    tokio::time::sleep(tokio::time::Duration::from_hours(1)).await;
155                }
156            });
157        });
158
159        let state_file = args.state_file.clone();
160        let startup_params = startup_params_from_args(args);
161        let waves = startup_params.waves.clone();
162        let window_title = waves
163            .as_ref()
164            .map_or_else(|| "Surfer".to_string(), WaveSource::window_title);
165
166        let state = match &state_file {
167            Some(file) => std::fs::read_to_string(file)
168                .with_context(|| format!("Failed to read state from {file}"))
169                .and_then(|content| {
170                    ron::from_str::<UserState>(&content)
171                        .with_context(|| format!("Failed to decode state from {file}"))
172                })
173                .map(SystemState::from)
174                .map(|mut s| {
175                    s.user.state_file = Some(file.into());
176                    s
177                })
178                .or_else(|e| {
179                    error!("Failed to read state file. Opening fresh session\n{e:#?}");
180                    SystemState::new()
181                })?,
182            None => SystemState::new()?,
183        }
184        .with_params(startup_params);
185
186        #[cfg(feature = "wasm_plugins")]
187        {
188            // Not using batch commands here as we want to start processing wasm plugins
189            // as soon as we start up, no need to wait for the waveform to load
190            let sender = state.channels.msg_sender.clone();
191            for message in discover_wasm_translators() {
192                if let Err(e) = sender.send(message) {
193                    error!("Failed to send message: {e}");
194                }
195            }
196        }
197        // install a file watcher that emits a `SuggestReloadWaveform` message
198        // whenever the user-provided file changes.
199        let _watcher = match waves {
200            Some(WaveSource::File(path)) => {
201                let sender = state.channels.msg_sender.clone();
202                FileWatcher::new(&path, move || {
203                    if let Err(e) = sender.send(Message::SuggestReloadWaveform) {
204                        error!("Message ReloadWaveform did not send:\n{e}");
205                    }
206                    // Force refresh UI to process messages. Otherwise, it is
207                    // deferred until a UI event occurs (like mouseover)
208                    if let Some(ctx) = EGUI_CONTEXT.read().unwrap().as_ref() {
209                        ctx.request_repaint();
210                    }
211                })
212                .inspect_err(|err| error!("Cannot set up the file watcher:\n{err}"))
213                .ok()
214            }
215            _ => None,
216        };
217
218        // Load icon using png crate
219        let icon_bytes = include_bytes!("../assets/com.gitlab.surferproject.surfer.png");
220        let decoder = png::Decoder::new(std::io::Cursor::new(&icon_bytes[..]));
221        let mut reader = decoder.read_info().expect("Failed to read PNG info");
222        let mut icon_data = vec![
223            0;
224            reader
225                .output_buffer_size()
226                .expect("Failed to calculate PNG buffer size")
227        ];
228        let info = reader
229            .next_frame(&mut icon_data)
230            .expect("Failed to decode PNG");
231
232        let options = eframe::NativeOptions {
233            viewport: egui::ViewportBuilder::default()
234                .with_app_id("org.surfer-project.surfer")
235                .with_title(window_title)
236                .with_icon(egui::viewport::IconData {
237                    rgba: icon_data,
238                    width: info.width,
239                    height: info.height,
240                })
241                .with_inner_size(Vec2::new(
242                    state.user.config.layout.window_width as f32,
243                    state.user.config.layout.window_height as f32,
244                ))
245                .with_position(Pos2::new(
246                    state.user.config.layout.window_x_position as f32,
247                    state.user.config.layout.window_y_position as f32,
248                )),
249            ..Default::default()
250        };
251
252        eframe::run_native("Surfer", options, Box::new(|cc| Ok(run_egui(cc, state)?))).unwrap();
253
254        Ok(())
255    }
256
257    fn panic_handler(info: &std::panic::PanicHookInfo) {
258        let backtrace = std::backtrace::Backtrace::force_capture();
259
260        eprintln!();
261        eprintln!("Surfer crashed due to a panic 😞");
262        eprintln!("Please report this issue at https://gitlab.com/surfer-project/surfer/-/issues");
263        eprintln!();
264        eprintln!("Some notes on reports:");
265        eprintln!(
266            "We are happy about any reports, but it makes it much easier for us to fix issues if you:",
267        );
268        eprintln!(" - Include the information below");
269        eprintln!(" - Try to reproduce the issue to give us steps on how to reproduce the issue");
270        eprintln!(" - Include (minimal) waveform file and state file you used");
271        eprintln!("   (you can upload those confidentially, for the surfer team only)");
272        eprintln!();
273
274        let location = info.location().unwrap();
275        let msg = if let Some(msg) = info.payload().downcast_ref::<&str>() {
276            (*msg).to_string()
277        } else if let Some(msg) = info.payload().downcast_ref::<String>() {
278            msg.clone()
279        } else {
280            "<panic message not a string>".to_owned()
281        };
282
283        eprintln!(
284            "Surfer version: {} (git: {})",
285            env!("CARGO_PKG_VERSION"),
286            env!("VERGEN_GIT_DESCRIBE"),
287        );
288        eprintln!(
289            "thread '{}' ({:?}) panicked at {}:{}:{:?}",
290            std::thread::current().name().unwrap_or("unknown"),
291            std::thread::current().id(),
292            location.file(),
293            location.line(),
294            location.column(),
295        );
296        eprintln!("  {msg}");
297        eprintln!();
298        eprintln!("backtrace:");
299        eprintln!("{backtrace}");
300    }
301
302    #[cfg(test)]
303    mod tests {
304        use super::*;
305
306        #[test]
307        fn command_file_prefers_single_sources() {
308            // Only --command_file
309            let args = Args::parse_from(["surfer", "--command-file", "C:/tmp/cmds.sucl"]);
310            let cf = args.command_file().unwrap();
311            assert!(cf.ends_with("cmds.sucl"));
312
313            // Only --script
314            let args = Args::parse_from(["surfer", "--script", "C:/tmp/scr.sucl"]);
315            let cf = args.command_file().unwrap();
316            assert!(cf.ends_with("scr.sucl"));
317        }
318
319        #[test]
320        fn command_file_conflict_returns_none() {
321            let args = Args::parse_from([
322                "surfer",
323                "--command-file",
324                "C:/tmp/cmds.sucl",
325                "--script",
326                "C:/tmp/scr.sucl",
327            ]);
328            assert!(args.command_file().is_none());
329        }
330    }
331}
332
333#[cfg(target_arch = "wasm32")]
334mod main_impl {
335    use libsurfer::logs;
336    use libsurfer::wasm_api::WebHandle;
337    use wasm_bindgen::JsCast;
338
339    // Calling main is not the intended way to start surfer, instead, it should be
340    // started by `wasm_api::WebHandle`
341    pub(crate) fn main() -> eyre::Result<()> {
342        simple_eyre::install()?;
343
344        logs::start_logging()?;
345
346        let document = web_sys::window()
347            .expect("No window")
348            .document()
349            .expect("No document");
350        let canvas = document
351            .get_element_by_id("the_canvas_id")
352            .expect("Failed to find the_canvas_id")
353            .dyn_into::<web_sys::HtmlCanvasElement>()
354            .expect("the_canvas_id was not a HtmlCanvasElement");
355
356        wasm_bindgen_futures::spawn_local(async {
357            let wh = WebHandle::new();
358            wh.start(canvas).await.expect("Failed to start surfer");
359        });
360
361        Ok(())
362    }
363}
364
365fn main() -> eyre::Result<()> {
366    main_impl::main()
367}