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

Merge pull request #22 from sasjs/session

Add session controller and execution controller
This commit is contained in:
Allan Bowe
2021-10-14 11:44:07 +01:00
committed by GitHub
17 changed files with 272 additions and 127 deletions

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"cSpell.words": [
"autoexec"
]
}

View File

@@ -0,0 +1,92 @@
import { getSessionController } from './'
import { readFile, fileExists, createFile } from '@sasjs/utils'
import path from 'path'
import { configuration } from '../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
import { Session } from '../types'
const execFilePromise = promisify(execFile)
export class ExecutionController {
async execute(
program = '',
autoExec?: string,
session?: Session,
vars?: any
) {
if (program) {
if (!(await fileExists(program))) {
throw 'ExecutionController: SAS file does not exist.'
}
program = await readFile(program)
if (vars) {
Object.keys(vars).forEach(
(key: string) => (program = `%let ${key}=${vars[key]};\n${program}`)
)
}
}
const sessionController = getSessionController()
if (!session) {
session = await sessionController.getSession()
session.inUse = true
}
let log = path.join(session.path, 'log.log')
let webout = path.join(session.path, 'webout.txt')
await createFile(webout, '')
program = `filename _webout "${webout}";\n${program}`
const code = path.join(session.path, 'code.sas')
if (!(await fileExists(code))) {
await createFile(code, program)
}
let additionalArgs: string[] = []
if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec]
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
'-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 = ''
if (stderr) return Promise.reject({ error: stderr, log: log })
if (await fileExists(webout)) webout = await readFile(webout)
else webout = ''
const debug = Object.keys(vars).find(
(key: string) => key.toLowerCase() === '_debug'
)
if (debug && vars[debug] >= 131) {
webout = `<html><body>
${webout}
<div style="text-align:left">
<hr /><h2>SAS Log</h2>
<pre>${log}</pre>
</div>
</body></html>`
}
session.inUse = false
sessionController.deleteSession(session)
return Promise.resolve(webout)
}
}

127
src/controllers/Session.ts Normal file
View File

