fix(core): show processing state during automatic startup

- track automatic core startup in shared runtime state
- expose the starting state through CoreStatus
- disable core controls while startup is in progress
- clear the processing state after startup succeeds or fails
- add regression tests for startup state handling
This commit is contained in:
lzt404 committed 2026-08-14 01:37:35 +08:00
1 parent 30fb184384
commit dbd97d862f
6 files changed
+54 -10

No files matched your search

+5 -1
View File
@@ -802,6 +802,7 @@ pub(crate) fn current_core_status(
let install_dir = core_install_dir()?;
let binary_path = find_core_binary(&install_dir);
let installed = binary_path.is_some();
let starting = process_state.is_some_and(CoreProcessState::is_starting);
let managed_pid = process_state.and_then(|state| state.managed_pid());
let management_port_open = management_port.map(is_management_port_open);
let process_id = match managed_pid {
@@ -814,7 +815,9 @@ pub(crate) fn current_core_status(
let running = process_id.is_some() && management_port_open.unwrap_or(true);
let current_version = read_core_metadata(&install_dir).map(|metadata| metadata.version);
let message = if !installed {
let message = if starting {
"CPA 内核正在启动".to_string()
} else if !installed {
"未安装 CPA 内核,请先安装最新版".to_string()
} else if running {
"CPA 内核正在运行".to_string()
@@ -825,6 +828,7 @@ pub(crate) fn current_core_status(
Ok(CoreStatus {
installed,
running,
starting,
managed: managed_pid.is_some(),
process_id,
current_version,
+31 -2
View File
@@ -39,7 +39,6 @@ use objc2::MainThreadMarker;
use objc2_app_kit::NSEvent;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[cfg(target_os = "windows")]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "macos")]
use std::sync::Arc;
@@ -276,6 +275,7 @@ struct AppUpdateInner {
#[derive(Default)]
struct CoreProcessState {
child: Mutex<Option<Child>>,
starting: AtomicBool,
#[cfg(windows)]
job: Mutex<Option<isize>>,
}
@@ -370,6 +370,7 @@ struct CorePlatform {
struct CoreStatus {
installed: bool,
running: bool,
starting: bool,
managed: bool,
process_id: Option<u32>,
current_version: Option<String>,
@@ -1341,6 +1342,23 @@ impl AppUpdateState {
}
impl CoreProcessState {
fn new(starting: bool) -> Self {
Self {
child: Mutex::new(None),
starting: AtomicBool::new(starting),
#[cfg(windows)]
job: Mutex::new(None),
}
}
fn is_starting(&self) -> bool {
self.starting.load(Ordering::Acquire)
}
fn set_starting(&self, starting: bool) {
self.starting.store(starting, Ordering::Release);
}
fn managed_pid(&self) -> Option<u32> {
let Ok(mut child) = self.child.lock() else {
return None;
@@ -1727,7 +1745,7 @@ fn main() {
)
.manage(CoreDownloadState::default())
.manage(AppUpdateState::default())
.manage(CoreProcessState::default())
.manage(CoreProcessState::new(gui_config.start_core_on_launch))
.manage(usage::UsageCollectorState::default())
.manage(GuiConfigState::new(gui_config))
.manage(MainWindowSizeState::new(initial_window_size))
@@ -1845,9 +1863,18 @@ fn main() {
let gui_config_state = core_app.state::<GuiConfigState>();
let process_state = core_app.state::<CoreProcessState>();
let Ok(config) = gui_config_state.snapshot() else {
process_state.set_starting(false);
return;
};
if config.start_core_on_launch {
if let Ok(status) =
current_core_status(Some(process_state.inner()), Some(config.port))
{
emit_core_status(&core_app, &status);
}
}
match auto_install_bundled_core_if_missing(&core_app) {
Ok(true) => eprintln!("未检测到 CPA 内核,已自动安装内置离线版本"),
Ok(false) => {}
@@ -1860,6 +1887,8 @@ fn main() {
}
}
process_state.set_starting(false);
if let Ok(status) =
current_core_status(Some(process_state.inner()), Some(config.port))
{
+8
View File
@@ -1,6 +1,14 @@
use super::support::*;
use super::*;
#[test]
fn core_process_starting_state_tracks_automatic_launch() {
let state = CoreProcessState::new(true);
assert!(state.is_starting());
state.set_starting(false);
assert!(!state.is_starting());
}
#[test]
fn replacing_a_core_preserves_only_regular_bundled_assets() {
let root = agent_test_home("bundled-assets");
+1
View File
@@ -6,6 +6,7 @@ fn windows_tray_presentation_tracks_core_state_and_busy_actions() {
let mut status = CoreStatus {
installed: false,
running: false,
starting: false,
managed: false,
process_id: None,
current_version: None,
+1
View File
@@ -5,6 +5,7 @@ import { listen } from '@tauri-apps/api/event';
export type CoreStatus = {
installed: boolean;
running: boolean;
starting: boolean;
managed: boolean;
processId: number | null;
currentVersion: string | null;
+8 -7
View File
@@ -584,9 +584,10 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
const currentVersion = coreStatus?.currentVersion ?? '';
const coreInstalled = Boolean(coreStatus?.installed);
const coreRunning = Boolean(coreStatus?.running);
const busy = checkingLatest || installing || processBusy;
const coreProcessBusy = processBusy || Boolean(coreStatus?.starting);
const busy = checkingLatest || installing || coreProcessBusy;
const installDisabled = busy || Boolean(coreStatus?.running);
const offlineInstallDisabled = installing || processBusy || coreRunning;
const offlineInstallDisabled = installing || coreProcessBusy || coreRunning;
const computedPercent =
progress?.percent ??
(progress?.total && progress.total > 0 ? (progress.downloaded / progress.total) * 100 : null);
@@ -702,7 +703,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
<h2>{t('kernel.control.title')}</h2>
</div>
<span className={`state-pill ${statusTone}`} title={statusError || undefined}>
{processBusy ? t('common.processing') : statusLabel}
{coreProcessBusy ? t('common.processing') : statusLabel}
</span>
</div>
@@ -736,7 +737,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
<button
type="button"
className={coreRunning ? 'danger-button' : 'primary-button'}
disabled={!coreInstalled || installing || processBusy}
disabled={!coreInstalled || installing || coreProcessBusy}
onClick={() =>
void runCoreProcessCommand(
coreRunning ? 'stop_core_process' : 'start_core_process',
@@ -744,12 +745,12 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
)
}
>
{processBusy ? t('common.processing') : coreRunning ? t('kernel.action.stop') : t('kernel.action.start')}
{coreProcessBusy ? t('common.processing') : coreRunning ? t('kernel.action.stop') : t('kernel.action.start')}
</button>
<button
type="button"
className="secondary-button"
disabled={!coreInstalled || !coreRunning || installing || processBusy}
disabled={!coreInstalled || !coreRunning || installing || coreProcessBusy}
onClick={() =>
void runCoreProcessCommand('restart_core_process', { success: t('kernel.notice.restarted') })
}
@@ -759,7 +760,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
<button
type="button"
className="secondary-button"
disabled={processBusy}
disabled={coreProcessBusy}
onClick={() => void refreshStatus()}
>
{t('kernel.control.refresh')}