mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-11 05:17:43 +08:00
With HMR, installing, enabling, disabling and removing bundles take effect live. Each profile recomposition compares the loaded modules below the profile directory and its linked targets with disk, stops the plugins whose files changed, and recomposes the profile through app-boot's recomposeProfile(): the rows the next generation drops are removed first, the latest runtime resolution is published once, and the generation is applied and judged. Afterwards HMR evicts the modules below the mappings the recomposition dropped. - app-boot: recomposeProfile(); PluginPackages.resolution; ResolutionRestartRequired; the resolver accepts in-place version changes and relinks, and relocates Node's cached link real paths. - plugin-manager: with HMR, installBundle refuses an installed package (already-installed), so an upgrade is a removal and an installation; every restart-required result carries a restartReason; removal uses the registry the package was installed from. - ui-plugin-manager: the Official Update removes and then installs; restart prompts name their reason.
125 lines
5.3 KiB
TypeScript
125 lines
5.3 KiB
TypeScript
/** Disk identities of loaded module files, used to find modules whose files changed after loading. */
|
|
import { mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
/**
|
|
* Coarsest file timestamp resolution among supported filesystems (FAT stores modification times in 2-second units).
|
|
* An unrecorded file modified within this interval before the last observation is treated as possibly changed.
|
|
*/
|
|
const TIMESTAMP_RESOLUTION_MS = 2000
|
|
|
|
interface FileIdentity {
|
|
/** Inode, size and modification time. */
|
|
key: string
|
|
/** Modification time in epoch milliseconds. */
|
|
modifiedAt: number
|
|
}
|
|
|
|
function identityOf(url: string): FileIdentity | undefined {
|
|
let stats
|
|
try {
|
|
stats = statSync(fileURLToPath(url))
|
|
} catch (_error) {
|
|
// A removed or unreadable file no longer provides the loaded module.
|
|
return undefined
|
|
}
|
|
return { key: `${String(stats.ino)}:${String(stats.size)}:${String(stats.mtimeMs)}`, modifiedAt: stats.mtimeMs }
|
|
}
|
|
|
|
/**
|
|
* Records the inode, size and modification time of loaded `file:` modules. A module is stale when its file
|
|
* disappeared or its identity differs from the recorded one. Change time is not recorded: creating a hard link to a
|
|
* file, as pnpm does when it imports from its store, changes it without changing content. A module first loaded after
|
|
* the last observation has no record; it is stale when its file was modified at or after that observation, less the
|
|
* timestamp resolution, so a reinstall between loading and the next comparison is detected at the cost of reloading a
|
|
* module that was first loaded after such a write.
|
|
*/
|
|
export class ModuleFiles {
|
|
/** Identity per recorded module; `undefined` marks a stale module, such as a pending one first loaded after the last observation. */
|
|
private records = new Map<string, string | undefined>()
|
|
private observedAt = Date.now()
|
|
|
|
/**
|
|
* The current time of the clock the operating system stamps local file writes with, read from a new marker file that is
|
|
* removed again. The system clock and file timestamps can differ by a clock tick in either direction, so module files
|
|
* are compared with this time; when no marker can be created, the system clock is used.
|
|
* @returns Epoch milliseconds.
|
|
*/
|
|
now(): number {
|
|
let directory: string
|
|
try {
|
|
directory = mkdtempSync(join(tmpdir(), 'dsh-hmr-clock-'))
|
|
} catch (_error) {
|
|
// An unwritable temporary directory leaves the less precise system clock.
|
|
return Date.now()
|
|
}
|
|
try {
|
|
const marker = join(directory, 'now')
|
|
writeFileSync(marker, '')
|
|
return statSync(marker).mtimeMs
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare loaded file modules with disk.
|
|
* @param urls `file:` URLs of the loaded modules to compare.
|
|
* @returns URLs of stale modules, and the identity read for each compared URL (`undefined` for a missing file).
|
|
*/
|
|
compare(urls: Iterable<string>): { stale: Set<string>; seen: Map<string, string | undefined> } {
|
|
const stale = new Set<string>()
|
|
const seen = new Map<string, string | undefined>()
|
|
for (const url of urls) {
|
|
const identity = identityOf(url)
|
|
seen.set(url, identity?.key)
|
|
if (identity === undefined) {
|
|
stale.add(url)
|
|
continue
|
|
}
|
|
if (this.records.has(url) ? this.records.get(url) !== identity.key
|
|
: identity.modifiedAt >= this.observedAt - TIMESTAMP_RESOLUTION_MS) stale.add(url)
|
|
}
|
|
return { stale, seen }
|
|
}
|
|
|
|
/**
|
|
* Replace every record with the identities of the given loaded modules.
|
|
* @param urls `file:` URLs of the loaded modules to record.
|
|
* @param options `seen` supplies identities read earlier for modules whose cache entry has not changed since, so a
|
|
* later write stays detectable; other modules are read now, and stay stale when their file was modified after `at`.
|
|
* `retained` modules keep their record, or are marked stale when they have none. `at` is the observation time from
|
|
* {@link now} used for unrecorded modules.
|
|
*/
|
|
observe(urls: Iterable<string>, options: {
|
|
seen?: ReadonlyMap<string, string | undefined>
|
|
retained?: ReadonlySet<string>
|
|
at?: number
|
|
} = {}): void {
|
|
const { seen = new Map<string, string | undefined>(), retained = new Set<string>(), at = this.now() } = options
|
|
const records = new Map<string, string | undefined>()
|
|
for (const url of urls) {
|
|
if (retained.has(url)) records.set(url, this.records.get(url))
|
|
else if (seen.has(url)) records.set(url, seen.get(url))
|
|
else {
|
|
// A module first loaded since `at` may have been evaluated before a write that followed it. A write in the same
|
|
// file clock tick as `at` counts as earlier, so files installed just before the comparison are not reloaded.
|
|
const identity = identityOf(url)
|
|
records.set(url, identity === undefined || identity.modifiedAt > at ? undefined : identity.key)
|
|
}
|
|
}
|
|
this.records = records
|
|
this.observedAt = at
|
|
}
|
|
|
|
/**
|
|
* Record the current identity of modules just evaluated again, leaving every other record unchanged.
|
|
* @param urls `file:` URLs of the re-evaluated modules.
|
|
*/
|
|
refresh(urls: Iterable<string>): void {
|
|
for (const url of urls) this.records.set(url, identityOf(url)?.key)
|
|
}
|
|
}
|