Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion apps/theme-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct BootstrapView {
catalog: Value,
catalog_state: CatalogStatus,
targets: Vec<platform::TargetView>,
target_diagnostic: Option<platform::DiscoveryDiagnostic>,
skin_state: skin_runtime::SkinRuntimeView,
persistence_state: persistence::PersistenceView,
update_state: UpdateView,
Expand Down Expand Up @@ -150,18 +151,25 @@ async fn bootstrap(
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone();
let target_discovery = platform::discover_targets();
Ok(BootstrapView {
app_version: app.package_info().version.to_string(),
platform: platform_name(),
catalog: catalog::present_catalog(&app, &catalog)?,
catalog_state,
targets: platform::discover_targets(),
targets: target_discovery.targets,
target_diagnostic: target_discovery.diagnostic,
skin_state: state.skin_runtime.current(),
persistence_state: persistence::current(&app)?,
update_state: state.updater.current(),
})
}

#[tauri::command]
fn discover_targets() -> platform::TargetDiscovery {
platform::discover_targets()
}

#[tauri::command]
async fn refresh_catalog(app: AppHandle, state: State<'_, DesktopState>) -> Result<Value, String> {
publish_catalog(
Expand Down Expand Up @@ -316,6 +324,15 @@ async fn disable_persistent_theme(
persistence::disable(&app)
}

#[tauri::command]
fn force_restart_persistent_theme(
app: AppHandle,
channel: String,
confirmed: bool,
) -> Result<persistence::PersistenceView, String> {
persistence::force_restart(&app, &channel, confirmed)
}

#[tauri::command]
fn open_external(app: AppHandle, target: String) -> Result<bool, String> {
let url = match target.as_str() {
Expand Down Expand Up @@ -376,6 +393,7 @@ pub fn run() {
})
.invoke_handler(tauri::generate_handler![
bootstrap,
discover_targets,
refresh_catalog,
copy_theme,
open_codex,
Expand All @@ -385,6 +403,7 @@ pub fn run() {
get_persistence_state,
enable_persistent_theme,
disable_persistent_theme,
force_restart_persistent_theme,
open_external,
check_for_app_update,
install_app_update
Expand Down
84 changes: 80 additions & 4 deletions apps/theme-manager/src-tauri/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ fn retry_delay(attempt: u8) -> Option<Duration> {
enum ControllerDecision {
Wait,
Blocked,
AwaitUserClose,
ApplyStopped,
Refresh,
Active,
Expand All @@ -500,10 +501,18 @@ fn controller_decision(
if phase == "retry-blocked" {
return ControllerDecision::Blocked;
}
if target_running && matches!(phase, "needs-user-close" | "force-closing") {
return ControllerDecision::AwaitUserClose;
}
if !target_running {
return if matches!(
phase,
APPLY_REQUESTED_PHASE | "restarting" | "retrying" | "error"
APPLY_REQUESTED_PHASE
| "restarting"
| "retrying"
| "error"
| "needs-user-close"
| "force-closing"
) {
ControllerDecision::ApplyStopped
} else {
Expand Down Expand Up @@ -531,6 +540,43 @@ async fn wait_until_stopped(target: &platform::TargetView) -> Result<(), String>
Err("ChatGPT 没有在安全等待时间内退出".into())
}

fn force_restart_is_authorized(confirmed: bool, phase: &str) -> Result<(), String> {
if !confirmed {
return Err("必须确认未发送的输入可能丢失,才能强制关闭 ChatGPT。".into());
}
if !matches!(phase, "needs-user-close" | "force-closing") {
return Err("只有正常退出超时后才能请求强制关闭 ChatGPT。".into());
}
Ok(())
}

pub fn force_restart(
app: &AppHandle,
channel: &str,
confirmed: bool,
) -> Result<PersistenceView, String> {
let root = persistence_root(app)?;
let selection = read_state_from(&root)?;
if !selection.enabled || selection.channel.as_deref() != Some(channel) {
return Err("所选 ChatGPT 不是当前启用的常驻主题目标。".into());
}
force_restart_is_authorized(confirmed, &selection.phase)?;
let target = platform::find_target(channel)?;
if !platform::target_is_running(&target)? {
return current(app);
}
platform::force_stop_target(&target)?;
update_status(
&root,
"force-closing",
Some("已请求强制关闭所选 ChatGPT,正在等待其退出后重新应用。".into()),
selection.attempts,
target.version,
selection.tested_version,
)?;
current(app)
}

async fn run_controller(app: AppHandle) -> Result<(), String> {
let root = persistence_root(&app)?;
fs::create_dir_all(&root).map_err(|error| format!("无法创建主题常驻状态目录:{error}"))?;
Expand Down Expand Up @@ -629,6 +675,10 @@ async fn run_controller(app: AppHandle) -> Result<(), String> {
tokio::time::sleep(POLL_INTERVAL).await;
continue;
}
ControllerDecision::AwaitUserClose => {
tokio::time::sleep(POLL_INTERVAL).await;
continue;
}
ControllerDecision::Wait => {
attempts = 0;
next_retry = tokio::time::Instant::now();
Expand Down Expand Up @@ -798,13 +848,15 @@ async fn run_controller(app: AppHandle) -> Result<(), String> {
if let Err(error) = wait_until_stopped(&target).await {
update_status(
&root,
"retrying",
Some(error),
"needs-user-close",
Some(format!(
"已请求所选 ChatGPT 正常退出,但安全等待超时:{error}"
)),
attempts,
installed,
Some(skin.tested_version),
)?;
next_retry = tokio::time::Instant::now() + delay;
next_retry = tokio::time::Instant::now();
continue;
}
match skin_runtime::apply(&app, &state.skin_runtime, &catalog, theme_id, mode, channel)
Expand Down Expand Up @@ -978,6 +1030,14 @@ mod tests {
controller_decision("retry-blocked", false, false, false),
ControllerDecision::Blocked
);
assert_eq!(
controller_decision("needs-user-close", false, false, false),
ControllerDecision::ApplyStopped
);
assert_eq!(
controller_decision("force-closing", false, false, false),
ControllerDecision::ApplyStopped
);
}

#[test]
Expand All @@ -994,5 +1054,21 @@ mod tests {
controller_decision(APPLY_REQUESTED_PHASE, true, false, false),
ControllerDecision::Restart
);
assert_eq!(
controller_decision("needs-user-close", true, false, false),
ControllerDecision::AwaitUserClose
);
assert_eq!(
controller_decision("force-closing", true, false, false),
ControllerDecision::AwaitUserClose
);
}

#[test]
fn force_restart_requires_a_second_confirmation_after_graceful_timeout() {
assert!(force_restart_is_authorized(false, "needs-user-close").is_err());
assert!(force_restart_is_authorized(true, "active").is_err());
assert!(force_restart_is_authorized(true, "needs-user-close").is_ok());
assert!(force_restart_is_authorized(true, "force-closing").is_ok());
}
}
Loading