Remove buggy FFI implementation in favor of Node API (implemented by Bun and Deno)
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-03-01 11:06:47 -05:00
parent 9458794fa3
commit ab42873182
7 changed files with 47 additions and 314 deletions

View File

@ -1,56 +0,0 @@
/**
* This is all the nasty ffi setup for the bun runtime
*/
import { dlopen, ptr, suffix } from 'bun:ffi';
import { IFFI } from '../common/runtime.ts';
const getLib = (name: string) => {
return dlopen(
name,
{
tcgetattr: {
args: ['i32', 'pointer'],
returns: 'i32',
},
tcsetattr: {
args: ['i32', 'i32', 'pointer'],
returns: 'i32',
},
cfmakeraw: {
args: ['pointer'],
returns: 'void',
},
},
);
};
let cStdLib: any = { symbols: {} };
try {
cStdLib = getLib(`libc.${suffix}`);
} catch {
try {
cStdLib = getLib(`libc.${suffix}.6`);
} catch {
throw new Error('Could not find c standard library');
}
}
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
let closed = false;
const BunFFI: IFFI = {
tcgetattr,
tcsetattr,
cfmakeraw,
getPointer: ptr,
close: () => {
if (!closed) {
cStdLib.close();
closed = true;
}
// Do nothing if FFI library was already closed
},
};
export default BunFFI;

View File

