Skip to content

Commit

Permalink
fix: Handle more errors to make it robust
Browse files Browse the repository at this point in the history
  • Loading branch information
ci010 committed Dec 4, 2024
1 parent 5ba89d2 commit 9c68c33
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 34 deletions.
2 changes: 1 addition & 1 deletion xmcl
6 changes: 2 additions & 4 deletions xmcl-keystone-ui/src/composables/instanceVersionInstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ function useInstanceVersionInstall(versions: Ref<VersionHeader[]>, servers: Ref<
}

export function useInstanceVersionInstallInstruction(path: Ref<string>, instances: Ref<Instance[]>, resolvedVersion: Ref<InstanceResolveVersion | undefined>, refreshResolvedVersion: () => void, versions: Ref<VersionHeader[]>, servers: Ref<ServerVersionHeader[]>, javas: Ref<JavaRecord[]>) {
const { diagnoseAssetIndex, diagnoseAssets, diagnoseJar, diagnoseLibraries, diagnoseProfile } = useService(DiagnoseServiceKey)
const { diagnoseAssets, diagnoseJar, diagnoseLibraries, diagnoseProfile } = useService(DiagnoseServiceKey)
const { installAssetsForVersion, installForge, installAssets, installMinecraftJar, installLibraries, installNeoForged, installDependencies, installOptifine, installByProfile } = useService(InstallServiceKey)
const { editInstance } = useService(InstanceServiceKey)
const { resolveLocalVersion } = useService(VersionServiceKey)
Expand Down Expand Up @@ -371,14 +371,12 @@ export function useInstanceVersionInstallInstruction(path: Ref<string>, instance
}
}

const assetIndexIssue = await diagnoseAssetIndex(resolved)
const { index: assetIndexIssue, assets: assetsIssue } = await diagnoseAssets(resolved)
if (abortSignal?.aborted) { throw kAbort }

if (assetIndexIssue) {
result.assetIndex = assetIndexIssue
} else {
const assetsIssue = await diagnoseAssets(resolved)
if (abortSignal?.aborted) { throw kAbort }
if (assetsIssue.length > 0) {
result.assets = assetsIssue
}
Expand Down
6 changes: 4 additions & 2 deletions xmcl-runtime-api/src/services/DiagnoseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { ServiceKey } from './Service'

export interface DiagnoseService {
diagnoseLibraries(currentVersion: ResolvedVersion): Promise<LibraryIssue[]>
diagnoseAssetIndex(currentVersion: ResolvedVersion): Promise<AssetIndexIssue | undefined>
diagnoseAssets(currentVersion: ResolvedVersion, strict?: boolean): Promise<AssetIssue[]>
diagnoseAssets(currentVersion: ResolvedVersion, strict?: boolean): Promise<{
index?: AssetIndexIssue
assets: AssetIssue[]
}>
diagnoseJar(currentVersion: ResolvedVersion, side?: 'client' | 'server'): Promise<MinecraftJarIssue | undefined>
diagnoseProfile(version: string, side?: 'client' | 'server', path?: string): Promise<InstallProfileIssueReport | undefined>
}
Expand Down
42 changes: 38 additions & 4 deletions xmcl-runtime/install/DiagnoseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,48 @@ export class DiagnoseService extends AbstractService implements IDiagnoseService
}
}

