processbuilder.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. const AdmZip = require('adm-zip')
  2. const {AssetGuard, Library} = require('./assetguard.js')
  3. const child_process = require('child_process')
  4. const ConfigManager = require('./configmanager.js')
  5. const crypto = require('crypto')
  6. const fs = require('fs')
  7. const mkpath = require('mkdirp')
  8. const os = require('os')
  9. const path = require('path')
  10. const rimraf = require('rimraf')
  11. const {URL} = require('url')
  12. class ProcessBuilder {
  13. constructor(distroServer, versionData, forgeData, authUser){
  14. this.gameDir = path.join(ConfigManager.getInstanceDirectory(), distroServer.id)
  15. this.commonDir = ConfigManager.getCommonDirectory()
  16. this.server = distroServer
  17. this.versionData = versionData
  18. this.forgeData = forgeData
  19. this.authUser = authUser
  20. this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
  21. this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
  22. this.libPath = path.join(this.commonDir, 'libraries')
  23. this.usingLiteLoader = false
  24. this.llPath = null
  25. }
  26. /**
  27. * Convienence method to run the functions typically used to build a process.
  28. */
  29. build(){
  30. mkpath.sync(this.gameDir)
  31. const tempNativePath = path.join(os.tmpdir(), ConfigManager.getTempNativeFolder(), crypto.pseudoRandomBytes(16).toString('hex'))
  32. process.throwDeprecation = true
  33. this.setupLiteLoader()
  34. const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.id).mods, this.server.modules)
  35. this.constructModList('forge', modObj.fMods, true)
  36. if(this.usingLiteLoader){
  37. this.constructModList('liteloader', modObj.lMods, true)
  38. }
  39. const uberModArr = modObj.fMods.concat(modObj.lMods)
  40. const args = this.constructJVMArguments(uberModArr, tempNativePath)
  41. console.log(args)
  42. const child = child_process.spawn(ConfigManager.getJavaExecutable(), args, {
  43. cwd: this.gameDir,
  44. detached: ConfigManager.getLaunchDetached()
  45. })
  46. if(ConfigManager.getLaunchDetached()){
  47. child.unref()
  48. }
  49. child.stdout.on('data', (data) => {
  50. console.log('Minecraft:', data.toString('utf8'))
  51. })
  52. child.stderr.on('data', (data) => {
  53. console.log('Minecraft:', data.toString('utf8'))
  54. })
  55. child.on('close', (code, signal) => {
  56. console.log('Exited with code', code)
  57. rimraf(tempNativePath, (err) => {
  58. if(err){
  59. console.warn('Error while deleting temp dir', err)
  60. } else {
  61. console.log('Temp dir deleted successfully.')
  62. }
  63. })
  64. })
  65. return child
  66. }
  67. /**
  68. * Determine if an optional mod is enabled from its configuration value. If the
  69. * configuration value is null, the required object will be used to
  70. * determine if it is enabled.
  71. *
  72. * A mod is enabled if:
  73. * * The configuration is not null and one of the following:
  74. * * The configuration is a boolean and true.
  75. * * The configuration is an object and its 'value' property is true.
  76. * * The configuration is null and one of the following:
  77. * * The required object is null.
  78. * * The required object's 'def' property is null or true.
  79. *
  80. * @param {Object | boolean} modCfg The mod configuration object.
  81. * @param {Object} required Optional. The required object from the mod's distro declaration.
  82. * @returns {boolean} True if the mod is enabled, false otherwise.
  83. */
  84. static isModEnabled(modCfg, required = null){
  85. return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && modCfg.value)) : required != null && required.def != null ? required.def : true
  86. }
  87. /**
  88. * Determine if a mod is optional.
  89. *
  90. * A mod is optional if its required object is not null and its 'value'
  91. * property is false.
  92. *
  93. * @param {Object} mdl The mod distro module.
  94. * @returns {boolean} True if the mod is optional, otherwise false.
  95. */
  96. static isModOptional(mdl){
  97. return mdl.required != null && mdl.required.value != null && mdl.required.value === false
  98. }
  99. /**
  100. * Function which performs a preliminary scan of the top level
  101. * mods. If liteloader is present here, we setup the special liteloader
  102. * launch options. Note that liteloader is only allowed as a top level
  103. * mod. It must not be declared as a submodule.
  104. */
  105. setupLiteLoader(){
  106. const mdls = this.server.modules
  107. for(let i=0; i<mdls.length; i++){
  108. if(mdls[i].type === 'liteloader'){
  109. const ll = mdls[i]
  110. if(ProcessBuilder.isModOptional(ll)){
  111. const modCfg = ConfigManager.getModConfiguration(this.server.id).mods
  112. if(ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(ll.id)], ll.required)){
  113. this.usingLiteLoader = true
  114. this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path)
  115. }
  116. } else {
  117. this.usingLiteLoader = true
  118. this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path)
  119. }
  120. }
  121. }
  122. }
  123. /**
  124. * Resolve an array of all enabled mods. These mods will be constructed into
  125. * a mod list format and enabled at launch.
  126. *
  127. * @param {Object} modCfg The mod configuration object.
  128. * @param {Array.<Object>} mdls An array of modules to parse.
  129. * @returns {{fMods: Array.<Object>, lMods: Array.<Object>}} An object which contains
  130. * a list of enabled forge mods and litemods.
  131. */
  132. resolveModConfiguration(modCfg, mdls){
  133. let fMods = []
  134. let lMods = []
  135. for(let i=0; i<mdls.length; i++){
  136. const mdl = mdls[i]
  137. if(mdl.type != null && (mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader')){
  138. const o = ProcessBuilder.isModOptional(mdl)
  139. const e = ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)], mdl.required)
  140. if(!o || (o && e)){
  141. if(mdl.sub_modules != null){
  142. const v = this.resolveModConfiguration(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)].mods, mdl.sub_modules)
  143. fMods = fMods.concat(v.fMods)
  144. lMods = lMods.concat(v.lMods)
  145. if(mdl.type === 'liteloader'){
  146. continue
  147. }
  148. }
  149. if(mdl.type === 'forgemod'){
  150. fMods.push(mdl)
  151. } else {
  152. lMods.push(mdl)
  153. }
  154. }
  155. }
  156. }
  157. return {
  158. fMods,
  159. lMods
  160. }
  161. }
  162. /**
  163. * Construct a mod list json object.
  164. *
  165. * @param {'forge' | 'liteloader'} type The mod list type to construct.
  166. * @param {Array.<Object>} mods An array of mods to add to the mod list.
  167. * @param {boolean} save Optional. Whether or not we should save the mod list file.
  168. */
  169. constructModList(type, mods, save = false){
  170. const modList = {
  171. repositoryRoot: path.join(this.commonDir, 'modstore')
  172. }
  173. const ids = []
  174. if(type === 'forge'){
  175. for(let i=0; i<mods.length; ++i){
  176. ids.push(mods[i].id)
  177. }
  178. } else {
  179. for(let i=0; i<mods.length; ++i){
  180. ids.push(mods[i].id + '@' + (mods[i].artifact.extension != null ? mods[i].artifact.extension.substring(1) : 'jar'))
  181. }
  182. }
  183. modList.modRef = ids
  184. if(save){
  185. const json = JSON.stringify(modList, null, 4)
  186. fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
  187. }
  188. return modList
  189. }
  190. /**
  191. * Construct the argument array that will be passed to the JVM process.
  192. *
  193. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  194. * @param {string} tempNativePath The path to store the native libraries.
  195. * @returns {Array.<string>} An array containing the full JVM arguments for this process.
  196. */
  197. constructJVMArguments(mods, tempNativePath){
  198. let args = ['-Xmx' + ConfigManager.getMaxRAM(),
  199. '-Xms' + ConfigManager.getMinRAM(),
  200. '-Djava.library.path=' + tempNativePath,
  201. '-cp',
  202. this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':'),
  203. this.forgeData.mainClass]
  204. if(process.platform === 'darwin'){
  205. args.unshift('-Xdock:name=WesterosCraft')
  206. args.unshift('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
  207. }
  208. args.splice(2, 0, ...ConfigManager.getJVMOptions())
  209. args = args.concat(this._resolveForgeArgs())
  210. return args
  211. }
  212. /**
  213. * Resolve the arguments required by forge.
  214. *
  215. * @returns {Array.<string>} An array containing the arguments required by forge.
  216. */
  217. _resolveForgeArgs(){
  218. const mcArgs = this.forgeData.minecraftArguments.split(' ')
  219. const argDiscovery = /\${*(.*)}/
  220. // Replace the declared variables with their proper values.
  221. for(let i=0; i<mcArgs.length; ++i){
  222. if(argDiscovery.test(mcArgs[i])){
  223. const identifier = mcArgs[i].match(argDiscovery)[1]
  224. let val = null;
  225. switch(identifier){
  226. case 'auth_player_name':
  227. val = this.authUser.displayName
  228. break
  229. case 'version_name':
  230. //val = versionData.id
  231. val = this.server.id
  232. break
  233. case 'game_directory':
  234. val = this.gameDir
  235. break
  236. case 'assets_root':
  237. val = path.join(this.commonDir, 'assets')
  238. break
  239. case 'assets_index_name':
  240. val = this.versionData.assets
  241. break
  242. case 'auth_uuid':
  243. val = this.authUser.uuid
  244. break
  245. case 'auth_access_token':
  246. val = this.authUser.accessToken
  247. break
  248. case 'user_type':
  249. val = 'MOJANG'
  250. break
  251. case 'version_type':
  252. val = this.versionData.type
  253. break
  254. }
  255. if(val != null){
  256. mcArgs[i] = val;
  257. }
  258. }
  259. }
  260. mcArgs.push('--modListFile')
  261. mcArgs.push('absolute:' + this.fmlDir)
  262. if(this.usingLiteLoader){
  263. mcArgs.push('--modRepo')
  264. mcArgs.push(this.llDir)
  265. mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
  266. mcArgs.unshift('--tweakClass')
  267. }
  268. // Prepare game resolution
  269. if(ConfigManager.getFullscreen()){
  270. mcArgs.unshift('--fullscreen')
  271. } else {
  272. mcArgs.unshift(ConfigManager.getGameWidth())
  273. mcArgs.unshift('--width')
  274. mcArgs.unshift(ConfigManager.getGameHeight())
  275. mcArgs.unshift('--height')
  276. }
  277. // Prepare autoconnect
  278. if(ConfigManager.getAutoConnect() && this.server.autoconnect){
  279. const serverURL = new URL('my://' + this.server.server_ip)
  280. mcArgs.unshift(serverURL.hostname)
  281. mcArgs.unshift('--server')
  282. if(serverURL.port){
  283. mcArgs.unshift(serverURL.port)
  284. mcArgs.unshift('--port')
  285. }
  286. }
  287. return mcArgs
  288. }
  289. /**
  290. * Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
  291. * libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
  292. * this method requires all enabled mods as an input
  293. *
  294. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  295. * @param {string} tempNativePath The path to store the native libraries.
  296. * @returns {Array.<string>} An array containing the paths of each library required by this process.
  297. */
  298. classpathArg(mods, tempNativePath){
  299. let cpArgs = []
  300. // Add the version.jar to the classpath.
  301. const version = this.versionData.id
  302. cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
  303. if(this.usingLiteLoader){
  304. cpArgs.push(this.llPath)
  305. }
  306. // Resolve the Mojang declared libraries.
  307. const mojangLibs = this._resolveMojangLibraries(tempNativePath)
  308. cpArgs = cpArgs.concat(mojangLibs)
  309. // Resolve the server declared libraries.
  310. const servLibs = this._resolveServerLibraries(mods)
  311. cpArgs = cpArgs.concat(servLibs)
  312. return cpArgs
  313. }
  314. /**
  315. * Resolve the libraries defined by Mojang's version data. This method will also extract
  316. * native libraries and point to the correct location for its classpath.
  317. *
  318. * TODO - clean up function
  319. *
  320. * @param {string} tempNativePath The path to store the native libraries.
  321. * @returns {Array.<string>} An array containing the paths of each library mojang declares.
  322. */
  323. _resolveMojangLibraries(tempNativePath){
  324. const libs = []
  325. const libArr = this.versionData.libraries
  326. mkpath.sync(tempNativePath)
  327. for(let i=0; i<libArr.length; i++){
  328. const lib = libArr[i]
  329. if(Library.validateRules(lib.rules)){
  330. if(lib.natives == null){
  331. const dlInfo = lib.downloads
  332. const artifact = dlInfo.artifact
  333. const to = path.join(this.libPath, artifact.path)
  334. libs.push(to)
  335. } else {
  336. // Extract the native library.
  337. const natives = lib.natives
  338. const extractInst = lib.extract
  339. const exclusionArr = extractInst.exclude
  340. const opSys = Library.mojangFriendlyOS()
  341. const indexId = natives[opSys]
  342. const dlInfo = lib.downloads
  343. const classifiers = dlInfo.classifiers
  344. const artifact = classifiers[indexId]
  345. // Location of native zip.
  346. const to = path.join(this.libPath, artifact.path)
  347. let zip = new AdmZip(to)
  348. let zipEntries = zip.getEntries()
  349. // Unzip the native zip.
  350. for(let i=0; i<zipEntries.length; i++){
  351. const fileName = zipEntries[i].entryName
  352. let shouldExclude = false
  353. // Exclude noted files.
  354. exclusionArr.forEach(function(exclusion){
  355. if(fileName.indexOf(exclusion) > -1){
  356. shouldExclude = true
  357. }
  358. })
  359. // Extract the file.
  360. if(!shouldExclude){
  361. fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
  362. if(err){
  363. console.error('Error while extracting native library:', err)
  364. }
  365. })
  366. }
  367. }
  368. }
  369. }
  370. }
  371. return libs
  372. }
  373. /**
  374. * Resolve the libraries declared by this server in order to add them to the classpath.
  375. * This method will also check each enabled mod for libraries, as mods are permitted to
  376. * declare libraries.
  377. *
  378. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  379. * @returns {Array.<string>} An array containing the paths of each library this server requires.
  380. */
  381. _resolveServerLibraries(mods){
  382. const mdles = this.server.modules
  383. let libs = []
  384. // Locate Forge/Libraries
  385. for(let i=0; i<mdles.length; i++){
  386. if(mdles[i].type != null && (mdles[i].type === 'forge-hosted' || mdles[i].type === 'library')){
  387. let lib = mdles[i]
  388. libs.push(path.join(this.libPath, lib.artifact.path == null ? AssetGuard._resolvePath(lib.id, lib.artifact.extension) : lib.artifact.path))
  389. if(lib.sub_modules != null){
  390. const res = this._resolveModuleLibraries(lib)
  391. if(res.length > 0){
  392. libs = libs.concat(res)
  393. }
  394. }
  395. }
  396. }
  397. //Check for any libraries in our mod list.
  398. for(let i=0; i<mods.length; i++){
  399. if(mods.sub_modules != null){
  400. const res = this._resolveModuleLibraries(mods[i])
  401. if(res.length > 0){
  402. libs = libs.concat(res)
  403. }
  404. }
  405. }
  406. return libs
  407. }
  408. /**
  409. * Recursively resolve the path of each library required by this module.
  410. *
  411. * @param {Object} mdle A module object from the server distro index.
  412. * @returns {Array.<string>} An array containing the paths of each library this module requires.
  413. */
  414. _resolveModuleLibraries(mdle){
  415. if(mdle.sub_modules == null){
  416. return []
  417. }
  418. let libs = []
  419. for(let i=0; i<mdle.sub_modules.length; i++){
  420. const sm = mdle.sub_modules[i]
  421. if(sm.type != null && sm.type == 'library'){
  422. libs.push(path.join(this.libPath, sm.artifact.path == null ? AssetGuard._resolvePath(sm.id, sm.artifact.extension) : sm.artifact.path))
  423. }
  424. // If this module has submodules, we need to resolve the libraries for those.
  425. // To avoid unnecessary recursive calls, base case is checked here.
  426. if(mdle.sub_modules != null){
  427. const res = this._resolveModuleLibraries(sm)
  428. if(res.length > 0){
  429. libs = libs.concat(res)
  430. }
  431. }
  432. }
  433. return libs
  434. }
  435. }
  436. module.exports = ProcessBuilder