Skip to content

Commit c599972

Browse files
committed
fix: worker conflict in update install
1 parent aa16c0d commit c599972

4 files changed

Lines changed: 81 additions & 3 deletions

File tree

web/app/components/DesktopUpdaterPanel.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
33
import { check } from '@tauri-apps/plugin-updater'
44
import { relaunch } from '@tauri-apps/plugin-process'
5+
import { invoke } from '@tauri-apps/api/core'
56
67
type UpdaterStatus = 'idle' | 'available' | 'downloading' | 'installed' | 'error'
78
@@ -102,6 +103,20 @@ async function installUpdate() {
102103
status.value = 'downloading'
103104
errorMessage.value = null
104105
resetDownloadProgress()
106+
107+
// Sur Windows, l'installeur NSIS réécrit `goupix-vinted-worker.exe` :
108+
// il faut impérativement tuer le sidecar (et ses enfants Chromium lancés
109+
// par nodriver) AVANT, sinon l'install échoue avec « Error opening file
110+
// for writing ». Le kill côté Rust propage un taskkill /F /T sur l'arbre.
111+
try {
112+
await invoke('stop_local_worker')
113+
} catch (killError) {
114+
console.warn('[Updater] stop_local_worker a échoué, on tente quand même l\'install :', killError)
115+
}
116+
// Laisse Windows libérer les handles sur le .exe et ses DLLs avant que
117+
// NSIS ne commence l'extraction.
118+
await new Promise((resolve) => setTimeout(resolve, 600))
119+
105120
await pendingUpdate.value.downloadAndInstall(onDownloadEvent)
106121
status.value = 'installed'
107122
} catch (error) {

web/src-tauri/nsis-hooks.nsh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
; Hooks NSIS personnalisés pour le bundle Tauri Windows.
2+
;
3+
; Contexte : GoupixDex embarque un sidecar PyInstaller `goupix-vinted-worker.exe`
4+
; qui lance lui-même Chromium via nodriver. Pendant une mise à jour, NSIS doit
5+
; réécrire ce .exe ; s'il est encore en mémoire (ou si un des Chromium enfants
6+
; garde un handle), l'install échoue avec :
7+
; « Error opening file for writing: ...\goupix-vinted-worker.exe »
8+
;
9+
; Côté app Tauri on appelle déjà la commande `stop_local_worker` avant
10+
; `downloadAndInstall`, mais ce hook joue le rôle de filet de sécurité
11+
; (install manuel depuis GitHub Releases, app crashée, anciens orphelins...).
12+
13+
!macro NSIS_HOOK_PREINSTALL
14+
DetailPrint "Arrêt du worker GoupixDex s'il est en cours d'exécution..."
15+
; /F = force, /T = tue aussi l'arborescence (Chromium lancés par nodriver).
16+
; On redirige la sortie vers nul pour rester silencieux si aucun process n'est trouvé.
17+
nsExec::Exec 'cmd /C taskkill /F /T /IM goupix-vinted-worker.exe >nul 2>&1'
18+
Pop $0
19+
Sleep 500
20+
!macroend

web/src-tauri/src/lib.rs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::path::PathBuf;
22
use std::sync::Mutex;
3+
#[cfg(target_os = "windows")]
4+
use std::os::windows::process::CommandExt;
35
use tauri::Manager;
46
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
57
use tauri_plugin_shell::ShellExt;
@@ -122,6 +124,39 @@ fn check_browser_availability() -> BrowserInfo {
122124
detect_browsers()
123125
}
124126

127+
#[cfg(target_os = "windows")]
128+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
129+
130+
/// Tue le worker local et toute son arborescence de sous-processus (Chromium
131+
/// lancés par nodriver notamment). Indispensable avant un remplacement sur
132+
/// disque de `goupix-vinted-worker.exe` (mise à jour NSIS) pour ne pas se
133+
/// retrouver avec un fichier verrouillé.
134+
fn kill_worker_tree(child: CommandChild) {
135+
#[cfg(target_os = "windows")]
136+
{
137+
let pid = child.pid();
138+
let _ = std::process::Command::new("taskkill")
139+
.args(["/F", "/T", "/PID", &pid.to_string()])
140+
.creation_flags(CREATE_NO_WINDOW)
141+
.status();
142+
}
143+
let _ = child.kill();
144+
}
145+
146+
/// Commande exposée au front-end pour fermer proprement le worker avant une
147+
/// mise à jour Tauri (voir `DesktopUpdaterPanel.vue`).
148+
#[tauri::command]
149+
fn stop_local_worker(state: tauri::State<'_, VintedLocalChild>) -> Result<(), String> {
150+
let child_opt = match state.0.lock() {
151+
Ok(mut guard) => guard.take(),
152+
Err(_) => None,
153+
};
154+
if let Some(child) = child_opt {
155+
kill_worker_tree(child);
156+
}
157+
Ok(())
158+
}
159+
125160
#[cfg(debug_assertions)]
126161
fn dev_repo_api_dir() -> Option<PathBuf> {
127162
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -276,7 +311,10 @@ pub fn run() {
276311
.plugin(tauri_plugin_process::init())
277312
.plugin(tauri_plugin_shell::init())
278313
.manage(VintedLocalChild(Mutex::new(None)))
279-
.invoke_handler(tauri::generate_handler![check_browser_availability])
314+
.invoke_handler(tauri::generate_handler![
315+
check_browser_availability,
316+
stop_local_worker
317+
])
280318
.setup(|app| {
281319
let handle = app.handle().clone();
282320
match spawn_worker(&handle) {
@@ -304,7 +342,7 @@ pub fn run() {
304342
if let tauri::RunEvent::Exit = event {
305343
if let Ok(mut guard) = app.state::<VintedLocalChild>().0.lock() {
306344
if let Some(child) = guard.take() {
307-
let _ = child.kill();
345+
kill_worker_tree(child);
308346
}
309347
}
310348
}

web/src-tauri/tauri.conf.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434
"../public/favicon-32x32.png",
3535
"../public/android-chrome-192x192.png",
3636
"../public/android-chrome-512x512.png"
37-
]
37+
],
38+
"windows": {
39+
"nsis": {
40+
"installerHooks": "nsis-hooks.nsh"
41+
}
42+
}
3843
},
3944
"plugins": {
4045
"updater": {

0 commit comments

Comments
 (0)