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

feat(session): add SessionController and ExecutionController

This commit is contained in:
Yury Shkoda
2021-10-11 14:16:22 +03:00
parent be2ad36a02
commit 6e0b04a6e5
9 changed files with 266 additions and 58 deletions

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

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

View File

@@ -0,0 +1,68 @@
import { getSessionController } from './session'
import { readFile, deleteFile, fileExists, createFile } from '@sasjs/utils'
import path from 'path'
import { configuration } from '../../package.json'
import { promisify } from 'util'
import { execFile } from 'child_process'
const execFilePromise = promisify(execFile)
export class ExecutionController {
async execute(program = '', autoExec?: string, debug?: number) {
console.log(`[ExecutionController]program: `, program)
console.log(`[ExecutionController]autoExec: `, autoExec)
if (program) {
if (!(await fileExists(program))) {
throw 'SASjsServer/Executor: SAS file does not exist.'
}
program = await readFile(program)
}
const sessionController = getSessionController()
const session = await sessionController.getSession()
console.log(`[ExecutionController]session: `, session)
let log = path.join(session.path, 'log.log')
await createFile(log, '')
let webout = path.join(session.path, 'webout.txt')
await createFile(webout, '')
const code = path.join(session.path, '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',
...additionalArgs,
session.path,
process.platform === 'win32' ? '-nosplash' : ''
]).catch((err) => ({ stderr: err, stdout: '' }))
log = await readFile(log)
if (stderr) return Promise.reject({ error: stderr, log: log })
webout = await readFile(webout)
if (debug && 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)
}
}

View File

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

View File

@@ -8,6 +8,7 @@ import {
generateUniqueFileName
} from '../utils'
import { configuration } from '../../package.json'
import { SessionController } from './session'
import { promisify } from 'util'
import { execFile } from 'child_process'
const execFilePromise = promisify(execFile)
@@ -21,79 +22,86 @@ export const processSas = async (query: ExecutionQuery): Promise<any> => {
return Promise.reject({ error: 'SAS file does not exist.' })
}
const sasFile: string = sasCodePath.split(path.sep).pop() || 'default'
// FIXME
const sessionController = new SessionController()
const sasLogPath = path.join(
getTmpLogFolderPath(),
generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log')
)
sessionController.getSession()
await createFile(sasLogPath, '')
return Promise.resolve('success')
const sasWeboutPath = path.join(
getTmpWeboutFolderPath(),
generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt')
)
// const sasFile: string = sasCodePath.split(path.sep).pop() || 'default'
await createFile(sasWeboutPath, '')
// const sasLogPath = path.join(
// getTmpLogFolderPath(),
// generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log')
// )
let sasCode = await readFile(sasCodePath)
// await createFile(sasLogPath, '')
const vars: any = query
Object.keys(query).forEach(
(key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`)
)
// const sasWeboutPath = path.join(
// getTmpWeboutFolderPath(),
// generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt')
// )
sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}`
// await createFile(sasWeboutPath, '')
const tmpSasCodePath = sasCodePath.replace(
sasFile,
generateUniqueFileName(sasFile)
)
// let sasCode = await readFile(sasCodePath)
await createFile(tmpSasCodePath, sasCode)
// const vars: any = query
// Object.keys(query).forEach(
// (key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`)
// )
const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
'-SYSIN',
tmpSasCodePath,
'-log',
sasLogPath,
process.platform === 'win32' ? '-nosplash' : ''
]).catch((err) => ({ stderr: err, stdout: '' }))
// sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}`
let log = ''
if (sasLogPath && (await fileExists(sasLogPath))) {
log = await readFile(sasLogPath)
}
// const tmpSasCodePath = sasCodePath.replace(
// sasFile,
// generateUniqueFileName(sasFile)
// )
await deleteFile(sasLogPath)
await deleteFile(tmpSasCodePath)
// await createFile(tmpSasCodePath, sasCode)
if (stderr) return Promise.reject({ error: stderr, log: log })
// const { stdout, stderr } = await execFilePromise(configuration.sasPath, [
// '-SYSIN',
// tmpSasCodePath,
// '-log',
// sasLogPath,
// process.platform === 'win32' ? '-nosplash' : ''
// ]).catch((err) => ({ stderr: err, stdout: '' }))
if (await fileExists(sasWeboutPath)) {
let webout = await readFile(sasWeboutPath)
// let log = ''
// if (sasLogPath && (await fileExists(sasLogPath))) {
// log = await readFile(sasLogPath)
// }
await deleteFile(sasWeboutPath)
// await deleteFile(sasLogPath)
// await deleteFile(tmpSasCodePath)
const debug = Object.keys(query).find(
(key: string) => key.toLowerCase() === '_debug'
)
// if (stderr) return Promise.reject({ error: stderr, log: log })
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>`
}
// if (await fileExists(sasWeboutPath)) {
// let webout = await readFile(sasWeboutPath)
return Promise.resolve(webout)
} else {
return Promise.resolve({
log: log
})
}
// 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
// })
// }
}

109
src/controllers/session.ts Normal file
View File

@@ -0,0 +1,109 @@
import { Session } from '../types'
import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils'
import { createFolder, createFile, generateTimestamp } from '@sasjs/utils'
import path from 'path'
import { ExecutionController } from './executor'
// /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas
// -LOG /tmp/mydir/demo.log
// -SYSIN /tmp/mydir/code.sas
// -AUTOEXEC /tmp/mydir/autoexec.sas
// -WORK /tmp/mydir
// 1. req (_program) for execution
// 2. check available session
// 2.3 spawn one more session
// 2.3.1 create folder
// 2.3.2 create autoexec
// 2.3.3 create _program.sas (empty)
// 2.3.4 /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas -LOG /tmp/sessionFolder/demo.log -SYSIN /tmp/sessionFolder/_program.sas -AUTOEXEC /tmp/sessionFolder/autoexec.sas -WORK /tmp/sessionFolder
// 2.3.5 wait for _program.sas to be deleted
// 2.3.6 add session to the session array
// ---
// 3.0 wait for session
// 3.1 create _program.sas in sessionFolder
// 3.2 poll session array
export class SessionController {
private sessions: Session[] = []
private executionController: ExecutionController
constructor() {
this.executionController = new ExecutionController()
}
public async getSession() {
if (this.sessions.length) {
const session: Session = this.sessions[0]
// TODO: check if session is not expired
return session
}
return await this.createSession()
}
private async createSession() {
if (!this.sessions.length) {
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.1,1);
end;
run;
EOL`
const autoExec = path.join(sessionFolder, 'autoexec.sas')
await createFile(autoExec, autoExecContent)
console.log(`[SessionController about to create first session]`)
this.executionController.execute('', autoExec)
const creationTimeStamp = sessionId.split('-').pop() as string
const session: Session = {
id: sessionId,
available: true,
creationTimeStamp: creationTimeStamp,
deathTimeStamp: (
parseInt(creationTimeStamp) +
15 * 60 * 1000 -
1000
).toString(),
path: sessionFolder
}
console.log(`[SessionController]session: `, session)
this.sessions.push(session)
return session
} else {
return this.sessions[0]
}
}
}
export const getSessionController = () => {
if (process.sessionController) return process.sessionController
process.sessionController = new SessionController()
return process.sessionController
}

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

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

@@ -0,0 +1,7 @@
export interface Session {
id: string
available: boolean
creationTimeStamp: string
deathTimeStamp: string
path: string
}

View File

@@ -1,3 +1,5 @@
// TODO: uppercase types
export * from './sas'
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('')