chore: stream app logs to log window (#5019)
* chore: stream app logs to log window * chore: remove unused states
This commit is contained in:
parent
2ae7417e10
commit
28c7e0d105
@ -1,15 +1,10 @@
|
||||
use rmcp::model::{CallToolRequestParam, CallToolResult, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::{fs, path::PathBuf};
|
||||
use tauri::{AppHandle, Manager, Runtime, State};
|
||||
|
||||
use super::{server, setup, state::AppState};
|
||||
|
||||
const CONFIGURATION_FILE_NAME: &str = "settings.json";
|
||||
const DEFAULT_MCP_CONFIG: &str = r#"{
|
||||
"mcpServers": {}
|
||||
}"#;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AppConfiguration {
|
||||
@ -296,100 +291,13 @@ pub async fn stop_server() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves all available tools from all MCP servers
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - Application state containing MCP server connections
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<Tool>, String>` - A vector of all tools if successful, or an error message if failed
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Locks the MCP servers mutex to access server connections
|
||||
/// 2. Iterates through all connected servers
|
||||
/// 3. Gets the list of tools from each server
|
||||
/// 4. Combines all tools into a single vector
|
||||
/// 5. Returns the combined list of all available tools
|
||||
#[tauri::command]
|
||||
pub async fn get_tools(state: State<'_, AppState>) -> Result<Vec<Tool>, String> {
|
||||
let servers = state.mcp_servers.lock().await;
|
||||
let mut all_tools: Vec<Tool> = Vec::new();
|
||||
|
||||
for (_, service) in servers.iter() {
|
||||
// List tools
|
||||
let tools = service.list_all_tools().await.map_err(|e| e.to_string())?;
|
||||
|
||||
for tool in tools {
|
||||
all_tools.push(tool);
|
||||
}
|
||||
pub async fn read_logs(app: AppHandle) -> Result<String, String> {
|
||||
let log_path = get_jan_data_folder_path(app).join("logs").join("app.log");
|
||||
if log_path.exists() {
|
||||
let content = fs::read_to_string(log_path).map_err(|e| e.to_string())?;
|
||||
Ok(content)
|
||||
} else {
|
||||
Err(format!("Log file not found"))
|
||||
}
|
||||
|
||||
Ok(all_tools)
|
||||
}
|
||||
|
||||
/// Calls a tool on an MCP server by name with optional arguments
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - Application state containing MCP server connections
|
||||
/// * `tool_name` - Name of the tool to call
|
||||
/// * `arguments` - Optional map of argument names to values
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<CallToolResult, String>` - Result of the tool call if successful, or error message if failed
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Locks the MCP servers mutex to access server connections
|
||||
/// 2. Searches through all servers for one containing the named tool
|
||||
/// 3. When found, calls the tool on that server with the provided arguments
|
||||
/// 4. Returns error if no server has the requested tool
|
||||
#[tauri::command]
|
||||
pub async fn call_tool(
|
||||
state: State<'_, AppState>,
|
||||
tool_name: String,
|
||||
arguments: Option<Map<String, Value>>,
|
||||
) -> Result<CallToolResult, String> {
|
||||
let servers = state.mcp_servers.lock().await;
|
||||
|
||||
// Iterate through servers and find the first one that contains the tool
|
||||
for (_, service) in servers.iter() {
|
||||
if let Ok(tools) = service.list_all_tools().await {
|
||||
if tools.iter().any(|t| t.name == tool_name) {
|
||||
return service
|
||||
.call_tool(CallToolRequestParam {
|
||||
name: tool_name.into(),
|
||||
arguments,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Tool {} not found", tool_name))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_mcp_configs(app: AppHandle) -> Result<String, String> {
|
||||
let mut path = get_jan_data_folder_path(app);
|
||||
path.push("mcp_config.json");
|
||||
log::info!("read mcp configs, path: {:?}", path);
|
||||
|
||||
// Create default empty config if file doesn't exist
|
||||
if !path.exists() {
|
||||
log::info!("mcp_config.json not found, creating default empty config");
|
||||
fs::write(&path, DEFAULT_MCP_CONFIG)
|
||||
.map_err(|e| format!("Failed to create default MCP config: {}", e))?;
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
return Ok(contents);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> {
|
||||
let mut path = get_jan_data_folder_path(app);
|
||||
path.push("mcp_config.json");
|
||||
log::info!("save mcp configs, path: {:?}", path);
|
||||
|
||||
fs::write(path, configs).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
use std::{collections::HashMap, env, sync::Arc};
|
||||
|
||||
use rmcp::model::{CallToolRequestParam, CallToolResult, Tool};
|
||||
use rmcp::{service::RunningService, transport::TokioChildProcess, RoleClient, ServiceExt};
|
||||
use serde_json::Value;
|
||||
use serde_json::{Map, Value};
|
||||
use tauri::{AppHandle, Emitter, Runtime, State};
|
||||
use tokio::{process::Command, sync::Mutex};
|
||||
use std::{fs};
|
||||
|
||||
use super::{cmd::get_jan_data_folder_path, state::AppState};
|
||||
|
||||
const DEFAULT_MCP_CONFIG: &str = r#"{
|
||||
"mcpServers": {}
|
||||
}"#;
|
||||
|
||||
|
||||
/// Runs MCP commands by reading configuration from a JSON file and initializing servers
|
||||
///
|
||||
/// # Arguments
|
||||
@ -175,6 +182,104 @@ pub async fn get_connected_servers(
|
||||
Ok(servers_map.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// Retrieves all available tools from all MCP servers
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - Application state containing MCP server connections
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<Vec<Tool>, String>` - A vector of all tools if successful, or an error message if failed
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Locks the MCP servers mutex to access server connections
|
||||
/// 2. Iterates through all connected servers
|
||||
/// 3. Gets the list of tools from each server
|
||||
/// 4. Combines all tools into a single vector
|
||||
/// 5. Returns the combined list of all available tools
|
||||
#[tauri::command]
|
||||
pub async fn get_tools(state: State<'_, AppState>) -> Result<Vec<Tool>, String> {
|
||||
let servers = state.mcp_servers.lock().await;
|
||||
let mut all_tools: Vec<Tool> = Vec::new();
|
||||
|
||||
for (_, service) in servers.iter() {
|
||||
// List tools
|
||||
let tools = service.list_all_tools().await.map_err(|e| e.to_string())?;
|
||||
|
||||
for tool in tools {
|
||||
all_tools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_tools)
|
||||
}
|
||||
|
||||
/// Calls a tool on an MCP server by name with optional arguments
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `state` - Application state containing MCP server connections
|
||||
/// * `tool_name` - Name of the tool to call
|
||||
/// * `arguments` - Optional map of argument names to values
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Result<CallToolResult, String>` - Result of the tool call if successful, or error message if failed
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Locks the MCP servers mutex to access server connections
|
||||
/// 2. Searches through all servers for one containing the named tool
|
||||
/// 3. When found, calls the tool on that server with the provided arguments
|
||||
/// 4. Returns error if no server has the requested tool
|
||||
#[tauri::command]
|
||||
pub async fn call_tool(
|
||||
state: State<'_, AppState>,
|
||||
tool_name: String,
|
||||
arguments: Option<Map<String, Value>>,
|
||||
) -> Result<CallToolResult, String> {
|
||||
let servers = state.mcp_servers.lock().await;
|
||||
|
||||
// Iterate through servers and find the first one that contains the tool
|
||||
for (_, service) in servers.iter() {
|
||||
if let Ok(tools) = service.list_all_tools().await {
|
||||
if tools.iter().any(|t| t.name == tool_name) {
|
||||
return service
|
||||
.call_tool(CallToolRequestParam {
|
||||
name: tool_name.into(),
|
||||
arguments,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Tool {} not found", tool_name))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_mcp_configs(app: AppHandle) -> Result<String, String> {
|
||||
let mut path = get_jan_data_folder_path(app);
|
||||
path.push("mcp_config.json");
|
||||
log::info!("read mcp configs, path: {:?}", path);
|
||||
|
||||
// Create default empty config if file doesn't exist
|
||||
if !path.exists() {
|
||||
log::info!("mcp_config.json not found, creating default empty config");
|
||||
fs::write(&path, DEFAULT_MCP_CONFIG)
|
||||
.map_err(|e| format!("Failed to create default MCP config: {}", e))?;
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(path).map_err(|e| e.to_string())?;
|
||||
return Ok(contents);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> {
|
||||
let mut path = get_jan_data_folder_path(app);
|
||||
path.push("mcp_config.json");
|
||||
log::info!("save mcp configs, path: {:?}", path);
|
||||
|
||||
fs::write(path, configs).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@ -44,13 +44,14 @@ pub fn run() {
|
||||
core::cmd::app_token,
|
||||
core::cmd::start_server,
|
||||
core::cmd::stop_server,
|
||||
core::cmd::save_mcp_configs,
|
||||
core::cmd::get_mcp_configs,
|
||||
core::cmd::read_logs,
|
||||
// MCP commands
|
||||
core::cmd::get_tools,
|
||||
core::cmd::call_tool,
|
||||
core::mcp::get_tools,
|
||||
core::mcp::call_tool,
|
||||
core::mcp::restart_mcp_servers,
|
||||
core::mcp::get_connected_servers,
|
||||
core::mcp::save_mcp_configs,
|
||||
core::mcp::get_mcp_configs,
|
||||
// Threads
|
||||
core::threads::list_threads,
|
||||
core::threads::create_thread,
|
||||
@ -76,14 +77,14 @@ pub fn run() {
|
||||
.setup(|app| {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.targets([if cfg!(debug_assertions) {
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout)
|
||||
} else {
|
||||
.targets([
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview),
|
||||
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Folder {
|
||||
path: get_jan_data_folder_path(app.handle().clone()).join("logs"),
|
||||
file_name: Some("app".to_string()),
|
||||
})
|
||||
}])
|
||||
}),
|
||||
])
|
||||
.build(),
|
||||
)?;
|
||||
// Install extensions
|
||||
|
||||
@ -6,6 +6,8 @@ type AppState = {
|
||||
streamingContent?: ThreadMessage
|
||||
loadingModel?: boolean
|
||||
tools: MCPTool[]
|
||||
serverStatus: 'running' | 'stopped' | 'pending'
|
||||
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
|
||||
updateStreamingContent: (content: ThreadMessage | undefined) => void
|
||||
updateLoadingModel: (loading: boolean) => void
|
||||
updateTools: (tools: MCPTool[]) => void
|
||||
@ -15,6 +17,7 @@ export const useAppState = create<AppState>()((set) => ({
|
||||
streamingContent: undefined,
|
||||
loadingModel: false,
|
||||
tools: [],
|
||||
serverStatus: 'stopped',
|
||||
updateStreamingContent: (content) => {
|
||||
set({ streamingContent: content })
|
||||
},
|
||||
@ -24,4 +27,5 @@ export const useAppState = create<AppState>()((set) => ({
|
||||
updateTools: (tools) => {
|
||||
set({ tools })
|
||||
},
|
||||
setServerStatus: (value) => set({ serverStatus: value }),
|
||||
}))
|
||||
|
||||
@ -21,9 +21,6 @@ type LocalApiServerState = {
|
||||
// Verbose server logs
|
||||
verboseLogs: boolean
|
||||
setVerboseLogs: (value: boolean) => void
|
||||
// Server status
|
||||
serverStatus: 'running' | 'stopped' | 'pending'
|
||||
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
|
||||
}
|
||||
|
||||
export const useLocalApiServer = create<LocalApiServerState>()(
|
||||
@ -41,8 +38,6 @@ export const useLocalApiServer = create<LocalApiServerState>()(
|
||||
setCorsEnabled: (value) => set({ corsEnabled: value }),
|
||||
verboseLogs: true,
|
||||
setVerboseLogs: (value) => set({ verboseLogs: value }),
|
||||
serverStatus: 'stopped',
|
||||
setServerStatus: (value) => set({ serverStatus: value }),
|
||||
}),
|
||||
{
|
||||
name: localStoregeKey.settingLocalApiServer,
|
||||
|
||||
@ -26,6 +26,7 @@ export const AppRoutes = [
|
||||
'getMcpConfigs',
|
||||
'restartMcpServers',
|
||||
'getConnectedServers',
|
||||
'readLogs',
|
||||
]
|
||||
// Define API routes based on different route types
|
||||
export const Routes = [...CoreRoutes, ...APIRoutes, ...AppRoutes].map((r) => ({
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ExtensionManager } from '@/lib/extension'
|
||||
import { APIs } from '@/lib/service'
|
||||
import { EventEmitter } from '@/services/eventsService'
|
||||
import { EventEmitter } from '@/services/events'
|
||||
import { EngineManager, ModelManager } from '@janhq/core'
|
||||
import { PropsWithChildren, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import { createFileRoute } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { parseLogLine, readLogs } from '@/services/app'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.localApiServerlogs as any)({
|
||||
@ -12,72 +14,36 @@ export const Route = createFileRoute(route.localApiServerlogs as any)({
|
||||
interface LogEntry {
|
||||
timestamp: string
|
||||
level: 'info' | 'warn' | 'error' | 'debug'
|
||||
target: string
|
||||
message: string
|
||||
}
|
||||
|
||||
// Generate dummy log data
|
||||
const generateDummyLogs = (): LogEntry[] => {
|
||||
const logs: LogEntry[] = []
|
||||
const levels: ('info' | 'warn' | 'error' | 'debug')[] = [
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
'debug',
|
||||
]
|
||||
const messages = [
|
||||
'Server started on port 3000',
|
||||
'Received request: GET /api/v1/models',
|
||||
'Processing request...',
|
||||
'Request completed in 120ms',
|
||||
'Connection established with client',
|
||||
'Authentication successful for user',
|
||||
'Failed to connect to database',
|
||||
'API rate limit exceeded',
|
||||
'Memory usage: 256MB',
|
||||
'CPU usage: 45%',
|
||||
'Websocket connection closed',
|
||||
'Cache miss for key: model_list',
|
||||
'Updating configuration...',
|
||||
'Configuration updated successfully',
|
||||
'Initializing model...',
|
||||
]
|
||||
|
||||
// Generate 50 log entries
|
||||
const now = new Date()
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const timestamp = new Date(now.getTime() - (50 - i) * 30000) // 30 seconds apart
|
||||
logs.push({
|
||||
timestamp: timestamp.toISOString(),
|
||||
level: levels[Math.floor(Math.random() * levels.length)],
|
||||
message: messages[Math.floor(Math.random() * messages.length)],
|
||||
})
|
||||
}
|
||||
|
||||
return logs
|
||||
}
|
||||
const SERVER_LOG_TARGET = 'app_lib::core::server'
|
||||
const LOG_EVENT_NAME = 'log://log'
|
||||
|
||||
function LogsViewer() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
// Load dummy logs when component mounts
|
||||
setLogs(generateDummyLogs())
|
||||
|
||||
// Simulate new logs coming in every 5 seconds
|
||||
const interval = setInterval(() => {
|
||||
setLogs((currentLogs) => {
|
||||
const newLog: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level: ['info', 'warn', 'error', 'debug'][
|
||||
Math.floor(Math.random() * 4)
|
||||
] as 'info' | 'warn' | 'error' | 'debug',
|
||||
message: `New activity at ${new Date().toLocaleTimeString()}`,
|
||||
}
|
||||
return [...currentLogs, newLog]
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
readLogs().then((logData) => {
|
||||
const logs = logData
|
||||
.filter((log) => log?.target === SERVER_LOG_TARGET)
|
||||
.filter(Boolean) as LogEntry[]
|
||||
setLogs(logs)
|
||||
})
|
||||
let unsubscribe = () => {}
|
||||
listen(LOG_EVENT_NAME, (event) => {
|
||||
const { message } = event.payload as { message: string }
|
||||
const log: LogEntry | undefined = parseLogLine(message)
|
||||
if (log?.target === SERVER_LOG_TARGET) {
|
||||
setLogs((prevLogs) => [...prevLogs, log])
|
||||
}
|
||||
}).then((unsub) => {
|
||||
unsubscribe = unsub
|
||||
})
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Function to get appropriate color for log level
|
||||
|
||||
@ -8,6 +8,17 @@ import { Card, CardItem } from '@/containers/Card'
|
||||
import LanguageSwitcher from '@/containers/LanguageSwitcher'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { factoryReset } from '@/services/app'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.settings.general as any)({
|
||||
@ -18,6 +29,11 @@ function General() {
|
||||
const { t } = useTranslation()
|
||||
const { spellCheckChatInput, setSpellCheckChatInput } = useGeneralSetting()
|
||||
|
||||
const resetApp = async () => {
|
||||
// TODO: Loading indicator
|
||||
await factoryReset()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<HeaderPage>
|
||||
@ -95,9 +111,42 @@ function General() {
|
||||
ns: 'settings',
|
||||
})}
|
||||
actions={
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Factory Reset</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to reset the app to factory
|
||||
settings? This action is irreversible and recommended
|
||||
only if the application is corrupted.
|
||||
</DialogDescription>
|
||||
<DialogFooter className="mt-2 flex items-center">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="hover:no-underline"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => resetApp()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@ -11,6 +11,7 @@ import { PortInput } from '@/containers/PortInput'
|
||||
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
|
||||
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.settings.local_api_server as any)({
|
||||
@ -27,10 +28,10 @@ function LocalAPIServer() {
|
||||
serverHost,
|
||||
serverPort,
|
||||
apiPrefix,
|
||||
serverStatus,
|
||||
setServerStatus,
|
||||
} = useLocalApiServer()
|
||||
|
||||
const { serverStatus, setServerStatus } = useAppState()
|
||||
|
||||
const toggleAPIServer = async () => {
|
||||
setServerStatus('pending')
|
||||
if (serverStatus === 'stopped') {
|
||||
@ -115,8 +116,7 @@ function LocalAPIServer() {
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={toggleAPIServer}>
|
||||
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`}{' '}
|
||||
Server
|
||||
{`${serverStatus === 'running' ? 'Stop' : 'Start'}`} Server
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
52
web-app/src/services/app.ts
Normal file
52
web-app/src/services/app.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { AppConfiguration, fs } from '@janhq/core'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/**
|
||||
* @description This function is used to reset the app to its factory settings.
|
||||
* It will remove all the data from the app, including the data folder and local storage.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const factoryReset = async () => {
|
||||
const appConfiguration: AppConfiguration | undefined =
|
||||
await window.core?.api?.getAppConfigurations()
|
||||
|
||||
const janDataFolderPath = appConfiguration?.data_folder
|
||||
if (janDataFolderPath) await fs.rm(janDataFolderPath)
|
||||
window.localStorage.clear()
|
||||
await window.core?.api?.installExtensions()
|
||||
await window.core?.api?.relaunch()
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to read the logs from the app.
|
||||
* It will return the logs as a string.
|
||||
* @returns
|
||||
*/
|
||||
export const readLogs = async () => {
|
||||
const logData: string = (await invoke('read_logs')) ?? ''
|
||||
return logData.split('\n').map(parseLogLine)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This function is used to parse a log line.
|
||||
* It will return the log line as an object.
|
||||
* @param line
|
||||
* @returns
|
||||
*/
|
||||
export const parseLogLine = (line: string) => {
|
||||
const regex = /^\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]\s(.*)$/
|
||||
const match = line.match(regex)
|
||||
|
||||
if (!match) return undefined // Skip invalid lines
|
||||
|
||||
const [, date, time, target, levelRaw, message] = match
|
||||
|
||||
const level = levelRaw.toLowerCase() as 'info' | 'warn' | 'error' | 'debug'
|
||||
|
||||
return {
|
||||
timestamp: `${date} ${time}`,
|
||||
level,
|
||||
target,
|
||||
message,
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user