From 9bcba15a3602c1140dfed38fc1c9da1a52dc9261 Mon Sep 17 00:00:00 2001 From: nig Date: Tue, 24 Sep 2024 17:06:18 +0200 Subject: [PATCH] [mimimi] initial napi-rs logging and panic handling Co-authored-by: jhm <17314077+jomapp@users.noreply.github.com> --- package-lock.json | 9 +- packages/node-mimimi/package.json | 4 +- packages/node-mimimi/src/console.rs | 166 ++++++++++++++++++ packages/node-mimimi/src/imap/mod.rs | 57 ++++++ packages/node-mimimi/src/imap_importer.rs | 36 ++++ packages/node-mimimi/src/lib.rs | 35 +--- packages/tuta-imap/package.json | 15 ++ .../tuta-imap/src/testing/jvm_singeleton.rs | 1 + src/common/desktop/DesktopMain.ts | 16 +- 9 files changed, 297 insertions(+), 42 deletions(-) create mode 100644 packages/node-mimimi/src/console.rs create mode 100644 packages/node-mimimi/src/imap/mod.rs create mode 100644 packages/node-mimimi/src/imap_importer.rs create mode 100644 packages/tuta-imap/package.json diff --git a/package-lock.json b/package-lock.json index fc00e2be2725..b840d77de64c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ ], "dependencies": { "@napi-rs/cli": "^2.18.4", - "@tutao/node-mimimi": "244.240917.0", + "@tutao/node-mimimi": "244.240923.0", "@tutao/oxmsg": "0.0.9-beta.0", "@tutao/tuta-wasm-loader": "246.240923.0", "@tutao/tutanota-crypto": "246.240923.0", @@ -9347,6 +9347,10 @@ "node": "*" } }, + "node_modules/tuta-imap": { + "resolved": "packages/tuta-imap", + "link": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10131,6 +10135,9 @@ "typescript": "5.3.3" } }, + "packages/tuta-imap": { + "version": "1.0.0" + }, "packages/tuta-wasm-loader": { "name": "@tutao/tuta-wasm-loader", "version": "246.240923.0", diff --git a/packages/node-mimimi/package.json b/packages/node-mimimi/package.json index f10985114ade..2df9d977326b 100644 --- a/packages/node-mimimi/package.json +++ b/packages/node-mimimi/package.json @@ -1,8 +1,8 @@ { "name": "@tutao/node-mimimi", "version": "244.240917.0", - "main": "./dist/mimimi.js", - "types": "./dist/mimimi.d.ts", + "main": "./dist/binding.cjs", + "types": "./dist/binding.d.ts", "napi": { "name": "node-mimimi", "triples": { diff --git a/packages/node-mimimi/src/console.rs b/packages/node-mimimi/src/console.rs new file mode 100644 index 000000000000..7ec83503a702 --- /dev/null +++ b/packages/node-mimimi/src/console.rs @@ -0,0 +1,166 @@ +use napi::bindgen_prelude::*; +use napi::{Env, JsFunction, JsObject, JsUndefined, Task}; + +/// todo: plumb through SDK's log messages? it's currently using simple_logger when not compiled +/// todo: for ios or android. + +pub fn setup_logging(env: Env) -> Result { + let (tx, rx) = std::sync::mpsc::channel::(); + let logger = Logger { rx: Some(rx) }; + let Ok(_async_task) = env.spawn(logger) else { + return Err(Error::from_reason("failed to spawn logger")); + }; + + let logger_thread_id = std::thread::current().id(); + let console = Console::new(tx); + let panic_console = console.clone(); + std::panic::set_hook(Box::new(move |panic_info| { + if logger_thread_id == std::thread::current().id() { + // logger is (probably) running on the currently panicking thread, + // so we can't use it to log to JS. this at least shows up in stderr. + eprintln!("PANIC MAIN {}", panic_info.to_string().as_str()); + eprintln!("MAIN PANIC {}", std::backtrace::Backtrace::force_capture().to_string().as_str()); + } else { + panic_console.error("PANIC", format!("thread {:?} {}", std::thread::current().name(), panic_info.to_string().as_str()).as_str()); + panic_console.error("PANIC", std::backtrace::Backtrace::force_capture().to_string().as_str()); + } + })); + + Ok(console) +} + +/// A way for the rust code to log messages to the main applications log files +/// without having to deal with obtaining a reference to console each time. +#[derive(Clone)] +pub struct Console { + tx: std::sync::mpsc::Sender, +} + +impl Console { + pub fn new(tx: std::sync::mpsc::Sender) -> Self { + Self { tx } + } + pub fn log(&self, tag: &str, message: &str) { + // todo: if the logger dies before us, print message to stdout instead and panic? + let _ = self.tx.send(LogMessage { + level: LogLevel::Log, + tag: tag.into(), + message: message.into(), + }); + } + pub fn warn(&self, tag: &str, message: &str) { + let _ = self.tx.send(LogMessage { + level: LogLevel::Warn, + tag: tag.into(), + message: message.into(), + }); + } + + pub fn error(&self, tag: &str, message: &str) { + let _ = self.tx.send(LogMessage { + level: LogLevel::Error, + tag: tag.into(), + message: message.into(), + }); + } +} + +/// The part of the logging setup that receives log messages from the rust log +/// {@link struct Console} and forwards them to the node environment to log. +struct Logger { + /// This is an option because we need to take it from the old instance before + /// rescheduling the listen job with a new one. + rx: Option>, +} + +impl Logger { + fn execute_log(&self, env: Env, log_message: LogMessage) { + let globals = env.get_global() + .expect("no globals in env"); + let console: JsObject = globals.get_named_property("console") + .expect("console property not found"); + + let formatted_message = format!("[{} {}] {}", log_message.marker(), log_message.tag, log_message.message); + let js_string: napi::JsString = env.create_string_from_std(formatted_message) + .expect("could not create string"); + + let js_error: JsFunction = console.get_named_property(log_message.method()) + .expect("logging fn not found"); + js_error.call(None, &[js_string]) + .expect("logging failed"); + } +} + +impl Task for Logger { + type Output = LogMessage; + type JsValue = JsUndefined; + + /// runs on the libuv thread pool. + fn compute(&mut self) -> Result { + if let Some(rx) = &self.rx { + Ok(rx.recv().unwrap_or_else(|_| LogMessage { + level: LogLevel::Finish, + tag: "Logger".to_string(), + message: "channel closed, logger finished".to_string(), + })) + } else { + // should not happen - each Logger instance listens for exactly one message and then + // gets dropped and reincarnated. + Ok(LogMessage { + level: LogLevel::Error, + tag: "Logger".to_string(), + message: "rx not available, already moved".to_string(), + }) + } + } + + fn resolve(&mut self, env: Env, output: Self::Output) -> Result { + let level = output.level; + self.execute_log(env, output); + if level != LogLevel::Finish { + // we only have a &mut self, so can't revive ourselves directly. + // I guess this is reincarnation. + let rx = self.rx.take(); + let _promise = env.spawn(Logger { rx }); + } + Ok(env.get_undefined()?) + } +} + +/// determines the urgency and some formatting of the log message +#[derive(Eq, PartialEq, Copy, Clone)] +enum LogLevel { + /// used if we want to log the fact that all consoles have been dropped (there will not be any more log messages) + Finish, + Log, + Warn, + Error, +} + +struct LogMessage { + pub level: LogLevel, + pub message: String, + pub tag: String, +} + +impl LogMessage { + /// get a prefix for labeling the log level in cases where it's + /// not obvious from terminal colors or similar + pub fn marker(&self) -> &str { + match self.level { + LogLevel::Finish | LogLevel::Log => "I", + LogLevel::Warn => "W", + LogLevel::Error => "E", + } + } + + /// the name of the logging method to use for each log level. + /// very js-specific. + pub fn method(&self) -> &str { + match self.level { + LogLevel::Finish | LogLevel::Log => "log", + LogLevel::Warn => "warn", + LogLevel::Error => "error", + } + } +} diff --git a/packages/node-mimimi/src/imap/mod.rs b/packages/node-mimimi/src/imap/mod.rs new file mode 100644 index 000000000000..e1ea8b629042 --- /dev/null +++ b/packages/node-mimimi/src/imap/mod.rs @@ -0,0 +1,57 @@ +#[napi(object)] +pub struct ImapImportParams { + /// hostname of the imap server to import mail from + pub host: String, + pub port: String, + pub username: Option, + pub password: Option, + pub access_token: Option, + pub root_import_mail_folder_name: String, +} + + +/// current state of the imap import for this tuta account +/// requires an initialized SDK! +#[napi(string_enum)] +pub enum ImapImportStatus { + NotInitialized, + Paused, + Running, + Postponed, + Finished, +} + +#[napi(object)] +pub struct ImapImportConfig { + pub status: ImapImportStatus, // as a string? + pub params: Option, +} + +#[napi] +pub struct ImapImp { + pub status: ImapImportStatus, +} + +#[napi] +impl ImapImp { + #[napi] + pub async fn get_imap_import_config(&self) -> ImapImportConfig { + todo!() + } + + #[napi] + pub async fn continue_import(&self, _imap_import_config: ImapImportConfig) -> ImapImportStatus { + todo!() + } + + #[napi] + pub async fn delete_import(&self) -> ImapImportStatus { + todo!() + } + + #[napi] + pub async fn pause_import(&self) -> ImapImportStatus { + todo!() + } +} + diff --git a/packages/node-mimimi/src/imap_importer.rs b/packages/node-mimimi/src/imap_importer.rs new file mode 100644 index 000000000000..b141ee469d2c --- /dev/null +++ b/packages/node-mimimi/src/imap_importer.rs @@ -0,0 +1,36 @@ +use crate::console::setup_logging; +use crate::console::Console; +use napi::bindgen_prelude::*; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; +use std::{panic, thread}; + +/// if set to true, an importer is already created for this instance of the addon. +static IMPORTER_INIT: AtomicBool = AtomicBool::new(false); + +#[napi] +pub struct ImapImporter { + console: Console, +} + +const TAG: &'static str = file!(); + +#[napi] +impl ImapImporter { + /// only to be used once and only from the javascript side! + #[napi(factory)] + pub fn setup(env: Env) -> Result { + if !IMPORTER_INIT.swap(true, Ordering::Relaxed) { + let console = setup_logging(env)?; + Ok(ImapImporter { console }) + } else { + Err(Error::from_reason("already created an importer!")) + } + } + + #[napi] + pub fn log_that(&self, that: String) -> Result<()> { + self.console.log("that", &that); + Err(Error::from_reason("done")) + } +} diff --git a/packages/node-mimimi/src/lib.rs b/packages/node-mimimi/src/lib.rs index ed657888a37d..f6a9b400c7cf 100644 --- a/packages/node-mimimi/src/lib.rs +++ b/packages/node-mimimi/src/lib.rs @@ -1,36 +1,7 @@ #![deny(clippy::all)] - #[macro_use] extern crate napi_derive; extern crate tutasdk; - -use napi::bindgen_prelude::*; -use napi::{JsFunction, JsNull}; -use std::fs; -use std::ops::Add; -use tutasdk::generated_id; - -#[napi] -pub fn get_id_byte_size_plus( - a: i32, - #[napi(ts_arg_type = "() => number")] cb: JsFunction, -) -> i32 { - println!("moo!"); - let other = cb.call::(None, &[]).unwrap() - .coerce_to_number().unwrap() - .get_int32().unwrap(); - (generated_id::GENERATED_ID_BYTES_LENGTH as i32).add(a).add(other) -} - -#[napi] -pub async fn read_file_async(path: String) -> Result<()> { - println!("async moo!"); - let contents = fs::read(path); - // TODO some async call here ... - Ok(()) -} - -#[test] -fn test() { - assert_eq!(get_id_byte_size_plus(0), 9); -} \ No newline at end of file +mod imap; +mod console; +mod imap_importer; diff --git a/packages/tuta-imap/package.json b/packages/tuta-imap/package.json new file mode 100644 index 000000000000..7d9882cb2d78 --- /dev/null +++ b/packages/tuta-imap/package.json @@ -0,0 +1,15 @@ +{ + "name": "tuta-imap", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo test done", + "build": "echo done" + }, + "repository": { + "type": "git", + "url": "https://github.com/tutao/tutanota.git" + }, + "private": true +} diff --git a/packages/tuta-imap/src/testing/jvm_singeleton.rs b/packages/tuta-imap/src/testing/jvm_singeleton.rs index bf0a0157b124..bed996435137 100644 --- a/packages/tuta-imap/src/testing/jvm_singeleton.rs +++ b/packages/tuta-imap/src/testing/jvm_singeleton.rs @@ -4,6 +4,7 @@ use j4rs::{ClasspathEntry, JvmBuilder}; static mut START_JVM_INVOCATION_COUNTER: i32 = 0; pub fn start_or_attach_to_jvm() -> i32 { + /// todo: SAFETY??? unsafe { if START_JVM_INVOCATION_COUNTER == 0 { // create exactly one jvm and attach to it whenever we create a new IMAP test server diff --git a/src/common/desktop/DesktopMain.ts b/src/common/desktop/DesktopMain.ts index 906dd0948088..ddc13d4a5845 100644 --- a/src/common/desktop/DesktopMain.ts +++ b/src/common/desktop/DesktopMain.ts @@ -49,7 +49,6 @@ import { DesktopPostLoginActions } from "./DesktopPostLoginActions.js" import { DesktopInterWindowEventFacade } from "./ipc/DesktopInterWindowEventFacade.js" import { OfflineDbFactory, PerWindowSqlCipherFacade } from "./db/PerWindowSqlCipherFacade.js" import { lazyMemoized } from "@tutao/tutanota-utils" -import { getIdByteSizePlus } from "@tutao/node-mimimi" import dns from "node:dns" import { getConfigFile } from "./config/ConfigFile.js" import { OfflineDbRefCounter } from "./db/OfflineDbRefCounter.js" @@ -71,6 +70,9 @@ import { ExposedNativeInterface } from "../native/common/NativeInterface.js" import { DelayedImpls, exposeLocalDelayed } from "../api/common/WorkerProxy.js" import { DefaultDateProvider } from "../calendar/date/CalendarUtils.js" import { AlarmScheduler } from "../calendar/date/AlarmScheduler.js" +import { ImapImporter } from "@tutao/node-mimimi" + +mp() /** * Should be injected during build time. @@ -86,12 +88,6 @@ setupAssetProtocol(electron) const TAG = "[DesktopMain]" -console.log( - ">>>>>>>>>>>>>> CODE FROM RUST!", - getIdByteSizePlus(42, () => -9), -) - -mp() type Components = { readonly wm: WindowManager readonly tfs: TempFs @@ -336,6 +332,11 @@ async function startupInstance(components: Components) { await onAppReady(components) } +function testImapImporter() { + const importer = ImapImporter.setup() + setTimeout(() => importer.logThat("my!"), 5000) +} + async function onAppReady(components: Components) { const { wm, keyStoreFacade, conf } = components keyStoreFacade.getDeviceKey().catch(() => { @@ -347,6 +348,7 @@ async function onAppReady(components: Components) { } }) err.init(wm) + testImapImporter() // only create a window if there are none (may already have created one, e.g. for mailto handling) // also don't show the window when we're an autolaunched tray app const w = await wm.getLastFocused(!((await conf.getVar(DesktopConfigKey.runAsTrayApp)) && opts.wasAutoLaunched))