diff --git a/xmcl b/xmcl index 0a5ac58e6..2659ae02a 160000 --- a/xmcl +++ b/xmcl @@ -1 +1 @@ -Subproject commit 0a5ac58e697b3e477a8412700c598dad3f1bfdfa +Subproject commit 2659ae02a802e67f2c8c52fba1c8cbac95871dcb diff --git a/xmcl-electron-app/main/ElectronController.ts b/xmcl-electron-app/main/ElectronController.ts index 3974369c4..a97e3e2d1 100644 --- a/xmcl-electron-app/main/ElectronController.ts +++ b/xmcl-electron-app/main/ElectronController.ts @@ -354,6 +354,7 @@ export class ElectronController implements LauncherAppController { browser.webContents.on('did-create-window', this.onWebContentCreateWindow) browser.webContents.setWindowOpenHandler(this.windowOpenHandler) browser.on('closed', () => { + this.mainWin = undefined this.multiplayerRef?.close() }) diff --git a/xmcl-keystone-ui/src/util/error.ts b/xmcl-keystone-ui/src/util/error.ts new file mode 100644 index 000000000..08f9a87c4 --- /dev/null +++ b/xmcl-keystone-ui/src/util/error.ts @@ -0,0 +1,9 @@ +export class AnyError extends Error { + constructor(name: string, message?: string, options?: ErrorOptions, properties?: any) { + super(message, options) + this.name = name + if (properties) { + Object.assign(this, properties) + } + } +} diff --git a/xmcl-runtime/app/LauncherApp.ts b/xmcl-runtime/app/LauncherApp.ts index 1e97565c3..d34986675 100644 --- a/xmcl-runtime/app/LauncherApp.ts +++ b/xmcl-runtime/app/LauncherApp.ts @@ -24,7 +24,7 @@ import { Shell } from './Shell' import { kGameDataPath, kTempDataPath } from './gameDataPath' import { InjectionKey, ObjectFactory } from './objectRegistry' -export const LauncherAppKey: InjectionKey = Symbol('LauncherAppKeyunchAppKey') +export const LauncherAppKey: InjectionKey = Symbol('LauncherAppKey') export interface LauncherApp { on(channel: 'app-booted', listener: (manifest: InstalledAppManifest) => void): this @@ -149,7 +149,7 @@ export class LauncherApp extends EventEmitter { /** * The disposers to dispose when the app is going to quit. */ - #disposers: (() => Promise)[] = [] + #disposers: (() => (Promise | void))[] = [] protected logger: Logger = this.getLogger('App') @@ -211,12 +211,12 @@ export class LauncherApp extends EventEmitter { * Reigster the disposer. The disposer will be called when the app is going to quit. * @param disposer The function to dispose the resource */ - registryDisposer(disposer: () => Promise) { + registryDisposer(disposer: () => Promise | void) { this.#disposers.push(disposer) } async dispose() { - await Promise.all(this.#disposers.map(m => m().catch(() => { }))) + await Promise.allSettled(this.#disposers.map(m => m())) } /** diff --git a/xmcl-runtime/app/objectRegistry.ts b/xmcl-runtime/app/objectRegistry.ts index c89b8623a..6b7668f2d 100644 --- a/xmcl-runtime/app/objectRegistry.ts +++ b/xmcl-runtime/app/objectRegistry.ts @@ -69,7 +69,7 @@ export class ObjectFactory { if (type) { params[i] = await this.getOrCreate(type) } else { - throw new AnyError('ObjectRegistryError', `Fail to get [${i}] param type for ${typeof Type === 'symbol' ? Type.toString() : (Type as Function).name} since it's not registered`) + throw new AnyError('ObjectRegistryError', `Fail to get [${i}](${type}) param type for ${typeof Type === 'symbol' ? Type.toString() : (Type as Function).name} since it's not registered`) } } } @@ -85,12 +85,11 @@ export class ObjectFactory { } type Constructor = (new (...args: any[]) => T) | (abstract new (...args: any[]) => T) -type ConstructorParameter = T extends (new (...args: infer P) => X) ? P : never const kParams = Symbol('params') export interface InjectionKey extends Symbol { } -export function Inject | InjectionKey>(con: V/* , ...args: V extends Constructor ? ConstructorParameter : never[] */) { +export function Inject | InjectionKey>(con: V) { return (target: any, _key: any, index: number) => { if (Reflect.has(target, kParams)) { Reflect.get(target, kParams)[index] = con diff --git a/xmcl-runtime/install/InstallService.ts b/xmcl-runtime/install/InstallService.ts index 06fde8833..02694841e 100644 --- a/xmcl-runtime/install/InstallService.ts +++ b/xmcl-runtime/install/InstallService.ts @@ -245,7 +245,7 @@ export class InstallService extends AbstractService implements IInstallService { @Lock((v) => [LockKey.version(v.minecraftVersion)]) async installLabyModVersion(options: InstallLabyModOptions) { const location = this.getPath() - const task = installLabyMod4Task(options.manifest, options.minecraftVersion, location, this.getInstallOptions()).setName('installLabyMod', { version: options.manifest.labyModVersion }) + const task = installLabyMod4Task(options.manifest, options.minecraftVersion, location, { ...this.getInstallOptions(), fetch: this.app.fetch }).setName('installLabyMod', { version: options.manifest.labyModVersion }) const version = await this.submit(task) return version } @@ -401,6 +401,10 @@ export class InstallService extends AbstractService implements IInstallService { const installOptions = this.getForgeInstallOptions() const side = options.side ?? 'client' + if (!validJavaPaths.length) { + throw new AnyError('ForgeInstallError', 'No valid java found!') + } + validJavaPaths.sort((a, b) => a.majorVersion === 8 ? -1 : b.majorVersion === 8 ? 1 : -1) const setting = await this.app.registry.get(kSettings) diff --git a/xmcl-runtime/instanceIO/InstanceFileDiscover.ts b/xmcl-runtime/instanceIO/InstanceFileDiscover.ts index 96041e87f..9d68c74bd 100644 --- a/xmcl-runtime/instanceIO/InstanceFileDiscover.ts +++ b/xmcl-runtime/instanceIO/InstanceFileDiscover.ts @@ -98,15 +98,17 @@ export async function decorateInstanceFiles(files: [InstanceFile, Stats][], const filePath = join(instancePath, relativePath) const ino = stat.ino if (isSpecialFile(relativePath)) { - const sha1 = sha1Lookup[ino] ?? await worker.checksum(filePath, 'sha1') - sha1Lookup[ino] = sha1 + const sha1 = sha1Lookup[ino] || await worker.checksum(filePath, 'sha1') + localFile.hashes.sha1 = sha1 } } - const metadataLookup = await resourceManager.getMetadataByHashes(Object.values(sha1Lookup)).then(v => { + const exsitedSha1 = files.map(f => f[0].hashes.sha1).filter(isNonnull) + + const metadataLookup = await resourceManager.getMetadataByHashes(exsitedSha1).then(v => { return Object.fromEntries(v.filter(isNonnull).map(m => [m.sha1, m])) }) - const urisLookup = await resourceManager.getUrisByHash(Object.values(sha1Lookup)).then(v => { + const urisLookup = await resourceManager.getUrisByHash(exsitedSha1).then(v => { return v.reduce((acc, cur) => { if (!acc[cur.sha1]) { acc[cur.sha1] = [] @@ -119,9 +121,8 @@ export async function decorateInstanceFiles(files: [InstanceFile, Stats][], for (const [localFile, stat] of files) { const relativePath = localFile.path const filePath = join(instancePath, relativePath) - const ino = stat.ino if (isSpecialFile(relativePath)) { - const sha1 = sha1Lookup[ino] + const sha1 = localFile.hashes.sha1 const metadata = metadataLookup[sha1] if (metadata?.modrinth) { localFile.modrinth = { @@ -138,14 +139,20 @@ export async function decorateInstanceFiles(files: [InstanceFile, Stats][], const uris = urisLookup[sha1] localFile.downloads = uris && uris.some(u => u.startsWith('http')) ? uris.filter(u => u.startsWith('http')) : undefined - localFile.hashes = await resolveHashes(filePath, worker, hashes, sha1) + localFile.hashes = { + ...localFile.hashes, + ...await resolveHashes(filePath, worker, hashes, sha1), + } // No download url... if ((!localFile.downloads || localFile.downloads.length === 0) && metadata) { undecoratedResources.add(localFile) } } else { - localFile.hashes = await resolveHashes(filePath, worker) + localFile.hashes = { + ...localFile.hashes, + ...await resolveHashes(filePath, worker, hashes), + } } } } diff --git a/xmcl-runtime/instanceIO/InstanceManifestService.ts b/xmcl-runtime/instanceIO/InstanceManifestService.ts index c8f17d22c..d46c38cbe 100644 --- a/xmcl-runtime/instanceIO/InstanceManifestService.ts +++ b/xmcl-runtime/instanceIO/InstanceManifestService.ts @@ -89,6 +89,7 @@ export class InstanceManifestService extends AbstractService implements IInstanc }).startAndWait() const updates = [...pendingResourceUpdates].map((file) => { + if (!file.hashes.sha1) return undefined return { hash: file.hashes.sha1, metadata: { diff --git a/xmcl-runtime/resource/core/watchResourcesDirectory.ts b/xmcl-runtime/resource/core/watchResourcesDirectory.ts index dd9319b29..9def26423 100644 --- a/xmcl-runtime/resource/core/watchResourcesDirectory.ts +++ b/xmcl-runtime/resource/core/watchResourcesDirectory.ts @@ -28,7 +28,14 @@ function createRevalidateFunction( onResourcePostRevalidate: (files: File[]) => void, ) { async function getUpserts() { - const entries = await getFiles(dir) + const entries = await getFiles(dir).catch((e) => { + if (isSystemError(e)) { + if (e.code === 'ENOENT') { + return [] + } + } + throw e + }) const inos = entries.map(e => e.ino) const records: Record = await context.db.selectFrom('snapshots') .selectAll() diff --git a/xmcl-runtime/save/InstanceSavesService.ts b/xmcl-runtime/save/InstanceSavesService.ts index 3b9f75140..ae552eaee 100644 --- a/xmcl-runtime/save/InstanceSavesService.ts +++ b/xmcl-runtime/save/InstanceSavesService.ts @@ -240,17 +240,21 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa if (newIsLink !== isLinkedMemo) { isLinkedMemo = !!newIsLink tryWatch() - const savePaths = await readdir(savesDir) + const savePaths = await readdir(savesDir).catch(() => []) const saves = await readAll(savePaths) state.instanceSaves(saves) } else if (!newIsLink) { - const savePaths = await readdir(savesDir) - if (savePaths.length !== state.saves.length) { - const toRemove = state.saves.filter((s) => !savePaths.includes(basename(s.path))) - toRemove.forEach((s) => state.instanceSaveRemove(s.path)) - const toAdd = savePaths.filter((s) => !state.saves.some((ss) => ss.name === s)) - const saves = await readAll(toAdd) - state.instanceSaves(saves) + const savePaths = await readdir(savesDir).catch(() => undefined) + if (!savePaths) { + state.instanceSaves([]) + } else { + if (savePaths.length !== state.saves.length) { + const toRemove = state.saves.filter((s) => !savePaths.includes(basename(s.path))) + toRemove.forEach((s) => state.instanceSaveRemove(s.path)) + const toAdd = savePaths.filter((s) => !state.saves.some((ss) => ss.name === s)) + const saves = await readAll(toAdd) + state.instanceSaves(saves) + } } } }) diff --git a/xmcl-runtime/user/OfficialUserService.ts b/xmcl-runtime/user/OfficialUserService.ts index 089f25b39..4ff9d5a77 100644 --- a/xmcl-runtime/user/OfficialUserService.ts +++ b/xmcl-runtime/user/OfficialUserService.ts @@ -1,8 +1,8 @@ import { OfficialUserService as IOfficialUserService, OfficialUserServiceKey, UserProfile } from '@xmcl/runtime-api' import { MojangChallengeResponse, MojangClient } from '@xmcl/user' -import { LauncherApp, LauncherAppKey, Inject } from '~/app' +import { Inject, LauncherApp, LauncherAppKey } from '~/app' import { AbstractService, ExposeServiceKey } from '~/service' -import { UserTokenStorage, kUserTokenStorage } from '~/user' +import { kUserTokenStorage } from '~/user' import { AnyError } from '../util/error' const UserAuthenticationError = AnyError.make('UserAuthenticationError') @@ -10,13 +10,13 @@ const UserAuthenticationError = AnyError.make('UserAuthenticationError') @ExposeServiceKey(OfficialUserServiceKey) export class OfficialUserService extends AbstractService implements IOfficialUserService { constructor(@Inject(LauncherAppKey) app: LauncherApp, - @Inject(kUserTokenStorage) private userTokenStorage: UserTokenStorage, @Inject(MojangClient) private mojangApi: MojangClient) { super(app) } async setName(user: UserProfile, name: string) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -24,7 +24,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async getNameChangeInformation(user: UserProfile) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -33,7 +34,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async checkNameAvailability(user: UserProfile, name: string) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -42,7 +44,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async hideCape(user: UserProfile) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -50,7 +53,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async showCape(user: UserProfile, capeId: string) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -58,7 +62,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async verifySecurityLocation(user: UserProfile) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -66,7 +71,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async getSecurityChallenges(user: UserProfile) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() } @@ -74,7 +80,8 @@ export class OfficialUserService extends AbstractService implements IOfficialUse } async submitSecurityChallenges(user: UserProfile, answers: MojangChallengeResponse[]) { - const token = await this.userTokenStorage.get(user) + const userTokenStorage = await this.app.registry.get(kUserTokenStorage) + const token = await userTokenStorage.get(user) if (!token) { throw new UserAuthenticationError() }