Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for vim #86

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ tsconfig.tsbuildinfo
playwright-report/
test-results/
test/package-lock.json
*.data
*.js
*.wasm
cockle_wasm_env/
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ The commands used here are either built-in commands implemented in TypeScript, o
commands compiled into `.js` and `.wasm` files. The latter are built by
[Emscripten-forge](https://emscripten-forge.org/) and are added to a deployment during the build process.

[Emscripten-forge](https://emscripten-forge.org/) packages containing WebAssembly commands that are
currently supported and tested are as follows. Each package contains a single commmand with the same
name as the package unless otherwise specified:

- `coreutils`: multiple core commands including `cat`, `cp`, `echo`, `ls`, `mkdir`, `mv`, `rm`, `touch`, `uname`, and `wc`
- `grep`
- `lua`
- `vim`

## Build

```bash
Expand Down
3 changes: 3 additions & 0 deletions demo/cockle-config-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
{
"package": "local-cmd",
"local_directory": "../test/local-packages"
},
{
"package": "vim"
}
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 34 additions & 4 deletions src/buffered_io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ abstract class BufferedIO {
protected _enabled: boolean = false;
protected _sharedArrayBuffer: SharedArrayBuffer;

protected _maxReadChars: number = 8; // Max number of actual characters in a token.
protected _maxReadChars: number = 64; // Max number of actual characters in a token.
protected _readArray: Int32Array;
protected _readCount: number = 0;

protected _maxWriteChars: number = 128; // Multiples of this can be sent consecutively.
protected _maxWriteChars: number = 256; // Multiples of this can be sent consecutively.
protected _writeArray: Int32Array;
}

Expand Down Expand Up @@ -278,8 +278,34 @@ export class WorkerBufferedIO extends BufferedIO {
this._allowAdjacentNewline = set;
}

/**
* Poll for whether readable (there is input ready to read) and/or writable.
* Currently assumes always writable.
*/
poll(timeoutMs: number): number {
// Constants.
const POLLIN = 1;
const POLLOUT = 4;

const t = timeoutMs > 0 ? timeoutMs : 0;
const readableCheck = Atomics.wait(this._readArray, READ_MAIN, this._readCount, t);
const readable = readableCheck === 'not-equal';

const writable = true;
return (readable ? POLLIN : 0) | (writable ? POLLOUT : 0);
}

read(): number[] {
Atomics.wait(this._readArray, READ_MAIN, this._readCount);
if ((this.termios.c_iflag & InputFlag.IXON) > 0) {
// Wait for main worker to store a new input characters.
Atomics.wait(this._readArray, READ_MAIN, this._readCount);
}

const readCount = Atomics.load(this._readArray, READ_MAIN);
if (readCount === this._readCount) {
return [];
}

const read = this._loadFromSharedArrayBuffer();
this._readCount++;

Expand All @@ -292,6 +318,10 @@ export class WorkerBufferedIO extends BufferedIO {
return ret;
}

get termios(): Termios {
return this._termios;
}

write(text: string | Int8Array | number[]): void {
let chars: number[] = [];
if (typeof text === 'string') {
Expand Down Expand Up @@ -428,7 +458,7 @@ export class WorkerBufferedIO extends BufferedIO {
return ret;
}

public termios: Termios = Termios.newDefaultWasm();
private _termios: Termios = Termios.newDefaultWasm();
private _allowAdjacentNewline = false;
private _writeColumn = 0;
}
65 changes: 36 additions & 29 deletions src/commands/wasm_command_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export abstract class WasmCommandRunner implements ICommandRunner {
let _getCharBuffer: number[] = [];

// Functions for monkey-patching.
function getChar(tty: any) {
function getChar(tty: any): number | null {
if (_getCharBuffer.length > 0) {
return _getCharBuffer.shift()!;
}
Expand Down Expand Up @@ -58,6 +58,10 @@ export abstract class WasmCommandRunner implements ICommandRunner {
];
}

function poll(stream: any, timeoutMs: number): number {
return bufferedIO.poll(timeoutMs);
}

function write(
stream: any,
buffer: Int8Array,
Expand Down Expand Up @@ -97,35 +101,38 @@ export abstract class WasmCommandRunner implements ICommandRunner {
exitCode = moduleExitCode;
}
},
preRun: (module: any) => {
if (Object.prototype.hasOwnProperty.call(module, 'FS')) {
// Use PROXYFS so that command sees the shared FS.
const FS = module.FS;
FS.mkdir(mountpoint, 0o777);
FS.mount(fileSystem.PROXYFS, { root: mountpoint, fs: fileSystem.FS }, mountpoint);
FS.chdir(fileSystem.FS.cwd());
preRun: [
(module: any) => {
if (Object.prototype.hasOwnProperty.call(module, 'FS')) {
// Use PROXYFS so that command sees the shared FS.
const FS = module.FS;
FS.mkdir(mountpoint, 0o777);
FS.mount(fileSystem.PROXYFS, { root: mountpoint, fs: fileSystem.FS }, mountpoint);
FS.chdir(fileSystem.FS.cwd());
}

if (Object.prototype.hasOwnProperty.call(module, 'ENV')) {
// Copy environment variables into command.
context.environment.copyIntoCommand(module.ENV, stdout.supportsAnsiEscapes());
}

if (Object.prototype.hasOwnProperty.call(module, 'TTY')) {
// Monkey patch get/set termios and get window size.
module.TTY.default_tty_ops.ioctl_tcgets = getTermios;
module.TTY.default_tty_ops.ioctl_tcsets = setTermios;
module.TTY.default_tty_ops.ioctl_tiocgwinsz = getWindowSize;

// May only need to be for some TTYs?
module.TTY.stream_ops.write = write;
module.TTY.stream_ops.poll = poll;

// Monkey patch stdin get_char.
const stdinDeviceId = module.FS.makedev(5, 0);
const stdinTty = module.TTY.ttys[stdinDeviceId];
stdinTty.ops.get_char = getChar;
}
}

if (Object.prototype.hasOwnProperty.call(module, 'ENV')) {
// Copy environment variables into command.
context.environment.copyIntoCommand(module.ENV, stdout.supportsAnsiEscapes());
}

if (Object.prototype.hasOwnProperty.call(module, 'TTY')) {
// Monkey patch get/set termios and get window size.
module.TTY.default_tty_ops.ioctl_tcgets = getTermios;
module.TTY.default_tty_ops.ioctl_tcsets = setTermios;
module.TTY.default_tty_ops.ioctl_tiocgwinsz = getWindowSize;

// Monkey patch write.
module.TTY.stream_ops.write = write;

// Monkey patch stdin get_char.
const stdinDeviceId = module.FS.makedev(5, 0);
const stdinTty = module.TTY.ttys[stdinDeviceId];
stdinTty.ops.get_char = getChar;
}
}
]
});

if (exitCode === undefined) {
Expand Down
25 changes: 24 additions & 1 deletion src/termios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export enum LocalFlag {
ECHOCTL = 0x0200, // Terminal special characters are echoed
ECHOPRT = 0x0400, // Characters are printed as they are erased
ECHOKE = 0x0800, // KILL is echoed by erasing each character on the line
IEXTEN = 0x8000 // Enable implementation-dewfined input processing
IEXTEN = 0x8000 // Enable implementation-defined input processing
}

export enum ControlCharacter {
Expand Down Expand Up @@ -77,6 +77,27 @@ export class Termios implements ITermios {
};
}

// Log to console for debug purposes.
log() {
const enumHelper = (enumType: any, name: string, enumValue: any) => {
const s: string[] = [];
for (const [k, v] of Object.entries(enumType).filter(([k, v]) => k[0].match(/\D/))) {
if ((enumValue & (v as number)) > 0) {
s.push(k);
}
}
return ` ${name} = ${enumValue} 0x${enumValue.toString(16)} = ${s.join(' ')}`;
};

const log: string[] = ['Termios:'];
log.push(enumHelper(InputFlag, 'c_iflag', this.c_iflag));
log.push(enumHelper(OutputFlag, 'c_oflag', this.c_oflag));
log.push(` c_cflag = ${this.c_cflag} 0x${this.c_cflag.toString(16)}`);
log.push(enumHelper(LocalFlag, 'c_lflag', this.c_lflag));
log.push(` c_cc = ${this.c_cc}`);
console.log(log.join('\n'));
}

static newDefaultWasm(): Termios {
const ret = new Termios();
ret.setDefaultWasm();
Expand All @@ -89,6 +110,7 @@ export class Termios implements ITermios {
this.c_cflag = iTermios.c_cflag;
this.c_lflag = iTermios.c_lflag;
this.c_cc = [...iTermios.c_cc]; // Shallow copy.
this.log();
}

setDefaultWasm(): void {
Expand Down Expand Up @@ -139,6 +161,7 @@ export class Termios implements ITermios {
0,
0
];
this.log();
}

c_iflag: InputFlag = 0 as InputFlag;
Expand Down
15 changes: 14 additions & 1 deletion src/tools/prepare_wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,26 @@ fs.writeFileSync(targetConfigFile, JSON.stringify(cockleConfig, null, 2));
const filenames = [targetConfigFile];

// Output wasm files and their javascript wrappers.
const requiredSuffixes = {
'.js': true,
'.wasm': true,
'.data': false,
'-fs.js': false,
'-fs.data': false
};
for (const packageConfig of cockleConfig) {
const sourceDirectory = packageConfig.local_directory ?? path.join(envPath, 'bin');
const moduleNames = packageConfig.modules.map((x: any) => x.name);
for (const moduleName of moduleNames) {
for (const suffix of ['.js', '.wasm']) {
for (const [suffix, required] of Object.entries(requiredSuffixes)) {
const filename = moduleName + suffix;
const srcFilename = path.join(sourceDirectory, filename);
if (!fs.existsSync(srcFilename)) {
if (required) {
throw new Error(`No such file: ${srcFilename}`);
}
continue;
}
if (wantCopy) {
const targetFileName = path.join(target, filename);
fs.copyFileSync(srcFilename, targetFileName);
Expand Down
3 changes: 3 additions & 0 deletions test/cockle-config-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
{
"package": "lua"
},
{
"package": "vim"
},
{
"package": "local-cmd",
"local_directory": "local-packages"
Expand Down
34 changes: 34 additions & 0 deletions test/tests/command/vim.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from '@playwright/test';
import { shellLineSimple, test } from '../utils';

test.describe('vim command', () => {
test('should output version', async ({ page }) => {
const output = await shellLineSimple(page, 'vim --version');
expect(output).toMatch(/^vim --version\r\nVIM - Vi IMproved 9.1/);
});

test('should run interactively and exit', async ({ page }) => {
await page.evaluate(async () => {
const { shell } = await globalThis.cockle.shellSetupEmpty();
await Promise.all([
shell.inputLine('vim'),
globalThis.cockle.terminalInput(shell, ['\x1b', ':', 'q', '\r'])
]);
});
});

test('should create new file', async ({ page }) => {
const output = await page.evaluate(async () => {
const { shell, output } = await globalThis.cockle.shellSetupEmpty();
await Promise.all([
shell.inputLine('vim'),
globalThis.cockle.terminalInput(shell, [...'ihi QW\x1b:wq out\r'])
]);
// New file should exist.
output.clear();
await shell.inputLine('cat out');
return output.text;
});
expect(output).toMatch(/^cat out\r\nhi QW\r\n/);
});
});
Loading