From 194eaec7d4a561468f83bf6efce484909ee532eb Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 3 Jun 2022 17:19:12 +0500 Subject: [PATCH 01/31] fix: add runtimes to global process object --- api/src/utils/setProcessVariables.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 4edac7c..22205b3 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -19,6 +19,13 @@ export const setProcessVariables = async () => { 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 absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd()) await createFolder(absPath) From 07295aa151175db8c93eeef806fc3b7fde40ac72 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 3 Jun 2022 17:23:28 +0500 Subject: [PATCH 02/31] feat: conver single session controller to two controller i.e. SASSessionController and JSSessionController --- .../internal/FileUploadController.ts | 45 +++++++++++++---- api/src/controllers/internal/Session.ts | 49 +++++++++++++++++-- api/src/types/system/express.d.ts | 2 +- api/src/types/system/process.d.ts | 4 +- 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index 9593b68..9fcf734 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -1,13 +1,15 @@ +import path from 'path' import { Request, RequestHandler } from 'express' import multer from 'multer' -import { uuidv4 } from '@sasjs/utils' -import { getSessionController } from '.' +import { uuidv4, fileExists, readFile } from '@sasjs/utils' +import { getSASSessionController, getJSSessionController } from '.' +import { getFilesFolder } from '../../utils' export class FileUploadController { private storage = multer.diskStorage({ destination: function (req: Request, file: any, cb: any) { //Sending the intercepted files to the sessions subfolder - cb(null, req.sasSession?.path) + cb(null, req.sasjsSession?.path) }, filename: function (req: Request, file: any, cb: any) { //req_file prefix + unique hash added to sas request files @@ -20,15 +22,38 @@ export class FileUploadController { //It will intercept request and generate unique uuid to be used as a subfolder name //that will store the files uploaded public preUploadMiddleware: RequestHandler = async (req, res, next) => { - let session + if (process.runTimes.length === 0) { + throw 'No runtime is specified in environment variables.' + } - const sessionController = getSessionController() - session = await sessionController.getSession() - // marking consumed true, so that it's not available - // as readySession for any other request - session.consumed = true + const programPath = req.query._program as string - req.sasSession = session + for (const runTime of process.runTimes) { + const codePath = + path + .join(getFilesFolder(), programPath) + .replace(new RegExp('/', 'g'), path.sep) + runTime + + if (await fileExists(programPath)) { + const program = await readFile(codePath) + + if (runTime === 'sas') { + const sessionController = getSASSessionController() + const session = await sessionController.getSession() + // marking consumed true, so that it's not available + // as readySession for any other request + session.consumed = true + + req.sasjsSession = session + } else if (runTime === 'js') { + const sessionController = getJSSessionController() + const session = await sessionController.getSession() + req.sasjsSession = session + } else { + throw `${runTime} runtime is not implemented yet.` + } + } + } next() } diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index b83d70e..4f7eac4 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -17,7 +17,7 @@ import { const execFilePromise = promisify(execFile) -export class SessionController { +export class SASSessionController { private sessions: Session[] = [] private getReadySessions = (): Session[] => @@ -152,12 +152,51 @@ ${autoExecContent}` } } -export const getSessionController = (): SessionController => { - if (process.sessionController) return process.sessionController +export class JSSessionController { + public async getSession() { + const sessionId = generateUniqueFileName(generateTimestamp()) + const sessionFolder = path.join(getSessionsFolder(), sessionId) - process.sessionController = new SessionController() + const creationTimeStamp = sessionId.split('-').pop() as string + // death time of session is 15 mins from creation + const deathTimeStamp = ( + parseInt(creationTimeStamp) + + 15 * 60 * 1000 - + 1000 + ).toString() - return process.sessionController + const session: Session = { + id: sessionId, + ready: true, + inUse: true, + consumed: false, + completed: false, + creationTimeStamp, + deathTimeStamp, + path: sessionFolder + } + + const headersPath = path.join(session.path, 'stpsrv_header.txt') + await createFile(headersPath, 'Content-type: application/json') + + return session + } +} + +export const getSASSessionController = (): SASSessionController => { + if (process.sasSessionController) return process.sasSessionController + + process.sasSessionController = new SASSessionController() + + return process.sasSessionController +} + +export const getJSSessionController = (): JSSessionController => { + if (process.jsSessionController) return process.jsSessionController + + process.jsSessionController = new JSSessionController() + + return process.jsSessionController } const autoExecContent = ` diff --git a/api/src/types/system/express.d.ts b/api/src/types/system/express.d.ts index 7ed4e79..d02d220 100644 --- a/api/src/types/system/express.d.ts +++ b/api/src/types/system/express.d.ts @@ -2,6 +2,6 @@ declare namespace Express { export interface Request { accessToken?: string user?: import('../').RequestUser - sasSession?: import('../').Session + sasjsSession?: import('../').Session } } diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index f7845bc..2d345c1 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -1,8 +1,10 @@ declare namespace NodeJS { export interface Process { + runTimes: string[] sasLoc: string driveLoc: string - sessionController?: import('../../controllers/internal').SessionController + sasSessionController?: import('../../controllers/internal').SASSessionController + jsSessionController?: import('../../controllers/internal').JSSessionController appStreamConfig: import('../').AppStreamConfig logger: import('@sasjs/utils/logger').Logger } From 5df619b3f63571e8e326261d8114869d33881d91 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 3 Jun 2022 17:24:29 +0500 Subject: [PATCH 03/31] fix: pass _program to execute file without extension --- api/src/controllers/stp.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index 6a5d378..d3b01bf 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -131,15 +131,11 @@ const executeReturnRaw = async ( _program: string ): Promise => { const query = req.query as ExecutionVars - const sasCodePath = - path - .join(getFilesFolder(), _program) - .replace(new RegExp('/', 'g'), path.sep) + '.sas' try { const { result, httpHeaders } = (await new ExecutionController().executeFile( - sasCodePath, + _program, getPreProgramVariables(req), query )) as ExecuteReturnRaw @@ -171,11 +167,6 @@ const executeReturnJson = async ( req: express.Request, _program: string ): Promise => { - const sasCodePath = - path - .join(getFilesFolder(), _program) - .replace(new RegExp('/', 'g'), path.sep) + '.sas' - const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files as MulterFile[]) : null @@ -183,12 +174,12 @@ const executeReturnJson = async ( try { const { webout, log, httpHeaders } = (await new ExecutionController().executeFile( - sasCodePath, + _program, getPreProgramVariables(req), { ...req.query, ...req.body }, { filesNamesMap: filesNamesMap }, true, - req.sasSession + req.sasjsSession )) as ExecuteReturnJson let weboutRes: string | IRecordOfAny = webout From c58666eb81514de500519e7b96c1981778ec149b Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 3 Jun 2022 17:26:21 +0500 Subject: [PATCH 04/31] fix: convert single executeProgram method to two methods i.e. executeSASProgram and executeJSProgram --- api/src/controllers/code.ts | 2 +- api/src/controllers/internal/Execution.ts | 152 ++++++++++++++++++++-- 2 files changed, 139 insertions(+), 15 deletions(-) diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index a4b1d74..bff85bb 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -46,7 +46,7 @@ const executeSASCode = async ( try { const { webout, log, httpHeaders } = - (await new ExecutionController().executeProgram( + (await new ExecutionController().executeSASProgram( code, getPreProgramVariables(req), { ...req.query, _debug: 131 }, diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index b4d8b4c..4ec2486 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -1,6 +1,6 @@ import path from 'path' import fs from 'fs' -import { getSessionController } from './' +import { getSASSessionController, getJSSessionController } from './' import { readFile, fileExists, @@ -42,22 +42,45 @@ export class ExecutionController { returnJson?: boolean, session?: Session ) { - if (!(await fileExists(programPath))) - throw 'ExecutionController: SAS file does not exist.' + if (process.runTimes.length === 0) { + throw 'No runtime is specified in environment variables.' + } - const program = await readFile(programPath) + for (const runTime of process.runTimes) { + const codePath = + path + .join(getFilesFolder(), programPath) + .replace(new RegExp('/', 'g'), path.sep) + runTime - return this.executeProgram( - program, - preProgramVariables, - vars, - otherArgs, - returnJson, - session - ) + if (await fileExists(programPath)) { + const program = await readFile(codePath) + + if (runTime === 'sas') { + return this.executeSASProgram( + program, + preProgramVariables, + vars, + otherArgs, + returnJson, + session + ) + } else if (runTime === 'js') { + return this.executeJSProgram( + program, + preProgramVariables, + vars, + otherArgs, + returnJson + ) + } else { + throw `${runTime} runtime is not implemented yet.` + } + } + } + throw 'ExecutionController: Program file does not exist.' } - async executeProgram( + async executeSASProgram( program: string, preProgramVariables: PreProgramVars, vars: ExecutionVars, @@ -65,7 +88,7 @@ export class ExecutionController { returnJson?: boolean, sessionByFileUpload?: Session ): Promise { - const sessionController = getSessionController() + const sessionController = getSASSessionController() const session = sessionByFileUpload ?? (await sessionController.getSession()) @@ -191,6 +214,107 @@ ${program}` } } + async executeJSProgram( + program: string, + preProgramVariables: PreProgramVars, + vars: ExecutionVars, + otherArgs?: any, + returnJson?: boolean, + sessionByFileUpload?: Session + ): Promise { + const sessionController = getJSSessionController() + + const session = + sessionByFileUpload ?? (await sessionController.getSession()) + + const logPath = path.join(session.path, 'log.log') + const headersPath = path.join(session.path, 'stpsrv_header.txt') + const weboutPath = path.join(session.path, 'webout.txt') + const tokenFile = path.join(session.path, 'reqHeaders.txt') + + await createFile( + tokenFile, + preProgramVariables?.httpHeaders.join('\n') ?? '' + ) + + const varStatments = Object.keys(vars).reduce( + (computed: string, key: string) => + `${computed}const ${key} = ${vars[key]};\n`, + '' + ) + + const preProgramVarStatments = ` +const _webout = ${weboutPath} +const _sasjs_tokenfile = ${tokenFile}; +const _sasjs_username = ${preProgramVariables?.username}; +const _sasjs_userid = ${preProgramVariables?.userId}; +const _sasjs_displayname = ${preProgramVariables?.displayName}; +const _metaperson = _sasjs_displayname; +const _metauser = _sasjs_username; +const sasjsprocessmode = 'Stored Program'; +` + + program = ` +/* runtime vars */ +${varStatments} + +/* dynamic user-provided vars */ +${preProgramVarStatments} + +/* actual job code */ +${program}` + + // todo: modify this commented block for js runtime + // if no files are uploaded filesNamesMap will be undefined + // if (otherArgs?.filesNamesMap) { + // const uploadSasCode = await generateFileUploadSasCode( + // otherArgs.filesNamesMap, + // session.path + // ) + + // //If sas code for the file is generated it will be appended to the top of sasCode + // if (uploadSasCode.length > 0) { + // program = `${uploadSasCode}` + program + // } + // } + + const codePath = path.join(session.path, 'code.sas') + await createFile(codePath, program) + + // todo: execute code.js file + + const log = (await fileExists(logPath)) ? await readFile(logPath) : '' + const headersContent = (await fileExists(headersPath)) + ? await readFile(headersPath) + : '' + const httpHeaders: HTTPHeaders = extractHeaders(headersContent) + const fileResponse: boolean = + httpHeaders.hasOwnProperty('content-type') && + !returnJson && // not a POST Request + !isDebugOn(vars) // Debug is not enabled + + const webout = (await fileExists(weboutPath)) + ? fileResponse + ? await readFileBinary(weboutPath) + : await readFile(weboutPath) + : '' + + if (returnJson) { + return { + httpHeaders, + webout, + log: isDebugOn(vars) ? log : undefined + } + } + + return { + httpHeaders, + result: isDebugOn(vars) + ? `${webout}

SAS Log

${log}
` + : webout + } + } + buildDirectoryTree() { const root: TreeNode = { name: 'files', From f561ba4bf09e4836d8d94128022b6817e91ff80e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sat, 4 Jun 2022 03:12:51 +0500 Subject: [PATCH 05/31] chore: documented sasjs_runtimes env variable in readme file --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index a3a2024..615c95b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,15 @@ SAS_PATH=/path/to/sas/executable.exe # This location is for SAS WORK, staged files, DRIVE, configuration etc SASJS_ROOT=./sasjs_root +# A comma separated string that defines the available runTimes. +# Priority is given to the runtime that comes first in string. +# Possible options at the moment are sas and js + +# options: [sas,js|js,sas|sas|js] default:sas +SASJS_RUNTIMES= + + + # options: [http|https] default: http PROTOCOL= From 596ada7ca88798d6d71f6845633a006fd22438ea Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Sat, 4 Jun 2022 03:16:07 +0500 Subject: [PATCH 06/31] feat: validate sasjs_runtimes env var --- api/src/utils/verifyEnvVariables.ts | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index 9d82069..469c90f 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -26,6 +26,11 @@ export enum LOG_FORMAT_MORGANType { tiny = 'tiny' } +export enum SASJSRunTimes { + SAS = 'sas', + JS = 'js' +} + export enum ReturnCode { Success, InvalidEnv @@ -46,6 +51,8 @@ export const verifyEnvVariables = (): ReturnCode => { errors.push(...verifyLOG_FORMAT_MORGAN()) + errors.push(...verifySASJSRunTimes()) + if (errors.length) { process.logger?.error( `Invalid environment variable(s) provided: \n${errors.join('\n')}` @@ -202,10 +209,35 @@ const verifyLOG_FORMAT_MORGAN = (): string[] => { return errors } +const verifySASJSRunTimes = (): string[] => { + const errors: string[] = [] + const { SASJS_RUNTIMES } = process.env + + if (SASJS_RUNTIMES) { + const runTimes = SASJS_RUNTIMES.split(',').map((runTime) => + runTime.toLowerCase() + ) + + const possibleRunTimes = Object.values(SASJSRunTimes) + + runTimes.forEach((runTime) => { + if (!possibleRunTimes.includes(runTime as SASJSRunTimes)) { + errors.push( + `- Invalid '${runTime}' runtime\n - valid options ${possibleRunTimes}` + ) + } + }) + } else { + process.env.SASJS_RUNTIMES = DEFAULTS.SASJS_RUNTIMES + } + return errors +} + const DEFAULTS = { MODE: ModeType.Desktop, PROTOCOL: ProtocolType.HTTP, PORT: '5000', HELMET_COEP: HelmetCoepType.TRUE, - LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common + LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common, + SASJS_RUNTIMES: SASJSRunTimes.SAS } From e5a7674fa1a09d043fec93af54a6af5eb2a7232a Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 6 Jun 2022 09:09:21 +0500 Subject: [PATCH 07/31] chore: refactor ExecutionController class to remove code duplications --- api/src/controllers/code.ts | 2 +- api/src/controllers/internal/Execution.ts | 255 +++++++++++----------- 2 files changed, 130 insertions(+), 127 deletions(-) diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index bff85bb..a4b1d74 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -46,7 +46,7 @@ const executeSASCode = async ( try { const { webout, log, httpHeaders } = - (await new ExecutionController().executeSASProgram( + (await new ExecutionController().executeProgram( code, getPreProgramVariables(req), { ...req.query, _debug: 131 }, diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 4ec2486..b73d8dd 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -1,5 +1,6 @@ import path from 'path' import fs from 'fs' +import { execFileSync } from 'child_process' import { getSASSessionController, getJSSessionController } from './' import { readFile, @@ -15,7 +16,8 @@ import { getFilesFolder, getMacrosFolder, HTTPHeaders, - isDebugOn + isDebugOn, + SASJSRunTimes } from '../../utils' export interface ExecutionVars { @@ -50,45 +52,53 @@ export class ExecutionController { const codePath = path .join(getFilesFolder(), programPath) - .replace(new RegExp('/', 'g'), path.sep) + runTime - - if (await fileExists(programPath)) { + .replace(new RegExp('/', 'g'), path.sep) + + '.' + + runTime + if (await fileExists(codePath)) { const program = await readFile(codePath) if (runTime === 'sas') { - return this.executeSASProgram( + return this.executeProgram( program, preProgramVariables, vars, otherArgs, returnJson, - session + session, + runTime ) } else if (runTime === 'js') { - return this.executeJSProgram( + return this.executeProgram( program, preProgramVariables, vars, otherArgs, - returnJson + returnJson, + session, + runTime ) } else { throw `${runTime} runtime is not implemented yet.` } } } - throw 'ExecutionController: Program file does not exist.' + throw `ExecutionController: ${programPath} file does not exist.` } - async executeSASProgram( + async executeProgram( program: string, preProgramVariables: PreProgramVars, vars: ExecutionVars, otherArgs?: any, returnJson?: boolean, - sessionByFileUpload?: Session + sessionByFileUpload?: Session, + runTime: string = 'sas' ): Promise { - const sessionController = getSASSessionController() + const sessionController = + runTime === SASJSRunTimes.JS + ? getJSSessionController() + : getSASSessionController() const session = sessionByFileUpload ?? (await sessionController.getSession()) @@ -106,6 +116,93 @@ export class ExecutionController { preProgramVariables?.httpHeaders.join('\n') ?? '' ) + if (runTime === SASJSRunTimes.JS) { + program = await this.createJSProgram( + program, + preProgramVariables, + vars, + weboutPath, + tokenFile + ) + + const codePath = path.join(session.path, 'code.js') + + await createFile(codePath, program) + + execFileSync('node', [codePath]) + } 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 headersContent = (await fileExists(headersPath)) + ? await readFile(headersPath) + : '' + const httpHeaders: HTTPHeaders = extractHeaders(headersContent) + const fileResponse: boolean = + httpHeaders.hasOwnProperty('content-type') && + !returnJson && // not a POST Request + !isDebugOn(vars) // Debug is not enabled + + const webout = (await fileExists(weboutPath)) + ? fileResponse + ? await readFileBinary(weboutPath) + : await readFile(weboutPath) + : '' + + // it should be deleted by scheduleSessionDestroy + session.inUse = false + + if (returnJson) { + return { + httpHeaders, + webout, + log: isDebugOn(vars) || session.crashed ? log : undefined + } + } + + return { + httpHeaders, + result: + isDebugOn(vars) || session.crashed + ? `${webout}

SAS Log

${log}
` + : webout + } + } + + private async createSASProgram( + program: string, + preProgramVariables: PreProgramVars, + vars: ExecutionVars, + session: Session, + weboutPath: string, + tokenFile: string, + otherArgs?: any + ) { const varStatments = Object.keys(vars).reduce( (computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`, @@ -161,100 +258,37 @@ ${program}` program = `${uploadSasCode}` + program } } - - 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 headersContent = (await fileExists(headersPath)) - ? await readFile(headersPath) - : '' - const httpHeaders: HTTPHeaders = extractHeaders(headersContent) - const fileResponse: boolean = - httpHeaders.hasOwnProperty('content-type') && - !returnJson && // not a POST Request - !isDebugOn(vars) // Debug is not enabled - - const webout = (await fileExists(weboutPath)) - ? fileResponse - ? await readFileBinary(weboutPath) - : await readFile(weboutPath) - : '' - - // it should be deleted by scheduleSessionDestroy - session.inUse = false - - if (returnJson) { - return { - httpHeaders, - webout, - log: isDebugOn(vars) || session.crashed ? log : undefined - } - } - - return { - httpHeaders, - result: - isDebugOn(vars) || session.crashed - ? `${webout}

SAS Log

${log}
` - : webout - } + return program } - async executeJSProgram( + private async createJSProgram( program: string, preProgramVariables: PreProgramVars, vars: ExecutionVars, - otherArgs?: any, - returnJson?: boolean, - sessionByFileUpload?: Session - ): Promise { - const sessionController = getJSSessionController() - - const session = - sessionByFileUpload ?? (await sessionController.getSession()) - - const logPath = path.join(session.path, 'log.log') - const headersPath = path.join(session.path, 'stpsrv_header.txt') - const weboutPath = path.join(session.path, 'webout.txt') - const tokenFile = path.join(session.path, 'reqHeaders.txt') - - await createFile( - tokenFile, - preProgramVariables?.httpHeaders.join('\n') ?? '' - ) - + weboutPath: string, + tokenFile: string + ) { const varStatments = Object.keys(vars).reduce( (computed: string, key: string) => - `${computed}const ${key} = ${vars[key]};\n`, + `${computed}const ${key} = '${vars[key]}';\n`, '' ) const preProgramVarStatments = ` -const _webout = ${weboutPath} -const _sasjs_tokenfile = ${tokenFile}; -const _sasjs_username = ${preProgramVariables?.username}; -const _sasjs_userid = ${preProgramVariables?.userId}; -const _sasjs_displayname = ${preProgramVariables?.displayName}; +const weboutPath = '${weboutPath}'; +const _sasjs_tokenfile = '${tokenFile}'; +const _sasjs_username = '${preProgramVariables?.username}'; +const _sasjs_userid = '${preProgramVariables?.userId}'; +const _sasjs_displayname = '${preProgramVariables?.displayName}'; const _metaperson = _sasjs_displayname; const _metauser = _sasjs_username; const sasjsprocessmode = 'Stored Program'; ` program = ` +/*require module for writing webout file*/ +const fs = require('fs-extra') + /* runtime vars */ ${varStatments} @@ -262,7 +296,11 @@ ${varStatments} ${preProgramVarStatments} /* actual job code */ -${program}` +${program} + +/* write webout file*/ +fs.promises.writeFile(weboutPath, JSON.stringify(webout)) +` // todo: modify this commented block for js runtime // if no files are uploaded filesNamesMap will be undefined @@ -277,42 +315,7 @@ ${program}` // program = `${uploadSasCode}` + program // } // } - - const codePath = path.join(session.path, 'code.sas') - await createFile(codePath, program) - - // todo: execute code.js file - - const log = (await fileExists(logPath)) ? await readFile(logPath) : '' - const headersContent = (await fileExists(headersPath)) - ? await readFile(headersPath) - : '' - const httpHeaders: HTTPHeaders = extractHeaders(headersContent) - const fileResponse: boolean = - httpHeaders.hasOwnProperty('content-type') && - !returnJson && // not a POST Request - !isDebugOn(vars) // Debug is not enabled - - const webout = (await fileExists(weboutPath)) - ? fileResponse - ? await readFileBinary(weboutPath) - : await readFile(weboutPath) - : '' - - if (returnJson) { - return { - httpHeaders, - webout, - log: isDebugOn(vars) ? log : undefined - } - } - - return { - httpHeaders, - result: isDebugOn(vars) - ? `${webout}

SAS Log

${log}
` - : webout - } + return program } buildDirectoryTree() { From b4443819d42afecebc0f382c58afb9010d4775ef Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 6 Jun 2022 15:19:39 +0500 Subject: [PATCH 08/31] fix: refactor code for session selection in preUploadMiddleware function --- .../internal/FileUploadController.ts | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index 9fcf734..9758aea 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -1,9 +1,9 @@ import path from 'path' import { Request, RequestHandler } from 'express' import multer from 'multer' -import { uuidv4, fileExists, readFile } from '@sasjs/utils' +import { uuidv4, fileExists } from '@sasjs/utils' import { getSASSessionController, getJSSessionController } from '.' -import { getFilesFolder } from '../../utils' +import { getFilesFolder, SASJSRunTimes } from '../../utils' export class FileUploadController { private storage = multer.diskStorage({ @@ -22,22 +22,22 @@ export class FileUploadController { //It will intercept request and generate unique uuid to be used as a subfolder name //that will store the files uploaded public preUploadMiddleware: RequestHandler = async (req, res, next) => { - if (process.runTimes.length === 0) { - throw 'No runtime is specified in environment variables.' - } - const programPath = req.query._program as string for (const runTime of process.runTimes) { const codePath = path .join(getFilesFolder(), programPath) - .replace(new RegExp('/', 'g'), path.sep) + runTime + .replace(new RegExp('/', 'g'), path.sep) + + '.' + + runTime - if (await fileExists(programPath)) { - const program = await readFile(codePath) - - if (runTime === 'sas') { + if (await fileExists(codePath)) { + if (runTime === SASJSRunTimes.JS) { + const sessionController = getJSSessionController() + const session = await sessionController.getSession() + req.sasjsSession = session + } else { const sessionController = getSASSessionController() const session = await sessionController.getSession() // marking consumed true, so that it's not available @@ -45,12 +45,6 @@ export class FileUploadController { session.consumed = true req.sasjsSession = session - } else if (runTime === 'js') { - const sessionController = getJSSessionController() - const session = await sessionController.getSession() - req.sasjsSession = session - } else { - throw `${runTime} runtime is not implemented yet.` } } } From dffe6d7121d569e5c7d13023c6ca68d8c901c88e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 6 Jun 2022 15:23:42 +0500 Subject: [PATCH 09/31] fix: refactor code in executeFile method of session controller --- api/src/controllers/internal/Execution.ts | 26 +++++++++-------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index b73d8dd..29210af 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -44,10 +44,6 @@ export class ExecutionController { returnJson?: boolean, session?: Session ) { - if (process.runTimes.length === 0) { - throw 'No runtime is specified in environment variables.' - } - for (const runTime of process.runTimes) { const codePath = path @@ -58,17 +54,7 @@ export class ExecutionController { if (await fileExists(codePath)) { const program = await readFile(codePath) - if (runTime === 'sas') { - return this.executeProgram( - program, - preProgramVariables, - vars, - otherArgs, - returnJson, - session, - runTime - ) - } else if (runTime === 'js') { + if (runTime === SASJSRunTimes.JS) { return this.executeProgram( program, preProgramVariables, @@ -79,7 +65,15 @@ export class ExecutionController { runTime ) } else { - throw `${runTime} runtime is not implemented yet.` + return this.executeProgram( + program, + preProgramVariables, + vars, + otherArgs, + returnJson, + session, + runTime + ) } } } From 6d6bda56267babde7b98cf69e32973d56d719f75 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 6 Jun 2022 17:23:09 +0500 Subject: [PATCH 10/31] fix: refactor code in preUploadMiddleware function --- .../internal/FileUploadController.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index 9758aea..4e4b832 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -33,19 +33,18 @@ export class FileUploadController { runTime if (await fileExists(codePath)) { + let sessionController if (runTime === SASJSRunTimes.JS) { - const sessionController = getJSSessionController() - const session = await sessionController.getSession() - req.sasjsSession = session + sessionController = getJSSessionController() } else { - const sessionController = getSASSessionController() - const session = await sessionController.getSession() - // marking consumed true, so that it's not available - // as readySession for any other request - session.consumed = true - - req.sasjsSession = session + sessionController = getSASSessionController() } + const session = await sessionController.getSession() + // marking consumed true, so that it's not available + // as readySession for any other request + session.consumed = true + req.sasjsSession = session + break } } From 2c704a544f4e31a8e8e833a9a62ba016bcfa6c7c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 6 Jun 2022 17:24:19 +0500 Subject: [PATCH 11/31] fix: refactor sas/js session controller classes to inherit from base session controller class --- api/src/controllers/internal/Session.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index 4f7eac4..c1d5d40 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -17,12 +17,14 @@ import { const execFilePromise = promisify(execFile) -export class SASSessionController { - private sessions: Session[] = [] +abstract class SessionController { + protected sessions: Session[] = [] - private getReadySessions = (): Session[] => + protected getReadySessions = (): Session[] => this.sessions.filter((sess: Session) => sess.ready && !sess.consumed) + protected abstract createSession(): Promise + public async getSession() { const readySessions = this.getReadySessions() @@ -34,8 +36,10 @@ export class SASSessionController { return session } +} - private async createSession(): Promise { +export class SASSessionController extends SessionController { + protected async createSession(): Promise { const sessionId = generateUniqueFileName(generateTimestamp()) const sessionFolder = path.join(getSessionsFolder(), sessionId) @@ -152,8 +156,8 @@ ${autoExecContent}` } } -export class JSSessionController { - public async getSession() { +export class JSSessionController extends SessionController { + protected async createSession(): Promise { const sessionId = generateUniqueFileName(generateTimestamp()) const sessionFolder = path.join(getSessionsFolder(), sessionId) @@ -179,6 +183,7 @@ export class JSSessionController { const headersPath = path.join(session.path, 'stpsrv_header.txt') await createFile(headersPath, 'Content-type: application/json') + this.sessions.push(session) return session } } From 9d5a5e051fd821295664ddb3a1fd64629894a44c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 7 Jun 2022 13:27:18 +0500 Subject: [PATCH 12/31] fix: no need to stringify _webout in preProgramVarStatements, developer should have _webout as string in actual code --- api/src/controllers/internal/Execution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 29210af..1df3703 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -293,7 +293,7 @@ ${preProgramVarStatments} ${program} /* write webout file*/ -fs.promises.writeFile(weboutPath, JSON.stringify(webout)) +fs.promises.writeFile(weboutPath, _webout) ` // todo: modify this commented block for js runtime From 058b3b00816e582e143953c2f0b8330bde2181b8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 8 Jun 2022 02:01:31 +0500 Subject: [PATCH 13/31] feat: configure child process with writeStream to write logs to log file --- api/src/controllers/internal/Execution.ts | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 1df3703..b246a20 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -1,6 +1,7 @@ import path from 'path' import fs from 'fs' import { execFileSync } from 'child_process' +import { once } from 'stream' import { getSASSessionController, getJSSessionController } from './' import { readFile, @@ -121,9 +122,29 @@ export class ExecutionController { const codePath = path.join(session.path, 'code.js') - await createFile(codePath, program) + try { + await createFile(codePath, program) - execFileSync('node', [codePath]) + // 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, From 16856165fb292dc9ffa897189ba105bd9f362267 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 9 Jun 2022 14:54:11 +0500 Subject: [PATCH 14/31] feat: create and inject code for uploaded files to code.js --- api/src/controllers/internal/Execution.ts | 38 ++++++++++++----------- api/src/utils/upload.ts | 34 +++++++++++++++++++- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index b246a20..50620c1 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -14,6 +14,7 @@ import { PreProgramVars, Session, TreeNode } from '../../types' import { extractHeaders, generateFileUploadSasCode, + generateFileUploadJSCode, getFilesFolder, getMacrosFolder, HTTPHeaders, @@ -116,8 +117,10 @@ export class ExecutionController { program, preProgramVariables, vars, + session, weboutPath, - tokenFile + tokenFile, + otherArgs ) const codePath = path.join(session.path, 'code.js') @@ -280,8 +283,10 @@ ${program}` program: string, preProgramVariables: PreProgramVars, vars: ExecutionVars, + session: Session, weboutPath: string, - tokenFile: string + tokenFile: string, + otherArgs?: any ) { const varStatments = Object.keys(vars).reduce( (computed: string, key: string) => @@ -300,10 +305,9 @@ const _metauser = _sasjs_username; const sasjsprocessmode = 'Stored Program'; ` - program = ` -/*require module for writing webout file*/ -const fs = require('fs-extra') + const requiredModules = `const fs = require('fs-extra')` + program = ` /* runtime vars */ ${varStatments} @@ -316,21 +320,19 @@ ${program} /* write webout file*/ fs.promises.writeFile(weboutPath, _webout) ` - - // todo: modify this commented block for js runtime // if no files are uploaded filesNamesMap will be undefined - // if (otherArgs?.filesNamesMap) { - // const uploadSasCode = await generateFileUploadSasCode( - // otherArgs.filesNamesMap, - // session.path - // ) + if (otherArgs?.filesNamesMap) { + const uploadJSCode = await generateFileUploadJSCode( + otherArgs.filesNamesMap, + session.path + ) - // //If sas code for the file is generated it will be appended to the top of sasCode - // if (uploadSasCode.length > 0) { - // program = `${uploadSasCode}` + program - // } - // } - return program + //If js code for the file is generated it will be appended to the top of jsCode + if (uploadJSCode.length > 0) { + program = `${uploadJSCode}\n` + program + } + } + return requiredModules + program } buildDirectoryTree() { diff --git a/api/src/utils/upload.ts b/api/src/utils/upload.ts index 640464c..cc9a47f 100644 --- a/api/src/utils/upload.ts +++ b/api/src/utils/upload.ts @@ -1,5 +1,6 @@ +import path from 'path' import { MulterFile } from '../types/Upload' -import { listFilesInFolder } from '@sasjs/utils' +import { listFilesInFolder, readFileBinary } from '@sasjs/utils' interface FilenameMapSingle { fieldName: string @@ -98,3 +99,34 @@ export const generateFileUploadSasCode = async ( return uploadSasCode } + +/** + * Generates the js code that references uploaded files in the concurrent request + * @param filesNamesMap object that maps hashed file names and original file names + * @param sessionFolder name of the folder that is created for the purpose of files in concurrent request + * @returns generated js code + */ +export const generateFileUploadJSCode = async ( + filesNamesMap: FilenamesMap, + sessionFolder: string +) => { + let uploadCode = '' + let fileCount = 0 + + const sessionFolderList: string[] = await listFilesInFolder(sessionFolder) + sessionFolderList.forEach(async (fileName) => { + if (fileName.includes('req_file')) { + fileCount++ + const filePath = path.join(sessionFolder, fileName) + uploadCode += `\nconst _WEBIN_FILEREF${fileCount} = fs.readFileSync('${filePath}')` + uploadCode += `\nconst _WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'` + uploadCode += `\nconst _WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'` + } + }) + + if (fileCount) { + uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode + } + + return uploadCode +} From de9ed15286127f3755168afd2ca6dc61cb1de54a Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 13 Jun 2022 20:51:44 +0500 Subject: [PATCH 15/31] chore: update error message when stored program not found --- api/src/controllers/internal/Execution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index dff5efc..0391f7f 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -79,7 +79,7 @@ export class ExecutionController { } } } - throw `The Stored Program at "${programPath}" does not exist, or you do not have permission to view it.` + throw `ExecutionController: The Stored Program at "${programPath}" does not exist, or you do not have permission to view it.` } async executeProgram( From c830f44e2977703150f17b436287e48d6ea3cdde Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 14 Jun 2022 16:48:58 +0500 Subject: [PATCH 16/31] chore: code fixes --- api/src/controllers/internal/Execution.ts | 497 +++++++++--------- .../internal/FileUploadController.ts | 50 +- api/src/controllers/stp.ts | 37 +- api/src/routes/api/stp.ts | 11 +- api/src/types/system/process.d.ts | 2 +- api/src/utils/getRunTimeAndFilePath.ts | 18 + api/src/utils/index.ts | 1 + api/src/utils/setProcessVariables.ts | 13 +- api/src/utils/verifyEnvVariables.ts | 24 +- 9 files changed, 343 insertions(+), 310 deletions(-) create mode 100644 api/src/utils/getRunTimeAndFilePath.ts diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 0391f7f..0bc80a7 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -19,7 +19,7 @@ import { getMacrosFolder, HTTPHeaders, isDebugOn, - SASJSRunTimes + RunTimeType } from '../../utils' export interface ExecutionVars { @@ -37,64 +37,56 @@ export interface ExecuteReturnJson { log?: string } -export class ExecutionController { - async executeFile( - programPath: string, - preProgramVariables: PreProgramVars, - vars: ExecutionVars, - otherArgs?: any, - returnJson?: boolean, - session?: Session - ) { - 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) +interface ExecuteFileParams { + programPath: string + preProgramVariables: PreProgramVars + vars: ExecutionVars + otherArgs?: any + returnJson?: boolean + session?: Session + runTime: RunTimeType +} - if (runTime === SASJSRunTimes.JS) { - return this.executeProgram( - program, - preProgramVariables, - vars, - otherArgs, - returnJson, - session, - runTime - ) - } else { - return this.executeProgram( - program, - preProgramVariables, - vars, - otherArgs, - returnJson, - session, - runTime - ) - } - } - } - throw `ExecutionController: The Stored Program at "${programPath}" does not exist, or you do not have permission to view it.` +interface ExecuteProgramParams extends Omit { + program: string +} + +export class ExecutionController { + async executeFile({ + programPath, + preProgramVariables, + vars, + otherArgs, + returnJson, + session, + runTime + }: ExecuteFileParams) { + const program = await readFile(programPath) + + return this.executeProgram({ + program, + preProgramVariables, + vars, + otherArgs, + returnJson, + session, + runTime + }) } - async executeProgram( - program: string, - preProgramVariables: PreProgramVars, - vars: ExecutionVars, - otherArgs?: any, - returnJson?: boolean, - sessionByFileUpload?: Session, - runTime: string = 'sas' - ): Promise { + async executeProgram({ + program, + preProgramVariables, + vars, + otherArgs, + returnJson, + session: sessionByFileUpload, + runTime + }: ExecuteProgramParams): Promise { const sessionController = - runTime === SASJSRunTimes.JS - ? getJSSessionController() - : getSASSessionController() + runTime === RunTimeType.SAS + ? getSASSessionController() + : getJSSessionController() const session = sessionByFileUpload ?? (await sessionController.getSession()) @@ -112,69 +104,17 @@ export class ExecutionController { preProgramVariables?.httpHeaders.join('\n') ?? '' ) - if (runTime === SASJSRunTimes.JS) { - program = await this.createJSProgram( - program, - preProgramVariables, - vars, - session, - weboutPath, - tokenFile, - 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) - } - } + await processProgram( + program, + preProgramVariables, + vars, + session, + weboutPath, + tokenFile, + runTime, + logPath, + otherArgs + ) const log = (await fileExists(logPath)) ? await readFile(logPath) : '' const headersContent = (await fileExists(headersPath)) @@ -212,129 +152,6 @@ export class ExecutionController { } } - private async createSASProgram( - program: string, - preProgramVariables: PreProgramVars, - vars: ExecutionVars, - session: Session, - weboutPath: string, - tokenFile: string, - otherArgs?: any - ) { - const varStatments = Object.keys(vars).reduce( - (computed: string, key: string) => - `${computed}%let ${key}=${vars[key]};\n`, - '' - ) - - const preProgramVarStatments = ` -%let _sasjs_tokenfile=${tokenFile}; -%let _sasjs_username=${preProgramVariables?.username}; -%let _sasjs_userid=${preProgramVariables?.userId}; -%let _sasjs_displayname=${preProgramVariables?.displayName}; -%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl}; -%let _sasjs_apipath=/SASjsApi/stp/execute; -%let _metaperson=&_sasjs_displayname; -%let _metauser=&_sasjs_username; -%let sasjsprocessmode=Stored Program; -%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt; - -%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG; -%macro _sasjs_server_init(); - %if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode; - %if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl; -%mend; -%_sasjs_server_init() -` - - program = ` -options insert=(SASAUTOS="${getMacrosFolder()}"); - -/* runtime vars */ -${varStatments} -filename _webout "${weboutPath}" mod; - -/* dynamic user-provided vars */ -${preProgramVarStatments} - -/* user autoexec starts */ -${otherArgs?.userAutoExec ?? ''} -/* user autoexec ends */ - -/* actual job code */ -${program}` - - // if no files are uploaded filesNamesMap will be undefined - if (otherArgs?.filesNamesMap) { - const uploadSasCode = await generateFileUploadSasCode( - otherArgs.filesNamesMap, - session.path - ) - - //If sas code for the file is generated it will be appended to the top of sasCode - if (uploadSasCode.length > 0) { - program = `${uploadSasCode}` + program - } - } - return program - } - - private async createJSProgram( - program: string, - preProgramVariables: PreProgramVars, - vars: ExecutionVars, - session: Session, - weboutPath: string, - tokenFile: string, - otherArgs?: any - ) { - const varStatments = Object.keys(vars).reduce( - (computed: string, key: string) => - `${computed}const ${key} = '${vars[key]}';\n`, - '' - ) - - const preProgramVarStatments = ` -const weboutPath = '${weboutPath}'; -const _sasjs_tokenfile = '${tokenFile}'; -const _sasjs_username = '${preProgramVariables?.username}'; -const _sasjs_userid = '${preProgramVariables?.userId}'; -const _sasjs_displayname = '${preProgramVariables?.displayName}'; -const _metaperson = _sasjs_displayname; -const _metauser = _sasjs_username; -const sasjsprocessmode = 'Stored Program'; -` - - const requiredModules = `const fs = require('fs-extra')` - - program = ` -/* runtime vars */ -${varStatments} - -/* dynamic user-provided vars */ -${preProgramVarStatments} - -/* actual job code */ -${program} - -/* write webout file*/ -fs.promises.writeFile(weboutPath, _webout) -` - // if no files are uploaded filesNamesMap will be undefined - if (otherArgs?.filesNamesMap) { - const uploadJSCode = await generateFileUploadJSCode( - otherArgs.filesNamesMap, - session.path - ) - - //If js code for the file is generated it will be appended to the top of jsCode - if (uploadJSCode.length > 0) { - program = `${uploadJSCode}\n` + program - } - } - return requiredModules + program - } - buildDirectoryTree() { const root: TreeNode = { name: 'files', @@ -374,3 +191,201 @@ fs.promises.writeFile(weboutPath, _webout) } const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + +const createSASProgram = async ( + program: string, + preProgramVariables: PreProgramVars, + vars: ExecutionVars, + session: Session, + weboutPath: string, + tokenFile: string, + otherArgs?: any +) => { + const varStatments = Object.keys(vars).reduce( + (computed: string, key: string) => `${computed}%let ${key}=${vars[key]};\n`, + '' + ) + + const preProgramVarStatments = ` +%let _sasjs_tokenfile=${tokenFile}; +%let _sasjs_username=${preProgramVariables?.username}; +%let _sasjs_userid=${preProgramVariables?.userId}; +%let _sasjs_displayname=${preProgramVariables?.displayName}; +%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl}; +%let _sasjs_apipath=/SASjsApi/stp/execute; +%let _metaperson=&_sasjs_displayname; +%let _metauser=&_sasjs_username; +%let sasjsprocessmode=Stored Program; +%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt; + +%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG; +%macro _sasjs_server_init(); +%if "&SYSPROCESSMODE"="" %then %let SYSPROCESSMODE=&sasjsprocessmode; +%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl; +%mend; +%_sasjs_server_init() +` + + program = ` +options insert=(SASAUTOS="${getMacrosFolder()}"); + +/* runtime vars */ +${varStatments} +filename _webout "${weboutPath}" mod; + +/* dynamic user-provided vars */ +${preProgramVarStatments} + +/* user autoexec starts */ +${otherArgs?.userAutoExec ?? ''} +/* user autoexec ends */ + +/* actual job code */ +${program}` + + // if no files are uploaded filesNamesMap will be undefined + if (otherArgs?.filesNamesMap) { + const uploadSasCode = await generateFileUploadSasCode( + otherArgs.filesNamesMap, + session.path + ) + + //If sas code for the file is generated it will be appended to the top of sasCode + if (uploadSasCode.length > 0) { + program = `${uploadSasCode}` + program + } + } + return program +} + +const createJSProgram = async ( + program: string, + preProgramVariables: PreProgramVars, + vars: ExecutionVars, + session: Session, + weboutPath: string, + tokenFile: string, + otherArgs?: any +) => { + const varStatments = Object.keys(vars).reduce( + (computed: string, key: string) => + `${computed}const ${key} = '${vars[key]}';\n`, + '' + ) + + const preProgramVarStatments = ` +const weboutPath = '${weboutPath}'; +const _sasjs_tokenfile = '${tokenFile}'; +const _sasjs_username = '${preProgramVariables?.username}'; +const _sasjs_userid = '${preProgramVariables?.userId}'; +const _sasjs_displayname = '${preProgramVariables?.displayName}'; +const _metaperson = _sasjs_displayname; +const _metauser = _sasjs_username; +const sasjsprocessmode = 'Stored Program'; +` + + const requiredModules = `const fs = require('fs-extra')` + + program = ` +/* runtime vars */ +${varStatments} + +/* dynamic user-provided vars */ +${preProgramVarStatments} + +/* actual job code */ +${program} + +/* write webout file*/ +fs.promises.writeFile(weboutPath, _webout) +` + // if no files are uploaded filesNamesMap will be undefined + if (otherArgs?.filesNamesMap) { + const uploadJSCode = await generateFileUploadJSCode( + otherArgs.filesNamesMap, + session.path + ) + + //If js code for the file is generated it will be appended to the top of jsCode + if (uploadJSCode.length > 0) { + program = `${uploadJSCode}\n` + program + } + } + return requiredModules + program +} + +const processProgram = async ( + program: string, + preProgramVariables: PreProgramVars, + vars: ExecutionVars, + session: Session, + 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 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 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) + } + } +} diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index 4e4b832..d267d43 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -1,9 +1,12 @@ -import path from 'path' import { Request, RequestHandler } from 'express' import multer from 'multer' -import { uuidv4, fileExists } from '@sasjs/utils' +import { uuidv4 } from '@sasjs/utils' import { getSASSessionController, getJSSessionController } from '.' -import { getFilesFolder, SASJSRunTimes } from '../../utils' +import { + executeProgramRawValidation, + getRunTimeAndFilePath, + RunTimeType +} from '../../utils' export class FileUploadController { 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 //that will store the files uploaded 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) { - const codePath = - path - .join(getFilesFolder(), programPath) - .replace(new RegExp('/', 'g'), path.sep) + - '.' + - runTime + if (errQ && errB) return res.status(400).send(errB.details[0].message) - if (await fileExists(codePath)) { - let sessionController - if (runTime === SASJSRunTimes.JS) { - sessionController = getJSSessionController() - } else { - sessionController = getSASSessionController() - } - const session = await sessionController.getSession() - // marking consumed true, so that it's not available - // as readySession for any other request - session.consumed = true - req.sasjsSession = session - break - } - } + const programPath = (query?._program ?? body?._program) as string + + const { runTime } = await getRunTimeAndFilePath(programPath) + + const sessionController = + runTime === RunTimeType.SAS + ? getSASSessionController() + : getJSSessionController() + + const session = await sessionController.getSession() + // marking consumed true, so that it's not available + // as readySession for any other request + session.consumed = true + + req.sasjsSession = session next() } diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index d3b01bf..624762c 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -1,5 +1,4 @@ import express from 'express' -import path from 'path' import { Request, Security, @@ -19,12 +18,12 @@ import { } from './internal' import { getPreProgramVariables, - getFilesFolder, HTTPHeaders, isDebugOn, LogLine, makeFilesNamesMap, - parseLogToArray + parseLogToArray, + getRunTimeAndFilePath } from '../utils' import { MulterFile } from '../types/Upload' @@ -132,13 +131,16 @@ const executeReturnRaw = async ( ): Promise => { const query = req.query as ExecutionVars + const { codePath, runTime } = await getRunTimeAndFilePath(_program) + try { const { result, httpHeaders } = - (await new ExecutionController().executeFile( - _program, - getPreProgramVariables(req), - query - )) as ExecuteReturnRaw + (await new ExecutionController().executeFile({ + programPath: codePath, + preProgramVariables: getPreProgramVariables(req), + vars: query, + runTime + })) as ExecuteReturnRaw // Should over-ride response header for debug // on GET request to see entire log rendering on browser. @@ -167,20 +169,23 @@ const executeReturnJson = async ( req: express.Request, _program: string ): Promise => { + const { codePath, runTime } = await getRunTimeAndFilePath(_program) + const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files as MulterFile[]) : null try { const { webout, log, httpHeaders } = - (await new ExecutionController().executeFile( - _program, - getPreProgramVariables(req), - { ...req.query, ...req.body }, - { filesNamesMap: filesNamesMap }, - true, - req.sasjsSession - )) as ExecuteReturnJson + (await new ExecutionController().executeFile({ + programPath: codePath, + preProgramVariables: getPreProgramVariables(req), + vars: { ...req.query, ...req.body }, + otherArgs: { filesNamesMap: filesNamesMap }, + returnJson: true, + session: req.sasjsSession, + runTime + })) as ExecuteReturnJson let weboutRes: string | IRecordOfAny = webout if (httpHeaders['content-type']?.toLowerCase() === 'application/json') { diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 5ea37db..858feb5 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -35,16 +35,17 @@ stpRouter.post( fileUploadController.preUploadMiddleware, fileUploadController.getMulterUploadObject().any(), async (req, res: any) => { - const { error: errQ, value: query } = executeProgramRawValidation(req.query) - const { error: errB, value: body } = executeProgramRawValidation(req.body) + // below validations are moved to preUploadMiddleware + // 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 { const response = await controller.executeReturnJson( req, - body, - query?._program + req.body, + req.query?._program as string ) // TODO: investigate if this code is required diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index 2d345c1..e8e83b4 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -1,11 +1,11 @@ declare namespace NodeJS { export interface Process { - runTimes: string[] sasLoc: string driveLoc: string sasSessionController?: import('../../controllers/internal').SASSessionController jsSessionController?: import('../../controllers/internal').JSSessionController appStreamConfig: import('../').AppStreamConfig logger: import('@sasjs/utils/logger').Logger + runTimes: import('../../utils').RunTimeType[] } } diff --git a/api/src/utils/getRunTimeAndFilePath.ts b/api/src/utils/getRunTimeAndFilePath.ts new file mode 100644 index 0000000..f25edf6 --- /dev/null +++ b/api/src/utils/getRunTimeAndFilePath.ts @@ -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.` +} diff --git a/api/src/utils/index.ts b/api/src/utils/index.ts index 09254ba..b520778 100644 --- a/api/src/utils/index.ts +++ b/api/src/utils/index.ts @@ -10,6 +10,7 @@ export * from './generateRefreshToken' export * from './getCertificates' export * from './getDesktopFields' export * from './getPreProgramVariables' +export * from './getRunTimeAndFilePath' export * from './getServerUrl' export * from './instantiateLogger' export * from './isDebugOn' diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 22205b3..43725a3 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -1,7 +1,7 @@ import path from 'path' import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils' -import { getDesktopFields, ModeType } from '.' +import { getDesktopFields, ModeType, RunTimeType } from '.' export const setProcessVariables = async () => { if (process.env.NODE_ENV === 'test') { @@ -19,18 +19,15 @@ export const setProcessVariables = async () => { 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 absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd()) await createFolder(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('sasDrive: ', process.driveLoc) + console.log('runTimes: ', process.runTimes) } diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index 469c90f..e596074 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -26,7 +26,7 @@ export enum LOG_FORMAT_MORGANType { tiny = 'tiny' } -export enum SASJSRunTimes { +export enum RunTimeType { SAS = 'sas', JS = 'js' } @@ -51,7 +51,7 @@ export const verifyEnvVariables = (): ReturnCode => { errors.push(...verifyLOG_FORMAT_MORGAN()) - errors.push(...verifySASJSRunTimes()) + errors.push(...verifyRUN_TIMES()) if (errors.length) { process.logger?.error( @@ -209,26 +209,24 @@ const verifyLOG_FORMAT_MORGAN = (): string[] => { return errors } -const verifySASJSRunTimes = (): string[] => { +const verifyRUN_TIMES = (): string[] => { const errors: string[] = [] - const { SASJS_RUNTIMES } = process.env + const { RUN_TIMES } = process.env - if (SASJS_RUNTIMES) { - const runTimes = SASJS_RUNTIMES.split(',').map((runTime) => - runTime.toLowerCase() - ) + if (RUN_TIMES) { + const runTimes = RUN_TIMES.split(',') - const possibleRunTimes = Object.values(SASJSRunTimes) + const runTimeTypes = Object.values(RunTimeType) runTimes.forEach((runTime) => { - if (!possibleRunTimes.includes(runTime as SASJSRunTimes)) { + if (!runTimeTypes.includes(runTime.toLowerCase() as RunTimeType)) { errors.push( - `- Invalid '${runTime}' runtime\n - valid options ${possibleRunTimes}` + `- Invalid '${runTime}' runtime\n - valid options ${runTimeTypes}` ) } }) } else { - process.env.SASJS_RUNTIMES = DEFAULTS.SASJS_RUNTIMES + process.env.RUN_TIMES = DEFAULTS.RUN_TIMES } return errors } @@ -239,5 +237,5 @@ const DEFAULTS = { PORT: '5000', HELMET_COEP: HelmetCoepType.TRUE, LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common, - SASJS_RUNTIMES: SASJSRunTimes.SAS + RUN_TIMES: RunTimeType.SAS } From 8e7c9e671c4f0fcc383bf1434012647a4f0a9b49 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 14 Jun 2022 17:05:13 +0500 Subject: [PATCH 17/31] chore: quick fix --- api/src/utils/verifyEnvVariables.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index e596074..c046061 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -219,7 +219,7 @@ const verifyRUN_TIMES = (): string[] => { const runTimeTypes = Object.values(RunTimeType) runTimes.forEach((runTime) => { - if (!runTimeTypes.includes(runTime.toLowerCase() as RunTimeType)) { + if (!runTimeTypes.includes(runTime as RunTimeType)) { errors.push( `- Invalid '${runTime}' runtime\n - valid options ${runTimeTypes}` ) From e359265c4be90d245b58ed8da5d31cd6b4bae1ae Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Tue, 14 Jun 2022 17:05:40 +0500 Subject: [PATCH 18/31] chore: quick fix --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 615c95b..523bda9 100644 --- a/README.md +++ b/README.md @@ -65,14 +65,6 @@ SAS_PATH=/path/to/sas/executable.exe # This location is for SAS WORK, staged files, DRIVE, configuration etc SASJS_ROOT=./sasjs_root -# A comma separated string that defines the available runTimes. -# Priority is given to the runtime that comes first in string. -# Possible options at the moment are sas and js - -# options: [sas,js|js,sas|sas|js] default:sas -SASJS_RUNTIMES= - - # options: [http|https] default: http PROTOCOL= @@ -138,6 +130,13 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json # Docs: https://www.npmjs.com/package/morgan#predefined-formats LOG_FORMAT_MORGAN= +# A comma separated string that defines the available runTimes. +# Priority is given to the runtime that comes first in string. +# Possible options at the moment are sas and js + +# options: [sas,js|js,sas|sas|js] default:sas +RUN_TIMES= + ``` ## Persisting the Session From 81501d17abd9adf3e6611635360ae7a0fdb05d61 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Wed, 15 Jun 2022 16:03:04 +0500 Subject: [PATCH 19/31] chore: code fixes --- api/src/controllers/code.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index a4b1d74..11d8ca6 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -6,7 +6,8 @@ import { getPreProgramVariables, getUserAutoExec, ModeType, - parseLogToArray + parseLogToArray, + RunTimeType } from '../utils' interface ExecuteSASCodePayload { @@ -46,13 +47,14 @@ const executeSASCode = async ( try { const { webout, log, httpHeaders } = - (await new ExecutionController().executeProgram( - code, - getPreProgramVariables(req), - { ...req.query, _debug: 131 }, - { userAutoExec }, - true - )) as ExecuteReturnJson + (await new ExecutionController().executeProgram({ + program: code, + preProgramVariables: getPreProgramVariables(req), + vars: { ...req.query, _debug: 131 }, + otherArgs: { userAutoExec }, + returnJson: true, + runTime: RunTimeType.SAS + })) as ExecuteReturnJson return { status: 'success', From 53854d001279462104b24c0e59a8c94ab4938a94 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 15 Jun 2022 16:18:07 +0500 Subject: [PATCH 20/31] fix: code fixes for executing program from program path including file extension --- api/.env.example | 2 + .../internal/FileUploadController.ts | 12 +++++- api/src/controllers/stp.ts | 8 ++-- api/src/utils/getRunTimeAndFilePath.ts | 37 ++++++++++++++----- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/api/.env.example b/api/.env.example index ab6de3e..a21c209 100644 --- a/api/.env.example +++ b/api/.env.example @@ -6,6 +6,8 @@ PROTOCOL=[http|https] default considered as http PRIVATE_KEY=privkey.pem FULL_CHAIN=fullchain.pem +RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas + PORT=[5000] default value is 5000 HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used diff --git a/api/src/controllers/internal/FileUploadController.ts b/api/src/controllers/internal/FileUploadController.ts index d267d43..7c6737c 100644 --- a/api/src/controllers/internal/FileUploadController.ts +++ b/api/src/controllers/internal/FileUploadController.ts @@ -32,7 +32,17 @@ export class FileUploadController { const programPath = (query?._program ?? body?._program) as string - const { runTime } = await getRunTimeAndFilePath(programPath) + let runTime + + try { + ;({ runTime } = await getRunTimeAndFilePath(programPath)) + } catch (err: any) { + res.status(400).send({ + status: 'failure', + message: 'Job execution failed', + error: typeof err === 'object' ? err.toString() : err + }) + } const sessionController = runTime === RunTimeType.SAS diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index 624762c..37e3dad 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -131,9 +131,9 @@ const executeReturnRaw = async ( ): Promise => { const query = req.query as ExecutionVars - const { codePath, runTime } = await getRunTimeAndFilePath(_program) - try { + const { codePath, runTime } = await getRunTimeAndFilePath(_program) + const { result, httpHeaders } = (await new ExecutionController().executeFile({ programPath: codePath, @@ -169,13 +169,13 @@ const executeReturnJson = async ( req: express.Request, _program: string ): Promise => { - const { codePath, runTime } = await getRunTimeAndFilePath(_program) - const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files as MulterFile[]) : null try { + const { codePath, runTime } = await getRunTimeAndFilePath(_program) + const { webout, log, httpHeaders } = (await new ExecutionController().executeFile({ programPath: codePath, diff --git a/api/src/utils/getRunTimeAndFilePath.ts b/api/src/utils/getRunTimeAndFilePath.ts index f25edf6..e36c931 100644 --- a/api/src/utils/getRunTimeAndFilePath.ts +++ b/api/src/utils/getRunTimeAndFilePath.ts @@ -1,18 +1,37 @@ import path from 'path' import { fileExists } from '@sasjs/utils' import { getFilesFolder } from './file' +import { RunTimeType } from '.' 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 + const ext = path.extname(programPath) + // if program path is provided with extension we should split that into code path and ext as run time + if (ext) { + const runTime = ext.slice(1) + const runTimeTypes = Object.values(RunTimeType) - if (await fileExists(codePath)) return { codePath, runTime } + if (!runTimeTypes.includes(runTime as RunTimeType)) { + throw `The '${runTime}' runtime is not supported.` + } + + const codePath = path + .join(getFilesFolder(), programPath) + .replace(new RegExp('/', 'g'), path.sep) + + if (await fileExists(codePath)) { + return { codePath, runTime: runTime as RunTimeType } + } + } else { + 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.` } From 1790e10fc1f9a6bdd5eb924b5396bd54ab0ddaf6 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 16 Jun 2022 22:14:47 +0500 Subject: [PATCH 21/31] chore: code fixes --- api/src/controllers/internal/Execution.ts | 6 ++++-- web/src/containers/Drive/main.tsx | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 0bc80a7..e6d6b34 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -284,7 +284,7 @@ const _metauser = _sasjs_username; const sasjsprocessmode = 'Stored Program'; ` - const requiredModules = `const fs = require('fs-extra')` + const requiredModules = `const fs = require('fs')` program = ` /* runtime vars */ @@ -297,7 +297,9 @@ ${preProgramVarStatments} ${program} /* write webout file*/ -fs.promises.writeFile(weboutPath, _webout) +fs.writeFile(weboutPath, _webout, function (err) { + if (err) throw err; +}) ` // if no files are uploaded filesNamesMap will be undefined if (otherArgs?.filesNamesMap) { diff --git a/web/src/containers/Drive/main.tsx b/web/src/containers/Drive/main.tsx index cd44e90..e3b48c5 100644 --- a/web/src/containers/Drive/main.tsx +++ b/web/src/containers/Drive/main.tsx @@ -94,10 +94,7 @@ const Main = (props: Props) => { setEditMode(false) } else { window.open( - `${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath.replace( - /.sas$/, - '' - )}` + `${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}` ) } } From 158acf1f97856c96a5711c609384ca119d5ef583 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 17 Jun 2022 04:02:40 +0500 Subject: [PATCH 22/31] chore: set sas,js as default run times --- api/.env.example | 2 +- api/src/utils/verifyEnvVariables.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/.env.example b/api/.env.example index a21c209..126b6ef 100644 --- a/api/.env.example +++ b/api/.env.example @@ -6,7 +6,7 @@ PROTOCOL=[http|https] default considered as http PRIVATE_KEY=privkey.pem FULL_CHAIN=fullchain.pem -RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas +RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas,js PORT=[5000] default value is 5000 diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index c046061..5019d65 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -237,5 +237,5 @@ const DEFAULTS = { PORT: '5000', HELMET_COEP: HelmetCoepType.TRUE, LOG_FORMAT_MORGAN: LOG_FORMAT_MORGANType.Common, - RUN_TIMES: RunTimeType.SAS + RUN_TIMES: `${RunTimeType.SAS},${RunTimeType.JS}` } From ab222cbaabd3f7c44b81e55220e2e015b31505d3 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 17 Jun 2022 18:12:03 +0500 Subject: [PATCH 23/31] chore: verify executable paths --- README.md | 7 ++++-- api/.env.example | 1 + api/src/controllers/internal/Execution.ts | 2 +- api/src/types/system/process.d.ts | 1 + api/src/utils/getDesktopFields.ts | 29 +++++++++++++++++++++-- api/src/utils/setProcessVariables.ts | 4 +++- api/src/utils/verifyEnvVariables.ts | 21 ++++++++++++++++ 7 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 523bda9..d3d209a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,9 @@ MODE= # Path to SAS executable (sas.exe / sas.sh) SAS_PATH=/path/to/sas/executable.exe +# Path to Node.js executable +NODE_PATH=node + # Path to working directory # This location is for SAS WORK, staged files, DRIVE, configuration etc SASJS_ROOT=./sasjs_root @@ -131,10 +134,10 @@ HELMET_CSP_CONFIG_PATH=./csp.config.json LOG_FORMAT_MORGAN= # A comma separated string that defines the available runTimes. -# Priority is given to the runtime that comes first in string. +# Priority is given to the runtime that cSAS_PATHomes first in string. # Possible options at the moment are sas and js -# options: [sas,js|js,sas|sas|js] default:sas +# options: [sas,js|js,sas|sas|js] default:sas,js RUN_TIMES= ``` diff --git a/api/.env.example b/api/.env.example index 126b6ef..c1640bb 100644 --- a/api/.env.example +++ b/api/.env.example @@ -19,6 +19,7 @@ AUTH_CODE_SECRET= SESSION_SECRET= DB_CONNECT=mongodb+srv://:@/?retryWrites=true&w=majority +NODE_PATH=node SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas SASJS_ROOT=./sasjs_root diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index e6d6b34..7a02275 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -349,7 +349,7 @@ const processProgram = async ( // waiting for the open event so that we can have underlying file descriptor await once(writeStream, 'open') - execFileSync('node', [codePath], { + execFileSync(process.nodeLoc, [codePath], { stdio: ['ignore', writeStream, writeStream] }) diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index e8e83b4..45f9572 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -1,6 +1,7 @@ declare namespace NodeJS { export interface Process { sasLoc: string + nodeLoc: string driveLoc: string sasSessionController?: import('../../controllers/internal').SASSessionController jsSessionController?: import('../../controllers/internal').JSSessionController diff --git a/api/src/utils/getDesktopFields.ts b/api/src/utils/getDesktopFields.ts index af499a7..74999fa 100644 --- a/api/src/utils/getDesktopFields.ts +++ b/api/src/utils/getDesktopFields.ts @@ -5,12 +5,13 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils' const isWindows = () => process.platform === 'win32' export const getDesktopFields = async () => { - const { SAS_PATH } = process.env + const { SAS_PATH, NODE_PATH } = process.env const sasLoc = SAS_PATH ?? (await getSASLocation()) + const nodeLoc = NODE_PATH ?? (await getNodeLocation()) // const driveLoc = DRIVE_PATH ?? (await getDriveLocation()) - return { sasLoc } + return { sasLoc, nodeLoc } } const getDriveLocation = async (): Promise => { @@ -61,3 +62,27 @@ const getSASLocation = async (): Promise => { return targetName } + +const getNodeLocation = async (): Promise => { + const validator = async (filePath: string) => { + if (!filePath) return 'Path to NodeJS executable is required.' + + if (!(await fileExists(filePath))) { + return 'No file found at provided path.' + } + + return true + } + + const defaultLocation = isWindows() + ? 'C:\\Program Files\\nodejs\\' + : '/usr/local/nodejs/bin' + + const targetName = await getString( + 'Please enter path to nodejs executable (absolute path): ', + validator, + defaultLocation + ) + + return targetName +} diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 43725a3..d365ec1 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -13,10 +13,12 @@ export const setProcessVariables = async () => { if (MODE === ModeType.Server) { process.sasLoc = process.env.SAS_PATH as string + process.nodeLoc = process.env.NODE_PATH as string } else { - const { sasLoc } = await getDesktopFields() + const { sasLoc, nodeLoc } = await getDesktopFields() process.sasLoc = sasLoc + process.nodeLoc = nodeLoc } const { SASJS_ROOT } = process.env diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index 5019d65..3c51363 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -53,6 +53,8 @@ export const verifyEnvVariables = (): ReturnCode => { errors.push(...verifyRUN_TIMES()) + errors.push(...verifyExecutablePaths()) + if (errors.length) { process.logger?.error( `Invalid environment variable(s) provided: \n${errors.join('\n')}` @@ -231,6 +233,25 @@ const verifyRUN_TIMES = (): string[] => { return errors } +const verifyExecutablePaths = () => { + const errors: string[] = [] + const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env + + if (MODE === ModeType.Server) { + const runTimes = RUN_TIMES?.split(',') + + if (runTimes?.includes(RunTimeType.SAS) && !SAS_PATH) { + errors.push(`- SAS_PATH is required for ${RunTimeType.SAS} run time`) + } + + if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) { + errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`) + } + } + + return errors +} + const DEFAULTS = { MODE: ModeType.Desktop, PROTOCOL: ProtocolType.HTTP, From 23b6692f02e4afa33c9dc95d242eb8645c19d546 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 17 Jun 2022 20:01:50 +0500 Subject: [PATCH 24/31] fix: code/execute controller logic to handle different runtimes --- api/public/swagger.yaml | 37 +++++++++++------ api/src/controllers/code.ts | 13 ++++-- api/src/controllers/info.ts | 7 +++- api/src/controllers/internal/Execution.ts | 11 ++++-- api/src/controllers/stp.ts | 48 +++++++---------------- api/src/routes/api/code.ts | 4 +- api/src/utils/validation.ts | 6 ++- web/src/containers/Studio/index.tsx | 42 +++++++++++++++++--- web/src/context/appContext.tsx | 10 +++++ 9 files changed, 114 insertions(+), 64 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index dd34dd8..d95d7ab 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -139,14 +139,24 @@ components: - httpHeaders type: object additionalProperties: false + RunTimeType: + enum: + - sas + - js + type: string ExecuteSASCodePayload: properties: code: type: string - description: 'Code of SAS program' - example: '* SAS Code HERE;' + description: 'Code of program' + example: '* Code HERE;' + runTime: + $ref: '#/components/schemas/RunTimeType' + description: 'runtime for program' + example: js required: - code + - runTime type: object additionalProperties: false MemberType.folder: @@ -437,11 +447,16 @@ components: type: array protocol: type: string + runTimes: + items: + type: string + type: array required: - mode - cors - whiteList - protocol + - runTimes type: object additionalProperties: false ExecuteReturnJsonPayload: @@ -1349,7 +1364,7 @@ paths: $ref: '#/components/schemas/InfoResponse' examples: 'Example 1': - value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http} + value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http, runTimes: [sas, js]} summary: 'Get server info (mode, cors, whiteList, protocol).' tags: - Info @@ -1387,8 +1402,8 @@ paths: anyOf: - {type: string} - {type: string, format: byte} - description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. Setting _debug=131 will\ncause the log to be streamed in the output.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response headers can be adjusted using the mfs_httpheader() macro. Any\nfile type can be returned, including binary files such as zip or xls.\n\nIf _debug is >= 131, response headers will contain Content-Type: 'text/plain'\n\nThis behaviour differs for POST requests, in which case the response is\nalways JSON." - summary: 'Execute Stored Program, return raw _webout content.' + description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms" + summary: 'Execute a Stored Program, returns raw _webout content.' tags: - STP security: @@ -1396,13 +1411,13 @@ paths: bearerAuth: [] parameters: - - description: 'Location of SAS program' + description: 'Location of SAS or JS code' in: query name: _program required: true schema: type: string - example: /Public/somefolder/some.file + example: /Projects/myApp/some/program post: operationId: ExecuteReturnJson responses: @@ -1415,8 +1430,8 @@ paths: examples: 'Example 1': value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} - description: "Trigger a SAS program using it's location in the _program URL parameter.\nEnable debugging using the _debug URL parameter. In any case, the log is\nalways returned in the log object.\n\nAdditional URL parameters are turned into SAS macro variables.\n\nAny files provided in the request body are placed into the SAS session with\ncorresponding _WEBIN_XXX variables created.\n\nThe response will be a JSON object with the following root attributes: log,\nwebout, headers.\n\nThe webout will be a nested JSON object ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content.\n\nResponse headers from the mfs_httpheader macro are simply listed in the\nheaders object, for POST requests they have no effect on the actual\nresponse header." - summary: 'Execute Stored Program, return JSON' + description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content." + summary: 'Execute a Stored Program, return a JSON object' tags: - STP security: @@ -1424,13 +1439,13 @@ paths: bearerAuth: [] parameters: - - description: 'Location of SAS program' + description: 'Location of SAS or JS code' in: query name: _program required: false schema: type: string - example: /Public/somefolder/some.file + example: /Projects/myApp/some/program requestBody: required: false content: diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index 11d8ca6..7ae5c18 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -12,10 +12,15 @@ import { interface ExecuteSASCodePayload { /** - * Code of SAS program - * @example "* SAS Code HERE;" + * Code of program + * @example "* Code HERE;" */ code: string + /** + * runtime for program + * @example "js" + */ + runTime: RunTimeType } @Security('bearerAuth') @@ -37,7 +42,7 @@ export class CodeController { const executeSASCode = async ( req: express.Request, - { code }: ExecuteSASCodePayload + { code, runTime }: ExecuteSASCodePayload ) => { const { user } = req const userAutoExec = @@ -53,7 +58,7 @@ const executeSASCode = async ( vars: { ...req.query, _debug: 131 }, otherArgs: { userAutoExec }, returnJson: true, - runTime: RunTimeType.SAS + runTime: runTime })) as ExecuteReturnJson return { diff --git a/api/src/controllers/info.ts b/api/src/controllers/info.ts index fca2604..5c4cf86 100644 --- a/api/src/controllers/info.ts +++ b/api/src/controllers/info.ts @@ -5,6 +5,7 @@ export interface InfoResponse { cors: string whiteList: string[] protocol: string + runTimes: string[] } @Route('SASjsApi/info') @@ -18,7 +19,8 @@ export class InfoController { mode: 'desktop', cors: 'enable', whiteList: ['http://example.com', 'http://example2.com'], - protocol: 'http' + protocol: 'http', + runTimes: ['sas', 'js'] }) @Get('/') public info(): InfoResponse { @@ -29,7 +31,8 @@ export class InfoController { (process.env.MODE === 'server' ? 'disable' : 'enable'), whiteList: process.env.WHITELIST?.split(' ')?.filter((url) => !!url) ?? [], - protocol: process.env.PROTOCOL ?? 'http' + protocol: process.env.PROTOCOL ?? 'http', + runTimes: process.runTimes } return response } diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 7a02275..723341f 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -274,6 +274,7 @@ const createJSProgram = async ( ) const preProgramVarStatments = ` +let _webout = ''; const weboutPath = '${weboutPath}'; const _sasjs_tokenfile = '${tokenFile}'; const _sasjs_username = '${preProgramVariables?.username}'; @@ -296,10 +297,12 @@ ${preProgramVarStatments} /* actual job code */ ${program} -/* write webout file*/ -fs.writeFile(weboutPath, _webout, function (err) { - if (err) throw err; -}) +/* write webout file only if webout exists*/ +if (_webout) { + fs.writeFile(weboutPath, _webout, function (err) { + if (err) throw err; + }) +} ` // if no files are uploaded filesNamesMap will be undefined if (otherArgs?.filesNamesMap) { diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index 37e3dad..a32c706 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -51,26 +51,15 @@ export interface ExecuteReturnJsonResponse { @Tags('STP') export class STPController { /** - * Trigger a SAS program using it's location in the _program URL parameter. - * Enable debugging using the _debug URL parameter. Setting _debug=131 will - * cause the log to be streamed in the output. + * Trigger a SAS or JS program using the _program URL parameter. * - * Additional URL parameters are turned into SAS macro variables. + * Accepts URL parameters and file uploads. For more details, see docs: * - * Any files provided in the request body are placed into the SAS session with - * corresponding _WEBIN_XXX variables created. + * https://server.sasjs.io/storedprograms * - * The response headers can be adjusted using the mfs_httpheader() macro. Any - * file type can be returned, including binary files such as zip or xls. - * - * If _debug is >= 131, response headers will contain Content-Type: 'text/plain' - * - * This behaviour differs for POST requests, in which case the response is - * always JSON. - * - * @summary Execute Stored Program, return raw _webout content. - * @param _program Location of SAS program - * @example _program "/Public/somefolder/some.file" + * @summary Execute a Stored Program, returns raw _webout content. + * @param _program Location of SAS or JS code + * @example _program "/Projects/myApp/some/program" */ @Get('/execute') public async executeReturnRaw( @@ -81,29 +70,22 @@ export class STPController { } /** - * Trigger a SAS program using it's location in the _program URL parameter. - * Enable debugging using the _debug URL parameter. In any case, the log is - * always returned in the log object. + * Trigger a SAS or JS program using the _program URL parameter. * - * Additional URL parameters are turned into SAS macro variables. + * Accepts URL parameters and file uploads. For more details, see docs: * - * Any files provided in the request body are placed into the SAS session with - * corresponding _WEBIN_XXX variables created. + * https://server.sasjs.io/storedprograms * - * The response will be a JSON object with the following root attributes: log, - * webout, headers. + * The response will be a JSON object with the following root attributes: + * log, webout, headers. * - * The webout will be a nested JSON object ONLY if the response-header + * The webout attribute will be nested JSON ONLY if the response-header * contains a content-type of application/json AND it is valid JSON. * Otherwise it will be a stringified version of the webout content. * - * Response headers from the mfs_httpheader macro are simply listed in the - * headers object, for POST requests they have no effect on the actual - * response header. - * - * @summary Execute Stored Program, return JSON - * @param _program Location of SAS program - * @example _program "/Public/somefolder/some.file" + * @summary Execute a Stored Program, return a JSON object + * @param _program Location of SAS or JS code + * @example _program "/Projects/myApp/some/program" */ @Example({ status: 'success', diff --git a/api/src/routes/api/code.ts b/api/src/routes/api/code.ts index efeaccd..8e866c5 100644 --- a/api/src/routes/api/code.ts +++ b/api/src/routes/api/code.ts @@ -1,5 +1,5 @@ import express from 'express' -import { runSASValidation } from '../../utils' +import { runCodeValidation } from '../../utils' import { CodeController } from '../../controllers/' const runRouter = express.Router() @@ -7,7 +7,7 @@ const runRouter = express.Router() const controller = new CodeController() runRouter.post('/execute', async (req, res) => { - const { error, value: body } = runSASValidation(req.body) + const { error, value: body } = runCodeValidation(req.body) if (error) return res.status(400).send(error.details[0].message) try { diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 3499715..150dc83 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -1,4 +1,5 @@ import Joi from 'joi' +import { RunTimeType } from '.' const usernameSchema = Joi.string().lowercase().alphanum().min(3).max(16) const passwordSchema = Joi.string().min(6).max(1024) @@ -114,9 +115,10 @@ export const folderParamValidation = (data: any): Joi.ValidationResult => _folderPath: Joi.string() }).validate(data) -export const runSASValidation = (data: any): Joi.ValidationResult => +export const runCodeValidation = (data: any): Joi.ValidationResult => Joi.object({ - code: Joi.string().required() + code: Joi.string().required(), + runTime: Joi.string().valid(...Object.values(RunTimeType)) }).validate(data) export const executeProgramRawValidation = (data: any): Joi.ValidationResult => diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index c043dd0..85b8080 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -1,13 +1,24 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState, useContext } from 'react' import axios from 'axios' -import Box from '@mui/material/Box' -import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material' +import { + Box, + MenuItem, + FormControl, + Select, + SelectChangeEvent, + Button, + Paper, + Tab, + Tooltip +} from '@mui/material' import { makeStyles } from '@mui/styles' import Editor, { EditorDidMount } from 'react-monaco-editor' import { useLocation } from 'react-router-dom' import { TabContext, TabList, TabPanel } from '@mui/lab' +import { AppContext, RunTimeType } from '../../context/appContext' + const useStyles = makeStyles(() => ({ root: { fontSize: '1rem', @@ -30,12 +41,14 @@ const useStyles = makeStyles(() => ({ })) const Studio = () => { + const appContext = useContext(AppContext) const location = useLocation() const [fileContent, setFileContent] = useState('') const [log, setLog] = useState('') const [ctrlPressed, setCtrlPressed] = useState(false) const [webout, setWebout] = useState('') - const [tab, setTab] = React.useState('1') + const [tab, setTab] = useState('1') + const [selectedRunTime, setSelectedRunTime] = useState(RunTimeType.SAS) const handleTabChange = (_e: any, newValue: string) => { setTab(newValue) @@ -57,7 +70,7 @@ const Studio = () => { const runCode = (code: string) => { axios - .post(`/SASjsApi/code/execute`, { code }) + .post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime }) .then((res: any) => { const parsedLog = res?.data?.log .map((logLine: any) => logLine.line) @@ -89,6 +102,10 @@ const Studio = () => { if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false) } + const handleChangeRunTime = (event: SelectChangeEvent) => { + setSelectedRunTime(event.target.value as RunTimeType) + } + useEffect(() => { const content = localStorage.getItem('fileContent') ?? '' setFileContent(content) @@ -149,8 +166,21 @@ const Studio = () => { RUN + + + + + - {/* */} > | null mode: ModeType + runTimes: RunTimeType[] logout: (() => void) | null } @@ -39,6 +45,7 @@ export const AppContext = createContext({ displayName: '', setDisplayName: null, mode: ModeType.Server, + runTimes: [], logout: null }) @@ -50,6 +57,7 @@ const AppContextProvider = (props: { children: ReactNode }) => { const [username, setUsername] = useState('') const [displayName, setDisplayName] = useState('') const [mode, setMode] = useState(ModeType.Server) + const [runTimes, setRunTimes] = useState([]) useEffect(() => { setCheckingSession(true) @@ -74,6 +82,7 @@ const AppContextProvider = (props: { children: ReactNode }) => { .then((res) => res.data) .then((data: any) => { setMode(data.mode) + setRunTimes(data.runTimes) }) .catch(() => {}) }, []) @@ -99,6 +108,7 @@ const AppContextProvider = (props: { children: ReactNode }) => { displayName, setDisplayName, mode, + runTimes, logout }} > From 9023cf33b5fa4b13c2d5e9b80ae307df69c7fc02 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 17 Jun 2022 23:17:23 +0500 Subject: [PATCH 25/31] fix(Studio): style fix for runtime dropdown --- web/src/containers/Studio/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 85b8080..10bd25d 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -166,7 +166,7 @@ const Studio = () => { RUN - +