Skip to content

Commit ff7e4f0

Browse files
Nicolettaclaude
authored andcommitted
fix: taskkill /T vor child.kill() – Python-Child überlebt nicht mehr
child.kill() tötete nur den PyInstaller-Bootloader (Parent); der Python/uvicorn-Child-Prozess wurde zur Waise und lief weiter. Lösung: Handle droppen, dann taskkill /F /T /PID für den ganzen Baum. onCloseRequested ohne preventDefault – App schließt sofort. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1c1096a commit ff7e4f0

File tree

3 files changed

+35
-41
lines changed

3 files changed

+35
-41
lines changed

src-tauri/src/lib.rs

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,36 @@ fn get_backend_port(state: tauri::State<BackendPort>) -> u16 {
1515
*state.0.lock().unwrap()
1616
}
1717

18-
/// IPC-Command: Frontend beendet den Backend-Sidecar explizit (beim Schließen und vor Update-Exit).
18+
/// Backend-Prozessbaum beenden.
1919
/// PyInstaller --onefile startet auf Windows zwei Prozesse (Bootloader + Python-Child).
20-
/// child.kill() beendet nur den Bootloader – /T killt den gesamten Prozessbaum.
21-
#[tauri::command]
22-
fn kill_backend(state: tauri::State<BackendChild>) {
23-
if let Some(child) = state.0.lock().unwrap().take() {
24-
#[cfg(target_os = "windows")]
20+
/// child.kill() tötet nur den Parent → Child läuft weiter.
21+
/// Lösung: zuerst taskkill /F /T /PID (killt ganzen Baum), dann Handle droppen.
22+
fn kill_backend_inner(child: CommandChild) {
23+
#[cfg(target_os = "windows")]
24+
{
2525
let pid = child.pid();
26-
26+
// Handle freigeben OHNE kill() – taskkill übernimmt den ganzen Baum atomisch
27+
drop(child);
28+
let _ = std::process::Command::new("taskkill")
29+
.args(["/F", "/T", "/PID", &pid.to_string()])
30+
.output();
31+
log::info!("Backend-Prozessbaum (PID {}) per taskkill /T beendet", pid);
32+
}
33+
#[cfg(not(target_os = "windows"))]
34+
{
2735
let _ = child.kill();
28-
29-
#[cfg(target_os = "windows")]
30-
{
31-
let _ = std::process::Command::new("taskkill")
32-
.args(["/F", "/T", "/PID", &pid.to_string()])
33-
.output();
34-
log::info!("Backend-Sidecar Prozessbaum (PID {}) per taskkill /T beendet", pid);
35-
}
36-
#[cfg(not(target_os = "windows"))]
3736
log::info!("Backend-Sidecar beendet");
3837
}
3938
}
4039

40+
/// IPC-Command: wird vom Frontend beim Schließen und vor dem Update-Exit aufgerufen
41+
#[tauri::command]
42+
fn kill_backend(state: tauri::State<BackendChild>) {
43+
if let Some(child) = state.0.lock().unwrap().take() {
44+
kill_backend_inner(child);
45+
}
46+
}
47+
4148
#[cfg_attr(mobile, tauri::mobile_entry_point)]
4249
pub fn run() {
4350
tauri::Builder::default()
@@ -56,15 +63,10 @@ pub fn run() {
5663
.build(),
5764
)
5865
.setup(|app| {
59-
// Freien Port wählen
6066
let port = portpicker::pick_unused_port().expect("Kein freier Port gefunden");
61-
6267
log::info!("Backend-Port: {}", port);
63-
64-
// State registrieren, damit get_backend_port es lesen kann
6568
app.manage(BackendPort(Mutex::new(port)));
6669

67-
// Sidecar starten
6870
let sidecar_cmd = app
6971
.shell()
7072
.sidecar("backend")
@@ -75,9 +77,7 @@ pub fn run() {
7577
.spawn()
7678
.expect("Backend-Sidecar konnte nicht gestartet werden");
7779

78-
// Child-Handle speichern, damit er beim App-Exit sauber beendet wird
7980
app.manage(BackendChild(Mutex::new(Some(child))));
80-
8181
log::info!("Backend-Sidecar gestartet auf Port {}", port);
8282
Ok(())
8383
})
@@ -87,20 +87,7 @@ pub fn run() {
8787
.run(|app_handle, event| {
8888
if let tauri::RunEvent::Exit = event {
8989
if let Some(child) = app_handle.state::<BackendChild>().0.lock().unwrap().take() {
90-
#[cfg(target_os = "windows")]
91-
let pid = child.pid();
92-
93-
let _ = child.kill();
94-
95-
#[cfg(target_os = "windows")]
96-
{
97-
let _ = std::process::Command::new("taskkill")
98-
.args(["/F", "/T", "/PID", &pid.to_string()])
99-
.output();
100-
log::info!("Backend-Sidecar Prozessbaum (PID {}) per taskkill /T beendet", pid);
101-
}
102-
#[cfg(not(target_os = "windows"))]
103-
log::info!("Backend-Sidecar beendet");
90+
kill_backend_inner(child);
10491
}
10592
}
10693
});

src/frontend/src/App.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,11 @@ export default function App() {
9595
async function register() {
9696
const { getCurrentWindow } = await import('@tauri-apps/api/window')
9797
const { invoke } = await import('@tauri-apps/api/core')
98-
const { exit } = await import('@tauri-apps/plugin-process')
9998

100-
unlisten = await getCurrentWindow().onCloseRequested(async event => {
101-
event.preventDefault()
99+
// Kein preventDefault – Fenster schließt normal weiter.
100+
// kill_backend wird proaktiv aufgerufen; RunEvent::Exit tut es nochmals (idempotent).
101+
unlisten = await getCurrentWindow().onCloseRequested(async () => {
102102
await invoke('kill_backend').catch(() => {})
103-
await exit(0)
104103
})
105104
}
106105

src/frontend/src/data/changelog.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export type ChangelogVersion = {
2222
}
2323

2424
export const CHANGELOG: ChangelogVersion[] = [
25+
{
26+
version: 'v0.1.21',
27+
datum: 'März 2026',
28+
eintraege: [
29+
{ typ: 'fix', text: 'App schließt wieder sofort – kein preventDefault mehr im Close-Handler' },
30+
{ typ: 'fix', text: 'Backend-Prozess wird jetzt zuverlässig beendet – taskkill /T läuft jetzt VOR dem child.kill(), damit der Python-Child-Prozess (PyInstaller) nicht als Waise weiterläuft' },
31+
],
32+
},
2533
{
2634
version: 'v0.1.19',
2735
datum: 'März 2026',

0 commit comments

Comments
 (0)