Skip to content

Commit

Permalink
railsware#800: initial Windows support
Browse files Browse the repository at this point in the history
  • Loading branch information
ForNeVeR committed May 20, 2017
1 parent b3c11ae commit dbfc4ac
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 22 deletions.
5 changes: 2 additions & 3 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.5",
"react": "15.5.4",
"react-addons-test-utils": "15.5.1",
"react-dom": "15.5.4",
Expand Down Expand Up @@ -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",
Expand Down
35 changes: 27 additions & 8 deletions src/PTY.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -84,7 +102,8 @@ export async function linedOutputOf(command: string, args: string[], directory:
}

export async function executeCommandWithShellConfig(command: string): Promise<string[]> {
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);
}
9 changes: 6 additions & 3 deletions src/shell/Aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import * as _ from "lodash";
export const aliasesFromConfig: Dictionary<string> = {};

export async function loadAliasesFromConfig(): Promise<void> {
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) {
Expand Down
25 changes: 23 additions & 2 deletions src/shell/CommandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
}
Expand All @@ -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');
});
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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`;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/shell/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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("set"));

lines.forEach(line => {
const [key, ...valueComponents] = line.trim().split("=");
Expand Down
12 changes: 12 additions & 0 deletions src/shell/Job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -68,6 +70,7 @@ export class Job extends EmitterWithUniqueID implements TerminalLikeDevice {
}

async execute({allowInterception = true} = {}): Promise<void> {
console.log('execute 1');
History.add(this.prompt.value);

if (this.status === Status.NotStarted) {
Expand All @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions src/utils/Shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Shell> = {
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;
Expand Down
1 change: 1 addition & 0 deletions src/views/1_ApplicationComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/views/4_PromptComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export class PromptComponent extends React.Component<Props, State> {
this.prompt.setValue(promptText);

if (!this.isEmpty()) {
console.log('executing');
this.props.job.execute();
}
}
Expand Down
1 change: 1 addition & 0 deletions src/views/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ApplicationComponent/>,
document.getElementById("react-entry-point"),
Expand Down
7 changes: 2 additions & 5 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit dbfc4ac

Please sign in to comment.