Skip to content

Commit

Permalink
[mimimi] initial napi-rs logging and panic handling
Browse files Browse the repository at this point in the history
Co-authored-by: jhm <[email protected]>
  • Loading branch information
ganthern and jomapp committed Sep 24, 2024
1 parent 9a46538 commit 9bcba15
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 42 deletions.
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/node-mimimi/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
166 changes: 166 additions & 0 deletions packages/node-mimimi/src/console.rs
Original file line number Diff line number Diff line change
@@ -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<Console> {
let (tx, rx) = std::sync::mpsc::channel::<LogMessage>();
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<LogMessage>,
}

impl Console {
pub fn new(tx: std::sync::mpsc::Sender<LogMessage>) -> 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<std::sync::mpsc::Receiver<LogMessage>>,
}

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<Self::Output> {
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<Self::JsValue> {
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",
}
}
}
57 changes: 57 additions & 0 deletions packages/node-mimimi/src/imap/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub password: Option<String>,
pub access_token: Option<String>,
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<ImapImportParams>,
}

#[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!()
}
}

36 changes: 36 additions & 0 deletions packages/node-mimimi/src/imap_importer.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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"))
}
}
35 changes: 3 additions & 32 deletions packages/node-mimimi/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<JsNull>(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);
}
mod imap;
mod console;
mod imap_importer;
15 changes: 15 additions & 0 deletions packages/tuta-imap/package.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions packages/tuta-imap/src/testing/jvm_singeleton.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9bcba15

Please sign in to comment.