Skip to content

Commit

Permalink
Add buffered stdin to accept terminal input whilst WASM commands running
Browse files Browse the repository at this point in the history
  • Loading branch information
ianthomas23 committed Jul 22, 2024
1 parent 6373b50 commit 58b5e63
Show file tree
Hide file tree
Showing 22 changed files with 233 additions and 82 deletions.
18 changes: 18 additions & 0 deletions src/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Callbacks used by a shell to call functions in the frontend.
*/

/**
* Send output string to be displayed in terminal.
*/
export interface IOutputCallback { (output: string): Promise<void> }

/**
* Enable/disable buffered stdin in the terminal.
*/
export interface IEnableBufferedStdinCallback { (enable: boolean): void }

/**
* Wait for and return a sequence of utf16 code units from stdin, if buffered stdin is enabled.
*/
export interface IStdinCallback { (): number[] }
13 changes: 6 additions & 7 deletions src/commands/wasm_command_runner.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { ICommandRunner } from "./command_runner"
import { Context } from "../context"
import { SingleCharInput } from "../io"

export abstract class WasmCommandRunner implements ICommandRunner {
abstract names(): string[]

async run(cmdName: string, context: Context): Promise<void> {
const { args, fileSystem, mountpoint, stdout} = context
const { args, fileSystem, mountpoint, stdin, stdout } = context

const start = Date.now()
if (!this._wasmModule) {
this._wasmModule = this._getWasmModule()
}

const stdin = new SingleCharInput(context.stdin)

const wasm = await this._wasmModule({
thisProgram: cmdName,
noInitialRun: true,
Expand Down Expand Up @@ -46,11 +43,13 @@ export abstract class WasmCommandRunner implements ICommandRunner {

// Monkey patch stdin get_char.
function getChar(tty: any) {
const charCode = stdin.readCharCode()
if (charCode === 4) { // EOT
const utf16codes = stdin.readChar()
// What to do with length other than 1?
const utf16 = utf16codes[0]
if (utf16 === 4) { // EOT
return null
} else {
return charCode
return utf16
}
}
const stdinDeviceId = module.FS.makedev(5, 0)
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { Aliases } from "./aliases"
export { IOutputCallback, IEnableBufferedStdinCallback, IStdinCallback } from "./callback"
export { Context } from "./context"
export { IFileSystem } from "./file_system"
export { IOutputCallback } from "./output_callback"
export { parse } from "./parse"
export { Shell } from "./shell"
export * from "./shell"
export { tokenize, Token } from "./tokenize"
4 changes: 2 additions & 2 deletions src/io/buffered_output.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IOutput } from "./output"

export abstract class BufferedOutput implements IOutput {
protected get allContent(): string {
get allContent(): string {
return this.data.join("")
}

protected clear() {
clear() {
this.data = []
}

Expand Down
10 changes: 6 additions & 4 deletions src/io/file_input.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { IFileSystem } from "../file_system"
import { IInput } from "./input"
import { InputAll } from "./input_all"

export class FileInput implements IInput {
constructor(readonly fileSystem: IFileSystem, readonly path: string) {}
export class FileInput extends InputAll {
constructor(readonly fileSystem: IFileSystem, readonly path: string) {
super()
}

read(): string {
readAll(): string {
const { FS } = this.fileSystem
const contents = FS.readFile(this.path, { "encoding": "utf8" })
return contents
Expand Down
3 changes: 2 additions & 1 deletion src/io/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ export * from "./console_output"
export * from "./file_input"
export * from "./file_output"
export * from "./input"
export * from "./input_all"
export * from "./output"
export * from "./pipe"
export * from "./pipe_input"
export * from "./redirect_output"
export * from "./single_char_input"
export * from "./terminal_input"
export * from "./terminal_output"
6 changes: 4 additions & 2 deletions src/io/input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export interface IInput {
/**
* Read and return the entire contents of this input.
* Read and return a single character as a sequence of ASCII character codes. Note this might be
* more than one actual character such as \n or escape code for up arrow, etc. No further input is
* indicated by a single-width character with an ASCII code of 4 (EOT = End Of Transmission).
*/
read(): string
readChar(): number[]
}
30 changes: 30 additions & 0 deletions src/io/input_all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IInput } from "./input"

export abstract class InputAll implements IInput {
/**
* Read and return the entire contents of this input. No special character is required to indicate
* the end of the input, it is just the end of the string. Should only be called once per object.
*/
abstract readAll(): string

readChar(): number[] {
if (this._buffer === undefined) {
this._buffer = this.readAll()
this._index = 0
}

if (this._index < this._buffer.length) {
const char = this._buffer[this._index++]
let ret: number[] = []
for (let i = 0; i < char.length; i++) {
ret.push(char.charCodeAt(i))
}
return ret
} else {
return [4] // EOT
}
}

private _buffer?: string
private _index: number = 0
}
15 changes: 9 additions & 6 deletions src/io/pipe.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { BufferedOutput } from "./buffered_output"
import { IInput } from "./input"
import { PipeInput } from "./pipe_input"

export class Pipe extends BufferedOutput implements IInput {
/**
* A Pipe provides IOutput and IInput, accepting output and passing it to the input.
* To obtain the input interface PipeInput, call the .input attribute.
*/
export class Pipe extends BufferedOutput {
override async flush(): Promise<void> {
}

read(): string {
const ret = this.allContent
this.clear()
return ret
get input(): PipeInput {
// Should restrict this to just one?
return new PipeInput(this)
}
}
14 changes: 14 additions & 0 deletions src/io/pipe_input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { InputAll } from "./input_all"
import { Pipe } from "./pipe"

export class PipeInput extends InputAll {
constructor(readonly pipe: Pipe) {
super()
}

readAll(): string {
const ret = this.pipe.allContent
this.pipe.clear()
return ret
}
}
27 changes: 0 additions & 27 deletions src/io/single_char_input.ts

This file was deleted.

20 changes: 18 additions & 2 deletions src/io/terminal_input.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import { IInput } from "./input"
import { IStdinCallback } from "../callback"

export class TerminalInput implements IInput {
read(): string {
return ""
constructor(readonly stdinCallback?: IStdinCallback) {}

readChar(): number[] {
if (this._finished || this.stdinCallback === undefined) {
return [4] // EOT
} else {
// What to do if more than one character?
const utf16 = this.stdinCallback()
if (utf16[0] == 4) {
this._finished = true
} else if (utf16[0] == 13) {
return [10]
}
return utf16
}
}

private _finished = false
}
2 changes: 1 addition & 1 deletion src/io/terminal_output.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BufferedOutput } from "./buffered_output"
import { IOutputCallback } from "../output_callback"
import { IOutputCallback } from "../callback"

export class TerminalOutput extends BufferedOutput {

Expand Down
1 change: 0 additions & 1 deletion src/output_callback.ts

This file was deleted.

39 changes: 30 additions & 9 deletions src/shell.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { Aliases } from "./aliases"
import { IOutputCallback, IEnableBufferedStdinCallback, IStdinCallback } from "./callback"
import { CommandRegistry } from "./command_registry"
import { Context } from "./context"
import { Environment } from "./environment"
import { IFileSystem } from "./file_system"
import { History } from "./history"
import { FileInput, FileOutput, IInput, IOutput, Pipe, TerminalInput, TerminalOutput } from "./io"
import { IOutputCallback } from "./output_callback"
import { CommandNode, PipeNode, parse } from "./parse"
import * as FsModule from './wasm/fs'

export namespace IShell {
export interface IOptions {
mountpoint?: string
outputCallback: IOutputCallback
enableBufferedStdinCallback?: IEnableBufferedStdinCallback
stdinCallback?: IStdinCallback
}
}

export class Shell {
constructor(
outputCallback: IOutputCallback,
mountpoint: string = '/drive',
) {
this._outputCallback = outputCallback
this._mountpoint = mountpoint;
constructor(options: IShell.IOptions) {
this._outputCallback = options.outputCallback
this._mountpoint = options.mountpoint ?? "/drive"
this._enableBufferedStdinCallback = options.enableBufferedStdinCallback
this._stdinCallback = options.stdinCallback
this._currentLine = ""
this._aliases = new Aliases()
this._environment = new Environment()
Expand Down Expand Up @@ -96,6 +104,8 @@ export class Shell {
}

async inputs(chars: string[]): Promise<void> {
// Might be best to not have this as it implies each input fron frontend in a single char when
// it can be multiple chars for escape sequences.
for (let i = 0; i < chars.length; ++i) {
await this.input(chars[i])
}
Expand Down Expand Up @@ -127,6 +137,10 @@ export class Shell {

// Keeping this public for tests.
async _runCommands(cmdText: string): Promise<void> {
if (this._enableBufferedStdinCallback) {
this._enableBufferedStdinCallback(true)
}

if (cmdText.startsWith("!")) {
// Get command from history and run that.
const index = parseInt(cmdText.slice(1))
Expand All @@ -141,7 +155,7 @@ export class Shell {

this._history.add(cmdText)

const stdin = new TerminalInput()
const stdin = new TerminalInput(this._stdinCallback)
const stdout = new TerminalOutput(this._outputCallback)
try {
const nodes = parse(cmdText, this._aliases)
Expand All @@ -154,7 +168,7 @@ export class Shell {
const n = commands.length
let prevPipe: Pipe
for (let i = 0; i < n; i++) {
const input = i == 0 ? stdin : prevPipe!
const input = i == 0 ? stdin : prevPipe!.input
const output = i < n-1 ? (prevPipe = new Pipe()) : stdout
await this._runCommand(commands[i], input, output)
}
Expand All @@ -167,6 +181,10 @@ export class Shell {
// Send result via output?????? With color. Should be to stderr.
stdout.write("\x1b[1;31m" + error + "\x1b[1;0m\r\n")
await stdout.flush()
} finally {
if (this._enableBufferedStdinCallback) {
this._enableBufferedStdinCallback(false)
}
}
}

Expand Down Expand Up @@ -219,6 +237,9 @@ export class Shell {
}

private readonly _outputCallback: IOutputCallback
private readonly _enableBufferedStdinCallback?: IEnableBufferedStdinCallback
private readonly _stdinCallback?: IStdinCallback

private _currentLine: string
private _aliases: Aliases
private _environment: Environment
Expand Down
16 changes: 7 additions & 9 deletions tests/io/input.test.ts → tests/io/file_input.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import { shell_setup_simple } from "../shell_setup"
import { FileInput, Input, SingleCharInput } from "../../src/io"
import { FileInput } from "../../src/io"

describe("FileInput", () => {
it("should read from file", async () => {
const { fileSystem } = await shell_setup_simple()
const fileInput = new FileInput(fileSystem, "file2")
const read = fileInput.read()
const read = fileInput.readAll()
expect(read).toEqual("Some other file\nSecond line")
})
})

describe("SingleCharInput", () => {
it("should read from file a character at a time", async () => {
const { fileSystem } = await shell_setup_simple()
const fileInput = new SingleCharInput(new FileInput(fileSystem, "file2"))
const fileInput = new FileInput(fileSystem, "file2")
const expected = "Some other file\nSecond line"
for (let i = 0; i < expected.length; i++) {
const charCode = fileInput.readCharCode()
expect(charCode).toEqual(expected.charCodeAt(i))
const charCodes = fileInput.readChar()
expect(charCodes[0]).toEqual(expected.charCodeAt(i))
}
for (let i = 0; i < 3; i++) {
const charCode = fileInput.readCharCode()
const charCodes = fileInput.readChar()
// Once end of input file reached, always returns char code 4 (EOT).
expect(charCode).toEqual(4)
expect(charCodes[0]).toEqual(4)
}
})
})
Loading

0 comments on commit 58b5e63

Please sign in to comment.