From cbe07b4abb2e936037874af1a088cd038e0fc731 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Sat, 13 Nov 2021 14:31:09 +0000 Subject: [PATCH] fix: session refactoring with Saad & Allan --- api/package-lock.json | 22 +++++++++ api/package.json | 1 + api/public/swagger.yaml | 4 +- api/src/app.ts | 4 +- api/src/controllers/internal/Execution.ts | 41 ++++++++-------- api/src/controllers/internal/Session.ts | 59 +++++++++++++++++++---- api/src/types/Session.ts | 1 + 7 files changed, 98 insertions(+), 34 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index c047631..ecbd640 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@sasjs/utils": "^2.33.1", "bcryptjs": "^2.4.3", + "cors": "^2.8.5", "express": "^4.17.1", "joi": "^17.4.2", "jsonwebtoken": "^8.5.1", @@ -3904,6 +3905,18 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cosmiconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", @@ -18036,6 +18049,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", diff --git a/api/package.json b/api/package.json index 6c00d35..e60b350 100644 --- a/api/package.json +++ b/api/package.json @@ -43,6 +43,7 @@ "dependencies": { "@sasjs/utils": "^2.33.1", "bcryptjs": "^2.4.3", + "cors": "^2.8.5", "express": "^4.17.1", "joi": "^17.4.2", "jsonwebtoken": "^8.5.1", diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 3d814f4..c4d4018 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -981,7 +981,7 @@ paths: application/json: schema: type: string - description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created." + description: "Trigger a SAS program using it's location in the _program parameter.\r\nEnable debugging using the _debug parameter.\r\nAdditional URL parameters are turned into SAS macro variables.\r\nAny files provided are placed into the session and\r\ncorresponding _WEBIN_XXX variables are created." summary: 'Execute Stored Program, return raw content' tags: - STP @@ -1005,7 +1005,7 @@ paths: application/json: schema: $ref: '#/components/schemas/ExecuteReturnJsonResponse' - description: "Trigger a SAS program using it's location in the _program parameter.\nEnable debugging using the _debug parameter.\nAdditional URL parameters are turned into SAS macro variables.\nAny files provided are placed into the session and\ncorresponding _WEBIN_XXX variables are created." + description: "Trigger a SAS program using it's location in the _program parameter.\r\nEnable debugging using the _debug parameter.\r\nAdditional URL parameters are turned into SAS macro variables.\r\nAny files provided are placed into the session and\r\ncorresponding _WEBIN_XXX variables are created." summary: 'Execute Stored Program, return JSON' tags: - STP diff --git a/api/src/app.ts b/api/src/app.ts index 0874ad0..f4c4b01 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -2,7 +2,6 @@ import path from 'path' import express from 'express' import morgan from 'morgan' import dotenv from 'dotenv' - import webRouter from './routes/web' import apiRouter from './routes/api' import { getWebBuildFolderPath } from './utils' @@ -10,6 +9,9 @@ import { connectDB } from './routes/api/auth' const app = express() +const cors=require('cors') +app.use(cors()) + app.use(express.json({ limit: '50mb' })) app.use(morgan('tiny')) app.use(express.static(path.join(__dirname, '../public'))) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 2aaf4de..1f1d6f3 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -2,13 +2,10 @@ import path from 'path' import fs from 'fs' import { getSessionController } from './' import { readFile, fileExists, createFile } from '@sasjs/utils' -import { configuration } from '../../../package.json' -import { promisify } from 'util' -import { execFile } from 'child_process' import { PreProgramVars, Session, TreeNode } from '../../types' import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils' - -const execFilePromise = promisify(execFile) +export const delay = (ms: number) => +new Promise((resolve) => setTimeout(resolve, ms)) export class ExecutionController { async execute( @@ -76,25 +73,25 @@ ${program}` } } - const code = path.join(session.path, 'code.sas') - if (!(await fileExists(code))) { - await createFile(code, 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) + fs.renameSync(codePath + '.bkp',codePath) + + // we now need to poll the session array + while ( + !session.completed + ) { + await delay(50) } - let additionalArgs: string[] = [] - if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec] - const sasLoc = process.sasLoc ?? configuration.sasPath - const { stdout, stderr } = await execFilePromise(sasLoc, [ - '-SYSIN', - code, - '-LOG', - log, - '-WORK', - session.path, - ...additionalArgs, - process.platform === 'win32' ? '-nosplash' : '' - ]).catch((err) => ({ stderr: err, stdout: '' })) if (await fileExists(log)) log = await readFile(log) else log = '' @@ -107,7 +104,7 @@ ${program}` ) let jsonResult - if ((debug && vars[debug] >= 131) || stderr) { + if ((debug && vars[debug] >= 131)) { webout = ` ${webout}
diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index f10a6f4..abc2723 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -1,4 +1,7 @@ import { Session } from '../../types' +import { configuration } from '../../../package.json' +import { promisify } from 'util' +import { execFile } from 'child_process' import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils' import { deleteFolder, @@ -9,6 +12,9 @@ import { } from '@sasjs/utils' import path from 'path' import { ExecutionController } from './Execution' +import { date } from 'joi' + +const execFilePromise = promisify(execFile) export class SessionController { private sessions: Session[] = [] @@ -34,7 +40,8 @@ export class SessionController { const sessionId = generateUniqueFileName(generateTimestamp()) const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId) - const autoExecContent = `data _null_; + const autoExecContent = ` + data _null_; /* remove the dummy SYSIN */ length fname $8; rc=filename(fname,getoption('SYSIN') ); @@ -45,12 +52,14 @@ export class SessionController { do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); slept=slept+sleep(0.01,1); end; + stop; run; - EOL` - +` + // the autoexec file is executed on SAS startup const autoExec = path.join(sessionFolder, 'autoexec.sas') await createFile(autoExec, autoExecContent) + // a dummy SYSIN code.sas file is necessary to start SAS await createFile(path.join(sessionFolder, 'code.sas'), '') const creationTimeStamp = sessionId.split('-').pop() as string @@ -65,17 +74,49 @@ export class SessionController { 1000 ).toString(), path: sessionFolder, - inUse: false + inUse: false, + completed: false } + // we do not want to leave sessions running forever + // we clean them up after a predefined period, if unused this.scheduleSessionDestroy(session) - this.executionController - .execute('', undefined, autoExec, session) - .catch(() => {}) + // create empty code.sas as SAS will not start without a SYSIN + const codePath = path.join(session.path, 'code.sas') + await createFile(codePath, '') + + // this.executionController + // .execute('', undefined, autoExec, session) + // .catch((err) => {console.log(err)}) + + + // trigger SAS but don't wait for completion - we need to + // update the session array to say that it is currently running + // however we also need a promise so that we can update the + // session array to say that it has (eventually) finished. + const sasLoc = process.sasLoc ?? configuration.sasPath + execFilePromise(sasLoc, [ + '-SYSIN', + codePath, + '-LOG', + path.join(session.path, 'log.log'), + '-WORK', + session.path, + '-AUTOEXEC', + path.join(session.path, 'autoexec.sas'), + process.platform === 'win32' ? '-nosplash' : '' + ]).then(() => { + session.completed=true + console.log('session completed', session) + }).catch((err) => {}) + + // we have a triggered session - add to array this.sessions.push(session) + // SAS has been triggered but we can't use it until + // the autoexec deletes the code.sas file await this.waitForSession(session) return session @@ -85,8 +126,6 @@ export class SessionController { if (await fileExists(path.join(session.path, 'code.sas'))) { while (await fileExists(path.join(session.path, 'code.sas'))) {} - await deleteFile(path.join(session.path, 'log.log')) - session.ready = true return Promise.resolve(session) @@ -98,8 +137,10 @@ export class SessionController { } public async deleteSession(session: Session) { + // remove the temporary files, to avoid buildup await deleteFolder(session.path) + // remove the session from the session array if (session.ready) { this.sessions = this.sessions.filter( (sess: Session) => sess.id !== session.id diff --git a/api/src/types/Session.ts b/api/src/types/Session.ts index 5484311..4eb9deb 100644 --- a/api/src/types/Session.ts +++ b/api/src/types/Session.ts @@ -5,4 +5,5 @@ export interface Session { deathTimeStamp: string path: string inUse: boolean + completed?: boolean }