/ bin / app / src / main.rs
main.rs
  1  /* This file is part of DarkFi (https://dark.fi)
  2   *
  3   * Copyright (C) 2020-2025 Dyne.org foundation
  4   *
  5   * This program is free software: you can redistribute it and/or modify
  6   * it under the terms of the GNU Affero General Public License as
  7   * published by the Free Software Foundation, either version 3 of the
  8   * License, or (at your option) any later version.
  9   *
 10   * This program is distributed in the hope that it will be useful,
 11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
 12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13   * GNU Affero General Public License for more details.
 14   *
 15   * You should have received a copy of the GNU Affero General Public License
 16   * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17   */
 18  
 19  // Hides the cmd.exe terminal on Windows.
 20  // Enable this when making release builds.
 21  #![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
 22  
 23  use clap::Parser;
 24  use darkfi::system::CondVar;
 25  use std::sync::{Arc, OnceLock};
 26  
 27  #[macro_use]
 28  extern crate tracing;
 29  
 30  #[derive(Debug)]
 31  pub enum AndroidSuggestEvent {
 32      Init,
 33      CreateInputConnect,
 34      Compose { text: String, cursor_pos: i32, is_commit: bool },
 35      ComposeRegion { start: usize, end: usize },
 36      FinishCompose,
 37      DeleteSurroundingText { left: usize, right: usize },
 38  }
 39  
 40  #[cfg(target_os = "android")]
 41  mod android;
 42  mod app;
 43  mod build_info;
 44  mod error;
 45  mod expr;
 46  mod gfx;
 47  mod logger;
 48  mod mesh;
 49  #[cfg(feature = "enable-netdebug")]
 50  mod net;
 51  mod plugin;
 52  mod prop;
 53  mod pubsub;
 54  //mod py;
 55  //mod ringbuf;
 56  mod scene;
 57  mod shape;
 58  mod text;
 59  mod text2;
 60  mod ui;
 61  mod util;
 62  
 63  use crate::{
 64      app::{App, AppPtr},
 65      gfx::EpochIndex,
 66      prop::{Property, PropertySubType, PropertyType},
 67      scene::{CallArgType, SceneNode, SceneNodeType},
 68      text::TextShaper,
 69      util::AsyncRuntime,
 70  };
 71  #[cfg(feature = "enable-netdebug")]
 72  use net::ZeroMQAdapter;
 73  #[cfg(feature = "enable-plugins")]
 74  use {
 75      darkfi_serial::{deserialize, Decodable, Encodable},
 76      gfx::RenderApi,
 77      prop::{PropertyBool, PropertyStr, Role},
 78      scene::{SceneNodePtr, Slot},
 79      std::io::Cursor,
 80      ui::chatview,
 81  };
 82  
 83  // This is historical, but ideally we can fix the entire project and remove this import.
 84  pub use util::ExecutorPtr;
 85  
 86  macro_rules! t { ($($arg:tt)*) => { trace!(target: "main", $($arg)*); } }
 87  #[cfg(feature = "enable-plugins")]
 88  macro_rules! d { ($($arg:tt)*) => { trace!(target: "main", $($arg)*); } }
 89  #[cfg(feature = "enable-plugins")]
 90  macro_rules! i { ($($arg:tt)*) => { trace!(target: "main", $($arg)*); } }
 91  
 92  fn panic_hook(panic_info: &std::panic::PanicHookInfo) {
 93      error!("panic occurred: {panic_info}");
 94      error!("{}", std::backtrace::Backtrace::force_capture().to_string());
 95      std::process::abort()
 96  }
 97  
 98  /// Contains values which persist between app restarts. For example on Android, we are
 99  /// running a foreground service. Everytime the UI restarts main() is called again.
