173 lines
5.0 KiB
JavaScript
173 lines
5.0 KiB
JavaScript
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file at
|
|
* https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
|
|
*
|
|
* Modified by Yuxi Evan You
|
|
*/
|
|
|
|
const fs = require('fs')
|
|
const os = require('os')
|
|
const path = require('path')
|
|
const colors = require('picocolors')
|
|
const childProcess = require('child_process')
|
|
|
|
const guessEditor = require('./guess')
|
|
const getArgumentsForPosition = require('./get-args')
|
|
|
|
function wrapErrorCallback (cb) {
|
|
return (fileName, errorMessage) => {
|
|
console.log()
|
|
console.log(
|
|
colors.red('Could not open ' + path.basename(fileName) + ' in the editor.')
|
|
)
|
|
if (errorMessage) {
|
|
if (errorMessage[errorMessage.length - 1] !== '.') {
|
|
errorMessage += '.'
|
|
}
|
|
console.log(
|
|
colors.red('The editor process exited with an error: ' + errorMessage)
|
|
)
|
|
}
|
|
console.log()
|
|
if (cb) cb(fileName, errorMessage)
|
|
}
|
|
}
|
|
|
|
function isTerminalEditor (editor) {
|
|
switch (editor) {
|
|
case 'vim':
|
|
case 'emacs':
|
|
case 'nano':
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
const positionRE = /:(\d+)(:(\d+))?$/
|
|
function parseFile (file) {
|
|
const fileName = file.replace(positionRE, '')
|
|
const match = file.match(positionRE)
|
|
const lineNumber = match && match[1]
|
|
const columnNumber = match && match[3]
|
|
return {
|
|
fileName,
|
|
lineNumber,
|
|
columnNumber
|
|
}
|
|
}
|
|
|
|
let _childProcess = null
|
|
|
|
function launchEditor (file, specifiedEditor, onErrorCallback) {
|
|
const parsed = parseFile(file)
|
|
let { fileName } = parsed
|
|
const { lineNumber, columnNumber } = parsed
|
|
|
|
if (!fs.existsSync(fileName)) {
|
|
return
|
|
}
|
|
|
|
if (typeof specifiedEditor === 'function') {
|
|
onErrorCallback = specifiedEditor
|
|
specifiedEditor = undefined
|
|
}
|
|
|
|
onErrorCallback = wrapErrorCallback(onErrorCallback)
|
|
|
|
const [editor, ...args] = guessEditor(specifiedEditor)
|
|
if (!editor) {
|
|
onErrorCallback(fileName, null)
|
|
return
|
|
}
|
|
|
|
if (
|
|
process.platform === 'linux' &&
|
|
fileName.startsWith('/mnt/') &&
|
|
/Microsoft/i.test(os.release())
|
|
) {
|
|
// Assume WSL / "Bash on Ubuntu on Windows" is being used, and
|
|
// that the file exists on the Windows file system.
|
|
// `os.release()` is "4.4.0-43-Microsoft" in the current release
|
|
// build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
|
|
// When a Windows editor is specified, interop functionality can
|
|
// handle the path translation, but only if a relative path is used.
|
|
fileName = path.relative('', fileName)
|
|
}
|
|
|
|
// cmd.exe on Windows is vulnerable to RCE attacks given a file name of the
|
|
// form "C:\Users\myusername\Downloads\& curl 172.21.93.52". Use a safe file
|
|
// name pattern to validate user-provided file names. This doesn't cover the
|
|
// entire range of valid file names but should cover almost all of them in practice.
|
|
// (Backport of
|
|
// https://github.com/facebook/create-react-app/pull/4866
|
|
// and
|
|
// https://github.com/facebook/create-react-app/pull/5431)
|
|
|
|
// Allows alphanumeric characters, periods, dashes, slashes, underscores, plus and space.
|
|
const WINDOWS_CMD_SAFE_FILE_NAME_PATTERN = /^([A-Za-z]:[/\\])?[\p{L}0-9/.\-\\_+ ]+$/u
|
|
if (
|
|
process.platform === 'win32' &&
|
|
!WINDOWS_CMD_SAFE_FILE_NAME_PATTERN.test(fileName.trim())
|
|
) {
|
|
console.log()
|
|
console.log(
|
|
colors.red('Could not open ' + path.basename(fileName) + ' in the editor.')
|
|
)
|
|
console.log()
|
|
console.log(
|
|
'When running on Windows, file names are checked against a safe file name ' +
|
|
'pattern to protect against remote code execution attacks. File names ' +
|
|
'may consist only of alphanumeric characters (all languages), periods, ' +
|
|
'dashes, slashes, and underscores.'
|
|
);
|
|
console.log()
|
|
return
|
|
}
|
|
|
|
if (lineNumber) {
|
|
const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
|
|
args.push.apply(args, extraArgs)
|
|
} else {
|
|
args.push(fileName)
|
|
}
|
|
|
|
if (_childProcess && isTerminalEditor(editor)) {
|
|
// There's an existing editor process already and it's attached
|
|
// to the terminal, so go kill it. Otherwise two separate editor
|
|
// instances attach to the stdin/stdout which gets confusing.
|
|
_childProcess.kill('SIGKILL')
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
// On Windows, launch the editor in a shell because spawn can only
|
|
// launch .exe files.
|
|
_childProcess = childProcess.spawn(
|
|
'cmd.exe',
|
|
['/C', editor].concat(args),
|
|
{ stdio: 'inherit' }
|
|
)
|
|
} else {
|
|
_childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
|
|
}
|
|
_childProcess.on('exit', function (errorCode) {
|
|
_childProcess = null
|
|
|
|
if (errorCode) {
|
|
onErrorCallback(fileName, '(code ' + errorCode + ')')
|
|
}
|
|
})
|
|
|
|
_childProcess.on('error', function (error) {
|
|
let { code, message } = error
|
|
if ('ENOENT' === code) {
|
|
message = `${message} ('${editor}' command does not exist in 'PATH')`
|
|
}
|
|
onErrorCallback(fileName, message);
|
|
})
|
|
}
|
|
|
|
module.exports = launchEditor
|