diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a134c48..765ec6fcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ importers: specifier: ^4.1.7 version: 4.1.7 '@types/node': - specifier: ~18 - version: 18.16.5 + specifier: ~20 + version: 20.12.7 '@types/semver': specifier: ^7.3.13 version: 7.3.13 @@ -412,9 +412,9 @@ importers: atomically: specifier: ^2.0.3 version: 2.0.3 - fast-json-stringify: - specifier: ^5.8.0 - version: 5.8.0 + chokidar: + specifier: ^4.0.3 + version: 4.0.3 fast-xml-parser: specifier: ^4.3.2 version: 4.3.2 @@ -439,9 +439,6 @@ importers: jschardet: specifier: 3.1.2 version: 3.1.2 - jsonwebtoken: - specifier: 9.0.2 - version: 9.0.2 kysely: specifier: ^0.26.1 version: 0.26.1 @@ -460,18 +457,12 @@ importers: node-disk-info: specifier: ^1.3.0 version: 1.3.0 - node-domexception: - specifier: ^2.0.1 - version: 2.0.1 node-sqlite3-wasm: specifier: ^0.8.16 version: 0.8.16 node-watch: specifier: ^0.7.4 version: 0.7.4 - normalize-url: - specifier: ^7.2.0 - version: 7.2.0 tar-stream: specifier: ^3.1.7 version: 3.1.7 @@ -507,8 +498,8 @@ importers: specifier: ^4.0.1 version: 4.0.1 '@types/node': - specifier: ~18 - version: 18.16.5 + specifier: ~20 + version: 20.12.7 '@types/tar-stream': specifier: ^3.1.3 version: 3.1.3 @@ -1780,9 +1771,6 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@fastify/deepmerge@1.3.0': - resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} - '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2236,9 +2224,6 @@ packages: '@types/node@18.16.5': resolution: {integrity: sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==} - '@types/node@20.1.0': - resolution: {integrity: sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==} - '@types/node@20.12.7': resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} @@ -2571,14 +2556,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -2835,14 +2812,14 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3569,15 +3546,9 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-json-stringify@5.8.0: - resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==} - fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@2.2.0: - resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} - fast-xml-parser@4.3.2: resolution: {integrity: sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==} hasBin: true @@ -4437,10 +4408,6 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} - normalize-url@7.2.0: - resolution: {integrity: sha512-uhXOdZry0L6M2UIo9BTt7FdpBDiAGN/7oItedQwPKh8jh31ZlvC8U9Xl/EJ3aijDHaywXTW3QbZ6LuCocur1YA==} - engines: {node: '>=12.20'} - npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -4675,6 +4642,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + regexp.prototype.flags@1.5.0: resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} engines: {node: '>= 0.4'} @@ -4727,9 +4698,6 @@ packages: rfc6902@5.0.1: resolution: {integrity: sha512-tYGfLpKIq9X7lrt4o3IkD9w9bpeAtsejfAqWNR98AoxfTsZqCepKa8eDlRiX8QMiCOD9vMx0/YbKLx0G1nPi5w==} - rfdc@1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -6056,8 +6024,6 @@ snapshots: '@eslint/js@8.57.0': {} - '@fastify/deepmerge@1.3.0': {} - '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -6552,7 +6518,7 @@ snapshots: '@types/graceful-fs@4.1.6': dependencies: - '@types/node': 18.16.5 + '@types/node': 18.15.13 '@types/http-cache-semantics@4.0.1': {} @@ -6582,7 +6548,7 @@ snapshots: '@types/lzma-native@4.0.1': dependencies: - '@types/node': 18.16.5 + '@types/node': 18.15.13 '@types/markdown-it@12.2.3': dependencies: @@ -6599,8 +6565,6 @@ snapshots: '@types/node@18.16.5': {} - '@types/node@20.1.0': {} - '@types/node@20.12.7': dependencies: undici-types: 5.26.5 @@ -6654,15 +6618,15 @@ snapshots: '@types/ws@8.5.4': dependencies: - '@types/node': 18.16.5 + '@types/node': 18.15.13 '@types/yauzl@2.10.0': dependencies: - '@types/node': 20.1.0 + '@types/node': 18.15.13 '@types/yazl@2.4.2': dependencies: - '@types/node': 18.16.5 + '@types/node': 18.15.13 '@types/yazl@2.4.5': dependencies: @@ -7103,10 +7067,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-formats@2.1.1(ajv@8.12.0): - optionalDependencies: - ajv: 8.12.0 - ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -7461,7 +7421,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@3.5.3: + chokidar@3.6.0: dependencies: anymatch: 3.1.3 braces: 3.0.2 @@ -7473,17 +7433,9 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chokidar@3.6.0: + chokidar@4.0.3: dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + readdirp: 4.0.2 chownr@1.1.4: {} @@ -8379,19 +8331,8 @@ snapshots: fast-json-stable-stringify@2.1.0: {} - fast-json-stringify@5.8.0: - dependencies: - '@fastify/deepmerge': 1.3.0 - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - fast-deep-equal: 3.1.3 - fast-uri: 2.2.0 - rfdc: 1.3.0 - fast-levenshtein@2.0.6: {} - fast-uri@2.2.0: {} - fast-xml-parser@4.3.2: dependencies: strnum: 1.0.5 @@ -9263,8 +9204,6 @@ snapshots: normalize-url@6.1.0: {} - normalize-url@7.2.0: {} - npm-run-path@4.0.1: dependencies: path-key: 3.1.1 @@ -9535,6 +9474,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + regexp.prototype.flags@1.5.0: dependencies: call-bind: 1.0.2 @@ -9586,8 +9527,6 @@ snapshots: rfc6902@5.0.1: {} - rfdc@1.3.0: {} - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -10132,7 +10071,7 @@ snapshots: unplugin@1.3.1: dependencies: acorn: 8.10.0 - chokidar: 3.5.3 + chokidar: 3.6.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 diff --git a/xmcl b/xmcl index 3d54b3f77..f056f9aeb 160000 --- a/xmcl +++ b/xmcl @@ -1 +1 @@ -Subproject commit 3d54b3f7787b41c25a3ea9596b97b046d7e9f161 +Subproject commit f056f9aebd3a54b8ca060aeda096f3cc9932b6ca diff --git a/xmcl-electron-app/main/ElectronLauncherApp.ts b/xmcl-electron-app/main/ElectronLauncherApp.ts index 775afcebd..d3da7a9e9 100644 --- a/xmcl-electron-app/main/ElectronLauncherApp.ts +++ b/xmcl-electron-app/main/ElectronLauncherApp.ts @@ -1,16 +1,18 @@ +import { NetworkErrorCode, NetworkException } from '@xmcl/runtime-api' import { LauncherApp, Shell } from '@xmcl/runtime/app' import { LAUNCHER_NAME } from '@xmcl/runtime/constant' import { Menu, app, net, shell } from 'electron' +import { stat } from 'fs-extra' import { join } from 'path' +import { AnyError } from '~/util/error' import { ElectronController } from './ElectronController' import { ElectronSecretStorage } from './ElectronSecretStorage' +import { ElectronSession } from './ElectronSession' import { IS_DEV } from './constant' import defaultApp from './defaultApp' import { definedPlugins } from './definedPlugins' import { ElectronUpdater } from './utils/updater' import { getWindowsUtils } from './utils/windowsUtils' -import { ElectronSession } from './ElectronSession' -import { stat } from 'fs-extra' class ElectronShell implements Shell { showItemInFolder = shell.showItemInFolder @@ -107,8 +109,41 @@ export default class ElectronLauncherApp extends LauncherApp { app.commandLine?.appendSwitch('ozone-platform-hint', 'auto') } - fetch: typeof fetch = (...args: any[]) => { - return net.fetch(args[0], args[1] ? { ...args[1], bypassCustomProtocolHandlers: true } : undefined) as any + fetch: typeof fetch = async (...args: any[]) => { + try { + return await net.fetch(args[0], args[1] ? { ...args[1], bypassCustomProtocolHandlers: true } : undefined) as any + } catch (e) { + if (e instanceof Error) { + let code: NetworkErrorCode | undefined + if (e.message === 'net::ERR_CONNECTION_CLOSED') { + code = NetworkErrorCode.CONNECTION_CLOSED + } else if (e.message === 'net::ERR_INTERNET_DISCONNECTED') { + code = NetworkErrorCode.INTERNET_DISCONNECTED + } else if (e.message === 'net::ERR_TIMED_OUT') { + code = NetworkErrorCode.TIMED_OUT + } else if (e.message === 'net::ERR_CONNECTION_RESET') { + code = NetworkErrorCode.CONNECTION_RESET + } else if (e.message === 'net::ERR_CONNECTION_TIMED_OUT') { + code = NetworkErrorCode.CONNECTION_TIMED_OUT + } else if (e.message === 'net::ERR_NAME_NOT_RESOLVED') { + code = NetworkErrorCode.DNS_NOTFOUND + } else if (e.message === 'net::NETWORK_CHANGED') { + code = NetworkErrorCode.NETWORK_CHANGED + } + if (code) { + // expected exceptions + throw new NetworkException({ + type: 'networkException', + code, + }) + } + // unexpected errors + if (e.message.startsWith('net::')) { + throw new AnyError('NetworkError', e.message) + } + } + throw e + } } windowsUtils = getWindowsUtils(this, this.logger) diff --git a/xmcl-electron-app/package.json b/xmcl-electron-app/package.json index 74addbf90..52b53369a 100644 --- a/xmcl-electron-app/package.json +++ b/xmcl-electron-app/package.json @@ -33,7 +33,7 @@ "@types/graceful-fs": "^4.1.5", "@types/lodash.debounce": "^4.0.7", "@types/lodash.throttle": "^4.1.7", - "@types/node": "~18", + "@types/node": "~20", "@types/semver": "^7.3.13", "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", diff --git a/xmcl-electron-app/preload/service.ts b/xmcl-electron-app/preload/service.ts index 4692a47ff..116cceb0e 100644 --- a/xmcl-electron-app/preload/service.ts +++ b/xmcl-electron-app/preload/service.ts @@ -1,6 +1,6 @@ /* eslint-disable no-dupe-class-members */ -import { AllStates, ServiceChannels, ServiceKey, MutableState, StateMetadata } from '@xmcl/runtime-api' +import { AllStates, ServiceChannels, ServiceKey, SharedState, StateMetadata } from '@xmcl/runtime-api' import { contextBridge, ipcRenderer } from 'electron' import EventEmitter from 'events' @@ -24,7 +24,7 @@ const typeToStatePrototype: Record = AllStates.reduce((ob const kEmitter = Symbol('Emitter') const kMethods = Symbol('Methods') -function createMutableState(val: T, id: string, methods: StateMetadata['methods']): MutableState { +function createSharedState(val: T, id: string, methods: StateMetadata['methods']): SharedState { const emitter = new EventEmitter() Object.defineProperty(val, kEmitter, { value: emitter }) Object.defineProperty(val, kMethods, { value: methods }) @@ -55,7 +55,7 @@ if (process.env.NODE_ENV === 'development') { console.log('serivce.ts preload') } -async function receive(_result: any, states: Record>>, pendingCommits: Record, gc: FinalizationRegistry) { +async function receive(_result: any, states: Record>>, pendingCommits: Record, gc: FinalizationRegistry) { if (typeof _result !== 'object') { return } @@ -86,7 +86,7 @@ async function receive(_result: any, states: Record, WeakRef>() - const states: Record>> = {} + const states: Record>> = {} const pendingCommits: Record = {} ipcRenderer.on('state-validating', (_, { id, semaphore }) => { diff --git a/xmcl-keystone-ui/locales/de.yaml b/xmcl-keystone-ui/locales/de.yaml index 92689123c..2bf127043 100644 --- a/xmcl-keystone-ui/locales/de.yaml +++ b/xmcl-keystone-ui/locales/de.yaml @@ -351,6 +351,7 @@ fileDetail: fileSize: Größe der Datei hash: Hash filter: Filter. +filterLocalOnly: Nur installiert anzeigen finish: Oberfläche forgeConfig: hint: >- @@ -483,6 +484,8 @@ instance: versionHint: Minecraft Version für dieses Spiel vmOptions: JVM-Parameter vmOptionsHint: Zusätzliche Argumente, die an die JVM übergeben werden + vmVar: Umgebungsvariablen + vmVarHint: Klicken Sie auf die Schaltfläche, um Umgebungsvariablen hinzuzufügen instanceAge: older: Alte threeDay: Innerhalb von drei Tagen @@ -580,7 +583,7 @@ java: modifyInstance: Ändern Sie den Pfad zu Java noMemory: Speichernutzung nicht einschränken refresh: Lokales Java aktualisieren - systemMemory: Aktueller Systemspeicher + systemMemory: 'Freier Systemspeicher: {free} / {total}' labyMod: disable: LabyMod ausschalten empty: LabyMod unterstützt nicht das aktuelle Minecraft @@ -818,9 +821,6 @@ modInstall: source: Quelle. switch: Version ändern upgrade: Aktualisierung der Änderungen -modSearchType: - all: Alle - local: Cache auf dem Datenträger modUpgradePolicy: curseforge: Zuerst Curseforge curseforgeOnly: Nur Curseforge diff --git a/xmcl-keystone-ui/locales/en.yaml b/xmcl-keystone-ui/locales/en.yaml index 2e481f4f0..0602b9ed2 100644 --- a/xmcl-keystone-ui/locales/en.yaml +++ b/xmcl-keystone-ui/locales/en.yaml @@ -308,6 +308,7 @@ fileDetail: fileSize: File Size hash: Hash filter: Filter +filterLocalOnly: Only show installed finish: Finish forgeConfig: hint: >- @@ -431,6 +432,8 @@ instance: versionHint: The Minecraft version of this game vmOptions: JVM Options vmOptionsHint: Extra arguments to pass to the JVM + vmVar: Environment Variables + vmVarHint: Click + button to add environemnt variables instanceAge: older: Older threeDay: In Three Days @@ -523,7 +526,7 @@ java: modifyInstance: Modify Java Path noMemory: Do not limit memory usage refresh: Refresh Local Java - systemMemory: Current System Memory + systemMemory: 'System Free Memory: {free} / {total}' labyMod: disable: Disable LabyMod empty: LabyMod does not support current Minecraft @@ -735,11 +738,6 @@ modInstall: source: Mod Source switch: Swtich Version upgrade: Upgrade Mods -modSearchType: - all: All - explore: Explore Market - installed: Installed - local: Disk Cache modUpgradePolicy: curseforge: Curseforge First curseforgeOnly: Curseforge Only diff --git a/xmcl-keystone-ui/locales/es-ES.yaml b/xmcl-keystone-ui/locales/es-ES.yaml index 237d2cc13..f79ee4394 100644 --- a/xmcl-keystone-ui/locales/es-ES.yaml +++ b/xmcl-keystone-ui/locales/es-ES.yaml @@ -274,6 +274,7 @@ feedback: Únete al grupo de QQ de comentarios y habla directamente con los autores. Número del grupo: {number} qqEnterGroup: Unirse +filterLocalOnly: Mostrar solo instalado forgeVersion: common: Común disable: Desactivar Forge @@ -396,6 +397,8 @@ instance: versionHint: La versión de Minecraft de este juego vmOptions: Opciones de JVM vmOptionsHint: Argumentos adicionales para pasar a la JVM + vmVar: Variables de entorno + vmVarHint: Haga clic en el botón para agregar variables ambientales instanceAge: older: Más Antiguo threeDay: En Tres Días @@ -564,9 +567,6 @@ modInstall: source: Fuente de Mods switch: Cambiar Versión upgrade: Actualizar Mods -modSearchType: - all: Todos - local: Caché en Disco modUpgradePolicy: curseforge: Forja maldita primero curseforgeOnly: Sólo forja maldita diff --git a/xmcl-keystone-ui/locales/fr.yaml b/xmcl-keystone-ui/locales/fr.yaml index b50738868..beb6898af 100644 --- a/xmcl-keystone-ui/locales/fr.yaml +++ b/xmcl-keystone-ui/locales/fr.yaml @@ -344,6 +344,7 @@ feedback: auteurs. Numéro de groupe : {number} qqEnterGroup: Rejoindre filter: Filtre +filterLocalOnly: Afficher uniquement installé finish: Valider forgeConfig: hint: >- @@ -468,6 +469,8 @@ instance: versionHint: La version Minecraft de ce jeu vmOptions: Options JVM vmOptionsHint: Arguments supplémentaires transmis à JVM + vmVar: Variables d'environnement + vmVarHint: Cliquez sur le bouton pour ajouter des variables d'environnement instanceAge: older: Plus âgée threeDay: Dans trois jours @@ -564,7 +567,7 @@ java: modifyInstance: Modifier le chemin Java noMemory: Ne pas limiter l'utilisation de la mémoire refresh: Actualiser l'exécutable Java Local - systemMemory: Mémoire Système Actuelle + systemMemory: "Mémoire libre du système\_: {free} / {total}" launch: killServer: Tuer le serveur Localhost launch: Jouer @@ -785,9 +788,6 @@ modInstall: search: Résultats searchHint: Rechercher et sélectionner un projet skipVersion: Ignorez les mods avec une version différente de Minecraft -modSearchType: - all: Tous - local: Cache disque modUpgradePolicy: curseforge: Forgemalédiction en premier curseforgeOnly: Forgemalédiction uniquement diff --git a/xmcl-keystone-ui/locales/gl.yaml b/xmcl-keystone-ui/locales/gl.yaml index 2485339c9..66d1dbc29 100644 --- a/xmcl-keystone-ui/locales/gl.yaml +++ b/xmcl-keystone-ui/locales/gl.yaml @@ -250,6 +250,7 @@ eula: {eula} de Minecraft. fabricVersion: showSnapshot: Mostrar instantáneas +filterLocalOnly: Só mostrar instalado importModpack: name: Importar Modpack instance: @@ -268,6 +269,8 @@ instance: useSharedOptionsDesc: Isto enlazará o options.txt a un ficheiro compartido na instancia useSharedServersList: Usa a lista de servidores compartidos useSharedServersListDesc: Isto enlazará o servers.dat a un ficheiro compartido na instancia + vmVar: Variables de ambiente + vmVarHint: Fai clic no botón para engadir variables de ambiente instanceDiscover: gameFolder: Descubre {count} cartafoles de xogos instanceFolder: Atopáronse {count} instancias diff --git a/xmcl-keystone-ui/locales/hu.yaml b/xmcl-keystone-ui/locales/hu.yaml index a73123764..381a414db 100644 --- a/xmcl-keystone-ui/locales/hu.yaml +++ b/xmcl-keystone-ui/locales/hu.yaml @@ -301,6 +301,7 @@ fileDetail: fileSize: Fájl méret hash: Hash filter: Szűrő +filterLocalOnly: Csak a telepített megjelenítés finish: Befejezés forgeConfig: hint: >- @@ -432,6 +433,8 @@ instance: versionHint: A játék Minecraft verziója vmOptions: JVM Beállítások vmOptionsHint: Extra argumentumok átadása a JVM-nek + vmVar: Környezeti változók + vmVarHint: Kattintson a gombra környezeti változók hozzáadásához instanceAge: older: Régebbi threeDay: Három napon belül @@ -528,7 +531,7 @@ java: modifyInstance: Java elérési útvonal módosítása noMemory: Ne korlátozza a memória használatát refresh: Helyi Java frissítése - systemMemory: Jelenlegi rendszermemória + systemMemory: 'Rendszermentes memória: {free} / {total}' labyMod: disable: LabyMod letiltása empty: A LabyMod nem támogatja a jelenlegi Minecraftot @@ -758,11 +761,6 @@ modInstall: source: Mod forrása switch: Verzió váltás upgrade: Modok frissítése -modSearchType: - all: Összes - explore: Piac felfedezése - installed: Telepítve - local: Lemez gyorsítótár modUpgradePolicy: curseforge: Curseforge először curseforgeOnly: Csak Curseforge diff --git a/xmcl-keystone-ui/locales/it-IT.yaml b/xmcl-keystone-ui/locales/it-IT.yaml index 9933a0ca1..ef6d429fb 100644 --- a/xmcl-keystone-ui/locales/it-IT.yaml +++ b/xmcl-keystone-ui/locales/it-IT.yaml @@ -310,6 +310,7 @@ fileDetail: fileSize: Dimensione del file hash: Hash filter: Filtro +filterLocalOnly: Mostra solo installato finish: Finito forgeConfig: hint: >- @@ -437,6 +438,8 @@ instance: versionHint: La versione di Minecraft di questo gioco vmOptions: Opzioni JVM vmOptionsHint: Argomenti aggiuntivi passati a JVM + vmVar: Variabili d'ambiente + vmVarHint: Fare clic sul pulsante per aggiungere variabili ambientali instanceAge: older: Più vecchio threeDay: In tre giorni @@ -531,7 +534,7 @@ java: modifyInstance: Modifica percorso Java noMemory: Non limitare l'utilizzo della memoria refresh: Aggiorna Java locale - systemMemory: Memoria di sistema attuale + systemMemory: 'Memoria libera del sistema: {free} / {total}' labyMod: disable: Disabilita LabyMod empty: LabyMod non supporta l'attuale versione di Minecraft @@ -758,11 +761,6 @@ modInstall: source: Sorgente Mod switch: Cambia versione upgrade: Aggiorna le Mod -modSearchType: - all: Tutti - explore: Esplora il mercato - installed: Installato - local: Cache del disco modUpgradePolicy: curseforge: Curseforge Maledizione curseforgeOnly: Solo Forgiamaledizione diff --git a/xmcl-keystone-ui/locales/ja-JP.yaml b/xmcl-keystone-ui/locales/ja-JP.yaml index 131dbc3bc..df5a0f643 100644 --- a/xmcl-keystone-ui/locales/ja-JP.yaml +++ b/xmcl-keystone-ui/locales/ja-JP.yaml @@ -321,6 +321,7 @@ fileDetail: fileSize: ファイルサイズ hash: ハッシュ値 filter: フィルター +filterLocalOnly: インストールされているものだけを表示 finish: 終了 forgeConfig: hint: 一回この構成でゲームを立ち上げると、構成ファイルを検出できます! @@ -433,6 +434,8 @@ instance: versionHint: この起動構成におけるMinecraftのバージョン vmOptions: JVM実行引数 vmOptionsHint: JVMに渡される追加の引数 + vmVar: 環境変数 + vmVarHint: ボタンをクリックして環境変数を追加します instanceAge: older: 古い threeDay: 三日以内 @@ -519,7 +522,7 @@ java: modifyInstance: Javaのパスを編集 noMemory: メモリの使用を制限しない refresh: ローカルのJavaをリフレッシュする - systemMemory: 現在のシステムメモリ + systemMemory: 'システム空きメモリ: {free} / {total}' labyMod: disable: LabyMod を使用しない empty: LabyMod は現在のMinecraftをサポートしていません @@ -706,11 +709,6 @@ modInstall: source: Modソース switch: バージョンを切り替え upgrade: Modをアップグレード -modSearchType: - all: すべて - explore: マーケットを探索 - installed: インストール済み - local: ディスクキャッシュ modUpgradePolicy: curseforge: '' curseforgeOnly: Curseforgeのみ diff --git a/xmcl-keystone-ui/locales/kz.yaml b/xmcl-keystone-ui/locales/kz.yaml index d77d4774e..f69edd132 100644 --- a/xmcl-keystone-ui/locales/kz.yaml +++ b/xmcl-keystone-ui/locales/kz.yaml @@ -367,6 +367,7 @@ fileDetail: fileSize: Файл өлшемі hash: Hash filter: Сүзгі +filterLocalOnly: Only show installed finish: Аяқтау forgeConfig: hint: >- @@ -382,3 +383,6 @@ forgeVersion: showBuggy: Баггиді көрсету showRecommendedAndLatestOnly: Ұсынылған және тек соңғы нұсқа version: Forge нұсқасы +instance: + vmVar: Environment Variables + vmVarHint: Click button to add environemnt variables diff --git a/xmcl-keystone-ui/locales/pl.yaml b/xmcl-keystone-ui/locales/pl.yaml index 1b5260f0e..da5c69865 100644 --- a/xmcl-keystone-ui/locales/pl.yaml +++ b/xmcl-keystone-ui/locales/pl.yaml @@ -320,6 +320,7 @@ fileDetail: fileSize: Rozmiar pliku hash: Hash filter: Filtr +filterLocalOnly: Pokaż tylko zainstalowane finish: Zakończenie forgeConfig: hint: >- @@ -453,6 +454,8 @@ instance: versionHint: Minecraftowa wersja tej gry vmOptions: Opcje JVM vmOptionsHint: Dodatkowe argumenty przekazywane do JVM + vmVar: Zmienne środowiskowe + vmVarHint: Kliknij przycisk, aby dodać zmienne środowiskowe instanceAge: older: Starszy threeDay: W trzy dni @@ -549,7 +552,7 @@ java: modifyInstance: Modyfikacja ścieżki Java noMemory: Nie ograniczaj użycia pamięci refresh: Odśwież lokalną Javę - systemMemory: Bieżąca pamięć systemowa + systemMemory: 'Wolna pamięć systemowa: {free} / {total}' labyMod: disable: Wyłącz LabyMod empty: LabyMod nie obsługuje aktualnej wersji Minecrafta @@ -773,11 +776,6 @@ modInstall: source: Mod Source switch: Wersja Swtich upgrade: Ulepszenia modów -modSearchType: - all: All - explore: Poznaj rynek - installed: Zainstalowany - local: Pamięć podręczna dysku modUpgradePolicy: curseforge: Najpierw Curseforge curseforgeOnly: Tylko Curseforge diff --git a/xmcl-keystone-ui/locales/pt-BR.yaml b/xmcl-keystone-ui/locales/pt-BR.yaml index f6c4e88bc..1b7a47c29 100644 --- a/xmcl-keystone-ui/locales/pt-BR.yaml +++ b/xmcl-keystone-ui/locales/pt-BR.yaml @@ -317,9 +317,9 @@ errors: ConnectTimeoutError: Tempo limite de conexão com o servidor. DNSNotFoundError: Erro na pesquisa DNS DatabaseNotOpened: >- - Banco de dados não aberto! O launcher não funcionará corretamente! - Selecione um diretório de dados que o launcher possa acessar. Você pode - tentar redefinir o diretório raiz de dados na página de configurações. + Banco de dados não aberto! O launcher não funcionará corretamente! Selecione + um diretório de dados que o launcher possa acessar. Você pode tentar + redefinir o diretório raiz de dados na página de configurações. DownloadAggregateError: Falha ao baixar o arquivo. DownloadFileSystemError: >- Erro ao acessar o caminho do arquivo de download. Verifique se o launcher @@ -379,6 +379,7 @@ fileDetail: fileSize: Tamanho do arquivo hash: Hash filter: Filtro +filterLocalOnly: Mostrar apenas instalado finish: Concluir forgeConfig: hint: >- @@ -510,6 +511,8 @@ instance: versionHint: A versão do Minecraft deste jogo vmOptions: Opções da JVM vmOptionsHint: Argumentos extras a serem passados para a JVM + vmVar: Variáveis ​​de ambiente + vmVarHint: Clique no botão para adicionar variáveis ​​de ambiente instanceAge: older: Antigo threeDay: Em três dias @@ -604,7 +607,7 @@ java: modifyInstance: Modificar o caminho do Java noMemory: Não limitar o uso de memória refresh: Atualizar Java local - systemMemory: Memória do sistema atual + systemMemory: 'Memória livre do sistema: {free} / {total}' labyMod: disable: Desabilitar LabyMod empty: LabyMod não é compatível com a versão atual do Minecraft. @@ -827,11 +830,6 @@ modInstall: source: Fonte do Mod switch: Trocar Versão upgrade: Atualizar Mods -modSearchType: - all: Todos - explore: Explorar Mercado - installed: Instalados - local: Cache Local modUpgradePolicy: curseforge: Curseforge Primeiro curseforgeOnly: Somente Curseforge @@ -1483,7 +1481,9 @@ setup: title: 'Bem-vindo ao X Minecraft Launcher. Antes de começar, precisamos que você ' shaderPack: deletion: Excluir Shaders Pack - deletionHint: Isso excluirá o arquivo do shaders pack em {path} e não poderá ser revertido. + deletionHint: >- + Isso excluirá o arquivo do shaders pack em {path} e não poderá ser + revertido. disabled: Shader Packs Desabilitados dropHint: Importar Shaders Pack empty: Nenhum shaders pack alocado @@ -1554,7 +1554,7 @@ theme: selectImage: Selecionar Imagem selectMusic: Selecionar Música selectVideo: Selecionar Vídeo - title: 'X Minecraft Launcher' + title: X Minecraft Launcher title: X Minecraft Launcher transportType: host: Candidato Host diff --git a/xmcl-keystone-ui/locales/ru.yaml b/xmcl-keystone-ui/locales/ru.yaml index 302fb62f2..2d55309f1 100644 --- a/xmcl-keystone-ui/locales/ru.yaml +++ b/xmcl-keystone-ui/locales/ru.yaml @@ -370,6 +370,7 @@ fileDetail: fileSize: Размер файла hash: Hash filter: Фильтр +filterLocalOnly: Показывать только установленные finish: Завершить forgeConfig: hint: >- @@ -494,6 +495,8 @@ instance: versionHint: Minecraft-версия этой игры vmOptions: Опции JVM vmOptionsHint: Дополнительные аргументы, передаваемые JVM + vmVar: Переменные среды + vmVarHint: Нажмите кнопку, чтобы добавить переменные среды. instanceAge: older: Старые threeDay: В течение трёх дней @@ -586,7 +589,7 @@ java: modifyInstance: Изменить путь к Java noMemory: Не ограничивать использование памяти refresh: Обновить локальный Java - systemMemory: Текущая оперативная память + systemMemory: 'Свободная память системы: {free} / {total}' labyMod: disable: LabyMod не выбран empty: LabyMod не поддерживает текущую версию Minecraft @@ -816,11 +819,6 @@ modInstall: source: Источник контента мода switch: Переключить версию upgrade: Обновить моды -modSearchType: - all: Все - explore: Исследуйте рынок - installed: Установлено - local: Дисковый кэш modUpgradePolicy: curseforge: Curseforge First curseforgeOnly: Только в Кёрсфордже diff --git a/xmcl-keystone-ui/locales/uk.yaml b/xmcl-keystone-ui/locales/uk.yaml index 14bd25318..00b1d354a 100644 --- a/xmcl-keystone-ui/locales/uk.yaml +++ b/xmcl-keystone-ui/locales/uk.yaml @@ -320,6 +320,7 @@ fileDetail: fileSize: Розмір Файлу hash: Хеш filter: Фільтр +filterLocalOnly: Показувати лише встановлені finish: Завершити forgeConfig: hint: >- @@ -444,6 +445,8 @@ instance: versionHint: Версія Minecraft для цієї гри vmOptions: Параметри JVM vmOptionsHint: Додаткові аргументи, що передаються JVM + vmVar: Змінні середовища + vmVarHint: Натисніть кнопку, щоб додати змінні середовища instanceAge: older: Старі threeDay: В протягом трьох днів @@ -536,7 +539,7 @@ java: modifyInstance: Змінити шлях до Java noMemory: Не обмежувати використання пам'яті refresh: Оновити локальну Java - systemMemory: Поточна системна пам'ять + systemMemory: 'Системна вільна пам''ять: {free} / {total}' labyMod: disable: Вимкнути LabyMod empty: LabyMod не підтримує поточний Minecraft @@ -753,10 +756,6 @@ modInstall: source: Джерело мода switch: Змінити версію upgrade: Оновлення модифікацій -modSearchType: - all: Всі - installed: Встановленно - local: Кеш на диску modUpgradePolicy: curseforge: Curseforge First curseforgeOnly: Тільки Curseforge diff --git a/xmcl-keystone-ui/locales/zh-CN.yaml b/xmcl-keystone-ui/locales/zh-CN.yaml index a2494688c..e109a2dc5 100644 --- a/xmcl-keystone-ui/locales/zh-CN.yaml +++ b/xmcl-keystone-ui/locales/zh-CN.yaml @@ -328,6 +328,7 @@ fileDetail: fileSize: 文件大小 hash: 哈希值 filter: 检索 +filterLocalOnly: 只显示已安装 finish: 完成 forgeConfig: hint: 第一次请启用该模组进入游戏,以便自动检测配置文件 @@ -448,6 +449,8 @@ instance: versionHint: 此游戏使用的 Minecraft 版本 vmOptions: JVM 启动选项 vmOptionsHint: 额外的 JVM 的启动参数 + vmVar: 环境变量 + vmVarHint: 点击按钮添加环境变量 instanceAge: older: 三天前 threeDay: 三天内 @@ -538,7 +541,7 @@ java: modifyInstance: 更改 Java 路径 noMemory: 不设置内存限制 refresh: 刷新本地 Java - systemMemory: 当前系统内存 + systemMemory: '系统空闲内存: {free} / {total}' labyMod: disable: 不使用 LabyMod empty: 当前 Minecraft 不支持 LabyMod @@ -733,11 +736,6 @@ modInstall: source: 模组来源 switch: 切换版本 upgrade: 更新模组 -modSearchType: - all: 所有 - explore: 浏览市场 - installed: 本地 - local: 磁盘缓存 modUpgradePolicy: curseforge: Curseforge 优先 curseforgeOnly: 仅限 Curseforge diff --git a/xmcl-keystone-ui/locales/zh-TW.yaml b/xmcl-keystone-ui/locales/zh-TW.yaml index b59beaf09..ba7b7432c 100644 --- a/xmcl-keystone-ui/locales/zh-TW.yaml +++ b/xmcl-keystone-ui/locales/zh-TW.yaml @@ -325,6 +325,7 @@ fileDetail: fileSize: 檔案大小 hash: 雜湊值 filter: 檢索 +filterLocalOnly: 只顯示已安裝 finish: 完成 forgeConfig: hint: 第一次請啟用該模組進入遊戲,以便自動檢測配置檔案 @@ -445,6 +446,8 @@ instance: versionHint: 此遊戲使用的 Minecraft 版本 vmOptions: JVM 啟動選項 vmOptionsHint: 額外的 JVM 的啟動引數 + vmVar: 環境變數 + vmVarHint: 點擊按鈕新增環境變數 instanceAge: older: 三天前 threeDay: 三天內 @@ -535,7 +538,7 @@ java: modifyInstance: 更改 Java 路徑 noMemory: 不設定記憶體限制 refresh: 重新整理本地 Java - systemMemory: 目前系統記憶體 + systemMemory: '系統可用記憶體: {free} / {total}' labyMod: disable: 不使用 LabyMod empty: 目前 Minecraft 不支援 LabyMod @@ -730,11 +733,6 @@ modInstall: source: 模組來源 switch: 切換版本 upgrade: 更新模組 -modSearchType: - all: 所有 - explore: 瀏覽市場 - installed: 本地 - local: 磁碟快取 modUpgradePolicy: curseforge: Curseforge 优先 curseforgeOnly: 限詛 Curseforge diff --git a/xmcl-keystone-ui/src/components/EnvVarAddItem.vue b/xmcl-keystone-ui/src/components/EnvVarAddItem.vue new file mode 100644 index 000000000..e806ccddb --- /dev/null +++ b/xmcl-keystone-ui/src/components/EnvVarAddItem.vue @@ -0,0 +1,57 @@ + + + diff --git a/xmcl-keystone-ui/src/components/EnvVarTableItem.vue b/xmcl-keystone-ui/src/components/EnvVarTableItem.vue new file mode 100644 index 000000000..d52b567e2 --- /dev/null +++ b/xmcl-keystone-ui/src/components/EnvVarTableItem.vue @@ -0,0 +1,64 @@ + + + + diff --git a/xmcl-keystone-ui/src/components/HomeCard.vue b/xmcl-keystone-ui/src/components/HomeCard.vue index d03d6b2a5..6e5e26fef 100644 --- a/xmcl-keystone-ui/src/components/HomeCard.vue +++ b/xmcl-keystone-ui/src/components/HomeCard.vue @@ -20,7 +20,7 @@ {{ title }} - + @@ -28,7 +28,10 @@ diff --git a/xmcl-keystone-ui/src/views/ModExtension.vue b/xmcl-keystone-ui/src/views/ModExtension.vue index 0d43f8019..33a4036cf 100644 --- a/xmcl-keystone-ui/src/views/ModExtension.vue +++ b/xmcl-keystone-ui/src/views/ModExtension.vue @@ -15,6 +15,7 @@ modrinth-category-filter="mod" :enable-curseforge.sync="isCurseforgeActive" :enable-modrinth.sync="isModrinthActive" + :local-only.sync="localOnly" :sort.sync="sort" :game-version.sync="gameVersion" :modloader.sync="modLoader" @@ -43,7 +44,7 @@ import { injection } from '@/util/inject' import debounce from 'lodash.debounce' const { runtime: version } = injection(kInstance) -const { modrinth, curseforge, gameVersion, cachedMods, modLoaderFilters, curseforgeCategory, modrinthCategories, isCurseforgeActive, isModrinthActive, sort } = injection(kModsSearch) +const { modrinth, curseforge, gameVersion, cachedMods, localOnly, curseforgeCategory, modrinthCategories, isCurseforgeActive, isModrinthActive, sort } = injection(kModsSearch) const { mods: modFiles } = injection(kInstanceModsContext) const curseforgeCount = computed(() => curseforge.value ? curseforge.value.length : 0) const modrinthCount = computed(() => modrinth.value ? modrinth.value.length : 0) diff --git a/xmcl-keystone-ui/src/views/ModItem.vue b/xmcl-keystone-ui/src/views/ModItem.vue index 9c42bf3fb..2f6144e29 100644 --- a/xmcl-keystone-ui/src/views/ModItem.vue +++ b/xmcl-keystone-ui/src/views/ModItem.vue @@ -54,7 +54,7 @@ const props = defineProps<{ const emit = defineEmits(['click', 'checked', 'install']) const { compatibility: compatibilities } = injection(kInstanceModsContext) -const compatibility = computed(() => props.item.installed[0] ? compatibilities.value[props.item.installed[0].modId] : []) +const compatibility = computed(() => props.item.installed[0] ? compatibilities.value[props.item.installed[0].modId] ?? [] : []) const { uninstall, disable, enable } = useService(InstanceModsServiceKey) const { path } = injection(kInstance) const _getContextMenuItems = useModItemContextMenuItems(computed(() => props.item), () => { @@ -62,7 +62,7 @@ const _getContextMenuItems = useModItemContextMenuItems(computed(() => props.ite uninstall({ path: path.value, mods: props.item.installed.map(i => i.path) }) } }, () => { }, () => { - if (props.item.installed.length > 0) { + if (props.item.installed && props.item.installed.length > 0) { if (props.item.installed[0].enabled) { disable({ path: path.value, mods: props.item.installed.map(i => i.path) }) } else { @@ -75,4 +75,6 @@ const getContextMenuItems = () => { if (items && items.length > 0) return items return _getContextMenuItems() } + +const hasLabel = computed(() => !props.dense && props.item.installed && (props.item.installed?.[0]?.tags.length + compatibility.value.length) > 0) diff --git a/xmcl-keystone-ui/src/views/ResourcePackExtension.vue b/xmcl-keystone-ui/src/views/ResourcePackExtension.vue index ff57206c2..0f2248dc2 100644 --- a/xmcl-keystone-ui/src/views/ResourcePackExtension.vue +++ b/xmcl-keystone-ui/src/views/ResourcePackExtension.vue @@ -20,6 +20,7 @@ :enable-modrinth.sync="isModrinthActive" :sort.sync="sort" :game-version.sync="gameVersion" + :local-only.sync="localOnly" /> @@ -36,7 +37,7 @@ import { kCompact } from '@/composables/scrollTop' import { getExtensionItemsFromRuntime } from '@/util/extensionItems' import { injection } from '@/util/inject' -const { keyword, curseforge, gameVersion, curseforgeCategory, isCurseforgeActive, sort } = injection(kSaveSearch) +const { keyword, localOnly, gameVersion, curseforgeCategory, isCurseforgeActive, sort } = injection(kSaveSearch) const { runtime: version } = injection(kInstance) const { isInstanceLinked } = injection(kInstanceSave) diff --git a/xmcl-keystone-ui/src/views/SettingGlobal.vue b/xmcl-keystone-ui/src/views/SettingGlobal.vue index 9dfab0972..b2fc58aea 100644 --- a/xmcl-keystone-ui/src/views/SettingGlobal.vue +++ b/xmcl-keystone-ui/src/views/SettingGlobal.vue @@ -63,7 +63,9 @@ class="m-1 mt-2" hide-details required - solo + dense + outlined + filled :placeholder="t('instance.prependCommandHint')" /> @@ -78,12 +80,45 @@ class="m-1 mt-2" hide-details required - solo + dense + outlined + filled :placeholder="t('instance.vmOptionsHint')" /> + + + + + {{ t("instance.vmVar") }} + + + {{ t("instance.vmVarHint") }} + + + + + add + + + + + + + + @@ -92,7 +127,9 @@ { assignMemory.value = globalAssignMemory.value @@ -168,9 +209,27 @@ const save = () => { globalDisableAuthlibInjector: disableAuthlibInjector.value, globalDisableElyByAuthlib: disableElyByAuthlib.value, globalPrependCommand: prependCommand.value, + globalEnv: env.value, }) } +const adding = ref(false) +function onAddEnvVar() { + adding.value = true +} +function onEnvVarCleared() { + adding.value = false +} +function onEnvVarAdded(key: string, value: string) { + adding.value = false + if (key === '') return + env.value = { ...env.value, [key]: value } +} +function onEnvVarDeleted(key: string) { + const { [key]: _, ...rest } = env.value + env.value = rest +} + useEventListener('beforeunload', save) onUnmounted(save) diff --git a/xmcl-keystone-ui/src/views/SettingJavaMemory.vue b/xmcl-keystone-ui/src/views/SettingJavaMemory.vue index 1feae34fb..951030af4 100644 --- a/xmcl-keystone-ui/src/views/SettingJavaMemory.vue +++ b/xmcl-keystone-ui/src/views/SettingJavaMemory.vue @@ -1,25 +1,5 @@ diff --git a/xmcl-keystone-ui/src/windows/main/Context.ts b/xmcl-keystone-ui/src/windows/main/Context.ts index 0f2417360..1b3dff3e9 100644 --- a/xmcl-keystone-ui/src/windows/main/Context.ts +++ b/xmcl-keystone-ui/src/windows/main/Context.ts @@ -7,6 +7,7 @@ import { kInstance, useInstance } from '@/composables/instance' import { kInstanceDefaultSource, useInstanceDefaultSource } from '@/composables/instanceDefaultSource' import { kInstanceFiles, useInstanceFiles } from '@/composables/instanceFiles' import { kInstanceJava, useInstanceJava } from '@/composables/instanceJava' +import { kInstanceJavaDiagnose, useInstanceJavaDiagnose } from '@/composables/instanceJavaDiagnose' import { kInstanceLaunch, useInstanceLaunch } from '@/composables/instanceLaunch' import { kInstanceModsContext, useInstanceMods } from '@/composables/instanceMods' import { kInstanceOptions, useInstanceOptions } from '@/composables/instanceOptions' @@ -54,6 +55,7 @@ export default defineComponent({ const settings = useSettingsState() const instanceVersion = useInstanceVersion(instance.instance, localVersions.versions, localVersions.servers) const instanceJava = useInstanceJava(instance.instance, instanceVersion.resolvedVersion, java.all) + provide(kInstanceJavaDiagnose, useInstanceJavaDiagnose(instanceJava)) const instanceDefaultSource = useInstanceDefaultSource(instance.path) const options = useInstanceOptions(instance.path) const saves = useInstanceSaves(instance.path) diff --git a/xmcl-runtime-api/index.ts b/xmcl-runtime-api/index.ts index 11fdb2808..b9e6e8fd0 100644 --- a/xmcl-runtime-api/index.ts +++ b/xmcl-runtime-api/index.ts @@ -78,7 +78,7 @@ export * from './src/state' export * from './src/task' export * from './src/util/authority' export * from './src/util/mavenVersion' -export * from './src/util/MutableState' +export * from './src/util/SharedState' export * from './src/util/mutex' export { default as packFormatVersionRange } from './src/util/packFormatVersionRange' export * from './src/util/promiseSignal' diff --git a/xmcl-runtime-api/src/entities/InstanceSchema.json b/xmcl-runtime-api/src/entities/InstanceSchema.json index 4b048fc83..d26ee4b36 100644 --- a/xmcl-runtime-api/src/entities/InstanceSchema.json +++ b/xmcl-runtime-api/src/entities/InstanceSchema.json @@ -117,6 +117,10 @@ "type": "string" } }, + "env": { + "description": "The launch environment variables", + "$ref": "#/definitions/Record" + }, "prependCommand": { "type": "string" }, @@ -272,6 +276,9 @@ "minecraft" ] }, + "Record": { + "type": "object" + }, "ModrinthUpstream": { "type": "object", "properties": { diff --git a/xmcl-runtime-api/src/entities/SettingSchema.json b/xmcl-runtime-api/src/entities/SettingSchema.json index b030251a3..93f2192c9 100644 --- a/xmcl-runtime-api/src/entities/SettingSchema.json +++ b/xmcl-runtime-api/src/entities/SettingSchema.json @@ -149,6 +149,11 @@ "default": "", "type": "string" }, + "globalEnv": { + "$ref": "#/definitions/Record", + "description": "The launch environment variables", + "default": {} + }, "discordPresence": { "default": true, "type": "boolean" @@ -184,6 +189,7 @@ "globalAssignMemory", "globalDisableAuthlibInjector", "globalDisableElyByAuthlib", + "globalEnv", "globalFastLaunch", "globalHideLauncher", "globalMaxMemory", @@ -200,6 +206,11 @@ "replaceNatives", "theme" ], + "definitions": { + "Record": { + "type": "object" + } + }, "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false } \ No newline at end of file diff --git a/xmcl-runtime-api/src/entities/exception.ts b/xmcl-runtime-api/src/entities/exception.ts index 645fc9740..8c35ce745 100644 --- a/xmcl-runtime-api/src/entities/exception.ts +++ b/xmcl-runtime-api/src/entities/exception.ts @@ -23,17 +23,33 @@ export interface InstanceNotFoundException extends ExceptionBase { instancePath: string } -export interface HTTPExceptions extends ExceptionBase { +export type NetworkExceptions = { type: 'httpException' + code: NetworkErrorCode.HTTP_STATUS method: string - code: string | number url: string statusCode: number body: any +} | { + type: 'networkException' + code: string | number +} + +export const enum NetworkErrorCode { + CONNECTION_CLOSED = 'CONNECTION_CLOSED', + INTERNET_DISCONNECTED = 'INTERNET_DISCONNECTED', + NETWORK_CHANGED = 'NETWORK_CHANGED', + CONNECTION_RESET = 'CONNECTION_RESET', + CONNECTION_TIMED_OUT = 'CONNECTION_TIMED_OUT', + TIMED_OUT = 'TIMED_OUT', + DNS_NOTFOUND = 'NAME_NOT_RESOLVED', + SOCKET_NOT_CONNECTED = 'SOCKET_NOT_CONNECTED', + // all above user can retry + HTTP_STATUS = 'HTTP_STATUS', } -export class HTTPException extends Exception { - name = 'HTTPException' +export class NetworkException extends Exception { + name = 'NetworkException' } export function isFileNoFound(e: unknown) { diff --git a/xmcl-runtime-api/src/entities/instance.schema.ts b/xmcl-runtime-api/src/entities/instance.schema.ts index a7a54547d..a2b2ec7e9 100644 --- a/xmcl-runtime-api/src/entities/instance.schema.ts +++ b/xmcl-runtime-api/src/entities/instance.schema.ts @@ -144,6 +144,10 @@ export interface InstanceData { * */ mcOptions?: string[] + /** + * The launch environment variables + */ + env?: Record prependCommand?: string /** diff --git a/xmcl-runtime-api/src/entities/instance.ts b/xmcl-runtime-api/src/entities/instance.ts index 3599e6921..c86cef207 100644 --- a/xmcl-runtime-api/src/entities/instance.ts +++ b/xmcl-runtime-api/src/entities/instance.ts @@ -16,6 +16,7 @@ export function createTemplate(): Instance { maxMemory: undefined, vmOptions: undefined, mcOptions: undefined, + env: undefined, url: '', icon: '', diff --git a/xmcl-runtime-api/src/entities/setting.schema.ts b/xmcl-runtime-api/src/entities/setting.schema.ts index 5b5f3ec72..58f41919a 100644 --- a/xmcl-runtime-api/src/entities/setting.schema.ts +++ b/xmcl-runtime-api/src/entities/setting.schema.ts @@ -116,6 +116,11 @@ export interface SettingSchema { * @default "" */ globalPrependCommand: string + /** + * The launch environment variables + * @default {} + */ + globalEnv: Record /** * @default true */ diff --git a/xmcl-runtime-api/src/entities/setting.ts b/xmcl-runtime-api/src/entities/setting.ts index e6e56899a..781a14212 100644 --- a/xmcl-runtime-api/src/entities/setting.ts +++ b/xmcl-runtime-api/src/entities/setting.ts @@ -13,6 +13,7 @@ export class Settings implements SettingSchema { globalFastLaunch = false globalHideLauncher = false globalShowLog = false + globalEnv: Record = {} discordPresence = false developerMode = false disableTelemetry = false @@ -81,6 +82,7 @@ export class Settings implements SettingSchema { this.globalShowLog = config.globalShowLog this.globalDisableElyByAuthlib = config.globalDisableElyByAuthlib this.globalDisableAuthlibInjector = config.globalDisableAuthlibInjector + this.globalEnv = config.globalEnv this.discordPresence = config.discordPresence this.developerMode = config.developerMode this.disableTelemetry = config.disableTelemetry @@ -198,6 +200,7 @@ export class Settings implements SettingSchema { globalDisableAuthlibInjector: boolean globalDisableElyByAuthlib: boolean globalPrependCommand: string + globalEnv: Record }) { this.globalMinMemory = settings.globalMinMemory this.globalMaxMemory = settings.globalMaxMemory @@ -210,5 +213,6 @@ export class Settings implements SettingSchema { this.globalDisableAuthlibInjector = settings.globalDisableAuthlibInjector this.globalDisableElyByAuthlib = settings.globalDisableElyByAuthlib this.globalPrependCommand = settings.globalPrependCommand + this.globalEnv = settings.globalEnv } } diff --git a/xmcl-runtime-api/src/services/BaseService.ts b/xmcl-runtime-api/src/services/BaseService.ts index 11b8116f9..c83bb8ba2 100644 --- a/xmcl-runtime-api/src/services/BaseService.ts +++ b/xmcl-runtime-api/src/services/BaseService.ts @@ -2,7 +2,7 @@ import { Exception } from '../entities/exception' import { LauncherProfile } from '../entities/launcherProfile' import { Platform } from '../entities/platform' import { Settings } from '../entities/setting' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface MigrateOptions { @@ -50,7 +50,7 @@ export interface BaseService { getSessionId(): Promise - getSettings(): Promise> + getSettings(): Promise> /** * Get the environment of the launcher */ @@ -109,7 +109,7 @@ export interface BaseService { isResourceDatabaseOpened(): Promise } -export type BaseServiceExceptions = { +export type MigrationExceptions = { /** * Throw when dest is a file */ @@ -130,6 +130,6 @@ export type BaseServiceExceptions = { destination: string } -export class BaseServiceException extends Exception { } +export class MigrationException extends Exception { } export const BaseServiceKey: ServiceKey = 'BaseService' diff --git a/xmcl-runtime-api/src/services/InstanceInstallService.ts b/xmcl-runtime-api/src/services/InstanceInstallService.ts index cb9cbbe9c..19e130c54 100644 --- a/xmcl-runtime-api/src/services/InstanceInstallService.ts +++ b/xmcl-runtime-api/src/services/InstanceInstallService.ts @@ -1,4 +1,3 @@ -import { Exception, InstanceNotFoundException } from '../entities/exception' import { InstanceFile } from '../entities/instanceManifest.schema' import { ServiceKey } from './Service' @@ -50,9 +49,4 @@ export interface InstanceInstallService { checkInstanceInstall(path: string): Promise } -export type InstanceInstallExceptions = InstanceNotFoundException - -export class InstanceInstallException extends Exception { -} - export const InstanceInstallServiceKey: ServiceKey = 'InstanceInstallService' diff --git a/xmcl-runtime-api/src/services/InstanceManagingService.ts b/xmcl-runtime-api/src/services/InstanceManagingService.ts index 1e5dcdd66..612f02985 100644 --- a/xmcl-runtime-api/src/services/InstanceManagingService.ts +++ b/xmcl-runtime-api/src/services/InstanceManagingService.ts @@ -1,4 +1,4 @@ -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface ImportHMCLModpackOptions { @@ -48,7 +48,7 @@ export class InstanceManagingState { * Provide the abilities to import/export instance from/to modpack */ export interface InstanceManagingService { - getState(): Promise> + getState(): Promise> /** * Create a managed instance * @param options diff --git a/xmcl-runtime-api/src/services/InstanceModsService.ts b/xmcl-runtime-api/src/services/InstanceModsService.ts index e5fed2340..4d6ba8fce 100644 --- a/xmcl-runtime-api/src/services/InstanceModsService.ts +++ b/xmcl-runtime-api/src/services/InstanceModsService.ts @@ -1,6 +1,6 @@ import { InstallMarketOptionWithInstance } from '../entities/market' import { ResourceState, Resource } from '../entities/resource' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface InstallModsOptions { @@ -22,7 +22,7 @@ export interface InstanceModsService { /** * Read all mods under the current instance */ - watch(instancePath: string): Promise> + watch(instancePath: string): Promise> /** * Refresh the metadata of the instance mods */ diff --git a/xmcl-runtime-api/src/services/InstanceOptionsService.ts b/xmcl-runtime-api/src/services/InstanceOptionsService.ts index e0c9681b6..08a7723e2 100644 --- a/xmcl-runtime-api/src/services/InstanceOptionsService.ts +++ b/xmcl-runtime-api/src/services/InstanceOptionsService.ts @@ -1,7 +1,6 @@ import type { Frame as GameSetting } from '@xmcl/gamesetting' -import { Exception, InstanceNotFoundException } from '../entities/exception' import { ShaderOptions } from '../entities/shaderpack' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface EditGameSettingOptions extends GameSetting { /** @@ -75,7 +74,7 @@ export class GameOptionsState implements GameOptions { * The service for game options & shader options */ export interface InstanceOptionsService { - watch(path: string): Promise> + watch(path: string): Promise> /** * Get the shader setting of the specific instance * @param instancePath The instance path @@ -128,7 +127,3 @@ export interface InstanceOptionsService { } export const InstanceOptionsServiceKey: ServiceKey = 'InstanceOptionsService' - -export type InstanceOptionExceptions = InstanceNotFoundException - -export class InstanceOptionException extends Exception { } diff --git a/xmcl-runtime-api/src/services/InstanceResourcePacksService.ts b/xmcl-runtime-api/src/services/InstanceResourcePacksService.ts index b0818702c..327c84097 100644 --- a/xmcl-runtime-api/src/services/InstanceResourcePacksService.ts +++ b/xmcl-runtime-api/src/services/InstanceResourcePacksService.ts @@ -1,6 +1,6 @@ import { InstallMarketOptionWithInstance } from '../entities/market' import { Resource, ResourceState } from '../entities/resource' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' /** @@ -27,7 +27,7 @@ export interface InstanceResourcePacksService { * Watch the `resourcepacks` directory under the instance path and import the resource packs. * @param instancePath The instance path */ - watch(instancePath: string): Promise> + watch(instancePath: string): Promise> /** * Manually install the resource packs to the instance. * diff --git a/xmcl-runtime-api/src/services/InstanceSavesService.ts b/xmcl-runtime-api/src/services/InstanceSavesService.ts index 661dc7460..fb14738d4 100644 --- a/xmcl-runtime-api/src/services/InstanceSavesService.ts +++ b/xmcl-runtime-api/src/services/InstanceSavesService.ts @@ -1,7 +1,7 @@ import { Exception, InstanceNotFoundException } from '../entities/exception' import { InstallMarketOptionWithInstance } from '../entities/market' import { InstanceSave, InstanceSaveHeader, Saves } from '../entities/save' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface ExportSaveOptions { @@ -115,7 +115,7 @@ export interface InstanceSavesService { * Watch instances saves * @param path */ - watch(path: string): Promise> + watch(path: string): Promise> /** * Clone a save under an instance to one or multiple instances. * @@ -175,22 +175,9 @@ export interface InstanceSavesService { export const InstanceSavesServiceKey: ServiceKey = 'InstanceSavesService' -export type InstanceSaveExceptions = { - /** - * - instanceDeleteNoSave -> no save match name provided - */ - type: 'instanceDeleteNoSave' - /** - * The save name - */ - name: string -} | { +export type ImportSaveExceptions = { type: 'instanceImportIllegalSave' path: string -} | { - type: 'instanceCopySaveNotFound' | 'instanceCopySaveUnexpected' - src: string - dest: string[] -} | InstanceNotFoundException +} -export class InstanceSaveException extends Exception { } +export class ImportSaveException extends Exception { } diff --git a/xmcl-runtime-api/src/services/InstanceServerInfoService.ts b/xmcl-runtime-api/src/services/InstanceServerInfoService.ts index dfde4d78e..94626b9b7 100644 --- a/xmcl-runtime-api/src/services/InstanceServerInfoService.ts +++ b/xmcl-runtime-api/src/services/InstanceServerInfoService.ts @@ -2,7 +2,7 @@ import type { Status } from '@xmcl/client' import type { ServerInfo } from '@xmcl/game-data' import { UNKNOWN_STATUS } from '../entities/serverStatus' import { ServiceKey } from './Service' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' export class ServerInfoWithStatus implements ServerInfo { status: Status = UNKNOWN_STATUS @@ -46,7 +46,7 @@ export interface InstanceServerInfoService { * Watch the server info in the instance folder. * @param instancePath The instance folder path */ - watch(instancePath: string): Promise> + watch(instancePath: string): Promise> isLinked(instancePath: string): Promise diff --git a/xmcl-runtime-api/src/services/InstanceService.ts b/xmcl-runtime-api/src/services/InstanceService.ts index f2904a466..a9102fdb3 100644 --- a/xmcl-runtime-api/src/services/InstanceService.ts +++ b/xmcl-runtime-api/src/services/InstanceService.ts @@ -1,4 +1,4 @@ -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { Exception } from '../entities/exception' import { Instance } from '../entities/instance' import { InstanceSchema } from '../entities/instance.schema' @@ -136,6 +136,9 @@ export class InstanceState { if ('java' in settings) { inst.java = settings.java } + if ('env' in settings) { + inst.env = settings.env + } inst.url = settings.url ?? inst.url inst.icon = settings.icon ?? inst.icon @@ -172,7 +175,7 @@ export class InstanceState { * Provide instance splitting service. It can split the game into multiple environment and dynamically deploy the resource to run. */ export interface InstanceService { - getSharedInstancesState(): Promise> + getSharedInstancesState(): Promise> /** * Create a managed instance (either a modpack or a server) under the managed folder. * @param option The creation option @@ -193,12 +196,6 @@ export interface InstanceService { * Otherwise, it will edit the instance on the provided path. */ editInstance(options: EditInstanceOptions & { instancePath: string }): Promise - /** - * Add a directory as managed instance folder. It will try to load the instance.json. - * If it's a common folder, it will try to create instance from the directory data. - * @param path The path of the instance - */ - addExternalInstance(path: string): Promise /** * Get or create a MANAGED instance via your unique id * @param id The unique id, can be any string, but it will convert to a string can be file name @@ -210,21 +207,3 @@ export interface InstanceService { } export const InstanceServiceKey: ServiceKey = 'InstanceService' - -export type InstanceExceptions = { - type: 'instanceNameDuplicated' - path: string - name: string -} | { - type: 'instanceNameRequired' -} | { - type: 'instanceNotFound' - path: string -} | { - type: 'instancePathInvalid' - path: string - reason: string -} - -export class InstanceException extends Exception { -} diff --git a/xmcl-runtime-api/src/services/InstanceShaderPacksService.ts b/xmcl-runtime-api/src/services/InstanceShaderPacksService.ts index 56d32eb1b..39a7b05d0 100644 --- a/xmcl-runtime-api/src/services/InstanceShaderPacksService.ts +++ b/xmcl-runtime-api/src/services/InstanceShaderPacksService.ts @@ -1,6 +1,6 @@ import { InstallMarketOptionWithInstance } from '../entities/market' import { Resource, ResourceState } from '../entities/resource' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface InstanceShaderPacksService { @@ -23,7 +23,7 @@ export interface InstanceShaderPacksService { * This will scan the `shaderpacks` directory and import all shaderpacks into the resource service. * @param instancePath The instance absolute path */ - watch(instancePath: string): Promise> + watch(instancePath: string): Promise> /** * Manually install the shaderpack to the instance. * diff --git a/xmcl-runtime-api/src/services/JavaService.ts b/xmcl-runtime-api/src/services/JavaService.ts index 54668c02e..30038e698 100644 --- a/xmcl-runtime-api/src/services/JavaService.ts +++ b/xmcl-runtime-api/src/services/JavaService.ts @@ -1,7 +1,7 @@ import { JavaVersion } from '@xmcl/core' import { JavaRecord } from '../entities/java' import { Java } from '../entities/java.schema' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export class JavaState { @@ -39,7 +39,7 @@ export class JavaState { } export interface JavaService { - getJavaState(): Promise> + getJavaState(): Promise> /** * Install a default jdk 8 or 16 to the a preserved location. It'll be installed under your launcher root location `jre` or `jre-next` folder */ diff --git a/xmcl-runtime-api/src/services/LaunchService.ts b/xmcl-runtime-api/src/services/LaunchService.ts index bda0548cf..9a4fc422e 100644 --- a/xmcl-runtime-api/src/services/LaunchService.ts +++ b/xmcl-runtime-api/src/services/LaunchService.ts @@ -1,9 +1,8 @@ -import { AUTHORITY_DEV } from '../util/authority' import { Exception } from '../entities/exception' import { UserProfile } from '../entities/user.schema' import { GenericEventEmitter } from '../events' +import { AUTHORITY_DEV } from '../util/authority' import { ServiceKey } from './Service' -import { UserExceptions } from './UserService' interface LaunchServiceEventMap { 'minecraft-window-ready': { pid: number } @@ -19,7 +18,7 @@ interface LaunchServiceEventMap { 'minecraft-stderr': { pid: number; stdout: string } 'launch-performance-pre': { id: string; name: string } 'launch-performance': { id: string; name: string; duration: number } - 'error': LaunchException + 'error': LaunchException | Error } export interface LaunchOptions { @@ -113,6 +112,10 @@ export interface LaunchOptions { * Prepend command before launch */ prependCommand?: string + /** + * The environment variables + */ + env?: Record disableElyByAuthlib?: boolean @@ -173,12 +176,6 @@ export interface LaunchService extends GenericEventEmitter> + openModpack(modpackPath: string): Promise> /** * Import the modpack as an instance * @param modpackPath The modpack file path @@ -161,12 +161,12 @@ export interface ModpackService { */ showModpacksFolder(): Promise - watchModpackFolder(): Promise> + watchModpackFolder(): Promise> removeModpack(path: string): Promise } -export function waitModpackFiles(modpack: MutableState) { +export function waitModpackFiles(modpack: SharedState) { return new Promise(resolve => { if (modpack.ready) { resolve(modpack.files) diff --git a/xmcl-runtime-api/src/services/PeerService.ts b/xmcl-runtime-api/src/services/PeerService.ts index 8cd917c6b..03568fad6 100644 --- a/xmcl-runtime-api/src/services/PeerService.ts +++ b/xmcl-runtime-api/src/services/PeerService.ts @@ -1,7 +1,7 @@ import { InstanceManifest } from '../entities/instanceManifest.schema' import { GenericEventEmitter } from '../events' import { ConnectionState, ConnectionUserInfo, IceGatheringState, Peer, SelectedCandidateInfo, SignalingState } from '../multiplayer' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export type NatType = 'Blocked'| 'Open Internet'| 'Full Cone'| 'Symmetric UDP Firewall'| 'Restrict NAT'| 'Restrict Port NAT'| 'Symmetric NAT' | 'Unknown' @@ -189,7 +189,7 @@ interface PeerServiceEvents { } export interface PeerService extends GenericEventEmitter { - getPeerState(): Promise> + getPeerState(): Promise> /** * Share the instance to other peers */ diff --git a/xmcl-runtime-api/src/services/UserService.ts b/xmcl-runtime-api/src/services/UserService.ts index 51190e21c..8423d1e6d 100644 --- a/xmcl-runtime-api/src/services/UserService.ts +++ b/xmcl-runtime-api/src/services/UserService.ts @@ -2,7 +2,7 @@ import { GameProfile } from '@xmcl/user' import { Exception } from '../entities/exception' import { GameProfileAndTexture, UserProfile } from '../entities/user.schema' import { GenericEventEmitter } from '../events' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { ServiceKey } from './Service' export interface RefreshSkinOptions { @@ -147,7 +147,7 @@ export interface RefreshUserOptions { } export interface UserService extends GenericEventEmitter { - getUserState(): Promise> + getUserState(): Promise> /** * Refresh the current user login status. * diff --git a/xmcl-runtime-api/src/services/VersionService.ts b/xmcl-runtime-api/src/services/VersionService.ts index f185b4aa5..3ae3217a1 100644 --- a/xmcl-runtime-api/src/services/VersionService.ts +++ b/xmcl-runtime-api/src/services/VersionService.ts @@ -1,13 +1,13 @@ import type { ResolvedVersion } from '@xmcl/core' import { ServiceKey } from './Service' -import { MutableState } from '../util/MutableState' +import { SharedState } from '../util/SharedState' import { LocalVersions, ResolvedServerVersion } from '../entities/version' /** * The local version service maintains the installed versions on disk */ export interface VersionService { - getLocalVersions(): Promise> + getLocalVersions(): Promise> /** * Scan .minecraft folder and copy libraries/assets/versions files from it to launcher managed place. * diff --git a/xmcl-runtime-api/src/util/MutableState.ts b/xmcl-runtime-api/src/util/SharedState.ts similarity index 64% rename from xmcl-runtime-api/src/util/MutableState.ts rename to xmcl-runtime-api/src/util/SharedState.ts index d82992c7c..6b5dc6d06 100644 --- a/xmcl-runtime-api/src/util/MutableState.ts +++ b/xmcl-runtime-api/src/util/SharedState.ts @@ -4,23 +4,23 @@ type Mutations = { [K in keyof T as T[K] extends Function ? K : never]: T[K] extends ((payload: infer P) => void) ? P : never } /** - * Generic representation of a mutable state + * The shared state can be transferred between main process and renderer process */ -export type MutableState = T & { +export type SharedState = T & { /** * The id of the state */ readonly id: string - subscribe>(key: K, listener: (payload: Mutations[K]) => void): MutableState - subscribe(key: 'state-validating', listener: (v: boolean) => void): MutableState + subscribe>(key: K, listener: (payload: Mutations[K]) => void): SharedState + subscribe(key: 'state-validating', listener: (v: boolean) => void): SharedState - unsubscribe>(key: K, listener: (payload: Mutations[K]) => void): MutableState - unsubscribe(key: 'state-validating', listener: (v: boolean) => void): MutableState + unsubscribe>(key: K, listener: (payload: Mutations[K]) => void): SharedState + unsubscribe(key: 'state-validating', listener: (v: boolean) => void): SharedState - subscribeAll>(listener: (mutation: K, payload: Mutations[keyof Mutations]) => void): MutableState + subscribeAll>(listener: (mutation: K, payload: Mutations[keyof Mutations]) => void): SharedState - unsubscribeAll>(listener: (mutation: K, payload: Mutations[keyof Mutations]) => void): MutableState + unsubscribeAll>(listener: (mutation: K, payload: Mutations[keyof Mutations]) => void): SharedState revalidate(): void } diff --git a/xmcl-runtime/base/BaseService.ts b/xmcl-runtime/base/BaseService.ts index 4481d15fe..ab5f36a51 100644 --- a/xmcl-runtime/base/BaseService.ts +++ b/xmcl-runtime/base/BaseService.ts @@ -1,4 +1,4 @@ -import { BaseServiceException, BaseServiceKey, Environment, BaseService as IBaseService, MigrateOptions, MutableState, PoolStats, Settings } from '@xmcl/runtime-api' +import { MigrationException, BaseServiceKey, Environment, BaseService as IBaseService, MigrateOptions, SharedState, PoolStats, Settings } from '@xmcl/runtime-api' import { readdir, rename, stat } from 'fs-extra' import os, { freemem, totalmem } from 'os' import { join } from 'path' @@ -44,7 +44,7 @@ export class BaseService extends AbstractService implements IBaseService { return this.app.registry.get(kGameDataPath).then(f => f()) } - async getSettings(): Promise> { + async getSettings(): Promise> { return this.app.registry.get(kSettings) } @@ -171,7 +171,7 @@ export class BaseService extends AbstractService implements IBaseService { const destination = options.destination const destStat = await stat(destination).catch(() => undefined) if (destStat && destStat.isFile()) { - throw new BaseServiceException({ + throw new MigrationException({ type: 'migrationDestinationIsFile', destination, }) @@ -179,7 +179,7 @@ export class BaseService extends AbstractService implements IBaseService { if (destStat && destStat.isDirectory()) { const files = await readdir(destination) if (files.length !== 0) { - throw new BaseServiceException({ + throw new MigrationException({ type: 'migrationDestinationIsNotEmptyDirectory', destination, }) @@ -204,7 +204,7 @@ export class BaseService extends AbstractService implements IBaseService { return } if (e.code === 'EPERM') { - throw new BaseServiceException({ + throw new MigrationException({ type: 'migrationNoPermission', source, destination, diff --git a/xmcl-runtime/install/InstallService.ts b/xmcl-runtime/install/InstallService.ts index fcf93afdd..225abc9dc 100644 --- a/xmcl-runtime/install/InstallService.ts +++ b/xmcl-runtime/install/InstallService.ts @@ -1,7 +1,7 @@ import { checksum, MinecraftFolder, ResolvedLibrary, Version } from '@xmcl/core' import { DownloadBaseOptions } from '@xmcl/file-transfer' import { DEFAULT_FORGE_MAVEN, DEFAULT_RESOURCE_ROOT_URL, DownloadTask, installAssetsTask, installByProfileTask, installFabric, InstallForgeOptions, installForgeTask, InstallJarTask, InstallJsonTask, installLabyMod4Task, installLibrariesTask, installLiteloaderTask, installNeoForgedTask, installOptifineTask, InstallProfile, installQuiltVersion, installResolvedAssetsTask, installResolvedLibrariesTask, installVersionTask, LiteloaderVersion, MinecraftVersion, Options, PostProcessFailedError } from '@xmcl/installer' -import { InstallForgeOptions as _InstallForgeOptions, Asset, InstallService as IInstallService, InstallableLibrary, InstallFabricOptions, InstallLabyModOptions, InstallNeoForgedOptions, InstallOptifineAsModOptions, InstallOptifineOptions, InstallQuiltOptions, InstallServiceKey, isFabricLoaderLibrary, isForgeLibrary, LockKey, MutableState, OptifineVersion, Settings } from '@xmcl/runtime-api' +import { InstallForgeOptions as _InstallForgeOptions, Asset, InstallService as IInstallService, InstallableLibrary, InstallFabricOptions, InstallLabyModOptions, InstallNeoForgedOptions, InstallOptifineAsModOptions, InstallOptifineOptions, InstallQuiltOptions, InstallServiceKey, isFabricLoaderLibrary, isForgeLibrary, LockKey, SharedState, OptifineVersion, Settings } from '@xmcl/runtime-api' import { CancelledError, task } from '@xmcl/task' import { spawn } from 'child_process' import { existsSync } from 'fs' @@ -30,7 +30,7 @@ export class InstallService extends AbstractService implements IInstallService { @Inject(JavaService) private javaService: JavaService, @Inject(kGameDataPath) private getPath: PathResolver, @Inject(kGFW) private gfw: GFW, - @Inject(kSettings) private settings: MutableState, + @Inject(kSettings) private settings: SharedState, @Inject(kDownloadOptions) private downloadOptions: DownloadBaseOptions, @Inject(kTaskExecutor) private submit: TaskFn, ) { diff --git a/xmcl-runtime/instance/InstanceOptionsService.ts b/xmcl-runtime/instance/InstanceOptionsService.ts index de94b5020..29a3f3e98 100644 --- a/xmcl-runtime/instance/InstanceOptionsService.ts +++ b/xmcl-runtime/instance/InstanceOptionsService.ts @@ -7,7 +7,7 @@ import { Inject, kGameDataPath, LauncherAppKey, PathResolver } from '~/app' import { AbstractService, ExposeServiceKey, ServiceStateManager } from '~/service' import { LauncherApp } from '../app/LauncherApp' import { AnyError, isSystemError } from '../util/error' -import { hardLinkFiles, isHardLinked, missing, unHardLinkFiles } from '../util/fs' +import { handleOnlyNotFound, hardLinkFiles, isHardLinked, missing, unHardLinkFiles } from '../util/fs' import { requireString } from '../util/object' import { InstanceService } from './InstanceService' @@ -193,9 +193,11 @@ export class InstanceOptionsService extends AbstractService implements IInstance async #getProperties(instancePath: string, name: string) { const filePath = join(instancePath, 'config', name) - if (await missing(filePath)) return {} - const content = await readFile(filePath, 'utf-8') + const content = await readFile(filePath, 'utf-8').catch(handleOnlyNotFound) + if (!content) { + return {} + } const lines = content.split('\n').map(l => l.split('=').map(s => s.trim())) const options = lines.reduce((a, b) => Object.assign(a, { [b[0]]: b[1] }), {}) as Record return options @@ -203,10 +205,6 @@ export class InstanceOptionsService extends AbstractService implements IInstance async editShaderOptions(options: EditShaderOptions): Promise { const instancePath = options.instancePath - // const instance = this.instanceService.state.all[instancePath] - // if (!instance) { - // throw new InstanceOptionException({ type: 'instanceNotFound', instancePath: options.instancePath! }) - // } const current = await this.getShaderOptions(instancePath) current.shaderPack = options.shaderPack diff --git a/xmcl-runtime/instance/InstanceService.ts b/xmcl-runtime/instance/InstanceService.ts index 03b8cf41c..7f5de46af 100644 --- a/xmcl-runtime/instance/InstanceService.ts +++ b/xmcl-runtime/instance/InstanceService.ts @@ -1,18 +1,16 @@ -import { ResolvedVersion, Version } from '@xmcl/core' -import { CreateInstanceOption, EditInstanceOptions, InstanceService as IInstanceService, Instance, InstanceException, InstanceSchema, InstanceServiceKey, InstanceState, InstancesSchema, MutableState, RuntimeVersions, createTemplate, filterForgeVersion, filterOptifineVersion, getExpectVersion, isFabricLoaderLibrary, isForgeLibrary, isOptifineLibrary } from '@xmcl/runtime-api' +import { CreateInstanceOption, EditInstanceOptions, InstanceService as IInstanceService, InstanceSchema, InstanceServiceKey, InstanceState, InstancesSchema, SharedState, RuntimeVersions, createTemplate } from '@xmcl/runtime-api' import filenamify from 'filenamify' import { existsSync } from 'fs' -import { copy, copyFile, ensureDir, readdir, readlink, rename, rm, stat } from 'fs-extra' +import { copy, ensureDir, readdir, readlink, rename, rm, stat } from 'fs-extra' import { basename, dirname, isAbsolute, join, relative, resolve } from 'path' import { Inject, LauncherAppKey, PathResolver, kGameDataPath } from '~/app' import { ImageStorage } from '~/imageStore' import { VersionMetadataService } from '~/install' -import { readLaunchProfile } from '~/launchProfile' -import { ExposeServiceKey, ServiceStateManager, Singleton, StatefulService } from '~/service' +import { ExposeServiceKey, ServiceStateManager, StatefulService } from '~/service' import { AnyError, isSystemError } from '~/util/error' import { validateDirectory } from '~/util/validate' import { LauncherApp } from '../app/LauncherApp' -import { copyPassively, ENOENT_ERROR, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, missing, readdirEnsured } from '../util/fs' +import { ENOENT_ERROR, exists, isDirectory, isPathDiskRootPath, linkWithTimeoutOrCopy, readdirEnsured } from '../util/fs' import { assignShallow, requireObject, requireString } from '../util/object' import { SafeFile, createSafeFile, createSafeIO } from '../util/persistance' @@ -65,24 +63,6 @@ export class InstanceService extends StatefulService implements I }) } - // if (Object.keys(state.all).length === 0) { - // const initial = this.app.getInitialInstance() - // if (initial) { - // try { - // await this.addExternalInstance(initial) - // const instance = Object.values(state.all)[0] - // // await this.mountInstance(instance.path) - // await this.instancesFile.write({ instances: Object.keys(this.state.all).map(normalizeInstancePath), selectedInstance: normalizeInstancePath(instance.path) }) - // } catch (e) { - // this.error(new Error(`Fail to initialize to ${initial}`, { cause: e })) - // await this.createAndMount({ name: 'Minecraft' }) - // } - // } else { - // this.log('Cannot find any instances, try to init one default modpack.') - // await this.createAndMount({ name: 'Minecraft' }) - // } - // } - this.state .subscribe('instanceEdit', async ({ path }) => { const inst = this.state.all[path] @@ -94,7 +74,7 @@ export class InstanceService extends StatefulService implements I this.instancesFile = createSafeFile(this.getAppDataPath('instances.json'), InstancesSchema, this, [this.getPath('instances.json')]) } - async getSharedInstancesState(): Promise> { + async getSharedInstancesState(): Promise> { await this.initialize() return this.state } @@ -211,9 +191,7 @@ export class InstanceService extends StatefulService implements I requireObject(payload) if (!payload.name) { - throw new InstanceException({ - type: 'instanceNameRequired', - }) + throw new TypeError('payload.name should not be empty!') } const instance = createTemplate() @@ -317,7 +295,7 @@ export class InstanceService extends StatefulService implements I await ensureDir(join(newPath, 'mods')) // hard link all source to new path const files = await readdir(modDirSrc) - await Promise.all(files.map(f => linkWithTimeoutOrCopy(join(modDirSrc, f), join(newPath, 'mods', f)))) + await Promise.allSettled(files.map(f => linkWithTimeoutOrCopy(join(modDirSrc, f), join(newPath, 'mods', f)))) } if (hasResourcepacks) { const resourcepacksDirSrc = join(path, 'resourcepacks') @@ -326,7 +304,7 @@ export class InstanceService extends StatefulService implements I // hard link all files await ensureDir(join(newPath, 'resourcepacks')) const files = await readdir(resourcepacksDirSrc) - await Promise.all(files.map(f => linkWithTimeoutOrCopy(join(resourcepacksDirSrc, f), join(newPath, 'resourcepacks', f)))) + await Promise.allSettled(files.map(f => linkWithTimeoutOrCopy(join(resourcepacksDirSrc, f), join(newPath, 'resourcepacks', f)))) } } if (hasShaderpacks) { @@ -336,7 +314,7 @@ export class InstanceService extends StatefulService implements I // hard link all files await ensureDir(join(newPath, 'shaderpacks')) const files = await readdir(shaderpacksDirSrc) - await Promise.all(files.map(f => linkWithTimeoutOrCopy(join(shaderpacksDirSrc, f), join(newPath, 'shaderpacks', f)))) + await Promise.allSettled(files.map(f => linkWithTimeoutOrCopy(join(shaderpacksDirSrc, f), join(newPath, 'shaderpacks', f)))) } } @@ -408,14 +386,10 @@ export class InstanceService extends StatefulService implements I state = this.state.all[instancePath] || this.state.instances.find(i => i.path === instancePath) if (!state) { - const error = new InstanceException({ - type: 'instanceNotFound', - path: instancePath, - }) this.error(new AnyError('InstanceNotFoundError', `Fail to find ${instancePath}. Existed: ${Object.keys(this.state.all).join(', ')}.`, )) - throw error + return } } @@ -437,11 +411,8 @@ export class InstanceService extends StatefulService implements I const newPath = join(dirname(instancePath), options.name) if (newPath !== instancePath) { if (this.state.instances.some(i => i.path === newPath)) { - throw new InstanceException({ - type: 'instanceNameDuplicated', - path: instancePath, - name: options.name, - }) + options.name = undefined + this.error(new AnyError('InstanceNameDuplicatedError')) } } } @@ -538,6 +509,13 @@ export class InstanceService extends StatefulService implements I } } + if ('env' in options) { + const hasDiff = typeof options.env !== typeof state.env || (options.env && state.env && Object.keys(options.env).some(k => options.env?.[k] !== state.env?.[k])) + if (hasDiff) { + result.env = options.env + } + } + if ('icon' in result && result.icon) { try { const iconURL = new URL(result.icon) @@ -560,146 +538,6 @@ export class InstanceService extends StatefulService implements I return resolve(path).startsWith(resolve(this.getPathUnder())) } - @Singleton() - async addExternalInstance(path: string): Promise { - const err = await validateDirectory(this.app.platform, path) - if (err && err !== 'exists') { - throw new InstanceException({ - type: 'instancePathInvalid', - path, - reason: err, - }) - } - - if (this.state.all[path]) { - this.log(`Skip to link already managed instance ${path}`) - return false - } - - if (resolve(path).startsWith(this.getPath()) || this.getPath().startsWith(resolve(path))) { - this.log(`Skip to add instance from root ${path}`) - return false - } - - // copy assets, library and versions - await Promise.all([ - copyPassively(resolve(path, 'libraries'), this.getPath('libraries')), - copyPassively(resolve(path, 'assets'), this.getPath('assets')), - ]) - - const versions = await readdir(resolve(path, 'versions')).catch(() => []) - const resolveVersions = [] as ResolvedVersion[] - const profile = await readLaunchProfile(path).catch(() => undefined) - let isVersionIsolated = false - await Promise.all(versions.map(async (v) => { - try { - // only resolve valid version - const version = await Version.parse(path, v) - resolveVersions.push(version) - const versionRoot = resolve(path, 'versions', v) - - const versionJson = resolve(versionRoot, `${v}.json`) - const versionJar = resolve(versionRoot, `${v}.jar`) - await Promise.all([ - copyFile(versionJar, this.getPath('versions', v, `${v}.jar`)).catch(() => undefined), - copyFile(versionJson, this.getPath('versions', v, `${v}.json`)).catch(() => undefined), - ]) - - const files = (await readdir(versionRoot)).filter(f => f !== '.DS_Store' && f !== `${v}.json` && f !== `${v}.jar`) - if (files.some(f => f === 'saves' || f === 'mods' || f === 'options.txt' || f === 'config' || f === 'PCL')) { - // this is an version isolation - const options: CreateInstanceOption = { - path: versionRoot, - name: version.id, - } - if (profile) { - for (const p of Object.values(profile.profiles)) { - if (p.lastVersionId === version.id) { - options.name = p.name - options.java = p.javaDir - options.vmOptions = p.javaArgs?.split(' ') || [] - break - } - } - } - options.runtime = { - minecraft: version.minecraftVersion, - forge: filterForgeVersion(version.libraries.find(isForgeLibrary)?.version ?? ''), - fabricLoader: version.libraries.find(isFabricLoaderLibrary)?.version ?? '', - optifine: filterOptifineVersion(version.libraries.find(isOptifineLibrary)?.version ?? ''), - } - isVersionIsolated = true - await this.createInstance(options) - } - } catch (e) { - if (e instanceof Error) this.error(e) - // TODO: handle - } - })) - - if (!isVersionIsolated) { - const options: CreateInstanceOption = { - path, - name: '', - } - if (profile) { - const sorted = Object.values(profile.profiles).sort((a, b) => - // @ts-ignore - new Date(b.lastUsed) - new Date(a.lastUsed)) - let version: ResolvedVersion | undefined - for (const p of sorted) { - const id = p.lastVersionId - version = resolveVersions.find(v => v.id === id) - options.name = p.name - options.java = p.javaDir - options.vmOptions = p.javaArgs?.split(' ') || [] - if (version) { - break - } - } - if (version) { - options.runtime = { - minecraft: version.minecraftVersion, - forge: filterForgeVersion(version.libraries.find(isForgeLibrary)?.version ?? ''), - fabricLoader: version.libraries.find(isFabricLoaderLibrary)?.version ?? '', - optifine: filterOptifineVersion(version.libraries.find(isOptifineLibrary)?.version ?? ''), - } - } else { - options.runtime = { - minecraft: this.versionMetadataService.getLatestRelease(), - } - } - } else { - const version = resolveVersions[0] - if (version) { - options.runtime = { - minecraft: version.minecraftVersion, - forge: filterForgeVersion(version.libraries.find(isForgeLibrary)?.version ?? ''), - fabricLoader: version.libraries.find(isFabricLoaderLibrary)?.version ?? '', - optifine: filterOptifineVersion(version.libraries.find(isOptifineLibrary)?.version ?? ''), - } - } else { - options.runtime = { - minecraft: this.versionMetadataService.getLatestRelease(), - } - } - } - - const dirPath = dirname(path) - const folderName = basename(dirPath) - if (folderName === 'minecraft' || folderName === '.minecraft') { - const name = getExpectVersion(options.runtime) - options.name = name - } else { - options.name = isPathDiskRootPath(dirPath) ? basename(path) : folderName - } - - await this.createInstance(options) - } - - return true - } - async acquireInstanceById(id: string): Promise { id = filenamify(id) this.log(`Acquire instance by id ${id}`) diff --git a/xmcl-runtime/java/JavaService.ts b/xmcl-runtime/java/JavaService.ts index 117ce5fc8..7eca6b5a2 100644 --- a/xmcl-runtime/java/JavaService.ts +++ b/xmcl-runtime/java/JavaService.ts @@ -1,6 +1,6 @@ import { JavaVersion } from '@xmcl/core' import { DEFAULT_RUNTIME_ALL_URL, JavaRuntimeManifest, JavaRuntimeTargetType, JavaRuntimes, installJavaRuntimeTask, parseJavaVersion, resolveJava, scanLocalJava } from '@xmcl/installer' -import { JavaService as IJavaService, Java, JavaRecord, JavaSchema, JavaServiceKey, JavaState, MutableState, Settings } from '@xmcl/runtime-api' +import { JavaService as IJavaService, Java, JavaRecord, JavaSchema, JavaServiceKey, JavaState, SharedState, Settings } from '@xmcl/runtime-api' import { chmod, ensureFile, readFile, stat } from 'fs-extra' import { dirname, join } from 'path' import { Inject, LauncherAppKey, PathResolver, kGameDataPath } from '~/app' @@ -52,7 +52,7 @@ export class JavaService extends StatefulService implements IJavaServ return Promise.resolve() } - async getJavaState(): Promise> { + async getJavaState(): Promise> { await this.initialize() return this.state } diff --git a/xmcl-runtime/launch/LaunchService.ts b/xmcl-runtime/launch/LaunchService.ts index d9d64696c..ba8106fa0 100644 --- a/xmcl-runtime/launch/LaunchService.ts +++ b/xmcl-runtime/launch/LaunchService.ts @@ -99,6 +99,7 @@ export class LaunchService extends AbstractService implements ILaunchService { extraExecOption: { detached: true, cwd: minecraftFolder.getPath('server'), + env: options.env, }, extraJVMArgs: jvmArgs, @@ -147,6 +148,7 @@ export class LaunchService extends AbstractService implements ILaunchService { extraExecOption: { detached: true, cwd: minecraftFolder.root, + env: options.env, }, extraJVMArgs: options.vmOptions?.filter(v => !!v), extraMCArgs: options.mcOptions?.filter(v => !!v), @@ -243,7 +245,17 @@ export class LaunchService extends AbstractService implements ILaunchService { if (e instanceof LaunchException) { throw e } - throw new LaunchException({ type: 'launchGeneralException', error: { ...(e as any), message: (e as any).message, stack: (e as any).stack } }) + if (e instanceof Error) { + if (!e.stack) { + e.stack = new Error().stack + } + if (e.name === 'Error') { + Object.assign(e, { + name: 'LaunchGeneralError', + }) + } + } + throw e } } @@ -453,7 +465,17 @@ export class LaunchService extends AbstractService implements ILaunchService { if (e instanceof LaunchException) { throw e } - throw new LaunchException({ type: 'launchGeneralException', error: { ...(e as any), message: (e as any).message, stack: (e as any).stack } }, (e as any).message, { cause: e }) + if (e instanceof Error) { + if (!e.stack) { + e.stack = new Error().stack + } + if (e.name === 'Error') { + Object.assign(e, { + name: 'LaunchGeneralError', + }) + } + } + throw e } } diff --git a/xmcl-runtime/mod/InstanceModsService.ts b/xmcl-runtime/mod/InstanceModsService.ts index b00921fc5..9e229605d 100644 --- a/xmcl-runtime/mod/InstanceModsService.ts +++ b/xmcl-runtime/mod/InstanceModsService.ts @@ -1,6 +1,6 @@ import { CurseforgeV1Client } from '@xmcl/curseforge' import { ModrinthV2Client } from '@xmcl/modrinth' -import { InstanceModsService as IInstanceModsService, InstallMarketOptionWithInstance, InstallModsOptions, InstanceModsServiceKey, ResourceState, LockKey, MutableState, Resource, ResourceDomain, getInstanceModStateKey } from '@xmcl/runtime-api' +import { InstanceModsService as IInstanceModsService, InstallMarketOptionWithInstance, InstallModsOptions, InstanceModsServiceKey, ResourceState, LockKey, SharedState, Resource, ResourceDomain, getInstanceModStateKey } from '@xmcl/runtime-api' import { emptyDir, ensureDir, rename, stat, unlink } from 'fs-extra' import { basename, dirname, join } from 'path' import { Inject, LauncherAppKey } from '~/app' @@ -25,7 +25,7 @@ export class InstanceModsService extends AbstractService implements IInstanceMod async refreshMetadata(instancePath: string): Promise { const stateManager = await this.app.registry.get(ServiceStateManager) - const state = stateManager.get>(getInstanceModStateKey(instancePath)) + const state = stateManager.get>(getInstanceModStateKey(instancePath)) if (state) { await state.revalidate() const modrinthClient = await this.app.registry.getOrCreate(ModrinthV2Client) @@ -112,7 +112,7 @@ export class InstanceModsService extends AbstractService implements IInstanceMod await this.app.shell.openDirectory(join(path, 'mods')) } - async watch(instancePath: string): Promise> { + async watch(instancePath: string): Promise> { if (!instancePath) throw new AnyError('WatchModError', 'Cannot watch instance mods on empty path') const lock = this.semaphoreManager.getLock(LockKey.instance(instancePath)) const stateManager = await this.app.registry.get(ServiceStateManager) diff --git a/xmcl-runtime/moddb/ProjectMappingService.ts b/xmcl-runtime/moddb/ProjectMappingService.ts index 9c7990bae..84b068c4a 100644 --- a/xmcl-runtime/moddb/ProjectMappingService.ts +++ b/xmcl-runtime/moddb/ProjectMappingService.ts @@ -42,6 +42,7 @@ export class ProjectMappingService extends AbstractService implements IProjectMa private async ensureDatabase(init = false) { const locale = this.settings.locale.toLowerCase() const gfw = await this.app.registry.get(kGFW) + const app = this.app if (!locale) return undefined if (this.#db?.locale === locale) return this.#db.db @@ -52,7 +53,7 @@ export class ProjectMappingService extends AbstractService implements IProjectMa async function exists() { try { - const resp = await fetch(original + '.sha256', { method: 'HEAD' }) + const resp = await app.fetch(original + '.sha256', { method: 'HEAD' }) if (!resp.ok) { return false } diff --git a/xmcl-runtime/modpack/ModpackService.ts b/xmcl-runtime/modpack/ModpackService.ts index a4c31b855..8e9b12086 100644 --- a/xmcl-runtime/modpack/ModpackService.ts +++ b/xmcl-runtime/modpack/ModpackService.ts @@ -1,5 +1,5 @@ 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 { CreateInstanceOption, CurseforgeModpackManifest, ExportModpackOptions, ModpackService as IModpackService, InstallMarketOptions, Instance, InstanceData, InstanceFile, McbbsModpackManifest, ModpackException, ModpackInstallProfile, ModpackServiceKey, ModpackState, ModrinthModpackManifest, SharedState, ResourceDomain, ResourceMetadata, ResourceState, UpdateResourcePayload, findMatchedVersion, getCurseforgeModpackFromInstance, getMcbbsModpackFromInstance, getModrinthModpackFromInstance, isAllowInModrinthModpack } from '@xmcl/runtime-api' import { ensureDir, mkdir, readdir, remove, stat, unlink } from 'fs-extra' import { dirname, join } from 'path' import { Entry, ZipFile } from 'yauzl' @@ -380,7 +380,7 @@ export class ModpackService extends AbstractService implements IModpackService { return files } - async openModpack(modpackFile: string): Promise> { + async openModpack(modpackFile: string): Promise> { const store = await this.app.registry.get(ServiceStateManager) const zipManager = await this.app.registry.getOrCreate(ZipManager) @@ -471,7 +471,7 @@ export class ModpackService extends AbstractService implements IModpackService { this.app.shell.openDirectory(this.getPath('modpacks')) } - async watchModpackFolder(): Promise> { + async watchModpackFolder(): Promise> { const states = await this.app.registry.getOrCreate(ServiceStateManager) return states.registerOrGet('modpacks', async ({ doAsyncOperation }) => { const dir = this.getPath('modpacks') diff --git a/xmcl-runtime/package.json b/xmcl-runtime/package.json index 30c04db62..99a11f092 100644 --- a/xmcl-runtime/package.json +++ b/xmcl-runtime/package.json @@ -22,7 +22,6 @@ "@azure/msal-common": "^14.14.0", "@azure/msal-node": "^2.12.0", "xxhash-wasm": "^1.0.2", - "jsonwebtoken": "9.0.2", "@node-rs/crc32-wasm32-wasi": "^1.10.3", "@xmcl/client": "workspace:*", "@xmcl/core": "workspace:*", @@ -49,7 +48,7 @@ "ajv": "^8.11.2", "applicationinsights": "^2.9.1", "atomically": "^2.0.3", - "fast-json-stringify": "^5.8.0", + "chokidar": "^4.0.3", "fast-xml-parser": "^4.3.2", "file-type": "^16.5.4", "filenamify": "^5.1.1", @@ -64,10 +63,8 @@ "murmurhash": "^2.0.1", "node-datachannel": "0.9.1", "node-disk-info": "^1.3.0", - "node-domexception": "^2.0.1", "node-sqlite3-wasm": "^0.8.16", "node-watch": "^0.7.4", - "normalize-url": "^7.2.0", "tar-stream": "^3.1.7", "undici": "7.1.1", "yazl": "^2.5.1" @@ -80,7 +77,7 @@ "@types/lodash.debounce": "^4.0.7", "@types/lodash.throttle": "^4.1.7", "@types/lzma-native": "^4.0.1", - "@types/node": "~18", + "@types/node": "~20", "@types/tar-stream": "^3.1.3", "@types/ws": "^8.5.3", "@types/yauzl": "^2.10.0", diff --git a/xmcl-runtime/peer/PeerService.ts b/xmcl-runtime/peer/PeerService.ts index 2a95c977b..d50c9d09a 100644 --- a/xmcl-runtime/peer/PeerService.ts +++ b/xmcl-runtime/peer/PeerService.ts @@ -1,5 +1,5 @@ import { DownloadTask } from '@xmcl/installer' -import { PeerService as IPeerService, MutableState, PeerServiceKey, PeerState, ShareInstanceOptions } from '@xmcl/runtime-api' +import { PeerService as IPeerService, SharedState, PeerServiceKey, PeerState, ShareInstanceOptions } from '@xmcl/runtime-api' import { Inject, LauncherApp, LauncherAppKey, kGameDataPath } from '~/app' import { ExposeServiceKey, ServiceStateManager, StatefulService } from '~/service' import { kPeerFacade } from './PeerServiceFacade' @@ -45,7 +45,7 @@ export class PeerService extends StatefulService implements IPeerServ }) } - async getPeerState(): Promise> { + async getPeerState(): Promise> { return this.state } diff --git a/xmcl-runtime/peer/multiplayerImpl.ts b/xmcl-runtime/peer/multiplayerImpl.ts index ddc846482..6e6740027 100644 --- a/xmcl-runtime/peer/multiplayerImpl.ts +++ b/xmcl-runtime/peer/multiplayerImpl.ts @@ -1,4 +1,4 @@ -import { MutableState, PeerState, SetRemoteDescriptionOptions, TransferDescription, createPromiseSignal } from '@xmcl/runtime-api' +import { SharedState, PeerState, SetRemoteDescriptionOptions, TransferDescription, createPromiseSignal } from '@xmcl/runtime-api' import { randomUUID } from 'crypto' import EventEmitter from 'events' import { promisify } from 'util' @@ -83,7 +83,7 @@ export class Peers { export function createMultiplayer() { const peers = new Peers() - const state = createPromiseSignal>() + const state = createPromiseSignal>() const emitter = new EventEmitter() let _PeerConnection: any let _RTCPeerConnection: typeof RTCPeerConnection @@ -479,7 +479,7 @@ export function createMultiplayer() { emitter, host, updateIceServers: iceServers.update, - setState: (_state: MutableState) => { + setState: (_state: SharedState) => { state.resolve(_state) _state.connectionClear() }, diff --git a/xmcl-runtime/presence/PresenceService.ts b/xmcl-runtime/presence/PresenceService.ts index 8a0019295..0dd9c3857 100644 --- a/xmcl-runtime/presence/PresenceService.ts +++ b/xmcl-runtime/presence/PresenceService.ts @@ -1,5 +1,5 @@ import { Client, SetActivity } from '@xmcl/discord-rpc' -import { PresenceService as IPresenceService, MutableState, PresenceServiceKey, Settings } from '@xmcl/runtime-api' +import { PresenceService as IPresenceService, SharedState, PresenceServiceKey, Settings } from '@xmcl/runtime-api' import { Inject, LauncherAppKey } from '~/app' import { AbstractService, ExposeServiceKey } from '~/service' import { kSettings } from '~/settings' @@ -12,7 +12,7 @@ export class PresenceService extends AbstractService implements IPresenceService } constructor(@Inject(LauncherAppKey) app: LauncherApp, - @Inject(kSettings) private settings: MutableState, + @Inject(kSettings) private settings: SharedState, ) { super(app, async () => { if (settings.discordPresence) { diff --git a/xmcl-runtime/resource/core/parseMetadata.ts b/xmcl-runtime/resource/core/parseMetadata.ts index 5009e7a0c..f63dcbe79 100644 --- a/xmcl-runtime/resource/core/parseMetadata.ts +++ b/xmcl-runtime/resource/core/parseMetadata.ts @@ -1,4 +1,4 @@ -import { File, ResourceDomain, ResourceMetadata } from '@xmcl/runtime-api' +import { Exception, File, ResourceDomain, ResourceMetadata } from '@xmcl/runtime-api' import { ResourceContext } from './ResourceContext' import { jsonArrayFrom } from './helper' import { upsertMetadata } from './upsertMetadata' @@ -6,6 +6,9 @@ import { ResourceSnapshotTable } from './schema' import { pickMetadata } from './generateResource' import { ResourceWorkerQueuePayload } from './ResourceWorkerQueuePayload' +class ParseException extends Exception<{ type: 'parseResourceException'; code: string }> { +} + export async function getOrParseMetadata(file: File, record: ResourceSnapshotTable, domain: ResourceDomain, context: ResourceContext, job: ResourceWorkerQueuePayload, parse: boolean) { @@ -20,13 +23,24 @@ export async function getOrParseMetadata(file: File, record: ResourceSnapshotTab .executeTakeFirst() .then(r => r ? ({ ...pickMetadata(r), icons: r?.icons.map(i => i.icon) }) : undefined) + function handleParseError(err: any): never { + // create a temp exception to bypass telemetry + if (err.name === 'InvalidZipFileError' || + err.name === 'MultiDiskZipFileError' || + err.name === 'InvalidCentralDirectoryFileHeaderError' || + err.name === 'CompressedUncompressedSizeMismatchError') { + throw new ParseException({ type: 'parseResourceException', code: err.name }) + } + throw err + } + if (parse) { if (!cachedMetadata) { const { metadata, uris, icons, name } = await context.parse({ path: file.path, fileType: record.fileType, domain, - }) + }).catch(handleParseError) const iconPaths = await Promise.all(icons.map(icon => context.image.addImage(icon).catch(() => ''))) const allIcons = iconPaths.filter(icon => icon) @@ -58,7 +72,7 @@ export async function getOrParseMetadata(file: File, record: ResourceSnapshotTab path: file.path, fileType: record.fileType, domain, - }) + }).catch(handleParseError) metadata.name = name diff --git a/xmcl-runtime/resource/parsers/index.ts b/xmcl-runtime/resource/parsers/index.ts index 154a82124..c54eafbd8 100644 --- a/xmcl-runtime/resource/parsers/index.ts +++ b/xmcl-runtime/resource/parsers/index.ts @@ -75,7 +75,23 @@ export class ResourceParser { } const icons: Uint8Array[] = [] - const fs = await openFileSystem(args.path) + const fs = await openFileSystem(args.path).catch((e) => { + if (e.message === 'Invalid zip file') { + Object.assign(e, { name: 'InvalidZipFileError' }) + throw e + } + if (e.message.startsWith('multi-disk zip files are not supported: found disk number')) { + Object.assign(e, { name: 'MultiDiskZipFileError' }) + throw e + } + if (e.message.startsWith('invalid central directory file header signature')) { + Object.assign(e, { name: 'InvalidCentralDirectoryFileHeaderError' }) + } + if (e.message.startsWith('compressed/uncompressed size mismatch for stored file')) { + Object.assign(e, { name: 'CompressedUncompressedSizeMismatchError' }) + } + throw e + }) const container: ResourceMetadata = {} const fileName = basename(args.path) const uris = [] as string[] diff --git a/xmcl-runtime/resource/pluginResourceWorker.ts b/xmcl-runtime/resource/pluginResourceWorker.ts index bbfb13426..2ba8e22ca 100644 --- a/xmcl-runtime/resource/pluginResourceWorker.ts +++ b/xmcl-runtime/resource/pluginResourceWorker.ts @@ -16,7 +16,7 @@ import createResourceWorker from './resource.worker?worker' import { kResourceContext } from './ResourceManager' import { kResourceWorker, ResourceWorker } from './worker' import { ServiceStateManager } from '~/service' -import { InstanceServiceKey, InstanceState, MutableState } from '@xmcl/runtime-api' +import { InstanceServiceKey, InstanceState, SharedState } from '@xmcl/runtime-api' import { getDomainedPath } from './core/snapshot' export const pluginResourceWorker: LauncherAppPlugin = async (app) => { @@ -72,7 +72,7 @@ export const pluginResourceWorker: LauncherAppPlugin = async (app) => { app.registry.get(ServiceStateManager).then((manager) => manager.get(InstanceServiceKey.toString())) .then((state) => { - (state as unknown as MutableState)?.subscribe('instanceRemove', (path) => { + (state as unknown as SharedState)?.subscribe('instanceRemove', (path) => { context.db.deleteFrom('snapshots') .where('domainedPath', 'like', `${getDomainedPath(path, context.root)}%`) .execute() diff --git a/xmcl-runtime/save/InstanceSavesService.ts b/xmcl-runtime/save/InstanceSavesService.ts index 7e95c5224..57c6e0e98 100644 --- a/xmcl-runtime/save/InstanceSavesService.ts +++ b/xmcl-runtime/save/InstanceSavesService.ts @@ -3,9 +3,9 @@ import { CloneSaveOptions, DeleteSaveOptions, ExportSaveOptions, getInstanceSaveKey, InstanceSavesService as IInstanceSavesService, + ImportSaveException, ImportSaveOptions, InstallMarketOptionWithInstance, - InstanceSaveException, InstanceSavesServiceKey, LaunchOptions, LinkSaveAsServerWorldOptions, @@ -27,7 +27,7 @@ import { LaunchService } from '~/launch' import { kMarketProvider } from '~/market' import { ResourceManager } from '~/resource' import { AbstractService, ExposeServiceKey, ServiceStateManager } from '~/service' -import { isSystemError } from '~/util/error' +import { AnyError, isSystemError } from '~/util/error' import { readlinkSafe } from '~/util/linkResourceFolder' import { LauncherApp } from '../app/LauncherApp' import { copyPassively, isDirectory, linkDirectory, missing, readdirIfPresent } from '../util/fs' @@ -90,8 +90,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa const savePath = isAbsolute(saveName) ? saveName : join(instancePath, 'saves', saveName) if (await missing(savePath)) { - // @ts-ignore - throw new InstanceSaveException({ type: 'instanceLinkSaveNotFound', name: saveName }) + throw new AnyError('InstanceLinkSaveNotFoundError', 'The save is not found.', undefined, { saveName }) } const serverWorldPath = join(instancePath, 'server', 'world') @@ -288,21 +287,15 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa const srcSavePath = join(srcInstancePath, saveName) if (await missing(srcSavePath)) { - throw new InstanceSaveException({ type: 'instanceCopySaveNotFound', src: srcSavePath, dest: destInstancePaths }, - `Cancel save copying of ${saveName}`) + throw new AnyError('CloneSaveSaveNotFoundError', `Cannot find save ${saveName}`, undefined, { + saveName, + }) } - // if (!this.instanceService.state.all[srcInstancePath]) { - // throw new InstanceSaveException({ - // type: 'instanceNotFound', - // instancePath: srcInstancePath, - // }, `Cannot find managed instance ${srcInstancePath}`) - // } if (destInstancePaths.some(p => !this.instanceService.state.all[p])) { const notFound = destInstancePaths.find(p => !this.instanceService.state.all[p])! - throw new InstanceSaveException({ - type: 'instanceNotFound', + throw new AnyError('CloneSaveInstanceNotFoundError', `Cannot find managed instance ${notFound}`, undefined, { instancePath: notFound, - }, `Cannot find managed instance ${notFound}`) + }) } const destSavePaths = destInstancePaths.map(d => join(d, 'saves', destSaveName)) @@ -325,7 +318,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa const savePath = instancePath ? join(instancePath, 'saves', saveName) : this.getPath('saves', saveName) if (await missing(savePath)) { - throw new InstanceSaveException({ type: 'instanceDeleteNoSave', name: saveName }) + return } await rm(savePath, { recursive: true, force: true }) @@ -339,7 +332,9 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa const savePath = join(instancePath, 'saves', saveName) if (await missing(savePath)) { - throw new InstanceSaveException({ type: 'instanceDeleteNoSave', name: saveName }) + throw new AnyError('InstanceDeleteNoSave', `Cannot find save ${saveName}`, undefined, { + saveName, + }) } await ensureDir(this.getPath('saves')) @@ -373,7 +368,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa if (isDir) { if (!existsSync(join(path, 'level.dat'))) { - throw new InstanceSaveException({ type: 'instanceImportIllegalSave', path }) + throw new ImportSaveException({ type: 'instanceImportIllegalSave', path }) } const sharedSavesDir = this.getPath('saves') @@ -401,7 +396,7 @@ export class InstanceSavesService extends AbstractService implements IInstanceSa } if (saveRoot === undefined) { - throw new InstanceSaveException({ type: 'instanceCopySaveUnexpected', src: path, dest: [dest] }) + throw new ImportSaveException({ type: 'instanceImportIllegalSave', path }) } const root = saveRoot diff --git a/xmcl-runtime/service/Service.ts b/xmcl-runtime/service/Service.ts index 8ef290665..02182cbc1 100644 --- a/xmcl-runtime/service/Service.ts +++ b/xmcl-runtime/service/Service.ts @@ -1,4 +1,4 @@ -import { createPromiseSignal, getServiceSemaphoreKey, MutableState, PromiseSignal, ServiceKey, State } from '@xmcl/runtime-api' +import { createPromiseSignal, getServiceSemaphoreKey, SharedState, PromiseSignal, ServiceKey, State } from '@xmcl/runtime-api' import { join } from 'path' import { EventEmitter } from 'stream' import { Logger } from '~/logger' @@ -235,9 +235,9 @@ export abstract class AbstractService extends EventEmitter { } export abstract class StatefulService> extends AbstractService { - state: MutableState + state: SharedState - constructor(app: LauncherApp, createState: () => MutableState, initializer?: () => Promise) { + constructor(app: LauncherApp, createState: () => SharedState, initializer?: () => Promise) { super(app, initializer) this.state = createState() } diff --git a/xmcl-runtime/service/ServiceStateContainer.ts b/xmcl-runtime/service/ServiceStateContainer.ts index 0744d50a0..d37baea2c 100644 --- a/xmcl-runtime/service/ServiceStateContainer.ts +++ b/xmcl-runtime/service/ServiceStateContainer.ts @@ -2,7 +2,7 @@ import EventEmitter from 'events' import { AnyError } from '~/util/error' import { ServiceStateContext } from './ServiceStateManager' import { Client } from '~/app' -import { MutableState, createPromiseSignal } from '@xmcl/runtime-api' +import { SharedState, createPromiseSignal } from '@xmcl/runtime-api' import { MutableStateImpl, kStateKey } from './stateUtils' export type ServiceStateFactory = (context: ServiceStateContext) => Promise<[T, () => void] | [T, () => void, () => Promise]> @@ -19,8 +19,8 @@ export class ServiceStateContainer implements ServiceStateContext { #revalidating: Promise | undefined private semaphore = 0 #clients: [Client, Function][] = [] - #state: MutableState | undefined - #signal = createPromiseSignal>() + #state: SharedState | undefined + #signal = createPromiseSignal>() #disposer: () => void = () => { } #revalidator?: () => Promise #emitter = new EventEmitter() diff --git a/xmcl-runtime/service/ServiceStateManager.ts b/xmcl-runtime/service/ServiceStateManager.ts index 0ff50bdda..4fe28f5c3 100644 --- a/xmcl-runtime/service/ServiceStateManager.ts +++ b/xmcl-runtime/service/ServiceStateManager.ts @@ -1,4 +1,4 @@ -import { MutableState, ServiceKey, State } from '@xmcl/runtime-api' +import { SharedState, ServiceKey, State } from '@xmcl/runtime-api' import { Client, LauncherApp } from '~/app' import { Logger } from '~/logger' import { AnyError } from '~/util/error' @@ -46,7 +46,7 @@ export class ServiceStateManager { * @param key The key of the state object * @returns The mutable state object */ - registerStatic(state: T, key: string | ServiceKey): MutableState { + registerStatic(state: T, key: string | ServiceKey): SharedState { const container = new ServiceStateContainer( key.toString(), this.#unregister, @@ -66,14 +66,14 @@ export class ServiceStateManager { * @param client The client to track the state * @param state */ - serializeAndTrack(client: Client, state: MutableState) { + serializeAndTrack(client: Client, state: SharedState) { const container = ServiceStateContainer.unwrap(state) if (!container) throw new TypeError('Unregistered state!') container.track(client) return JSON.parse(JSON.stringify(state)) } - get = MutableState>>(id: string): T | undefined { + get = SharedState>>(id: string): T | undefined { return this.containers[id]?.state as any } @@ -89,7 +89,7 @@ export class ServiceStateManager { await this.#revalidate(container) } - async registerOrGet>(id: string, factory: ServiceStateFactory): Promise> { + async registerOrGet>(id: string, factory: ServiceStateFactory): Promise> { if (this.containers[id]) { const container = this.containers[id] await this.#revalidate(container) diff --git a/xmcl-runtime/service/pluginServicesHandler.ts b/xmcl-runtime/service/pluginServicesHandler.ts index c24aa68d1..8e82ef9af 100644 --- a/xmcl-runtime/service/pluginServicesHandler.ts +++ b/xmcl-runtime/service/pluginServicesHandler.ts @@ -1,9 +1,9 @@ import { ServiceKey } from '@xmcl/runtime-api' import { Client, LauncherAppPlugin } from '../app' -import { AnyError, serializeError } from '../util/error' +import { AnyError, getNormalizeException, getSerializedError } from '../util/error' import { AbstractService, ServiceConstructor, getServiceKey } from './Service' -import { isStateObject } from './stateUtils' import { ServiceStateManager } from './ServiceStateManager' +import { isStateObject } from './stateUtils' export const pluginServicesHandler = (services: ServiceConstructor[]): LauncherAppPlugin => (app, manifest) => { const logger = app.getLogger('Services') @@ -66,16 +66,26 @@ export const pluginServicesHandler = (services: ServiceConstructor[]): LauncherA return { result: r } } catch (e) { app.emit('service-call-end', serviceName, serviceMethod, Date.now() - start, false) - logger.warn(`Error during service call ${serviceName}.${serviceMethod}:`) - if (e instanceof Error) { - Object.assign(e, { payload }) - logger.error(e, serviceName) - } else { - logger.error(new AnyError('UnknownServiceError', JSON.stringify(e), undefined, { payload }), serviceName) + const exception = await getNormalizeException(e) + + if (!exception) { + // only log the error if it is not a known exception + const err = Object.assign( + e instanceof Error + ? e + : new AnyError('ServiceUnknownError', typeof e === 'string' ? e : JSON.stringify(e), undefined), + { payload, serviceMethod }, + ) + logger.warn(`Error during service call ${serviceName}.${serviceMethod}:`) + logger.error(err, serviceName) } - const error = await serializeError(e) - error.serviceName = serviceName - error.serviceMethod = serviceMethod + + // serailize the error and send to client + const error = await getSerializedError(exception || e, { + serviceName, + serviceMethod, + payload, + }) return { error } } } diff --git a/xmcl-runtime/service/stateUtils.ts b/xmcl-runtime/service/stateUtils.ts index 3194e59b5..4676de4f0 100644 --- a/xmcl-runtime/service/stateUtils.ts +++ b/xmcl-runtime/service/stateUtils.ts @@ -1,9 +1,9 @@ -import { MutableState } from '@xmcl/runtime-api' +import { SharedState } from '@xmcl/runtime-api' import EventEmitter from 'events' export const kStateKey = '__state__' -export function isStateObject(v: object): v is MutableState { +export function isStateObject(v: object): v is SharedState { return v && typeof v === 'object' && kStateKey in v } diff --git a/xmcl-runtime/settings/pluginSettings.ts b/xmcl-runtime/settings/pluginSettings.ts index 369d40194..d2cc7325b 100644 --- a/xmcl-runtime/settings/pluginSettings.ts +++ b/xmcl-runtime/settings/pluginSettings.ts @@ -41,6 +41,7 @@ export const pluginSettings: LauncherAppPlugin = async (app) => { globalDisableElyByAuthlib: state.globalDisableElyByAuthlib, enableDedicatedGPUOptimization: state.enableDedicatedGPUOptimization, replaceNatives: state.replaceNatives, + globalEnv: state.globalEnv, }), 1000) app.registryDisposer(async () => { diff --git a/xmcl-runtime/settings/settings.ts b/xmcl-runtime/settings/settings.ts index 2a3bcabe0..b2fac805c 100644 --- a/xmcl-runtime/settings/settings.ts +++ b/xmcl-runtime/settings/settings.ts @@ -1,7 +1,7 @@ -import { MutableState, Settings } from '@xmcl/runtime-api' +import { SharedState, Settings } from '@xmcl/runtime-api' import { InjectionKey } from '~/app' -export const kSettings: InjectionKey> = Symbol('settings') +export const kSettings: InjectionKey> = Symbol('settings') export function shouldOverrideApiSet(s: Settings, gfw: boolean) { if (s.apiSetsPreference === 'mojang') { diff --git a/xmcl-runtime/task/pluginTasks.ts b/xmcl-runtime/task/pluginTasks.ts index 91d09f14a..76d76e879 100644 --- a/xmcl-runtime/task/pluginTasks.ts +++ b/xmcl-runtime/task/pluginTasks.ts @@ -2,7 +2,7 @@ import { CancelledError, Task, TaskContext } from '@xmcl/task' import { randomUUID } from 'crypto' import { EventEmitter } from 'events' import { Client, LauncherAppPlugin } from '~/app' -import { serializeError } from '../util/error' +import { getNormalizeException, getSerializedError } from '../util/error' import { TaskEventEmitter, createTaskPusher, kTaskExecutor, kTasks, mapTaskToTaskPayload } from './task' export const pluginTasks: LauncherAppPlugin = (app) => { @@ -76,17 +76,18 @@ export const pluginTasks: LauncherAppPlugin = (app) => { if (error instanceof CancelledError) { emitter.emit('cancel', uid, task) } else { - const e = await serializeError(error) - emitter.emit('fail', uid, task, e) - Reflect.set(task, 'error', e) + const exception = await getNormalizeException(error) + const serializedError = await getSerializedError(exception || error, { + task: task.name, + }) + emitter.emit('fail', uid, task, serializedError) + Reflect.set(task, 'error', serializedError) logger.warn(`Task ${task.path} (${Object.getPrototypeOf(task).constructor.name}) ${task.name}(${uid}) failed!`) - if (error instanceof Array) { - for (const e of error) { - logger.warn(e) - } + if (exception) { + logger.warn(exception) } else { - logger.warn(error) + logger.error(error) } } }, diff --git a/xmcl-runtime/telemetry/pluginTelemetry.ts b/xmcl-runtime/telemetry/pluginTelemetry.ts index e6885de24..5d671b90d 100644 --- a/xmcl-runtime/telemetry/pluginTelemetry.ts +++ b/xmcl-runtime/telemetry/pluginTelemetry.ts @@ -1,4 +1,4 @@ -import { LaunchService as ILaunchService } from '@xmcl/runtime-api' +import { Exception, LaunchService as ILaunchService } from '@xmcl/runtime-api' import type { Contracts } from 'applicationinsights' import { randomUUID } from 'crypto' import { LauncherAppPlugin } from '~/app' @@ -40,7 +40,7 @@ export const pluginTelemetry: LauncherAppPlugin = async (app) => { appInsight.setup(DEFAULT_APP_INSIGHT_KEY) .setDistributedTracingMode(appInsight.DistributedTracingModes.AI_AND_W3C) - .setAutoCollectExceptions(true) + .setAutoCollectExceptions(false) .setAutoCollectPerformance(false) .setAutoCollectConsole(false) .setAutoCollectHeartbeat(false) @@ -190,6 +190,10 @@ export const pluginTelemetry: LauncherAppPlugin = async (app) => { app.logEmitter.on('failure', (destination, tag, e: Error) => { if (settings.disableTelemetry) return + if (e instanceof Exception) { + // Skip for exception + return + } defaultClient.trackException({ exception: e, properties: e ? { ...e } : undefined, diff --git a/xmcl-runtime/user/UserService.ts b/xmcl-runtime/user/UserService.ts index c9c3c4f80..486b96a22 100644 --- a/xmcl-runtime/user/UserService.ts +++ b/xmcl-runtime/user/UserService.ts @@ -4,7 +4,7 @@ import { AUTHORITY_MICROSOFT, UserService as IUserService, LoginOptions, - MutableState, + SharedState, RefreshUserOptions, SaveSkinOptions, UploadSkinOptions, UserException, @@ -92,7 +92,7 @@ export class UserService extends StatefulService implements IUserServ } } - async getUserState(): Promise> { + async getUserState(): Promise> { await this.initialize() return this.state } diff --git a/xmcl-runtime/util/error.ts b/xmcl-runtime/util/error.ts index eeb88c619..bb20a774b 100644 --- a/xmcl-runtime/util/error.ts +++ b/xmcl-runtime/util/error.ts @@ -1,11 +1,11 @@ -import { HTTPException } from '@xmcl/runtime-api' +import { NetworkErrorCode, NetworkException } from '@xmcl/runtime-api' import { Dispatcher, errors } from 'undici' export interface SystemError extends Error { /** * Please see `constants.errno` in `os` module */ - errno: number + errno: number | string code: string syscall?: string path?: string @@ -29,10 +29,6 @@ export class AnyError extends Error { } } -export interface SystemErrorWithSyscall extends SystemError { - syscall: string -} - export function isSystemError(e: any): e is SystemError { if (typeof e.errno === 'number' && typeof e.code === 'string' && e instanceof Error) { return true @@ -40,40 +36,87 @@ export function isSystemError(e: any): e is SystemError { return false } -export async function serializeError(e: unknown): Promise { - if ((e instanceof AggregateError) || - // @ts-ignore - (e.name === 'AggregateError' && e.errors instanceof Array)) { - e = (e as any).errors +/** + * Convert some common error to exception. + * @returns The exception or `undefined` if the error is not recognized. + */ +export async function getNormalizeException(e: unknown) { + if (isSystemError(e)) { + return getNomralizedSystemError(e) } - if (e instanceof Array) { - if (e.length !== 1) { - return Promise.all(e.map(serializeError)) - } - return serializeError(e[0]) + if (e instanceof errors.UndiciError) { + return await getNormalizedUndiciException(e) } + return undefined +} - const error: any = { +function getNomralizedSystemError(e: SystemError) { + if ((e.errno === 'ENOTFOUND' || e.code === 'ENOTFOUND') && e.syscall === 'getaddrinfo') { + throw new NetworkException({ + type: 'networkException', + code: NetworkErrorCode.DNS_NOTFOUND, + }) + } + if (e.code === 'ECONNRESET') { + throw new NetworkException({ + type: 'networkException', + code: NetworkErrorCode.CONNECTION_RESET, + }) } +} - const serializeUndiciError = async (e: errors.UndiciError) => { - const options: Dispatcher.DispatchOptions | undefined = (e as any).options - let body = '' as string | object - if (e instanceof errors.ResponseStatusCodeError) { - body = e.body || '' - } - return new HTTPException({ +async function getNormalizedUndiciException(e: errors.UndiciError) { + const options: Dispatcher.DispatchOptions | undefined = (e as any).options + let body = '' as string | object + if (e instanceof errors.ResponseStatusCodeError) { + body = e.body || '' + } + let code: NetworkErrorCode | undefined + if (e.code === 'UND_ERR_CONNECT_TIMEOUT') { + code = NetworkErrorCode.CONNECTION_TIMED_OUT + } else if (e.code === 'UND_ERR_SOCKET') { + code = NetworkErrorCode.SOCKET_NOT_CONNECTED + } else if (e.code === 'UND_ERR_HEADERS_TIMEOUT') { + code = NetworkErrorCode.TIMED_OUT + } else if (e.code === 'UND_ERR_BODY_TIMEOUT') { + code = NetworkErrorCode.TIMED_OUT + } else if (e.code === 'UND_ERR_RESPONSE_STATUS_CODE') { + code = NetworkErrorCode.HTTP_STATUS + } + return code === NetworkErrorCode.HTTP_STATUS + ? new NetworkException({ type: 'httpException', - code: e.code, + code, method: options?.method || '', url: (e as any).url ?? (options ? new URL(options?.path, options.origin as any).toString() : ''), statusCode: e instanceof errors.ResponseStatusCodeError ? e.statusCode : 0, body, }) + : code + ? new NetworkException({ + type: 'networkException', + code, + }) + : undefined +} + +/** + * Convert the error to plain object to be transferred to the renderer process + */ +export async function getSerializedError(e: unknown, context: object): Promise { + if ((e instanceof AggregateError) || + // @ts-ignore + (e.name === 'AggregateError' && e.errors instanceof Array)) { + e = (e as any).errors + } + if (e instanceof Array) { + if (e.length !== 1) { + return Promise.all(e.map(v => getSerializedError(v, context))) + } + return getSerializedError(e[0], context) } - if (e instanceof errors.UndiciError) { - e = await serializeUndiciError(e) + const error: any = { } if (e instanceof Error) { @@ -89,7 +132,6 @@ export async function serializeError(e: unknown): Promise { if (error) { error.message = error.toString() } - error.exception = { type: 'GeneralException' } } return error } diff --git a/xmcl-runtime/util/fs.ts b/xmcl-runtime/util/fs.ts index 1b64a6c74..2682ac549 100644 --- a/xmcl-runtime/util/fs.ts +++ b/xmcl-runtime/util/fs.ts @@ -252,8 +252,8 @@ export const ENOENT_ERROR = 'ENOENT' */ export const EPERM_ERROR = 'EPERM' -function handleOnlyNotFound(e: unknown) { - if (isSystemError(e) && e.code === 'ENOENT') { +export function handleOnlyNotFound(e: unknown) { + if (isSystemError(e) && e.code === ENOENT_ERROR) { return undefined } throw e diff --git a/xmcl-runtime/version/VersionService.ts b/xmcl-runtime/version/VersionService.ts index db832aac9..28e1a695e 100644 --- a/xmcl-runtime/version/VersionService.ts +++ b/xmcl-runtime/version/VersionService.ts @@ -1,5 +1,5 @@ import { LibraryInfo, ResolvedVersion, Version, VersionParseError } from '@xmcl/core' -import { VersionService as IVersionService, VersionHeader, LocalVersions, MutableState, ResolvedServerVersion, VersionServiceKey, filterForgeVersion, filterOptifineVersion, findLabyModVersion, findNeoForgedVersion, isFabricLoaderLibrary, isForgeLibrary, isOptifineLibrary, isQuiltLibrary, getResolvedVersionHeader } from '@xmcl/runtime-api' +import { VersionService as IVersionService, VersionHeader, LocalVersions, SharedState, ResolvedServerVersion, VersionServiceKey, filterForgeVersion, filterOptifineVersion, findLabyModVersion, findNeoForgedVersion, isFabricLoaderLibrary, isForgeLibrary, isOptifineLibrary, isQuiltLibrary, getResolvedVersionHeader } from '@xmcl/runtime-api' import { task } from '@xmcl/task' import { FSWatcher } from 'fs' import { ensureDir, readFile, readdir, rm } from 'fs-extra' @@ -84,7 +84,7 @@ export class VersionService extends StatefulService implements IV this.resolvers.push(resolver) } - async getLocalVersions(): Promise> { + async getLocalVersions(): Promise> { return this.state }