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 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<R: Runtime>(app_handle: tauri::AppHandle<R>) -> PathBuf {
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 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();

View File

@ -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,

View File

@ -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<string | undefined>()
const [isCopied, setIsCopied] = useState(false)
const [selectedNewPath, setSelectedNewPath] = useState<string | null>(null)
@ -91,6 +93,7 @@ function General() {
}, [])
const resetApp = async () => {
pausePolling()
// TODO: Loading indicator
await factoryReset()
}

View File

@ -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()
})
})
})
})

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 { 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')
}
/**