From 1ad25a4b32bba6ae732d446d3eb03d1c7e1e7512 Mon Sep 17 00:00:00 2001 From: Will Fancher Date: Wed, 1 Aug 2018 08:08:29 -0400 Subject: [PATCH] Begin reorganizing the kernel with kernel-space typescript fs --- example/index.html | 9 ++ example/package.json | 19 ++++ example/src/index.ts | 20 +++++ example/tsconfig.json | 15 ++++ example/webpack.config.js | 16 ++++ kernel/package.json | 26 ++++++ kernel/src/index.ts | 98 +++++++++++++++++++++ kernel/src/stdio_handles.ts | 170 ++++++++++++++++++++++++++++++++++++ kernel/tsconfig.json | 15 ++++ 9 files changed, 388 insertions(+) create mode 100644 example/index.html create mode 100644 example/package.json create mode 100644 example/src/index.ts create mode 100644 example/tsconfig.json create mode 100644 example/webpack.config.js create mode 100644 kernel/package.json create mode 100644 kernel/src/index.ts create mode 100644 kernel/src/stdio_handles.ts create mode 100644 kernel/tsconfig.json diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..a581623 --- /dev/null +++ b/example/index.html @@ -0,0 +1,9 @@ + + + + Getting Started + + + + + diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..ad1fe29 --- /dev/null +++ b/example/package.json @@ -0,0 +1,19 @@ +{ + "name": "webabi-example", + "version": "0.0.1", + "description": "", + "scripts": { + "build": "tsc", + "install": "npm run build", + "test": "npm run build && webpack && node build/main.js" + }, + "author": "Will Fancher", + "dependencies": { + "typescript": "^2.9.2", + "webabi-kernel": "file:../kernel" + }, + "devDependencies": { + "webpack": "^4.16.3", + "webpack-cli": "^3.1.0" + } +} diff --git a/example/src/index.ts b/example/src/index.ts new file mode 100644 index 0000000..7691e73 --- /dev/null +++ b/example/src/index.ts @@ -0,0 +1,20 @@ +import { Device, configureFileSystem, BFSCallback, Stats, File, FileFlag } from "webabi-kernel"; + +class JSaddleDevice implements Device { + open(flag: FileFlag, cb: BFSCallback): void { + } + openSync(flag: FileFlag, mode: number): File { + throw "NYI"; + } + stat(isLstat: boolean | null, cb: BFSCallback): void { + } + statSync(isLstat: boolean | null): Stats { + throw "NYI"; + } +} + +configureFileSystem({ "/jsaddle": new JSaddleDevice() }, (err, fs) => { + console.log(err); + let buf = Buffer.from("hi\n"); + fs.write(1, buf, 0, buf.length, null, () => {}); +}); diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..156c09a --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "outDir": "dist", + "lib": ["dom", "es2015", "es2016", "es2017"], + "module": "commonjs", + "declaration": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/example/webpack.config.js b/example/webpack.config.js new file mode 100644 index 0000000..fc441df --- /dev/null +++ b/example/webpack.config.js @@ -0,0 +1,16 @@ +const path = require('path'); + +console.log(require.resolve("webabi-kernel")); + +module.exports = { + entry: './dist/index.js', + mode: "production", + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'build') + }, + resolve: { + // Using file:../kernel in package.json requires this + symlinks: false + } +}; diff --git a/kernel/package.json b/kernel/package.json new file mode 100644 index 0000000..e904b18 --- /dev/null +++ b/kernel/package.json @@ -0,0 +1,26 @@ +{ + "name": "webabi-kernel", + "version": "0.0.1", + "description": "", + "scripts": { + "build": "tsc", + "main": "npm run tsc && node dist/foo.js", + "install": "npm run build" + }, + "main": "dist/index.js", + "author": "Will Fancher", + "dependencies": { + "@types/archiver": "^2.0.0", + "@types/async": "^2.0.49", + "@types/body-parser": "^1.16.4", + "@types/dropboxjs": "0.0.29", + "@types/express": "^4.0.36", + "@types/filesystem": "0.0.28", + "@types/isomorphic-fetch": "^0.0.34", + "@types/mocha": "^5.2.5", + "@types/node": "^7.0", + "@types/rimraf": "^2.0.2", + "browserfs": "^1.4.3", + "typescript": "^2.9.2" + } +} diff --git a/kernel/src/index.ts b/kernel/src/index.ts new file mode 100644 index 0000000..0500894 --- /dev/null +++ b/kernel/src/index.ts @@ -0,0 +1,98 @@ +import MountableFileSystem from "browserfs/dist/node/backend/MountableFileSystem"; +import * as handles from "./stdio_handles"; +import { BaseFileSystem, FileSystemConstructor, BFSCallback, + BFSOneArgCallback, BFSThreeArgCallback, FileSystem, + FileSystemOptions } from "browserfs/dist/node/core/file_system"; +import { FileType } from 'browserfs/dist/node/core/node_fs_stats'; +import Stats from 'browserfs/dist/node/core/node_fs_stats'; +import { File } from "browserfs/dist/node/core/file"; +import { FileFlag } from "browserfs/dist/node/core/file_flag"; +import { ApiError, ErrorCode } from 'browserfs/dist/node/core/api_error'; +import FS from "browserfs/dist/node/core/FS"; + +export interface Device { + open(flag: FileFlag, cb: BFSCallback): void; + stat(isLstat: boolean | null, cb: BFSCallback): void; +} + +export interface DeviceFileSystemOptions { + devices: {[name: string]: Device}; +} + +export class DeviceFileSystem extends BaseFileSystem implements FileSystem { + public static readonly Name = "DeviceFileSystem"; + public static readonly Options: FileSystemOptions = {}; + + public static Create(opts: DeviceFileSystemOptions, cb: BFSCallback): void { + return cb(null, new DeviceFileSystem(opts)); + } + + public static isAvailable(): boolean { + return true; + } + + options: DeviceFileSystemOptions; + + constructor(options: DeviceFileSystemOptions) { + super(); + this.options = options; + } + + public getName() { + return "DeviceFileSystem"; + } + public isReadOnly() { + return false; + } + public supportsProps() { + return false; + } + public supportsSynch() { + return false; + } + + public openFile(p: string, flag: FileFlag, cb: BFSCallback): void { + if (this.options.devices.hasOwnProperty(p)) { + return this.options.devices[p].open(flag, cb); + } else { + return cb(ApiError.ENOENT(p)); + } + } + public stat(p: string, isLstat: boolean | null, cb: BFSCallback): void { + if (this.options.devices.hasOwnProperty(p)) { + return this.options.devices[p].stat(isLstat, cb); + } else { + return cb(ApiError.ENOENT(p)); + } + } +} + +export function configureFileSystem(devices: { [name: string]: Device }, cb: BFSCallback): void { + DeviceFileSystem.Create({ devices: devices }, (e, dfs) => { + if (e) { + cb(e); + return; + } + MountableFileSystem.Create({ + "/dev": dfs + }, (e, mfs) => { + if (e) { + cb(e); + return + } + + const fs = new FS(); + fs.initialize(mfs); + + const fdMap: {[id: number]: File} = (fs as any).fdMap; + fdMap[0] = handles.stdin; + fdMap[1] = handles.stdout; + fdMap[2] = handles.stderr; + + cb(undefined, fs); + }); + }); +} + +// Re-export for device implementors +export { BFSCallback, Stats, File, FileFlag }; diff --git a/kernel/src/stdio_handles.ts b/kernel/src/stdio_handles.ts new file mode 100644 index 0000000..1c4ea94 --- /dev/null +++ b/kernel/src/stdio_handles.ts @@ -0,0 +1,170 @@ +import { File, BaseFile } from "browserfs/dist/node/core/file"; +import { BaseFileSystem, FileSystemConstructor, BFSCallback, BFSOneArgCallback, BFSThreeArgCallback, FileSystem, FileSystemOptions } from "browserfs/dist/node/core/file_system"; +import { FileType } from 'browserfs/dist/node/core/node_fs_stats'; +import Stats from 'browserfs/dist/node/core/node_fs_stats'; +import { ApiError, ErrorCode } from 'browserfs/dist/node/core/api_error'; + +export let stdin: File; +export let stdout: File; +export let stderr: File; + +class UselessFile extends BaseFile implements File { + getPos(): number | undefined { + return undefined; + } + stat(cb: BFSCallback): void { + return cb(undefined, new Stats(FileType.FILE, 0)); + } + statSync(): Stats { + return new Stats(FileType.FILE, 0) + } + close(cb: BFSOneArgCallback): void { + cb(new ApiError(ErrorCode.ENOTSUP)); + } + closeSync(): void { + throw new ApiError(ErrorCode.ENOTSUP); + } + truncate(len: number, cb: BFSOneArgCallback): void { + cb(new ApiError(ErrorCode.ENOTSUP)); + } + truncateSync(len: number): void { + throw new ApiError(ErrorCode.ENOTSUP); + } + write(buffer: Buffer, offset: number, length: number, position: number | null, cb: BFSThreeArgCallback): void { + cb(new ApiError(ErrorCode.ENOTSUP)); + } + writeSync(buffer: Buffer, offset: number, length: number, position: number | null): number { + throw new ApiError(ErrorCode.ENOTSUP); + } + read(buffer: Buffer, offset: number, length: number, position: number | null, cb: BFSThreeArgCallback): void { + cb(new ApiError(ErrorCode.ENOTSUP)); + } + readSync(buffer: Buffer, offset: number, length: number, position: number): number { + throw new ApiError(ErrorCode.ENOTSUP); + } +} + +if (process && !(process as any).browser) { + interface Request { + buffer: Buffer; + offset: number; + length: number; + cb: BFSThreeArgCallback; + } + class ReadWriteStreamFile extends UselessFile implements File { + stream: NodeJS.ReadWriteStream; + requests: [Request] = <[Request]> []; + leftover?: Buffer = null; + + constructor(stream: NodeJS.ReadWriteStream) { + super(); + this.stream = stream; + this.stream.pause(); + this.stream.on("error", (err) => { + const reqs = this.requests; + this.requests = <[Request]> []; + for (const req of reqs) { + req.cb(err, undefined, undefined); + } + }); + this.stream.on("data", (buf) => { + this.stream.pause(); + if (this.leftover) { + buf = Buffer.concat([this.leftover, buf]); + this.leftover = null; + } + this.onData(buf); + }); + } + + onData(buf: Buffer): void { + const reqs = this.requests; + this.requests = <[Request]> []; + let nextBuf: Buffer | null = null; + for (const req of reqs) { + if (buf.length > req.length) { + nextBuf = buf.slice(req.length); + buf = buf.slice(0, req.length); + } else { + nextBuf = null; + } + + const copied = buf.copy(req.buffer, req.offset); + req.cb(undefined, copied, req.buffer); + + buf = nextBuf; + } + + if (nextBuf) { + // nextBuf may still have the old leftover underlying it. + // Use Buffer.from to avoid retaining the entire history. + this.leftover = Buffer.from(nextBuf); + } + }; + close(cb: BFSOneArgCallback): void { + this.stream.end(cb); + } + write(buffer: Buffer, offset: number, length: number, position: number | null, cb: BFSThreeArgCallback): void { + this.stream.write(buffer.slice(offset, offset + length), (err) => { + if (err) { + cb(err); + } else { + cb(undefined, length, buffer); + } + }); + } + read(buffer: Buffer, offset: number, length: number, position: number | null, cb: BFSThreeArgCallback): void { + this.stream.resume(); + this.requests.push({ + buffer: buffer, + offset: offset, + length: length, + cb: cb + }); + } + } + + stdin = new ReadWriteStreamFile(process.stdin); + stdout = new ReadWriteStreamFile(process.stdout); + stderr = new ReadWriteStreamFile(process.stderr); +} else { + class ConsoleFile extends UselessFile implements File { + log: (msg: string) => void; + buffer?: Buffer = null; + + constructor(log: (msg: string) => void) { + super(); + this.log = log; + } + + write(buffer: Buffer, offset: number, length: number, position: number | null, cb: BFSThreeArgCallback): void { + let slicedBuffer = buffer.slice(offset, offset + length); + let n = slicedBuffer.lastIndexOf("\n"); + if (n < 0) { + if (this.buffer) { + this.buffer = Buffer.concat([this.buffer, slicedBuffer]); + } else { + this.buffer = slicedBuffer; + } + } else { + let logBuffer = slicedBuffer.slice(0, n); + if (this.buffer) { + logBuffer = Buffer.concat([this.buffer, logBuffer]); + } + this.log(logBuffer.toString()); + + // + 1 to skip the \n + if (n + 1 < slicedBuffer.length) { + this.buffer = slicedBuffer.slice(n + 1); + } else { + this.buffer = null; + } + } + cb(undefined, length, buffer); + } + } + + stdin = new UselessFile(); + stdout = new ConsoleFile((msg) => console.log(msg)); + stderr = new ConsoleFile((msg) => console.error(msg)); +} diff --git a/kernel/tsconfig.json b/kernel/tsconfig.json new file mode 100644 index 0000000..156c09a --- /dev/null +++ b/kernel/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "outDir": "dist", + "lib": ["dom", "es2015", "es2016", "es2017"], + "module": "commonjs", + "declaration": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +}