fix(market): upgrade dshmarket to 1.15.0 - #141
Merged
Merged
Conversation
On macOS Harness runs inside an Electron utility process, so its
process.execPath and argv0 point at the Electron helper rather than a Node
binary. Plugins re-invoke the dsh CLI through the executable running them —
dsh-market forwards process.execArgv with it — and that child booted as an
Electron app, where the leading --expose-internals shifts argv and the CLI
answers "error: --profile <name> is required". Updating a plugin from the
market therefore always failed on macOS ("Update failed: dshmarket").
The flag cannot travel in the Harness process environment: the utility
process is launched with Chromium switches Node rejects as bad options, so
setting it there stops Harness from starting at all (exit 2304). The Harness
entry declares Node mode from the inside instead, after its own switches are
parsed, marking only the children. The packaged node/pnpm shims declare it
themselves too, so a caller that scrubs the environment still gets Node
semantics.
Verified end to end on a dev launch: POST /dsh-market/update for dshmarket
returned exitCode 0 and moved the profile from 1.15.0 to 1.17.1.
Windows cannot replace a directory while something holds a handle inside it, and pnpm finishes each package by renaming <pkg>_tmp_<pid>_<n> onto <pkg>. With Harness running — it has the profile's modules loaded, and the platform's scanners open files behind everyone's back — that final rename fails, which is what "Update failed: dshmarket — EPERM ... rename '…argparse_tmp_19856_4' -> '…argparse'" is. dshmarket 1.15.0 made this reachable by gaining runtime dependencies (js-yaml -> argparse, undici) where 1.9.0 had none. pnpm is reached by name through the packaged shim by every profile package operation — the desktop installer and the community market alike — so the shim now points at a runner that owns the recovery for both: retry once (a scanner's handle is gone within a second), then move the blocked directory aside and let pnpm install over the freed name. Renaming the directory itself succeeds where replacing its contents does not. Anything unrecognized passes straight through with the same exit code and output. The sidelined copies are swept alongside pnpm's staging directories before Harness next starts, when nothing holds them. Verified on macOS that install and failure propagation still behave through the new shim path (dshmarket@1.15.0 installs; a bad spec still exits 1). The recovery itself is covered by unit tests over the real failure text — its effect needs a Windows run to confirm.
Two things the first cut of the runner got wrong, both found by a Windows report that looked exactly like the one before it. It could not be told apart from an unwrapped pnpm. The runner propagates pnpm's own diagnostics verbatim when recovery does not help, so an unchanged error message proved nothing about whether the runner ran at all. Every step now announces itself on the same stream the market reports verbatim: a failure report without "dsh-desktop pnpm runner:" lines is a report from a pnpm this runner never wrapped, which is the first thing to establish before reading anything else into it. And it made a failing install slower — up to three pnpm runs where the hosts already allow fifteen minutes for one, which is what a failed install feeling like a hang is made of. A run that stops producing output for two minutes (DSH_DESKTOP_PNPM_IDLE_TIMEOUT_MS) is now stopped rather than waited out, and a run stopped that way is not retried: whatever wedged it is still there. The Windows kill takes the process tree, since pnpm leaves children behind. A runner that cannot be staged no longer takes the shims down with it: pnpm stays reachable without the recovery, and the harness log says which of the two happened.
A pnpm run that dies partway through leaves a directory under the profile's node_modules carrying a package's name without being one — the Windows locked rename is the failure that keeps producing them. pnpm cannot rename its staging directory onto that name afterwards, so the profile stays stuck and every later attempt fails identically, whatever version of DSH Desktop is installed over it. Upgrading has to be enough to get out of that state. Nothing holds those directories before Harness starts, which makes the launch path the one place they can be cleared safely — so that is where the repair goes: clear the directories that are not packages, then reinstall the profile's dependencies with Harness still stopped. Clearing alone would amputate, since the packages are still wanted; the existing prune now runs after the repair, so it only drops what the install could not restore. An undamaged profile costs one directory scan. A repair failure is not fatal: the prune still keeps the profile bootable, and both outcomes are written to the Harness log, which is where someone diagnosing a failed install is already looking.
The idle timeout could hang on exactly the platform it was written for. killTree returned as soon as taskkill was spawned, so a taskkill that misses left the runner waiting forever on an exit that never comes — the hang the idle timeout exists to prevent. Windows CI caught it as a five-second test timeout. The direct kill is now the guarantee and taskkill only adds the tree, and a killed run is written off after a grace period regardless, so no failure of the kill itself can turn into a wait. Both paths are covered, with the kill injected so the test states the behavior instead of the platform. Also drop a directory symlink from the repair test onto a junction, which Windows can create without Developer Mode or elevation.
main #142 upgraded DSH to 0.1.1-rc.1 and reworked the macOS utility-process options into an object, which is where all three conflicts sat. The disclaim decision is main's: it now travels as `{ disclaim: !developmentBuild }` rather than this branch's boolean parameter, so 5733b85's intent survives in main's shape and the branch drops its own variant. The Harness-children fix on top of it is unaffected — it never touched disclaim, only whether the children of the utility process are addressable as Node.
… release workflow
The idle timeout fired on Windows against a live install. Silence was the wrong signal: without a TTY pnpm drops its progress display, so resolution, a cold download and a large link phase can each pass without a single line, and two minutes of quiet is ordinary rather than wedged. A fail-fast that turns a slow install into a failed one is worse than the wait it saves. Liveness now also counts what a run cannot fake — changes under the profile it installs into — and the allowance goes to five minutes of neither output nor filesystem activity, still well inside the hosts' fifteen-minute ceiling that made a wedged install feel like a hang. The report says which of the two was missing, so a future firing is readable. That the timeout fired at all is worth recording: only this runner prints that line, so the packaged shim does reach it on Windows — which had been the open question behind every identical-looking failure report so far.
The recovery was skipped in the one case it was written for. pnpm raises the locked rename inside a worker and does not always unwind from it, so the run reports the blocker and then stops responding — and the runner, having had to stop it, treated it as undiagnosable and returned without moving anything aside. A stopped run says nothing about whether its blocker can be cleared; what decides that is whether the output names one, so that is what the branch tests now. Being stopped for silence is also no longer the same event as failing. Once the blocked rename is in the output the outcome is decided, so the run gets twenty seconds to exit on its own (DSH_DESKTOP_PNPM_FAILURE_STALL_MS) rather than the five-minute idle allowance meant for a run nobody can read yet. The two-minute silence reported from Windows was this: dsh-market always passes --reporter=ndjson, which narrates every step, so a run that says nothing for that long has stopped working rather than gone quiet — the worker had already died on the rename.
The Windows log showed the repair running against a live Harness: on a restart, launchHarness() repaired before runtime.start(), and start() is what stops the previous process. So the one step that assumes nothing holds the profile ran in exactly the condition it exists to avoid, and its reinstall failed — clearing three damaged directories without putting the packages back, which leaves the profile worse than it found it. The launch path now stops Harness itself before repairing. Two things the same log made visible: The desktop and the Harness-side installer both write shims into <dshHome>/.desktop-bin, but only one of them routed pnpm through the lock-recovery runner. A desktop-written shim replaced a runner-routed pnpm with a plain one, silently dropping the recovery until Harness next rewrote them. Both writers now share the same command. And a failed repair reported "dsh: pnpm failed in profile directory …" — dsh's own wrapper line, which is always last and names no cause. The line that names one is reported instead, so the next failure is readable.
Updating a plugin never succeeded on a Windows account whose name is not
ASCII. `C:\Users\数据项素\...` is every path a profile has, and Node's
recursive `rm` removes nothing under such a path while still resolving
successfully:
rmSync('C:\Users\Public\ascii', { recursive: true }) -> gone
rmSync('C:\Users\Public\中文', { recursive: true }) -> still there
That is the whole failure. Replacing a package means renaming a staging
directory onto an existing one, which Windows refuses outright; pnpm's
recovery deletes the destination first and retries for a minute, and its
delete is `fs.rmSync(p, { recursive: true, force: true })` — so the
destination never goes away and all sixty seconds are spent losing. Each
blocked package costs a minute before ERR_PNPM_EPERM, and the desktop's
own sweeps were the same call: one reported twelve cleared directories
over a profile where all twelve were still on disk.
Every cleanup now walks the tree itself — unlink each file, rmdir on the
way out, symlinks detached rather than followed — and confirms the path
is gone before reporting it. The sweeps reach into a package's own
node_modules too, where a replaced dependency of a dependency stages.
Two things that kept the escape hatches shut are opened with it. The
runner frees every destination the failed run staged for instead of only
the one pnpm named, because an update blocks on several at once and one
per attempt cannot finish inside three. And the launch repair stops
asking for a frozen lockfile: a `pnpm add` that dies while linking has
already written the new version into pnpm-lock.yaml while package.json
still names the old one, so freezing there fails on the divergence that
the repair exists to resolve.
Co-Authored-By: Claude Opus 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.
Bump default recommended
dshmarketversion from1.9.0to1.15.0and update market installer tests.