fix(desktop): stop sidecars before updater install

This commit is contained in:
Relakkes Yang 2026-04-25 13:21:31 +08:00
parent 0ef020ebf8
commit 55c5958b55
5 changed files with 67 additions and 9 deletions

View File

@ -55,7 +55,7 @@ jobs:
# Windows x64
- platform: windows-latest
rust_target: x86_64-pc-windows-msvc
tauri_args: '--bundles msi'
tauri_args: '--bundles nsis'
label: Windows-x64
asset_name_pattern: 'Claude-Code-Haha_[version]_windows_x64_[bundle][ext]'
@ -176,6 +176,6 @@ jobs:
releaseBody: ${{ steps.release_notes.outputs.body }}
releaseDraft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }}
prerelease: false
updaterJsonPreferNsis: false
updaterJsonPreferNsis: true
releaseAssetNamePattern: ${{ matrix.asset_name_pattern }}
args: ${{ matrix.tauri_args }} --config src-tauri/tauri.release-ci.json

View File

@ -112,6 +112,22 @@ fn restart_adapters_sidecar(app: AppHandle) -> Result<(), String> {
Ok(())
}
#[tauri::command]
fn prepare_for_update_install(app: AppHandle) -> Result<(), String> {
stop_server_sidecar(&app);
stop_adapters_sidecar(&app);
#[cfg(target_os = "windows")]
{
kill_windows_sidecars();
}
// Give Windows a short moment to release executable file handles before the
// updater starts replacing bundled sidecars in the install directory.
std::thread::sleep(Duration::from_millis(750));
Ok(())
}
#[tauri::command]
fn terminal_spawn(
app: AppHandle,
@ -721,6 +737,21 @@ fn stop_adapters_sidecar(app: &AppHandle) {
}
}
#[cfg(target_os = "windows")]
fn kill_windows_sidecars() {
for image_name in [
"claude-sidecar-x86_64-pc-windows-msvc.exe",
"claude-sidecar-aarch64-pc-windows-msvc.exe",
"claude-sidecar.exe",
] {
let _ = StdCommand::new("taskkill")
.args(["/F", "/T", "/IM", image_name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
#[cfg(test)]
mod tests {
use super::{decode_terminal_output, default_utf8_locale, ensure_utf8_locale, parse_env_block};
@ -817,6 +848,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
get_server_url,
restart_adapters_sidecar,
prepare_for_update_install,
terminal_spawn,
terminal_write,
terminal_resize,

View File

@ -1,6 +1,8 @@
!macro NSIS_HOOK_PREINSTALL
DetailPrint "Stopping running Claude Code Haha processes..."
nsExec::ExecToLog 'taskkill /F /T /IM claude-code-desktop.exe'
DetailPrint "Stopping running Claude Code Haha sidecars..."
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar-x86_64-pc-windows-msvc.exe'
Pop $0
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar-aarch64-pc-windows-msvc.exe'
Pop $0
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar.exe'
Pop $0
@ -11,6 +13,10 @@
DetailPrint "Stopping running Claude Code Haha processes..."
nsExec::ExecToLog 'taskkill /F /T /IM claude-code-desktop.exe'
Pop $0
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar-x86_64-pc-windows-msvc.exe'
Pop $0
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar-aarch64-pc-windows-msvc.exe'
Pop $0
nsExec::ExecToLog 'taskkill /F /T /IM claude-sidecar.exe'
Pop $0
Sleep 1000

View File

@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const check = vi.fn()
const relaunch = vi.fn()
const invoke = vi.fn()
vi.mock('@tauri-apps/plugin-updater', () => ({
check,
@ -11,10 +12,15 @@ vi.mock('@tauri-apps/plugin-process', () => ({
relaunch,
}))
vi.mock('@tauri-apps/api/core', () => ({
invoke,
}))
describe('updateStore', () => {
beforeEach(() => {
check.mockReset()
relaunch.mockReset()
invoke.mockReset()
window.localStorage.clear()
Object.defineProperty(window, '__TAURI_INTERNALS__', {
configurable: true,
@ -89,20 +95,23 @@ describe('updateStore', () => {
expect(useUpdateStore.getState().shouldPrompt).toBe(true)
})
it('computes download progress from content length and relaunches after install', async () => {
const downloadAndInstall = vi.fn(async (onEvent?: (event: unknown) => void) => {
it('downloads, stops sidecars, installs, and relaunches', async () => {
const download = vi.fn(async (onEvent?: (event: unknown) => void) => {
onEvent?.({ event: 'Started', data: { contentLength: 200 } })
onEvent?.({ event: 'Progress', data: { chunkLength: 50 } })
onEvent?.({ event: 'Progress', data: { chunkLength: 150 } })
onEvent?.({ event: 'Finished' })
})
const install = vi.fn().mockResolvedValue(undefined)
check.mockResolvedValue({
version: '0.2.0',
body: 'Notes',
downloadAndInstall,
download,
install,
close: vi.fn().mockResolvedValue(undefined),
})
invoke.mockResolvedValue(undefined)
relaunch.mockResolvedValue(undefined)
vi.resetModules()
@ -111,7 +120,14 @@ describe('updateStore', () => {
await useUpdateStore.getState().checkForUpdates()
await useUpdateStore.getState().installUpdate()
expect(downloadAndInstall).toHaveBeenCalledTimes(1)
expect(download).toHaveBeenCalledTimes(1)
expect(invoke).toHaveBeenCalledWith('prepare_for_update_install')
expect(install).toHaveBeenCalledTimes(1)
const prepareCallOrder = invoke.mock.invocationCallOrder[0]
const installCallOrder = install.mock.invocationCallOrder[0]
expect(prepareCallOrder).toBeDefined()
expect(installCallOrder).toBeDefined()
expect(prepareCallOrder!).toBeLessThan(installCallOrder!)
expect(useUpdateStore.getState().progressPercent).toBe(100)
expect(useUpdateStore.getState().status).toBe('restarting')
expect(relaunch).toHaveBeenCalledTimes(1)

View File

@ -191,11 +191,12 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
try {
writeDismissedUpdateVersion(null)
const { invoke } = await import('@tauri-apps/api/core')
const { relaunch } = await import('@tauri-apps/plugin-process')
let totalBytes: number | null = null
let downloadedBytes = 0
await update.downloadAndInstall((event) => {
await update.download((event) => {
if (event.event === 'Started') {
totalBytes = event.data.contentLength ?? null
downloadedBytes = 0
@ -226,6 +227,9 @@ export const useUpdateStore = create<UpdateStore>((set, get) => ({
}
})
await invoke('prepare_for_update_install')
await update.install()
set((state) => ({
...state,
status: 'restarting',