Conversation
zbeosf
force-pushed
the
fix/hide-at-launch-http-api
branch
from
August 26, 2026 04:57
67e81e1 to
504bf3b
Compare
zbeosf
force-pushed
the
fix/hide-at-launch-http-api
branch
from
August 26, 2026 05:20
504bf3b to
8a9007e
Compare
oldj
added a commit
that referenced
this pull request
Sep 3, 2026
fix(http-api): apply toggle in the backend when no renderer is alive
Owner
|
Thanks for the thorough fix and tests! Merged into 感谢修复与详尽的测试!已合入 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1038 — with Hide at launch on,
GET /api/toggleanswersokbut does nothing.api_toggledoes not change state itself: it broadcaststoggle_itemand relies on the main window'sonToggleItemto run the apply pipeline. A Tauri event with no listener is dropped, and there are two supported configurations with no main window —hide_at_launchskips window creation at setup, andlightweight_modedestroys the window on close. In both, the broadcast reaches nobody and the endpoint still reports success.The module doc of
http_api.rsalready names both the assumption and the way out:That premise does not hold under
hide_at_launch. This PR takes the documented alternative, but only for the case the premise misses.What changed
http_api.rs—api_togglebroadcasts as before when a main window exists, and falls back to applying in the handler when there is none. The common path is untouched, sochoice_modesemantics stay centralised in the renderer for every UI-driven toggle.commands.rs— the apply pipeline (privileged write, apply history,system_hosts_updated, tray title refresh,cmd_after_hosts_apply) is extracted fromapply_hosts_selectionintoapply_aggregated_content, so both entry points produce identical side effects.apply_hosts_selectionkeeps its behaviour and just calls it.storage/manifest.rs—set_on_state_of_item, a port of the renderer'ssetOnStateOfItem(src/common/hostsFn.ts): single-choice exclusion at top level and inside folders,folder_modeoverriding the globalchoice_mode, the optional folder cascade, and the parent-reconcile walk.The backend path goes through the same
apply_aggregated_contentpipeline a UI-driven apply uses — a bare write would silently skip history and the post-apply command. Beyond that it takes the same precautions the renderer relies on:write mode not set.rather than takingapply_to_system_hosts's overwrite default and wiping hand-written entries for anyone still carrying the empty Electron-era value.store_lockafter the write, then re-applies the flip. The privileged write can sit on an auth prompt for minutes, and the refresh scanner may legitimately rewritemanifest.jsonmeanwhile — saving the pre-apply snapshot would clobber it. Same shape as the remote-refresh path inrefresh.rs.spawn_blockingso it never pins a worker of the runtime shared by the HTTP server and every async command.tray::refresh_titlereadsmanifest.jsonfrom disk, so the call inside the pipeline sees the pre-toggle tree; without a second call the menubar trails one toggle behind, and with no window open it is the only place the active profile is visible. Also emitstray_list_updatedso a lazily-built tray mini window does not keep showing stale state.cancelled.,write mode not set.,applied but not persisted.,apply failed.— instead of collapsing a dismissed prompt, a policy denial and a full disk into one opaque reply.Nothing is created, shown, or hidden differently —
hide_at_launchstill skips window creation exactly as before, and the structural test asserting that still passes.Tests
set_on_state_of_item: multi/single choice, folder cascade, single-choice folders, parent reconcile, nested folders, unknown ids,folder_modeabsent/0 inheriting the globalchoice_mode, single-choice cascading into the folders it switches off, and cascade skipping non-folder nodes.cargo test→ 182 passed, 0 failed (including the existingwindow_configcanaries, which assert thathide_at_launchmust not create a window — untouched by this PR).npm run test:all(typecheck + vitest 84 + cargo + playwright 44) passes.Manually verified on macOS with
hide_at_launch = true,http_api_on = true, no window ever opened:/etc/hostswrittenmulti_chose_folder_switch_all)choice_mode = 1, toggle another profilecmd_after_hosts_applyshow_title_on_traywrite_mode = ""/etc/hostsuntouchedNotes
This also restores the bundled Alfred workflow, whose action is exactly
curl 'http://127.0.0.1:50761/api/toggle?id={query}'.中文说明
修复 #1038 —— 开启「启动时隐藏窗口」后,
/api/toggle返回ok却没有任何效果。api_toggle本身不修改状态,它只广播toggle_item,依赖主窗口的onToggleItem完成切换与应用。没有监听者的 Tauri 事件会被丢弃,而「启动时隐藏」会完全跳过主窗口的创建,于是广播无人接收,接口却仍然报告成功。http_api.rs的模块注释中已经同时写出了这个前提和出路:在 handler 里直接应用「即使没有 renderer 存活也能工作」。本 PR 采用的正是这个方案,但只用在前提不成立的那种情况。改动内容
http_api.rs—— 主窗口存在时照旧广播(UI 触发的切换仍然走 renderer,choice_mode语义保持集中在一处);只有在没有主窗口时才在 handler 内直接应用。commands.rs—— 把应用流程(提权写入、应用历史、system_hosts_updated广播、托盘标题刷新、cmd_after_hosts_apply)从apply_hosts_selection中抽取为apply_aggregated_content,使两个入口产生完全一致的副作用;apply_hosts_selection行为不变,只是改为调用它。storage/manifest.rs—— 新增set_on_state_of_item,把 renderer 的setOnStateOfItem(src/common/hostsFn.ts)移植到 Rust:顶层与文件夹内的单选互斥、folder_mode覆盖全局choice_mode、可选的文件夹级联,以及父节点状态的回溯同步。后端路径走的是与 UI 触发完全相同的
apply_aggregated_content流程(只做裸写入会静默跳过历史记录和应用后命令),并且额外做了渲染进程本来就依赖的几项保护:write mode not set.,而不是套用 overwrite 默认值把用户手写的 hosts 条目清掉。store_lock下重新读取并重新应用:提权写入可能在认证框上停留数分钟,其间远程刷新可能合法地改写了manifest.json,直接保存旧快照会覆盖它。做法与refresh.rs的远程刷新路径一致。spawn_blocking执行阻塞写入,避免占用 HTTP 服务与所有 async 命令共用的运行时线程。tray::refresh_title从磁盘读取 manifest,流程内那次调用看到的还是切换前的树;不补这一次的话菜单栏会永远慢一步,而在没有窗口时那是用户唯一能看到当前方案的地方。同时发出tray_list_updated,避免已创建的托盘小窗显示过期状态。cancelled./write mode not set./applied but not persisted./apply failed.。窗口的创建、显示、隐藏行为没有任何改变 —— 「启动时隐藏」依然跳过窗口创建,对应的结构性测试也依然通过。
测试
新增 15 个
set_on_state_of_item单元测试(多选/单选、文件夹级联、单选文件夹、父节点回溯、嵌套文件夹、未知 id,以及folder_mode缺省/为 0 时继承全局choice_mode等)。cargo test→ 182 passed, 0 failed;npm run test:all(typecheck + vitest 84 + cargo + playwright 44)全部通过。在 macOS 上以
hide_at_launch = true、http_api_on = true、且从未打开过窗口的状态下手动验证:切换顶层方案、切换文件夹(子项全开)、关闭其中一个子项(父节点自动关闭、兄弟节点不受影响)、choice_mode = 1时切换另一个方案(原选中项被清除),/etc/hosts均正确写入。附注
这同时也修复了自带的 Alfred workflow —— 它的动作正是
curl 'http://127.0.0.1:50761/api/toggle?id={query}'。