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:
parent
40d0a1cdca
commit
cb62004107
@ -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
29
src/MicrosoftType.ts
Normal 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
98
src/dto/Minecraft.ts
Normal 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
49
src/lang/en_US.json
Normal 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.."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
343
src/main.ts
343
src/main.ts
@ -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()
|
||||||
|
}
|
||||||
|
})
|
@ -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();
|
||||||
|
@ -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.')
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
26
src/scripts/LangLoader.ts
Normal 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
70
src/scripts/Preloading.ts
Normal 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.')
|
||||||
|
}
|
||||||
|
})
|
9
src/scripts/views/welcome.ts
Normal file
9
src/scripts/views/welcome.ts
Normal 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
873
src/services/AssetGuard.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
62
src/services/DiscordRichPresence.ts
Normal file
62
src/services/DiscordRichPresence.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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
18
src/util/DevUtil.ts
Normal 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
49
src/util/JavaType.ts
Normal 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,
|
||||||
|
}
|
@ -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))
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user