feat: add IPC logging for all command invocations

- Introduced a macro `ipc_log!` to log the invocation and result of Tauri IPC commands in the Rust backend.
- Updated existing commands in `dm.rs` and `identity.rs` to use the new logging macro.
- Wrapped the `invoke` function in the frontend to log all IPC calls and their results, including redacting sensitive arguments.
This commit is contained in:
cloudwithax 2026-02-16 08:29:22 -05:00
parent 70377f13b8
commit 197d8ec16c
6 changed files with 757 additions and 633 deletions

View File

@ -12,8 +12,11 @@ use crate::protocol::messages::{
use crate::verification;
use crate::AppState;
use super::ipc_log;
#[tauri::command]
pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Result<(), String> {
ipc_log!("start_node", {
let identity = state.identity.lock().await;
let id = identity
.as_ref()
@ -205,10 +208,12 @@ pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Re
}
Ok(())
})
}
#[tauri::command]
pub async fn stop_node(state: State<'_, AppState>) -> Result<(), String> {
ipc_log!("stop_node", {
let mut node_handle = state.node_handle.lock().await;
if let Some(handle) = node_handle.take() {
@ -224,6 +229,7 @@ pub async fn stop_node(state: State<'_, AppState>) -> Result<(), String> {
}
Ok(())
})
}
#[tauri::command]
@ -232,6 +238,7 @@ pub async fn send_message(
channel_id: String,
content: String,
) -> Result<ChatMessage, String> {
ipc_log!("send_message", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
@ -271,6 +278,7 @@ pub async fn send_message(
}
Ok(msg)
})
}
#[tauri::command]
@ -280,13 +288,16 @@ pub async fn get_messages(
before: Option<u64>,
limit: Option<usize>,
) -> Result<Vec<ChatMessage>, String> {
ipc_log!("get_messages", {
let engine = state.crdt_engine.lock().await;
let community_id = find_community_for_channel(&engine, &channel_id)?;
engine.get_messages(&community_id, &channel_id, before, limit.unwrap_or(50))
})
}
#[tauri::command]
pub async fn send_typing(state: State<'_, AppState>, channel_id: String) -> Result<(), String> {
ipc_log!("send_typing", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
@ -318,11 +329,13 @@ pub async fn send_typing(state: State<'_, AppState>, channel_id: String) -> Resu
}
Ok(())
})
}
// broadcast current user status to all joined communities
#[tauri::command]
pub async fn broadcast_presence(state: State<'_, AppState>, status: String) -> Result<(), String> {
ipc_log!("broadcast_presence", {
let peer_status = match status.as_str() {
"online" => PeerStatus::Online,
"idle" => PeerStatus::Idle,
@ -343,6 +356,7 @@ pub async fn broadcast_presence(state: State<'_, AppState>, status: String) -> R
}
Ok(())
})
}
// find which community a channel belongs to by checking all loaded documents
@ -367,6 +381,7 @@ fn find_community_for_channel(
// between a general internet outage and the relay being unreachable
#[tauri::command]
pub async fn check_internet_connectivity() -> Result<bool, String> {
ipc_log!("check_internet_connectivity", {
let hosts = vec![
("www.apple.com", 80),
("www.google.com", 80),
@ -386,4 +401,5 @@ pub async fn check_internet_connectivity() -> Result<bool, String> {
let results = futures::future::join_all(futures).await;
Ok(results.iter().any(|r| matches!(r, Ok(Ok(_)))))
})
}

View File

@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use sha2::{Digest, Sha256};
use tauri::State;
use super::ipc_log;
use crate::crdt::sync::{DocumentSnapshot, SyncMessage};
use crate::node::gossip;
use crate::node::NodeCommand;
@ -100,6 +101,7 @@ pub async fn create_community(
name: String,
description: String,
) -> Result<CommunityMeta, String> {
ipc_log!("create_community", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
@ -168,6 +170,7 @@ pub async fn create_community(
}
Ok(meta)
})
}
#[tauri::command]
@ -175,6 +178,7 @@ pub async fn join_community(
state: State<'_, AppState>,
invite_code: String,
) -> Result<CommunityMeta, String> {
ipc_log!("join_community", {
let invite = crate::protocol::community::InviteCode::decode(&invite_code)?;
let local_peer_id = {
@ -272,6 +276,7 @@ pub async fn join_community(
request_sync(&state).await;
Ok(meta)
})
}
#[tauri::command]
@ -279,6 +284,7 @@ pub async fn leave_community(
state: State<'_, AppState>,
community_id: String,
) -> Result<(), String> {
ipc_log!("leave_community", {
let local_peer_id = {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
@ -349,10 +355,12 @@ pub async fn leave_community(
guard.remove(&community_id);
Ok(())
})
}
#[tauri::command]
pub async fn get_communities(state: State<'_, AppState>) -> Result<Vec<CommunityMeta>, String> {
ipc_log!("get_communities", {
let engine = state.crdt_engine.lock().await;
let mut communities = Vec::new();
@ -363,6 +371,7 @@ pub async fn get_communities(state: State<'_, AppState>) -> Result<Vec<Community
}
Ok(communities)
})
}
#[tauri::command]
@ -374,6 +383,7 @@ pub async fn create_channel(
kind: Option<String>,
category_id: Option<String>,
) -> Result<ChannelMeta, String> {
ipc_log!("create_channel", {
let mut hasher = Sha256::new();
hasher.update(community_id.as_bytes());
hasher.update(name.as_bytes());
@ -425,6 +435,7 @@ pub async fn create_channel(
broadcast_sync(&state, &community_id).await;
Ok(channel)
})
}
#[tauri::command]
@ -432,8 +443,10 @@ pub async fn get_channels(
state: State<'_, AppState>,
community_id: String,
) -> Result<Vec<ChannelMeta>, String> {
ipc_log!("get_channels", {
let engine = state.crdt_engine.lock().await;
engine.get_channels(&community_id)
})
}
#[tauri::command]
@ -442,6 +455,7 @@ pub async fn create_category(
community_id: String,
name: String,
) -> Result<CategoryMeta, String> {
ipc_log!("create_category", {
let mut hasher = Sha256::new();
hasher.update(community_id.as_bytes());
hasher.update(name.as_bytes());
@ -467,6 +481,7 @@ pub async fn create_category(
broadcast_sync(&state, &community_id).await;
Ok(category)
})
}
#[tauri::command]

View File

@ -2,6 +2,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
use tauri::State;
use super::ipc_log;
use crate::node::gossip;
use crate::node::NodeCommand;
use crate::protocol::messages::{
@ -19,6 +20,7 @@ pub async fn send_dm(
peer_id: String,
content: String,
) -> Result<DirectMessage, String> {
ipc_log!("send_dm", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
@ -116,6 +118,7 @@ pub async fn send_dm(
}
Ok(msg)
})
}
// load dm messages for a conversation with a specific peer
@ -126,6 +129,7 @@ pub async fn get_dm_messages(
before: Option<u64>,
limit: Option<usize>,
) -> Result<Vec<DirectMessage>, String> {
ipc_log!("get_dm_messages", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -137,6 +141,7 @@ pub async fn get_dm_messages(
.storage
.load_dm_messages(&conversation_id, before, limit.unwrap_or(50))
.map_err(|e| format!("failed to load dm messages: {}", e))
})
}
// search dm messages on the backend using sqlite indexes
@ -152,6 +157,7 @@ pub async fn search_dm_messages(
date_before: Option<u64>,
limit: Option<usize>,
) -> Result<Vec<DirectMessage>, String> {
ipc_log!("search_dm_messages", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -179,6 +185,7 @@ pub async fn search_dm_messages(
.storage
.search_dm_messages(&conversation_id, &params)
.map_err(|e| format!("failed to search dm messages: {}", e))
})
}
// load all dm conversations for the sidebar
@ -186,17 +193,20 @@ pub async fn search_dm_messages(
pub async fn get_dm_conversations(
state: State<'_, AppState>,
) -> Result<Vec<DMConversationMeta>, String> {
ipc_log!("get_dm_conversations", {
let conversations = state
.storage
.load_all_dm_conversations()
.map_err(|e| format!("failed to load dm conversations: {}", e))?;
Ok(conversations.into_iter().map(|(_, meta)| meta).collect())
})
}
// mark all messages in a dm conversation as read
#[tauri::command]
pub async fn mark_dm_read(state: State<'_, AppState>, peer_id: String) -> Result<(), String> {
ipc_log!("mark_dm_read", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -215,6 +225,7 @@ pub async fn mark_dm_read(state: State<'_, AppState>, peer_id: String) -> Result
.storage
.save_dm_conversation(&conversation_id, &meta)
.map_err(|e| format!("failed to save conversation: {}", e))
})
}
// delete a dm conversation and all its messages
@ -223,6 +234,7 @@ pub async fn delete_dm_conversation(
state: State<'_, AppState>,
peer_id: String,
) -> Result<(), String> {
ipc_log!("delete_dm_conversation", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -244,11 +256,13 @@ pub async fn delete_dm_conversation(
.storage
.remove_dm_conversation(&conversation_id)
.map_err(|e| format!("failed to delete conversation: {}", e))
})
}
// send a typing indicator in a dm conversation
#[tauri::command]
pub async fn send_dm_typing(state: State<'_, AppState>, peer_id: String) -> Result<(), String> {
ipc_log!("send_dm_typing", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -278,6 +292,7 @@ pub async fn send_dm_typing(state: State<'_, AppState>, peer_id: String) -> Resu
}
Ok(())
})
}
// open a dm conversation with a peer (creates metadata on disk and subscribes to topic)
@ -288,6 +303,7 @@ pub async fn open_dm_conversation(
peer_id: String,
display_name: String,
) -> Result<DMConversationMeta, String> {
ipc_log!("open_dm_conversation", {
let identity = state.identity.lock().await;
let id = identity.as_ref().ok_or("no identity loaded")?;
let local_peer_id = id.peer_id.to_string();
@ -352,4 +368,5 @@ pub async fn open_dm_conversation(
}
Ok(meta)
})
}

View File

@ -11,6 +11,8 @@ use crate::storage::UserSettings;
use crate::verification::{self, ChallengeSubmission};
use crate::AppState;
use super::ipc_log;
// build a signed profile announcement and publish it on the directory topic
// so all connected peers immediately learn about the updated profile.
// silently no-ops if the node isn't running yet.
@ -46,17 +48,17 @@ async fn announce_profile(id: &DuskIdentity, state: &AppState) {
#[tauri::command]
pub async fn has_identity(state: State<'_, AppState>) -> Result<bool, String> {
Ok(state.storage.has_identity())
ipc_log!("has_identity", Ok(state.storage.has_identity()))
}
#[tauri::command]
pub async fn load_identity(state: State<'_, AppState>) -> Result<Option<PublicIdentity>, String> {
ipc_log!("load_identity", {
let mut identity = state.identity.lock().await;
if identity.is_some() {
return Ok(identity.as_ref().map(|id| id.public_identity()));
}
Ok(identity.as_ref().map(|id| id.public_identity()))
} else {
match DuskIdentity::load(&state.storage) {
Ok(loaded) => {
let public = loaded.public_identity();
@ -66,6 +68,8 @@ pub async fn load_identity(state: State<'_, AppState>) -> Result<Option<PublicId
Err(_) => Ok(None),
}
}
})
}
#[tauri::command]
pub async fn create_identity(
@ -74,14 +78,15 @@ pub async fn create_identity(
bio: Option<String>,
challenge_data: Option<ChallengeSubmission>,
) -> Result<PublicIdentity, String> {
ipc_log!("create_identity", {
// require challenge data and re-validate behavioral analysis in rust
let challenge = challenge_data.ok_or("verification required")?;
let result = verification::analyze_challenge(&challenge);
if !result.is_human {
return Err("verification failed".to_string());
}
let mut new_identity = DuskIdentity::generate(&display_name, &bio.unwrap_or_default());
Err("verification failed".to_string())
} else {
let mut new_identity =
DuskIdentity::generate(&display_name, &bio.unwrap_or_default());
// generate a cryptographic proof binding the verification to this keypair
let proof = verification::generate_proof(
@ -112,9 +117,12 @@ pub async fn create_identity(
Ok(public)
}
})
}
#[tauri::command]
pub async fn update_display_name(state: State<'_, AppState>, name: String) -> Result<(), String> {
ipc_log!("update_display_name", {
let mut identity = state.identity.lock().await;
let id = identity.as_mut().ok_or("no identity loaded")?;
@ -124,6 +132,7 @@ pub async fn update_display_name(state: State<'_, AppState>, name: String) -> Re
announce_profile(id, &state).await;
Ok(())
})
}
#[tauri::command]
@ -132,6 +141,7 @@ pub async fn update_profile(
display_name: String,
bio: String,
) -> Result<PublicIdentity, String> {
ipc_log!("update_profile", {
let mut identity = state.identity.lock().await;
let id = identity.as_mut().ok_or("no identity loaded")?;
@ -145,14 +155,17 @@ pub async fn update_profile(
announce_profile(id, &state).await;
Ok(public)
})
}
#[tauri::command]
pub async fn load_settings(state: State<'_, AppState>) -> Result<UserSettings, String> {
ipc_log!("load_settings", {
state
.storage
.load_settings()
.map_err(|e| format!("failed to load settings: {}", e))
})
}
#[tauri::command]
@ -160,6 +173,7 @@ pub async fn save_settings(
state: State<'_, AppState>,
settings: UserSettings,
) -> Result<(), String> {
ipc_log!("save_settings", {
// check if status changed so we can broadcast the new presence
let old_status = state
.storage
@ -214,12 +228,14 @@ pub async fn save_settings(
.storage
.save_settings(&settings)
.map_err(|e| format!("failed to save settings: {}", e))
})
}
// -- user directory commands --
#[tauri::command]
pub async fn get_known_peers(state: State<'_, AppState>) -> Result<Vec<DirectoryEntry>, String> {
ipc_log!("get_known_peers", {
let entries = state
.storage
.load_directory()
@ -229,6 +245,7 @@ pub async fn get_known_peers(state: State<'_, AppState>) -> Result<Vec<Directory
// sort by last seen (most recent first)
peers.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
Ok(peers)
})
}
#[tauri::command]
@ -236,6 +253,7 @@ pub async fn search_directory(
state: State<'_, AppState>,
query: String,
) -> Result<Vec<DirectoryEntry>, String> {
ipc_log!("search_directory", {
let entries = state
.storage
.load_directory()
@ -252,10 +270,12 @@ pub async fn search_directory(
results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen));
Ok(results)
})
}
#[tauri::command]
pub async fn get_friends(state: State<'_, AppState>) -> Result<Vec<DirectoryEntry>, String> {
ipc_log!("get_friends", {
let entries = state
.storage
.load_directory()
@ -272,14 +292,17 @@ pub async fn get_friends(state: State<'_, AppState>) -> Result<Vec<DirectoryEntr
.cmp(&b.display_name.to_lowercase())
});
Ok(friends)
})
}
#[tauri::command]
pub async fn add_friend(state: State<'_, AppState>, peer_id: String) -> Result<(), String> {
ipc_log!("add_friend", {
state
.storage
.set_friend_status(&peer_id, true)
.map_err(|e| format!("failed to add friend: {}", e))
})
}
#[tauri::command]

View File

@ -1,3 +1,20 @@
// logs every tauri ipc command invocation and its result to the terminal
macro_rules! ipc_log {
($cmd:expr, $body:expr) => {{
let start = std::time::Instant::now();
log::info!("[ipc] -> {}", $cmd);
let result = $body;
let elapsed = start.elapsed();
match &result {
Ok(_) => log::info!("[ipc] <- {} ok ({:.1?})", $cmd, elapsed),
Err(e) => log::error!("[ipc] <- {} err ({:.1?}): {}", $cmd, elapsed, e),
}
result
}};
}
pub(crate) use ipc_log;
pub mod chat;
pub mod community;
pub mod dm;

View File

@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core";
import { invoke as tauriInvoke } from "@tauri-apps/api/core";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import type {
PublicIdentity,
@ -19,6 +19,42 @@ import type {
GifResponse,
} from "./types";
// wrapped invoke that logs all ipc calls and errors
async function invoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
const startTime = performance.now();
const logPrefix = `[ipc] ${cmd}`;
// log the call with args (redacting sensitive data)
const safeArgs = args ? redactSensitiveArgs(args) : undefined;
console.log(`${logPrefix} called`, safeArgs ? { args: safeArgs } : "");
try {
const result = await tauriInvoke<T>(cmd, args);
const duration = (performance.now() - startTime).toFixed(2);
console.log(`${logPrefix} success (${duration}ms)`, result);
return result;
} catch (error) {
const duration = (performance.now() - startTime).toFixed(2);
console.error(`${logPrefix} error (${duration}ms)`, error);
throw error;
}
}
// redact potentially sensitive values from args before logging
function redactSensitiveArgs(args: Record<string, unknown>): Record<string, unknown> {
const sensitiveKeys = ["password", "secret", "token", "key", "credential", "private"];
const redacted = { ...args };
for (const key of Object.keys(redacted)) {
const lowerKey = key.toLowerCase();
if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) {
redacted[key] = "[REDACTED]";
}
}
return redacted;
}
// -- identity --
export async function hasIdentity(): Promise<boolean> {