async diagnoseAssets(currentVersion: ResolvedVersion, strict = false): Promise<AssetIssue[]> {
async diagnoseAssets(currentVersion: ResolvedVersion, strict = false): Promise<{
index?: AssetIndexIssue
assets: AssetIssue[]
}> {
this.log(`Diagnose for version ${currentVersion.id} assets`)
const minecraft = new MinecraftFolder(this.getPath())
const objects: Record<string, { hash: string; size: number }> = (await readFile(minecraft.getAssetsIndex(currentVersion.assets), 'utf-8').then((b) => JSON.parse(b.toString()))).objects

const assetsIssues = await diagnoseAssets(objects, minecraft, { strict, checksum: this.worker.checksum })
const assetIndexIssue = await diagnoseAssetIndex(currentVersion, minecraft, true)
if (assetIndexIssue) {
assetIndexIssue.version = currentVersion.id
return {
index: assetIndexIssue,
assets: [],
}
}

const assetsIndexPath = minecraft.getAssetsIndex(currentVersion.assetIndex?.sha1 ?? currentVersion.assets)

try {
const content = await readFile(assetsIndexPath, 'utf-8').then((b) => JSON.parse(b.toString()))

return assetsIssues
const objects: Record<string, { hash: string; size: number }> = content.objects

const assetsIssues = await diagnoseAssets(objects, minecraft, { strict, checksum: this.worker.checksum })

return {
assets: assetsIssues,
}
} catch (e) {
return {
index: {
type: 'missing',
role: 'assetIndex',
file: assetsIndexPath,
expectedChecksum: currentVersion.assetIndex?.sha1 ?? currentVersion.assets,
receivedChecksum: '',
hint: 'The asset index file is missing',
version: currentVersion.id,
},
assets: [],
}
}
}

async diagnoseJar(currentVersion: ResolvedVersion, side: 'client' | 'server' = 'client'): Promise<MinecraftJarIssue | undefined> {
Expand Down
19 changes: 14 additions & 5 deletions xmcl-runtime/instance/InstanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { ImageStorage } from '~/imageStore'
import { VersionMetadataService } from '~/install'
import { readLaunchProfile } from '~/launchProfile'
import { ExposeServiceKey, ServiceStateManager, Singleton, StatefulService } from '~/service'
import { AnyError } from '~/util/error'
import { AnyError, isSystemError } from '~/util/error'
import { validateDirectory } from '~/util/validate'
import { LauncherApp } from '../app/LauncherApp'
import { copyPassively, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, missing, readdirEnsured } from '../util/fs'
import { copyPassively, ENOENT_ERROR, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, missing, readdirEnsured } from '../util/fs'
import { assignShallow, requireObject, requireString } from '../util/object'
import { SafeFile, createSafeFile, createSafeIO } from '../util/persistance'

Expand Down Expand Up @@ -355,13 +355,22 @@ export class InstanceService extends StatefulService<InstanceState> implements I
const lock = this.semaphoreManager.getLock(`remove://${path}`)
if (isManaged && await exists(path)) {
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 })
try {
await rm(path, { recursive: true, force: true, maxRetries: 3 })
} catch (e) {
if (isSystemError(e) && e.code === ENOENT_ERROR) {
this.warn(`Fail to remove instance ${path}`)
} else {
if ((e as any).name === 'Error') {
(e as any).name = 'InstanceDeleteError'
}
throw e
}
}

this.#removeHandlers[path] = []
})
Expand Down
9 changes: 8 additions & 1 deletion xmcl-runtime/java/JavaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,14 @@ export class JavaService extends StatefulService<JavaState> implements IJavaServ
return result
}

const manifest = await fetchJava(target.component)
const manifest = await fetchJava(target.component).catch(e => {
if (e.name === 'Error') {
if (e.message === 'net::ERR_CONNECTION_RESET') {
e.name = 'ConnectionResetError'
}
}
throw e
})
this.log(`Install jre runtime ${target.component} (${target.majorVersion}) ${manifest.version.name} ${manifest.version.released}`)
const dest = this.getPath('jre', target.component)

