Skip to content

Commit

Permalink
Speed up editor detection on Windows
Browse files Browse the repository at this point in the history
before ~600ms
after ~300ms
  • Loading branch information
levrik committed Jul 2, 2017
1 parent df00d80 commit cd28840
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 95 deletions.
249 changes: 155 additions & 94 deletions packages/react-dev-utils/launchEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,59 @@
const fs = require('fs');
const path = require('path');
const child_process = require('child_process');
const EventEmitter = require('events').EventEmitter;
const os = require('os');
const chalk = require('chalk');
const shellQuote = require('shell-quote');

// Inspired by https://github.com/rannn505/node-powershell
const EOI = 'EOI';
class PowerShell extends EventEmitter {
constructor() {
super();

this._proc = child_process.spawn(
'powershell.exe',
['-NoLogo', '-NoProfile', '-NoExit', '-Command', '-'],
{
stdio: 'pipe',
}
);

let output = [];
this._proc.stdout.on('data', data => {
if (data.indexOf(EOI) !== -1) {
this.emit('resolve', output.join(''));
output = [];
} else {
output.push(data);
}
});

this._proc.on('error', () => {
this._proc = null;
});
}

invoke(cmd) {
if (!this._proc) {
return Promise.resolve('');
}

return new Promise(resolve => {
this.on('resolve', data => {
resolve(data);
this.removeAllListeners('resolve');
});

this._proc.stdin.write(cmd);
this._proc.stdin.write(os.EOL);
this._proc.stdin.write(`echo ${EOI}`);
this._proc.stdin.write(os.EOL);
});
}
}

function isTerminalEditor(editor) {
switch (editor) {
case 'vim':
Expand Down Expand Up @@ -104,57 +153,65 @@ function getArgumentsForLineNumber(editor, fileName, lineNumber, workspace) {
return [fileName];
}

function guessEditor() {
// Explicit config always wins
if (process.env.REACT_EDITOR) {
return shellQuote.parse(process.env.REACT_EDITOR);
let powerShellAgent = null;
function launchPowerShellAgent() {
if (!powerShellAgent) {
powerShellAgent = new PowerShell();
}
}

// Using `ps x` on OSX or `Get-Process` on Windows we can find out which editor is currently running.
// Potentially we could use similar technique for Linux
try {
if (process.platform === 'darwin') {
const output = child_process.execSync('ps x').toString();
const processNames = Object.keys(COMMON_EDITORS_OSX);
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i];
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_OSX[processName]];
}
}
} else if (process.platform === 'win32') {
const output = child_process
.execSync('powershell -Command "Get-Process | Select-Object Path"', {
stdio: ['pipe', 'pipe', 'ignore'],
})
.toString();
const runningProcesses = output.split('\r\n');
for (let i = 0; i < runningProcesses.length; i++) {
// `Get-Process` sometimes returns empty lines
if (!runningProcesses[i]) {
continue;
function guessEditor() {
return new Promise(resolve => {
// Explicit config always wins
if (process.env.REACT_EDITOR) {
return resolve(shellQuote.parse(process.env.REACT_EDITOR));
}

// Using `ps x` on OSX or `Get-Process` on Windows we can find out which editor is currently running.
// Potentially we could use similar technique for Linux
try {
if (process.platform === 'darwin') {
const output = child_process.execSync('ps x').toString();
const processNames = Object.keys(COMMON_EDITORS_OSX);
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i];
if (output.indexOf(processName) !== -1) {
return resolve([COMMON_EDITORS_OSX[processName]]);
}
}
} else if (process.platform === 'win32' && powerShellAgent) {
return powerShellAgent
.invoke('Get-Process | Select-Object Path')
.then(output => {
const runningProcesses = output.split('\r\n');
for (let i = 0; i < runningProcesses.length; i++) {
// `Get-Process` sometimes returns empty lines
if (!runningProcesses[i]) {
continue;
}

const fullProcessPath = runningProcesses[i].trim();
const shortProcessName = path.basename(fullProcessPath);
const fullProcessPath = runningProcesses[i].trim();
const shortProcessName = path.basename(fullProcessPath);

if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return [fullProcessPath];
}
if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return resolve([fullProcessPath]);
}
}
});
}
} catch (error) {
// Ignore...
}
} catch (error) {
// Ignore...
}

// Last resort, use old skool env vars
if (process.env.VISUAL) {
return [process.env.VISUAL];
} else if (process.env.EDITOR) {
return [process.env.EDITOR];
}
// Last resort, use old skool env vars
if (process.env.VISUAL) {
return resolve([process.env.VISUAL]);
} else if (process.env.EDITOR) {
return resolve([process.env.EDITOR]);
}

return [null];
return resolve([null]);
});
}

function printInstructions(fileName, errorMessage) {
Expand Down Expand Up @@ -195,64 +252,68 @@ function launchEditor(fileName, lineNumber) {
return;
}

let [editor, ...args] = guessEditor();
if (!editor) {
printInstructions(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);
}
guessEditor().then(([editor, ...args]) => {
if (!editor) {
printInstructions(fileName, null);
return;
}

let workspace = null;
if (lineNumber) {
args = args.concat(
getArgumentsForLineNumber(editor, fileName, lineNumber, workspace)
);
} else {
args.push(fileName);
}
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);
}

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');
}
let workspace = null;
if (lineNumber) {
args = args.concat(
getArgumentsForLineNumber(editor, fileName, lineNumber, workspace)
);
} else {
args.push(fileName);
}

if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = child_process.spawn(
'cmd.exe',
['/C', editor].concat(args),
{ stdio: 'inherit' }
);
} else {
_childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
}
_childProcess.on('exit', function(errorCode) {
_childProcess = null;
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 (errorCode) {
printInstructions(fileName, '(code ' + errorCode + ')');
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
_childProcess = child_process.spawn(
'cmd.exe',
['/C', editor].concat(args),
{ stdio: 'inherit' }
);
} else {
_childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
}
});
_childProcess.on('exit', function(errorCode) {
_childProcess = null;

_childProcess.on('error', function(error) {
printInstructions(fileName, error.message);
if (errorCode) {
printInstructions(fileName, '(code ' + errorCode + ')');
}
});

_childProcess.on('error', function(error) {
printInstructions(fileName, error.message);
});
});
}

module.exports = launchEditor;
module.exports = {
launchEditor,
launchPowerShellAgent,
};
2 changes: 1 addition & 1 deletion packages/react-error-overlay/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
'use strict';

const launchEditor = require('react-dev-utils/launchEditor');
const { launchEditor } = require('react-dev-utils/launchEditor');

module.exports = function createLaunchEditorMiddleware() {
return function launchEditorMiddleware(req, res, next) {
Expand Down
6 changes: 6 additions & 0 deletions packages/react-scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ choosePort(HOST, DEFAULT_PORT)
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
if (process.platform === 'win32') {
const {
launchPowerShellAgent,
} = require('react-dev-utils/launchEditor');
launchPowerShellAgent();
}
});

['SIGINT', 'SIGTERM'].forEach(function(sig) {
Expand Down

0 comments on commit cd28840

Please sign in to comment.