diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 0884f59e863..6858011e0cc 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -987,6 +987,7 @@ dependencies = [ "base64", "desktop_core", "hex", + "log", "napi", "napi-build", "napi-derive", diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index d59581a5e2e..669f166e748 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -18,6 +18,7 @@ base64 = { workspace = true } hex = { workspace = true } anyhow = { workspace = true } desktop_core = { path = "../core" } +log = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 6ded4e3db93..952f2571c5d 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -185,3 +185,13 @@ export declare namespace crypto { export declare namespace passkey_authenticator { export function register(): void } +export declare namespace logging { + export const enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4 + } + export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 8cbc526487e..37796ef6f59 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -807,3 +807,61 @@ pub mod passkey_authenticator { }) } } + +#[napi] +pub mod logging { + use log::{Level, Metadata, Record}; + use napi::threadsafe_function::{ + ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode, + }; + use std::sync::OnceLock; + struct JsLogger(OnceLock>); + static JS_LOGGER: JsLogger = JsLogger(OnceLock::new()); + + #[napi] + pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl From for LogLevel { + fn from(level: Level) -> Self { + match level { + Level::Trace => LogLevel::Trace, + Level::Debug => LogLevel::Debug, + Level::Info => LogLevel::Info, + Level::Warn => LogLevel::Warn, + Level::Error => LogLevel::Error, + } + } + } + + #[napi] + pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) { + let _ = JS_LOGGER.0.set(js_log_fn); + let _ = log::set_logger(&JS_LOGGER); + log::set_max_level(log::LevelFilter::Debug); + } + + impl log::Log for JsLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if !self.enabled(record.metadata()) { + return; + } + let Some(logger) = self.0.get() else { + return; + }; + let msg = (record.level().into(), record.args().to_string()); + let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking); + } + + fn flush(&self) {} + } +} diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index d7100b54825..947f4449271 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -7,6 +7,7 @@ import log from "electron-log/main"; import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum"; import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { logging } from "@bitwarden/desktop-napi"; import { isDev } from "../../utils"; @@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService { ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { this.write(level, message, ...optionalParams); }); + + logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message)); + } + + private writeNapiLog(level: logging.LogLevel, message: string) { + let levelType: LogLevelType; + + switch (level) { + case logging.LogLevel.Debug: + levelType = LogLevelType.Debug; + break; + case logging.LogLevel.Warn: + levelType = LogLevelType.Warning; + break; + case logging.LogLevel.Error: + levelType = LogLevelType.Error; + break; + default: + levelType = LogLevelType.Info; + break; + } + + this.write(levelType, "[NAPI] " + message); } write(level: LogLevelType, message?: any, ...optionalParams: any[]) { diff --git a/apps/desktop/src/platform/services/electron-log.service.spec.ts b/apps/desktop/src/platform/services/electron-log.service.spec.ts index 918508977fd..db3093e08e2 100644 --- a/apps/desktop/src/platform/services/electron-log.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-log.service.spec.ts @@ -5,6 +5,14 @@ jest.mock("electron", () => ({ ipcMain: { handle: jest.fn(), on: jest.fn() }, })); +jest.mock("@bitwarden/desktop-napi", () => { + return { + logging: { + initNapiLog: jest.fn(), + }, + }; +}); + describe("ElectronLogMainService", () => { it("sets dev based on electron method", () => { process.env.ELECTRON_IS_DEV = "1";