Pipe output from forked processes back to parent.

Also cleaned up the code a bit. Switched to use lambda declarations in promises and renamed 'fulfill' to 'resolve', as it should be,
This commit is contained in:
Daniel Scalzi 2018-05-07 18:15:59 -04:00
parent 0c1ebd0ce0
commit cd4f7918c8
No known key found for this signature in database
GPG Key ID: 5CA2F145B63535F9
5 changed files with 84 additions and 73 deletions

View File

@ -98,7 +98,7 @@ class Library extends Asset {
if(rules == null) return true if(rules == null) return true
let result = true let result = true
rules.forEach(function(rule){ rules.forEach((rule) => {
const action = rule['action'] const action = rule['action']
const osProp = rule['os'] const osProp = rule['os']
if(action != null){ if(action != null){
@ -381,7 +381,7 @@ class AssetGuard extends EventEmitter {
* @returns {Promise.<Object>} A promise which resolves to the distribution data object. * @returns {Promise.<Object>} A promise which resolves to the distribution data object.
*/ */
static retrieveDistributionData(launcherPath, cached = true){ static retrieveDistributionData(launcherPath, cached = true){
return new Promise(function(resolve, reject){ return new Promise((resolve, reject) => {
if(!cached || distributionData == null){ if(!cached || distributionData == null){
// TODO Download file from upstream. // TODO Download file from upstream.
const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json' const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
@ -401,7 +401,7 @@ class AssetGuard extends EventEmitter {
// Workaround while file is not hosted. // Workaround while file is not hosted.
/*fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => { /*fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
distributionData = JSON.parse(data) distributionData = JSON.parse(data)
fulfill(distributionData) resolve(distributionData)
})*/ })*/
} else { } else {
resolve(distributionData) resolve(distributionData)
@ -477,7 +477,11 @@ class AssetGuard extends EventEmitter {
* @returns {Promise.<void>} An empty promise to indicate the extraction has completed. * @returns {Promise.<void>} An empty promise to indicate the extraction has completed.
*/ */
static _extractPackXZ(filePaths, javaExecutable){ static _extractPackXZ(filePaths, javaExecutable){
return new Promise(function(resolve, reject){ console.log('[PackXZExtract] Starting')
return new Promise((resolve, reject) => {
let libPath let libPath
if(isDev){ if(isDev){
libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
@ -492,13 +496,13 @@ class AssetGuard extends EventEmitter {
const filePath = filePaths.join(',') const filePath = filePaths.join(',')
const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath]) const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
child.stdout.on('data', (data) => { child.stdout.on('data', (data) => {
console.log('PackXZExtract:', data.toString('utf8')) console.log('[PackXZExtract]', data.toString('utf8'))
}) })
child.stderr.on('data', (data) => { child.stderr.on('data', (data) => {
console.log('PackXZExtract:', data.toString('utf8')) console.log('[PackXZExtract]', data.toString('utf8'))
}) })
child.on('close', (code, signal) => { child.on('close', (code, signal) => {
console.log('PackXZExtract: Exited with code', code) console.log('[PackXZExtract]', 'Exited with code', code)
resolve() resolve()
}) })
}) })
@ -515,7 +519,7 @@ class AssetGuard extends EventEmitter {
* @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json. * @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json.
*/ */
static _finalizeForgeAsset(asset, basePath){ static _finalizeForgeAsset(asset, basePath){
return new Promise(function(resolve, reject){ return new Promise((resolve, reject) => {
fs.readFile(asset.to, (err, data) => { fs.readFile(asset.to, (err, data) => {
const zip = new AdmZip(data) const zip = new AdmZip(data)
const zipEntries = zip.getEntries() const zipEntries = zip.getEntries()
@ -1066,9 +1070,9 @@ class AssetGuard extends EventEmitter {
*/ */
validateAssets(versionData, force = false){ validateAssets(versionData, force = false){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
self._assetChainIndexData(versionData, force).then(() => { self._assetChainIndexData(versionData, force).then(() => {
fulfill() resolve()
}) })
}) })
} }
@ -1083,7 +1087,7 @@ class AssetGuard extends EventEmitter {
*/ */
_assetChainIndexData(versionData, force = false){ _assetChainIndexData(versionData, force = false){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
//Asset index constants. //Asset index constants.
const assetIndex = versionData.assetIndex const assetIndex = versionData.assetIndex
const name = assetIndex.id + '.json' const name = assetIndex.id + '.json'
@ -1095,16 +1099,16 @@ class AssetGuard extends EventEmitter {
console.log('Downloading ' + versionData.id + ' asset index.') console.log('Downloading ' + versionData.id + ' asset index.')
mkpath.sync(indexPath) mkpath.sync(indexPath)
const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc)) const stream = request(assetIndex.url).pipe(fs.createWriteStream(assetIndexLoc))
stream.on('finish', function() { stream.on('finish', () => {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
self._assetChainValidateAssets(versionData, data).then(() => { self._assetChainValidateAssets(versionData, data).then(() => {
fulfill() resolve()
}) })
}) })
} else { } else {
data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8')) data = JSON.parse(fs.readFileSync(assetIndexLoc, 'utf-8'))
self._assetChainValidateAssets(versionData, data).then(() => { self._assetChainValidateAssets(versionData, data).then(() => {
fulfill() resolve()
}) })
} }
}) })
@ -1119,7 +1123,7 @@ class AssetGuard extends EventEmitter {
*/ */
_assetChainValidateAssets(versionData, indexData){ _assetChainValidateAssets(versionData, indexData){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
//Asset constants //Asset constants
const resourceURL = 'http://resources.download.minecraft.net/' const resourceURL = 'http://resources.download.minecraft.net/'
@ -1132,7 +1136,7 @@ class AssetGuard extends EventEmitter {
let acc = 0 let acc = 0
const total = Object.keys(indexData.objects).length const total = Object.keys(indexData.objects).length
//const objKeys = Object.keys(data.objects) //const objKeys = Object.keys(data.objects)
async.forEachOfLimit(indexData.objects, 10, function(value, key, cb){ async.forEachOfLimit(indexData.objects, 10, (value, key, cb) => {
acc++ acc++
self.emit('assetVal', {acc, total}) self.emit('assetVal', {acc, total})
const hash = value.hash const hash = value.hash
@ -1144,9 +1148,9 @@ class AssetGuard extends EventEmitter {
assetDlQueue.push(ast) assetDlQueue.push(ast)
} }
cb() cb()
}, function(err){ }, (err) => {
self.assets = new DLTracker(assetDlQueue, dlSize) self.assets = new DLTracker(assetDlQueue, dlSize)
fulfill() resolve()
}) })
}) })
} }
@ -1167,7 +1171,7 @@ class AssetGuard extends EventEmitter {
*/ */
validateLibraries(versionData){ validateLibraries(versionData){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
const libArr = versionData.libraries const libArr = versionData.libraries
const libPath = path.join(self.basePath, 'libraries') const libPath = path.join(self.basePath, 'libraries')
@ -1176,7 +1180,7 @@ class AssetGuard extends EventEmitter {
let dlSize = 0 let dlSize = 0
//Check validity of each library. If the hashs don't match, download the library. //Check validity of each library. If the hashs don't match, download the library.
async.eachLimit(libArr, 5, function(lib, cb){ async.eachLimit(libArr, 5, (lib, cb) => {
if(Library.validateRules(lib.rules)){ if(Library.validateRules(lib.rules)){
let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]] let artifact = (lib.natives == null) ? lib.downloads.artifact : lib.downloads.classifiers[lib.natives[Library.mojangFriendlyOS()]]
const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path)) const libItm = new Library(lib.name, artifact.sha1, artifact.size, artifact.url, path.join(libPath, artifact.path))
@ -1186,9 +1190,9 @@ class AssetGuard extends EventEmitter {
} }
} }
cb() cb()
}, function(err){ }, (err) => {
self.libraries = new DLTracker(libDlQueue, dlSize) self.libraries = new DLTracker(libDlQueue, dlSize)
fulfill() resolve()
}) })
}) })
} }
@ -1207,10 +1211,10 @@ class AssetGuard extends EventEmitter {
*/ */
validateMiscellaneous(versionData){ validateMiscellaneous(versionData){
const self = this const self = this
return new Promise(async function(fulfill, reject){ return new Promise(async (resolve, reject) => {
await self.validateClient(versionData) await self.validateClient(versionData)
await self.validateLogConfig(versionData) await self.validateLogConfig(versionData)
fulfill() resolve()
}) })
} }
@ -1223,7 +1227,7 @@ class AssetGuard extends EventEmitter {
*/ */
validateClient(versionData, force = false){ validateClient(versionData, force = false){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
const clientData = versionData.downloads.client const clientData = versionData.downloads.client
const version = versionData.id const version = versionData.id
const targetPath = path.join(self.basePath, 'versions', version) const targetPath = path.join(self.basePath, 'versions', version)
@ -1234,9 +1238,9 @@ class AssetGuard extends EventEmitter {
if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){ if(!AssetGuard._validateLocal(client.to, 'sha1', client.hash) || force){
self.files.dlqueue.push(client) self.files.dlqueue.push(client)
self.files.dlsize += client.size*1 self.files.dlsize += client.size*1
fulfill() resolve()
} else { } else {
fulfill() resolve()
} }
}) })
} }
@ -1250,7 +1254,7 @@ class AssetGuard extends EventEmitter {
*/ */
validateLogConfig(versionData){ validateLogConfig(versionData){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
const client = versionData.logging.client const client = versionData.logging.client
const file = client.file const file = client.file
const targetPath = path.join(self.basePath, 'assets', 'log_configs') const targetPath = path.join(self.basePath, 'assets', 'log_configs')
@ -1260,9 +1264,9 @@ class AssetGuard extends EventEmitter {
if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){ if(!AssetGuard._validateLocal(logConfig.to, 'sha1', logConfig.hash)){
self.files.dlqueue.push(logConfig) self.files.dlqueue.push(logConfig)
self.files.dlsize += logConfig.size*1 self.files.dlsize += logConfig.size*1
fulfill() resolve()
} else { } else {
fulfill() resolve()
} }
}) })
} }
@ -1280,7 +1284,7 @@ class AssetGuard extends EventEmitter {
*/ */
validateDistribution(serverpackid){ validateDistribution(serverpackid){
const self = this const self = this
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
AssetGuard.retrieveDistributionData(self.launcherPath, false).then((value) => { AssetGuard.retrieveDistributionData(self.launcherPath, false).then((value) => {
/*const servers = value.servers /*const servers = value.servers
let serv = null let serv = null
@ -1300,29 +1304,16 @@ class AssetGuard extends EventEmitter {
// Correct our workaround here. // Correct our workaround here.
let decompressqueue = self.forge.callback let decompressqueue = self.forge.callback
self.extractQueue = decompressqueue self.extractQueue = decompressqueue
self.forge.callback = function(asset, self){ self.forge.callback = (asset, self) => {
if(asset.type === 'forge-hosted' || asset.type === 'forge'){ if(asset.type === 'forge-hosted' || asset.type === 'forge'){
AssetGuard._finalizeForgeAsset(asset, self.basePath) AssetGuard._finalizeForgeAsset(asset, self.basePath)
} }
} }
fulfill(serv) resolve(serv)
}) })
}) })
} }
/*//TODO The file should be hosted, the following code is for local testing.
_chainValidateDistributionIndex(basePath){
return new Promise(function(fulfill, reject){
//const distroURL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/westeroscraft.json'
//const targetFile = path.join(basePath, 'westeroscraft.json')
//TEMP WORKAROUND TO TEST WHILE THIS IS NOT HOSTED
fs.readFile(path.join(__dirname, '..', 'westeroscraft.json'), 'utf-8', (err, data) => {
fulfill(JSON.parse(data))
})
})
}*/
_parseDistroModules(modules, version){ _parseDistroModules(modules, version){
let alist = [] let alist = []
let asize = 0; let asize = 0;
@ -1378,7 +1369,7 @@ class AssetGuard extends EventEmitter {
*/ */
loadForgeData(serverpack){ loadForgeData(serverpack){
const self = this const self = this
return new Promise(async function(fulfill, reject){ return new Promise(async (resolve, reject) => {
let distro = AssetGuard.retrieveDistributionDataSync(self.launcherPath) let distro = AssetGuard.retrieveDistributionDataSync(self.launcherPath)
const servers = distro.servers const servers = distro.servers
@ -1398,7 +1389,7 @@ class AssetGuard extends EventEmitter {
let obPath = obArtifact.path == null ? path.join(self.basePath, 'libraries', AssetGuard._resolvePath(ob.id, obArtifact.extension)) : obArtifact.path let obPath = obArtifact.path == null ? path.join(self.basePath, 'libraries', AssetGuard._resolvePath(ob.id, obArtifact.extension)) : obArtifact.path
let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type) let asset = new DistroModule(ob.id, obArtifact.MD5, obArtifact.size, obArtifact.url, obPath, ob.type)
let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.basePath) let forgeData = await AssetGuard._finalizeForgeAsset(asset, self.basePath)
fulfill(forgeData) resolve(forgeData)
return return
} }
} }
@ -1541,7 +1532,7 @@ class AssetGuard extends EventEmitter {
if(concurrentDlQueue.length === 0){ if(concurrentDlQueue.length === 0){
return false return false
} else { } else {
async.eachLimit(concurrentDlQueue, limit, function(asset, cb){ async.eachLimit(concurrentDlQueue, limit, (asset, cb) => {
let count = 0; let count = 0;
mkpath.sync(path.join(asset.to, "..")) mkpath.sync(path.join(asset.to, ".."))
let req = request(asset.from) let req = request(asset.from)
@ -1570,14 +1561,14 @@ class AssetGuard extends EventEmitter {
req.on('error', (err) => { req.on('error', (err) => {
self.emit('dlerror', err) self.emit('dlerror', err)
}) })
req.on('data', function(chunk){ req.on('data', (chunk) => {
count += chunk.length count += chunk.length
self.progress += chunk.length self.progress += chunk.length
acc += chunk.length acc += chunk.length
self.emit(identifier + 'dlprogress', acc) self.emit(identifier + 'dlprogress', acc)
self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize}) self.emit('totaldlprogress', {acc: self.progress, total: self.totaldlsize})
}) })
}, function(err){ }, (err) => {
if(err){ if(err){
self.emit(identifier + 'dlerror') self.emit(identifier + 'dlerror')
console.log('An item in ' + identifier + ' failed to process'); console.log('An item in ' + identifier + ' failed to process');

View File

@ -24,7 +24,7 @@ const Mojang = require('./mojang.js')
* @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object. * @returns {Promise.<Object>} Promise which resolves the resolved authenticated account object.
*/ */
exports.addAccount = async function(username, password){ exports.addAccount = async function(username, password){
try{ try {
const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken) const session = await Mojang.authenticate(username, password, ConfigManager.getClientToken)
const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name) const ret = ConfigManager.addAuthAccount(session.selectedProfile.id, session.accessToken, username, session.selectedProfile.name)
ConfigManager.save() ConfigManager.save()

View File

@ -80,7 +80,7 @@ exports.statusToHex = function(status){
* @see http://wiki.vg/Mojang_API#API_Status * @see http://wiki.vg/Mojang_API#API_Status
*/ */
exports.status = function(){ exports.status = function(){
return new Promise(function(fulfill, reject) { return new Promise((resolve, reject) => {
request.get('https://status.mojang.com/check', request.get('https://status.mojang.com/check',
{ {
json: true json: true
@ -102,7 +102,7 @@ exports.status = function(){
} }
} }
} }
fulfill(statuses) resolve(statuses)
} }
}) })
}) })
@ -120,7 +120,7 @@ exports.status = function(){
* @see http://wiki.vg/Authentication#Authenticate * @see http://wiki.vg/Authentication#Authenticate
*/ */
exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){ exports.authenticate = function(username, password, clientToken, requestUser = true, agent = minecraftAgent){
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
request.post(authpath + '/authenticate', request.post(authpath + '/authenticate',
{ {
json: true, json: true,
@ -138,7 +138,7 @@ exports.authenticate = function(username, password, clientToken, requestUser = t
reject(error) reject(error)
} else { } else {
if(response.statusCode === 200){ if(response.statusCode === 200){
fulfill(body) resolve(body)
} else { } else {
reject(body || {code: 'ENOTFOUND'}) reject(body || {code: 'ENOTFOUND'})
} }
@ -157,7 +157,7 @@ exports.authenticate = function(username, password, clientToken, requestUser = t
* @see http://wiki.vg/Authentication#Validate * @see http://wiki.vg/Authentication#Validate
*/ */
exports.validate = function(accessToken, clientToken){ exports.validate = function(accessToken, clientToken){
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
request.post(authpath + '/validate', request.post(authpath + '/validate',
{ {
json: true, json: true,
@ -172,10 +172,10 @@ exports.validate = function(accessToken, clientToken){
reject(error) reject(error)
} else { } else {
if(response.statusCode === 403){ if(response.statusCode === 403){
fulfill(false) resolve(false)
} else { } else {
// 204 if valid // 204 if valid
fulfill(true) resolve(true)
} }
} }
}) })
@ -192,7 +192,7 @@ exports.validate = function(accessToken, clientToken){
* @see http://wiki.vg/Authentication#Invalidate * @see http://wiki.vg/Authentication#Invalidate
*/ */
exports.invalidate = function(accessToken, clientToken){ exports.invalidate = function(accessToken, clientToken){
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
request.post(authpath + '/invalidate', request.post(authpath + '/invalidate',
{ {
json: true, json: true,
@ -207,7 +207,7 @@ exports.invalidate = function(accessToken, clientToken){
reject(error) reject(error)
} else { } else {
if(response.statusCode === 204){ if(response.statusCode === 204){
fulfill() resolve()
} else { } else {
reject(body) reject(body)
} }
@ -228,7 +228,7 @@ exports.invalidate = function(accessToken, clientToken){
* @see http://wiki.vg/Authentication#Refresh * @see http://wiki.vg/Authentication#Refresh
*/ */
exports.refresh = function(accessToken, clientToken, requestUser = true){ exports.refresh = function(accessToken, clientToken, requestUser = true){
return new Promise(function(fulfill, reject){ return new Promise((resolve, reject) => {
request.post(authpath + '/refresh', request.post(authpath + '/refresh',
{ {
json: true, json: true,
@ -244,7 +244,7 @@ exports.refresh = function(accessToken, clientToken, requestUser = true){
reject(error) reject(error)
} else { } else {
if(response.statusCode === 200){ if(response.statusCode === 200){
fulfill(body) resolve(body)
} else { } else {
reject(body) reject(body)
} }

View File

@ -6,12 +6,12 @@ const cp = require('child_process')
const {URL} = require('url') const {URL} = require('url')
// Internal Requirements // Internal Requirements
const {AssetGuard} = require(path.join(__dirname, 'assets', 'js', 'assetguard.js')) const {AssetGuard} = require('./assets/js/assetguard.js')
const AuthManager = require(path.join(__dirname, 'assets', 'js', 'authmanager.js')) const AuthManager = require('./assets/js/authmanager.js')
const DiscordWrapper = require(path.join(__dirname, 'assets', 'js', 'discordwrapper.js')) const DiscordWrapper = require('./assets/js/discordwrapper.js')
const Mojang = require(path.join(__dirname, 'assets', 'js', 'mojang.js')) const Mojang = require('./assets/js/mojang.js')
const ProcessBuilder = require(path.join(__dirname, 'assets', 'js', 'processbuilder.js')) const ProcessBuilder = require('./assets/js/processbuilder.js')
const ServerStatus = require(path.join(__dirname, 'assets', 'js', 'serverstatus.js')) const ServerStatus = require('./assets/js/serverstatus.js')
// Launch Elements // Launch Elements
const launch_content = document.getElementById('launch_content') const launch_content = document.getElementById('launch_content')
@ -223,7 +223,17 @@ function asyncSystemScan(launchAfter = true){
ConfigManager.getGameDirectory(), ConfigManager.getGameDirectory(),
ConfigManager.getLauncherDirectory(), ConfigManager.getLauncherDirectory(),
ConfigManager.getJavaExecutable() ConfigManager.getJavaExecutable()
]) ], {
stdio: 'pipe'
})
// Stdout
sysAEx.stdio[1].on('data', (data) => {
console.log('%c[SysAEx]', 'color: #353232; font-weight: bold', data.toString('utf-8'))
})
// Stderr
sysAEx.stdio[2].on('data', (data) => {
console.log('%c[SysAEx]', 'color: #353232; font-weight: bold', data.toString('utf-8'))
})
sysAEx.on('message', (m) => { sysAEx.on('message', (m) => {
if(m.content === 'validateJava'){ if(m.content === 'validateJava'){
@ -392,7 +402,17 @@ function dlAsync(login = true){
ConfigManager.getGameDirectory(), ConfigManager.getGameDirectory(),
ConfigManager.getLauncherDirectory(), ConfigManager.getLauncherDirectory(),
ConfigManager.getJavaExecutable() ConfigManager.getJavaExecutable()
]) ], {
stdio: 'pipe'
})
// Stdout
aEx.stdio[1].on('data', (data) => {
console.log('%c[AEx]', 'color: #353232; font-weight: bold', data.toString('utf-8'))
})
// Stderr
aEx.stdio[2].on('data', (data) => {
console.log('%c[AEx]', 'color: #353232; font-weight: bold', data.toString('utf-8'))
})
// Establish communications between the AssetExec and current process. // Establish communications between the AssetExec and current process.
aEx.on('message', (m) => { aEx.on('message', (m) => {

View File

@ -4,7 +4,7 @@
*/ */
// Requirements // Requirements
const path = require('path') const path = require('path')
const ConfigManager = require(path.join(__dirname, 'assets', 'js', 'configmanager.js')) const ConfigManager = require('./assets/js/configmanager.js')
let rscShouldLoad = false let rscShouldLoad = false