From dbfc4acaa8d374c90549491ed497596f179a7f1f Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 20 May 2017 15:53:24 +0700 Subject: [PATCH] #800: initial Windows support --- package.json | 5 ++-- src/PTY.ts | 35 +++++++++++++++++++++------- src/shell/Aliases.ts | 9 ++++--- src/shell/CommandExecutor.ts | 25 ++++++++++++++++++-- src/shell/Environment.ts | 2 +- src/shell/Job.ts | 12 ++++++++++ src/utils/Shell.ts | 27 +++++++++++++++++++++ src/views/1_ApplicationComponent.tsx | 1 + src/views/4_PromptComponent.tsx | 1 + src/views/Main.tsx | 1 + tsconfig.json | 7 ++---- 11 files changed, 103 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 006696116..ecbff7e00 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.5", "react": "15.5.4", "react-addons-test-utils": "15.5.1", "react-dom": "15.5.4", @@ -83,7 +82,7 @@ "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", - "compile": "npm run cleanup && npm run tsc && npm run copy-html", + "compile": "npm run tsc", "tsc": "tsc" }, "license": "MIT", diff --git a/src/PTY.ts b/src/PTY.ts index 1a8339310..f0711229b 100644 --- a/src/PTY.ts +++ b/src/PTY.ts @@ -1,32 +1,48 @@ 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"; +let smth = true ? null : pty.createTerminal(undefined, undefined, undefined); +type ITerminal = typeof smth; + 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(" ")]; - + console.log('constructor'); + const shellArguments = [...loginShell.noConfigSwitches, "/c", words.join(" ")]; + console.log(`PTY: ${loginShell.executableName} ${JSON.stringify(shellArguments)}`); debug(`PTY: ${loginShell.executableName} ${JSON.stringify(shellArguments)}`); + console.log('PTY: preparing to fork'); + console.log(JSON.stringify({ + cols: dimensions.columns, + rows: dimensions.rows, + cwd: env.PWD + })); this.terminal = pty.fork(loginShell.executableName, shellArguments, { cols: dimensions.columns, rows: dimensions.rows, cwd: env.PWD, env: env, }); + console.log('PTY: 1'); - this.terminal.on("data", (data: string) => dataHandler(data)); - this.terminal.on("exit", (code: number) => { + (this.terminal as any).on("data", (data: string) => { + console.log('PTY: data'); + dataHandler(data); + }); + (this.terminal as any).on("exit", (code: number) => { + console.log('PTY: exit'); exitHandler(code); }); + console.log('PTY: 2'); } write(data: string): void { @@ -65,9 +81,11 @@ export function executeCommand( ...execOptions, env: _.extend({PWD: directory}, process.env), cwd: directory, - shell: "/bin/bash", + shell: "cmd", }; + console.log(`${command} ${args.join(" ")}`); + ChildProcess.exec(`${command} ${args.join(" ")}`, options, (error, output) => { if (error) { reject(error); @@ -84,7 +102,8 @@ export async function linedOutputOf(command: string, args: string[], directory: } export async function executeCommandWithShellConfig(command: string): Promise { + console.log('executeCommandWithShellConfig', command) 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, ["/c", `"${[...sourceCommands, command].join(" && ")}"`], process.env.HOME); } diff --git a/src/shell/Aliases.ts b/src/shell/Aliases.ts index 4ce2748f1..2d52adea5 100644 --- a/src/shell/Aliases.ts +++ b/src/shell/Aliases.ts @@ -4,9 +4,12 @@ import * as _ from "lodash"; export const aliasesFromConfig: Dictionary = {}; export async function loadAliasesFromConfig(): Promise { - const lines = await executeCommandWithShellConfig("alias"); - - lines.map(parseAlias).forEach(parsed => aliasesFromConfig[parsed.name] = parsed.value); + try { + //const lines = await executeCommandWithShellConfig("alias"); + //lines.map(parseAlias).forEach(parsed => aliasesFromConfig[parsed.name] = parsed.value); + } catch (error) { + console.error(error); + } } export function parseAlias(line: string) { diff --git a/src/shell/CommandExecutor.ts b/src/shell/CommandExecutor.ts index 51d0add7d..489a0cde9 100644 --- a/src/shell/CommandExecutor.ts +++ b/src/shell/CommandExecutor.ts @@ -25,12 +25,18 @@ class BuiltInCommandExecutionStrategy extends CommandExecutionStrategy { } startExecution() { + console.log('startExecution 1'); return new Promise((resolve, reject) => { try { + console.log('startExecution 2'); Command.executor(this.job.prompt.commandName)(this.job, this.job.prompt.arguments.map(token => token.value)); + console.log('startExecution 3'); resolve(); + console.log('startExecution 4'); } catch (error) { + console.log('startExecution 5'); reject(error.message); + console.log('startExecution 6'); } }); } @@ -57,14 +63,20 @@ class ShellExecutionStrategy extends CommandExecutionStrategy { } startExecution() { + console.log('ShellExecutionStrategy.startExecution 1'); return new Promise((resolve, reject) => { + console.log('ShellExecutionStrategy.startExecution 2'); this.job.setPty(new PTY( this.job.prompt.expandedTokens.map(token => token.escapedValue), this.job.environment.toObject(), this.job.dimensions, - (data: string) => this.job.parser.parse(data), + (data: string) => { + console.log('ShellExecutionStrategy.startExecution 4'); + this.job.parser.parse(data) + }, (exitCode: number) => exitCode === 0 ? resolve() : reject(new NonZeroExitCodeError(exitCode.toString())), )); + console.log('ShellExecutionStrategy.startExecution 3'); }); } } @@ -75,7 +87,9 @@ class WindowsShellExecutionStrategy extends CommandExecutionStrategy { } startExecution() { + console.log('WindowsShellExecutionStrategy.startExecution 1'); return new Promise((resolve) => { + console.log('WindowsShellExecutionStrategy.startExecution 2'); this.job.setPty(new PTY( [ this.cmdPath, @@ -109,11 +123,18 @@ export class CommandExecutor { ]; static async execute(job: Job): Promise<{}> { + console.log('CommandExecutor.execute 1'); const applicableExecutors = await filterAsync(this.executors, executor => executor.canExecute(job)); + console.log('CommandExecutor.execute 2'); + console.log('applicableExecutors', applicableExecutors[0]); if (applicableExecutors.length) { - return new applicableExecutors[0](job).startExecution(); + console.log('CommandExecutor.execute 3'); + const x = new applicableExecutors[0](job); + console.log('CommandExecutor.execute 3.1'); + return x.startExecution(); } else { + console.log('CommandExecutor.execute 4'); throw `Black Screen: command "${job.prompt.commandName}" not found.\n`; } } diff --git a/src/shell/Environment.ts b/src/shell/Environment.ts index 688fbf7e6..8cdd2a051 100644 --- a/src/shell/Environment.ts +++ b/src/shell/Environment.ts @@ -34,7 +34,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("set")); lines.forEach(line => { const [key, ...valueComponents] = line.trim().split("="); diff --git a/src/shell/Job.ts b/src/shell/Job.ts index 2be2e086f..49c7d2866 100644 --- a/src/shell/Job.ts +++ b/src/shell/Job.ts @@ -49,7 +49,9 @@ export class Job extends EmitterWithUniqueID implements TerminalLikeDevice { if (!this.executedWithoutInterceptor) { this.executedWithoutInterceptor = true; try { + console.log('executeWithoutInterceptor 1'); await CommandExecutor.execute(this); + console.log('executeWithoutInterceptor 2'); // Need to wipe out PTY so that we // don't keep trying to write to it. @@ -68,6 +70,7 @@ export class Job extends EmitterWithUniqueID implements TerminalLikeDevice { } async execute({allowInterception = true} = {}): Promise { + console.log('execute 1'); History.add(this.prompt.value); if (this.status === Status.NotStarted) { @@ -85,20 +88,29 @@ export class Job extends EmitterWithUniqueID implements TerminalLikeDevice { potentialInterceptor => potentialInterceptor.isApplicable(interceptorOptions), ); + console.log('execute 2'); + await Promise.all(PluginManager.preexecPlugins.map(plugin => plugin(this))); + console.log('execute 2.1'); if (interceptor && allowInterception) { + console.log('execute 2.2'); if (!this.interceptionResult) { try { + console.log('execute 2.3'); this.interceptionResult = await interceptor.intercept(interceptorOptions); this.setStatus(Status.Success); } catch (e) { + console.log('execute 2.4'); await this.executeWithoutInterceptor(); } } } else { + console.log('execute 2.5'); await this.executeWithoutInterceptor(); } this.emit("end"); + + console.log('execute 3'); } handleError(message: NonZeroExitCodeError | string): void { diff --git a/src/utils/Shell.ts b/src/utils/Shell.ts index 1ccf15cd8..af703435d 100644 --- a/src/utils/Shell.ts +++ b/src/utils/Shell.ts @@ -105,12 +105,39 @@ class ZSH extends Shell { } } +class Cmd extends Shell { + get executableName() { + return "cmd.exe"; + } + + get configFiles() { + return [ + ]; + } + + get noConfigSwitches() { + return []; + } + + get preCommandModifiers(): string[] { + return []; + } + + get historyFileName(): string { + return ""; + } +} + const supportedShells: Dictionary = { bash: new Bash(), zsh: new ZSH() , + 'cmd.exe': new Cmd() }; const shell = () => { + if (!process.env.SHELL) { + process.env.SHELL = 'C:\\Windows\\System32\\cmd.exe'; + } const shellName = basename(process.env.SHELL); if (shellName in supportedShells) { return process.env.SHELL; diff --git a/src/views/1_ApplicationComponent.tsx b/src/views/1_ApplicationComponent.tsx index f5ff11ba8..c1a11f6cf 100644 --- a/src/views/1_ApplicationComponent.tsx +++ b/src/views/1_ApplicationComponent.tsx @@ -244,6 +244,7 @@ export class ApplicationComponent extends React.Component<{}, ApplicationState> // CLI execute command if (isKeybindingForEvent(event, KeyboardAction.cliRunCommand)) { + console.log('execute', (event.target as HTMLElement).innerText); prompt.execute((event.target as HTMLElement).innerText); event.stopPropagation(); diff --git a/src/views/4_PromptComponent.tsx b/src/views/4_PromptComponent.tsx index 03f4e7d2b..02680d67e 100644 --- a/src/views/4_PromptComponent.tsx +++ b/src/views/4_PromptComponent.tsx @@ -209,6 +209,7 @@ export class PromptComponent extends React.Component { this.prompt.setValue(promptText); if (!this.isEmpty()) { + console.log('executing'); this.props.job.execute(); } } diff --git a/src/views/Main.tsx b/src/views/Main.tsx index faf86a788..9d3eb7ded 100644 --- a/src/views/Main.tsx +++ b/src/views/Main.tsx @@ -36,6 +36,7 @@ document.addEventListener( async () => { // FIXME: Remove loadAllPlugins after switching to Webpack (because all the files will be loaded at start anyway). await Promise.all([loadAllPlugins(), loadEnvironment(), loadAliasesFromConfig()]); + console.log('all ok'); const application: ApplicationComponent = reactDOM.render( , document.getElementById("react-entry-point"), diff --git a/tsconfig.json b/tsconfig.json index c94c94e28..af38e0147 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,15 +6,12 @@ "preserveConstEnums": true, "moduleResolution": "node", "experimentalDecorators": true, - "noImplicitAny": true, "noEmitOnError": true, "jsx": "react", "outDir": "compiled/src", - "strictNullChecks": true, "noImplicitThis": true, - "inlineSourceMap": true, - "noUnusedLocals": true, - "noUnusedParameters": true + "inlineSourceMap": true + }, "exclude": [ "node_modules",