1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-16 18:30:06 +00:00

chore: code fixes

This commit is contained in:
Saad Jutt
2022-06-14 16:48:58 +05:00
parent de9ed15286
commit c830f44e29
9 changed files with 343 additions and 310 deletions

View File

@@ -19,7 +19,7 @@ import {
getMacrosFolder, getMacrosFolder,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn,
SASJSRunTimes RunTimeType
} from '../../utils' } from '../../utils'
export interface ExecutionVars { export interface ExecutionVars {
@@ -37,27 +37,33 @@ export interface ExecuteReturnJson {
log?: string log?: string
} }
export class ExecutionController { interface ExecuteFileParams {
async executeFile( programPath: string
programPath: string, preProgramVariables: PreProgramVars
preProgramVariables: PreProgramVars, vars: ExecutionVars
vars: ExecutionVars, otherArgs?: any
otherArgs?: any, returnJson?: boolean
returnJson?: boolean,
session?: Session session?: Session
) { runTime: RunTimeType
for (const runTime of process.runTimes) { }
const codePath =
path
.join(getFilesFolder(), programPath)
.replace(new RegExp('/', 'g'), path.sep) +
'.' +
runTime
if (await fileExists(codePath)) {
const program = await readFile(codePath)
if (runTime === SASJSRunTimes.JS) { interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
return this.executeProgram( program: string
}
export class ExecutionController {
async executeFile({
programPath,
preProgramVariables,
vars,
otherArgs,
returnJson,
session,
runTime
}: ExecuteFileParams) {
const program = await readFile(programPath)
return this.executeProgram({
program, program,
preProgramVariables, preProgramVariables,
vars, vars,
@@ -65,36 +71,22 @@ export class ExecutionController {
returnJson, returnJson,
session, session,
runTime runTime
) })
} else { }
return this.executeProgram(
async executeProgram({
program, program,
preProgramVariables, preProgramVariables,
vars, vars,
otherArgs, otherArgs,
returnJson, returnJson,
session, session: sessionByFileUpload,
runTime runTime
) }: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
}
}
}
throw `ExecutionController: The Stored Program at "${programPath}" does not exist, or you do not have permission to view it.`
}
async executeProgram(
program: string,
preProgramVariables: PreProgramVars,
vars: ExecutionVars,
otherArgs?: any,
returnJson?: boolean,
sessionByFileUpload?: Session,
runTime: string = 'sas'
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
const sessionController = const sessionController =
runTime === SASJSRunTimes.JS runTime === RunTimeType.SAS
? getJSSessionController() ? getSASSessionController()
: getSASSessionController() : getJSSessionController()
const session = const session =
sessionByFileUpload ?? (await sessionController.getSession()) sessionByFileUpload ?? (await sessionController.getSession())
@@ -112,70 +104,18 @@ export class ExecutionController {
preProgramVariables?.httpHeaders.join('\n') ?? '' preProgramVariables?.httpHeaders.join('\n') ?? ''
) )
if (runTime === SASJSRunTimes.JS) { await processProgram(
program = await this.createJSProgram(
program, program,
preProgramVariables, preProgramVariables,
vars, vars,
session, session,
weboutPath, weboutPath,
tokenFile, tokenFile,
runTime,
logPath,
otherArgs otherArgs
) )
const codePath = path.join(session.path, 'code.js')
try {
await createFile(codePath, program)
// create a stream that will write to console outputs to log file
const writeStream = fs.createWriteStream(logPath)
// waiting for the open event so that we can have underlying file descriptor
await once(writeStream, 'open')
execFileSync('node', [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.js program to log and end write stream
writeStream.end(program)
session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
program = await this.createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
}
const log = (await fileExists(logPath)) ? await readFile(logPath) : '' const log = (await fileExists(logPath)) ? await readFile(logPath) : ''
const headersContent = (await fileExists(headersPath)) const headersContent = (await fileExists(headersPath))
? await readFile(headersPath) ? await readFile(headersPath)
@@ -212,7 +152,47 @@ export class ExecutionController {
} }
} }
private async createSASProgram( buildDirectoryTree() {
const root: TreeNode = {
name: 'files',
relativePath: '',
absolutePath: getFilesFolder(),
children: []
}
const stack = [root]
while (stack.length) {
const currentNode = stack.pop()
if (currentNode) {
const children = fs.readdirSync(currentNode.absolutePath)
for (let child of children) {
const absoluteChildPath = `${currentNode.absolutePath}/${child}`
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
children: []
}
currentNode.children.push(childNode)
if (fs.statSync(childNode.absolutePath).isDirectory()) {
stack.push(childNode)
}
}
}
}
return root
}
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const createSASProgram = async (
program: string, program: string,
preProgramVariables: PreProgramVars, preProgramVariables: PreProgramVars,
vars: ExecutionVars, vars: ExecutionVars,
@@ -220,10 +200,9 @@ export class ExecutionController {
weboutPath: string, weboutPath: string,
tokenFile: string, tokenFile: string,
otherArgs?: any otherArgs?: any
) { ) => {
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => (computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`,
`${computed}%let ${key}=${vars[key]};\n`,
'' ''
) )
@@ -279,7 +258,7 @@ ${program}`
return program return program
} }
private async createJSProgram( const createJSProgram = async (
program: string, program: string,
preProgramVariables: PreProgramVars, preProgramVariables: PreProgramVars,
vars: ExecutionVars, vars: ExecutionVars,
@@ -287,7 +266,7 @@ ${program}`
weboutPath: string, weboutPath: string,
tokenFile: string, tokenFile: string,
otherArgs?: any otherArgs?: any
) { ) => {
const varStatments = Object.keys(vars).reduce( const varStatments = Object.keys(vars).reduce(
(computed: string, key: string) => (computed: string, key: string) =>
`${computed}const ${key} = '${vars[key]}';\n`, `${computed}const ${key} = '${vars[key]}';\n`,
@@ -335,42 +314,78 @@ fs.promises.writeFile(weboutPath, _webout)
return requiredModules + program return requiredModules + program
} }
buildDirectoryTree() { const processProgram = async (
const root: TreeNode = { program: string,
name: 'files', preProgramVariables: PreProgramVars,
relativePath: '', vars: ExecutionVars,
absolutePath: getFilesFolder(), session: Session,
children: [] weboutPath: string,
} tokenFile: string,
runTime: RunTimeType,
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const stack = [root] const codePath = path.join(session.path, 'code.js')
while (stack.length) { try {
const currentNode = stack.pop() await createFile(codePath, program)
if (currentNode) { // create a stream that will write to console outputs to log file
const children = fs.readdirSync(currentNode.absolutePath) const writeStream = fs.createWriteStream(logPath)
for (let child of children) { // waiting for the open event so that we can have underlying file descriptor
const absoluteChildPath = `${currentNode.absolutePath}/${child}` await once(writeStream, 'open')
const relativeChildPath = `${currentNode.relativePath}/${child}`
const childNode: TreeNode = {
name: child,
relativePath: relativeChildPath,
absolutePath: absoluteChildPath,
children: []
}
currentNode.children.push(childNode)
if (fs.statSync(childNode.absolutePath).isDirectory()) { execFileSync('node', [codePath], {
stack.push(childNode) stdio: ['ignore', writeStream, writeStream]
} })
}
}
}
return root // copy the code.js program to log and end write stream
} writeStream.end(program)
}
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) session.completed = true
console.log('session completed', session)
} catch (err: any) {
session.completed = true
session.crashed = err.toString()
console.log('session crashed', session.id, session.crashed)
}
} else {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.sas')
// Creating this file in a RUNNING session will break out
// the autoexec loop and actually execute the program
// but - given it will take several milliseconds to create
// (which can mean SAS trying to run a partial program, or
// failing due to file lock) we first create the file THEN
// we rename it.
await createFile(codePath + '.bkp', program)
await moveFile(codePath + '.bkp', codePath)
// we now need to poll the session status
while (!session.completed) {
await delay(50)
}
}
}

View File

@@ -1,9 +1,12 @@
import path from 'path'
import { Request, RequestHandler } from 'express' import { Request, RequestHandler } from 'express'
import multer from 'multer' import multer from 'multer'
import { uuidv4, fileExists } from '@sasjs/utils' import { uuidv4 } from '@sasjs/utils'
import { getSASSessionController, getJSSessionController } from '.' import { getSASSessionController, getJSSessionController } from '.'
import { getFilesFolder, SASJSRunTimes } from '../../utils' import {
executeProgramRawValidation,
getRunTimeAndFilePath,
RunTimeType
} from '../../utils'
export class FileUploadController { export class FileUploadController {
private storage = multer.diskStorage({ private storage = multer.diskStorage({
@@ -22,31 +25,26 @@ export class FileUploadController {
//It will intercept request and generate unique uuid to be used as a subfolder name //It will intercept request and generate unique uuid to be used as a subfolder name
//that will store the files uploaded //that will store the files uploaded
public preUploadMiddleware: RequestHandler = async (req, res, next) => { public preUploadMiddleware: RequestHandler = async (req, res, next) => {
const programPath = req.query._program as string const { error: errQ, value: query } = executeProgramRawValidation(req.query)
const { error: errB, value: body } = executeProgramRawValidation(req.body)
for (const runTime of process.runTimes) { if (errQ && errB) return res.status(400).send(errB.details[0].message)
const codePath =
path const programPath = (query?._program ?? body?._program) as string
.join(getFilesFolder(), programPath)
.replace(new RegExp('/', 'g'), path.sep) + const { runTime } = await getRunTimeAndFilePath(programPath)
'.' +
runTime const sessionController =
runTime === RunTimeType.SAS
? getSASSessionController()
: getJSSessionController()
if (await fileExists(codePath)) {
let sessionController
if (runTime === SASJSRunTimes.JS) {
sessionController = getJSSessionController()
} else {
sessionController = getSASSessionController()
}
const session = await sessionController.getSession() const session = await sessionController.getSession()
// marking consumed true, so that it's not available // marking consumed true, so that it's not available
// as readySession for any other request // as readySession for any other request
session.consumed = true session.consumed = true
req.sasjsSession = session req.sasjsSession = session
break
}
}
next() next()
} }

View File

@@ -1,5 +1,4 @@
import express from 'express' import express from 'express'
import path from 'path'
import { import {
Request, Request,
Security, Security,
@@ -19,12 +18,12 @@ import {
} from './internal' } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getFilesFolder,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn,
LogLine, LogLine,
makeFilesNamesMap, makeFilesNamesMap,
parseLogToArray parseLogToArray,
getRunTimeAndFilePath
} from '../utils' } from '../utils'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
@@ -132,13 +131,16 @@ const executeReturnRaw = async (
): Promise<string | Buffer> => { ): Promise<string | Buffer> => {
const query = req.query as ExecutionVars const query = req.query as ExecutionVars
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
try { try {
const { result, httpHeaders } = const { result, httpHeaders } =
(await new ExecutionController().executeFile( (await new ExecutionController().executeFile({
_program, programPath: codePath,
getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
query vars: query,
)) as ExecuteReturnRaw runTime
})) as ExecuteReturnRaw
// Should over-ride response header for debug // Should over-ride response header for debug
// on GET request to see entire log rendering on browser. // on GET request to see entire log rendering on browser.
@@ -167,20 +169,23 @@ const executeReturnJson = async (
req: express.Request, req: express.Request,
_program: string _program: string
): Promise<ExecuteReturnJsonResponse> => { ): Promise<ExecuteReturnJsonResponse> => {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const filesNamesMap = req.files?.length const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[]) ? makeFilesNamesMap(req.files as MulterFile[])
: null : null
try { try {
const { webout, log, httpHeaders } = const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile( (await new ExecutionController().executeFile({
_program, programPath: codePath,
getPreProgramVariables(req), preProgramVariables: getPreProgramVariables(req),
{ ...req.query, ...req.body }, vars: { ...req.query, ...req.body },
{ filesNamesMap: filesNamesMap }, otherArgs: { filesNamesMap: filesNamesMap },
true, returnJson: true,
req.sasjsSession session: req.sasjsSession,
)) as ExecuteReturnJson runTime
})) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') { if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {

View File

@@ -35,16 +35,17 @@ stpRouter.post(
fileUploadController.preUploadMiddleware, fileUploadController.preUploadMiddleware,
fileUploadController.getMulterUploadObject().any(), fileUploadController.getMulterUploadObject().any(),
async (req, res: any) => { async (req, res: any) => {
const { error: errQ, value: query } = executeProgramRawValidation(req.query) // below validations are moved to preUploadMiddleware
const { error: errB, value: body } = executeProgramRawValidation(req.body) // const { error: errQ, value: query } = executeProgramRawValidation(req.query)
// const { error: errB, value: body } = executeProgramRawValidation(req.body)
if (errQ && errB) return res.status(400).send(errB.details[0].message) // if (errQ && errB) return res.status(400).send(errB.details[0].message)
try { try {
const response = await controller.executeReturnJson( const response = await controller.executeReturnJson(
req, req,
body, req.body,
query?._program req.query?._program as string
) )
// TODO: investigate if this code is required // TODO: investigate if this code is required

View File

@@ -1,11 +1,11 @@
declare namespace NodeJS { declare namespace NodeJS {
export interface Process { export interface Process {
runTimes: string[]
sasLoc: string sasLoc: string
driveLoc: string driveLoc: string
sasSessionController?: import('../../controllers/internal').SASSessionController sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController jsSessionController?: import('../../controllers/internal').JSSessionController
appStreamConfig: import('../').AppStreamConfig appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]
} }
} }

View File

@@ -0,0 +1,18 @@
import path from 'path'
import { fileExists } from '@sasjs/utils'
import { getFilesFolder } from './file'
export const getRunTimeAndFilePath = async (programPath: string) => {
for (const runTime of process.runTimes) {
const codePath =
path
.join(getFilesFolder(), programPath)
.replace(new RegExp('/', 'g'), path.sep) +
'.' +
runTime
if (await fileExists(codePath)) return { codePath, runTime }
}
throw `The Program at (${programPath}) does not exist.`
}

View File

@@ -10,6 +10,7 @@ export * from './generateRefreshToken'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './getPreProgramVariables' export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl' export * from './getServerUrl'
export * from './instantiateLogger' export * from './instantiateLogger'
export * from './isDebugOn' export * from './isDebugOn'

View File

@@ -1,7 +1,7 @@
import path from 'path' import path from 'path'
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
import { getDesktopFields, ModeType } from '.' import { getDesktopFields, ModeType, RunTimeType } from '.'
export const setProcessVariables = async () => { export const setProcessVariables = async () => {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
@@ -19,18 +19,15 @@ export const setProcessVariables = async () => {
process.sasLoc = sasLoc process.sasLoc = sasLoc
} }
const { SASJS_RUNTIMES } = process.env
const runTimes = SASJS_RUNTIMES
? SASJS_RUNTIMES.split(',').map((runTime) => runTime.toLowerCase())
: ['sas']
process.runTimes = runTimes
const { SASJS_ROOT } = process.env const { SASJS_ROOT } = process.env
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd()) const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
await createFolder(absPath) await createFolder(absPath)
process.driveLoc = getRealPath(absPath) process.driveLoc = getRealPath(absPath)
const { RUN_TIMES } = process.env
process.runTimes = (RUN_TIMES as string).split(',') as RunTimeType[]
console.log('sasLoc: ', process.sasLoc) console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc) console.log('sasDrive: ', process.driveLoc)
console.log('runTimes: ', process.runTimes)
} }

View File

@@ -26,7 +26,7 @@ export enum LOG_FORMAT_MORGANType {
tiny = 'tiny' tiny = 'tiny'
} }
export enum SASJSRunTimes { export enum RunTimeType {
SAS = 'sas', SAS = 'sas',
JS = 'js' JS = 'js'
} }
@@ -51,7 +51,7 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyLOG_FORMAT_MORGAN()) errors.push(...verifyLOG_FORMAT_MORGAN())
errors.push(...verifySASJSRunTimes()) errors.push(...verifyRUN_TIMES())
if (errors.length) { if (errors.length) {
process.logger?.error( process.logger?.error(
@@ -209,26 +209,24 @@ const verifyLOG_FORMAT_MORGAN = (): string[] => {
return errors return errors
} }
const verifySASJSRunTimes = (): string[] => { const verifyRUN_TIMES = (): string[] => {
const errors: string[] = [] const errors: string[] = []
const { SASJS_RUNTIMES } = process.env const { RUN_TIMES } = process.env
if (SASJS_RUNTIMES) { if (RUN_TIMES) {
const runTimes = SASJS_RUNTIMES.split(',').map((runTime) => const runTimes = RUN_TIMES.split(',')
runTime.toLowerCase()
)
const possibleRunTimes = Object.values(SASJSRunTimes) const runTimeTypes = Object.values(RunTimeType)
runTimes.forEach((runTime) => { runTimes.forEach((runTime) => {
if (!possibleRunTimes.includes(runTime as SASJSRunTimes)) { if (!runTimeTypes.includes(runTime.toLowerCase() as RunTimeType)) {
errors.push( errors.push(
`- Invalid '${runTime}' runtime\n - valid options ${possibleRunTimes}` `- Invalid '${runTime}' runtime\n - valid options ${runTimeTypes}`
) )
} }
}) })
} else { } else {
process.env.SASJS_RUNTIMES = DEFAULTS.SASJS_RUNTIMES process.env.RUN_TIMES = DEFAULTS.RUN_TIMES
} }
return errors return errors
} }
@@ -239,5 +237,5 @@ const DEFAULTS = {
PORT: '5000', PORT: '5000',
HELMET_COEP: HelmetCoepType.TRUE, HELMET_COEP: HelmetCoepType.TRUE,
LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common, LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common,
SASJS_RUNTIMES: SASJSRunTimes.SAS RUN_TIMES: RunTimeType.SAS
} }