Skip to content

Commit

Permalink
fix: Handle more fs errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ci010 committed Dec 3, 2024
1 parent 6262c73 commit 812d0c1
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 51 deletions.
2 changes: 1 addition & 1 deletion xmcl
2 changes: 1 addition & 1 deletion xmcl-runtime/install/InstallService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
9 changes: 4 additions & 5 deletions xmcl-runtime/instance/InstanceOptionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
}

Expand Down
22 changes: 15 additions & 7 deletions xmcl-runtime/instance/InstanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -25,7 +25,7 @@ const INSTANCES_FOLDER = 'instances'
export class InstanceService extends StatefulService<InstanceState> implements IInstanceService {
protected readonly instancesFile: SafeFile<InstancesSchema>
protected readonly instanceFile = createSafeIO(InstanceSchema, this)
#removeHandlers: Record<string, (() => Promise<void> | void)[]> = {}
#removeHandlers: Record<string, (WeakRef<() => Promise<void> | void>)[]> = {}

constructor(@Inject(LauncherAppKey) app: LauncherApp,
@Inject(ServiceStateManager) store: ServiceStateManager,
Expand Down Expand Up @@ -352,11 +352,19 @@ export class InstanceService extends StatefulService<InstanceState> 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)
Expand All @@ -366,7 +374,7 @@ export class InstanceService extends StatefulService<InstanceState> implements I
if (!this.#removeHandlers[path]) {
this.#removeHandlers[path] = []
}
this.#removeHandlers[path].push(handler)
this.#removeHandlers[path].push(new WeakRef(handler))
}

/**
Expand Down
16 changes: 13 additions & 3 deletions xmcl-runtime/launch/pluginLaunchPrecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions xmcl-runtime/mod/InstanceModsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand All @@ -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
Expand All @@ -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))
}
Expand Down Expand Up @@ -141,20 +143,31 @@ export class InstanceModsService extends AbstractService implements IInstanceMod
async install({ mods: resources, path }: InstallModsOptions) {
const promises: Promise<void>[] = []
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<any> | 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
}))
}
Expand Down
48 changes: 27 additions & 21 deletions xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -135,36 +135,42 @@ export abstract class AbstractInstanceDomainService extends AbstractService {
return this.store.registerOrGet(key, async () => {
let watcher: ReturnType<ResourceManager['watchSecondary']> | 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()
Expand Down
10 changes: 4 additions & 6 deletions xmcl-runtime/save/InstanceSavesService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
}

Expand Down
8 changes: 5 additions & 3 deletions xmcl-runtime/util/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 812d0c1

Please sign in to comment.