From 6e0b04a6e548ac31baee726c9249b7e25f50f0bf Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 14:16:22 +0300 Subject: [PATCH] feat(session): add SessionController and ExecutionController --- .vscode/settings.json | 5 ++ src/controllers/executor.ts | 68 ++++++++++++++++++++ src/controllers/index.ts | 1 + src/controllers/sas.ts | 122 +++++++++++++++++++----------------- src/controllers/session.ts | 109 ++++++++++++++++++++++++++++++++ src/types/Process.d.ts | 5 ++ src/types/Session.ts | 7 +++ src/types/index.ts | 2 + src/utils/file.ts | 5 +- 9 files changed, 266 insertions(+), 58 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/controllers/executor.ts create mode 100644 src/controllers/session.ts create mode 100644 src/types/Process.d.ts create mode 100644 src/types/Session.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fb07265 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "autoexec" + ] +} \ No newline at end of file diff --git a/src/controllers/executor.ts b/src/controllers/executor.ts new file mode 100644 index 0000000..db00297 --- /dev/null +++ b/src/controllers/executor.ts @@ -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 = ` +${webout} +
+

SAS Log

+
${log}
+
+` + } + + return Promise.resolve(webout) + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index e90f306..4df137d 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1,3 @@ export * from './sas' export * from './deploy' +export * from './session' diff --git a/src/controllers/sas.ts b/src/controllers/sas.ts index afdd4dc..e30e382 100644 --- a/src/controllers/sas.ts +++ b/src/controllers/sas.ts @@ -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 => { 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 = ` -${webout} -
-

SAS Log

-
${log}
-
-` - } + // 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 = ` + // ${webout} + //
+ //

SAS Log

+ //
${log}
+ //
+ // ` + // } + + // return Promise.resolve(webout) + // } else { + // return Promise.resolve({ + // log: log + // }) + // } } diff --git a/src/controllers/session.ts b/src/controllers/session.ts new file mode 100644 index 0000000..0b17c73 --- /dev/null +++ b/src/controllers/session.ts @@ -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 +} diff --git a/src/types/Process.d.ts b/src/types/Process.d.ts new file mode 100644 index 0000000..501fd16 --- /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/Session.ts b/src/types/Session.ts new file mode 100644 index 0000000..9066363 --- /dev/null +++ b/src/types/Session.ts @@ -0,0 +1,7 @@ +export interface Session { + id: string + available: boolean + creationTimeStamp: string + deathTimeStamp: string + path: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 7897c28..1c326b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +// TODO: uppercase types export * from './sas' 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('')