diff --git a/xmcl b/xmcl index 663d7ddc5..3c52ba15a 160000 --- a/xmcl +++ b/xmcl @@ -1 +1 @@ -Subproject commit 663d7ddc5fc21fc73215a02ab46563915a2742ca +Subproject commit 3c52ba15a906068a121ef4253f9d79acd6d9c715 diff --git a/xmcl-runtime/install/InstallService.ts b/xmcl-runtime/install/InstallService.ts index 02694841e..99b4880cf 100644 --- a/xmcl-runtime/install/InstallService.ts +++ b/xmcl-runtime/install/InstallService.ts @@ -576,7 +576,7 @@ export class InstallService extends AbstractService implements IInstallService { if (isNaN(contentLength)) { throw new Error() } - const localLength = (await stat(path)).size + const localLength = (await stat(path).catch(() => ({ size: 0 }))).size if (contentLength !== localLength) { throw new Error() } diff --git a/xmcl-runtime/instance/InstanceOptionsService.ts b/xmcl-runtime/instance/InstanceOptionsService.ts index 7c12ac869..de94b5020 100644 --- a/xmcl-runtime/instance/InstanceOptionsService.ts +++ b/xmcl-runtime/instance/InstanceOptionsService.ts @@ -97,15 +97,14 @@ export class InstanceOptionsService extends AbstractService implements IInstance }) const instanceService = await this.app.registry.get(InstanceService) - instanceService.registerRemoveHandler(path, () => { + const dispose = () => { watcher.close() - }) + } + instanceService.registerRemoveHandler(path, dispose) await Promise.all([loadOptions(path), loadShaderOptions(path)]) - return [state, () => { - watcher.close() - }] + return [state, dispose] }) } diff --git a/xmcl-runtime/instance/InstanceService.ts b/xmcl-runtime/instance/InstanceService.ts index 7c597e980..89309cf33 100644 --- a/xmcl-runtime/instance/InstanceService.ts +++ b/xmcl-runtime/instance/InstanceService.ts @@ -12,7 +12,7 @@ import { ExposeServiceKey, ServiceStateManager, Singleton, StatefulService } fro import { AnyError } from '~/util/error' import { validateDirectory } from '~/util/validate' import { LauncherApp } from '../app/LauncherApp' -import { copyPassively, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, readdirEnsured } from '../util/fs' +import { copyPassively, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, missing, readdirEnsured } from '../util/fs' import { assignShallow, requireObject, requireString } from '../util/object' import { SafeFile, createSafeFile, createSafeIO } from '../util/persistance' @@ -25,7 +25,7 @@ const INSTANCES_FOLDER = 'instances' export class InstanceService extends StatefulService implements IInstanceService { protected readonly instancesFile: SafeFile protected readonly instanceFile = createSafeIO(InstanceSchema, this) - #removeHandlers: Record Promise | void)[]> = {} + #removeHandlers: Record Promise | void>)[]> = {} constructor(@Inject(LauncherAppKey) app: LauncherApp, @Inject(ServiceStateManager) store: ServiceStateManager, @@ -352,11 +352,19 @@ export class InstanceService extends StatefulService implements I requireString(path) const isManaged = this.isUnderManaged(path) + const lock = this.semaphoreManager.getLock(`remove://${path}`) if (isManaged && await exists(path)) { - for (const handler of this.#removeHandlers[path] || []) { - await handler() - } - await rm(path, { recursive: true, force: true }) + await lock.write(async () => { + if (await missing(path)) return + + const oldHandlers = this.#removeHandlers[path] + for (const handlerRef of oldHandlers || []) { + handlerRef.deref()?.() + } + await rm(path, { recursive: true, force: true }) + + this.#removeHandlers[path] = [] + }) } this.state.instanceRemove(path) @@ -366,7 +374,7 @@ export class InstanceService extends StatefulService implements I if (!this.#removeHandlers[path]) { this.#removeHandlers[path] = [] } - this.#removeHandlers[path].push(handler) + this.#removeHandlers[path].push(new WeakRef(handler)) } /** diff --git a/xmcl-runtime/launch/pluginLaunchPrecheck.ts b/xmcl-runtime/launch/pluginLaunchPrecheck.ts index f217cc902..83a0255b4 100644 --- a/xmcl-runtime/launch/pluginLaunchPrecheck.ts +++ b/xmcl-runtime/launch/pluginLaunchPrecheck.ts @@ -8,6 +8,7 @@ import { InstanceService } from '~/instance' import { JavaService, JavaValidation } from '~/java' import { LaunchService } from '~/launch' import { PeerService } from '~/peer' +import { isSystemError } from '~/util/error' import { linkDirectory, missing } from '~/util/fs' export const pluginLaunchPrecheck: LauncherAppPlugin = async (app) => { @@ -23,17 +24,26 @@ export const pluginLaunchPrecheck: LauncherAppPlugin = async (app) => { // relink if (linkTarget !== fromPath) { await unlink(toPath) - await linkDirectory(fromPath, toPath, launchService) + await linkDirectory(fromPath, toPath, launchService).catch(e => { + e.name = 'LaunchLinkError' + launchService.error(e) + }) } return } const fstat = await stat(toPath).catch((e) => undefined) if (!fstat) { - await linkDirectory(fromPath, toPath, launchService) + await linkDirectory(fromPath, toPath, launchService).catch(e => { + e.name = 'LaunchLinkError' + launchService.error(e) + }) return } await move(toPath, join(toPath + '.bk')) - await linkDirectory(fromPath, toPath, launchService) + await linkDirectory(fromPath, toPath, launchService).catch(e => { + e.name = 'LaunchLinkError' + launchService.error(e) + }) } const ensureLinkFolderFromRoot = async (gameDirectory: string, folder: string) => { const fromPath = getPath(folder) diff --git a/xmcl-runtime/mod/InstanceModsService.ts b/xmcl-runtime/mod/InstanceModsService.ts index 41fb49469..b00921fc5 100644 --- a/xmcl-runtime/mod/InstanceModsService.ts +++ b/xmcl-runtime/mod/InstanceModsService.ts @@ -9,7 +9,7 @@ import { ResourceManager, kResourceWorker } from '~/resource' import { AbstractService, ExposeServiceKey, ServiceStateManager } from '~/service' import { AnyError, isSystemError } from '~/util/error' import { LauncherApp } from '../app/LauncherApp' -import { linkWithTimeoutOrCopy, readdirIfPresent } from '../util/fs' +import { linkDirectory, linkWithTimeoutOrCopy, readdirIfPresent } from '../util/fs' import { InstanceService } from '~/instance' /** @@ -36,6 +36,7 @@ export class InstanceModsService extends AbstractService implements IInstanceMod try { const versions = await modrinthClient.getProjectVersionsByHash(all.map(v => v.hash)) const options = Object.entries(versions).map(([hash, version]) => { + if (!hash) return undefined const f = all.find(f => f.hash === hash) if (f) return { hash: f.hash, metadata: { modrinth: { projectId: version.project_id, versionId: version.id } } } return undefined @@ -51,6 +52,7 @@ export class InstanceModsService extends AbstractService implements IInstanceMod try { const chunkSize = 8 const allChunks = [] as Resource[][] + all = all.filter(a => !!a.hash && !a.isDirectory) for (let i = 0; i < all.length; i += chunkSize) { allChunks.push(all.slice(i, i + chunkSize)) } @@ -141,20 +143,31 @@ export class InstanceModsService extends AbstractService implements IInstanceMod async install({ mods: resources, path }: InstallModsOptions) { const promises: Promise[] = [] this.log(`Install ${resources.length} to ${path}/mods`) + const modDir = join(path, ResourceDomain.Mods) for (const res of resources) { + if (res.startsWith(modDir)) { + // some stupid case + continue + } const src = res - const dest = join(path, ResourceDomain.Mods, basename(res)) + const dest = join(modDir, basename(res)) const [srcStat, destStat] = await Promise.all([stat(src), stat(dest).catch(() => undefined)]) let promise: Promise | undefined if (!destStat) { - promise = linkWithTimeoutOrCopy(src, dest) + if (srcStat.isDirectory()) { + promise = linkDirectory(src, dest, this.logger) + } else { + promise = linkWithTimeoutOrCopy(src, dest) + } } else if (srcStat.ino !== destStat.ino) { promise = unlink(dest).then(() => linkWithTimeoutOrCopy(src, dest)) } if (promise) { promises.push(promise.catch((e) => { - this.error(new Error(`Cannot deploy the resource from ${src} to ${dest}`, { cause: e })) + if (e.name === 'Error') { + e.name = 'ModInstallError' + } throw e })) } diff --git a/xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts b/xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts index 7c5bbfabd..0a86d2ab4 100644 --- a/xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts +++ b/xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts @@ -7,7 +7,7 @@ import { InstanceService } from '~/instance' import { kMarketProvider } from '~/market' import { ResourceManager } from '~/resource' import { AbstractService, ServiceStateManager } from '~/service' -import { AnyError } from '~/util/error' +import { AnyError, isSystemError } from '~/util/error' import { isNotFoundError, linkOrCopyFile } from '../util/fs' import { isLinked, readdirSafe, tryLink } from '../util/linkResourceFolder' @@ -135,36 +135,42 @@ export abstract class AbstractInstanceDomainService extends AbstractService { return this.store.registerOrGet(key, async () => { let watcher: ReturnType | undefined - const folder = join(instancePath, this.domain) - - if (existsSync(folder)) { - if (!await this.isLinked(instancePath)) { + const tryWatch = () => { + try { watcher = manager.watchSecondary( instancePath, this.domain, ) + this.log(`Watching instance ${instancePath} for ${this.domain}`) + } catch (e) { + if (isSystemError(e)) { + if (e.code === 'ENOENT') { + // ignore + this.log(`Instance ${instancePath} not exist. Skip watching.`) + } else { + throw e + } + } } } + if (!await this.isLinked(instancePath)) { + tryWatch() + } + const instanceService = await this.app.registry.get(InstanceService) - instanceService.registerRemoveHandler(instancePath, () => { + const dispose = () => { watcher?.dispose() - }) + } + instanceService.registerRemoveHandler(instancePath, dispose) - return [this.state, () => { - watcher?.dispose() - }, async () => { - if (existsSync(folder)) { - const isLinked = await this.isLinked(instancePath) - if (!isLinked && !watcher) { - watcher = manager.watchSecondary( - instancePath, - this.domain, - ) - } else if (isLinked && watcher) { - watcher.dispose() - watcher = undefined - } + return [this.state, dispose, async () => { + const isLinked = await this.isLinked(instancePath) + if (!isLinked && !watcher) { + tryWatch() + } else if (isLinked && watcher) { + watcher.dispose() + watcher = undefined } await watcher?.revalidate() diff --git a/xmcl-runtime/save/InstanceSavesService.ts b/xmcl-runtime/save/InstanceSavesService.ts index b69da989e..7e95c5224 100644 --- a/xmcl-runtime/save/InstanceSavesService.ts +++ b/xmcl-runtime/save/InstanceSavesService.ts @@ -261,15 +261,13 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa }) const instanceService = await this.app.registry.get(InstanceService) - instanceService.registerRemoveHandler(path, () => { + const dispose = () => { launchService.off('minecraft-exit', onExit) watcher?.close() - }) + } + instanceService.registerRemoveHandler(path, dispose) - return [state, () => { - launchService.off('minecraft-exit', onExit) - watcher?.close() - }, revalidate] + return [state, dispose, revalidate] }) } diff --git a/xmcl-runtime/util/fs.ts b/xmcl-runtime/util/fs.ts index de1ea15f3..1b64a6c74 100644 --- a/xmcl-runtime/util/fs.ts +++ b/xmcl-runtime/util/fs.ts @@ -170,9 +170,11 @@ export async function linkDirectory(srcPath: string, destPath: string, logger: L await symlink(srcPath, destPath, 'dir') return true } catch (e) { - logger.warn(`Cannot create symbolic link ${srcPath} -> ${destPath} by dir, try junction: ${e}`) - if ((e as any).code === EPERM_ERROR && platform() === 'win32') { - await symlink(srcPath, destPath, 'junction') + if ((e as any).code === EPERM_ERROR && process.platform === 'win32') { + await symlink(srcPath, destPath, 'junction').catch(e => { + e.junction = true + throw e + }) return false } throw e