import { Settings } from './SettingsHelper'; import { CommandType, EnvironmentType } from './../models/PanelSettings'; import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings'; import { window, env as vscodeEnv, ProgressLocation, Uri, commands } from 'vscode'; import { ArticleHelper, Logger, Telemetry } from '.'; import { Folders, WORKSPACE_PLACEHOLDER } from '../commands/Folders'; import { exec, execSync } from 'child_process'; import * as os from 'os'; import { join } from 'path'; import { Notifications } from './Notifications'; import ContentProvider from '../providers/ContentProvider'; import { Dashboard } from '../commands/Dashboard'; import { DashboardCommand } from '../dashboardWebView/DashboardCommand'; import { ParsedFrontMatter } from '../parsers'; import { TelemetryEvent } from '../constants/TelemetryEvent'; import { SETTING_CUSTOM_SCRIPTS } from '../constants'; import { existsAsync } from '../utils'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; export class CustomScript { /** * Retrieve all scripts * @returns */ public static async getScripts(): Promise { const scripts = Settings.get(SETTING_CUSTOM_SCRIPTS) || []; return scripts; } /** * Run a script * @param script * @param path */ public static async run(script: ICustomScript, path: string | null = null): Promise { const wsFolder = Folders.getWorkspaceFolder(); if (wsFolder) { const wsPath = wsFolder.fsPath; if (script.type === ScriptType.MediaFile || script.type === ScriptType.MediaFolder) { Telemetry.send(TelemetryEvent.runMediaScript); await CustomScript.runMediaScript(wsPath, path, script); } else { Telemetry.send(TelemetryEvent.runCustomScript); if (script.bulk) { // Run script on all files await CustomScript.bulkRun(wsPath, script); } else if (path) { // Run script for provided path await CustomScript.singleRun(wsPath, script, path); } else { // Run script on current file. await CustomScript.singleRun(wsPath, script); } } } } /** * Run the script on the current file * @param wsPath * @param script * @param path * @returns */ private static async singleRun( wsPath: string, script: ICustomScript, path: string | null = null ): Promise { let articlePath: string | null = path; let article: ParsedFrontMatter | null | undefined = null; if (!path) { const editor = window.activeTextEditor; if (!editor) return; articlePath = editor.document.uri.fsPath; article = ArticleHelper.getFrontMatter(editor); } else { article = await ArticleHelper.getFrontMatterByPath(path); } if (articlePath && article) { return window.withProgress( { location: ProgressLocation.Notification, title: `Executing: ${script.title}`, cancellable: false }, async () => { const output = await CustomScript.runScript( wsPath, article, articlePath as string, script ); await CustomScript.showOutput(output, script, articlePath); } ); } else { Notifications.warning( l10n.t(LocalizationKey.helpersCustomScriptSingleRunArticleWarning, script.title) ); } } /** * Run the script on multiple files * @param wsPath * @param script * @returns */ private static async bulkRun(wsPath: string, script: ICustomScript): Promise { const folders = await Folders.getInfo(); if (!folders || folders.length === 0) { Notifications.warning( l10n.t(LocalizationKey.helpersCustomScriptBulkRunNoFilesWarning, script.title) ); return; } let output: string[] = []; window.withProgress( { location: ProgressLocation.Notification, title: l10n.t(LocalizationKey.helpersCustomScriptExecuting, script.title), cancellable: false }, async (progress, token) => { for await (const folder of folders) { if (folder.lastModified.length > 0) { for await (const file of folder.lastModified) { try { const article = await ArticleHelper.getFrontMatterByPath(file.filePath); if (article) { const crntOutput = await CustomScript.runScript( wsPath, article, file.filePath, script ); if (crntOutput) { output.push(crntOutput); } } } catch (error) { // Skipping file } } } } await CustomScript.showOutput(output.join(`\n`), script); } ); } /** * Run a script for a media file * @param wsPath * @param path * @param script * @returns */ private static async runMediaScript( wsPath: string, path: string | null, script: ICustomScript ): Promise { if (!path) { Notifications.error( l10n.t(LocalizationKey.helpersCustomScriptRunMediaScriptNoFolderWarning, script.title) ); return; } return new Promise((resolve, reject) => { window.withProgress( { location: ProgressLocation.Notification, title: l10n.t(LocalizationKey.helpersCustomScriptExecuting, script.title), cancellable: false }, async () => { try { const output = await CustomScript.executeScript( script, wsPath, `"${wsPath}" "${path}"` ); await CustomScript.showOutput(output, script); Dashboard.postWebviewMessage({ command: DashboardCommand.mediaUpdate }); return; } catch (e) { Notifications.errorWithOutput(`${script.title} -`); return; } } ); }); } /** * Script runner * @param wsPath * @param article * @param contentPath * @param script * @returns */ private static async runScript( wsPath: string, article: ParsedFrontMatter | null | undefined, contentPath: string, script: ICustomScript ): Promise { try { let articleData = ''; if (os.type() === 'Windows_NT') { const jsonData = JSON.stringify(article?.data); if (script.command && script.command.toLowerCase() === 'powershell') { articleData = `'${jsonData.replace(/"/g, `\\"`)}'`; } else { articleData = `"${jsonData.replace(/"/g, `\\"`)}"`; } } else { articleData = JSON.stringify(article?.data).replace(/'/g, '%27'); articleData = `'${articleData}'`; } const output = await CustomScript.executeScript( script, wsPath, `"${wsPath}" "${contentPath}" ${articleData}` ); return output; } catch (e) { if (typeof e === 'string') { Notifications.error(`${script.title}: ${e}`); } else { Notifications.error(`${script.title}: ${(e as Error).message}`); } return null; } } /** * Show/process the output of the script * @param output * @param script */ private static async showOutput( output: string | null, script: ICustomScript, articlePath?: string | null ): Promise { if (output) { try { const data = JSON.parse(output); if (data.frontmatter) { let article = null; const editor = window.activeTextEditor; if (!articlePath) { if (!editor) return; articlePath = editor.document.uri.fsPath; article = ArticleHelper.getFrontMatter(editor); } else { article = await ArticleHelper.getFrontMatterByPath(articlePath); } if (article && article.data) { for (const key in data.frontmatter) { article.data[key] = data.frontmatter[key]; } if (articlePath) { await ArticleHelper.updateByPath(articlePath, article); } else if (editor) { await ArticleHelper.update(editor, article); } else { Logger.error(`Couldn't update article.`); throw new Error(`Couldn't update article.`); } Notifications.info( l10n.t(LocalizationKey.helpersCustomScriptShowOutputFrontMatterSuccess, script.title) ); } } else if (data.fmAction) { if (data.fmAction === 'open' && data.fmPath) { const uri = data.fmPath.startsWith(`http`) ? data.fmPath : Uri.file(data.fmPath); commands.executeCommand('vscode.open', uri); } } else { Logger.error(`No frontmatter found.`); throw new Error(`No frontmatter found.`); } } catch (error) { if (script.output === 'editor') { ContentProvider.show(output, script.title, script.outputType || 'text'); } else { window .showInformationMessage( `${script.title}: ${output}`, l10n.t(LocalizationKey.helpersCustomScriptShowOutputCopyOutputAction) ) .then((value) => { if (value === l10n.t(LocalizationKey.helpersCustomScriptShowOutputCopyOutputAction)) { vscodeEnv.clipboard.writeText(output); } }); } } } else { Notifications.info( l10n.t(LocalizationKey.helpersCustomScriptShowOutputSuccess, script.title) ); } } /** * Execute script * @param script * @param wsPath * @param args * @returns */ public static async executeScript( script: ICustomScript, wsPath: string, args: string ): Promise { const osType = os.type(); // Check the command to use let command = script.nodeBin || 'node'; if (script.command && script.command !== CommandType.Node) { command = script.command; } let scriptPath = join(wsPath, script.script); if (script.script.includes(WORKSPACE_PLACEHOLDER)) { scriptPath = Folders.getAbsFilePath(script.script); } // Check if there is an environments overwrite required if (script.environments) { let crntType: EnvironmentType | null = null; if (osType === 'Windows_NT') { crntType = 'windows'; } else if (osType === 'Darwin') { crntType = 'macos'; } else { crntType = 'linux'; } const environment = script.environments.find((e) => e.type === crntType); if (environment && environment.script && environment.command) { if (await CustomScript.validateCommand(environment.command)) { command = environment.command; scriptPath = join(wsPath, environment.script); if (environment.script.includes(WORKSPACE_PLACEHOLDER)) { scriptPath = Folders.getAbsFilePath(environment.script); } } } } if (!(await existsAsync(scriptPath))) { Logger.error(`Script not found: ${scriptPath}`); throw new Error(`Script not found: ${scriptPath}`); } if (osType === 'Windows_NT' && command.toLowerCase() === 'powershell') { command = `${command} -File`; } const fullScript = `${command} "${scriptPath}" ${args}`; Logger.info(l10n.t(LocalizationKey.helpersCustomScriptExecuting, fullScript)); const output: string = await CustomScript.executeScriptAsync(fullScript, wsPath); try { const data = JSON.parse(output); if (data.questions) { const answers: string[] = []; for (const question of data.questions) { if (question.name && question.message) { let answer; if (question.options) { answer = await window.showQuickPick(question.options, { title: question.message, ignoreFocusOut: true, canPickMany: false }); } else { answer = await window.showInputBox({ prompt: question.message, value: question.default || '', ignoreFocusOut: true, title: question.message }); } if (answer) { answers.push(`${question.name}="${answer}"`); } else { return ''; } } } if (answers.length > 0) { const newScript = `${fullScript} ${answers.join(' ')}`; return await CustomScript.executeScriptAsync(newScript, wsPath); } } } catch (error) { // Not a JSON } return output; } /** * Execute script async * @param fullScript * @param wsPath * @returns */ private static async executeScriptAsync(fullScript: string, wsPath: string): Promise { return new Promise(async (resolve, reject) => { exec(fullScript, { cwd: wsPath }, (error, stdout) => { if (error) { Logger.error(error.message); reject(error.message); return; } if (stdout && stdout.endsWith(`\n`)) { // Remove empty line at the end of the string stdout = stdout.slice(0, -1); } resolve(stdout); }); }); } /** * Validate if the command is exists * @param command * @returns */ private static async validateCommand(command: string) { try { execSync(command); return true; } catch (e) { Logger.error(l10n.t(LocalizationKey.helpersCustomScriptValidateCommandError, command)); return false; } } }