Skip to content
This repository has been archived by the owner on May 21, 2019. It is now read-only.

Commit

Permalink
#800: support for Windows' cmd shell
Browse files Browse the repository at this point in the history
  • Loading branch information
ForNeVeR committed May 25, 2017
1 parent b3c11ae commit 81c5089
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 31 deletions.
6 changes: 0 additions & 6 deletions housekeeping/start.sh

This file was deleted.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -54,14 +53,19 @@
"@types/klaw": "1.3.2",
"@types/mocha": "2.2.41",
"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",
Expand All @@ -74,15 +78,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_RUN_AS_NODE=1 electron $(which 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 && mocha",
"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"
},
Expand Down
23 changes: 14 additions & 9 deletions src/PTY.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
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 = <any> pty.fork(loginShell.executableName, shellArguments, {
cols: dimensions.columns,
rows: dimensions.rows,
cwd: env.PWD,
env: env,
});

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 {
Expand Down Expand Up @@ -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) => {
Expand All @@ -86,5 +91,5 @@ export async function linedOutputOf(command: string, args: string[], directory:
export async function executeCommandWithShellConfig(command: string): Promise<string[]> {
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.interactiveCommandSwitches, loginShell.combineCommands([...sourceCommands, command])], process.env.HOME);
}
5 changes: 2 additions & 3 deletions src/shell/Aliases.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {executeCommandWithShellConfig} from "../PTY";
import {loginShell} from "../utils/Shell";
import * as _ from "lodash";

export const aliasesFromConfig: Dictionary<string> = {};

export async function loadAliasesFromConfig(): Promise<void> {
const lines = await executeCommandWithShellConfig("alias");

const lines = await loginShell.loadAliases();
lines.map(parseAlias).forEach(parsed => aliasesFromConfig[parsed.name] = parsed.value);
}

Expand Down
3 changes: 2 additions & 1 deletion src/shell/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -34,7 +35,7 @@ export const preprocessEnv = (lines: string[]) => {

export const processEnvironment: Dictionary<string> = {};
export async function loadEnvironment(): Promise<void> {
const lines = preprocessEnv(await executeCommandWithShellConfig("env"));
const lines = preprocessEnv(await executeCommandWithShellConfig(loginShell.environmentCommand));

lines.forEach(line => {
const [key, ...valueComponents] = line.trim().split("=");
Expand Down
101 changes: 94 additions & 7 deletions src/utils/Shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>;

abstract combineCommands(commands: string[]): string;

async existingConfigFiles(): Promise<string[]> {
const resolvedConfigFiles = this.configFiles.map(fileName => resolveFile(process.env.HOME, fileName));
Expand All @@ -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";
}
Expand Down Expand Up @@ -65,7 +99,7 @@ class Bash extends Shell {
}
}

class ZSH extends Shell {
class ZSH extends UnixShell {
get executableName() {
return "zsh";
}
Expand Down Expand Up @@ -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<Shell> = {
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;
}
};

Expand Down

0 comments on commit 81c5089

Please sign in to comment.