fix: factory reset fail with access denied error (#5952)

* fix: factory reset fail due to access denied error

* fix: unused import

* fix: tests
This commit is contained in:
Louis 2025-07-28 23:20:45 +07:00 committed by GitHub
parent 07421d7f53
commit 812a8082b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 54 deletions

View File

@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use std::{fs, io, path::PathBuf}; use std::{fs, io, path::PathBuf};
use tauri::{AppHandle, Manager, Runtime, State}; use tauri::{AppHandle, Manager, Runtime, State};
use crate::core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
use super::{server, setup, state::AppState}; use super::{server, setup, state::AppState};
const CONFIGURATION_FILE_NAME: &str = "settings.json"; 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") 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] #[tauri::command]
pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf { pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| { let app_path = app_handle.path().app_data_dir().unwrap_or_else(|err| {

View File

@ -199,7 +199,7 @@ pub fn setup_mcp(app: &App) {
let state = app.state::<AppState>(); let state = app.state::<AppState>();
let servers = state.mcp_servers.clone(); let servers = state.mcp_servers.clone();
let app_handle: tauri::AppHandle = app.handle().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(); let app_handle_for_kill = app_handle.clone();
app_handle.listen("kill-mcp-servers", move |_event| { app_handle.listen("kill-mcp-servers", move |_event| {
let app_handle = app_handle_for_kill.clone(); let app_handle = app_handle_for_kill.clone();

View File

@ -1,4 +1,5 @@
mod core; mod core;
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
use core::{ use core::{
cmd::get_jan_data_folder_path, cmd::get_jan_data_folder_path,
setup::{self, setup_mcp}, setup::{self, setup_mcp},
@ -8,7 +9,6 @@ use core::{
use reqwest::Client; use reqwest::Client;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
use core::utils::extensions::inference_llamacpp_extension::cleanup::cleanup_processes;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@ -58,6 +58,7 @@ pub fn run() {
core::cmd::get_server_status, core::cmd::get_server_status,
core::cmd::read_logs, core::cmd::read_logs,
core::cmd::change_app_data_folder, core::cmd::change_app_data_folder,
core::cmd::factory_reset,
// MCP commands // MCP commands
core::mcp::get_tools, core::mcp::get_tools,
core::mcp::call_tool, core::mcp::call_tool,

View File

@ -45,6 +45,7 @@ import { emit } from '@tauri-apps/api/event'
import { stopAllModels } from '@/services/models' import { stopAllModels } from '@/services/models'
import { SystemEvent } from '@/types/events' import { SystemEvent } from '@/types/events'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { useHardware } from '@/hooks/useHardware'
import { getConnectedServers } from '@/services/mcp' import { getConnectedServers } from '@/services/mcp'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { useMCPServers } from '@/hooks/useMCPServers' import { useMCPServers } from '@/hooks/useMCPServers'
@ -75,6 +76,7 @@ function General() {
} }
} }
const { checkForUpdate } = useAppUpdater() const { checkForUpdate } = useAppUpdater()
const { pausePolling } = useHardware()
const [janDataFolder, setJanDataFolder] = useState<string | undefined>() const [janDataFolder, setJanDataFolder] = useState<string | undefined>()
const [isCopied, setIsCopied] = useState(false) const [isCopied, setIsCopied] = useState(false)
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null) const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
@ -91,6 +93,7 @@ function General() {
}, []) }, [])
const resetApp = async () => { const resetApp = async () => {
pausePolling()
// TODO: Loading indicator // TODO: Loading indicator
await factoryReset() await factoryReset()
} }

View File

@ -4,26 +4,26 @@ import {
readLogs, readLogs,
parseLogLine, parseLogLine,
getJanDataFolder, getJanDataFolder,
relocateJanDataFolder relocateJanDataFolder,
} from '../app' } from '../app'
// Mock dependencies // Mock dependencies
vi.mock('@tauri-apps/api/core', () => ({ vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn() invoke: vi.fn(),
})) }))
vi.mock('@tauri-apps/api/event', () => ({ vi.mock('@tauri-apps/api/event', () => ({
emit: vi.fn() emit: vi.fn(),
})) }))
vi.mock('../models', () => ({ vi.mock('../models', () => ({
stopAllModels: vi.fn() stopAllModels: vi.fn(),
})) }))
vi.mock('@janhq/core', () => ({ vi.mock('@janhq/core', () => ({
fs: { fs: {
rm: vi.fn() rm: vi.fn(),
} },
})) }))
// Mock the global window object // Mock the global window object
@ -33,22 +33,22 @@ const mockWindow = {
installExtensions: vi.fn(), installExtensions: vi.fn(),
relaunch: vi.fn(), relaunch: vi.fn(),
getAppConfigurations: vi.fn(), getAppConfigurations: vi.fn(),
changeAppDataFolder: vi.fn() changeAppDataFolder: vi.fn(),
} },
}, },
localStorage: { localStorage: {
clear: vi.fn() clear: vi.fn(),
} },
} }
Object.defineProperty(window, 'core', { Object.defineProperty(window, 'core', {
value: mockWindow.core, value: mockWindow.core,
writable: true writable: true,
}) })
Object.defineProperty(window, 'localStorage', { Object.defineProperty(window, 'localStorage', {
value: mockWindow.localStorage, value: mockWindow.localStorage,
writable: true writable: true,
}) })
describe('app service', () => { describe('app service', () => {
@ -65,7 +65,7 @@ describe('app service', () => {
timestamp: '2024-01-01 10:00:00Z', timestamp: '2024-01-01 10:00:00Z',
level: 'info', level: 'info',
target: 'target', target: 'target',
message: 'Test message' message: 'Test message',
}) })
}) })
@ -83,7 +83,8 @@ describe('app service', () => {
describe('readLogs', () => { describe('readLogs', () => {
it('should read and parse logs', async () => { it('should read and parse logs', async () => {
const { invoke } = await import('@tauri-apps/api/core') 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) vi.mocked(invoke).mockResolvedValue(mockLogs)
const result = await readLogs() const result = await readLogs()
@ -123,21 +124,18 @@ describe('app service', () => {
await relocateJanDataFolder(newPath) await relocateJanDataFolder(newPath)
expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({ newDataFolder: newPath }) expect(mockWindow.core.api.changeAppDataFolder).toHaveBeenCalledWith({
newDataFolder: newPath,
})
}) })
}) })
describe('factoryReset', () => { describe('factoryReset', () => {
it('should perform factory reset', async () => { it('should perform factory reset', async () => {
const { stopAllModels } = await import('../models') const { stopAllModels } = await import('../models')
const { emit } = await import('@tauri-apps/api/event') const { invoke } = await import('@tauri-apps/api/core')
const { fs } = await import('@janhq/core')
vi.mocked(stopAllModels).mockResolvedValue() 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 // Use fake timers
vi.useFakeTimers() vi.useFakeTimers()
@ -150,8 +148,8 @@ describe('app service', () => {
await factoryResetPromise await factoryResetPromise
expect(stopAllModels).toHaveBeenCalled() expect(stopAllModels).toHaveBeenCalled()
expect(emit).toHaveBeenCalledWith('kill-sidecar')
expect(mockWindow.localStorage.clear).toHaveBeenCalled() expect(mockWindow.localStorage.clear).toHaveBeenCalled()
expect(invoke).toHaveBeenCalledWith('factory_reset')
vi.useRealTimers() vi.useRealTimers()
}) })

View File

@ -1,8 +1,6 @@
import { AppConfiguration, fs } from '@janhq/core' import { AppConfiguration } from '@janhq/core'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { emit } from '@tauri-apps/api/event'
import { stopAllModels } from './models' import { stopAllModels } from './models'
import { SystemEvent } from '@/types/events'
/** /**
* @description This function is used to reset the app to its factory settings. * @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 () => { export const factoryReset = async () => {
// Kill background processes and remove data folder // Kill background processes and remove data folder
await stopAllModels() await stopAllModels()
emit(SystemEvent.KILL_SIDECAR) window.localStorage.clear()
setTimeout(async () => { await invoke('factory_reset')
const janDataFolderPath = await getJanDataFolder()
if (janDataFolderPath) await fs.rm(janDataFolderPath)
window.localStorage.clear()
await window.core?.api?.installExtensions()
await window.core?.api?.relaunch()
}, 1000)
} }
/** /**