100  /// However the global state remains intact.
101  struct God {
102      _bg_runtime: AsyncRuntime,
103      _bg_ex: ExecutorPtr,
104  
105      pub fg_runtime: AsyncRuntime,
106      pub fg_ex: ExecutorPtr,
107  
108      /// App must fully finish setup() before start() is allowed to begin.
109      cv_app_is_setup: Arc<CondVar>,
110      app: AppPtr,
111  
112      /// This is the main rendering API used to send commands to the gfx subsystem.
113      /// We have a ref here so the gfx subsystem can increment the epoch counter.
114      render_api: gfx::RenderApi,
115      /// This is how the gfx subsystem receives messages from the render API.
116      method_recv: async_channel::Receiver<(gfx::EpochIndex, gfx::GraphicsMethod)>,
117      /// Publisher to send input and window events to subscribers.
118      event_pub: gfx::GraphicsEventPublisherPtr,
119  
120      /// A WorkerGuard for file logging used to ensure buffered logs are flushed
121      /// to their output in the case of abrupt terminations of a process.
122      _file_logging_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
123  }
124  
125  impl God {
126      fn new() -> Self {
127          // Abort the application on panic right away
128          std::panic::set_hook(Box::new(panic_hook));
129  
130          text2::init_txt_ctx();
131          let file_logging_guard = logger::setup_logging();
132  
133          info!(target: "main", "Creating the app");
134  
135          #[cfg(target_os = "android")]
136          {
137              use crate::android::get_appdata_path;
138  
139              // Workaround for this bug
140              // https://gitlab.torproject.org/tpo/core/arti/-/issues/999
141              unsafe {
142                  std::env::set_var("HOME", get_appdata_path().as_os_str());
143              }
144          }
145  
146          let exe_path = std::env::current_exe().unwrap();
147          let basename = exe_path.parent().unwrap();
148          std::env::set_current_dir(basename).unwrap();
149  
150          let bg_ex = Arc::new(smol::Executor::new());
151          let fg_ex = Arc::new(smol::Executor::new());
152          let sg_root = SceneNode::root();
153  
154          let bg_runtime = AsyncRuntime::new(bg_ex.clone(), "bg");
155          bg_runtime.start();
156  
157          let fg_runtime = AsyncRuntime::new(fg_ex.clone(), "fg");
158  
159          let (method_send, method_recv) = async_channel::unbounded();
160          // The UI actually needs to be running for this to reply back.
161          // Otherwise calls will just hang.
162          let render_api = gfx::RenderApi::new(method_send);
163          let event_pub = gfx::GraphicsEventPublisher::new();
164  
165          let text_shaper = TextShaper::new();
166  
167          let app = App::new(sg_root.clone(), render_api.clone(), text_shaper, fg_ex.clone());
168  
169          let app2 = app.clone();
170          let cv_app_is_setup = Arc::new(CondVar::new());
171          let cv = cv_app_is_setup.clone();
172          let app_task = fg_ex.spawn(async move {
173              app2.setup().await.unwrap();
174              cv.notify();
175          });
176          fg_runtime.push_task(app_task);
177  
178          #[cfg(feature = "enable-netdebug")]
179          {
180              let sg_root = sg_root.clone();
181              let ex = bg_ex.clone();
182              let render_api = render_api.clone();
183              let zmq_task = bg_ex.spawn(async {
184                  let zmq_rpc = ZeroMQAdapter::new(sg_root, render_api, ex).await;
185                  zmq_rpc.run().await;
186              });
187              bg_runtime.push_task(zmq_task);
188          }
189  
190          #[cfg(feature = "enable-plugins")]
191          {
192              let ex = bg_ex.clone();
193              let cv = cv_app_is_setup.clone();
194              let render_api = render_api.clone();
195              let plug_task = bg_ex.spawn(async move {
196                  load_plugins(ex, sg_root, render_api, cv).await;
197              });
198              bg_runtime.push_task(plug_task);
199          }
200  
201          #[cfg(not(feature = "enable-plugins"))]
202          warn!(target: "main", "Plugins are disabled in this build");
203  
204          Self {
205              _bg_runtime: bg_runtime,
206              _bg_ex: bg_ex,
207  
208              fg_runtime,
209              fg_ex,
210              cv_app_is_setup,
211              app,
212  
213              render_api,
214              method_recv,
215              event_pub,
216              _file_logging_guard: file_logging_guard,
217          }
218      }
219  
220      /// Start the app. Can only happen once the window is ready.
221      pub fn start_app(&self, epoch: EpochIndex) {
222          info!(target: "main", "Starting the app");
223          #[cfg(target_os = "android")]
224          {
225              use crate::android::{get_appdata_path, get_external_storage_path};
226  
227              info!("App internal data path: {:?}", get_appdata_path());
228              info!("App external storage path: {:?}", get_external_storage_path());
229  
230              //let paths = std::fs::read_dir("/data/data/darkfi.darkfi/").unwrap();
231              //for path in paths {
232              //    debug!("{}", path.unwrap().path().display())
233              //}
234          }
235  
236          info!("Target OS: {}", build_info::TARGET_OS);
237          info!("Target arch: {}", build_info::TARGET_ARCH);
238          let cwd = std::env::current_dir().unwrap();
239          info!("Current dir: {}", cwd.display());
240  
241          self.fg_runtime.start_with_count(2);
242  
243          let app = self.app.clone();
244          let cv = self.cv_app_is_setup.clone();
245          let event_pub = self.event_pub.clone();
246          smol::block_on(async move {
247              cv.wait().await;
248              app.start(event_pub, epoch).await;
249          });
250      }
251  
252      /// Put the app to sleep until the next restart.
253      pub fn stop_app(&self) {
254          self.fg_runtime.stop();
255          self.app.stop();
256          info!(target: "main", "App stopped");
257      }
258  }
259  
260  impl std::fmt::Debug for God {
261      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262          write!(f, "God")
263      }
264  }
265  
266  static GOD: OnceLock<God> = OnceLock::new();
267  
268  #[cfg(feature = "enable-plugins")]
269  async fn load_plugins(
270      ex: ExecutorPtr,
271      sg_root: SceneNodePtr,
272      render_api: RenderApi,
273      cv: Arc<CondVar>,
274  ) {
275      let plugin = SceneNode::new("plugin", SceneNodeType::PluginRoot);
276      let plugin = plugin.setup_null();
277      sg_root.link(plugin.clone());
278  
279      let darkirc = create_darkirc("darkirc");
280      let darkirc = darkirc
281          .setup(|me| async {
282              plugin::DarkIrc::new(me, ex.clone()).await.expect("DarkIrc pimpl setup")
283          })
284          .await;
285  
286      let (slot, recvr) = Slot::new("recvmsg");
287      darkirc.register("recv", slot).unwrap();
288      let sg_root2 = sg_root.clone();
289      let darkirc_nick = PropertyStr::wrap(&darkirc, Role::App, "nick", 0).unwrap();
290      let render_api2 = render_api.clone();
291      let listen_recv = ex.spawn(async move {
292          while let Ok(data) = recvr.recv().await {
293              let atom = &mut render_api2.make_guard(gfxtag!("darkirc msg recv"));
294  
295              let mut cur = Cursor::new(&data);
296              let channel = String::decode(&mut cur).unwrap();
297              let timestamp = chatview::Timestamp::decode(&mut cur).unwrap();
298              let id = chatview::MessageId::decode(&mut cur).unwrap();
299              let nick = String::decode(&mut cur).unwrap();
300              let msg = String::decode(&mut cur).unwrap();
301  
302              let node_path = format!("/window/{channel}_chat_layer/content/chatty");
303              t!("Attempting to relay message to {node_path}");
304              let Some(chatview) = sg_root2.lookup_node(&node_path) else {
305                  d!("Ignoring message since {node_path} doesn't exist");
306                  continue
307              };
308  
309              // I prefer to just re-encode because the code is clearer.
310              let mut data = vec![];
311              timestamp.encode(&mut data).unwrap();
312              id.encode(&mut data).unwrap();
313              nick.encode(&mut data).unwrap();
314              msg.encode(&mut data).unwrap();
315              if let Err(err) = chatview.call_method("insert_line", data).await {
316                  error!(
317                      target: "app",
318                      "Call method {node_path}::insert_line({timestamp}, {id}, {nick}, '{msg}'): {err:?}"
319                  );
320              }
321  
322              // Apply coloring when you get a message
323              let chat_path = format!("/window/{channel}_chat_layer");
324              let chat_layer = sg_root2.lookup_node(chat_path).unwrap();
325              if chat_layer.get_property_bool("is_visible").unwrap() {
326                  continue
327              }
328  
329              let node_path = format!("/window/menu_layer/{channel}_channel_label");
330              let menu_label = sg_root2.lookup_node(&node_path).unwrap();
331              let prop = menu_label.get_property("text_color").unwrap();
332              if msg.contains(&darkirc_nick.get()) {
333                  // Nick highlight
334                  prop.set_f32(atom, Role::App, 0, 0.56).unwrap();
335                  prop.set_f32(atom, Role::App, 1, 0.61).unwrap();
336                  prop.set_f32(atom, Role::App, 2, 1.).unwrap();
337                  prop.set_f32(atom, Role::App, 3, 1.).unwrap();
338              } else {
339                  // Normal channel activity
340                  prop.set_f32(atom, Role::App, 0, 0.36).unwrap();
341                  prop.set_f32(atom, Role::App, 1, 1.).unwrap();
342                  prop.set_f32(atom, Role::App, 2, 0.51).unwrap();
343                  prop.set_f32(atom, Role::App, 3, 1.).unwrap();
344              }
345          }
346      });
347  
348      let (slot, recvr) = Slot::new("connect");
349      darkirc.register("connect", slot).unwrap();
350      let sg_root2 = sg_root.clone();
351      let listen_connect = ex.spawn(async move {
352          cv.wait().await;
353          let net0 = sg_root2.lookup_node("/window/netstatus_layer/net0").unwrap();
354          let net1 = sg_root2.lookup_node("/window/netstatus_layer/net1").unwrap();
355          let net2 = sg_root2.lookup_node("/window/netstatus_layer/net2").unwrap();
356          let net3 = sg_root2.lookup_node("/window/netstatus_layer/net3").unwrap();
357  
358          let net0_is_visible = PropertyBool::wrap(&net0, Role::App, "is_visible", 0).unwrap();
359          let net1_is_visible = PropertyBool::wrap(&net1, Role::App, "is_visible", 0).unwrap();
360          let net2_is_visible = PropertyBool::wrap(&net2, Role::App, "is_visible", 0).unwrap();
361          let net3_is_visible = PropertyBool::wrap(&net3, Role::App, "is_visible", 0).unwrap();
362  
363          while let Ok(data) = recvr.recv().await {
364              let (peers_count, is_dag_synced): (u32, bool) = deserialize(&data).unwrap();
365  
366              let atom = &mut render_api.make_guard(gfxtag!("netstatus change"));
367  
368              if peers_count == 0 {
369                  net0_is_visible.set(atom, true);
370                  net1_is_visible.set(atom, false);
371                  net2_is_visible.set(atom, false);
372                  net3_is_visible.set(atom, false);
373                  continue
374              }
375  
376              assert!(peers_count > 0);
377              if !is_dag_synced {
378                  net0_is_visible.set(atom, false);
379                  net1_is_visible.set(atom, true);
380                  net2_is_visible.set(atom, false);
381                  net3_is_visible.set(atom, false);
382                  continue
383              }
384  
385              assert!(peers_count > 0 && is_dag_synced);
386              if peers_count == 1 {
387                  net0_is_visible.set(atom, false);
388                  net1_is_visible.set(atom, false);
389                  net2_is_visible.set(atom, true);
390                  net3_is_visible.set(atom, false);
391                  continue
392              }
393  
394              net0_is_visible.set(atom, false);
395              net1_is_visible.set(atom, false);
396              net2_is_visible.set(atom, false);
397              net3_is_visible.set(atom, true);
398          }
399      });
400  
401      plugin.link(darkirc);
402  
403      i!("Plugins loaded");
404      futures::join!(listen_recv, listen_connect);
405  }
406  
407  pub fn create_darkirc(name: &str) -> SceneNode {
408      t!("create_darkirc({name})");
409      let mut node = SceneNode::new(name, SceneNodeType::Plugin);
410  
411      let mut prop = Property::new("nick", PropertyType::Str, PropertySubType::Null);
412      prop.set_ui_text("Nick", "Nickname");
413      prop.set_defaults_str(vec!["anon".to_string()]).unwrap();
414      node.add_property(prop).unwrap();
415  
416      node.add_signal(
417          "recv",
418          "Message received",
419          vec![
420              ("channel", "Channel", CallArgType::Str),
421              ("timestamp", "Timestamp", CallArgType::Uint64),
422              ("id", "ID", CallArgType::Hash),
423              ("nick", "Nick", CallArgType::Str),
424              ("msg", "Message", CallArgType::Str),
425          ],
426      )
427      .unwrap();
428  
429      node.add_signal(
430          "connect",
431          "Connections and disconnects",
432          vec![
433              ("peers_count", "Peers Count", CallArgType::Uint32),
434              ("dag_synced", "Is DAG Synced", CallArgType::Bool),
435          ],
436      )
437      .unwrap();
438  
439      node.add_method(
440          "send",
441          vec![("channel", "Channel", CallArgType::Str), ("msg", "Message", CallArgType::Str)],
442          None,
443      )
444      .unwrap();
445  
446      node
447  }
448  
449  /// Simple program to greet a person
450  #[derive(Parser, Debug)]
451  #[command(version, about, long_about = None)]
452  struct Args {
453      /// On Linux use the X11 backend
454      #[arg(long)]
455      linux_x11_backend: bool,
456  
457      /// On Linux use the wayland backend
458      #[arg(long)]
459      linux_wayland_backend: bool,
460  }
461  
462  fn main() {
463      let args = Args::parse();
464  
465      GOD.get_or_init(God::new);
466  
467      // Reuse render_api, event_pub and text_shaper
468      // No need for setup(), just wait for gfx start then call .start()
469      // ZMQ, darkirc stay running
470  
471      let linux_backend = if args.linux_wayland_backend {
472          if args.linux_x11_backend {
473              miniquad::conf::LinuxBackend::WaylandWithX11Fallback
474          } else {
475              miniquad::conf::LinuxBackend::WaylandOnly
476          }
477      } else if args.linux_x11_backend {
478          miniquad::conf::LinuxBackend::X11Only
479      } else {
480          miniquad::conf::LinuxBackend::WaylandWithX11Fallback
481      };
482  
483      gfx::run_gui(linux_backend);
484      debug!(target: "main", "Started GFX backend");
485  }
486  
487  /*
488  use rustpython_vm::{self as pyvm, convert::ToPyObject};
489  
490  fn main() {
491      let module = pyvm::Interpreter::without_stdlib(Default::default()).enter(|vm| {
492          let source = r#"
493  def foo():
494      open("hihi", "w")
495      return 110
496  #max(1 + lw/3, 4*10) + foo(2, True)
497  "#;
498          //let code_obj = vm
499          //    .compile(source, pyvm::compiler::Mode::Exec, "<embedded>".to_owned())
500          //    .map_err(|err| vm.new_syntax_error(&err, Some(source))).unwrap();
501          //code_obj
502          pyvm::import::import_source(vm, "lain", source).unwrap()
503      });
504  
505      fn foo(x: u32, y: bool) -> u32 {
506          if y {
507              2 * x
508          } else {
509              x
510          }
511      }
512  
513      let res = pyvm::Interpreter::without_stdlib(Default::default()).enter(|vm| {
514          let globals = vm.ctx.new_dict();
515          globals.set_item("lw", vm.ctx.new_int(110).to_pyobject(vm), vm).unwrap();
516          globals.set_item("lh", vm.ctx.new_int(4).to_pyobject(vm), vm).unwrap();
517          globals.set_item("foo", vm.new_function("foo", foo).into(), vm).unwrap();
518  
519          let scope = pyvm::scope::Scope::new(None, globals);
520  
521          let foo_fn = module.get_attr("foo", vm).unwrap();
522          foo_fn.call((), vm).unwrap()
523  
524          //vm.run_code_obj(code_obj, scope).unwrap()
525      });
526      println!("{:?}", res);
527  }
528  */