blob: 5a08f81c1ae17081c265c7e02a8105c62ea1932e [file] [log] [blame]
/**
*
* Copyright (c) 2020 Silicon Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Environment utilities for ZAP
*
* @module JS API: Environment utilities
*/
const path = require('path')
const os = require('os')
const fs = require('fs')
const pino = require('pino')
const zapBaseUrl = 'http://localhost:'
let saveFileFormat = 2 // This is the enabled only .zap file format
/**
* Set save file format.
* @param {*} n
*/
export function setSaveFileFormat(n) {
saveFileFormat = n
}
/**
* Get save file format.
*
* @returns saveFileFormat
*/
export function defaultFileFormat() {
return saveFileFormat
}
/**
*
* @returns path to zcl.json file
*/
export function builtinSilabsZclMetafile() {
return locateProjectResource('./zcl-builtin/silabs/zcl.json')
}
/**
* Used to retrieve zcl-special.json by zcl reload test in zcl-loader.test.js
*
* @returns path to zcl-special.json file used by zcl-loader.test.js
*/
export function builtinSilabsZclSpecialMetafile() {
return locateProjectResource('./zcl-builtin/silabs/zcl-special.json')
}
/**
*
*
* @returns path to general.xml file
*/
export function builtinSilabsZclGeneralXmlFile() {
return locateProjectResource('./zcl-builtin/silabs/general.xml')
}
/**
* Used to retrieve general-special.xml by zcl reload test in zcl-loader.test.js
*
* @returns path to general-special.xml file used by zcl-loader.test.js
*/
export function builtinSilabsSpecialZclGeneralSpecialXmlFile() {
return locateProjectResource('./zcl-builtin/silabs/general-special.xml')
}
/**
* Get builtin matter ZCL json file
* @returns matter ZCL json file
*/
export function builtinMatterZclMetafile() {
return locateProjectResource('./zcl-builtin/matter/zcl.json')
}
/**
* Get builtin matter ZCL json file
* @returns matter ZCL json file
*/
export function builtinNewMatterZclMetafile() {
return locateProjectResource('./zcl-builtin/matter/zcl-new-data-model.json')
}
/**
* Get builtin dotdot ZCL json file
* @returns dotdot ZCL json file
*/
export function builtinDotdotZclMetafile() {
return locateProjectResource('./zcl-builtin/dotdot/library.xml')
}
/**
* Get builtin Matter ZCL json file
* @returns matter ZCL json file
*/
export function builtinMatterZclMetafile2() {
return locateProjectResource(
'./zcl-builtin/matter/zcl-with-test-extensions.json'
)
}
/**
* No builtin meta template file.
* @returns null
*/
export function builtinTemplateMetafile() {
return null // No default.
}
export const environmentVariable = {
logLevel: {
name: 'ZAP_LOGLEVEL',
description: 'Sets the log level. If unset, then default is: warn.'
},
uniqueStateDir: {
name: 'ZAP_TEMPSTATE',
description:
'If set to 1, then instead of .zap, a unique temporary state directory will be created.'
},
stateDir: {
name: 'ZAP_DIR',
description:
'Sets a state directory. Can be overriden by --stateDirectory option. If unset, default is: ~/.zap'
},
skipPostGen: {
name: 'ZAP_SKIP_POST_GENERATION',
description:
'If there is a defined post-generation action for zap, you can set this to variable to 1 to skip it.'
},
reuseZapInstance: {
name: 'ZAP_REUSE_INSTANCE',
description:
'If set to 1, default behavior of zap will be to reuse existing instance.'
},
cleanupDelay: {
name: 'ZAP_CLEANUP_DELAY',
description:
'Amount of millisecons zap will wait for cleanups to perform. This is workaround for some SQLite bug. If unset, default is: 1500'
},
zapGenerationLog: {
name: 'ZAP_GENERATION_LOG',
description:
'Points to a JSON file, which will be used to log every generation performed.'
},
jenkinsHome: {
name: 'JENKINS_HOME',
description:
'When this env variable is present, zap will assume Jenkins environment. That will assume ZAP_TEMPSTATE and ZAP_SKIP_POST_GENERATION to be 1 by default.'
},
saveFileFormat: {
name: 'ZAP_SAVE_FILE_FORMAT',
description: `Overrides a default saved zap file format, ${defaultFileFormat()}. It should be an integer number 0 or greater. This only affects file saving.`
}
}
// builtin pino levels: trace=10, debug=20, info=30, warn=40
const pinoOptions = {
name: 'zap',
level: process.env[environmentVariable.logLevel.name] || 'info', // This sets the default log level. If you set this, to say `sql`, then you will get SQL queries.
customLevels: {
fatal: 60,
error: 50,
warn: 40,
info: 30,
ipc: 27,
browser: 25,
sql: 22,
debug: 20,
trace: 10,
all: 1
},
timestamp: pino.stdTimeFunctions.isoTime
}
// Basic environment tie-ins
let pino_logger = pino(pinoOptions)
let explicit_logger_set = false
let httpStaticContentPath = path.join(__dirname, '../../../spa')
let versionObject = null
let applicationStateDirectory = null
let file_pino_logger = pino(
pinoOptions,
pino.destination({ dest: path.join(appDirectory(), 'zap.log'), sync: true })
)
/**
* Set up the devlopment environment.
*/
export function setDevelopmentEnv() {
// @ts-ignore
process.env.DEV = true
// @ts-ignore
global.__statics = path.join('src', 'statics').replace(/\\/g, '\\\\')
httpStaticContentPath = path.join(__dirname, '../../spa')
// @ts-ignore
global.__backend = path.join(__dirname, '../').replace(/\\/g, '\\\\')
}
/**
* Set up the production environment.
*/
export function setProductionEnv() {
// @ts-ignore
global.__statics = path.join(__dirname, 'statics').replace(/\\/g, '\\\\')
// @ts-ignore
global.__backend = path.join(__dirname, '../').replace(/\\/g, '\\\\')
httpStaticContentPath = path
.join(__dirname, '../../../spa')
.replace(/\\/g, '\\\\')
}
/**
* set explicit_logger_set
*/
export function logInitStdout() {
if (!explicit_logger_set) {
pino_logger = pino(pinoOptions, pino.destination(1))
explicit_logger_set = true
}
}
/**
* Create zap.log file for logging.
*/
export function logInitLogFile() {
if (!explicit_logger_set) {
pino_logger = pino(
pinoOptions,
pino.destination(path.join(appDirectory(), 'zap.log'))
)
explicit_logger_set = true
}
}
/**
* Set the state directory. This method is intended to be called
* only at the application startup, when CLI args are being parsed.
* This method honors '~/' being the first characters in its argument.
*
* @param {*} path Absolute path. Typically '~/.zap'.
*/
export function setAppDirectory(directoryPath) {
let appDir
if (directoryPath.startsWith('~/')) {
appDir = path.join(os.homedir(), directoryPath.substring(2))
} else {
appDir = directoryPath
}
if (!fs.existsSync(appDir)) {
fs.mkdirSync(appDir, { recursive: true })
}
applicationStateDirectory = appDir
return applicationStateDirectory
}
/**
* Returns an app directory. It creates it, if it doesn't exist
*
* @returns state directory, which is guaranteed to be already existing
*/
export function appDirectory() {
if (applicationStateDirectory == null) {
let appDir = path.join(os.homedir(), '.zap')
if (!fs.existsSync(appDir)) {
fs.mkdirSync(appDir, { recursive: true })
}
applicationStateDirectory = appDir
return appDir
}
return applicationStateDirectory
}
/**
* Get path to icons directory.
*
* @returns path to icons directory
*/
export function iconsDirectory() {
// @ts-ignore
return path.join(global.__backend, '/icons')
}
/**
* Get path to sqlite schema file.
*
* @returns path to sqlite schema file
*/
export function schemaFile() {
// @ts-ignore
return path.join(global.__backend, '/db/zap-schema.sql')
}
/**
* Get sqlite file path relative to app directory.
*
* @param {*} filename
* @returns sqlite file path
*/
export function sqliteFile(filename = 'zap') {
return path.join(appDirectory(), `${filename}.sqlite`)
}
/**
* Get sqlite test file name.
*
* @param {*} id
* @param {*} deleteExistingFile
* @returns sqlite test file name
*/
export function sqliteTestFile(id, deleteExistingFile = true) {
let dir = path.join(__dirname, '../../test/.zap')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
let fileName = path.join(dir, `test-${id}.sqlite`)
if (deleteExistingFile && fs.existsSync(fileName)) fs.unlinkSync(fileName)
return fileName
}
/**
* Returns a version as a single on-line string.
*
*/
export function zapVersionAsString() {
let vo = zapVersion()
return `ver. ${vo.version}, featureLevel ${vo.featureLevel}, commit: ${
vo.hash
} from ${vo.date}, mode: ${vo.source ? 'source' : 'binary'}, exe: ${vo.exe}`
}
/**
* This function locates a resource in the project, such as various
* JSON files and zcl-builtin stuff.
*
* It needs to adapt to a change in path that can occur when
* things are copied into the dist/ directory.
*
* @param filePath
* @returns located project resource
*/
export function locateProjectResource(filePath) {
if (fs.existsSync(path.join(__dirname, '../../zcl-builtin/'))) {
return path.join(__dirname, '../../', filePath)
} else if (fs.existsSync(path.join(__dirname, '../../../zcl-builtin/'))) {
return path.join(__dirname, '../../../', filePath)
} else {
throw new Error(
`Zap integrity error: can not locate resource: ${filePath}.`
)
}
}
/**
* Returns the zap version.
*
* @returns zap version, which is an object that
* contains 'version', 'featureLevel', 'hash', 'timestamp' and 'date'
*/
export function zapVersion() {
if (versionObject == null) {
versionObject = {
version: '',
featureLevel: 0,
hash: 0,
timestamp: 0,
date: '',
exe: process.argv0
}
try {
let p = require(locateProjectResource('./apack.json'))
versionObject.featureLevel = p.featureLevel
} catch (err) {
logError('Could not retrieve featureLevel from apack.json')
versionObject.featureLevel = 0
}
try {
let ver = require(locateProjectResource('./.version.json'))
versionObject.hash = ver.hash
versionObject.timestamp = ver.timestamp
versionObject.date = ver.date
versionObject.version = ver.zapVersion
} catch {
logError('Could not retrieve version from .version.json')
}
try {
let readme = locateProjectResource('./README.md')
if (fs.existsSync(readme)) {
versionObject.source = true
}
} catch (err) {
console.log(err)
logError('Could not identify source vs binary mode.')
}
}
return versionObject
}
/**
* Get zapBaseUrl.
*
* @returns zapBaseUrl
*/
export function baseUrl() {
return zapBaseUrl
}
/**
* Prints the data to stderr, without much fuss.
* @param msg
*/
export function printToStderr(msg) {
console.error(msg)
}
/**
* Base level common logger.
*
* @param {*} level
* @param {*} msg
* @param {*} err
*/
export function log(level, msg, err = null) {
let objectToLog = {
msg: msg
}
if (err != null) {
objectToLog.err = err
// @ts-ignore
objectToLog.err.alert = '⛔'
}
pino_logger[level](objectToLog)
}
/**
* Info level message.
*
* @param {*} msg
* @param {*} err
*/
export function logInfo(msg, err = null) {
log('info', msg, err)
}
/**
* Error level message.
*
* @param {*} msg
* @param {*} err
*/
export function logError(msg, err = null) {
log('error', msg, err)
}
/**
* Warning level message.
*
* @param {*} msg
* @param {*} err
*/
export function logWarning(msg, err = null) {
log('warn', msg, err)
}
/**
* Sql level message.
*
* @param {*} msg
* @param {*} err
*/
export function logSql(msg, query, args) {
if (query == null) {
log('sql', msg)
} else {
// If you want a printout of all the queries for someting,
// you can uncomment this next line. This is sometimes useful
// in development environment to build a map of which queries
// are bottlenecks, so they should be cached.
//console.log(`SQL: ${query.replace(/\s/g, ' ')}`)
log('sql', `${msg} => ${query}: ${args}`)
}
}
/**
* Browser level message.
*
* @param {*} msg
* @param {*} err
*/
export function logBrowser(msg, err = null) {
log('browser', msg, err)
}
/**
* IPC level message.
*
* @param {*} msg
* @param {*} err
*/
export function logIpc(msg, err = null) {
log('ipc', msg, err)
}
/**
* Debug level message.
*
* @param {*} msg
* @param {*} err
*/
export function logDebug(msg, err = null) {
log('debug', msg, err)
}
/**
* Log Warning level message to zap.log file.
*
* @param {*} msg
*/
export function logWarningToFile(msg) {
let objectToLog = {
msg: msg
}
file_pino_logger.warn(objectToLog)
}
/**
* Returns true if major or minor component of versions is different.
*
* @param {*} versionsArray
* @param {*} providedVersion
* @returns boolean
*/
export function isMatchingVersion(versionsArray, providedVersion) {
let ret = false
let v2 = providedVersion.split('.')
versionsArray.forEach((element) => {
let v1 = element.split('.')
if (v1.length != 3 || v2.length != 3) return
if (v1[0] != 'x' && v1[0] != v2[0]) return
if (v1[1] != 'x' && v1[1] != v2[1]) return
if (v1[2] != 'x' && v1[2] != v2[2]) return
ret = true
})
return ret
}
/**
* Returns true if versions of node and electron are matching.
* If versions are not matching, it prints out a warhing
* and returns false.
*
* @returns true or false, depending on match
*/
export function versionsCheck() {
let expectedNodeVersion = ['v14.x.x', 'v16.x.x', 'v18.x.x', 'v20.x.x']
let expectedElectronVersion = [
'17.4.x',
'18.x.x',
'24.x.x',
'27.x.x',
'31.x.x'
]
let nodeVersion = process.version
let electronVersion = process.versions.electron
let ret = true
if (!isMatchingVersion(expectedNodeVersion, nodeVersion)) {
ret = false
console.log(`Expected node versions: ${expectedNodeVersion}`)
console.log(`Provided node version: ${nodeVersion}`)
console.log(
'WARNING: you are using different node version than recommended.'
)
}
if (
electronVersion != null &&
!isMatchingVersion(expectedElectronVersion, electronVersion)
) {
ret = false
console.log(`Expected electron version: ${expectedElectronVersion}`)
console.log(`Provided electron version: ${electronVersion}`)
console.log(
'WARNING: you are using different electron version that recommended.'
)
}
return ret
}
/**
* Returns path to HTTP static content while taking into account DEV / PROD modes.
*
* @returns full path to HTTP static content
*/
export function httpStaticContent() {
return httpStaticContentPath
}