processbuilder.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  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. static isModEnabled(modCfg, required = null){
  68. return modCfg != null ? ((typeof modCfg === 'boolean' && modCfg) || (typeof modCfg === 'object' && modCfg.value)) : required != null && required.def != null ? required.def : true
  69. }
  70. static isModOptional(mdl){
  71. mdl.required != null && mdl.required.value != null && mdl.required.value === false
  72. }
  73. setupLiteLoader(){
  74. const mdls = this.server.modules
  75. for(let i=0; i<mdls.length; i++){
  76. if(mdls[i].type === 'liteloader'){
  77. const ll = mdls[i]
  78. if(ProcessBuilder.isModOptional(ll)){
  79. const modCfg = ConfigManager.getModConfiguration(this.server.id).mods
  80. if(ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(ll.id)], ll.required)){
  81. this.usingLiteLoader = true
  82. this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path)
  83. }
  84. } else {
  85. this.usingLiteLoader = true
  86. this.llPath = path.join(this.libPath, ll.artifact.path == null ? AssetGuard._resolvePath(ll.id, ll.artifact.extension) : ll.artifact.path)
  87. }
  88. }
  89. }
  90. }
  91. resolveModConfiguration(modCfg, mdls){
  92. let fMods = []
  93. let lMods = []
  94. for(let i=0; i<mdls.length; i++){
  95. const mdl = mdls[i]
  96. if(mdl.type != null && (mdl.type === 'forgemod' || mdl.type === 'litemod' || mdl.type === 'liteloader')){
  97. if(mdl.sub_modules != null){
  98. const v = this.resolveModConfiguration(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)], mdl.sub_modules)
  99. fMods = fMods.concat(v.fMods)
  100. lMods = lMods.concat(v.lMods)
  101. if(mdl.type === 'liteloader'){
  102. continue
  103. }
  104. }
  105. if(ProcessBuilder.isModOptional(mdl)){
  106. if(ProcessBuilder.isModEnabled(modCfg[AssetGuard._resolveWithoutVersion(mdl.id)]), mdl.required){
  107. if(mdl.type === 'forgemod'){
  108. fMods.push(mdl)
  109. } else {
  110. lMods.push(mdl)
  111. }
  112. }
  113. } else {
  114. if(mdl.type === 'forgemod'){
  115. fMods.push(mdl)
  116. } else {
  117. lMods.push(mdl)
  118. }
  119. }
  120. }
  121. }
  122. return {
  123. fMods,
  124. lMods
  125. }
  126. }
  127. constructModList(type, mods, save = false){
  128. const modList = {
  129. repositoryRoot: path.join(this.commonDir, 'modstore')
  130. }
  131. const ids = []
  132. for(let i=0; i<mods.length; ++i){
  133. if(type === 'forge'){
  134. ids.push(mods[i].id)
  135. } else {
  136. ids.push(mods[i].id + '@' + (mods[i].artifact.extension != null ? mods[i].artifact.extension.substring(1) : 'jar'))
  137. }
  138. }
  139. modList.modRef = ids
  140. if(save){
  141. const json = JSON.stringify(modList, null, 4)
  142. fs.writeFileSync(type === 'forge' ? this.fmlDir : this.llDir, json, 'UTF-8')
  143. }
  144. return modList
  145. }
  146. /**
  147. * Construct the argument array that will be passed to the JVM process.
  148. *
  149. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  150. * @param {string} tempNativePath The path to store the native libraries.
  151. * @returns {Array.<string>} An array containing the full JVM arguments for this process.
  152. */
  153. constructJVMArguments(mods, tempNativePath){
  154. let args = ['-Xmx' + ConfigManager.getMaxRAM(),
  155. '-Xms' + ConfigManager.getMinRAM(),
  156. '-Djava.library.path=' + tempNativePath,
  157. '-cp',
  158. this.classpathArg(mods, tempNativePath).join(process.platform === 'win32' ? ';' : ':'),
  159. this.forgeData.mainClass]
  160. if(process.platform === 'darwin'){
  161. args.unshift('-Xdock:name=WesterosCraft')
  162. args.unshift('-Xdock:icon=' + path.join(__dirname, '..', 'images', 'minecraft.icns'))
  163. }
  164. args.splice(2, 0, ...ConfigManager.getJVMOptions())
  165. args = args.concat(this._resolveForgeArgs())
  166. return args
  167. }
  168. /**
  169. * Resolve the arguments required by forge.
  170. *
  171. * @returns {Array.<string>} An array containing the arguments required by forge.
  172. */
  173. _resolveForgeArgs(){
  174. const mcArgs = this.forgeData.minecraftArguments.split(' ')
  175. const argDiscovery = /\${*(.*)}/
  176. // Replace the declared variables with their proper values.
  177. for(let i=0; i<mcArgs.length; ++i){
  178. if(argDiscovery.test(mcArgs[i])){
  179. const identifier = mcArgs[i].match(argDiscovery)[1]
  180. let val = null;
  181. switch(identifier){
  182. case 'auth_player_name':
  183. val = this.authUser.displayName
  184. break
  185. case 'version_name':
  186. //val = versionData.id
  187. val = this.server.id
  188. break
  189. case 'game_directory':
  190. val = this.gameDir
  191. break
  192. case 'assets_root':
  193. val = path.join(this.commonDir, 'assets')
  194. break
  195. case 'assets_index_name':
  196. val = this.versionData.assets
  197. break
  198. case 'auth_uuid':
  199. val = this.authUser.uuid
  200. break
  201. case 'auth_access_token':
  202. val = this.authUser.accessToken
  203. break
  204. case 'user_type':
  205. val = 'MOJANG'
  206. break
  207. case 'version_type':
  208. val = this.versionData.type
  209. break
  210. }
  211. if(val != null){
  212. mcArgs[i] = val;
  213. }
  214. }
  215. }
  216. mcArgs.push('--modListFile')
  217. mcArgs.push('absolute:' + this.fmlDir)
  218. if(this.usingLiteLoader){
  219. mcArgs.push('--modRepo')
  220. mcArgs.push(this.llDir)
  221. mcArgs.unshift('com.mumfrey.liteloader.launch.LiteLoaderTweaker')
  222. mcArgs.unshift('--tweakClass')
  223. }
  224. // Prepare game resolution
  225. if(ConfigManager.getFullscreen()){
  226. mcArgs.unshift('--fullscreen')
  227. } else {
  228. mcArgs.unshift(ConfigManager.getGameWidth())
  229. mcArgs.unshift('--width')
  230. mcArgs.unshift(ConfigManager.getGameHeight())
  231. mcArgs.unshift('--height')
  232. }
  233. // Prepare autoconnect
  234. if(ConfigManager.getAutoConnect() && this.server.autoconnect){
  235. const serverURL = new URL('my://' + this.server.server_ip)
  236. mcArgs.unshift(serverURL.hostname)
  237. mcArgs.unshift('--server')
  238. if(serverURL.port){
  239. mcArgs.unshift(serverURL.port)
  240. mcArgs.unshift('--port')
  241. }
  242. }
  243. return mcArgs
  244. }
  245. /**
  246. * Resolve the full classpath argument list for this process. This method will resolve all Mojang-declared
  247. * libraries as well as the libraries declared by the server. Since mods are permitted to declare libraries,
  248. * this method requires all enabled mods as an input
  249. *
  250. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  251. * @param {string} tempNativePath The path to store the native libraries.
  252. * @returns {Array.<string>} An array containing the paths of each library required by this process.
  253. */
  254. classpathArg(mods, tempNativePath){
  255. let cpArgs = []
  256. // Add the version.jar to the classpath.
  257. const version = this.versionData.id
  258. cpArgs.push(path.join(this.commonDir, 'versions', version, version + '.jar'))
  259. if(this.usingLiteLoader){
  260. cpArgs.push(this.llPath)
  261. }
  262. // Resolve the Mojang declared libraries.
  263. const mojangLibs = this._resolveMojangLibraries(tempNativePath)
  264. cpArgs = cpArgs.concat(mojangLibs)
  265. // Resolve the server declared libraries.
  266. const servLibs = this._resolveServerLibraries(mods)
  267. cpArgs = cpArgs.concat(servLibs)
  268. return cpArgs
  269. }
  270. /**
  271. * Resolve the libraries defined by Mojang's version data. This method will also extract
  272. * native libraries and point to the correct location for its classpath.
  273. *
  274. * TODO - clean up function
  275. *
  276. * @param {string} tempNativePath The path to store the native libraries.
  277. * @returns {Array.<string>} An array containing the paths of each library mojang declares.
  278. */
  279. _resolveMojangLibraries(tempNativePath){
  280. const libs = []
  281. const libArr = this.versionData.libraries
  282. mkpath.sync(tempNativePath)
  283. for(let i=0; i<libArr.length; i++){
  284. const lib = libArr[i]
  285. if(Library.validateRules(lib.rules)){
  286. if(lib.natives == null){
  287. const dlInfo = lib.downloads
  288. const artifact = dlInfo.artifact
  289. const to = path.join(this.libPath, artifact.path)
  290. libs.push(to)
  291. } else {
  292. // Extract the native library.
  293. const natives = lib.natives
  294. const extractInst = lib.extract
  295. const exclusionArr = extractInst.exclude
  296. const opSys = Library.mojangFriendlyOS()
  297. const indexId = natives[opSys]
  298. const dlInfo = lib.downloads
  299. const classifiers = dlInfo.classifiers
  300. const artifact = classifiers[indexId]
  301. // Location of native zip.
  302. const to = path.join(this.libPath, artifact.path)
  303. let zip = new AdmZip(to)
  304. let zipEntries = zip.getEntries()
  305. // Unzip the native zip.
  306. for(let i=0; i<zipEntries.length; i++){
  307. const fileName = zipEntries[i].entryName
  308. let shouldExclude = false
  309. // Exclude noted files.
  310. exclusionArr.forEach(function(exclusion){
  311. if(fileName.indexOf(exclusion) > -1){
  312. shouldExclude = true
  313. }
  314. })
  315. // Extract the file.
  316. if(!shouldExclude){
  317. fs.writeFile(path.join(tempNativePath, fileName), zipEntries[i].getData(), (err) => {
  318. if(err){
  319. console.error('Error while extracting native library:', err)
  320. }
  321. })
  322. }
  323. }
  324. }
  325. }
  326. }
  327. return libs
  328. }
  329. /**
  330. * Resolve the libraries declared by this server in order to add them to the classpath.
  331. * This method will also check each enabled mod for libraries, as mods are permitted to
  332. * declare libraries.
  333. *
  334. * @param {Array.<Object>} mods An array of enabled mods which will be launched with this process.
  335. * @returns {Array.<string>} An array containing the paths of each library this server requires.
  336. */
  337. _resolveServerLibraries(mods){
  338. const mdles = this.server.modules
  339. let libs = []
  340. // Locate Forge/Libraries
  341. for(let i=0; i<mdles.length; i++){
  342. if(mdles[i].type != null && (mdles[i].type === 'forge-hosted' || mdles[i].type === 'library')){
  343. let lib = mdles[i]
  344. libs.push(path.join(this.libPath, lib.artifact.path == null ? AssetGuard._resolvePath(lib.id, lib.artifact.extension) : lib.artifact.path))
  345. if(lib.sub_modules != null){
  346. const res = this._resolveModuleLibraries(lib)
  347. if(res.length > 0){
  348. libs = libs.concat(res)
  349. }
  350. }
  351. }
  352. }
  353. //Check for any libraries in our mod list.
  354. for(let i=0; i<mods.length; i++){
  355. if(mods.sub_modules != null){
  356. const res = this._resolveModuleLibraries(mods[i])
  357. if(res.length > 0){
  358. libs = libs.concat(res)
  359. }
  360. }
  361. }
  362. return libs
  363. }
  364. /**
  365. * Recursively resolve the path of each library required by this module.
  366. *
  367. * @param {Object} mdle A module object from the server distro index.
  368. * @returns {Array.<string>} An array containing the paths of each library this module requires.
  369. */
  370. _resolveModuleLibraries(mdle){
  371. if(mdle.sub_modules == null){
  372. return []
  373. }
  374. let libs = []
  375. for(let i=0; i<mdle.sub_modules.length; i++){
  376. const sm = mdle.sub_modules[i]
  377. if(sm.type != null && sm.type == 'library'){
  378. libs.push(path.join(this.libPath, sm.artifact.path == null ? AssetGuard._resolvePath(sm.id, sm.artifact.extension) : sm.artifact.path))
  379. }
  380. // If this module has submodules, we need to resolve the libraries for those.
  381. // To avoid unnecessary recursive calls, base case is checked here.
  382. if(mdle.sub_modules != null){
  383. const res = this._resolveModuleLibraries(sm)
  384. if(res.length > 0){
  385. libs = libs.concat(res)
  386. }
  387. }
  388. }
  389. return libs
  390. }
  391. }
  392. module.exports = ProcessBuilder