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:
22
api/package-lock.json
generated
22
api/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')))
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface Session {
|
|||||||
deathTimeStamp: string
|
deathTimeStamp: string
|
||||||
path: string
|
path: string
|
||||||
inUse: boolean
|
inUse: boolean
|
||||||
|
completed?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user