1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-11 03:34:35 +00:00

fix: session refactoring with Saad & Allan

This commit is contained in:
Allan Bowe
2021-11-13 14:31:09 +00:00
parent a4ac5dc280
commit cbe07b4abb
7 changed files with 98 additions and 34 deletions

22
api/package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.33.1", "@sasjs/utils": "^2.33.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
@@ -3904,6 +3905,18 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "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": { "node_modules/cosmiconfig": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "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": { "cosmiconfig": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz",

View File

@@ -43,6 +43,7 @@
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.33.1", "@sasjs/utils": "^2.33.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"joi": "^17.4.2", "joi": "^17.4.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",

View File

@@ -981,7 +981,7 @@ paths:
application/json: application/json:
schema: schema:
type: string 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' summary: 'Execute Stored Program, return raw content'
tags: tags:
- STP - STP
@@ -1005,7 +1005,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteReturnJsonResponse' $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' summary: 'Execute Stored Program, return JSON'
tags: tags:
- STP - STP

View File

@@ -2,7 +2,6 @@ import path from 'path'
import express from 'express' import express from 'express'
import morgan from 'morgan' import morgan from 'morgan'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import webRouter from './routes/web' import webRouter from './routes/web'
import apiRouter from './routes/api' import apiRouter from './routes/api'
import { getWebBuildFolderPath } from './utils' import { getWebBuildFolderPath } from './utils'
@@ -10,6 +9,9 @@ import { connectDB } from './routes/api/auth'
const app = express() const app = express()
const cors=require('cors')
app.use(cors())
app.use(express.json({ limit: '50mb' })) app.use(express.json({ limit: '50mb' }))
app.use(morgan('tiny')) app.use(morgan('tiny'))
app.use(express.static(path.join(__dirname, '../public'))) app.use(express.static(path.join(__dirname, '../public')))

View File

@@ -2,13 +2,10 @@ import path from 'path'
import fs from 'fs' import fs from 'fs'
import { getSessionController } from './' import { getSessionController } from './'
import { readFile, fileExists, createFile } from '@sasjs/utils' 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 { PreProgramVars, Session, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils' import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
export const delay = (ms: number) =>
const execFilePromise = promisify(execFile) new Promise((resolve) => setTimeout(resolve, ms))
export class ExecutionController { export class ExecutionController {
async execute( async execute(
@@ -76,25 +73,25 @@ ${program}`
} }
} }
const code = path.join(session.path, 'code.sas') const codePath = path.join(session.path, 'code.sas')
if (!(await fileExists(code))) {
await createFile(code, program) // 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) if (await fileExists(log)) log = await readFile(log)
else log = '' else log = ''
@@ -107,7 +104,7 @@ ${program}`
) )
let jsonResult let jsonResult
if ((debug && vars[debug] >= 131) || stderr) { if ((debug && vars[debug] >= 131)) {
webout = `<html><body> webout = `<html><body>
${webout} ${webout}
<div style="text-align:left"> <div style="text-align:left">

View File

@@ -1,4 +1,7 @@
import { Session } from '../../types' import { Session } from '../../types'
import { configuration } from '../../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils' import { getTmpSessionsFolderPath, generateUniqueFileName } from '../../utils'
import { import {
deleteFolder, deleteFolder,
@@ -9,6 +12,9 @@ import {
} from '@sasjs/utils' } from '@sasjs/utils'
import path from 'path' import path from 'path'
import { ExecutionController } from './Execution' import { ExecutionController } from './Execution'
import { date } from 'joi'
const execFilePromise = promisify(execFile)
export class SessionController { export class SessionController {
private sessions: Session[] = [] private sessions: Session[] = []
@@ -34,7 +40,8 @@ export class SessionController {
const sessionId = generateUniqueFileName(generateTimestamp()) const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId) const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
const autoExecContent = `data _null_; const autoExecContent = `
data _null_;
/* remove the dummy SYSIN */ /* remove the dummy SYSIN */
length fname $8; length fname $8;
rc=filename(fname,getoption('SYSIN') ); rc=filename(fname,getoption('SYSIN') );
@@ -45,12 +52,14 @@ export class SessionController {
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
slept=slept+sleep(0.01,1); slept=slept+sleep(0.01,1);
end; end;
stop;
run; run;
EOL` `
// the autoexec file is executed on SAS startup
const autoExec = path.join(sessionFolder, 'autoexec.sas') const autoExec = path.join(sessionFolder, 'autoexec.sas')
await createFile(autoExec, autoExecContent) await createFile(autoExec, autoExecContent)
// a dummy SYSIN code.sas file is necessary to start SAS
await createFile(path.join(sessionFolder, 'code.sas'), '') await createFile(path.join(sessionFolder, 'code.sas'), '')
const creationTimeStamp = sessionId.split('-').pop() as string const creationTimeStamp = sessionId.split('-').pop() as string
@@ -65,17 +74,49 @@ export class SessionController {
1000 1000
).toString(), ).toString(),
path: sessionFolder, 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.scheduleSessionDestroy(session)
this.executionController // create empty code.sas as SAS will not start without a SYSIN
.execute('', undefined, autoExec, session) const codePath = path.join(session.path, 'code.sas')
.catch(() => {}) 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) 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) await this.waitForSession(session)
return session return session
@@ -85,8 +126,6 @@ export class SessionController {
if (await fileExists(path.join(session.path, 'code.sas'))) { if (await fileExists(path.join(session.path, 'code.sas'))) {
while (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 session.ready = true
return Promise.resolve(session) return Promise.resolve(session)
@@ -98,8 +137,10 @@ export class SessionController {
} }
public async deleteSession(session: Session) { public async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path) await deleteFolder(session.path)
// remove the session from the session array
if (session.ready) { if (session.ready) {
this.sessions = this.sessions.filter( this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id (sess: Session) => sess.id !== session.id

View File

@@ -5,4 +5,5 @@ export interface Session {
deathTimeStamp: string deathTimeStamp: string
path: string path: string
inUse: boolean inUse: boolean
completed?: boolean
} }