Even more Refactoring

Diving a bit in some http request and startic to replace old promise style + request package by node-fetch
This commit is contained in:
Shadowner 2023-03-09 01:53:11 +01:00
parent 40d0a1cdca
commit cb62004107
24 changed files with 2552 additions and 183 deletions

View File

@ -24,6 +24,10 @@
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.8", "@electron/remote": "^2.0.8",
"@types/adm-zip": "^0.5.0",
"@types/ejs": "^3.1.2",
"@types/node": "^18.14.6",
"@types/request": "^2.48.8",
"adm-zip": "^0.5.9", "adm-zip": "^0.5.9",
"async": "^3.2.4", "async": "^3.2.4",
"discord-rpc-patch": "^4.0.1", "discord-rpc-patch": "^4.0.1",
@ -36,6 +40,7 @@
"helios-core": "~0.1.2", "helios-core": "~0.1.2",
"jquery": "^3.6.1", "jquery": "^3.6.1",
"node-disk-info": "^1.3.0", "node-disk-info": "^1.3.0",
"node-fetch": "^3.3.0",
"node-stream-zip": "^1.15.0", "node-stream-zip": "^1.15.0",
"request": "^2.88.2", "request": "^2.88.2",
"semver": "^7.3.8", "semver": "^7.3.8",

29
src/MicrosoftType.ts Normal file
View File

@ -0,0 +1,29 @@
import { ConfigManager } from './manager/ConfigManager';
// NOTE FOR THIRD-PARTY
// REPLACE THIS CLIENT ID WITH YOUR APPLICATION ID.
// SEE https://github.com/dscalzi/HeliosLauncher/blob/master/docs/MicrosoftAuth.md
exports.AZURE_CLIENT_ID = ConfigManager.azureClientId;
// SEE NOTE ABOVE.
// Opcodes
export enum MSFT_OPCODE {
OPEN_LOGIN = 'MSFT_AUTH_OPEN_LOGIN',
OPEN_LOGOUT = 'MSFT_AUTH_OPEN_LOGOUT',
REPLY_LOGIN = 'MSFT_AUTH_REPLY_LOGIN',
REPLY_LOGOUT = 'MSFT_AUTH_REPLY_LOGOUT'
}
// Reply types for REPLY opcode.
export enum MSFT_REPLY_TYPE {
SUCCESS = 'MSFT_AUTH_REPLY_SUCCESS',
ERROR = 'MSFT_AUTH_REPLY_ERROR'
}
// Error types for ERROR reply.
export enum MSFT_ERROR {
ALREADY_OPEN = 'MSFT_AUTH_ERR_ALREADY_OPEN',
NOT_FINISHED = 'MSFT_AUTH_ERR_NOT_FINISHED'
}
export enum SHELL_OPCODE {
TRASH_ITEM = 'TRASH_ITEM'
}

98
src/dto/Minecraft.ts Normal file
View File

@ -0,0 +1,98 @@
export type MinecraftGameManifest = {
latest: {
release: string,
snapshot: string
},
versions: {
id: string,
type: string,
url: string,
time: string,
releaseTime: string
}[]
}
export type MinecraftGameVersionManifest = {
arguments: {
game: (ArgumentRule | string)[],
jvm: (ArgumentRule | string)[]
},
minimumLauncherVersion: number,
releaseTime: Date,
time: Date,
//Could be more precise I think
type: string
assets: string,
complianceLevel: number,
id: string,
assetIndex:
{
id: string,
sha1: string,
size: number,
totalSize: number,
url: string
},
downloads:
{
client: MinecraftFileInfo
client_mappings: MinecraftFileInfo
server: MinecraftFileInfo,
server_mappings: MinecraftFileInfo
},
javaVersion: { component: string, majorVersion: number },
logging: {
client:
{
argument: string,
file: MinecraftFileInfo,
type: string
}
},
mainClass: string,
libraries: MinecraftLibrairie[]
}
export type MinecraftLibrairie = {
downloads: {
artifact: MinecraftLibrairieFile,
classifiers: Record<string, MinecraftLibrairieFile>
},
extract?: {
exclude: string[]
},
name: string,
natives: Record<string, string>,
rules: MinecraftRule
}
export type MinecraftLibrairieFile = Omit<MinecraftFileInfo, 'id'> & {
path: string,
}
export type MinecraftFileInfo = {
id?: string,
sha1: string,
size: number,
url: string,
}
export type MinecraftRule = {
action: string,
features: Record<string, boolean>
os: {
name: string,
version?: string
}
}
export type ArgumentRule = {
rules: MinecraftRule[],
value: string[],
}
export type MinecraftAssetJson = {
objects: Record<string, { hash: string, size: number }>
}

49
src/lang/en_US.json Normal file
View File

@ -0,0 +1,49 @@
{
"html": {
"avatarOverlay": "Edit"
},
"js": {
"login": {
"error": {
"invalidValue": "* Invalid Value",
"requiredValue": "* Required",
"userMigrated": {
"title": "Error During Login:<br>Invalid Credentials",
"desc": "You've attempted to login with a migrated account. Try again using the account email as the username."
},
"invalidCredentials": {
"title": "Error During Login:<br>Invalid Credentials",
"desc": "The email or password you've entered is incorrect. Please try again."
},
"rateLimit": {
"title": "Error During Login:<br>Too Many Attempts",
"desc": "There have been too many login attempts with this account recently. Please try again later."
},
"noInternet": {
"title": "Error During Login:<br>No Internet Connection",
"desc": "You must be connected to the internet in order to login. Please connect and try again."
},
"authDown": {
"title": "Error During Login:<br>Authentication Server Offline",
"desc": "Mojang's authentication server is currently offline or unreachable. Please wait a bit and try again. You can check the status of the server on <a href=\"https://help.mojang.com/\">Mojang's help portal</a>."
},
"notPaid": {
"title": "Error During Login:<br>Game Not Purchased",
"desc": "The account you are trying to login with has not purchased a copy of Minecraft.<br>You may purchase a copy on <a href=\"https://minecraft.net/\">Minecraft.net</a>"
},
"unknown": {
"title": "Error During Login:<br>Unknown Error"
}
},
"login": "LOGIN",
"loggingIn": "LOGGING IN",
"success": "SUCCESS",
"tryAgain": "Try Again"
},
"landing": {
"launch": {
"pleaseWait": "Please wait.."
}
}
}
}

View File

@ -0,0 +1,343 @@
import remoteMain from "@electron/remote/main";
import { app, ipcMain, Menu, MenuItem, shell } from "electron";
import { autoUpdater } from "electron-updater";
import { join } from "path";
import { prerelease } from "semver";
import { DevUtil } from "./util/DevUtil";
import { MSFT_ERROR, MSFT_OPCODE, MSFT_REPLY_TYPE, SHELL_OPCODE } from './MicrosoftType';
import { BrowserWindow } from "@electron/remote";
import { readdirSync } from "fs-extra";
import { pathToFileURL } from "url";
import { data } from "ejs-electron";
import { ConfigManager } from "./manager/ConfigManager";
remoteMain.initialize();
// Setup auto updater.
function initAutoUpdater(event, data) {
if (data) {
autoUpdater.allowPrerelease = true
} else {
// Defaults to true if application version contains prerelease components (e.g. 0.12.1-alpha.1)
// autoUpdater.allowPrerelease = true
}
if (DevUtil.IsDev) {
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.updateConfigPath = join(__dirname, 'dev-app-update.yml')
}
if (process.platform === 'darwin') {
autoUpdater.autoDownload = false
}
autoUpdater.on('update-available', (info) => {
event.sender.send('autoUpdateNotification', 'update-available', info)
})
autoUpdater.on('update-downloaded', (info) => {
event.sender.send('autoUpdateNotification', 'update-downloaded', info)
})
autoUpdater.on('update-not-available', (info) => {
event.sender.send('autoUpdateNotification', 'update-not-available', info)
})
autoUpdater.on('checking-for-update', () => {
event.sender.send('autoUpdateNotification', 'checking-for-update')
})
autoUpdater.on('error', (err) => {
event.sender.send('autoUpdateNotification', 'realerror', err)
})
}
// Open channel to listen for update actions.
ipcMain.on('autoUpdateAction', (event, arg, data) => {
switch (arg) {
case 'initAutoUpdater':
console.log('Initializing auto updater.')
initAutoUpdater(event, data)
event.sender.send('autoUpdateNotification', 'ready')
break
case 'checkForUpdate':
autoUpdater.checkForUpdates()
.catch(err => {
event.sender.send('autoUpdateNotification', 'realerror', err)
})
break
case 'allowPrereleaseChange':
if (!data) {
const preRelComp = prerelease(app.getVersion())
if (preRelComp != null && preRelComp.length > 0) {
autoUpdater.allowPrerelease = true
} else {
autoUpdater.allowPrerelease = data
}
} else {
autoUpdater.allowPrerelease = data
}
break
case 'installUpdateNow':
autoUpdater.quitAndInstall()
break
default:
console.log('Unknown argument', arg)
break
}
});
// Redirect distribution index event from preloader to renderer.
ipcMain.on('distributionIndexDone', (event, res) => {
event.sender.send('distributionIndexDone', res)
})
// Handle trash item.
ipcMain.handle(SHELL_OPCODE.TRASH_ITEM, async (event, ...args) => {
try {
await shell.trashItem(args[0])
return {
result: true
}
} catch (error) {
return {
result: false,
error: error
}
}
})
const REDIRECT_URI_PREFIX = 'https://login.microsoftonline.com/common/oauth2/nativeclient?'
// Microsoft Auth Login
let msftAuthWindow
let msftAuthSuccess
let msftAuthViewSuccess
let msftAuthViewOnClose
ipcMain.on(MSFT_OPCODE.OPEN_LOGIN, (ipcEvent, ...arguments_) => {
if (msftAuthWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN, msftAuthViewOnClose)
return
}
msftAuthSuccess = false
msftAuthViewSuccess = arguments_[0]
msftAuthViewOnClose = arguments_[1]
msftAuthWindow = new BrowserWindow({
title: 'Microsoft Login',
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftAuthWindow.on('closed', () => {
msftAuthWindow = undefined
})
msftAuthWindow.on('close', () => {
if (!msftAuthSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED, msftAuthViewOnClose)
}
})
msftAuthWindow.webContents.on('did-navigate', (_, uri) => {
if (uri.startsWith(REDIRECT_URI_PREFIX)) {
let queries = uri.substring(REDIRECT_URI_PREFIX.length).split('#', 1).toString().split('&')
let queryMap = {}
queries.forEach(query => {
const [name, value] = query.split('=')
queryMap[name] = decodeURI(value)
})
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGIN, MSFT_REPLY_TYPE.SUCCESS, queryMap, msftAuthViewSuccess)
msftAuthSuccess = true
msftAuthWindow.close()
msftAuthWindow = null
}
})
msftAuthWindow.removeMenu()
msftAuthWindow.loadURL(`https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?prompt=select_account&client_id=${ConfigManager.azureClientId}&response_type=code&scope=XboxLive.signin%20offline_access&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient`)
})
// Microsoft Auth Logout
let msftLogoutWindow
let msftLogoutSuccess
let msftLogoutSuccessSent
ipcMain.on(MSFT_OPCODE.OPEN_LOGOUT, (ipcEvent, uuid, isLastAccount) => {
if (msftLogoutWindow) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.ALREADY_OPEN)
return
}
msftLogoutSuccess = false
msftLogoutSuccessSent = false
msftLogoutWindow = new BrowserWindow({
title: 'Microsoft Logout',
backgroundColor: '#222222',
width: 520,
height: 600,
frame: true,
icon: getPlatformIcon('SealCircle')
})
msftLogoutWindow.on('closed', () => {
msftLogoutWindow = undefined
})
msftLogoutWindow.on('close', () => {
if (!msftLogoutSuccess) {
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.ERROR, MSFT_ERROR.NOT_FINISHED)
} else if (!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
})
msftLogoutWindow.webContents.on('did-navigate', (_, uri) => {
if (uri.startsWith('https://login.microsoftonline.com/common/oauth2/v2.0/logoutsession')) {
msftLogoutSuccess = true
setTimeout(() => {
if (!msftLogoutSuccessSent) {
msftLogoutSuccessSent = true
ipcEvent.reply(MSFT_OPCODE.REPLY_LOGOUT, MSFT_REPLY_TYPE.SUCCESS, uuid, isLastAccount)
}
if (msftLogoutWindow) {
msftLogoutWindow.close()
msftLogoutWindow = null
}
}, 5000)
}
})
msftLogoutWindow.removeMenu()
msftLogoutWindow.loadURL('https://login.microsoftonline.com/common/oauth2/v2.0/logout')
})
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win
function createWindow() {
win = new BrowserWindow({
width: 980,
height: 552,
icon: getPlatformIcon('SealCircle'),
frame: false,
webPreferences: {
preload: join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
nodeIntegration: true,
contextIsolation: false
},
backgroundColor: '#171614'
})
remoteMain.enable(win.webContents)
data('bkid', Math.floor((Math.random() * readdirSync(join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
win.loadURL(pathToFileURL(join(__dirname, 'app', 'app.ejs')).toString())
/*win.once('ready-to-show', () => {
win.show()
})*/
win.removeMenu()
win.resizable = true
win.on('closed', () => {
win = null
})
}
function createMenu() {
if (process.platform === 'darwin') {
// Extend default included application menu to continue support for quit keyboard shortcut
let applicationSubMenu = new MenuItem({
label: 'Application',
submenu: [{
label: 'About Application',
}, {
type: 'separator'
}, {
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit()
}
}]
})
// New edit menu adds support for text-editing keyboard shortcuts
let editSubMenu = new MenuItem({
label: "Edit",
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
}, {
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
}, {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
}]
})
// Bundle submenus into a single template and build a menu object with it
let menuTemplate = [applicationSubMenu, editSubMenu]
let menuObject = Menu.buildFromTemplate(menuTemplate)
// Assign it to the application
Menu.setApplicationMenu(menuObject)
}
}
function getPlatformIcon(filename) {
let ext
switch (process.platform) {
case 'win32':
ext = 'ico'
break
case 'darwin':
case 'linux':
default:
ext = 'png'
break
}
return join(__dirname, 'app', 'assets', 'images', `${filename}.${ext}`)
}
app.on('ready', createWindow)
app.on('ready', createMenu)
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})

