Repository navigation
fix(market): disable the generation pointer when uninstalling the market - #351
Merged
Merged
Conversation
Uninstalling the plugin market ran `dsh plugin remove --workspace-root dshmarket`, which edits only `dependencies` and `node_modules`. When dshmarket is installed as a generation, `desired.json` is what the next launch projects from — so prepareGenerationsForLaunch() rebuilt the market during the restart the uninstall itself triggers, and the whole operation looked like it had done nothing. Branch on the install form instead: a generation goes through uninstallGenerationPlugin() (disableGeneration + reproject), everything else keeps the existing pnpm removal. The form is read BEFORE the removal, so a generation whose disable fails reports a failure rather than a success with the pointer still in place. Not routed through removeProfilePluginCompletely(): dshmarket is a CORE_BUNDLES name and beginRemoval() refuses it by design, which is the guard that stops recovery and Safe Mode from tearing out a core bundle. Fixes #330
yaojin3616
added a commit
that referenced
this pull request
Sep 11, 2026
… paths (#380) * fix(market): update dshmarket in the shared tree, not via link switch `dshmarket` is a core bundle the profile migration deliberately keeps in the shared hoisted tree (`KEEP_IN_SHARED_TREE`), so its entry under `profiles/web/node_modules` is a real directory, never a generation link. Since #352, `runExternalMarketPluginInstall` routes every `add` — market self-updates included — through `installGeneration` + the immediate `publishInstalledGeneration`, which refuses a non-link directory by design: Cannot switch a non-link plugin directory: .../node_modules/dshmarket So every `dshmarket` self-update failed, and because it throws before any move, the market's rollback check also reports the previous build could not be verified. Before #352 the boundary staged the switch for the next cold start (`exposeMissingGenerationLinks` no-op'd on an existing entry), so the update eventually landed; #352 turned that into a hard failure. Branch on the spec: a `dshmarket` package goes through the ordinary shared-tree `add --workspace-root`, which updates the directory in place — the same asymmetry #351 already fixed on the uninstall side. The market's registry is still resolved and pinned (#337), and any stale dshmarket generation id is pruned from `desired.json`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(startup): preserve plugin ownership and select compatible updates * fix(market): stage dshmarket/loaded-bundle updates for next restart #352 replaced the market install boundary's "staged for next restart" publish step with publishInstalledGeneration(), which switches the profile node_modules entry synchronously while Harness is running. For any entry that is already in use that recreates the Windows locked-rename conflict the generation model exists to avoid: - dshmarket is a core bundle kept in the shared tree, so its entry is a real directory. publishInstalledGeneration() refuses a non-link entry: every market self-update failed with "Cannot switch a non-link plugin directory", and the market's rollback check then reported the previous build could not be verified. - a loaded generation junction cannot be renamed on Windows, so a plugin update mid-session hangs on the rename retry — the market UI sits on "in progress" until the registry lock times out. Restore the pre-#352 behavior: install the new generation, point desired.json at it, expose only a brand-new link for the market's add validation, and let the next cold start's projection switch the entry while Harness is stopped. The #337 registry resolution added by #352 is kept — only the immediate-switch step is reverted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(market): enforce verified baseline before normal startup * fix(market): preserve shared-tree and generation update contracts * fix(market): keep dshmarket out of the generation system entirely generation-migration.ts already keeps dshmarket in the shared profile tree forever (KEEP_IN_SHARED_TREE) — it is a core bundle, never meant to be a generation. Two recent paths broke that invariant by installing it as one anyway: - runExternalMarketPluginInstall only routed dshmarket to the shared-tree updateSharedMarket() when its node_modules entry was NOT currently a symlink; once anything made it one, later "add" calls fell through to installGeneration()/publishInstalledGeneration() like any third-party plugin. - ensureMarketBaseline's default upgrader (upgradePluginToGeneration) installed the verified baseline as a generation and published it as a link, for dshmarket specifically. Once dshmarket became a generation, whether it stayed a real directory or a symlink depended on whichever code path touched it last — and a build that reached the live profile that way (dshmarket 1.38.0, missing a host `dsh-settings` export) could not be detected or rolled back afterward: plugin-recovery excludes core bundles from its candidate list by design, so the crash showed "plugin recovery detection: unresolved" with no way to fix it short of hand-editing the profile. Close both paths: - runExternalMarketPluginInstall always routes a dshmarket spec to updateSharedMarket(), which now also drops a stale generation symlink (leaving the generation directory itself untouched) before running pnpm, so a previously-projected Market collapses back to a real directory instead of being treated as an ordinary generation update. - plugin-upgrade.ts gains upgradeMarketInSharedTree(), which cleans up any stale generation ownership/override, drops a stale symlink, and reinstalls the whole shared profile tree via installProfileDependenciesWithDsh() (Harness already stopped at this point in startup) instead of installGeneration(). market-baseline.ts now defaults to it, and also repairs a dshmarket entry that is a generation link even when its version already meets the baseline — the version check alone can't see that shape and would otherwise leave it a generation forever. upgradePluginToGeneration() itself is unchanged and still correctly used for ordinary third-party plugin upgrades (recovery-page and Safe Mode paths), which are unaffected — isThirdPartyPackageName()/CORE_BUNDLES already exclude dshmarket from those candidate lists. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(market): stop projection from resurrecting a dshmarket generation 38be336 closed the two writers that could install dshmarket as a generation, but not the projector that recreated one on every launch. `desired.json` still held a `dshmarket+1.38.0` pointer, and `resolveEnabledGenerations` enables whatever it lists — so `projectGenerations` re-linked the broken build each boot, ahead of any repair. The symlink's mtime after installing the previous fix is the proof: it was rewritten minutes later, by the fixed build itself. Two failure modes compounded: - projection ran before the market repair, so the repair could only ever clean up after it; and - the repair sat behind `shouldDeferProfileMaintenance`, which is true while any plugin removal lacks `bootVerifiedAt` — a flag only a successful boot sets. A market that cannot load is what stops the boot, so the gate could never clear itself: the repair needed a boot, the boot needed the repair. Enforce the invariant where it cannot be outrun, and let the repair run early enough to matter: - `resolveEnabledGenerations` skips shared-tree-only packages, so a stray pointer is inert instead of a link. Nothing can project dshmarket, whatever `desired.json` says. - `demoteMarketGeneration` undoes a legacy projection before projection runs: link, pointer, ownership marker and override all go, while the `dependencies` declaration and `bundles` entry stay — dropping those reads as "the market was uninstalled" and every later repair declines to reinstall it. - both that and `ensureMarketBaseline` now run ahead of projection and ahead of the removal-verification gate. A frozen migration still blocks them; that path keeps the profile byte-for-byte for rollback. - `pnpmRunnerPath` reaches `installProfileDependenciesWithDsh`. Without it `ensureProfilePnpmShim` refuses to run a plain pnpm against a profile that still projects ordinary plugins, which failed the install with "the generation-aware pnpm runner is unavailable". The option is required rather than optional so the compiler catches the next omission. Verified on a profile in exactly the reported state — a 1.38.0 market generation plus an unverified removal — which now self-heals to a real 1.45.1 directory in the shared tree on the next launch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(recovery): check and handle blocking plugins individually * fix(recovery): batch upgrades and invalidate repaired plugin failures * fix(recovery): automatically combine plugin upgrades and removals * fix(market): execute pnpm runner through linked package paths * fix(recovery): use Electron networking and retry failed market checks --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.
Fixes #330
问题
「设置 → 插件 → 插件市场 → 卸载插件市场」后重启,插件市场原样回来。
卸载走的是 IPC
market:uninstall→uninstallMarketAndRestart()→removeProfilePluginWithDsh(),也就是dsh plugin remove --workspace-root dshmarket。这是 pnpm 层面的删除,只改dependencies和node_modules。但当 dshmarket 是 generation 形态时,
desired.json才是权威:卸载自身触发的那次重启里,prepareGenerationsForLaunch()→projectGenerations()以desired.json为准,把 junction、dependencies、bundles、generationProjection、pnpm.overrides全部重建回来。加上 pnpm runner 的suspendGenerationProjectionForPnpm()/restore()会在命令前后把投影字段摘除再写回,pnpm 删除的那点效果也留不住。结果就是:卸载"成功",重启后一切照旧。
修复
按安装形态分支:
uninstallGenerationPlugin()(disableGeneration+ 重新投影 + 校验),也就是恢复流程和安全模式一直在用的那条路;形态在删除之前判定(新增
isProjectedGenerationPlugin())。原因是uninstallGenerationPlugin()的false有两种含义——"不是 generation,请走 pnpm" 和 "是 generation,但禁用失败"——混为一谈正是"指针还在却报成功"的来源。先问清楚,禁用失败就如实抛错。为什么不复用
removeProfilePluginCompletely()issue 里建议直接改调它,但那条路走不通:
dshmarket在CORE_BUNDLES里,isThirdPartyPackageName()返回 false,beginRemoval()会以Refusing to remove core package dshmarket拒绝。那个守卫的作用是防止插件恢复/安全模式流程扯掉核心 bundle,不该为这次用户主动发起的卸载而放宽。所以只取它内部的 generation 分支。测试
test/generation-launch.test.ts新增 4 条:isProjectedGenerationPlugin能区分 generation 形态与普通 manifest 形态;desired.json、dependencies、bundles、pnpm.overrides、node_modules/dshmarket全部干净;dsh plugin remove的结果)时,下一次启动投影会把它完整重建回来——这正是本 issue 的根因;去掉本 PR 的 src 改动后,其中 2 条失败;加上后全绿。
全量
vitest run:733 passed / 1 failed,3 个文件失败(ppt-activation、ppt-fonts、safe-mode-runtime)。这 3 个在未改动的main上同样失败(本地缺 PPT 运行时产物),与本 PR 无关。说明
issue 里提到「HTTP 路径是 generation-aware 的,只是桌面端没选它」——实际上市场自我卸载的 HTTP 路径 (
runUninstall()) 也不走runPlugin(),而是直接runProfileCommand(buildMarketUninstallArguments()),同样是纯 pnpm 删除。所以改走 HTTP 并不能解决问题,得在这里修。issue 末尾「附加信息」里的
bundles残留问题不在本 PR 范围内——那条已由8b018c9(syncBundles: true)修复,包含在 0.8.0-rc.3 及以后,v0.7.2 没有。