@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
406 lines • 16.3 kB
JavaScript
"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