View File

@ -3,7 +3,7 @@ import { LoggerUtil } from "helios-core/.";
import { existsSync, readFileSync, writeFileSync } from "fs-extra"; import { existsSync, readFileSync, writeFileSync } from "fs-extra";
import os from 'os'; import os from 'os';
import { join } from 'path'; import { join } from 'path';
import { mcVersionAtLeast } from "../util/MinecraftUtil"; import { MinecraftUtil } from "../util/MinecraftUtil";
import { resolveMaxRAM, resolveMinRAM } from "../util/System"; import { resolveMaxRAM, resolveMinRAM } from "../util/System";
const logger = LoggerUtil.getLogger("ConfigManager"); const logger = LoggerUtil.getLogger("ConfigManager");
@ -49,7 +49,7 @@ export class ConfigManager {
private static launcherDir = process.env.CONFIG_DIRECT_PATH ?? require("@electron/remote").app.getPath("userData"); private static launcherDir = process.env.CONFIG_DIRECT_PATH ?? require("@electron/remote").app.getPath("userData");
public static readonly DistributionURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'; public static readonly DistributionURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json';
public static readonly launcherName = 'Helios-Launcher' public static readonly launcherName = 'Helios-Launcher'
public static readonly azureClientId = '1ce6e35a-126f-48fd-97fb-54d143ac6d45'
/** /**
* Three types of values: * Three types of values:
* Static = Explicitly declared. * Static = Explicitly declared.
@ -771,7 +771,7 @@ export class ConfigManager {
}; };
private static defaultJavaConfig(mcVersion: string) { private static defaultJavaConfig(mcVersion: string) {
if (mcVersionAtLeast("1.17", mcVersion)) { if (MinecraftUtil.mcVersionAtLeast("1.17", mcVersion)) {
return this.defaultJavaConfig117(); return this.defaultJavaConfig117();
} else { } else {
return this.defaultJavaConfigBelow117(); return this.defaultJavaConfigBelow117();

View File

@ -1,10 +1,10 @@
import { readFile, writeFile } from "fs-extra" import { readFile, writeFile } from "fs-extra"
import { LoggerUtil } from "helios-core/."; import { LoggerUtil } from "helios-core/.";
import { DevUtil } from '../util/isDev'; import { DevUtil } from '../util/DevUtil';
import request from "request";
import { ConfigManager } from "./ConfigManager"; import { ConfigManager } from "./ConfigManager";
import { join } from 'path'; import { join } from 'path';
import { DistroIndex } from '../models/DistroIndex'; import { DistroIndex, IDistroIndex } from '../models/DistroIndex';
import fetch from 'node-fetch';
const logger = LoggerUtil.getLogger('DistroManager') const logger = LoggerUtil.getLogger('DistroManager')
export enum DistroTypes { export enum DistroTypes {
@ -20,69 +20,40 @@ export enum DistroTypes {
export class DistroManager { export class DistroManager {
public distribution!: DistroIndex; public static distribution?: DistroIndex;
private readonly DISTRO_PATH = join(ConfigManager.getLauncherDirectory(), 'distribution.json') private static readonly DISTRO_PATH = join(ConfigManager.getLauncherDirectory(), 'distribution.json')
private readonly DEV_PATH = join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json') private static readonly DEV_PATH = join(ConfigManager.getLauncherDirectory(), 'dev_distribution.json')
/** /**
* @returns {Promise.<DistroIndex>} * @returns {Promise.<DistroIndex>}
*/ */
public pullRemote() { public static async pullRemote() {
if (DevUtil.IsDev) { if (DevUtil.IsDev) return this.pullLocal();
return exports.pullLocal()
}
return new Promise((resolve, reject) => {
const opts = {
url: ConfigManager.DistributionURL,
timeout: 2500
}
const distroDest = join(ConfigManager.getLauncherDirectory(), 'distribution.json')
request(opts, (error: Error, _resp: any, body: string) => {
if (!error) {
try { const distroDest = join(ConfigManager.getLauncherDirectory(), 'distribution.json')
this.distribution = DistroIndex.fromJSON(JSON.parse(body)) const response = await fetch(ConfigManager.DistributionURL, { signal: AbortSignal.timeout(2500) });
} catch (e) {
reject(e)
return
}
writeFile(distroDest, body, 'utf-8', (err) => { this.distribution = DistroIndex.fromJSON(await response.json() as IDistroIndex);
if (!err) {
resolve(this.distribution) writeFile(distroDest, JSON.stringify(this.distribution), 'utf-8').catch(e => {
return logger.warn("Failed to save local distribution.json")
} else { logger.warn(e);
reject(err) });
return
} return this.distribution;
})
} else {
reject(error)
return
}
})
})
} }
/** /**
* @returns {Promise.<DistroIndex>} * @returns {Promise.<DistroIndex>}
*/ */
public pullLocal() { public static async pullLocal() {
return new Promise((resolve, reject) => { const file = await readFile(DevUtil.IsDev ? this.DEV_PATH : this.DISTRO_PATH, 'utf-8');
readFile(DevUtil.IsDev ? this.DEV_PATH : this.DISTRO_PATH, 'utf-8', (err, d) => { this.distribution = DistroIndex.fromJSON(JSON.parse(file));
if (!err) { return this.distribution;
this.distribution = DistroIndex.fromJSON(JSON.parse(d))
resolve(this.distribution)
return
} else {
reject(err)
return
}
})
})
} }
public setDevMode(value: boolean) { public static setDevMode(value: boolean) {
if (value) { if (value) {
logger.info('Developer mode enabled.') logger.info('Developer mode enabled.')
logger.info('If you don\'t know what that means, revert immediately.') logger.info('If you don\'t know what that means, revert immediately.')

View File

@ -2,6 +2,14 @@
* Represents the download information * Represents the download information
* for a specific module. * for a specific module.
*/ */
export interface IArtifact {
MD5: string,
size: string,
url: string,
path: string,
}
export class Artifact { export class Artifact {
/** /**
@ -11,12 +19,7 @@ export class Artifact {
* *
* @returns {Artifact} The parsed Artifact. * @returns {Artifact} The parsed Artifact.
*/ */
public static fromJSON(json: { public static fromJSON(json: IArtifact) {
MD5: string,
size: string,
url: string,
path: string,
}) {
return new Artifact(json.MD5, json.size, json.url, json.path) return new Artifact(json.MD5, json.size, json.url, json.path)
} }
@ -27,6 +30,8 @@ export class Artifact {
public path: string, public path: string,
) { } ) { }
//TODO: Remove those property
/** /**
* Get the MD5 hash of the artifact. This value may * Get the MD5 hash of the artifact. This value may
* be undefined for artifacts which are not to be * be undefined for artifacts which are not to be

View File

@ -1,16 +1,25 @@
/** Class representing a base asset. */ /** Class representing a base asset. */
export interface IAsset {
id: string,
hash: string,
size: number,
from: string,
to: string
}
export class Asset { export class Asset {
/** /**
* Create an asset. * Create an asset.
* *
* @param {any} id The id of the asset. * @param {string} id The id of the asset.
* @param {string} hash The hash value of the asset. * @param {string} hash The hash value of the asset.
* @param {number} size The size in bytes of the asset. * @param {number} size The size in bytes of the asset.
* @param {string} from The url where the asset can be found. * @param {string} from The url where the asset can be found.
* @param {string} to The absolute local file path of the asset. * @param {string} to The absolute local file path of the asset.
*/ */
constructor( constructor(
public id: any, public id: string,
public hash: string, public hash: string,
public size: number, public size: number,
public from: string, public from: string,

View File

@ -16,7 +16,6 @@ export class DLTracker {
public dlqueue: Asset[], public dlqueue: Asset[],
public dlsize: number, public dlsize: number,
public callback?: (asset: Asset) => void) { public callback?: (asset: Asset) => void) {
} }
} }

View File

@ -1,4 +1,5 @@
import { Asset } from "./Asset" import { Asset } from "./Asset"
import { DistroTypes } from '../manager/DistroManager';
export class DistroAsset extends Asset { export class DistroAsset extends Asset {
@ -14,8 +15,12 @@ export class DistroAsset extends Asset {
* @param {string} to The absolute local file path of the asset. * @param {string} to The absolute local file path of the asset.
* @param {string} type The the module type. * @param {string} type The the module type.
*/ */
constructor(id: any, hash: string, size: number, from: string, to: string, constructor(public id: any,
public type hash: string,
size: number,
from: string,
to: string,
public type: DistroTypes
) { ) {
super(id, hash, size, from, to) super(id, hash, size, from, to)
this.type = type this.type = type

View File

@ -1,6 +1,6 @@
import { IServer, Server } from './Server'; import { IServer, Server } from './Server';
interface IDistroIndex { export interface IDistroIndex {
version: string, version: string,
rss: string, rss: string,
servers: IServer[] servers: IServer[]
@ -36,6 +36,10 @@ export class DistroIndex {
this.resolveServers(servers); this.resolveServers(servers);
} }
public getServer(id: string) {
return this.servers.find(server => server.id === id);
}
private resolveServers(serverJsons: IServer[]) { private resolveServers(serverJsons: IServer[]) {
const servers: Server[] = [] const servers: Server[] = []
for (let serverJson of serverJsons) { for (let serverJson of serverJsons) {

View File

@ -1,11 +1,27 @@
import { LoggerUtil } from 'helios-core/.'; import { LoggerUtil } from 'helios-core/.';
import { ConfigManager } from '../manager/ConfigManager'; import { ConfigManager } from '../manager/ConfigManager';
import { Artifact } from './Artifact'; import { Artifact, IArtifact } from './Artifact';
import { join } from 'path'; import { join } from 'path';
import { DistroTypes } from '../manager/DistroManager'; import { DistroTypes } from '../manager/DistroManager';
import { Required, IRequired } from './Required';
const logger = LoggerUtil.getLogger('Module') const logger = LoggerUtil.getLogger('Module')
export interface IModule {
artifactExt: string;
artifactClassifier?: string;
artifactVersion: string;
artifactID: string;
artifactGroup: string;
subModules: IModule[];
required: IRequired;
artifact: IArtifact;
id: string,
name: string,
type: DistroTypes,
classpath: boolean
}
export class Module { export class Module {
/** /**
@ -16,7 +32,7 @@ export class Module {
* *
* @returns {Module} The parsed Module. * @returns {Module} The parsed Module.
*/ */
public static fromJSON(json, serverid) { public static fromJSON(json: IModule, serverid: string) {
return new Module(json.id, json.name, json.type, json.classpath, json.required, json.artifact, json.subModules, serverid) return new Module(json.id, json.name, json.type, json.classpath, json.required, json.artifact, json.subModules, serverid)
} }
@ -50,7 +66,8 @@ export class Module {
public artifactGroup: string; public artifactGroup: string;
public subModules: Module[] = [] public subModules: Module[] = []
public required: Required;
public artifact: Artifact
/** /**
* @returns {string} The identifier without he version or extension. * @returns {string} The identifier without he version or extension.
*/ */
@ -70,17 +87,22 @@ export class Module {
} }
constructor(public identifier: string,
constructor(
public identifier: string,
public name: string, public name: string,
public type: DistroTypes, public type: DistroTypes,
public classpath: boolean = true, public classpath: boolean = true,
public required = Required.fromJSON(required), required: IRequired,
public artifact = Artifact.fromJSON(artifact), artifact: IArtifact,
subModules, subModules: IModule[],
serverid serverid: string
) { ) {
this.required = Required.fromJSON(required);
this.artifact = Artifact.fromJSON(artifact);
this.resolveMetaData() this.resolveMetaData()
this.resolveArtifactPath(artifact.path, serverid) this.resolveArtifactPath(this.artifact.path, serverid)
this.resolveSubModules(subModules, serverid) this.resolveSubModules(subModules, serverid)
} }

View File

@ -17,7 +17,7 @@ export class Required {
* *
* @returns {Required} The parsed Required object. * @returns {Required} The parsed Required object.
*/ */
static fromJSON(json: IRequired) { public static fromJSON(json: IRequired) {
if (json == null) { if (json == null) {
return new Required(true, true) return new Required(true, true)
} else { } else {

26
src/scripts/LangLoader.ts Normal file
View File

@ -0,0 +1,26 @@
import { readFileSync } from 'fs-extra';
import { join } from 'path';
import type LangType from "../lang/en_US.json";
export class LangLoader {
public static lang?: typeof LangType;
public static loadLanguage(langId: string) {
this.lang = readFileSync(join(__dirname, '..', 'lang', `${langId}.json`)).toJSON() as any || undefined;
}
public static query(langId: string) {
if (!this.lang) return "";
let query = langId.split('.')
let res = this.lang
for (let q of query) {
res = res[q]
}
return res === this.lang ? {} : res
}
public static queryJS(id) {
return this.query(`js.${id}`)
}
}

70
src/scripts/Preloading.ts Normal file
View File

@ -0,0 +1,70 @@
import { ipcRenderer } from "electron";
import { remove } from "fs-extra";
import { LoggerUtil } from "helios-core/.";
import { ConfigManager } from "../manager/ConfigManager";
import { DistroManager } from "../manager/DistroManager";
import { join } from 'path';
import os from 'os';
import { LangLoader } from "./LangLoader";
const logger = LoggerUtil.getLogger('Preloader')
logger.info('Loading..')
// Load ConfigManager
ConfigManager.load()
// Load Strings
LangLoader.loadLanguage('en_US')
function onDistroLoad(data) {
if (data != null) {
// Resolve the selected server if its value has yet to be set.
if (ConfigManager.getSelectedServer() == null || data.getServer(ConfigManager.getSelectedServer()) == null) {
logger.info('Determining default selected server..')
ConfigManager.selectedServer = data.getMainServer().getID();
ConfigManager.save()
}
}
ipcRenderer.send('distributionIndexDone', data != null)
}
// Ensure Distribution is downloaded and cached.
DistroManager.pullRemote().then((data) => {
logger.info('Loaded distribution index.')
onDistroLoad(data)
}).catch((err) => {
logger.info('Failed to load distribution index.')
logger.error(err)
logger.info('Attempting to load an older version of the distribution index.')
// Try getting a local copy, better than nothing.
DistroManager.pullLocal().then((data) => {
logger.info('Successfully loaded an older version of the distribution index.')
onDistroLoad(data)
}).catch((err) => {
logger.info('Failed to load an older version of the distribution index.')
logger.info('Application cannot run.')
logger.error(err)
onDistroLoad(null)
})
})
// Clean up temp dir incase previous launches ended unexpectedly.
remove(join(os.tmpdir(), ConfigManager.tempNativeFolder), (err) => {
if (err) {
logger.warn('Error while cleaning natives directory', err)
} else {
logger.info('Cleaned natives directory.')
}
})

View File

@ -0,0 +1,9 @@
/**
* Script for welcome.ejs
*/
document.getElementById('welcomeButton')?.addEventListener('click', e => {
loginOptionsCancelEnabled(false) // False by default, be explicit.
loginOptionsViewOnLoginSuccess = VIEWS.landing
loginOptionsViewOnLoginCancel = VIEWS.loginOptions
switchView(VIEWS.welcome, VIEWS.loginOptions)
})

873
src/services/AssetGuard.ts Normal file
View File

@ -0,0 +1,873 @@
import { LoggerUtil } from "helios-core/.";
import * as EventEmitter from 'events';
import { DLTracker } from '../models/DLTracker';
import { createHash } from "crypto";
import { existsSync, readFileSync, readFile, ensureDirSync, writeFileSync, createWriteStream, unlink, createReadStream, remove, ensureDir, writeFile, pathExists, pathExistsSync } from 'fs-extra';
import AdmZip from 'adm-zip';
import { DevUtil } from '../util/DevUtil';
import { join } from 'path';
import { spawn } from 'child_process';
import request from 'request';
import asyncModule from "async";
import { Asset } from "../models/Asset";
import { Library } from '../models/Library';
import { DistroAsset } from '../models/DistroAsset';
import { Module } from '../models/Module';
import { Artifact } from '../models/Artifact';
import { Server } from '../models/Server';
import { DistroManager, DistroTypes } from '../manager/DistroManager';
import { MinecraftUtil } from "../util/MinecraftUtil";
import { createGunzip } from "zlib";
import { extract } from "tar-fs";
import { JavaGuard } from './JavaGuard';
import { StreamZipAsync } from "node-stream-zip";
import { ConfigManager } from "../manager/ConfigManager";
import fetch from 'node-fetch';
import { MinecraftGameManifest, MinecraftGameVersionManifest, MinecraftAssetJson } from '../dto/Minecraft';
const logger = LoggerUtil.getLogger('AssetGuard');
export class AssetGuard extends EventEmitter {
// Static Utility Functions
// #region
// Static Hash Validation Functions
// #region
/**
* Calculates the hash for a file using the specified algorithm.
*
* @param {Buffer} buffer The buffer containing file data.
* @param {string} algo The hash algorithm.
* @returns {string} The calculated hash in hex.
*/
private static calculateHash(buffer: Buffer, algo: string): string {
return createHash(algo).update(buffer).digest('hex')
}
/**
* Used to parse a checksums file. This is specifically designed for
* the checksums.sha1 files found inside the forge scala dependencies.
*
* @param {string} content The string content of the checksums file.
* @returns { Record<string, string>} An object with keys being the file names, and values being the hashes.
*/
private static parseChecksumsFile(content: string): Record<string, string> {
let finalContent: Record<string, string> = {}
let lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
let bits = lines[i].split(' ')
if (bits[1] == null) {
continue
}
finalContent[bits[1]] = bits[0]
}
return finalContent
}
/**
* Validate that a file exists and matches a given hash value.
*
* @param {string} filePath The path of the file to validate.
* @param {string} algo The hash algorithm to check against.
* @param {string} hash The existing hash to check against.
* @returns {boolean} True if the file exists and calculated hash matches the given hash, otherwise false.
*/
private static validateLocal(filePath: string, algo: string, hash: string): boolean {
if (existsSync(filePath)) {
//No hash provided, have to assume it's good.
if (hash == null) {
return true
}
let buf = readFileSync(filePath)
let calcdhash = AssetGuard.calculateHash(buf, algo)
return calcdhash === hash.toLowerCase()
}
return false
}
/**
* Validates a file in the style used by forge's version index.
*
* @param {string} filePath The path of the file to validate.
* @param {Array.<string>} checksums The checksums listed in the forge version index.
* @returns {boolean} True if the file exists and the hashes match, otherwise false.
*/
private static validateForgeChecksum(filePath: string, checksums: string[]): boolean {
if (existsSync(filePath)) {
if (checksums == null || checksums.length === 0) {
return true
}
let buf = readFileSync(filePath)
let calcdhash = AssetGuard.calculateHash(buf, 'sha1')
let valid = checksums.includes(calcdhash)
if (!valid && filePath.endsWith('.jar')) {
valid = AssetGuard.validateForgeJar(Buffer.from(filePath), checksums)
}
return valid
}
return false
}
/**
* Validates a forge jar file dependency who declares a checksums.sha1 file.
* This can be an expensive task as it usually requires that we calculate thousands
* of hashes.
*
* @param {Buffer} buffer The buffer of the jar file.
* @param {Array.<string>} checksums The checksums listed in the forge version index.
* @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes.
*/
private static validateForgeJar(buffer: Buffer, checksums: string[]): boolean {
// Double pass method was the quickest I found. I tried a version where we store data
// to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
const hashes: Record<string, string> = {}
let expected: Record<string, string> = {}
const zip = new AdmZip(buffer)
const zipEntries = zip.getEntries()
//First pass
for (let i = 0; i < zipEntries.length; i++) {
let entry = zipEntries[i]
if (entry.entryName === 'checksums.sha1') {
expected = AssetGuard.parseChecksumsFile(zip.readAsText(entry))
}
hashes[entry.entryName] = AssetGuard.calculateHash(entry.getData(), 'sha1')
}
if (!checksums.includes(hashes['checksums.sha1'])) {
return false
}
//Check against expected
const expectedEntries = Object.keys(expected)
for (let i = 0; i < expectedEntries.length; i++) {
if (expected[expectedEntries[i]] !== hashes[expectedEntries[i]]) {
return false
}
}
return true
}
// #endregion
// Miscellaneous Static Functions
// #region
/**
* Extracts and unpacks a file from .pack.xz format.
*
* @param {Array.<string>} filePaths The paths of the files to be extracted and unpacked.
* @returns {Promise.<void>} An empty promise to indicate the extraction has completed.
*/
private static extractPackXZ(filePaths: string[], javaExecutable: string): Promise<void> {
const extractLogger = LoggerUtil.getLogger('PackXZExtract')
extractLogger.info('Starting')
return new Promise((resolve, reject) => {
let libPath: string;
if (DevUtil.IsDev) {
libPath = join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
} else {
if (process.platform === 'darwin') {
libPath = join(process.cwd(), 'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar')
} else {
libPath = join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar')
}
}
const filePath = filePaths.join(',')
const child = spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
child.stdout.on('data', (data) => {
extractLogger.info(data.toString('utf8'))
})
child.stderr.on('data', (data) => {
extractLogger.info(data.toString('utf8'))
})
child.on('close', (code, _signal) => {
extractLogger.info('Exited with code', code)
resolve(undefined);
})
})
}
/**
* Function which finalizes the forge installation process. This creates a 'version'
* instance for forge and saves its version.json file into that instance. If that
* instance already exists, the contents of the version.json file are read and returned
* in a promise.
*
* @param {Asset} asset The Asset object representing Forge.
* @param {string} commonPath The common path for shared game files.
* @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json.
*/
private static finalizeForgeAsset(asset: Asset, commonPath: string): Promise<object> {
return new Promise((resolve, reject) => {
readFile(asset.to, (err, data) => {
const zip = new AdmZip(data)
const zipEntries = zip.getEntries()
for (let i = 0; i < zipEntries.length; i++) {
if (zipEntries[i].entryName === 'version.json') {
const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
const versionPath = join(commonPath, 'versions', forgeVersion.id)
const versionFile = join(versionPath, forgeVersion.id + '.json')
if (!existsSync(versionFile)) {
ensureDirSync(versionPath)
writeFileSync(join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
resolve(forgeVersion)
} else {
//Read the saved file to allow for user modifications.
resolve(JSON.parse(readFileSync(versionFile, 'utf-8')))
}
return
}
}
//We didn't find forge's version.json.
reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
})
})
}
// #endregion
// #endregion
public totaldlsize = 0;
public progress = 0;
public assets = new DLTracker([], 0);
public libraries = new DLTracker([], 0);
public files = new DLTracker([], 0);
public forge = new DLTracker([], 0);
public java = new DLTracker([], 0);
public extractQueue: string[] = [];
/**
* Create an instance of AssetGuard.
* On creation the object's properties are never-null default
* values. Each identifier is resolved to an empty DLTracker.
*
* @param {string} commonPath The common path for shared game files.
* @param {string} javaexec The path to a java executable which will be used
* to finalize installation.
*/
constructor(
public commonPath: string,
public javaexec: string
) {
super()
this.commonPath = commonPath
this.javaexec = javaexec
}
// Validation Functions
// #region
/**
* Loads the version data for a given minecraft version.
*
* @param {string} version The game version for which to load the index data.
* @param {boolean} force Optional. If true, the version index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<MinecraftGameVersionManifest>} Promise which resolves to the version data object.
*/
public async loadVersionData(version: string, force: boolean = false): Promise<MinecraftGameVersionManifest> {
const versionPath = join(this.commonPath, 'versions', version)
const versionFile = join(versionPath, version + '.json')
if (!existsSync(versionFile) || force) {
const url = await this.getVersionDataUrl(version)
if (!url) throw new Error("No URL");
//This download will never be tracked as it's essential and trivial.
logger.info('Preparing download of ' + version + ' assets.')
await ensureDir(versionPath);
const response = await fetch(url);
const json = await response.json() as MinecraftGameVersionManifest;
response.text().then(text => {
writeFile(versionFile, response.text());
});
return json;
}
return JSON.parse(await readFile(versionFile, 'utf-8'));
}
/**
* Parses Mojang's version manifest and retrieves the url of the version
* data index.
*
* //TODO:Get the JSON to type
*
* @param {string} version The version to lookup.
* @returns {Promise.<string>} Promise which resolves to the url of the version data index.
* If the version could not be found, resolves to null.
*/
public async getVersionDataUrl(versionId: string): Promise<null | string> {
const response = await fetch('https://launchermeta.mojang.com/mc/game/version_manifest.json');
const manifest = await response.json() as MinecraftGameManifest;
const version = manifest.versions.find(v => v.id === versionId)
return version?.url || null
}
// Asset (Category=''') Validation Functions
// #region
/**
* Public asset validation function. This function will handle the validation of assets.
* It will parse the asset index specified in the version data, analyzing each
* asset entry. In this analysis it will check to see if the local file exists and is valid.
* If not, it will be added to the download queue for the 'assets' identifier.
*
* @param {MinecraftGameVersionManifest} versionData The version data for the assets.
* @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
public async validateAssets(versionData: MinecraftGameVersionManifest, force: boolean = false): Promise<void> {
return this.assetChainIndexData(versionData, force);
}
//Chain the asset tasks to provide full async. The below functions are private.
/**
* Private function used to chain the asset validation process. This function retrieves
* the index data.
* @param {MinecraftGameVersionManifest} versionData
* @param {boolean} force
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
private async assetChainIndexData(versionData: MinecraftGameVersionManifest, force: boolean = false): Promise<void> {
//Asset index constants.
const assetIndex = versionData.assetIndex
const name = assetIndex.id + '.json'
const indexPath = join(this.commonPath, 'assets', 'indexes')
const assetIndexLoc = join(indexPath, name)
let assetJson: MinecraftAssetJson;
if (force || !pathExistsSync(assetIndexLoc)) {
logger.info('Downloading ' + versionData.id + ' asset index.')
await ensureDir(indexPath)
const response = await fetch(assetIndex.url);
assetJson = await response.json() as MinecraftAssetJson;
response.text().then(txt => {
writeFile(assetIndexLoc, txt, { encoding: 'utf8' })
});
} else {
assetJson = JSON.parse(await readFile(assetIndexLoc, 'utf-8')) as MinecraftAssetJson;
}
return this.assetChainValidateAssets(assetJson)
}
/**
* Private function used to chain the asset validation process. This function processes
* the assets and enqueues missing or invalid files.
* @param {MinecraftGameVersionManifest} versionData
* @param {boolean} force
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
private assetChainValidateAssets(indexData: MinecraftAssetJson): Promise<void> {
return new Promise((resolve, reject) => {
//Asset constants
const resourceURL = 'https://resources.download.minecraft.net/'
const localPath = join(this.commonPath, 'assets')
const objectPath = join(localPath, 'objects')
const assetDlQueue: Asset[] = []
let dlSize = 0
let acc = 0
const total = Object.keys(indexData.objects).length
//const objKeys = Object.keys(data.objects)
asyncModule.forEachOfLimit(indexData.objects, 10, (value, key, cb) => {
acc++
this.emit('progress', 'assets', acc, total)
const hash = value.hash
const assetName = join(hash.substring(0, 2), hash)
const urlName = hash.substring(0, 2) + '/' + hash
const ast = new Asset(key, hash, value.size, resourceURL + urlName, join(objectPath, assetName))
if (!AssetGuard.validateLocal(ast.to, 'sha1', ast.hash)) {
dlSize += (ast.size * 1)
assetDlQueue.push(ast)
}
cb()
}, (err) => {
this.assets = new DLTracker(assetDlQueue, dlSize)
resolve(undefined)
})
})
}
// #endregion
// Library (Category=''') Validation Functions
// #region
/**
* Public library validation function. This function will handle the validation of libraries.
* It will parse the version data, analyzing each library entry. In this analysis, it will
* check to see if the local file exists and is valid. If not, it will be added to the download
* queue for the 'libraries' identifier.
*
* @param {MinecraftGameVersionManifest} versionData The version data for the assets.
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
public validateLibraries(versionData: MinecraftGameVersionManifest): Promise<void> {
return new Promise((resolve, reject) => {
const libArr = versionData.libraries
const libPath = join(this.commonPath, 'libraries')
const libDlQueue: Library[] = []
let dlSize = 0
//Check validity of each library. If the hashs don't match, download the library.
asyncModule.eachLimit(libArr, 5, (lib, cb) => {
if (Library.validateRules(lib.rules, lib.natives)) {
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()].replace('${arch}', process.arch.replace('x', ''))]
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, join(libPath, artifact.path))
if (!AssetGuard.validateLocal(libItm.to, 'sha1', libItm.hash)) {
dlSize += (libItm.size * 1)
libDlQueue.push(libItm)
}
}
cb()
}, (err) => {
this.libraries = new DLTracker(libDlQueue, dlSize)
resolve(undefined)
})
})
}
// #endregion
// Miscellaneous (Category=files) Validation Functions
// #region
/**
* Public miscellaneous mojang file validation function. These files will be enqueued under
* the 'files' identifier.
*
* @param {MinecraftGameVersionManifest} versionData The version data for the assets.
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
public async validateMiscellaneous(versionData: MinecraftGameVersionManifest): Promise<void> {
await this.validateClient(versionData);
await this.validateLogConfig(versionData);
}
/**
* Validate client file - artifact renamed from client.jar to '{version}'.jar.
*
* @param {MinecraftGameVersionManifest} versionData The version data for the assets.
* @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {Promise.<void>} An empty promise to indicate the async processing has completed.
*/
public async validateClient(versionData: MinecraftGameVersionManifest, force: boolean = false): Promise<void> {
const clientData = versionData.downloads.client;
const version = versionData.id;
const targetPath = join(this.commonPath, 'versions', version);
const targetFile = version + '.jar';
let client = new Asset(version + ' client', clientData.sha1, clientData.size, clientData.url, join(targetPath, targetFile));
if (!AssetGuard.validateLocal(client.to, 'sha1', client.hash) || force) {
this.files.dlqueue.push(client);
this.files.dlsize += client.size * 1;
}
}
/**
* Validate log config.
*
* @param {MinecraftGameVersionManifest} versionData The version data for the assets.
* @param {boolean} force Optional. If true, the asset index will be downloaded even if it exists locally. Defaults to false.
* @returns {void} An empty promise to indicate the async processing has completed.
*/
public validateLogConfig(versionData: MinecraftGameVersionManifest): void {
const client = versionData.logging.client
const file = client.file
const targetPath = join(this.commonPath, 'assets', 'log_configs')
if (!file.id) throw new Error("No file ID");
const logConfig = new Asset(file.id, file.sha1, file.size, file.url, join(targetPath, file.id ?? ''))
if (!AssetGuard.validateLocal(logConfig.to, 'sha1', logConfig.hash)) {
this.files.dlqueue.push(logConfig)
this.files.dlsize += logConfig.size * 1
}
}
// #endregion
// Distribution (Category=forge) Validation Functions
// #region
/**
* Validate the distribution.
*
* @param {Server} server The Server to validate.
* @returns {Server} A promise which resolves to the server distribution object.
*/
public validateDistribution(server: Server): Server {
this.forge = this.parseDistroModules(server.modules, server.minecraftVersion, server.id);
return server;
}
public parseDistroModules(modules: Module[], version: string, servid: string) {
let assets: DistroAsset[] = [];
let asize = 0;
for (let module of modules) {
let modArtifact = module.artifact;
let finalPath = modArtifact.path;
let distroAsset = new DistroAsset(module.identifier, modArtifact.getHash(), Number(modArtifact.size), modArtifact.getURL(), finalPath, module.type)
const validationPath = finalPath.toLowerCase().endsWith('.pack.xz')
? finalPath.substring(0, finalPath.toLowerCase().lastIndexOf('.pack.xz'))
: finalPath
if (!AssetGuard.validateLocal(validationPath, 'MD5', distroAsset.hash)) {
asize += distroAsset.size * 1
assets.push(distroAsset)
if (validationPath !== finalPath) this.extractQueue.push(finalPath)
}
//Recursively process the submodules then combine the results.
if (module.subModules != null) {
let dltrack = this.parseDistroModules(module.subModules, version, servid)
asize += dltrack.dlsize * 1
assets = assets.concat(dltrack.dlqueue as DistroAsset[])
}
}
return new DLTracker(assets, asize)
}
/**
* Loads Forge's version.json data into memory for the specified server id.
*
* @param {Server} server The Server to load Forge data for.
* @returns {Promise.<Object>} A promise which resolves to Forge's version.json data.
*/
public async loadForgeData(server: Server): Promise<object> {
const modules = server.modules
for (let module of modules) {
const type = module.type
if (type === DistroTypes.ForgeHosted || type === DistroTypes.Forge) {
if (MinecraftUtil.isForgeGradle3(server.minecraftVersion, module.artifactVersion)) {
// Read Manifest
for (let subModule of module.subModules) {
if (subModule.type === DistroTypes.VersionManifest) {
return JSON.parse(readFileSync(subModule.artifact.getPath(), 'utf-8'))
}
}
throw new Error('No forge version manifest found!')
} else {
const modArtifact = module.artifact
const artifactPath = modArtifact.getPath()
const asset = new DistroAsset(module.identifier, modArtifact.getHash(), Number(modArtifact.size), modArtifact.getURL(), artifactPath, type)
try {
let forgeData = await AssetGuard.finalizeForgeAsset(asset, this.commonPath)
return forgeData;
} catch (err) {
throw err;
}
}
}
}
throw new Error('No forge module found!')
}
private parseForgeLibraries() {
/* TODO
* Forge asset validations are already implemented. When there's nothing much
* to work on, implement forge downloads using forge's version.json. This is to
* have the code on standby if we ever need it (since it's half implemented already).
*/
}
// #endregion
// Java (Category=''') Validation (download) Functions
// #region
private enqueueOpenJDK(dataDir: string, mcVersion: string) {
const major = MinecraftUtil.mcVersionAtLeast('1.17', mcVersion) ? '17' : '8'
JavaGuard.latestOpenJDK(major).then(verData => {
if (verData != null) {
dataDir = join(dataDir, 'runtime', 'x64')
const fDir = join(dataDir, verData.name)
//TODO : Verify it doesn't break a thing
const jre = new Asset(verData.name, '', verData.size, verData.uri, fDir)
this.java = new DLTracker([jre], jre.size, (asset) => {
if (verData.name.endsWith('zip')) {
this.extractJdkZip(asset.to, dataDir)
} else {
// Tar.gz
let h: string;
createReadStream(asset.to)
.on('error', err => logger.error(err))
.pipe(createGunzip())
.on('error', err => logger.error(err))
.pipe(extract(dataDir, {
map: (header) => {
if (h == null) {
h = header.name
}
}
}))
.on('error', err => logger.error(err))
.on('finish', () => {
unlink(asset.to, err => {
if (err) {
logger.error(err)
}
if (h.indexOf('/') > -1) {
h = h.substring(0, h.indexOf('/'))
}
const pos = join(dataDir, h)
this.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
})
})
}
})
return true;
} else {
return false;
}
})
}
public async extractJdkZip(zipPath: string, runtimeDir: string) {
const zip = new StreamZipAsync({
file: zipPath,
storeEntries: true
});
let pos = ''
try {
const entries = await zip.entries()
pos = join(runtimeDir, Object.keys(entries)[0])
logger.info('Extracting jdk..')
await zip.extract(null, runtimeDir)
logger.info('Cleaning up..')
await remove(zipPath)
logger.info('Jdk extraction complete.')
} catch (err) {
logger.error(err)
} finally {
zip.close()
this.emit('complete', 'java', JavaGuard.javaExecFromRoot(pos))
}
}
// #endregion
// #endregion
// Control Flow Functions
// #region
/**
* Initiate an async download process for an AssetGuard DLTracker.
* //TODO: really ?
* @param {string} identifier The identifier of the AssetGuard DLTracker.
* @param {number} limit Optional. The number of async processes to run in parallel.
* @returns {boolean} True if the process began, otherwise false.
*/
public startAsyncProcess(identifier: string, limit: number = 5): boolean {
const dlTracker = this[identifier]
const dlQueue = dlTracker.dlqueue
if (dlQueue.length > 0) {
logger.info('DLQueue', dlQueue)
asyncModule.eachLimit(dlQueue, limit, (asset, cb) => {
ensureDirSync(join(asset.to, '..'))
const req = request(asset.from)
req.pause()
req.on('response', (resp) => {
if (resp.statusCode === 200) {
let doHashCheck = false
const contentLength = parseInt(resp.headers['content-length'] ?? '')
if (contentLength !== asset.size) {
logger.warn(`WARN: Got ${contentLength} bytes for ${asset.id}: Expected ${asset.size}`)
doHashCheck = true
// Adjust download
this.totaldlsize -= asset.size
this.totaldlsize += contentLength
}
const writeStream = createWriteStream(asset.to)
writeStream.on('close', () => {
if (dlTracker.callback != null) {
dlTracker.callback.apply(dlTracker, [asset, self])
}
if (doHashCheck) {
const isValid = AssetGuard.validateLocal(asset.to, asset.type != null ? 'md5' : 'sha1', asset.hash)
if (isValid) {
logger.warn(`Hashes match for ${asset.id}, byte mismatch is an issue in the distro index.`)
} else {
logger.error(`Hashes do not match, ${asset.id} may be corrupted.`)
}
}
cb()
});
req.pipe(writeStream)
req.resume()
} else {
req.abort()
logger.error(`Failed to download ${asset.id}(${typeof asset.from === 'object' ? asset.from.url : asset.from}). Response code ${resp.statusCode}`)
this.progress += asset.size * 1
this.emit('progress', 'download', this.progress, this.totaldlsize)
cb()
}
})
req.on('error', (err) => {
this.emit('error', 'download', err)
})
req.on('data', (chunk) => {
this.progress += chunk.length
this.emit('progress', 'download', this.progress, this.totaldlsize)
})
}, (err) => {
if (err) {
logger.warn('An item in ' + identifier + ' failed to process')
} else {
logger.info('All ' + identifier + ' have been processed successfully')
}
//this.totaldlsize -= dlTracker.dlsize
//this.progress -= dlTracker.dlsize
self[identifier] = new DLTracker([], 0)
if (this.progress >= this.totaldlsize) {
if (this.extractQueue.length > 0) {
this.emit('progress', 'extract', 1, 1)
//this.emit('extracting')
AssetGuard.extractPackXZ(this.extractQueue, this.javaexec).then(() => {
this.extractQueue = []
this.emit('complete', 'download')
})
} else {
this.emit('complete', 'download')
}
}
})
return true
}
return false
}
/**
* //TODO: Refacto
* This function will initiate the download processed for the specified identifiers. If no argument is
* given, all identifiers will be initiated. Note that in order for files to be processed you need to run
* the processing function corresponding to that identifier. If you run this function without processing
* the files, it is likely nothing will be enqueued in the object and processing will complete
* immediately. Once all downloads are complete, this function will fire the 'complete' event on the
* global object instance.
*
* @param {Array.<{id: string, limit: number}>} identifiers Optional. The identifiers to process and corresponding parallel async task limit.
*/
processDlQueues(identifiers = [{ id: 'assets', limit: 20 }, { id: 'libraries', limit: 5 }, { id: 'files', limit: 5 }, { id: 'forge', limit: 5 }]) {
return new Promise((resolve, _reject) => {
let shouldFire = true
// Assign dltracking variables.
this.totaldlsize = 0
this.progress = 0
for (let iden of identifiers) {
this.totaldlsize += this[iden.id].dlsize
}
this.once('complete', () => {
resolve(undefined)
})
for (let iden of identifiers) {
let r = this.startAsyncProcess(iden.id, iden.limit)
if (r) shouldFire = false
}
if (shouldFire) {
this.emit('complete', 'download')
}
})
}
async validateEverything(serverid: string, dev = false) {
try {
if (!ConfigManager.isLoaded) ConfigManager.load()
DistroManager.setDevMode(dev)
const distroIndex = await DistroManager.pullLocal()
const server = distroIndex.getServer(serverid)
if (!server) throw new Error(`No Such Server ${serverid}`)
// Validate Everything
await this.validateDistribution(server)
this.emit('validate', 'distribution')
const versionData = await this.loadVersionData(server.minecraftVersion)
this.emit('validate', 'version')
await this.validateAssets(versionData)
this.emit('validate', 'assets')
await this.validateLibraries(versionData)
this.emit('validate', 'libraries')
await this.validateMiscellaneous(versionData)
this.emit('validate', 'files')
await this.processDlQueues()
//this.emit('complete', 'download')
const forgeData = await this.loadForgeData(server)
return {
versionData,
forgeData
}
} catch (err) {
return {
versionData: null,
forgeData: null,
error: err
}
}
}
}

View File

@ -0,0 +1,62 @@
import { LoggerUtil } from "helios-core/.";
import { Client } from "discord-rpc-patch";
import RPCClient from "discord-rpc-patch/src/client";
const logger = LoggerUtil.getLogger('DiscordWrapper')
export class DiscordRichPresence {
private static client?: RPCClient;
private static activity?: {
details: string,
state: string,
largeImageKey: string,
largeImageText: string,
smallImageKey: string,
smallImageText: string,
startTimestamp: number,
instance: boolean,
};
public static initRPC(genSettings, servSettings, initialDetails = 'Waiting for this.Client..') {
this.client = new Client({ transport: 'ipc' })
this.activity = {
details: initialDetails,
state: 'Server: ' + servSettings.shortId,
largeImageKey: servSettings.largeImageKey,
largeImageText: servSettings.largeImageText,
smallImageKey: genSettings.smallImageKey,
smallImageText: genSettings.smallImageText,
startTimestamp: new Date().getTime(),
instance: false
}
this.client.on('ready', () => {
logger.info('Discord RPC Connected')
this.client.setActivity(activity)
})
this.client.login({ clientId: genSettings.clientId }).catch(error => {
if (error.message.includes('ENOENT')) {
logger.info('Unable to initialize Discord Rich Presence, no client detected.')
} else {
logger.info('Unable to initialize Discord Rich Presence: ' + error.message, error)
}
})
}
public static updateDetails(details) {
if (!this.client || !this.activity) return;
this.activity.details = details
this.client.setActivity(this.activity)
}
public static shutdownRPC() {
if (!this.client) return
this.client.clearActivity()
this.client.destroy()
this.client = undefined
this.activity = undefined
}
}

View File

@ -1,5 +1,729 @@
import * as EventEmitter from "events"; import * as EventEmitter from "events";
import { LoggerUtil } from 'helios-core/.';
import { DevUtil } from '../util/DevUtil';
import { join } from 'path';
import { existsSync, pathExists, readdir } from 'fs-extra';
import Registry from "winreg";
import { MinecraftUtil } from '../util/MinecraftUtil';
import { exec } from "child_process";
import nodeDiskInfo from "node-disk-info";
import fetch from "node-fetch";
import { AdoptiumBinary, JavaMetaObject, JavaRuntimeVersion } from '../util/JavaType';
const logger = LoggerUtil.getLogger("JavaGuard");
export class JavaGuard extends EventEmitter { export class JavaGuard extends EventEmitter {
/**
* Fetch the last open JDK binary.
*
* HOTFIX: Uses Corretto 8 for macOS.
* See: https://github.com/dscalzi/HeliosLauncher/issues/70
* See: https://github.com/AdoptOpenJDK/openjdk-support/issues/101
*
* @param {string} major The major version of Java to fetch.
*
* @returns {Promise.<OpenJDKData>} Promise which resolved to an object containing the JRE download data.
*/
public static latestOpenJDK(major = '8') {
return process.platform === 'darwin' ?
this.latestCorretto(major) : this.latestAdoptium(major);
}
private static async latestAdoptium(major: string) {
const majorNum = Number(major)
const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform)
const url = `https://api.adoptium.net/v3/assets/latest/${major}/hotspot?vendor=eclipse`
const response = await fetch(url).catch(_e => { logger.error(_e); return null });
if (!response) return null;
const json = await response.json() as AdoptiumBinary[]
const targetBinary = json.find(entry => {
return entry.version.major === majorNum
&& entry.binary.os === sanitizedOS
&& entry.binary.image_type === 'jdk'
&& entry.binary.architecture === 'x64'
});
return targetBinary ?
{
uri: targetBinary.binary.package.link,
size: targetBinary.binary.package.size,
name: targetBinary.binary.package.name
}
: null
}
private static async latestCorretto(major: string) {
let sanitizedOS: string, ext: string;
switch (process.platform) {
case 'win32':
sanitizedOS = 'windows'
ext = 'zip'
break
case 'darwin':
sanitizedOS = 'macos'
ext = 'tar.gz'
break
case 'linux':
sanitizedOS = 'linux'
ext = 'tar.gz'
break
default:
sanitizedOS = process.platform
ext = 'tar.gz'
break
}
const arch = DevUtil.isARM64 ? 'aarch64' : 'x64'
const url = `https://corretto.aws/downloads/latest/amazon-corretto-${major}-${arch}-${sanitizedOS}-jdk.${ext}`
const response = await fetch(url).catch(e => { logger.error(e); return null; });
if (!response) return null;
return {
uri: url,
size: Number(response.headers.get("content-length")),
name: url.substring(url.lastIndexOf('/') + 1)
}
}
/**
* Returns the path of the OS-specific executable for the given Java
* installation. Supported OS's are win32, darwin, linux.
*
* @param {string} rootDir The root directory of the Java installation.
* @returns {string} The path to the Java executable.
*/
public static javaExecFromRoot(rootDir) {
if (process.platform === 'win32') {
return join(rootDir, 'bin', 'javaw.exe')
} else if (process.platform === 'darwin') {
return join(rootDir, 'Contents', 'Home', 'bin', 'java')
} else if (process.platform === 'linux') {
return join(rootDir, 'bin', 'java')
}
return rootDir
}
/**
* Check to see if the given path points to a Java executable.
*
* @param {string} pth The path to check against.
* @returns {boolean} True if the path points to a Java executable, otherwise false.
*/
public static isJavaExecPath(pth: string) {
if (pth == null) {
return false
}
if (process.platform === 'win32') {
return pth.endsWith(join('bin', 'javaw.exe'))
} else if (process.platform === 'darwin') {
return pth.endsWith(join('bin', 'java'))
} else if (process.platform === 'linux') {
return pth.endsWith(join('bin', 'java'))
}
return false
}
/**
* Load Mojang's launcher.json file.
*
* //TODO: Import the launcher.json to have autocompletion
*
* @returns {Promise.<Object>} Promise which resolves to Mojang's launcher.json object.
*/
public static async loadMojangLauncherData() {
const response = await fetch('https://launchermeta.mojang.com/mc/launcher.json').catch(e => { logger.error(e); return null });
if (!response) return null;
return response.json();
}
/**
* Parses a **full** Java Runtime version string and resolves
* the version information. Dynamically detects the formatting
* to use.
*
* @param {string} versionString Full version string to parse.
* @returns Object containing the version information.
*/
static parseJavaRuntimeVersion(versionString: string): JavaRuntimeVersion {
const major = versionString.split('.')[0]
return major == "1" ?
JavaGuard.parseJavaRuntimeVersion_8(versionString)
: JavaGuard.parseJavaRuntimeVersion_9(versionString)
}
/**
* Parses a **full** Java Runtime version string and resolves
* the version information. Uses Java 8 formatting.
*
* @param {string} versionString Full version string to parse.
* @returns Object containing the version information.
*/
public static parseJavaRuntimeVersion_8(versionString: string) {
// 1.{major}.0_{update}-b{build}
// ex. 1.8.0_152-b16
let pts = versionString.split('-')
const build = parseInt(pts[1].substring(1))
pts = pts[0].split('_')
const update = parseInt(pts[1])
const major = parseInt(pts[0].split('.')[1])
return {
build,
update,
major,
minor: undefined,
revision: undefined
}
}
/**
* Parses a **full** Java Runtime version string and resolves
* the version information. Uses Java 9+ formatting.
*
* @param {string} verString Full version string to parse.
* @returns Object containing the version information.
*/
public static parseJavaRuntimeVersion_9(verString: string) {
// {major}.{minor}.{revision}+{build}
// ex. 10.0.2+13
let pts = verString.split('+')
const build = parseInt(pts[1])
pts = pts[0].split('.')
const major = parseInt(pts[0])
const minor = parseInt(pts[1])
const revision = parseInt(pts[2])
return {
build,
major,
minor,
revision
}
}
/**
* Checks for the presence of the environment variable JAVA_HOME. If it exits, we will check
* to see if the value points to a path which exists. If the path exits, the path is returned.
*
* @returns {string} The path defined by JAVA_HOME, if it exists. Otherwise null.
*/
private static scanJavaHome() {
const jHome = process.env.JAVA_HOME
if (!jHome) return null;
try {
let res = existsSync(jHome)
return res ? jHome : null
} catch (err) {
// Malformed JAVA_HOME property.
return null
}
}
/**
* Scans the registry for 64-bit Java entries. The paths of each entry are added to
* a set and returned. Currently, only Java 8 (1.8) is supported.
*
* @returns {Promise.<Set.<string>>} A promise which resolves to a set of 64-bit Java root
* paths found in the registry.
*/
private static scanRegistry(): Promise<Set<string>> {
return new Promise((resolve, _reject) => {
// Keys for Java v9.0.0 and later:
// 'SOFTWARE\\JavaSoft\\JRE'
// 'SOFTWARE\\JavaSoft\\JDK'
// Forge does not yet support Java 9, therefore we do not.
// Keys for Java 1.8 and prior:
const regKeys = [
'\\SOFTWARE\\JavaSoft\\Java Runtime Environment',
'\\SOFTWARE\\JavaSoft\\Java Development Kit'
]
let keysDone = 0
const candidates = new Set<string>()
for (let i = 0; i < regKeys.length; i++) {
const key = new Registry({
hive: Registry.HKLM,
key: regKeys[i],
arch: 'x64'
})
key.keyExists((err, exists) => {
if (exists) {
key.keys((err, javaVers) => {
if (err) {
keysDone++
console.error(err)
// REG KEY DONE
// DUE TO ERROR
if (keysDone === regKeys.length) {
resolve(candidates)
}
} else {
if (javaVers.length === 0) {
// REG KEY DONE
// NO SUBKEYS
keysDone++
if (keysDone === regKeys.length) {
resolve(candidates)
}
} else {
let numDone = 0
for (let j = 0; j < javaVers.length; j++) {
const javaVer = javaVers[j]
const vKey = javaVer.key.substring(javaVer.key.lastIndexOf('\\') + 1)
// Only Java 8 is supported currently.
if (parseFloat(vKey) === 1.8) {
javaVer.get('JavaHome', (err, res) => {
const jHome = res.value
if (jHome.indexOf('(x86)') === -1) {
candidates.add(jHome)
}
// SUBKEY DONE
numDone++
if (numDone === javaVers.length) {
keysDone++
if (keysDone === regKeys.length) {
resolve(candidates)
}
}
})
} else {
// SUBKEY DONE
// NOT JAVA 8
numDone++
if (numDone === javaVers.length) {
keysDone++
if (keysDone === regKeys.length) {
resolve(candidates)
}
}
}
}
}
}
})
} else {
// REG KEY DONE
// DUE TO NON-EXISTANCE
keysDone++
if (keysDone === regKeys.length) {
resolve(candidates)
}
}
})
}
})
}
/**
* See if JRE exists in the Internet Plug-Ins folder.
*
* @returns {string} The path of the JRE if found, otherwise null.
*/
private static scanInternetPlugins() {
// /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java
const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin'
const res = existsSync(JavaGuard.javaExecFromRoot(pth))
return res ? pth : null
}
/**
* Scan a directory for root JVM folders.
*
* @param {string} scanDir The directory to scan.
* @returns {Promise.<Set.<string>>} A promise which resolves to a set of the discovered
* root JVM folders.
*/
private static async scanFileSystem(scanDir) {
let res = new Set<string>()
if (await pathExists(scanDir)) {
const files = await readdir(scanDir)
for (let i = 0; i < files.length; i++) {
const combinedPath = join(scanDir, files[i])
const execPath = JavaGuard.javaExecFromRoot(combinedPath)
if (await pathExists(execPath)) {
res.add(combinedPath)
}
}
}
return res
}
/**
* Sort an array of JVM meta objects. Best candidates are placed before all others.
* Sorts based on version and gives priority to JREs over JDKs if versions match.
*
* @param {Object[]} validArr An array of JVM meta objects.
* @returns {Object[]} A sorted array of JVM meta objects.
*/
private static sortValidJavaArray(validArr: JavaMetaObject[]) {
const retArr = validArr.sort((a, b) => {
if (a.version.major === b.version.major) {
if (a.version.major < 9) {
// Java 8
if (a.version.update === b.version.update) {
if (a.version.build === b.version.build) {
// Same version, give priority to JRE.
if (a.execPath!.toLowerCase().indexOf('jdk') > -1) {
return b.execPath!.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
} else {
return -1
}
} else {
return a.version.build > b.version.build ? -1 : 1
}
} else {
return a.version.update! > b.version.update! ? -1 : 1
}
} else {
// Java 9+
if (a.version.minor === b.version.minor) {
if (a.version.revision === b.version.revision) {
// Same version, give priority to JRE.
if (a.execPath!.toLowerCase().indexOf('jdk') > -1) {
return b.execPath!.toLowerCase().indexOf('jdk') > -1 ? 0 : 1
} else {
return -1
}
} else {
return a.version.revision! > b.version.revision! ? -1 : 1
}
} else {
return a.version.minor! > b.version.minor! ? -1 : 1
}
}
} else {
return a.version.major > b.version.major ? -1 : 1
}
})
return retArr
}
constructor(public mcVersion: string) {
super();
}
/**
* Validates the output of a JVM's properties. Currently validates that a JRE is x64
* and that the major = 8, update > 52.
*
* @param {string} stderr The output to validate.
*
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
* The validity is stored inside the `valid` property.
*/
private validateJVMProperties(stderr: string) {
const res = stderr
const props = res.split('\n')
const goal = 2
let checksum = 0
const meta: any = {}
for (let i = 0; i < props.length; i++) {
if (props[i].indexOf('sun.arch.data.model') > -1) {
const arch = props[i].split('=')[1].trim()
const parsedArch = parseInt(arch)
logger.debug(props[i].trim())
if (parsedArch === 64) {
meta.arch = parsedArch
++checksum
if (checksum === goal) break;
}
} else if (props[i].indexOf('java.runtime.version') > -1) {
let verString = props[i].split('=')[1].trim()
logger.debug(props[i].trim())
const objectVersion = JavaGuard.parseJavaRuntimeVersion(verString)
// TODO implement a support matrix eventually. Right now this is good enough
// 1.7-1.16 = Java 8
// 1.17+ = Java 17
// Actual support may vary, but we're going with this rule for simplicity.
if (objectVersion.major < 9 && !MinecraftUtil.mcVersionAtLeast('1.17', this.mcVersion)) {
// Java 8
if (objectVersion.major === 8 && objectVersion.update! > 52) {
meta.version = objectVersion
++checksum
if (checksum === goal) break;
}
} else if (objectVersion.major >= 17 && MinecraftUtil.mcVersionAtLeast('1.17', this.mcVersion)) {
// Java 9+
meta.version = objectVersion
++checksum
if (checksum === goal) break;
}
// Space included so we get only the vendor.
} else if (props[i].lastIndexOf('java.vendor ') > -1) {
let vendorName = props[i].split('=')[1].trim()
logger.debug(props[i].trim())
meta.vendor = vendorName
} else if (props[i].indexOf('os.arch') > -1) {
meta.isARM = props[i].split('=')[1].trim() === 'aarch64'
}
}
meta.valid = checksum === goal
return meta
}
/**
* Validates that a Java binary is at least 64 bit. This makes use of the non-standard
* command line option -XshowSettings:properties. The output of this contains a property,
* sun.arch.data.model = ARCH, in which ARCH is either 32 or 64. This option is supported
* in Java 8 and 9. Since this is a non-standard option. This will resolve to true if
* the function's code throws errors. That would indicate that the option is changed or
* removed.
*
* @param {string} binaryExecPath Path to the java executable we wish to validate.
*
* @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
* The validity is stored inside the `valid` property.
*/
private validateJavaBinary(binaryExecPath): Promise<JavaMetaObject | null> {
return new Promise((resolve, _reject) => {
if (!JavaGuard.isJavaExecPath(binaryExecPath)) {
resolve(null)
} else if (existsSync(binaryExecPath)) {
// Workaround (javaw.exe no longer outputs this information.)
logger.debug(typeof binaryExecPath)
if (binaryExecPath.indexOf('javaw.exe') > -1) {
binaryExecPath.replace('javaw.exe', 'java.exe')
}
exec('"' + binaryExecPath + '" -XshowSettings:properties', (_err, _stdout, stderr) => {
try {
// Output is stored in stderr?
resolve(this.validateJVMProperties(stderr) as JavaMetaObject)
} catch (err) {
// Output format might have changed, validation cannot be completed.
resolve(null)
}
})
} else {
resolve(null)
}
})
}
/**
*
* @param {Set.<string>} rootSet A set of JVM root strings to validate.
* @returns {Promise.<Object[]>} A promise which resolves to an array of meta objects
* for each valid JVM root directory.
*/
private async validateJavaRootSet(rootSet: Set<string>) {
const rootArr = Array.from(rootSet)
const validArr: JavaMetaObject[] = []
for (let i = 0; i < rootArr.length; i++) {
const execPath = JavaGuard.javaExecFromRoot(rootArr[i])
let metaObj: JavaMetaObject | null = await this.validateJavaBinary(execPath);
if (!metaObj) continue;
metaObj.execPath = execPath
validArr.push(metaObj)
}
return validArr
}
/**
* Attempts to find a valid x64 installation of Java on Windows machines.
* Possible paths will be pulled from the registry and the JAVA_HOME environment
* variable. The paths will be sorted with higher versions preceeding lower, and
* JREs preceeding JDKs. The binaries at the sorted paths will then be validated.
* The first validated is returned.
*
* Higher versions > Lower versions
* If versions are equal, JRE > JDK.
*
* @param {string} dataDir The base launcher directory.
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
* x64 Java installation. If none are found, null is returned.
*/
private async win32JavaValidate(dataDir: string): Promise<string | null> {
// Get possible paths from the registry.
let pathSet1 = await JavaGuard.scanRegistry()
if (pathSet1.size === 0) {
// Do a manual file system scan of program files.
// Check all drives
const driveMounts = nodeDiskInfo.getDiskInfoSync().map(({ mounted }) => mounted)
for (const mount of driveMounts) {
pathSet1 = new Set<string>([
...pathSet1,
...(await JavaGuard.scanFileSystem(`${mount}\\Program Files\\Java`)),
...(await JavaGuard.scanFileSystem(`${mount}\\Program Files\\Eclipse Adoptium`)),
...(await JavaGuard.scanFileSystem(`${mount}\\Program Files\\Eclipse Foundation`)),
...(await JavaGuard.scanFileSystem(`${mount}\\Program Files\\AdoptOpenJDK`))
])
}
}
// Get possible paths from the data directory.
const pathSet2 = await JavaGuard.scanFileSystem(join(dataDir, 'runtime', 'x64'))
// Merge the results.
const uberSet = new Set([...pathSet1, ...pathSet2])
// Validate JAVA_HOME.
const jHome = JavaGuard.scanJavaHome()
if (jHome != null && jHome.indexOf('(x86)') === -1) {
uberSet.add(jHome)
}
let pathArr = await this.validateJavaRootSet(uberSet)
pathArr = JavaGuard.sortValidJavaArray(pathArr)
return pathArr.length > 0 ? pathArr[0].execPath! : null;
}
/**
* Attempts to find a valid x64 installation of Java on MacOS.
* The system JVM directory is scanned for possible installations.
* The JAVA_HOME enviroment variable and internet plugins directory
* are also scanned and validated.
*
* Higher versions > Lower versions
* If versions are equal, JRE > JDK.
*
* @param {string} dataDir The base launcher directory.
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
* x64 Java installation. If none are found, null is returned.
*
* Added: On the system with ARM architecture attempts to find aarch64 Java.
*
*/
private async darwinJavaValidate(dataDir: string): Promise<string | null> {
const pathSet1 = await JavaGuard.scanFileSystem('/Library/Java/JavaVirtualMachines')
const pathSet2 = await JavaGuard.scanFileSystem(join(dataDir, 'runtime', 'x64'))
const uberSet = new Set([...pathSet1, ...pathSet2])
// Check Internet Plugins folder.
const iPPath = JavaGuard.scanInternetPlugins()
if (iPPath != null) {
uberSet.add(iPPath)
}
// Check the JAVA_HOME environment variable.
let jHome = JavaGuard.scanJavaHome()
if (jHome != null) {
// Ensure we are at the absolute root.
if (jHome.includes('/Contents/Home')) {
jHome = jHome.substring(0, jHome.indexOf('/Contents/Home'))
}
uberSet.add(jHome)
}
let pathArr = await this.validateJavaRootSet(uberSet)
pathArr = JavaGuard.sortValidJavaArray(pathArr)
if (pathArr.length > 0) {
// TODO Revise this a bit, seems to work for now. Discovery logic should
// probably just filter out the invalid architectures before it even
// gets to this point.
if (DevUtil.isARM64) {
return pathArr.find(({ isARM }) => isARM)?.execPath ?? null
} else {
return pathArr.find(({ isARM }) => !isARM)?.execPath ?? null
}
} else {
return null
}
}
/**
* Attempts to find a valid x64 installation of Java on Linux.
* The system JVM directory is scanned for possible installations.
* The JAVA_HOME enviroment variable is also scanned and validated.
*
* Higher versions > Lower versions
* If versions are equal, JRE > JDK.
*
* @param {string} dataDir The base launcher directory.
* @returns {Promise.<string>} A Promise which resolves to the executable path of a valid
* x64 Java installation. If none are found, null is returned.
*/
async linuxJavaValidate(dataDir: string): Promise<string | null> {
const pathSet1 = await JavaGuard.scanFileSystem('/usr/lib/jvm')
const pathSet2 = await JavaGuard.scanFileSystem(join(dataDir, 'runtime', 'x64'))
const uberSet = new Set([...pathSet1, ...pathSet2])
// Validate JAVA_HOME
const jHome = JavaGuard.scanJavaHome()
if (jHome != null) {
uberSet.add(jHome)
}
let pathArr = await this.validateJavaRootSet(uberSet)
pathArr = JavaGuard.sortValidJavaArray(pathArr)
return pathArr.length > 0 ? pathArr[0].execPath! : null;
}
/**
* Retrieve the path of a valid x64 Java installation.
*
* @param {string} dataDir The base launcher directory.
* @returns {string} A path to a valid x64 Java installation, null if none found.
*/
public async validateJava(dataDir) {
return this[process.platform + 'JavaValidate'](dataDir)
}
} }

View File

@ -12,6 +12,8 @@ import { DistroTypes } from "../manager/DistroManager";
import { Library } from "../models/Library"; import { Library } from "../models/Library";
import { Module } from "../models/Module"; import { Module } from "../models/Module";
import { Required } from "../models/Required"; import { Required } from "../models/Required";
import { ArgumentRule, MinecraftGameVersionManifest } from '../dto/Minecraft';
import { Server } from '../models/Server';
const logger = LoggerUtil.getLogger('ProcessBuilder') const logger = LoggerUtil.getLogger('ProcessBuilder')
export default class ProcessBuilder { export default class ProcessBuilder {
@ -42,14 +44,14 @@ export default class ProcessBuilder {
public llPath?: string; public llPath?: string;
constructor( constructor(
public server, public server: Server,
public versionData, public versionData: MinecraftGameVersionManifest,
public forgeData, public forgeData,
public authUser, public authUser,
public launcherVersion public launcherVersion
) { ) {
this.gameDir = join(ConfigManager.instanceDirectory, server.getID()) this.gameDir = join(ConfigManager.instanceDirectory, server.id)
this.commonDir = ConfigManager.commonDirectory this.commonDir = ConfigManager.commonDirectory
this.versionData = versionData this.versionData = versionData
this.forgeData = forgeData this.forgeData = forgeData
@ -72,27 +74,27 @@ export default class ProcessBuilder {
process.throwDeprecation = true process.throwDeprecation = true
this.setupLiteLoader() this.setupLiteLoader()
logger.info('Using liteloader:', this.usingLiteLoader) logger.info('Using liteloader:', this.usingLiteLoader)
const modObj = this.resolveModConfiguration(ConfigManager.getModConfigurationForServer(this.server.getID()).mods, this.server.getModules()) const modObj = this.resolveModConfiguration(ConfigManager.getModConfigurationForServer(this.server.id).mods, this.server.modules)
// Mod list below 1.13 // Mod list below 1.13
if (!MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { if (!MinecraftUtil.mcVersionAtLeast('1.13', this.server.minecraftVersion)) {
this.constructJSONModList('forge', modObj.forgeMods, true) this.constructJSONModList('forge', modObj.forgeMods, true)
if (this.usingLiteLoader) { if (this.usingLiteLoader) {
this.constructJSONModList('liteloader', modObj.liteMods, true) this.constructJSONModList('liteloader', modObj.liteMods, true)
} }
} }
const uberModArr = modObj.forgeMods.concat(modObj.liteMods) const everyMods = modObj.forgeMods.concat(modObj.liteMods)
let args = this.constructJVMArguments(uberModArr, tempNativePath) let args = this.constructJVMArguments(everyMods, tempNativePath)
if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.minecraftVersion)) {
//args = args.concat(this.constructModArguments(modObj.forgeMods)) //args = args.concat(this.constructModArguments(modObj.forgeMods))
args = args.concat(this.constructModList(modObj.forgeMods)) args = args.concat(this.constructModList(modObj.forgeMods))
} }
logger.info('Launch Arguments:', args) logger.info('Launch Arguments:', args)
const child = spawn(ConfigManager.getJavaExecutable(this.server.getID()), args, { const child = spawn(ConfigManager.getJavaExecutable(this.server.id), args, {
cwd: this.gameDir, cwd: this.gameDir,
detached: ConfigManager.getLaunchDetached() detached: ConfigManager.getLaunchDetached()
}) })
@ -132,20 +134,20 @@ export default class ProcessBuilder {
* mod. It must not be declared as a submodule. * mod. It must not be declared as a submodule.
*/ */
public setupLiteLoader() { public setupLiteLoader() {
for (let ll of this.server.getModules()) { for (let module of this.server.modules) {
if (ll.getType() === DistroTypes.LiteLoader) { if (module.type === DistroTypes.LiteLoader) {
if (!ll.getRequired().isRequired()) { if (!module.required.isRequired) {
const modCfg = ConfigManager.getModConfigurationForServer(this.server.getID()).mods const modCfg = ConfigManager.getModConfigurationForServer(this.server.id).mods
if (ProcessBuilder.isModEnabled(modCfg[ll.getVersionlessID()], ll.getRequired())) { if (ProcessBuilder.isModEnabled(modCfg[module.versionlessID], module.required)) {
if (existsSync(ll.getArtifact().getPath())) { if (existsSync(module.artifact.getPath())) {
this.usingLiteLoader = true this.usingLiteLoader = true
this.llPath = ll.getArtifact().getPath() this.llPath = module.artifact.getPath()
} }
} }
} else { } else {
if (existsSync(ll.getArtifact().getPath())) { if (existsSync(module.artifact.getPath())) {
this.usingLiteLoader = true this.usingLiteLoader = true
this.llPath = ll.getArtifact().getPath() this.llPath = module.artifact.getPath()
} }
} }
} }
@ -168,7 +170,7 @@ export default class ProcessBuilder {
for (let module of modules) { for (let module of modules) {
const type = module.type; const type = module.type;
if (type === DistroTypes.ForgeMod || type === DistroTypes.LiteMod || type === DistroTypes.LiteLoader) { if (type === DistroTypes.ForgeMod || type === DistroTypes.LiteMod || type === DistroTypes.LiteLoader) {
const isRequired = !module.required.isRequired() const isRequired = !module.required.isRequired
const isEnabled = ProcessBuilder.isModEnabled(modConfig[module.versionlessID], module.required) const isEnabled = ProcessBuilder.isModEnabled(modConfig[module.versionlessID], module.required)
if (!isRequired || (isRequired && isEnabled)) { if (!isRequired || (isRequired && isEnabled)) {
if (module.hasSubModules) { if (module.hasSubModules) {
@ -209,7 +211,7 @@ export default class ProcessBuilder {
modRef: [] modRef: []
} }
const ids = [] const ids: string[] = []
if (type === 'forge') { if (type === 'forge') {
for (let mod of mods) { for (let mod of mods) {
ids.push(mod.extensionlessID) ids.push(mod.extensionlessID)
@ -237,8 +239,8 @@ export default class ProcessBuilder {
* @param {string} tempNativePath The path to store the native libraries. * @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process. * @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/ */
public constructJVMArguments(mods: Module, tempNativePath: string) { public constructJVMArguments(mods: Module[], tempNativePath: string): string[] {
if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())) { if (MinecraftUtil.mcVersionAtLeast('1.13', this.server.minecraftVersion)) {
return this.constructJVMArguments113(mods, tempNativePath) return this.constructJVMArguments113(mods, tempNativePath)
} else { } else {
return this.constructJVMArguments112(mods, tempNativePath) return this.constructJVMArguments112(mods, tempNativePath)
@ -258,7 +260,7 @@ export default class ProcessBuilder {
public classpathArg(mods: Module[], tempNativePath: string) { public classpathArg(mods: Module[], tempNativePath: string) {
let cpArgs: string[] = [] let cpArgs: string[] = []
if (!MinecraftUtil.mcVersionAtLeast('1.17', this.server.getMinecraftVersion())) { if (!MinecraftUtil.mcVersionAtLeast('1.17', this.server.minecraftVersion)) {
// Add the version.jar to the classpath. // Add the version.jar to the classpath.
// Must not be added to the classpath for Forge 1.17+. // Must not be added to the classpath for Forge 1.17+.
const version = this.versionData.id const version = this.versionData.id
@ -340,7 +342,7 @@ export default class ProcessBuilder {
* @returns {{[id: string]: string}} An object containing the paths of each library this server requires. * @returns {{[id: string]: string}} An object containing the paths of each library this server requires.
*/ */
private resolveServerLibraries(mods: Module[]) { private resolveServerLibraries(mods: Module[]) {
const modules: Module[] = this.server.getModules(); const modules: Module[] = this.server.modules;
let libs: Record<string, string> = {} let libs: Record<string, string> = {}
// Locate Forge/Libraries // Locate Forge/Libraries
@ -351,6 +353,8 @@ export default class ProcessBuilder {
if (!module.hasSubModules) continue; if (!module.hasSubModules) continue;
const res = this.resolveModuleLibraries(module) const res = this.resolveModuleLibraries(module)
if (res.length > 0) { if (res.length > 0) {
//TODO: I don't understand why ?
libs = { ...libs, ...res } libs = { ...libs, ...res }
} }
} }
@ -362,6 +366,7 @@ export default class ProcessBuilder {
if (!mod.hasSubModules) continue; if (!mod.hasSubModules) continue;
const res = this.resolveModuleLibraries(mods[i]) const res = this.resolveModuleLibraries(mods[i])
if (res.length > 0) { if (res.length > 0) {
//TODO: I don't understand why ?
libs = { ...libs, ...res } libs = { ...libs, ...res }
} }
} }
@ -539,9 +544,9 @@ export default class ProcessBuilder {
args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`) args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`)
args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns')) args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns'))
} }
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.id))
args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) args.push('-Xms' + ConfigManager.getMinRAM(this.server.id))
args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) args = args.concat(ConfigManager.getJVMOptions(this.server.id))
args.push('-Djava.library.path=' + tempNativePath) args.push('-Djava.library.path=' + tempNativePath)
// Main Java Class // Main Java Class
@ -563,12 +568,12 @@ export default class ProcessBuilder {
* @param {string} tempNativePath The path to store the native libraries. * @param {string} tempNativePath The path to store the native libraries.
* @returns {Array.<string>} An array containing the full JVM arguments for this process. * @returns {Array.<string>} An array containing the full JVM arguments for this process.
*/ */
private constructJVMArguments113(mods: Module, tempNativePath: string) { private constructJVMArguments113(mods: Module[], tempNativePath: string): string[] {
const argDiscovery = /\${*(.*)}/ const argDiscovery = /\${*(.*)}/
// JVM Arguments First // JVM Arguments First
let args: string[] = this.versionData.arguments.jvm let args = this.versionData.arguments.jvm
// Debug securejarhandler // Debug securejarhandler
// args.push('-Dbsl.debug=true') // args.push('-Dbsl.debug=true')
@ -590,9 +595,9 @@ export default class ProcessBuilder {
args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`) args.push(`-Xdock:name=${ConfigManager.launcherName.replace(" ", "")}`)
args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns')) args.push('-Xdock:icon=' + join(__dirname, '..', 'images', 'minecraft.icns'))
} }
args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.getID())) args.push('-Xmx' + ConfigManager.getMaxRAM(this.server.id))
args.push('-Xms' + ConfigManager.getMinRAM(this.server.getID())) args.push('-Xms' + ConfigManager.getMinRAM(this.server.id))
args = args.concat(ConfigManager.getJVMOptions(this.server.getID())) args = args.concat(ConfigManager.getJVMOptions(this.server.id))
// Main Java Class // Main Java Class
args.push(this.forgeData.mainClass) args.push(this.forgeData.mainClass)
@ -601,62 +606,19 @@ export default class ProcessBuilder {
args = args.concat(this.versionData.arguments.game) args = args.concat(this.versionData.arguments.game)
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (typeof args[i] === 'object' && args[i].rules != null) { const argument = args[i];
if (typeof argument === 'string') {
let checksum = 0 if (argDiscovery.test(argument)) {
for (let rule of args[i].rules) { const identifier = argument.match(argDiscovery)![1]
if (rule.os != null) { let val: string | null = null;
if (rule.os.name === Library.mojangFriendlyOS()
&& (rule.os.version == null || new RegExp(rule.os.version).test(os.release()))) {
if (rule.action === 'allow') {
checksum++
}
} else {
if (rule.action === 'disallow') {
checksum++
}
}
} else if (rule.features != null) {
// We don't have many 'features' in the index at the moment.
// This should be fine for a while.
if (rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true) {
if (ConfigManager.getFullscreen()) {
args[i].value = [
'--fullscreen',
'true'
]
}
checksum++
}
}
}
// TODO splice not push
if (checksum === args[i].rules.length) {
if (typeof args[i].value === 'string') {
args[i] = args[i].value
} else if (typeof args[i].value === 'object') {
//args = args.concat(args[i].value)
args.splice(i, 1, ...args[i].value)
}
// Decrement i to reprocess the resolved value
i--
} else {
args[i] = null
}
} else if (typeof args[i] === 'string') {
if (argDiscovery.test(args[i])) {
const identifier = args[i].match(argDiscovery)[1]
let val = null
switch (identifier) { switch (identifier) {
case 'auth_player_name': case 'auth_player_name':
val = this.authUser.displayName.trim() val = this.authUser.displayName.trim()
break break
case 'version_name': case 'version_name':
//val = versionData.id //val = versionData.id
val = this.server.getID() val = this.server.id
break break
case 'game_directory': case 'game_directory':
val = this.gameDir val = this.gameDir
@ -680,28 +642,73 @@ export default class ProcessBuilder {
val = this.versionData.type val = this.versionData.type
break break
case 'resolution_width': case 'resolution_width':
val = ConfigManager.getGameWidth() val = ConfigManager.getGameWidth().toString();
break break
case 'resolution_height': case 'resolution_height':
val = ConfigManager.getGameHeight() val = ConfigManager.getGameHeight().toString();
break break
case 'natives_directory': case 'natives_directory':
val = args[i].replace(argDiscovery, tempNativePath) val = argument.replace(argDiscovery, tempNativePath)
break break
case 'launcher_name': case 'launcher_name':
val = args[i].replace(argDiscovery, ConfigManager.launcherName) val = argument.replace(argDiscovery, ConfigManager.launcherName)
break break
case 'launcher_version': case 'launcher_version':
val = args[i].replace(argDiscovery, this.launcherVersion) val = argument.replace(argDiscovery, this.launcherVersion)
break break
case 'classpath': case 'classpath':
val = this.classpathArg(mods, tempNativePath).join(ProcessBuilder.classpathSeparator) val = this.classpathArg(mods, tempNativePath).join(ProcessBuilder.classpathSeparator)
break break
} }
if (val != null) { if (val) {
args[i] = val args[i] = val
} }
} }
} else if (argument.rules != null) {
let checksum = 0
for (let rule of argument.rules) {
if (rule.os != null) {
if (rule.os.name === Library.mojangFriendlyOS()
&& (rule.os.version == null || new RegExp(rule.os.version).test(os.release()))) {
if (rule.action === 'allow') {
checksum++
}
} else {
if (rule.action === 'disallow') {
checksum++
}
}
} else if (rule.features != null) {
// We don't have many 'features' in the index at the moment.
// This should be fine for a while.
// TODO: Make it a bit better
if (rule.features.has_custom_resolution != null && rule.features.has_custom_resolution === true) {
if (ConfigManager.getFullscreen()) {
(args[i] as ArgumentRule).value = [
'--fullscreen',
'true'
]
}
checksum++
}
}
}
// TODO splice not push
if (checksum === argument.rules.length) {
if (typeof argument.value === 'string') {
args[i] = argument.value
} else if (typeof argument.value === 'object') {
args.splice(i, 1, ...argument.value)
}
// Decrement i to reprocess the resolved value
i--;
} else {
// If not whith the checksum remove the element.
args.splice(i, 1)
}
} }
} }
@ -727,11 +734,9 @@ export default class ProcessBuilder {
args = args.concat(this.forgeData.arguments.game) args = args.concat(this.forgeData.arguments.game)
// Filter null values // Filter null values
args = args.filter(arg => { args = args.filter(arg => typeof arg === 'string')
return arg != null
})
return args return args as string[]
} }
private lteMinorVersion(version: number) { private lteMinorVersion(version: number) {
@ -782,14 +787,14 @@ export default class ProcessBuilder {
for (let i = 0; i < mcArgs.length; ++i) { for (let i = 0; i < mcArgs.length; ++i) {
if (argDiscovery.test(mcArgs[i])) { if (argDiscovery.test(mcArgs[i])) {
const identifier = mcArgs[i].match(argDiscovery)[1] const identifier = mcArgs[i].match(argDiscovery)[1]
let val = null let val: string | null = null
switch (identifier) { switch (identifier) {
case 'auth_player_name': case 'auth_player_name':
val = this.authUser.displayName.trim() val = this.authUser.displayName.trim()
break break
case 'version_name': case 'version_name':
//val = versionData.id //val = versionData.id
val = this.server.getID() val = this.server.id
break break
case 'game_directory': case 'game_directory':
val = this.gameDir val = this.gameDir
@ -858,10 +863,11 @@ export default class ProcessBuilder {
return mcArgs return mcArgs
} }
//TODO: Not a huge fan of working by reference in Typescript/JS
private processAutoConnectArg(args: string[]) { // Can be a bit shitty some times
if (ConfigManager.getAutoConnect() && this.server.isAutoConnect()) { private processAutoConnectArg(args: any[]) {
const serverURL = new URL('my://' + this.server.getAddress()) if (ConfigManager.getAutoConnect() && this.server.autoconnect) {
const serverURL = new URL('my://' + this.server.address)
args.push('--server') args.push('--server')
args.push(serverURL.hostname) args.push(serverURL.hostname)
if (serverURL.port) { if (serverURL.port) {

18
src/util/DevUtil.ts Normal file
View File

@ -0,0 +1,18 @@
const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV ?? "", 10) === 1
const isEnvSet = 'ELECTRON_IS_DEV' in process.env
export class DevUtil {
private static enforceDevMode = false;
public static get IsDev() {
//@ts-ignore
return this.enforceDevMode ?? isEnvSet ? getFromEnv : (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath))
}
public static set IsDev(value) {
this.enforceDevMode = value;
}
public static get isARM64() { return process.arch === 'arm64' }
}

49
src/util/JavaType.ts Normal file
View File

@ -0,0 +1,49 @@
export type AdoptiumBinary = {
binary: {
architecture: string,
download_count: number,
heap_size: string,
image_type: string,
jvm_impl: string,
os: string,
package: {
checksum: string,
checksum_link: string,
download_count: number,
link: string,
metadata_link: string,
name: string,
signature_link: string,
size: number
},
project: string,
scm_ref: string,
updated_at: string
}
release_link: string,
release_name: string,
vendor: string,
version: {
build: number,
major: number,
minor: number,
openjdk_version: string,
security: number,
semver: string
}
}
export type JavaRuntimeVersion = {
build: number,
major: number,
minor?: number,
revision?: number,
update?: number,
execPath?: string,
}
export type JavaMetaObject = {
execPath?: string,
version: JavaRuntimeVersion,
isARM?: boolean,
}

View File

@ -21,14 +21,10 @@ export class MinecraftUtil {
public static isForgeGradle3(mcVersion: string, forgeVersion: string) { public static isForgeGradle3(mcVersion: string, forgeVersion: string) {
if (this.mcVersionAtLeast('1.13', mcVersion)) { if (this.mcVersionAtLeast('1.13', mcVersion)) return true;
return true
}
try { try {
const forgeVer = forgeVersion.split('-')[1] const forgeVer = forgeVersion.split('-')[1]
const maxFG2 = [14, 23, 5, 2847] const maxFG2 = [14, 23, 5, 2847]
const verSplit = forgeVer.split('.').map(v => Number(v)) const verSplit = forgeVer.split('.').map(v => Number(v))
@ -39,16 +35,13 @@ export class MinecraftUtil {
return false return false
} }
} }
return false return false
} catch (err) { } catch (err) {
throw new Error('Forge version is complex (changed).. launcher requires a patch.') throw new Error('Forge version is complex (changed).. launcher requires a patch.')
} }
} }
public static isAutoconnectBroken(forgeVersion: string) { public static isAutoconnectBroken(forgeVersion: string) {
const minWorking = [31, 2, 15] const minWorking = [31, 2, 15]
const verSplit = forgeVersion.split('.').map(v => Number(v)) const verSplit = forgeVersion.split('.').map(v => Number(v))