mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
Fix keep-server-running on macOS: ignore SIGHUP, watchdog grace period, build script fixes
This commit is contained in:
+16
-1
@@ -58,6 +58,12 @@ def disable_watchdog():
|
|||||||
"""Disable the parent watchdog so the server keeps running after parent exits."""
|
"""Disable the parent watchdog so the server keeps running after parent exits."""
|
||||||
global _watchdog_disabled
|
global _watchdog_disabled
|
||||||
_watchdog_disabled = True
|
_watchdog_disabled = True
|
||||||
|
# Ignore SIGHUP so the server survives when the parent Tauri process exits.
|
||||||
|
# On Unix, child processes receive SIGHUP when the parent's session leader
|
||||||
|
# exits, which would kill the server even though we want it to persist.
|
||||||
|
if sys.platform != "win32":
|
||||||
|
import signal
|
||||||
|
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||||
|
|
||||||
|
|
||||||
def _start_parent_watchdog(parent_pid, data_dir=None):
|
def _start_parent_watchdog(parent_pid, data_dir=None):
|
||||||
@@ -130,7 +136,16 @@ def _start_parent_watchdog(parent_pid, data_dir=None):
|
|||||||
watchdog_logger.info("Watchdog disabled (keep server running), stopping monitor")
|
watchdog_logger.info("Watchdog disabled (keep server running), stopping monitor")
|
||||||
return
|
return
|
||||||
if not _is_pid_alive(parent_pid):
|
if not _is_pid_alive(parent_pid):
|
||||||
watchdog_logger.info(f"Parent process {parent_pid} gone, shutting down server...")
|
# Parent is gone. Before shutting down, give the app a moment
|
||||||
|
# to send /watchdog/disable — there is a race where the Tauri
|
||||||
|
# RunEvent::Exit handler sends the disable request while we are
|
||||||
|
# mid-iteration (already past the _watchdog_disabled check above).
|
||||||
|
watchdog_logger.info(f"Parent process {parent_pid} gone, waiting for possible disable request...")
|
||||||
|
time.sleep(1)
|
||||||
|
if _watchdog_disabled:
|
||||||
|
watchdog_logger.info("Watchdog was disabled during grace period, keeping server alive")
|
||||||
|
return
|
||||||
|
watchdog_logger.info("Watchdog still enabled after grace period, shutting down server...")
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
# sys.exit triggers SystemExit, allowing uvicorn to run
|
# sys.exit triggers SystemExit, allowing uvicorn to run
|
||||||
# shutdown handlers. os.kill(SIGTERM) on Windows calls
|
# shutdown handlers. os.kill(SIGTERM) on Windows calls
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
from PyInstaller.utils.hooks import collect_data_files
|
from PyInstaller.utils.hooks import collect_data_files
|
||||||
from PyInstaller.utils.hooks import collect_submodules
|
from PyInstaller.utils.hooks import collect_submodules
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
from PyInstaller.utils.hooks import copy_metadata
|
from PyInstaller.utils.hooks import copy_metadata
|
||||||
|
|
||||||
datas = []
|
datas = []
|
||||||
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
|
binaries = []
|
||||||
|
hiddenimports = ['backend', 'backend.main', 'backend.config', 'backend.database', 'backend.models', 'backend.profiles', 'backend.history', 'backend.tts', 'backend.transcribe', 'backend.platform_detect', 'backend.backends', 'backend.backends.pytorch_backend', 'backend.utils.audio', 'backend.utils.cache', 'backend.utils.progress', 'backend.utils.hf_progress', 'backend.utils.validation', 'backend.cuda_download', 'backend.effects', 'backend.utils.effects', 'backend.versions', 'pedalboard', 'torch', 'transformers', 'fastapi', 'uvicorn', 'sqlalchemy', 'librosa', 'soundfile', 'qwen_tts', 'qwen_tts.inference', 'qwen_tts.inference.qwen3_tts_model', 'qwen_tts.inference.qwen3_tts_tokenizer', 'qwen_tts.core', 'qwen_tts.cli', 'pkg_resources.extern', 'backend.backends.mlx_backend', 'mlx', 'mlx.core', 'mlx.nn', 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.stt']
|
||||||
datas += collect_data_files('qwen_tts')
|
datas += collect_data_files('qwen_tts')
|
||||||
# Use collect_all (not collect_data_files) so native .dylib and .metallib
|
|
||||||
# files are bundled as binaries, not data. Without this, MLX raises OSError
|
|
||||||
# when loading Metal shaders inside the PyInstaller bundle.
|
|
||||||
from PyInstaller.utils.hooks import collect_all as _collect_all
|
|
||||||
_mlx_datas, _mlx_bins, _mlx_hidden = _collect_all('mlx')
|
|
||||||
_mlxa_datas, _mlxa_bins, _mlxa_hidden = _collect_all('mlx_audio')
|
|
||||||
datas += _mlx_datas + _mlxa_datas
|
|
||||||
datas += copy_metadata('qwen-tts')
|
datas += copy_metadata('qwen-tts')
|
||||||
hiddenimports += collect_submodules('qwen_tts')
|
hiddenimports += collect_submodules('qwen_tts')
|
||||||
hiddenimports += collect_submodules('jaraco')
|
hiddenimports += collect_submodules('jaraco')
|
||||||
hiddenimports += collect_submodules('mlx')
|
hiddenimports += collect_submodules('mlx')
|
||||||
hiddenimports += collect_submodules('mlx_audio')
|
hiddenimports += collect_submodules('mlx_audio')
|
||||||
|
tmp_ret = collect_all('mlx')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
tmp_ret = collect_all('mlx_audio')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
a = Analysis(
|
||||||
['server.py'],
|
['server.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=_mlx_bins + _mlxa_bins,
|
binaries=binaries,
|
||||||
datas=datas,
|
datas=datas,
|
||||||
hiddenimports=hiddenimports,
|
hiddenimports=hiddenimports,
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
excludes=['nvidia', 'nvidia.cublas', 'nvidia.cuda_cupti', 'nvidia.cuda_nvrtc', 'nvidia.cuda_runtime', 'nvidia.cudnn', 'nvidia.cufft', 'nvidia.curand', 'nvidia.cusolver', 'nvidia.cusparse', 'nvidia.nccl', 'nvidia.nvjitlink', 'nvidia.nvtx'],
|
||||||
noarchive=False,
|
noarchive=False,
|
||||||
optimize=0,
|
optimize=0,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Github, Globe, Languages, MessageSquare, Zap } from 'lucide-react';
|
import { Github, Globe, Languages, MessageSquare, Zap } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { ControlUI } from '@/components/ControlUI';
|
import { ControlUI } from '@/components/ControlUI';
|
||||||
import { Features } from '@/components/Features';
|
import { Features } from '@/components/Features';
|
||||||
@@ -51,13 +50,11 @@ export default function Home() {
|
|||||||
'drop-shadow(0 0 20px hsl(43 60% 50% / 0.4)) drop-shadow(0 0 60px hsl(43 60% 50% / 0.2))',
|
'drop-shadow(0 0 20px hsl(43 60% 50% / 0.4)) drop-shadow(0 0 60px hsl(43 60% 50% / 0.2))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
src="/voicebox-logo-app.webp"
|
src="/voicebox-logo-app.webp"
|
||||||
alt="Voicebox"
|
alt="Voicebox"
|
||||||
width={160}
|
|
||||||
height={160}
|
|
||||||
className="h-full w-full object-contain mix-blend-lighten"
|
className="h-full w-full object-contain mix-blend-lighten"
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ PLATFORM=$(rustc --print host-tuple 2>/dev/null || echo "unknown")
|
|||||||
echo "Building voicebox-server for platform: $PLATFORM"
|
echo "Building voicebox-server for platform: $PLATFORM"
|
||||||
|
|
||||||
# Build Python binary
|
# Build Python binary
|
||||||
|
# Resolve PATH to absolute paths before changing directory
|
||||||
|
export PATH="$(cd "$(dirname "$0")/.." && pwd)/backend/venv/bin:$PATH"
|
||||||
cd backend
|
cd backend
|
||||||
|
|
||||||
# Check if PyInstaller is installed
|
# Check if PyInstaller is installed
|
||||||
if ! python -c "import PyInstaller" 2>/dev/null; then
|
if ! python -c "import PyInstaller" 2>/dev/null; then
|
||||||
echo "Installing PyInstaller..."
|
echo "Installing PyInstaller..."
|
||||||
pip install pyinstaller
|
python -m pip install pyinstaller
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build binary
|
# Build binary
|
||||||
|
|||||||
Generated
+1
-1
@@ -5041,7 +5041,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "voicebox"
|
name = "voicebox"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
|
|||||||
Binary file not shown.
@@ -572,6 +572,7 @@ async fn restart_server(
|
|||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
fn set_keep_server_running(state: State<'_, ServerState>, keep_running: bool) {
|
fn set_keep_server_running(state: State<'_, ServerState>, keep_running: bool) {
|
||||||
|
println!("set_keep_server_running called with: {}", keep_running);
|
||||||
*state.keep_running_on_close.lock().unwrap() = keep_running;
|
*state.keep_running_on_close.lock().unwrap() = keep_running;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,6 +763,8 @@ pub fn run() {
|
|||||||
RunEvent::Exit => {
|
RunEvent::Exit => {
|
||||||
let state = app.state::<ServerState>();
|
let state = app.state::<ServerState>();
|
||||||
let keep_running = *state.keep_running_on_close.lock().unwrap();
|
let keep_running = *state.keep_running_on_close.lock().unwrap();
|
||||||
|
let has_pid = state.server_pid.lock().unwrap().is_some();
|
||||||
|
println!("RunEvent::Exit — keep_running={}, has_pid={}", keep_running, has_pid);
|
||||||
|
|
||||||
if keep_running {
|
if keep_running {
|
||||||
// Tell the server to disable its watchdog so it survives
|
// Tell the server to disable its watchdog so it survives
|
||||||
@@ -771,9 +774,13 @@ pub fn run() {
|
|||||||
.timeout(std::time::Duration::from_secs(2))
|
.timeout(std::time::Duration::from_secs(2))
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = client
|
match client
|
||||||
.post(&format!("http://127.0.0.1:{}/watchdog/disable", SERVER_PORT))
|
.post(&format!("http://127.0.0.1:{}/watchdog/disable", SERVER_PORT))
|
||||||
.send();
|
.send()
|
||||||
|
{
|
||||||
|
Ok(resp) => println!("Watchdog disable response: {}", resp.status()),
|
||||||
|
Err(e) => eprintln!("Failed to disable watchdog: {}", e),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Server will self-terminate via parent-pid watchdog when
|
// Server will self-terminate via parent-pid watchdog when
|
||||||
// this process exits. On Unix, also send SIGTERM for
|
// this process exits. On Unix, also send SIGTERM for
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ class TauriLifecycle implements PlatformLifecycle {
|
|||||||
// @ts-expect-error - accessing module-level variable from another module
|
// @ts-expect-error - accessing module-level variable from another module
|
||||||
const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;
|
const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[lifecycle] window-close-requested: keepRunning=%s, serverStartedByApp=%s',
|
||||||
|
keepRunning,
|
||||||
|
serverStartedByApp,
|
||||||
|
);
|
||||||
|
|
||||||
if (!keepRunning && serverStartedByApp) {
|
if (!keepRunning && serverStartedByApp) {
|
||||||
// Stop server before closing (only if we started it)
|
// Stop server before closing (only if we started it)
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user