Expand Down
9 changes: 8 additions & 1 deletion xmcl-runtime/launch/LaunchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,14 @@ export class LaunchService extends AbstractService implements ILaunchService {

this.log('Launching client with these option...')
this.log(JSON.stringify(op, (k, v) => (k === 'accessToken' ? '***' : v), 2))
process = await this.#track(launch(op), 'spawn-minecraft-process', operationId)
try {
process = await this.#track(launch(op), 'spawn-minecraft-process', operationId)
} catch (e) {
if (isSystemError(e) && e.code === 'EPERM') {
throw new LaunchException({ type: 'launchJavaNoPermission', javaPath: op.javaPath }, 'Fail to spawn process')
}
throw e
}
} else {
launchOptions = await this.#generateServerOptions(options, version)
for (const plugin of this.middlewares) {
Expand Down
8 changes: 7 additions & 1 deletion xmcl-runtime/launch/pluginLaunchPrecheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export const pluginLaunchPrecheck: LauncherAppPlugin = async (app) => {
})
return
}
await move(toPath, join(toPath + '.bk'))
try {
await move(toPath, join(toPath + '.bk'))
} catch (e) {
if ((e as any).message === 'dest already exists.') {
await move(toPath, join(toPath + Date.now() + '.bk'))
}
}
await linkDirectory(fromPath, toPath, launchService).catch(e => {
e.name = 'LaunchLinkError'
launchService.error(e)
Expand Down
6 changes: 4 additions & 2 deletions xmcl-runtime/modpack/ModpackService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ModrinthV2Client } from '@xmcl/modrinth'
import { CreateInstanceOption, CurseforgeModpackManifest, ExportModpackOptions, ModpackService as IModpackService, InstallMarketOptions, Instance, InstanceData, InstanceFile, McbbsModpackManifest, ModpackException, ModpackInstallProfile, ModpackServiceKey, ModpackState, ModrinthModpackManifest, MutableState, ResourceDomain, ResourceMetadata, ResourceState, UpdateResourcePayload, findMatchedVersion, getCurseforgeModpackFromInstance, getMcbbsModpackFromInstance, getModrinthModpackFromInstance, isAllowInModrinthModpack } from '@xmcl/runtime-api'
import { mkdir, readdir, remove, stat, unlink } from 'fs-extra'
import { ensureDir, mkdir, readdir, remove, stat, unlink } from 'fs-extra'
import { dirname, join } from 'path'
import { Entry, ZipFile } from 'yauzl'
import { Inject, LauncherApp, LauncherAppKey, PathResolver, kGameDataPath } from '~/app'
Expand Down Expand Up @@ -474,7 +474,9 @@ export class ModpackService extends AbstractService implements IModpackService {
async watchModpackFolder(): Promise<MutableState<ResourceState>> {
const states = await this.app.registry.getOrCreate(ServiceStateManager)
return states.registerOrGet('modpacks', async ({ doAsyncOperation }) => {
const { dispose, revalidate, state } = this.resourceManager.watch(this.getPath('modpacks'),
const dir = this.getPath('modpacks')
await ensureDir(dir)
const { dispose, revalidate, state } = this.resourceManager.watch(dir,
ResourceDomain.Modpacks,
(func) => doAsyncOperation(func()),
)
Expand Down
34 changes: 22 additions & 12 deletions xmcl-runtime/resourcePack/AbstractInstanceDoaminService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,19 @@ export abstract class AbstractInstanceDomainService extends AbstractService {

const key = `instance-${this.domain}://${instancePath}`

if (typeof linkedStatus === 'boolean') {
await unlink(destPath)
await mkdir(destPath)

this.store.get(key)?.revalidate()
if (linkedStatus) {
try {
await unlink(destPath)
await mkdir(destPath)

this.store.get(key)?.revalidate()
} catch (e) {
if (isSystemError(e) && e.code === 'EPERM') {
return
}
(e as any).name = 'UnlinkResourceFolderError'
throw e
}
}
}

Expand All @@ -70,13 +78,15 @@ export abstract class AbstractInstanceDomainService extends AbstractService {
if (!isLinked) {
// Backup the old folder
const files = await readdirSafe(destPath)
const backupDir = join(destPath, '.backup')
await ensureDir(backupDir)
for (const f of files) {
const s = await stat(join(destPath, f))
if (s.isDirectory()) {
// move to backup dir
await rename(join(destPath, f), join(backupDir, f))
if (files.length > 0) {
const backupDir = join(destPath, '.backup')
await ensureDir(backupDir)
for (const f of files) {
const s = await stat(join(destPath, f))
if (s.isDirectory()) {
// move to backup dir
await rename(join(destPath, f), join(backupDir, f))
}
}
}
await remove(destPath)
Expand Down
2 changes: 1 addition & 1 deletion xmcl-runtime/util/linkResourceFolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { sep } from 'path'

export function readlinkSafe(path: string) {
return readlink(path).catch(e => {
if (isSystemError(e) && e.code === ENOENT_ERROR) {
if (isSystemError(e) && (e.code === ENOENT_ERROR || e.code === 'EINVAL' || e.code === 'EISDIR')) {
return undefined
}
if (!e.stack) {
Expand Down

0 comments on commit 9c68c33

Please sign in to comment.