diff --git a/src-dotnet/PuduLauncher/Services/TtsInstallService.cs b/src-dotnet/PuduLauncher/Services/TtsInstallService.cs index 6cb5625..eaa0907 100644 --- a/src-dotnet/PuduLauncher/Services/TtsInstallService.cs +++ b/src-dotnet/PuduLauncher/Services/TtsInstallService.cs @@ -39,6 +39,13 @@ public async Task RunInstallerAsync(string extractDir, string installPath, Cance UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, + // Redirect stdin so the installer (and the bundled Python it spawns for + // the python env step) gets a private handle instead of inheriting the + // sidecar's stdin, a pipe the Rust host owns for the ACK/SHUTDOWN signal. + // The bundled Python blocks while initializing its standard streams on + // that inherited pipe, hanging the install. See TtsServerService for the + // same fix on the server process. + RedirectStandardInput = true, }; using var process = new Process(); @@ -46,6 +53,11 @@ public async Task RunInstallerAsync(string extractDir, string installPath, Cance process.EnableRaisingEvents = true; process.Start(); + // The installer never reads stdin; close it so the child sees EOF rather + // than an open idle pipe. The redirect itself is what keeps it off the + // sidecar's inherited stdin (see ProcessStartInfo above). + process.StandardInput.Close(); + lock (_processLock) { _currentInstallerProcess = process; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 55d3720..9a99154 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "pudu-launcher", - "version": "2.0.1", + "version": "2.0.2", "identifier": "com.corp0.pudu-launcher", "build": { "beforeDevCommand": "npm run generate-ts && npm run build-sidecar && npm run dev", diff --git a/src/components/layouts/tts/TtsInstallerLayout.tsx b/src/components/layouts/tts/TtsInstallerLayout.tsx index bc3117b..b33d784 100644 --- a/src/components/layouts/tts/TtsInstallerLayout.tsx +++ b/src/components/layouts/tts/TtsInstallerLayout.tsx @@ -56,6 +56,7 @@ export default function TtsInstallerLayout(props: TtsInstallerLayoutProps) { currentStep={currentStep} stepLabels={stepLabels} isComplete={isComplete} + startNumber={0} /> {(statusMessage || statusLabel) && ( diff --git a/src/components/organisms/common/PuduStepper.tsx b/src/components/organisms/common/PuduStepper.tsx index 15355b6..0772b09 100644 --- a/src/components/organisms/common/PuduStepper.tsx +++ b/src/components/organisms/common/PuduStepper.tsx @@ -6,10 +6,14 @@ interface PuduStepperProps { currentStep: number; stepLabels?: string[]; isComplete?: boolean; + // Number shown on the first indicator. Defaults to 1. Set to 0 to align the + // displayed numbers with externally numbered phases (for example installer + // log lines that count from a zero-based "prepare" step). + startNumber?: number; } export default function PuduStepper(props: PuduStepperProps) { - const { maxSteps, currentStep, stepLabels, isComplete = false } = props; + const { maxSteps, currentStep, stepLabels, isComplete = false, startNumber = 1 } = props; const stepCount = Math.max(1, Math.floor(maxSteps)); const activeStep = Math.min(Math.max(Math.floor(currentStep), 1), stepCount); @@ -32,7 +36,7 @@ export default function PuduStepper(props: PuduStepperProps) { variant={indicatorVariant} color={indicatorColor} > - {isCompleted ? : stepNumber} + {isCompleted ? : stepNumber - 1 + startNumber} } > diff --git a/src/contextProviders/TtsInstallerContextProvider.tsx b/src/contextProviders/TtsInstallerContextProvider.tsx index b56041c..e859972 100644 --- a/src/contextProviders/TtsInstallerContextProvider.tsx +++ b/src/contextProviders/TtsInstallerContextProvider.tsx @@ -85,10 +85,8 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { const { ttsState, status, statusMessage, installLogs, clearInstallLogs } = useTtsState(); const [isInstallerOpen, setIsInstallerOpen] = useState(false); - const [maxReachedStep, setMaxReachedStep] = useState(1); const installSessionRef = useRef(false); const prevStatusRef = useRef(null); - const prevLogLengthRef = useRef(0); const prevUpdateAvailableRef = useRef(false); const beginInstallSession = () => { @@ -98,7 +96,6 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { installSessionRef.current = true; clearInstallLogs(); - setMaxReachedStep(1); }; // React to status changes @@ -132,25 +129,18 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { showInfo({ message: "TTS server stopped" }); } - setMaxReachedStep((prev) => Math.max(prev, stepFromStatus(status))); prevStatusRef.current = status; }, [status]); - // React to new install log lines + // React to new install log lines. This effect only opens the installer modal + // when logs start streaming. The active step is derived from installLogs + // directly (see currentStep below) rather than tracked incrementally, so it + // can never desync from the log contents. useEffect(() => { if (installLogs.length > 0 && !installSessionRef.current) { beginInstallSession(); setIsInstallerOpen(true); } - - for (let i = prevLogLengthRef.current; i < installLogs.length; i++) { - const parsed = inferStepFromLogLine(installLogs[i]); - if (parsed !== null) { - setMaxReachedStep((prev) => Math.max(prev, parsed)); - } - } - - prevLogLengthRef.current = installLogs.length; }, [installLogs]); useEffect(() => { @@ -184,7 +174,20 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) { const statusLabel = status !== null ? (TTS_STATUS_LABELS[status] ?? `Status ${status}`) : "Unknown"; - const currentStep = maxReachedStep; + // Derive the active step from the log contents on every render instead of + // tracking it incrementally. An incremental index cursor desynced from + // installLogs whenever the shared log array was cleared for a new session, + // skipping step markers and freezing the stepper (for example showing + // "Python" while packages were already installing). Re-scanning is cheap + // (logs are capped at 400 lines) and cannot drift from what the log shows. + const stepFromLogs = installLogs.reduce((maxStep, line) => { + const parsed = inferStepFromLogLine(line); + return parsed === null ? maxStep : Math.max(maxStep, parsed); + }, 1); + const currentStep = Math.min( + Math.max(stepFromLogs, stepFromStatus(status)), + INSTALL_STEP_LABELS.length, + ); const isInstallComplete = status === TTS_STATUS.Installed; const stepStatusMessage = isInstallComplete ? INSTALL_SUCCESS_MESSAGE