diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index 8bb80d2f8..ffa1b8a53 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::{fs, io, path::PathBuf}; use tauri::{AppHandle, Manager, Runtime, State}; +use crate::core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes; + use super::{server, setup, state::AppState}; const CONFIGURATION_FILE_NAME: &str = "settings.json"; @@ -104,6 +106,40 @@ pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf { get_jan_data_folder_path(app_handle).join("extensions") } +#[tauri::command] +pub fn factory_reset(app_handle: tauri::AppHandle, state: State<'_, AppState>) { + // close window + let windows = app_handle.webview_windows(); + for (label, window) in windows.iter() { + window.close().unwrap_or_else(|_| { + log::warn!("Failed to close window: {:?}", label); + }); + } + let data_folder = get_jan_data_folder_path(app_handle.clone()); + log::info!("Factory reset, removing data folder: {:?}", data_folder); + + tauri::async_runtime::block_on(async { + cleanup_processes(state).await; + + if data_folder.exists() { + if let Err(e) = fs::remove_dir_all(&data_folder) { + log::error!("Failed to remove data folder: {}", e); + return; + } + } + + // Recreate the data folder + let _ = fs::create_dir_all(&data_folder).map_err(|e| e.to_string()); + + // Reset the configuration + let mut default_config = AppConfiguration::default(); + default_config.data_folder = default_data_folder_path(app_handle.clone()); + let _ = update_app_configuration(app_handle.clone(), default_config); + + app_handle.restart(); + }); +} + #[tauri::command] pub fn get_configuration_file_path(app_handle: tauri::AppHandle) -> PathBuf { let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| { diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index a2875072c..ec39bcc49 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -199,7 +199,7 @@ pub fn setup_mcp(app: &App) { let state = app.state::(); let servers = state.mcp_servers.clone(); let app_handle: tauri::AppHandle = app.handle().clone(); - // Setup kill-mcp-servers event listener (similar to cortex kill-sidecar) + // Setup kill-mcp-servers event listener (similar to kill-sidecar) let app_handle_for_kill = app_handle.clone(); app_handle.listen("kill-mcp-servers", move |_event| { let app_handle = app_handle_for_kill.clone(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 69a512a0e..5174cd578 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod core; +use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes; use core::{ cmd::get_jan_data_folder_path, setup::{self, setup_mcp}, @@ -8,7 +9,6 @@ use core::{ use reqwest::Client; use std::{collections::HashMap, sync::Arc}; use tauri::{Emitter, Manager}; -use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes; use tokio::sync::Mutex; @@ -58,6 +58,7 @@ pub fn run() { core::cmd::get_server_status, core::cmd::read_logs, core::cmd::change_app_data_folder, + core::cmd::factory_reset, // MCP commands core::mcp::get_tools, core::mcp::call_tool, diff --git a/web-app/src/routes/settings/general.tsx b/web-app/src/routes/settings/general.tsx index d9e7c6e57..a5ce5ec26 100644 --- a/web-app/src/routes/settings/general.tsx +++ b/web-app/src/routes/settings/general.tsx @@ -45,6 +45,7 @@ import { emit } from '@tauri-apps/api/event' import { stopAllModels } from '@/services/models' import { SystemEvent } from '@/types/events' import { Input } from '@/components/ui/input' +import { useHardware } from '@/hooks/useHardware' import { getConnectedServers } from '@/services/mcp' import { invoke } from '@tauri-apps/api/core' import { useMCPServers } from '@/hooks/useMCPServers' @@ -75,6 +76,7 @@ function General() { } } const { checkForUpdate } = useAppUpdater() + const { pausePolling } = useHardware() const [janDataFolder, setJanDataFolder] = useState() const [isCopied, setIsCopied] = useState(false) const [selectedNewPath, setSelectedNewPath] = useState(null) @@ -91,6 +93,7 @@ function General() { }, []) const resetApp = async () => { + pausePolling() // TODO: Loading indicator await factoryReset() } diff --git a/web-app/src/services/__tests__/app.test.ts b/web-app/src/services/__tests__/app.test.ts index 592e5b1d1..56a591a75 100644 --- a/web-app/src/services/__tests__/app.test.ts +++ b/web-app/src/services/__tests__/app.test.ts @@ -1,29 +1,29 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { +import { factoryReset, readLogs, parseLogLine, getJanDataFolder, - relocateJanDataFolder + relocateJanDataFolder, } from '../app' // Mock dependencies vi.mock('@tauri-apps/api/core', () => ({ - invoke: vi.fn() + invoke: vi.fn(), })) vi.mock('@tauri-apps/api/event', () => ({ - emit: vi.fn() + emit: vi.fn(), })) vi.mock('../models', () => ({ - stopAllModels: vi.fn() + stopAllModels: vi.fn(), })) vi.mock('@janhq/core', () => ({ fs: { - rm: vi.fn() - } + rm: vi.fn(), + }, })) // Mock the global window object @@ -33,22 +33,22 @@ const mockWindow = { installExtensions: vi.fn(), relaunch: vi.fn(), getAppConfigurations: vi.fn(), - changeAppDataFolder: vi.fn() - } + changeAppDataFolder: vi.fn(), + }, }, localStorage: { - clear: vi.fn() - } + clear: vi.fn(), + }, } Object.defineProperty(window, 'core', { value: mockWindow.core, - writable: true + writable: true, }) Object.defineProperty(window, 'localStorage', { value: mockWindow.localStorage, - writable: true + writable: true, }) describe('app service', () => { @@ -60,19 +60,19 @@ describe('app service', () => { it('should parse valid log line', () => { const logLine = '[2024-01-01][10:00:00Z][target][INFO] Test message' const result = parseLogLine(logLine) - + expect(result).toEqual({ timestamp: '2024-01-01 10:00:00Z', level: 'info', target: 'target', - message: 'Test message' + message: 'Test message', }) }) it('should handle invalid log line format', () => { const logLine = 'Invalid log line' const result = parseLogLine(logLine) - + expect(result.message).toBe('Invalid log line') expect(result.level).toBe('info') expect(result.target).toBe('info') @@ -83,11 +83,12 @@ describe('app service', () => { describe('readLogs', () => { it('should read and parse logs', async () => { const { invoke } = await import('@tauri-apps/api/core') - const mockLogs = '[2024-01-01][10:00:00Z][target][INFO] Test message\n[2024-01-01][10:01:00Z][target][ERROR] Error message' + const mockLogs = + '[2024-01-01][10:00:00Z][target][INFO] Test message\n[2024-01-01][10:01:00Z][target][ERROR] Error message' vi.mocked(invoke).mockResolvedValue(mockLogs) - + const result = await readLogs() - + expect(invoke).toHaveBeenCalledWith('read_logs') expect(result).toHaveLength(2) expect(result[0].message).toBe('Test message') @@ -97,9 +98,9 @@ describe('app service', () => { it('should handle empty logs', async () => { const { invoke } = await import('@tauri-apps/api/core') vi.mocked(invoke).mockResolvedValue('') - + const result = await readLogs() - + expect(result).toEqual([expect.objectContaining({ message: '' })]) }) }) @@ -108,9 +109,9 @@ describe('app service', () => { it('should get jan data folder path', async () => { const mockConfig = { data_folder: '/path/to/jan/data' } mockWindow.core.api.getAppConfigurations.mockResolvedValue(mockConfig) - + const result = await getJanDataFolder() - + expect(mockWindow.core.api.getAppConfigurations).toHaveBeenCalled() expect(result).toBe('/path/to/jan/data') }) @@ -120,40 +121,37 @@ describe('app service', () => { it('should relocate jan data folder', async () => { const newPath = '/new/path/to/jan/data' mockWindow.core.api.changeAppDataFolder.mockResolvedValue(undefined) - + await relocateJanDataFolder(newPath) - - expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({ newDataFolder: newPath }) + + expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({ + newDataFolder: newPath, + }) }) }) describe('factoryReset', () => { it('should perform factory reset', async () => { const { stopAllModels } = await import('../models') - const { emit } = await import('@tauri-apps/api/event') - const { fs } = await import('@janhq/core') - + const { invoke } = await import('@tauri-apps/api/core') + vi.mocked(stopAllModels).mockResolvedValue() - mockWindow.core.api.getAppConfigurations.mockResolvedValue({ data_folder: '/path/to/jan/data' }) - vi.mocked(fs.rm).mockResolvedValue() - mockWindow.core.api.installExtensions.mockResolvedValue() - mockWindow.core.api.relaunch.mockResolvedValue() - + // Use fake timers vi.useFakeTimers() - + const factoryResetPromise = factoryReset() - + // Advance timers and run all pending timers await vi.advanceTimersByTimeAsync(1000) - + await factoryResetPromise - + expect(stopAllModels).toHaveBeenCalled() - expect(emit).toHaveBeenCalledWith('kill-sidecar') expect(mockWindow.localStorage.clear).toHaveBeenCalled() - + expect(invoke).toHaveBeenCalledWith('factory_reset') + vi.useRealTimers() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/app.ts b/web-app/src/services/app.ts index ae7cbdac5..c13e018b7 100644 --- a/web-app/src/services/app.ts +++ b/web-app/src/services/app.ts @@ -1,8 +1,6 @@ -import { AppConfiguration, fs } from '@janhq/core' +import { AppConfiguration } from '@janhq/core' import { invoke } from '@tauri-apps/api/core' -import { emit } from '@tauri-apps/api/event' import { stopAllModels } from './models' -import { SystemEvent } from '@/types/events' /** * @description This function is used to reset the app to its factory settings. @@ -12,14 +10,8 @@ import { SystemEvent } from '@/types/events' export const factoryReset = async () => { // Kill background processes and remove data folder await stopAllModels() - emit(SystemEvent.KILL_SIDECAR) - setTimeout(async () => { - const janDataFolderPath = await getJanDataFolder() - if (janDataFolderPath) await fs.rm(janDataFolderPath) - window.localStorage.clear() - await window.core?.api?.installExtensions() - await window.core?.api?.relaunch() - }, 1000) + window.localStorage.clear() + await invoke('factory_reset') } /**