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