@ -1,28 +1,13 @@
/** /**
* Wrap the runtime-specific hook into stdin * Wrap the runtime-specific hook into stdin
*/ */
import process from 'node:process';
import Ansi from '../common/ansi.ts'; import Ansi from '../common/ansi.ts';
import { defaultTerminalSize } from '../common/config.ts'; import { defaultTerminalSize } from '../common/config.ts';
import { readKey } from '../common/fns.ts'; import { readKey } from '../common/fns.ts';
import { ITerminal, ITerminalSize } from '../common/types.ts'; import { ITerminal, ITerminalSize } from '../common/types.ts';
const BunTerminalIO: ITerminal = { async function _getTerminalSizeFromAnsi(): Promise<ITerminalSize> {
// Deno only returns arguments passed to the script, so
// remove the bun runtime executable, and entry script arguments
// to have consistent argument lists
argv: (Bun.argv.length > 2) ? Bun.argv.slice(2) : [],
inputLoop: async function* inputLoop() {
for await (const chunk of Bun.stdin.stream()) {
yield chunk;
}
return null;
},
/**
* Get the size of the terminal window via ANSI codes
* @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way
*/
getTerminalSize: async function getTerminalSize(): Promise<ITerminalSize> {
// Tell the cursor to move to Row 999 and Column 999 // Tell the cursor to move to Row 999 and Column 999
// Since this command specifically doesn't go off the screen // Since this command specifically doesn't go off the screen
// When we ask where the cursor is, we should get the size of the screen // When we ask where the cursor is, we should get the size of the screen
@ -53,6 +38,31 @@ const BunTerminalIO: ITerminal = {
rows, rows,
cols, cols,
}; };
}
const BunTerminalIO: ITerminal = {
// Deno only returns arguments passed to the script, so
// remove the bun runtime executable, and entry script arguments
// to have consistent argument lists
argv: (Bun.argv.length > 2) ? Bun.argv.slice(2) : [],
inputLoop: async function* inputLoop() {
for await (const chunk of Bun.stdin.stream()) {
yield chunk;
}
return null;
},
/**
* Get the size of the terminal window via ANSI codes
* @see https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html#window-size-the-hard-way
*/
getTerminalSize: function getTerminalSize(): Promise<ITerminalSize> {
const [cols, rows] = process.stdout.getWindowSize();
return Promise.resolve({
rows,
cols,
});
}, },
readStdin: async function (): Promise<string | null> { readStdin: async function (): Promise<string | null> {
const raw = await BunTerminalIO.readStdinRaw(); const raw = await BunTerminalIO.readStdinRaw();

View File

@ -1,6 +1,6 @@
import process from 'node:process';
import { readKey } from './fns.ts'; import { readKey } from './fns.ts';
import { getRuntime, logError } from './runtime.ts'; import { getRuntime, logError } from './runtime.ts';
import { getTermios } from './termios.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
export async function main() { export async function main() {
@ -8,16 +8,14 @@ export async function main() {
const { term } = rt; const { term } = rt;
// Setup raw mode, and tear down on error or normal exit // Setup raw mode, and tear down on error or normal exit
const t = await getTermios(); process.stdin.setRawMode(true);
t.enableRawMode();
rt.onExit(() => { rt.onExit(() => {
t.disableRawMode(); process.stdin.setRawMode(false);
t.cleanup();
}); });
// Setup error handler to log to file // Setup error handler to log to file
rt.onEvent('error', (error) => { rt.onEvent('error', (error) => {
t.disableRawMode(); process.stdin.setRawMode(false);
logError(JSON.stringify(error, null, 2)); logError(JSON.stringify(error, null, 2));
}); });

View File

@ -1,9 +1,9 @@
import process from 'node:process';
import { IRuntime, ITestBase } from './types.ts'; import { IRuntime, ITestBase } from './types.ts';
import { getTermios } from './termios.ts';
import { noop } from './fns.ts'; import { noop } from './fns.ts';
import { SCROLL_ERR_FILE, SCROLL_LOG_FILE } from './config.ts'; import { SCROLL_ERR_FILE, SCROLL_LOG_FILE } from './config.ts';
export type { IFFI, IFileIO, IRuntime, ITerminal } from './types.ts'; export type { IFileIO, IRuntime, ITerminal } from './types.ts';
/** /**
* Which Typescript runtime is currently being used * Which Typescript runtime is currently being used
@ -53,12 +53,9 @@ export function logError(s: unknown): void {
*/ */
export function die(s: string | Error): void { export function die(s: string | Error): void {
logError(s); logError(s);
getTermios().then((t) => { process.stdin.setRawMode(false);
t.disableRawMode();
t.cleanup();
console.error(s); console.error(s);
getRuntime().then((r) => r.exit()); getRuntime().then((r) => r.exit());
});
} }
/** /**

View File

@ -1,132 +0,0 @@
import { die, IFFI, importForRuntime, log, LogLevel } from './runtime.ts';
export const STDIN_FILENO = 0;
export const TCSANOW = 0;
export const TERMIOS_SIZE = 60;
/**
* Implementation to toggle raw mode
*/
class Termios {
/**
* The ffi implementation for the current runtime
* @private
*/
#ffi: IFFI;
/**
* Are we in raw mode?
* @private
*/
#inRawMode: boolean;
/**
* The saved version of the termios struct for cooked/canonical mode
* @private
*/
#cookedTermios: Uint8Array;
/**
* The data for the termios struct we are manipulating
* @private
*/
#termios: Uint8Array;
/**
* Has the nasty ffi stuff been cleaned up?
* @private
*/
#cleaned: boolean = false;
/**
* The pointer to the termios struct
* @private
*/
readonly #ptr: unknown;
constructor(ffi: IFFI) {
this.#ffi = ffi;
this.#inRawMode = false;
// These are the TypedArrays linked to the raw pointer data
this.#cookedTermios = new Uint8Array(TERMIOS_SIZE);
this.#termios = new Uint8Array(TERMIOS_SIZE);
// The current pointer for C
this.#ptr = ffi.getPointer(this.#termios);
}
cleanup() {
if (!this.#cleaned) {
this.#ffi.close();
this.#cleaned = true;
}
log('Attempting to cleanup Termios class again', LogLevel.Warning);
}
enableRawMode() {
if (this.#inRawMode) {
throw new Error('Can not enable raw mode when in raw mode');
}
// Get the current termios settings
let res = this.#ffi.tcgetattr(STDIN_FILENO, this.#ptr);
if (res === -1) {
die('Failed to get terminal settings');
}
// The #ptr property is pointing to the #termios TypedArray. As the pointer
// is manipulated, the TypedArray is as well. We will use this to save
// the original canonical/cooked terminal settings for disabling raw mode later.
// @ts-ignore: bad type definition
this.#cookedTermios = new Uint8Array(this.#termios, 0, 60);
// Update termios struct with the raw settings
this.#ffi.cfmakeraw(this.#ptr);
// Actually set the new termios settings
res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, this.#ptr);
if (res === -1) {
die("Failed to update terminal settings. Can't enter raw mode");
}
this.#inRawMode = true;
}
disableRawMode() {
// Don't even bother throwing an error if we try to disable raw mode
// and aren't in raw mode. It just doesn't really matter.
if (!this.#inRawMode) {
log(
'Attempting to disable raw mode when not in raw mode',
LogLevel.Warning,
);
return;
}
const oldTermiosPtr = this.#ffi.getPointer(this.#cookedTermios);
const res = this.#ffi.tcsetattr(STDIN_FILENO, TCSANOW, oldTermiosPtr);
if (res === -1) {
die('Failed to restore canonical mode.');
}
this.#inRawMode = false;
}
}
let termiosSingleton: Termios | null = null;
export const getTermios = async () => {
if (termiosSingleton !== null) {
return termiosSingleton;
}
// Get the runtime-specific ffi wrappers
const ffi = await importForRuntime('ffi');
termiosSingleton = new Termios(ffi);
return termiosSingleton;
};

View File

@ -8,37 +8,6 @@ export interface ITerminalSize {
cols: number; cols: number;
} }
/**
* The native functions for getting/setting terminal settings
*/
export interface IFFI {
/**
* Get the existing termios settings (for canonical mode)
*/
tcgetattr(fd: number, termiosPtr: unknown): number;
/**
* Update the termios settings
*/
tcsetattr(fd: number, act: number, termiosPtr: unknown): number;
/**
* Update the termios pointer with raw mode settings
*/
cfmakeraw(termiosPtr: unknown): void;
/**
* Convert a TypedArray to an opaque pointer for ffi calls
*/
// deno-lint-ignore no-explicit-any
getPointer(ta: any): unknown;
/**
* Closes the FFI handle
*/
close(): void;
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Runtime adapter interfaces // Runtime adapter interfaces
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -1,53 +0,0 @@
// Deno-specific ffi code
import { IFFI } from '../common/runtime.ts';
let suffix = '';
switch (Deno.build.os) {
case 'windows':
suffix = 'dll';
break;
case 'darwin':
suffix = 'dylib';
break;
default:
suffix = 'so.6';
break;
}
const cSharedLib = `libc.${suffix}`;
const cStdLib = Deno.dlopen(
cSharedLib,
{
tcgetattr: {
parameters: ['i32', 'pointer'],
result: 'i32',
},
tcsetattr: {
parameters: ['i32', 'i32', 'pointer'],
result: 'i32',
},
cfmakeraw: {
parameters: ['pointer'],
result: 'void',
},
} as const,
);
const { tcgetattr, tcsetattr, cfmakeraw } = cStdLib.symbols;
let closed = false;
const DenoFFI: IFFI = {
tcgetattr,
tcsetattr,
cfmakeraw,
getPointer: Deno.UnsafePointer.of,
close: () => {
if (!closed) {
cStdLib.close();
closed = true;
}
// Do nothing if FFI library was already closed
},
};
export default DenoFFI;