From d0daa7acb47360b3e1d546c445a003bba0162856 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sun, 28 May 2017 14:17:05 +0800 Subject: [PATCH] #800: support for Windows' cmd shell (#1033) --- housekeeping/start.sh | 6 --- package.json | 14 ++++-- src/PTY.ts | 23 +++++---- src/shell/Aliases.ts | 5 +- src/shell/Environment.ts | 3 +- src/utils/Shell.ts | 101 ++++++++++++++++++++++++++++++++++++--- 6 files changed, 121 insertions(+), 31 deletions(-) delete mode 100644 housekeeping/start.sh diff --git a/housekeeping/start.sh b/housekeeping/start.sh deleted file mode 100644 index 2fdb7066a..000000000 --- a/housekeeping/start.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -node_modules/.bin/tsc --watch > /dev/tty & -WATCH_PID=$! -NODE_ENV=development npm run electron -kill $WATCH_PID diff --git a/package.json b/package.json index c92734798..61ae4c962 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@types/klaw": "1.3.2", "@types/lodash": "4.14.64", "@types/node": "7.0.18", - "@types/pty.js": "0.2.32", "@types/react": "15.0.24", "child-process-promise": "2.2.1", "dirStat": "0.0.2", @@ -42,7 +41,7 @@ "lodash": "4.17.4", "mode-to-permissions": "0.0.2", "node-ansiparser": "2.2.0", - "pty.js": "shockone/pty.js", + "node-pty": "0.6.6", "react": "15.5.4", "react-addons-test-utils": "15.5.1", "react-dom": "15.5.4", @@ -55,14 +54,19 @@ "@types/mocha": "2.2.41", "@types/webdriverio": "4.7.0", "chai": "3.5.0", + "concurrently": "3.4.0", + "cpx": "1.5.0", + "cross-env": "5.0.0", "devtron": "1.4.0", "electron": "1.6.7", "electron-builder": "17.5.0", "electron-mocha": "3.4.0", "electron-rebuild": "1.5.11", "enzyme": "2.8.2", + "mkdirp": "0.5.1", "mocha": "3.3.0", "npm-check-updates": "2.11.1", + "rimraf": "2.6.1", "spectron": "3.7.0", "ts-node": "3.0.4", "tslint": "5.2.0", @@ -75,15 +79,15 @@ "release": "build --publish always --draft=false", "electron": "electron .", "prestart": "npm install && npm run compile", - "start": "bash housekeeping/start.sh", + "start": "concurrently --kill-others -s first \"tsc --watch\" \"cross-env NODE_ENV=development npm run electron\"", "test": "npm run lint && npm run unit-tests && npm run ui-tests && npm run integration-tests && build --publish never", "unit-tests": "electron-mocha --require ts-node/register $(find test -name '*_spec.ts')", "ui-tests": "electron-mocha --require ts-node/register $(find test -name '*_spec.tsx')", "integration-tests": "npm run compile && electron-mocha --require ts-node/register test/e2e.ts", "update-dependencies": "ncu -u", "lint": "tslint `find src -name '*.ts*'` `find test -name '*.ts*'`", - "cleanup": "rm -rf compiled/src", - "copy-html": "mkdir -p compiled/src/views && cp src/views/index.html compiled/src/views", + "cleanup": "rimraf compiled/src", + "copy-html": "mkdirp compiled/src/views && cpx src/views/index.html compiled/src/views", "compile": "npm run cleanup && npm run tsc && npm run copy-html", "tsc": "tsc" }, diff --git a/src/PTY.ts b/src/PTY.ts index 1a8339310..264a6f680 100644 --- a/src/PTY.ts +++ b/src/PTY.ts @@ -1,22 +1,29 @@ import * as ChildProcess from "child_process"; import * as OS from "os"; import * as _ from "lodash"; -import * as pty from "pty.js"; +import * as pty from "node-pty"; import {loginShell} from "./utils/Shell"; import {debug} from "./utils/Common"; +interface ITerminal { + write(data: string): void; + resize(cols: number, rows: number): void; + kill(signal?: string): void; + on(type: string, listener: (...args: any[]) => any): void; +} + export class PTY { - private terminal: pty.Terminal; + private terminal: ITerminal; // TODO: write proper signatures. // TODO: use generators. // TODO: terminate. https://github.com/atom/atom/blob/v1.0.15/src/task.coffee#L151 constructor(words: EscapedShellWord[], env: ProcessEnvironment, dimensions: Dimensions, dataHandler: (d: string) => void, exitHandler: (c: number) => void) { - const shellArguments = [...loginShell.noConfigSwitches, "-i", "-c", words.join(" ")]; + const shellArguments = [...loginShell.noConfigSwitches, ...loginShell.interactiveCommandSwitches, words.join(" ")]; debug(`PTY: ${loginShell.executableName} ${JSON.stringify(shellArguments)}`); - this.terminal = pty.fork(loginShell.executableName, shellArguments, { + this.terminal = pty.fork(loginShell.executableName, shellArguments, { cols: dimensions.columns, rows: dimensions.rows, cwd: env.PWD, @@ -24,9 +31,7 @@ export class PTY { }); this.terminal.on("data", (data: string) => dataHandler(data)); - this.terminal.on("exit", (code: number) => { - exitHandler(code); - }); + this.terminal.on("exit", (code: number) => exitHandler(code)); } write(data: string): void { @@ -65,7 +70,7 @@ export function executeCommand( ...execOptions, env: _.extend({PWD: directory}, process.env), cwd: directory, - shell: "/bin/bash", + shell: loginShell.commandExecutorPath, }; ChildProcess.exec(`${command} ${args.join(" ")}`, options, (error, output) => { @@ -86,5 +91,5 @@ export async function linedOutputOf(command: string, args: string[], directory: export async function executeCommandWithShellConfig(command: string): Promise { const sourceCommands = (await loginShell.existingConfigFiles()).map(fileName => `source ${fileName} &> /dev/null`); - return await linedOutputOf(loginShell.executableName, ["-c", `'${[...sourceCommands, command].join("; ")}'`], process.env.HOME); + return await linedOutputOf(loginShell.executableName, [...loginShell.executeCommandSwitches, loginShell.combineCommands([...sourceCommands, command])], process.env.HOME); } diff --git a/src/shell/Aliases.ts b/src/shell/Aliases.ts index 4ce2748f1..d7a556c6c 100644 --- a/src/shell/Aliases.ts +++ b/src/shell/Aliases.ts @@ -1,11 +1,10 @@ -import {executeCommandWithShellConfig} from "../PTY"; +import {loginShell} from "../utils/Shell"; import * as _ from "lodash"; export const aliasesFromConfig: Dictionary = {}; export async function loadAliasesFromConfig(): Promise { - const lines = await executeCommandWithShellConfig("alias"); - + const lines = await loginShell.loadAliases(); lines.map(parseAlias).forEach(parsed => aliasesFromConfig[parsed.name] = parsed.value); } diff --git a/src/shell/Environment.ts b/src/shell/Environment.ts index 688fbf7e6..11b8309bd 100644 --- a/src/shell/Environment.ts +++ b/src/shell/Environment.ts @@ -4,6 +4,7 @@ import {clone} from "lodash"; import {homeDirectory} from "../utils/Common"; import * as Path from "path"; import {AbstractOrderedSet} from "../utils/OrderedSet"; +import {loginShell} from "../utils/Shell"; const ignoredEnvironmentVariables = [ "NODE_ENV", @@ -34,7 +35,7 @@ export const preprocessEnv = (lines: string[]) => { export const processEnvironment: Dictionary = {}; export async function loadEnvironment(): Promise { - const lines = preprocessEnv(await executeCommandWithShellConfig("env")); + const lines = preprocessEnv(await executeCommandWithShellConfig(loginShell.environmentCommand)); lines.forEach(line => { const [key, ...valueComponents] = line.trim().split("="); diff --git a/src/utils/Shell.ts b/src/utils/Shell.ts index 1ccf15cd8..7b08a7cd0 100644 --- a/src/utils/Shell.ts +++ b/src/utils/Shell.ts @@ -2,15 +2,23 @@ import {basename} from "path"; import {readFileSync, statSync} from "fs"; import * as Path from "path"; import {EOL} from "os"; -import {resolveFile, io, filterAsync, homeDirectory} from "./Common"; +import {resolveFile, io, isWindows, filterAsync, homeDirectory} from "./Common"; +import {executeCommandWithShellConfig} from "../PTY"; import * as _ from "lodash"; abstract class Shell { abstract get executableName(): string; abstract get configFiles(): string[]; abstract get noConfigSwitches(): string[]; + abstract get executeCommandSwitches(): string[]; + abstract get interactiveCommandSwitches(): string[]; abstract get preCommandModifiers(): string[]; abstract get historyFileName(): string; + abstract get commandExecutorPath(): string; + abstract get environmentCommand(): string; + abstract loadAliases(): Promise; + + abstract combineCommands(commands: string[]): string; async existingConfigFiles(): Promise { const resolvedConfigFiles = this.configFiles.map(fileName => resolveFile(process.env.HOME, fileName)); @@ -33,7 +41,33 @@ abstract class Shell { } } -class Bash extends Shell { +abstract class UnixShell extends Shell { + get executeCommandSwitches() { + return ["-c"]; + } + + get interactiveCommandSwitches() { + return ["-i", "-c"]; + } + + get commandExecutorPath() { + return "/bin/bash"; + } + + get environmentCommand() { + return "env"; + } + + loadAliases() { + return executeCommandWithShellConfig("alias"); + } + + combineCommands(commands: string[]) { + return `'${commands.join("; ")}'`; + } +} + +class Bash extends UnixShell { get executableName() { return "bash"; } @@ -65,7 +99,7 @@ class Bash extends Shell { } } -class ZSH extends Shell { +class ZSH extends UnixShell { get executableName() { return "zsh"; } @@ -105,18 +139,71 @@ class ZSH extends Shell { } } +class Cmd extends Shell { + static get cmdPath() { + return Path.join(process.env.WINDIR, "System32", "cmd.exe"); + } + + get executableName() { + return "cmd.exe"; + } + + get configFiles() { + return [ + ]; + } + + get executeCommandSwitches() { + return ["/c"]; + } + + get interactiveCommandSwitches() { + return ["/c"]; + } + + get noConfigSwitches() { + return []; + } + + get preCommandModifiers(): string[] { + return []; + } + + get historyFileName(): string { + return ""; + } + + get commandExecutorPath() { + return Cmd.cmdPath; + } + + get environmentCommand() { + return "set"; + } + + combineCommands(commands: string[]) { + return `"${commands.join(" && ")}`; + } + + async loadAliases() { + return []; + } +} + const supportedShells: Dictionary = { bash: new Bash(), - zsh: new ZSH() , + zsh: new ZSH(), + "cmd.exe": new Cmd(), }; const shell = () => { - const shellName = basename(process.env.SHELL); + const shellName = process.env.SHELL ? basename(process.env.SHELL) : ""; if (shellName in supportedShells) { return process.env.SHELL; } else { - console.error(`${shellName} is not supported; defaulting to /bin/bash`); - return "/bin/bash"; + const defaultShell = isWindows ? Cmd.cmdPath : "/bin/bash"; + console.error(`${shellName} is not supported; defaulting to ${defaultShell}`); + return defaultShell; } };