1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +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": {
"@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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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')))

View File

@@ -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 = `<html><body>
${webout}
<div style="text-align:left">

View File

@@ -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

View File

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