/// /// 'use strict'; // TODO: Cleanup this place // Add options for execPythonFile import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import * as child_process from 'child_process'; import * as settings from './configSettings'; import { CancellationToken } from 'vscode'; import { isNotInstalledError } from './helpers'; export const IS_WINDOWS = /^win/.test(process.platform); const PATH_VARIABLE_NAME = IS_WINDOWS ? 'Path' : 'PATH'; const PathValidity: Map = new Map(); export function validatePath(filePath: string): Promise { if (filePath.length === 0) { return Promise.resolve(''); } if (PathValidity.has(filePath)) { return Promise.resolve(PathValidity.get(filePath) ? filePath : ''); } return new Promise(resolve => { fs.exists(filePath, exists => { PathValidity.set(filePath, exists); return resolve(exists ? filePath : ''); }); }); } let pythonInterpretterDirectory: string = null; let previouslyIdentifiedPythonPath: string = null; let customEnvVariables: any = null; // If config settings change then clear env variables that we have cached // Remember, the path to the python interpreter can change, hence we need to re-set the paths settings.PythonSettings.getInstance().on('change', function () { customEnvVariables = null; }); export function getPythonInterpreterDirectory(): Promise { // If we already have it and the python path hasn't changed, yay if (pythonInterpretterDirectory && previouslyIdentifiedPythonPath === settings.PythonSettings.getInstance().pythonPath) { return Promise.resolve(pythonInterpretterDirectory); } return new Promise(resolve => { let pythonFileName = settings.PythonSettings.getInstance().pythonPath; // Check if we have the path if (path.basename(pythonFileName) === pythonFileName) { // No path provided return resolve(''); } // If we can execute the python, then get the path from the fully qualified name child_process.execFile(pythonFileName, ['-c', 'print(1234)'], (error, stdout, stderr) => { // Yes this is a valid python path if (stdout.startsWith('1234')) { return resolve(path.dirname(pythonFileName)); } // No idea, didn't work, hence don't reject, but return empty path resolve(''); }); }).then(value => { // Cache and return previouslyIdentifiedPythonPath = settings.PythonSettings.getInstance().pythonPath; return pythonInterpretterDirectory = value; }).catch(() => { // Don't care what the error is, all we know is that this doesn't work return pythonInterpretterDirectory = ''; }); } export function execPythonFile(file: string, args: string[], cwd: string, includeErrorAsResponse: boolean = false, stdOut: (line: string) => void = null, token?: CancellationToken): Promise { // If running the python file, then always revert to execFileInternal // Cuz python interpreter is always a file and we can and will always run it using child_process.execFile() if (file === settings.PythonSettings.getInstance().pythonPath) { if (stdOut) { return spawnFileInternal(file, args, { cwd }, includeErrorAsResponse, stdOut, token); } return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse); } return getPythonInterpreterDirectory().then(pyPath => { // We don't have a path if (pyPath.length === 0) { if (stdOut) { return spawnFileInternal(file, args, { cwd }, includeErrorAsResponse, stdOut, token); } return execFileInternal(file, args, { cwd: cwd }, includeErrorAsResponse); } if (customEnvVariables === null) { // Ensure to include the path of the current python let newPath = ''; if (IS_WINDOWS) { newPath = pyPath + '\\' + path.delimiter + path.join(pyPath, 'Scripts\\') + path.delimiter + process.env[PATH_VARIABLE_NAME]; // This needs to be done for windows process.env[PATH_VARIABLE_NAME] = newPath; } else { newPath = pyPath + path.delimiter + process.env[PATH_VARIABLE_NAME]; } let customSettings = <{ [key: string]: string }>{}; customSettings[PATH_VARIABLE_NAME] = newPath; customEnvVariables = mergeEnvVariables(customSettings); } if (stdOut) { return spawnFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse, stdOut, token); } return execFileInternal(file, args, { cwd, env: customEnvVariables }, includeErrorAsResponse); }); } function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string): Promise { return new Promise((resolve, reject) => { if (isNotInstalledError(error)) { return reject(error); } // pylint: // In the case of pylint we have some messages (such as config file not found and using default etc...) being returned in stderr // These error messages are useless when using pylint if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { return resolve(stdout + '\n' + stderr); } let hasErrors = (error && error.message.length > 0) || (stderr && stderr.length > 0); if (hasErrors && (typeof stdout !== 'string' || stdout.length === 0)) { let errorMsg = (error && error.message) ? error.message : (stderr && stderr.length > 0 ? stderr + '' : ''); return reject(errorMsg); } resolve(stdout + ''); }); } function execFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean): Promise { return new Promise((resolve, reject) => { child_process.execFile(file, args, options, (error, stdout, stderr) => { handleResponse(file, includeErrorAsResponse, error, stdout, stderr).then(resolve, reject); }); }); } function spawnFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean, stdOut: (line: string) => void, token?: CancellationToken): Promise { return new Promise((resolve, reject) => { let proc = child_process.spawn(file, args, options); let error = ''; let exited = false; if (token && token.onCancellationRequested) { token.onCancellationRequested(() => { if (!exited && proc) { proc.kill(); proc = null; } }); } proc.on('error', error => { return reject(error); }); proc.stdout.setEncoding('utf8'); proc.stderr.setEncoding('utf8'); proc.stdout.on('data', function (data: string) { if (token && token.isCancellationRequested) { return; } stdOut(data); }); proc.stderr.on('data', function (data: string) { if (token && token.isCancellationRequested) { return; } if (includeErrorAsResponse) { stdOut(data); } else { error += data; } }); proc.on('exit', function (code) { exited = true; if (token && token.isCancellationRequested) { return reject(); } if (error.length > 0) { return reject(error); } resolve(); }); }); } function execInternal(command: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean): Promise { return new Promise((resolve, reject) => { child_process.exec([command].concat(args).join(' '), options, (error, stdout, stderr) => { handleResponse(command, includeErrorAsResponse, error, stdout, stderr).then(resolve, reject); }); }); } export function mergeEnvVariables(newVariables: { [key: string]: string }): any { for (let setting in process.env) { if (!newVariables[setting]) { newVariables[setting] = process.env[setting]; } } return newVariables; } export function formatErrorForLogging(error: Error | string): string { let message: string = ''; if (typeof error === 'string') { message = error; } else { if (error.message) { message = `Error Message: ${error.message}`; } if (error.name && error.message.indexOf(error.name) === -1) { message += `, (${error.name})`; } const innerException = (error as any).innerException; if (innerException && (innerException.message || innerException.name)) { if (innerException.message) { message += `, Inner Error Message: ${innerException.message}`; } if (innerException.name && innerException.message.indexOf(innerException.name) === -1) { message += `, (${innerException.name})`; } } } return message; }