@@ -0,0 +1,127 @@
import { Session } from '../types'
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils'
import {
deleteFolder,
createFile,
fileExists,
deleteFile,
generateTimestamp
} from '@sasjs/utils'
import path from 'path'
import { ExecutionController } from './Execution'
export class SessionController {
private sessions: Session[] = []
private executionController: ExecutionController
constructor() {
this.executionController = new ExecutionController()
}
public async getSession() {
const readySessions = this.sessions.filter((sess: Session) => sess.ready)
const session = readySessions.length
? readySessions[0]
: await this.createSession()
if (readySessions.length < 2) this.createSession()
return session
}
private async createSession() {
const sessionId = generateUniqueFileName(generateTimestamp())
const sessionFolder = path.join(await getTmpSessionsFolderPath(), sessionId)
const autoExecContent = `data _null_;
/* remove the dummy SYSIN */
length fname $8;
rc=filename(fname,getoption('SYSIN') );
if rc = 0 and fexist(fname) then rc=fdelete(fname);
rc=filename(fname);
/* now wait for the real SYSIN */
slept=0;
do until ( fileexist(getoption('SYSIN')) or slept>(60*15) );
slept=slept+sleep(0.01,1);
end;
run;
EOL`
const autoExec = path.join(sessionFolder, 'autoexec.sas')
await createFile(autoExec, autoExecContent)
await createFile(path.join(sessionFolder, 'code.sas'), '')
const creationTimeStamp = sessionId.split('-').pop() as string
const session: Session = {
id: sessionId,
ready: false,
creationTimeStamp: creationTimeStamp,
deathTimeStamp: (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString(),
path: sessionFolder,
inUse: false
}
this.scheduleSessionDestroy(session)
this.executionController.execute('', autoExec, session).catch(() => {})
this.sessions.push(session)
await this.waitForSession(session)
return session
}
public async waitForSession(session: Session) {
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)
} else {
session.ready = true
return Promise.resolve(session)
}
}
public async deleteSession(session: Session) {
await deleteFolder(session.path)
if (session.ready) {
this.sessions = this.sessions.filter(
(sess: Session) => sess.id !== session.id
)
}
}
private scheduleSessionDestroy(session: Session) {
setTimeout(async () => {
if (session.inUse) {
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
this.scheduleSessionDestroy(session)
} else {
await this.deleteSession(session)
}
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
}
}
export const getSessionController = () => {
if (process.sessionController) return process.sessionController
process.sessionController = new SessionController()
return process.sessionController
}

View File

@@ -3,6 +3,7 @@ import { getTmpFilesFolderPath } from '../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
import path from 'path'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (
members: [FolderMember, ServiceMember],
parentFolders: string[] = []

View File

@@ -1,2 +1,3 @@
export * from './sas'
export * from './deploy'
export * from './Session'
export * from './Execution'

View File

@@ -1,99 +0,0 @@
import { readFile, deleteFile, fileExists, createFile } from '@sasjs/utils'
import path from 'path'
import { ExecutionResult, ExecutionQuery } from '../types'
import {
getTmpFilesFolderPath,
getTmpLogFolderPath,
getTmpWeboutFolderPath,
generateUniqueFileName
} from '../utils'
import { configuration } from '../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
const execFilePromise = promisify(execFile)
export const processSas = async (query: ExecutionQuery): Promise<any> => {
const sasCodePath = path
.join(getTmpFilesFolderPath(), query._program)
.replace(new RegExp('/', 'g'), path.sep)
if (!(await fileExists(sasCodePath))) {
return Promise.reject({ error: 'SAS file does not exist.' })
}
const sasFile: string = sasCodePath.split(path.sep).pop() || 'default'
const sasLogPath = path.join(
getTmpLogFolderPath(),
generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log')
)
await createFile(sasLogPath, '')
const sasWeboutPath = path.join(
getTmpWeboutFolderPath(),
generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt')
)
await createFile(sasWeboutPath, '')
let sasCode = await readFile(sasCodePath)
const vars: any = query
Object.keys(query).forEach(
(key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`)
)
sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}`
const tmpSasCodePath = sasCodePath.replace(
sasFile,
generateUniqueFileName(sasFile)
)
await createFile(tmpSasCodePath, sasCode)
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
'-SYSIN',
tmpSasCodePath,
'-log',
sasLogPath,
process.platform === 'win32' ? '-nosplash' : ''
]).catch((err) => ({ stderr: err, stdout: '' }))
let log = ''
if (sasLogPath && (await fileExists(sasLogPath))) {
log = await readFile(sasLogPath)
}
await deleteFile(sasLogPath)
await deleteFile(tmpSasCodePath)
if (stderr) return Promise.reject({ error: stderr, log: log })
if (await fileExists(sasWeboutPath)) {
let webout = await readFile(sasWeboutPath)
await deleteFile(sasWeboutPath)
const debug = Object.keys(query).find(
(key: string) => key.toLowerCase() === '_debug'
)
if (debug && (query as any)[debug] >= 131) {
webout = `<html><body>
${webout}
<div style="text-align:left">
<hr /><h2>SAS Log</h2>
<pre>${log}</pre>
</div>
</body></html>`
}
return Promise.resolve(webout)
} else {
return Promise.resolve({
log: log
})
}
}

View File

@@ -1,23 +1,14 @@
import express from 'express'
import { processSas, createFileTree, getTreeExample } from '../controllers'
import { createFileTree, getTreeExample } from '../controllers'
import { ExecutionResult, isRequestQuery, isFileTree } from '../types'
import path from 'path'
import { getTmpFilesFolderPath } from '../utils'
import { ExecutionController } from '../controllers'
const router = express.Router()
router.get('/', async (req, res) => {
const query = req.query
if (!isRequestQuery(query)) {
res.send('Welcome to @sasjs/server API')
return
}
const result: ExecutionResult = await processSas(query)
res.send(`<b>Executed!</b><br>
<p>Log is located:</p> ${result.logPath}<br>
<p>Log:</p> <textarea style="width: 100%; height: 100%">${result.log}</textarea>`)
router.get('/', async (_, res) => {
res.status(200).send('Welcome to @sasjs/server API')
})
router.post('/deploy', async (req, res) => {
@@ -54,20 +45,21 @@ router.get('/SASjsExecutor', async (req, res) => {
})
router.get('/SASjsExecutor/do', async (req, res) => {
const queryEntries = Object.keys(req.query).map((entry: string) =>
entry.toLowerCase()
)
if (isRequestQuery(req.query)) {
await processSas({ ...req.query })
.then((result) => {
const sasCodePath = path
.join(getTmpFilesFolderPath(), req.query._program)
.replace(new RegExp('/', 'g'), path.sep)
await new ExecutionController()
.execute(sasCodePath, undefined, undefined, { ...req.query })
.then((result: {}) => {
res.status(200).send(result)
})
.catch((err) => {
.catch((err: {} | string) => {
res.status(400).send({
status: 'failure',
message: 'Job execution failed.',
...err
...(typeof err === 'object' ? err : { details: err })
})
})
} else {

5
src/types/Process.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace NodeJS {
export interface Process {
sessionController?: import('../controllers/Session').SessionController
}
}

View File

@@ -1,4 +1,5 @@
import { MacroVars } from '@sasjs/utils'
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars

8
src/types/Session.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface Session {
id: string
ready: boolean
creationTimeStamp: string
deathTimeStamp: string
path: string
inUse: boolean
}

View File

@@ -1,3 +1,5 @@
export * from './sas'
export * from './request'
export * from './fileTree'
// TODO: uppercase types
export * from './Execution'
export * from './Request'
export * from './FileTree'
export * from './Session'

View File

@@ -12,12 +12,15 @@ export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs')
export const getTmpWeboutFolderPath = () =>
path.join(getTmpFolderPath(), 'webouts')
export const getTmpSessionsFolderPath = () =>
path.join(getTmpFolderPath(), 'sessions')
export const generateUniqueFileName = (fileName: string, extension = '') =>
[
fileName,
'-',
Math.round(Math.random() * 100000),
'-',
generateTimestamp(),
new Date().getTime(),
extension
].join('')

View File

@@ -1 +1,2 @@
export * from './file'
export * from './sleep'

3
src/utils/sleep.ts Normal file
View File

@@ -0,0 +1,3 @@
export const sleep = async (delay: number) => {
await new Promise((resolve) => setTimeout(resolve, delay))
}

View File

@@ -7,5 +7,8 @@
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true
},
"ts-node": {
"files": true
}
}