diff --git a/src/rmarkdown/knit.ts b/src/rmarkdown/knit.ts index 39f906546..25a5afbd0 100644 --- a/src/rmarkdown/knit.ts +++ b/src/rmarkdown/knit.ts @@ -4,7 +4,7 @@ import * as fs from 'fs-extra'; import path = require('path'); import yaml = require('js-yaml'); -import { RMarkdownManager, KnitWorkingDirectory } from './manager'; +import { RMarkdownManager, KnitWorkingDirectory, DisposableProcess } from './manager'; import { runTextInTerm } from '../rTerminal'; import { rmdPreviewManager } from '../extension'; @@ -26,23 +26,21 @@ interface IYamlFrontmatter { } export class RMarkdownKnitManager extends RMarkdownManager { - private async renderDocument(rPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string) { + private async renderDocument(rDocumentPath: string, docPath: string, docName: string, yamlParams: IYamlFrontmatter, outputFormat?: string): Promise { const openOutfile: boolean = util.config().get('rmarkdown.knit.openOutputFile') ?? false; const knitWorkingDir = this.getKnitDir(knitDir, docPath); const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`; - const knitCommand = await this.getKnitCommand(yamlParams, rPath, outputFormat); - this.rPath = await util.getRpath(true); + const knitCommand = await this.getKnitCommand(yamlParams, rDocumentPath, outputFormat); + this.rPath = await util.getRpath(); const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'gms'); - const cmd = ( - `${this.rPath} --silent --slave --no-save --no-restore ` + - `-e "knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText})" ` + - `-e "cat('${lim}', ` + + const cmd = + `knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText});` + + `cat('${lim}', ` + `${knitCommand}, ` + `'${lim}',` + - `sep='')"` - ); + `sep='')`; const callback = (dat: string) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); @@ -68,7 +66,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { return await this.knitWithProgress( { fileName: docName, - filePath: rPath, + filePath: rDocumentPath, cmd: cmd, rCmd: knitCommand, rOutputFormat: outputFormat, @@ -122,7 +120,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { `rmarkdown::render(${docPath})`; } - return knitCommand.replace(/['"]/g, '\\"'); + return knitCommand.replace(/['"]/g, '\''); } // check if the workspace of the document is a R Markdown site. @@ -217,12 +215,12 @@ export class RMarkdownKnitManager extends RMarkdownManager { const isSaved = await util.saveDocument(wad); if (isSaved) { - let rPath = util.ToRStringLiteral(wad.fileName, '"'); + let rDocumentPath = util.ToRStringLiteral(wad.fileName, '"'); let encodingParam = util.config().get('source.encoding'); encodingParam = `encoding = "${encodingParam}"`; - rPath = [rPath, encodingParam].join(', '); + rDocumentPath = [rDocumentPath, encodingParam].join(', '); if (echo) { - rPath = [rPath, 'echo = TRUE'].join(', '); + rDocumentPath = [rDocumentPath, 'echo = TRUE'].join(', '); } // allow users to opt out of background process @@ -233,7 +231,7 @@ export class RMarkdownKnitManager extends RMarkdownManager { } else { this.busyUriStore.add(busyPath); await this.renderDocument( - rPath, + rDocumentPath, wad.uri.fsPath, path.basename(wad.uri.fsPath), this.getYamlFrontmatter(wad.uri.fsPath), @@ -243,9 +241,9 @@ export class RMarkdownKnitManager extends RMarkdownManager { } } else { if (outputFormat === undefined) { - void runTextInTerm(`rmarkdown::render(${rPath})`); + void runTextInTerm(`rmarkdown::render(${rDocumentPath})`); } else { - void runTextInTerm(`rmarkdown::render(${rPath}, "${outputFormat}")`); + void runTextInTerm(`rmarkdown::render(${rDocumentPath}, '${outputFormat}')`); } } } diff --git a/src/rmarkdown/manager.ts b/src/rmarkdown/manager.ts index 7de093958..0ef0a45a1 100644 --- a/src/rmarkdown/manager.ts +++ b/src/rmarkdown/manager.ts @@ -8,6 +8,15 @@ export enum KnitWorkingDirectory { workspaceRoot = 'workspace root', } +export type DisposableProcess = cp.ChildProcessWithoutNullStreams & vscode.Disposable; + +export interface IKnitRejection { + cp: DisposableProcess; + wasCancelled: boolean; +} + +const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); + interface IKnitArgs { filePath: string; fileName: string; @@ -18,13 +27,6 @@ interface IKnitArgs { onRejection?: (...args: unknown[]) => unknown; } -export interface IKnitRejection { - cp: cp.ChildProcessWithoutNullStreams; - wasCancelled: boolean; -} - -const rMarkdownOutput: vscode.OutputChannel = vscode.window.createOutputChannel('R Markdown'); - export abstract class RMarkdownManager { protected rPath: string = undefined; protected rMarkdownOutput: vscode.OutputChannel = rMarkdownOutput; @@ -51,18 +53,44 @@ export abstract class RMarkdownManager { } } - protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress): Promise { + protected async knitDocument(args: IKnitArgs, token?: vscode.CancellationToken, progress?: vscode.Progress): Promise { // vscode.Progress auto-increments progress, so we use this // variable to set progress to a specific number let currentProgress = 0; - return await new Promise( + let printOutput = true; + + return await new Promise( (resolve, reject) => { const cmd = args.cmd; const fileName = args.fileName; - let childProcess: cp.ChildProcessWithoutNullStreams; + const processArgs = [ + `--silent`, + `--slave`, + `--no-save`, + `--no-restore`, + `-e`, + cmd + ]; + const processOptions = { + env: process.env + }; + + let childProcess: DisposableProcess; try { - childProcess = cp.exec(cmd); + childProcess = util.asDisposable( + cp.spawn( + `${this.rPath}`, + processArgs, + processOptions + ), + () => { + if (childProcess.kill('SIGKILL')) { + rMarkdownOutput.appendLine('[VSC-R] terminating R process'); + printOutput = false; + } + } + ); progress.report({ increment: 0, message: '0%' @@ -81,9 +109,13 @@ export abstract class RMarkdownManager { childProcess.stdout.on('data', (data: Buffer) => { const dat = data.toString('utf8'); - this.rMarkdownOutput.appendLine(dat); + if (printOutput) { + this.rMarkdownOutput.appendLine(dat); + + } const percentRegex = /[0-9]+(?=%)/g; const percentRegOutput = dat.match(percentRegex); + if (percentRegOutput) { for (const item of percentRegOutput) { const perc = Number(item); @@ -108,7 +140,9 @@ export abstract class RMarkdownManager { childProcess.stderr.on('data', (data: Buffer) => { const dat = data.toString('utf8'); - this.rMarkdownOutput.appendLine(dat); + if (printOutput) { + this.rMarkdownOutput.appendLine(dat); + } }); childProcess.on('exit', (code, signal) => { @@ -126,10 +160,10 @@ export abstract class RMarkdownManager { ); } - protected async knitWithProgress(args: IKnitArgs): Promise { - let childProcess: cp.ChildProcessWithoutNullStreams = undefined; + protected async knitWithProgress(args: IKnitArgs): Promise { + let childProcess: DisposableProcess = undefined; await util.doWithProgress(async (token: vscode.CancellationToken, progress: vscode.Progress) => { - childProcess = await this.knitDocument(args, token, progress) as cp.ChildProcessWithoutNullStreams; + childProcess = await this.knitDocument(args, token, progress) as DisposableProcess; }, vscode.ProgressLocation.Notification, `Knitting ${args.fileName} ${args.rOutputFormat ? 'to ' + args.rOutputFormat : ''} `, @@ -140,8 +174,8 @@ export abstract class RMarkdownManager { this.rMarkdownOutput.show(true); } // this can occur when a successfuly knitted document is later altered (while still being previewed) and subsequently fails to knit - args?.onRejection ? args.onRejection(args.filePath, rejection) : - rejection?.cp.kill('SIGKILL'); + args?.onRejection?.(args.filePath, rejection); + rejection.cp?.dispose(); }); return childProcess; } diff --git a/src/rmarkdown/preview.ts b/src/rmarkdown/preview.ts index 571c4f1f9..4f72ecd8b 100644 --- a/src/rmarkdown/preview.ts +++ b/src/rmarkdown/preview.ts @@ -1,5 +1,3 @@ - -import * as cp from 'child_process'; import * as vscode from 'vscode'; import * as fs from 'fs-extra'; import * as cheerio from 'cheerio'; @@ -11,11 +9,11 @@ import crypto = require('crypto'); import { config, readContent, setContext, escapeHtml, UriIcon, saveDocument, getRpath } from '../util'; import { extensionContext, tmpDir } from '../extension'; import { knitDir } from './knit'; -import { IKnitRejection, RMarkdownManager } from './manager'; +import { DisposableProcess, RMarkdownManager } from './manager'; class RMarkdownPreview extends vscode.Disposable { title: string; - cp: cp.ChildProcessWithoutNullStreams; + cp: DisposableProcess; panel: vscode.WebviewPanel; resourceViewColumn: vscode.ViewColumn; outputUri: vscode.Uri; @@ -25,7 +23,7 @@ class RMarkdownPreview extends vscode.Disposable { autoRefresh: boolean; mtime: number; - constructor(title: string, cp: cp.ChildProcessWithoutNullStreams, panel: vscode.WebviewPanel, + constructor(title: string, cp: DisposableProcess, panel: vscode.WebviewPanel, resourceViewColumn: vscode.ViewColumn, outputUri: vscode.Uri, filePath: string, RMarkdownPreviewManager: RMarkdownPreviewManager, useCodeTheme: boolean, autoRefresh: boolean) { super(() => { @@ -268,7 +266,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { toUpdate?.cp?.kill('SIGKILL'); if (toUpdate) { - const childProcess: cp.ChildProcessWithoutNullStreams | void = await this.previewDocument(previewUri, toUpdate.title).catch(() => { + const childProcess: DisposableProcess | void = await this.previewDocument(previewUri, toUpdate.title).catch(() => { void vscode.window.showErrorMessage('There was an error in knitting the document. Please check the R Markdown output stream.'); this.rMarkdownOutput.show(true); this.previewStore.delete(previewUri); @@ -283,26 +281,25 @@ export class RMarkdownPreviewManager extends RMarkdownManager { } - private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn) { + private async previewDocument(filePath: string, fileName?: string, viewer?: vscode.ViewColumn, currentViewColumn?: vscode.ViewColumn): Promise { const knitWorkingDir = this.getKnitDir(knitDir, filePath); const knitWorkingDirText = knitWorkingDir ? `'${knitWorkingDir}'` : `NULL`; - this.rPath = await getRpath(true); + this.rPath = await getRpath(); const lim = '---vsc---'; const re = new RegExp(`.*${lim}(.*)${lim}.*`, 'ms'); const outputFile = path.join(tmpDir(), crypto.createHash('sha256').update(filePath).digest('hex') + '.html'); const cmd = ( - `${this.rPath} --silent --slave --no-save --no-restore ` + - `-e "knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText})" ` + - `-e "cat('${lim}', rmarkdown::render(` + + `knitr::opts_knit[['set']](root.dir = ${knitWorkingDirText});` + + `cat('${lim}', rmarkdown::render(` + `'${filePath.replace(/\\/g, '/')}',` + `output_format = rmarkdown::html_document(),` + `output_file = '${outputFile.replace(/\\/g, '/')}',` + `intermediates_dir = '${tmpDir().replace(/\\/g, '/')}'), '${lim}',` + - `sep='')"` + `sep='')` ); - const callback = (dat: string, childProcess: cp.ChildProcessWithoutNullStreams) => { + const callback = (dat: string, childProcess: DisposableProcess) => { const outputUrl = re.exec(dat)?.[0]?.replace(re, '$1'); if (outputUrl) { if (viewer !== undefined) { @@ -322,11 +319,9 @@ export class RMarkdownPreviewManager extends RMarkdownManager { return false; }; - const onRejected = (filePath: string, rejection: IKnitRejection) => { + const onRejected = (filePath: string) => { if (this.previewStore.has(filePath)) { this.previewStore.delete(filePath); - } else { - rejection.cp.kill('SIGKILL'); } }; @@ -342,7 +337,7 @@ export class RMarkdownPreviewManager extends RMarkdownManager { ); } - private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: cp.ChildProcessWithoutNullStreams, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { + private openPreview(outputUri: vscode.Uri, filePath: string, title: string, cp: DisposableProcess, viewer: vscode.ViewColumn, resourceViewColumn: vscode.ViewColumn, autoRefresh: boolean): void { const panel = vscode.window.createWebviewPanel( 'previewRmd', diff --git a/src/util.ts b/src/util.ts index cffeeb3a8..9b14f13e5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -371,3 +371,20 @@ export class UriIcon { this.light = vscode.Uri.file(path.join(extIconPath, 'light', id + '.svg')); } } + +/** + * As Disposable. + * + * Create a dispose method for any given object, and push it to the + * extension subscriptions array + * + * @param {T} toDispose - the object to add dispose to + * @param {Function} disposeFunction - the method called when the object is disposed + * @returns returned object is considered types T and vscode.Disposable + */ +export function asDisposable(toDispose: T, disposeFunction: (...args: unknown[]) => unknown): T & vscode.Disposable { + type disposeType = T & vscode.Disposable; + (toDispose as disposeType).dispose = () => disposeFunction(); + extensionContext.subscriptions.push(toDispose as disposeType); + return toDispose as disposeType; +}