fix(desktop): adopt an unmarked copy of a package's own desktop half
Folders already installed by shipped builds have no marker, and reconcile refused them on every pass, so the stuck "copying…" row never recovered. Reconcile now stamps such a folder in place when its `plugin.js` is byte for byte the package's own half — that can only be our pre-marker copy, and adopting it writes no files. Anything that differs is a standalone plugin the user installed and is still never touched (#112450). Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
This commit is contained in:
@@ -182,6 +182,25 @@ describe('reconcileUnifiedDesktopHalves', () => {
|
||||
expect(fs.readFileSync(path.join(appRoot, 'media', 'plugin.js'), 'utf8')).toBe('user standalone')
|
||||
})
|
||||
|
||||
it('adopts an unmarked copy of its own half that a pre-marker install left behind', async () => {
|
||||
const home = makeHome()
|
||||
const appRoot = path.join(home, 'desktop-plugins')
|
||||
// What `installDesktopPluginFromGit` used to publish: the package's half,
|
||||
// byte for byte, with no marker. The page showed it as a second, enabled
|
||||
// row while the agent row waited on "copying…".
|
||||
write(path.join(home, 'plugins', 'media', 'desktop', 'plugin.js'), 'package half')
|
||||
write(path.join(appRoot, 'media', 'plugin.js'), 'package half')
|
||||
|
||||
expect(await reconcileUnifiedDesktopHalves(home, appRoot)).toEqual([path.join(appRoot, 'media')])
|
||||
|
||||
const marker = JSON.parse(fs.readFileSync(path.join(appRoot, 'media', PACKAGE_MARKER), 'utf8'))
|
||||
expect(marker.package).toBe('media')
|
||||
// Adoption is a stamp, not a copy: the folder's own files are untouched and
|
||||
// the next pass has nothing left to do.
|
||||
expect(fs.readFileSync(path.join(appRoot, 'media', 'plugin.js'), 'utf8')).toBe('package half')
|
||||
expect(await reconcileUnifiedDesktopHalves(home, appRoot)).toEqual([])
|
||||
})
|
||||
|
||||
it('replaces an interrupted marker-less copy, stamps it, and converges on retry', async () => {
|
||||
const home = makeHome()
|
||||
const appRoot = path.join(home, 'desktop-plugins')
|
||||
|
||||
@@ -146,12 +146,28 @@ async function readMarker(dir: string): Promise<DesktopHalfMarker | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Same bytes on both paths. Unreadable either side answers `false` — a
|
||||
* comparison we cannot make is never evidence of a match. */
|
||||
async function sameFile(a: string, b: string): Promise<boolean> {
|
||||
try {
|
||||
const [left, right] = await Promise.all([fs.promises.readFile(a), fs.promises.readFile(b)])
|
||||
|
||||
return left.equals(right)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the marker into a desktop-half folder. The one place that serializes
|
||||
* it, so the git installer and this reconcile cannot drift. */
|
||||
export async function writeDesktopHalfMarker(dir: string, marker: DesktopHalfMarker): Promise<void> {
|
||||
await fs.promises.writeFile(path.join(dir, PACKAGE_MARKER), JSON.stringify(marker, null, 2) + '\n')
|
||||
}
|
||||
|
||||
/** Copy one unified package's `desktop/` half into the app root as
|
||||
* `<appRoot>/<packageName>/`, stamping the marker. Skips when the root copy is
|
||||
* already current for this source; replaces it when the source is newer. A
|
||||
* root folder of the same name WITHOUT a marker is a standalone install the
|
||||
* user made on purpose and is never overwritten. Returns the target path
|
||||
* when a copy happened. */
|
||||
* already current for this source; replaces it when the source is newer.
|
||||
* Returns the target path when a copy (or an adoption) happened. */
|
||||
export async function materializeDesktopHalf(
|
||||
packageDir: string,
|
||||
appRoot: string,
|
||||
@@ -179,12 +195,36 @@ export async function materializeDesktopHalf(
|
||||
const target = path.join(appRoot, packageName)
|
||||
const existing = await readMarker(target)
|
||||
|
||||
const marker: DesktopHalfMarker = {
|
||||
package: packageName,
|
||||
source: sourceDir,
|
||||
sourceMtimeMs: stat.mtimeMs,
|
||||
...(await packageOrigin(packageDir))
|
||||
}
|
||||
|
||||
if (fs.existsSync(target)) {
|
||||
if (!existing) {
|
||||
// A real standalone install has its entry point. A marker-less folder
|
||||
// without one is an interrupted unified-package copy: the old copy
|
||||
// wrote the marker last, so leaving it here would block every retry.
|
||||
// A marker-less folder carrying the entry point is either a standalone
|
||||
// plugin the user installed on purpose (never touch it) or a desktop half
|
||||
// this app copied out before it stamped markers — `installDesktopPluginFromGit`
|
||||
// published without one, which left the Plugins page waiting on "copying…"
|
||||
// beside a second, already-enabled row, forever, because this function
|
||||
// then refused the folder on every pass.
|
||||
//
|
||||
// Identical entry points tell the two apart: our own copy of this
|
||||
// package's half still matches it byte for byte, so adopting it is a
|
||||
// no-op on disk — stamp the marker in place and the row pairs, with the
|
||||
// opt-in posture a marker implies. Anything that differs is the user's
|
||||
// and is left exactly as it was (#112450). A marker-less folder with no
|
||||
// entry point is an interrupted copy (the marker is written last) and is
|
||||
// replaced as before.
|
||||
if (fs.existsSync(path.join(target, 'plugin.js'))) {
|
||||
if (await sameFile(path.join(target, 'plugin.js'), entry)) {
|
||||
await writeDesktopHalfMarker(target, marker)
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -194,16 +234,7 @@ export async function materializeDesktopHalf(
|
||||
}
|
||||
}
|
||||
|
||||
const marker: DesktopHalfMarker = {
|
||||
package: packageName,
|
||||
source: sourceDir,
|
||||
sourceMtimeMs: stat.mtimeMs,
|
||||
...(await packageOrigin(packageDir))
|
||||
}
|
||||
|
||||
await publishDesktopTree(sourceDir, target, staged =>
|
||||
fs.promises.writeFile(path.join(staged, PACKAGE_MARKER), JSON.stringify(marker, null, 2) + '\n')
|
||||
)
|
||||
await publishDesktopTree(sourceDir, target, staged => writeDesktopHalfMarker(staged, marker))
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user