Replies: 2 comments
Your diagnosis holds on every point I could check — and yes, the rename-before-import is deliberate (it's documented as such)1. Your quote is byte-exact, and the ordering is intentional
/** Move the sections of the removed `settings.yaml` into the active profile once the Loader has settled every entry.
* The document is renamed before the first write, so a partial import never repeats; a section the running
* composition rejects is logged and remains only in the renamed file. */
private async importLegacyDocument(): Promise<void> {
const profile = this.ownerContext.profileContext
const path = join(profile.home, 'settings.yaml')
if (!existsSync(path)) return
const imported = `${path}.imported`
await rename(path, imported)
const sections = parse(await readFile(imported, 'utf8')) as Record<string, object> | null
for (const [section, values] of Object.entries(sections ?? {})) {
const ns = LEGACY_SECTION_ENTRIES[section] ?? section
try {
await this.update(ns, values)
} catch (error) {
this.ownerContext.logger.warn('settings: section %s of %s was not imported into entry %s', section, imported, ns)
this.ownerContext.logger.warn(error)
}
}
this.ownerContext.logger.info('settings: imported %s into profile %s', imported, profile.name)
}So: yes, the rename-before-import is by design, and the doc comment names exactly the property it buys — "a partial import never repeats". But note what that property assumes: that the import cannot fail. Once 2. Your lock analysis is confirmed down to the numbers
const DEFAULT_LOCK_WAIT_MS = 2_000
...
const lockPath = `${filename}.lock`
const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS)
...
await writeFile(lockPath, `${process.pid}\n`, { mode: 0o600, flag: 'wx' })
...
if (Date.now() >= deadline) {
throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`)
}
⇒ Your suggestion (3) is cheap and well-founded: the PID is already sitting in the file. Something like "…(held by pid 20702, not running); remove the lock file to recover" costs one read plus a 3. A second defect you didn't mention: the success line is unconditionalLine 257 logs 4. On your four suggestions
5. Why the welcome notice is the thing that broke (it closes the loop)That acknowledgement is not stored in welcomeNoticeVersion: Volatile<string | undefined> // packages/client/ui-settings-general/src/index.ts:11
welcomeNoticeVersion: z.string().volatile(), // :16and 6. One thing to change in your report: the PR offer
So a PR would not be accepted — posting the diagnosis and the patch in the discussion, as you have, is the right channel (the same document says the team reads Discussions and weighs them when allocating resources). If you want to make it maximally actionable, include the four changed lines for (1) and the two-line change for (3) inline, plus the Your workaround is also correct as written, and I'd keep the "only if the PID is dead" guard — deleting a live holder's lock is how you get two writers. |
补一条:我刚把"失败是不可见的"这条查得更深了,结论是比你说的还要糟我在自己的答复里说了" 那些 export const enum LoggerLevel { ERROR = 0, INFO = 1, WARN = 2, DEBUG = 3 }
...
const targetLevel = exporter.levels?.[this.name] ?? exporter.levels?.default ?? this.level ?? LoggerLevel.INFO
if (targetLevel < level) continue
所以你观察到的"stdout 什么都没有、成功那行即使成功也不出现"是准确的,而且是轻描淡写: 因此第 2 条的最小可靠修法不是调日志级别,而是让 另外修正我自己对第 4 条的轻描淡写我上一条说"服务端消息是现成的、被客户端丢掉了",这话没错,但我把它说得太轻了。查下来它是在更下面一层被丢的: // packages/client/ui-settings/src/client/config-form.ts:139-143
const response = await this.ctx.remote.settings.mutate(this.spec.namespace, ownedOps, revision)
if (!response.ok) {
await this.recover(generation)
return false // ← response.error(含 atomic-write 原文)在这里被丢弃
}那个契约只有布尔值( 还有一个对比,可能对"该等多久"的讨论有用那把锁的另一个消费者等的是 120 秒(插件管理器在同一个 |
Uh oh!
There was an error while loading. Please reload this page.
Environment:
@deepseek-ai/dsh0.1.7-alpha.2 (npm global), Node.js v25.1.0, macOS 15.4.1, profile:webSummary
An orphaned writer lock (
<profile>/package.json.lockleft behind by a killed process) makes the one-shot legacysettings.yamlmigration fail silently and permanently. The user's settings (theme, model defaults, welcome-notice acknowledgement) are dropped, the welcome notice reappears, and acknowledging it fails with a generic "please try again" that can never succeed.I believe the migration behavior is a bug; the lock timeout reporting and the UI error copy are related rough edges that made this very hard to diagnose.
What happened
dsh plugin --profile web add ...process was killed (timeout) while it held the profile writer lock. The lock file survived with the dead PID inside:dsh webboot,SettingsForms.importLegacyDocument()(in@deepseek-ai/dsh-settings) runs:update()goes throughwithFileLock(.../package.json)→ waits 2s →atomic-write: timed out waiting for the writer lock at .../package.json.lock→ caught →logger.warn→ next section. All five sections were dropped..importedback, delete the stale lock, and reboot to recover.welcomeNoticeVersionwas already acknowledged in the old file.{"code":"settings/rejected","message":"atomic-write: timed out waiting for the writer lock at /Users/<u>/.dsh/profiles/web/package.json.lock","details":{"ns":"ui-settings-general"}}Repro steps
dsh plugin addwith a SIGKILL), leaving<profile>/package.json.lockbehind with a dead PID.~/.dsh/settings.yamlin place (pre-0.1.6 layout) and startdsh web.settings.yaml.imported, no section is imported, nothing is printed to stdout, and the migration will never be retried.Why I think this is a bug
Migration is not failure-atomic. Renaming the source file before knowing the imports succeed converts any transient write failure into permanent, silent data loss. Suggestion: import all sections first and rename only after full success — or rename back on failure — so a later boot can retry.
The failure is invisible. Neither
logger.info("settings: imported ...")nor thelogger.warnfor a failed section appeared on stdout in my runs (the success line never appeared either, even when the import worked). A migration that can lose user settings should be loud — or logged to a file.The lock timeout error is not actionable.
withFileLockalready writes the holder's PID into the lock file, but the timeout error doesn't include it. Not auto-deleting orphan locks is a defensible choice ("orphan recovery is an operator action"), but then please include the PID and whether it is still alive in the error:Without this, diagnosis took several steps that an ordinary user would not know to perform.
The UI swallows the real error.
settings/rejectedcarries a precise message, but the welcome notice shows only a generic "请重试" for a condition that retries cannot fix. Passing the server message through (at least in a details/tooltip) would have made this a 10-second fix.Current workaround (for anyone hitting this)
Was the rename-before-import behavior intentional? Happy to open a PR for (1) and (3) if it helps.
All reactions