UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

406 lines 16.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.RShell = exports.DEFAULT_R_PATH = exports.DEFAULT_OUTPUT_COLLECTOR_CONFIGURATION = void 0; exports.getDefaultRShellOptions = getDefaultRShellOptions; const child_process_1 = require("child_process"); const objects_1 = require("../util/objects"); const readline = __importStar(require("readline")); const log_1 = require("../util/log"); const preload_1 = __importDefault(require("semver/preload")); const os_1 = require("../util/os"); const fs_1 = __importDefault(require("fs")); const init_1 = require("./init"); const convert_values_1 = require("./lang-4.x/convert-values"); const retriever_1 = require("./retriever"); exports.DEFAULT_OUTPUT_COLLECTOR_CONFIGURATION = { from: 'stdout', postamble: `🐧${'-'.repeat(5)}🐧`, timeout: { ms: 750_000, resetOnNewData: true }, keepPostamble: false, automaticallyTrimOutput: true, errorStopsWaiting: true }; exports.DEFAULT_R_PATH = (0, os_1.getPlatform)() === 'windows' ? 'R.exe' : 'R'; let DEFAULT_R_SHELL_OPTIONS = undefined; /** * Get the default RShell options, possibly using the given config to override some values */ function getDefaultRShellOptions(config) { if (!DEFAULT_R_SHELL_OPTIONS) { DEFAULT_R_SHELL_OPTIONS = { pathToRExecutable: config?.rPath ?? exports.DEFAULT_R_PATH, // -s is a short form of --no-echo (and the old version --slave), but this one works in R 3 and 4 // (see https://github.com/wch/r-source/commit/f1ff49e74593341c74c20de9517f31a22c8bcb04) commandLineOptions: ['--vanilla', '--quiet', '--no-save', '-s'], cwd: process.cwd(), env: undefined, eol: '\n', homeLibPath: (0, os_1.getPlatform)() === 'windows' ? undefined : '~/.r-libs', sessionName: 'default', revive: 0 /* RShellReviveOptions.Never */, onRevive: () => { } }; } return DEFAULT_R_SHELL_OPTIONS; } /** * The `RShell` represents an interactive session with the R interpreter. * You can configure it by {@link RShellOptions}. * * At the moment we are using a live R session (and not networking etc.) to communicate with R easily, * which allows us to install packages etc. However, this might and probably will change in the future * (leaving this as a legacy mode :D) */ class RShell { name = 'r-shell'; async = true; options; session; log; versionCache = null; // should never be more than one, but let's be sure tempDirs = new Set(); constructor(config, options) { this.options = { ...getDefaultRShellOptions(config), ...options }; this.log = log_1.log.getSubLogger({ name: this.options.sessionName }); this.session = new RShellSession(this.options, this.log); this.revive(); } parse(request) { return (0, retriever_1.retrieveParseDataFromRCode)(request, this); } information() { return { name: 'r-shell', rVersion: async () => await this.rVersion(), sendCommandWithOutput: (command, addonConfig) => { return this.sendCommandWithOutput(command, addonConfig); } }; } revive() { if (this.options.revive === 0 /* RShellReviveOptions.Never */) { return; } this.session.onExit((code, signal) => { if (this.options.revive === 2 /* RShellReviveOptions.Always */ || (this.options.revive === 1 /* RShellReviveOptions.OnError */ && code !== 0)) { this.log.warn(`R session exited with code ${code}, reviving!`); this.options.onRevive(code, signal); this.session = new RShellSession(this.options, this.log); this.revive(); } }); } /** * sends the given command directly to the current R session * will not do anything to alter input markers! */ sendCommand(command) { if (this.log.settings.minLevel <= 1 /* LogLevel.Trace */) { this.log.trace(`> ${JSON.stringify(command)}`); } this._sendCommand(command); } async rVersion() { return (await this.usedRVersion())?.format() ?? 'unknown'; } async usedRVersion() { if (this.versionCache !== null) { return this.versionCache; } // retrieve raw version: const result = await this.sendCommandWithOutput(`cat(paste0(R.version$major,".",R.version$minor), ${(0, convert_values_1.ts2r)(this.options.eol)})`, { timeout: { ms: 5000, resetOnNewData: false, // just resolve on timeout and handle the empty array case below onTimeout: resolve => resolve([]) } }); (0, log_1.expensiveTrace)(this.log, () => `raw version: ${JSON.stringify(result)}`); if (result.length === 1) { this.versionCache = preload_1.default.coerce(result[0]); return this.versionCache; } else { return null; } } injectLibPaths(...paths) { (0, log_1.expensiveTrace)(this.log, () => `injecting lib paths ${JSON.stringify(paths)}`); this._sendCommand(`.libPaths(c(.libPaths(), ${paths.map(convert_values_1.ts2r).join(',')}))`); } tryToInjectHomeLibPath() { // ensure the path exists first if (this.options.homeLibPath === undefined) { this.log.debug('ensuring home lib path exists (automatic inject)'); this.sendCommand('if(!dir.exists(Sys.getenv("R_LIBS_USER"))) { dir.create(path=Sys.getenv("R_LIBS_USER"),showWarnings=FALSE,recursive=TRUE) }'); this.sendCommand('.libPaths(c(.libPaths(), Sys.getenv("R_LIBS_USER")))'); } else { this.injectLibPaths(this.options.homeLibPath); } } /** * checks if a given package is already installed on the system! */ async isPackageInstalled(packageName) { this.log.debug(`checking if package "${packageName}" is installed`); const result = await this.sendCommandWithOutput(`cat(system.file(package="${packageName}")!="","${this.options.eol}")`); return result.length === 1 && result[0] === 'TRUE'; } /** * Send a command and collect the output * @param command - The R command to execute (similar to {@link sendCommand}) * @param addonConfig - Further configuration on how and what to collect: see {@link OutputCollectorConfiguration}, * defaults are set in {@link DEFAULT_OUTPUT_COLLECTOR_CONFIGURATION} */ async sendCommandWithOutput(command, addonConfig) { const config = (0, objects_1.deepMergeObject)(exports.DEFAULT_OUTPUT_COLLECTOR_CONFIGURATION, addonConfig); (0, log_1.expensiveTrace)(this.log, () => `> ${JSON.stringify(command)}`); const output = await this.session.collectLinesUntil(config.from, { predicate: data => data === config.postamble, includeInResult: config.keepPostamble // we do not want the postamble }, config.timeout, () => { this._sendCommand(command); if (config.from === 'stderr') { this._sendCommand(`cat("${config.postamble}${this.options.eol}",file=stderr())`); } else { this._sendCommand(`cat("${config.postamble}${this.options.eol}")`); } }); if (config.automaticallyTrimOutput) { return output.map(line => line.trim()); } else { return output; } } /** * execute multiple commands in order * @see sendCommand */ sendCommands(...commands) { for (const element of commands) { this.sendCommand(element); } } /** * clears the R environment using the `rm` command. */ clearEnvironment() { this.log.debug('clearing environment'); // run rm(list=ls()) but ignore 'flowr_get_ast', which is the compile command installed this._sendCommand('rm(list=setdiff(ls(), "flowr_get_ast"))'); } /** * Obtain the temporary directory used by R. * Additionally, this marks the directory for removal when the shell exits. */ async obtainTmpDir() { this.sendCommand('temp<-tempdir()'); const [tempdir] = await this.sendCommandWithOutput(`cat(temp,${(0, convert_values_1.ts2r)(this.options.eol)})`); this.tempDirs.add(tempdir); return tempdir; } /** * Close the current R session, makes the object effectively invalid (can no longer be reopened etc.) * @returns true if the operation succeeds, false otherwise */ close() { return this.session.end([...this.tempDirs]); } _sendCommand(command) { this.session.writeLine(command); } } exports.RShell = RShell; /** * Used to deal with the underlying input-output streams of the R process */ class RShellSession { bareSession; sessionStdOut; sessionStdErr; options; collectionTimeout; constructor(options, log) { this.bareSession = (0, child_process_1.spawn)(options.pathToRExecutable, options.commandLineOptions, { env: options.env, cwd: options.cwd, windowsHide: true }); this.sessionStdOut = readline.createInterface({ input: this.bareSession.stdout, terminal: false }); this.sessionStdErr = readline.createInterface({ input: this.bareSession.stderr, terminal: false }); this.onExit(() => { this.end(); }); this.options = options; // initialize the session this.writeLine((0, init_1.initCommand)(options.eol)); if (log.settings.minLevel <= 1 /* LogLevel.Trace */) { this.bareSession.stdout.on('data', (data) => { log.trace(`< ${data.toString()}`); }); this.bareSession.on('close', (code) => { log.trace(`session exited with code ${code}`); }); } this.bareSession.stderr.on('data', (data) => { log.warn(`< ${data}`); }); } write(data) { this.bareSession.stdin.write(data); } writeLine(data) { this.write(`${data}${this.options.eol}`); } /** * Collect lines from the selected streams until the given condition is met or the timeout is reached * * This method does allow other listeners to consume the same input * @param from - The stream(s) to collect the information from * @param until - If the predicate returns true, this will stop the collection and resolve the promise * @param timeout - Configuration for how and when to timeout * @param action - Event to be performed after all listeners are installed, this might be the action that triggers the output you want to collect */ async collectLinesUntil(from, until, timeout, action) { const result = []; let handler; let error; return await new Promise((resolve, reject) => { const makeTimer = () => setTimeout(() => { if (timeout.onTimeout) { timeout.onTimeout(resolve, reject, result); } else { reject(new Error(`timeout of ${timeout.ms}ms reached (${JSON.stringify(result)})`)); } }, timeout.ms); this.collectionTimeout = makeTimer(); handler = (data) => { const end = until.predicate(data); if (!end || until.includeInResult) { result.push(data); } if (end) { clearTimeout(this.collectionTimeout); resolve(result); } else if (timeout.resetOnNewData) { clearTimeout(this.collectionTimeout); this.collectionTimeout = makeTimer(); } }; error = () => { resolve(result); }; this.onExit(error); this.on(from, 'line', handler); action?.(); }).finally(() => { this.removeListener(from, 'line', handler); this.bareSession.removeListener('exit', error); this.bareSession.stdin.removeListener('error', error); }); } /** * close the current R session, makes the object effectively invalid (can no longer be reopened etc.) * @param filesToUnlink - If set, these files will be unlinked before closing the session (e.g., to clean up tempfiles) * @returns true if the kill succeeds, false otherwise * @see RShell#close */ end(filesToUnlink) { if (filesToUnlink !== undefined) { log_1.log.info(`unlinking ${filesToUnlink.length} files (${JSON.stringify(filesToUnlink)})`); for (const f of filesToUnlink) { try { fs_1.default.rmSync(f, { recursive: true, force: true }); } catch { log_1.log.error(`failed to unlink file ${f}`); } } } const killResult = this.bareSession.kill(); if (this.collectionTimeout !== undefined) { clearTimeout(this.collectionTimeout); } this.sessionStdOut.close(); this.sessionStdErr.close(); log_1.log.info(`killed R session with pid ${this.bareSession.pid ?? '<unknown>'} and result ${killResult ? 'successful' : 'failed'} (including streams)`); return killResult; } onExit(callback) { this.bareSession.on('exit', callback); this.bareSession.stdin.on('error', callback); } // eslint-disable-next-line @typescript-eslint/no-explicit-any on(from, event, listener) { const both = from === 'both'; if (both || from === 'stdout') { this.sessionStdOut.on(event, listener); } if (both || from === 'stderr') { this.sessionStdErr.on(event, listener); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any removeListener(from, event, listener) { const both = from === 'both'; if (both || from === 'stdout') { this.sessionStdOut.removeListener(event, listener); } if (both || from === 'stderr') { this.sessionStdErr.removeListener(event, listener); } } } //# sourceMappingURL=shell.js.map