diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..9a770f0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "cSpell.words": [
+ "autoexec"
+ ]
+}
diff --git a/src/controllers/Execution.ts b/src/controllers/Execution.ts
new file mode 100644
index 0000000..04f835f
--- /dev/null
+++ b/src/controllers/Execution.ts
@@ -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 = `
+${webout}
+
+`
+ }
+
+ session.inUse = false
+
+ sessionController.deleteSession(session)
+
+ return Promise.resolve(webout)
+ }
+}
diff --git a/src/controllers/Session.ts b/src/controllers/Session.ts
new file mode 100644
index 0000000..3e9af19
--- /dev/null
+++ b/src/controllers/Session.ts
@@ -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
+}
diff --git a/src/controllers/deploy.ts b/src/controllers/deploy.ts
index 3df7c43..2c1f1db 100644
--- a/src/controllers/deploy.ts
+++ b/src/controllers/deploy.ts
@@ -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[] = []
diff --git a/src/controllers/index.ts b/src/controllers/index.ts
index e90f306..2990104 100644
--- a/src/controllers/index.ts
+++ b/src/controllers/index.ts
@@ -1,2 +1,3 @@
-export * from './sas'
export * from './deploy'
+export * from './Session'
+export * from './Execution'
diff --git a/src/controllers/sas.ts b/src/controllers/sas.ts
deleted file mode 100644
index afdd4dc..0000000
--- a/src/controllers/sas.ts
+++ /dev/null
@@ -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 => {
- 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 = `
-${webout}
-
-`
- }
-
- return Promise.resolve(webout)
- } else {
- return Promise.resolve({
- log: log
- })
- }
-}
diff --git a/src/routes/index.ts b/src/routes/index.ts
index bd27d0a..1fc3bc9 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -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(`Executed!
-Log is located:
${result.logPath}
-Log:
`)
+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 {
diff --git a/src/types/sas.ts b/src/types/Execution.ts
similarity index 100%
rename from src/types/sas.ts
rename to src/types/Execution.ts
diff --git a/src/types/fileTree.ts b/src/types/FileTree.ts
similarity index 100%
rename from src/types/fileTree.ts
rename to src/types/FileTree.ts
diff --git a/src/types/Process.d.ts b/src/types/Process.d.ts
new file mode 100644
index 0000000..50313f7
--- /dev/null
+++ b/src/types/Process.d.ts
@@ -0,0 +1,5 @@
+declare namespace NodeJS {
+ export interface Process {
+ sessionController?: import('../controllers/Session').SessionController
+ }
+}
diff --git a/src/types/request.ts b/src/types/Request.ts
similarity index 99%
rename from src/types/request.ts
rename to src/types/Request.ts
index 26c872f..6bc7e2b 100644
--- a/src/types/request.ts
+++ b/src/types/Request.ts
@@ -1,4 +1,5 @@
import { MacroVars } from '@sasjs/utils'
+
export interface ExecutionQuery {
_program: string
macroVars?: MacroVars
diff --git a/src/types/Session.ts b/src/types/Session.ts
new file mode 100644
index 0000000..5484311
--- /dev/null
+++ b/src/types/Session.ts
@@ -0,0 +1,8 @@
+export interface Session {
+ id: string
+ ready: boolean
+ creationTimeStamp: string
+ deathTimeStamp: string
+ path: string
+ inUse: boolean
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 7897c28..2377b59 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -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'
diff --git a/src/utils/file.ts b/src/utils/file.ts
index bd3f759..fe09816 100644
--- a/src/utils/file.ts
+++ b/src/utils/file.ts
@@ -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('')
diff --git a/src/utils/index.ts b/src/utils/index.ts
index d0a70d5..31581eb 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1 +1,2 @@
export * from './file'
+export * from './sleep'
diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts
new file mode 100644
index 0000000..61d7522
--- /dev/null
+++ b/src/utils/sleep.ts
@@ -0,0 +1,3 @@
+export const sleep = async (delay: number) => {
+ await new Promise((resolve) => setTimeout(resolve, delay))
+}
diff --git a/tsconfig.json b/tsconfig.json
index 50262e5..391d326 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,5 +7,8 @@
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true
+ },
+ "ts-node": {
+ "files": true
}
}