From 6e0b04a6e548ac31baee726c9249b7e25f50f0bf Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 14:16:22 +0300 Subject: [PATCH 1/8] 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('') From 37b6936cca3cff9c1ca26ec7b4b938a357c448df Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 14:35:47 +0300 Subject: [PATCH 2/8] fix(ts): enable files --- tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) 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 } } From 8b2564120def137f80647064e28062b880d58efe Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:31:46 +0000 Subject: [PATCH 3/8] feat(execution): add ExecutionController working with session --- src/controllers/Execution.ts | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/controllers/Execution.ts 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} +
+

SAS Log

+
${log}
+
+` + } + + session.inUse = false + + sessionController.deleteSession(session) + + return Promise.resolve(webout) + } +} From 6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:32:16 +0000 Subject: [PATCH 4/8] feat(session): add SessionController --- src/controllers/Session.ts | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/controllers/Session.ts 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 +} From 9cf02b25d03acee9df3052f5b4a4850395ad3823 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:34:08 +0000 Subject: [PATCH 5/8] refactor: improve types and imports --- src/controllers/deploy.ts | 1 + src/controllers/executor.ts | 68 --------------- src/controllers/index.ts | 4 +- src/controllers/sas.ts | 107 ------------------------ src/controllers/session.ts | 109 ------------------------- src/types/{sas.ts => Execution.ts} | 0 src/types/{fileTree.ts => FileTree.ts} | 0 src/types/Process.d.ts | 2 +- src/types/{request.ts => Request.ts} | 1 + src/types/Session.ts | 3 +- src/types/index.ts | 6 +- 11 files changed, 10 insertions(+), 291 deletions(-) delete mode 100644 src/controllers/executor.ts delete mode 100644 src/controllers/sas.ts delete mode 100644 src/controllers/session.ts rename src/types/{sas.ts => Execution.ts} (100%) rename src/types/{fileTree.ts => FileTree.ts} (100%) rename src/types/{request.ts => Request.ts} (99%) 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/executor.ts b/src/controllers/executor.ts deleted file mode 100644 index db00297..0000000 --- a/src/controllers/executor.ts +++ /dev/null @@ -1,68 +0,0 @@ -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 4df137d..2990104 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ -export * from './sas' export * from './deploy' -export * from './session' +export * from './Session' +export * from './Execution' diff --git a/src/controllers/sas.ts b/src/controllers/sas.ts deleted file mode 100644 index e30e382..0000000 --- a/src/controllers/sas.ts +++ /dev/null @@ -1,107 +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 { SessionController } from './session' -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.' }) - } - - // FIXME - const sessionController = new SessionController() - - sessionController.getSession() - - return Promise.resolve('success') - - // 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} - //
- //

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 deleted file mode 100644 index 0b17c73..0000000 --- a/src/controllers/session.ts +++ /dev/null @@ -1,109 +0,0 @@ -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/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 index 501fd16..50313f7 100644 --- a/src/types/Process.d.ts +++ b/src/types/Process.d.ts @@ -1,5 +1,5 @@ declare namespace NodeJS { export interface Process { - sessionController?: import('../controllers/session').SessionController + 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 index 9066363..5484311 100644 --- a/src/types/Session.ts +++ b/src/types/Session.ts @@ -1,7 +1,8 @@ export interface Session { id: string - available: boolean + ready: boolean creationTimeStamp: string deathTimeStamp: string path: string + inUse: boolean } diff --git a/src/types/index.ts b/src/types/index.ts index 1c326b2..2377b59 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ // TODO: uppercase types -export * from './sas' -export * from './request' -export * from './fileTree' +export * from './Execution' +export * from './Request' +export * from './FileTree' export * from './Session' From 129cb7c128155e72d83341589c23af92fdfad59c Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:34:37 +0000 Subject: [PATCH 6/8] chore(utils): add sleep util --- src/routes/index.ts | 38 +++++++++++++++----------------------- src/utils/sleep.ts | 3 +++ 2 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 src/utils/sleep.ts 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/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)) +} From ba0722b98c41ab7452a56c468090f90260028fc3 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:35:10 +0000 Subject: [PATCH 7/8] chore(utils): add sleep export --- src/utils/index.ts | 1 + 1 file changed, 1 insertion(+) 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' From 00c7d2150c87639764d3d2658642e2221a8309c7 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:40:01 +0000 Subject: [PATCH 8/8] chore(git): add blank line to the file end --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fb07265..9a770f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "cSpell.words": [ "autoexec" ] -} \ No newline at end of file +}