diff --git a/packages/ide/src/fill/child_process.ts b/packages/ide/src/fill/child_process.ts index a7415d04e..a41522084 100644 --- a/packages/ide/src/fill/child_process.ts +++ b/packages/ide/src/fill/child_process.ts @@ -1,10 +1,85 @@ -import { CP } from "@coder/protocol"; +import * as cp from "child_process"; +import { Client, useBuffer } from "@coder/protocol"; import { client } from "./client"; import { promisify } from "./util"; -const cp = new CP(client); +class CP { + public constructor( + private readonly client: Client, + ) { } + + public exec = ( + command: string, + options?: { encoding?: BufferEncoding | string | "buffer" | null } & cp.ExecOptions | null | ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void), + callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void), + ): cp.ChildProcess => { + // TODO: Probably should add an `exec` instead of using `spawn`, especially + // since bash might not be available. + const childProcess = this.client.spawn("bash", ["-c", command.replace(/"/g, "\\\"")]); + + let stdout = ""; + childProcess.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + let stderr = ""; + childProcess.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + childProcess.on("exit", (exitCode) => { + const error = exitCode !== 0 ? new Error(stderr) : null; + if (typeof options === "function") { + callback = options; + } + if (callback) { + // @ts-ignore not sure how to make this work. + callback( + error, + useBuffer(options) ? Buffer.from(stdout) : stdout, + useBuffer(options) ? Buffer.from(stderr) : stderr, + ); + } + }); + + // @ts-ignore TODO: not fully implemented + return childProcess; + } + + public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => { + if (options && options.env && options.env.AMD_ENTRYPOINT) { + // @ts-ignore TODO: not fully implemented + return this.client.bootstrapFork( + options.env.AMD_ENTRYPOINT, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); + } + + // @ts-ignore TODO: not fully implemented + return this.client.fork( + modulePath, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); + } + + public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => { + // @ts-ignore TODO: not fully implemented + return this.client.spawn( + command, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); + } +} + +const fillCp = new CP(client); // tslint:disable-next-line no-any makes util.promisify return an object -(cp as any).exec[promisify.customPromisifyArgs] = ["stdout", "stderr"]; +(fillCp as any).exec[promisify.customPromisifyArgs] = ["stdout", "stderr"]; -export = cp; +export = fillCp; diff --git a/packages/ide/src/fill/fs.ts b/packages/ide/src/fill/fs.ts index f93c5ceae..b17ac9819 100644 --- a/packages/ide/src/fill/fs.ts +++ b/packages/ide/src/fill/fs.ts @@ -1,4 +1,802 @@ -import { FS } from "@coder/protocol"; +import { exec, ChildProcess } from "child_process"; +import { EventEmitter } from "events"; +import * as fs from "fs"; +import * as stream from "stream"; +import { Client, IEncodingOptions, IEncodingOptionsCallback, escapePath, useBuffer } from "@coder/protocol"; import { client } from "./client"; +// Use this to get around Webpack inserting our fills. +// TODO: is there a better way? +declare var _require: typeof require; +declare var _Buffer: typeof Buffer; + +/** + * Implements the native fs module + * Doesn't use `implements typeof import("fs")` to remove need for __promisify__ impls + * + * TODO: For now we can't use async in the evaluate calls because they get + * transpiled to TypeScript's helpers. tslib is included but we also need to set + * _this somehow which the __awaiter helper uses. + */ +class FS { + public constructor( + private readonly client: Client, + ) { } + + public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof mode === "function") { + callback = mode; + mode = undefined; + } + this.client.evaluate((path, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.access)(path, mode); + }, path, mode).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + // tslint:disable-next-line no-any + public appendFile = (file: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((path, data, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.appendFile)(path, data, options); + }, file, data, options).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.chmod)(path, mode); + }, path, mode).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path, uid, gid) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.chown)(path, uid, gid); + }, path, uid, gid).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.close)(fd); + }, fd).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof flags === "function") { + callback = flags; + } + this.client.evaluate((src, dest, flags) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.copyFile)(src, dest, flags); + }, src, dest, typeof flags !== "function" ? flags : undefined).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + /** + * This should NOT be used for long-term writes. + * The runnable will be killed after the timeout specified in evaluate.ts + */ + public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => { + const ae = this.client.run((ae, path, options) => { + const fs = _require("fs") as typeof import("fs"); + const str = fs.createWriteStream(path, options); + ae.on("write", (d, e) => str.write(_Buffer.from(d, "utf8"))); + ae.on("close", () => str.close()); + str.on("close", () => ae.emit("close")); + str.on("open", (fd) => ae.emit("open", fd)); + str.on("error", (err) => ae.emit(err)); + }, path, options); + + return new (class WriteStream extends stream.Writable implements fs.WriteStream { + + private _bytesWritten: number = 0; + + public constructor() { + super({ + write: (data, encoding, cb) => { + this._bytesWritten += data.length; + ae.emit("write", Buffer.from(data, encoding), encoding); + cb(); + }, + }); + + ae.on("open", (a) => this.emit("open", a)); + ae.on("close", () => this.emit("close")); + } + + public get bytesWritten(): number { + return this._bytesWritten; + } + + public get path(): string | Buffer { + return ""; + } + + public close(): void { + ae.emit("close"); + } + + }) as fs.WriteStream; + } + + public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => { + this.client.evaluate((path) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.exists)(path); + }, path).then((r) => { + callback(r); + }).catch(() => { + callback(false); + }); + } + + public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.fchmod)(fd, mode); + }, fd, mode).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd, uid, gid) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.fchown)(fd, uid, gid); + }, fd, uid, gid).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.fdatasync)(fd); + }, fd).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { + this.client.evaluate((fd) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + const tslib = _require("tslib") as typeof import("tslib"); + + return util.promisify(fs.fstat)(fd).then((stats) => { + return tslib.__assign(stats, { + _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, + _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, + _isDirectory: stats.isDirectory(), + _isFIFO: stats.isFIFO ? stats.isFIFO() : false, + _isFile: stats.isFile(), + _isSocket: stats.isSocket ? stats.isSocket() : false, + _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, + }); + }); + }, fd).then((stats) => { + callback(undefined!, new Stats(stats)); + }).catch((ex) => { + callback(ex, undefined!); + }); + } + + public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.fsync)(fd); + }, fd).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof len === "function") { + callback = len; + len = undefined; + } + this.client.evaluate((fd, len) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.ftruncate)(fd, len); + }, fd, len).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((fd, atime, mtime) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.futimes)(fd, atime, mtime); + }, fd, atime, mtime).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.lchmod)(path, mode); + }, path, mode).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path, uid, gid) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.lchown)(path, uid, gid); + }, path, uid, gid).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((existingPath, newPath) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.link)(existingPath, newPath); + }, existingPath, newPath).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { + this.client.evaluate((path) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + const tslib = _require("tslib") as typeof import("tslib"); + + return util.promisify(fs.lstat)(path).then((stats) => { + return tslib.__assign(stats, { + _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, + _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, + _isDirectory: stats.isDirectory(), + _isFIFO: stats.isFIFO ? stats.isFIFO() : false, + _isFile: stats.isFile(), + _isSocket: stats.isSocket ? stats.isSocket() : false, + _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, + }); + }); + }, path).then((stats) => { + callback(undefined!, new Stats(stats)); + }).catch((ex) => { + callback(ex, undefined!); + }); + } + + public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof mode === "function") { + callback = mode; + mode = undefined; + } + this.client.evaluate((path, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.mkdir)(path, mode); + }, path, mode).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((prefix, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.mkdtemp)(prefix, options); + }, prefix, options).then((folder) => { + callback!(undefined!, folder); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => { + if (typeof mode === "function") { + callback = mode; + mode = undefined; + } + this.client.evaluate((path, flags, mode) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.open)(path, flags, mode); + }, path, flags, mode).then((fd) => { + callback!(undefined!, fd); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public read = (fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => { + this.client.evaluate((fd, length, position) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + const buffer = new _Buffer(length); + + return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => { + return { + bytesRead: resp.bytesRead, + content: (resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer).toString("utf8"), + }; + }); + }, fd, length, position).then((resp) => { + const newBuf = Buffer.from(resp.content, "utf8"); + buffer.set(newBuf, offset); + callback(undefined!, resp.bytesRead, newBuf as TBuffer); + }).catch((ex) => { + callback(ex, undefined!, undefined!); + }); + } + + public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((path, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.readFile)(path, options).then((value) => value.toString()); + }, path, options).then((buffer) => { + callback!(undefined!, buffer); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + // TODO: options can also take `withFileTypes` but the types aren't working. + this.client.evaluate((path, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.readdir)(path, options); + }, path, options).then((files) => { + callback!(undefined!, files); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((path, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.readlink)(path, options); + }, path, options).then((linkString) => { + callback!(undefined!, linkString); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((path, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.realpath)(path, options); + }, path, options).then((resolvedPath) => { + callback!(undefined!, resolvedPath); + }).catch((ex) => { + callback!(ex, undefined!); + }); + } + + public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((oldPath, newPath) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.rename)(oldPath, newPath); + }, oldPath, newPath).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.rmdir)(path); + }, path).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { + this.client.evaluate((path) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + const tslib = _require("tslib") as typeof import("tslib"); + + return util.promisify(fs.stat)(path).then((stats) => { + return tslib.__assign(stats, { + /** + * We need to check if functions exist because nexe's implemented FS + * lib doesnt implement fs.stats properly + */ + _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, + _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, + _isDirectory: stats.isDirectory(), + _isFIFO: stats.isFIFO ? stats.isFIFO() : false, + _isFile: stats.isFile(), + _isSocket: stats.isSocket ? stats.isSocket() : false, + _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, + }); + }); + }, path).then((stats) => { + callback(undefined!, new Stats(stats)); + }).catch((ex) => { + callback(ex, undefined!); + }); + } + + public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof type === "function") { + callback = type; + type = undefined; + } + this.client.evaluate((target, path, type) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.symlink)(target, path, type); + }, target, path, type).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof len === "function") { + callback = len; + len = undefined; + } + this.client.evaluate((path, len) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.truncate)(path, len); + }, path, len).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.unlink)(path); + }, path).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { + this.client.evaluate((path, atime, mtime) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.utimes)(path, atime, mtime); + }, path, atime, mtime).then(() => { + callback(undefined!); + }).catch((ex) => { + callback(ex); + }); + } + + public write = (fd: number, buffer: TBuffer, offset: number | undefined, length: number | undefined, position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => { + if (typeof position === "function") { + callback = position; + position = undefined; + } + this.client.evaluate((fd, buffer, offset, length, position) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.write)(fd, _Buffer.from(buffer, "utf8"), offset, length, position).then((resp) => { + return { + bytesWritten: resp.bytesWritten, + content: resp.buffer.toString("utf8"), + }; + }); + }, fd, buffer.toString(), offset, length, position).then((r) => { + callback!(undefined!, r.bytesWritten, Buffer.from(r.content, "utf8") as TBuffer); + }).catch((ex) => { + callback!(ex, undefined!, undefined!); + }); + } + + // tslint:disable-next-line no-any + public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => { + if (typeof options === "function") { + callback = options; + options = undefined; + } + this.client.evaluate((path, data, options) => { + const fs = _require("fs") as typeof import("fs"); + const util = _require("util") as typeof import("util"); + + return util.promisify(fs.writeFile)(path, data, options); + }, path, data, options).then(() => { + callback!(undefined!); + }).catch((ex) => { + callback!(ex); + }); + } + + public watch = (filename: fs.PathLike, options: IEncodingOptions, listener?: ((event: string, filename: string) => void) | ((event: string, filename: Buffer) => void)): fs.FSWatcher => { + // TODO: can we modify `evaluate` for long-running processes like watch? + // Especially since inotifywait might not be available. + const buffer = new NewlineInputBuffer((msg): void => { + msg = msg.trim(); + const index = msg.lastIndexOf(":"); + const events = msg.substring(index + 1).split(","); + const baseFilename = msg.substring(0, index).split("/").pop(); + events.forEach((event) => { + switch (event) { + // Rename is emitted when a file appears or disappears in the directory. + case "CREATE": + case "DELETE": + case "MOVED_FROM": + case "MOVED_TO": + watcher.emit("rename", baseFilename); + break; + case "CLOSE_WRITE": + watcher.emit("change", baseFilename); + break; + } + }); + }); + + // TODO: `exec` is undefined for some reason. + const process = exec(`inotifywait ${escapePath(filename.toString())} -m --format "%w%f:%e"`); + process.on("exit", (exitCode) => { + watcher.emit("error", new Error(`process terminated unexpectedly with code ${exitCode}`)); + }); + process.stdout.on("data", (data) => { + buffer.push(data); + }); + + const watcher = new Watcher(process); + if (listener) { + const l = listener; + watcher.on("change", (filename) => { + // @ts-ignore not sure how to make this work. + l("change", useBuffer(options) ? Buffer.from(filename) : filename); + }); + watcher.on("rename", (filename) => { + // @ts-ignore not sure how to make this work. + l("rename", useBuffer(options) ? Buffer.from(filename) : filename); + }); + } + + return watcher; + } +} + +class Watcher extends EventEmitter implements fs.FSWatcher { + public constructor(private readonly process: ChildProcess) { + super(); + } + + public close(): void { + this.process.kill(); + } +} + +interface IStats { + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + atime: Date | string; + mtime: Date | string; + ctime: Date | string; + birthtime: Date | string; + _isFile: boolean; + _isDirectory: boolean; + _isBlockDevice: boolean; + _isCharacterDevice: boolean; + _isSymbolicLink: boolean; + _isFIFO: boolean; + _isSocket: boolean; +} + +class Stats implements fs.Stats { + public readonly atime: Date; + public readonly mtime: Date; + public readonly ctime: Date; + public readonly birthtime: Date; + + public constructor(private readonly stats: IStats) { + this.atime = new Date(stats.atime); + this.mtime = new Date(stats.mtime); + this.ctime = new Date(stats.ctime); + this.birthtime = new Date(stats.birthtime); + } + + public get dev(): number { return this.stats.dev; } + public get ino(): number { return this.stats.ino; } + public get mode(): number { return this.stats.mode; } + public get nlink(): number { return this.stats.nlink; } + public get uid(): number { return this.stats.uid; } + public get gid(): number { return this.stats.gid; } + public get rdev(): number { return this.stats.rdev; } + public get size(): number { return this.stats.size; } + public get blksize(): number { return this.stats.blksize; } + public get blocks(): number { return this.stats.blocks; } + public get atimeMs(): number { return this.stats.atimeMs; } + public get mtimeMs(): number { return this.stats.mtimeMs; } + public get ctimeMs(): number { return this.stats.ctimeMs; } + public get birthtimeMs(): number { return this.stats.birthtimeMs; } + public isFile(): boolean { return this.stats._isFile; } + public isDirectory(): boolean { return this.stats._isDirectory; } + public isBlockDevice(): boolean { return this.stats._isBlockDevice; } + public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; } + public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; } + public isFIFO(): boolean { return this.stats._isFIFO; } + public isSocket(): boolean { return this.stats._isSocket; } + + public toObject(): object { + return JSON.parse(JSON.stringify(this)); + } +} + +/** + * Class for safely taking input and turning it into separate messages. + * Assumes that messages are split by newlines. + */ +class NewlineInputBuffer { + private callback: (msg: string) => void; + private buffer: string | undefined; + + public constructor(callback: (msg: string) => void) { + this.callback = callback; + } + + /** + * Add data to be buffered. + */ + public push(data: string | Uint8Array): void { + let input = typeof data === "string" ? data : data.toString(); + if (this.buffer) { + input = this.buffer + input; + this.buffer = undefined; + } + const lines = input.split("\n"); + const length = lines.length - 1; + const lastLine = lines[length]; + if (lastLine.length > 0) { + this.buffer = lastLine; + } + lines.pop(); // This is either the line we buffered or an empty string. + for (let i = 0; i < length; ++i) { + this.callback(lines[i]); + } + } +} + export = new FS(client); diff --git a/packages/ide/src/fill/net.ts b/packages/ide/src/fill/net.ts index 89affd7e4..26b267dae 100644 --- a/packages/ide/src/fill/net.ts +++ b/packages/ide/src/fill/net.ts @@ -1,4 +1,56 @@ -import { Net } from "@coder/protocol"; +import * as net from "net"; +import { Client } from "@coder/protocol"; import { client } from "./client"; +type NodeNet = typeof net; + +/** + * Implementation of net for the browser. + */ +class Net implements NodeNet { + public constructor( + private readonly client: Client, + ) {} + + public get Socket(): typeof net.Socket { + // @ts-ignore + return this.client.Socket; + } + + public get Server(): typeof net.Server { + throw new Error("not implemented"); + } + + public connect(): net.Socket { + throw new Error("not implemented"); + } + + public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket { + if (typeof target === "object") { + throw new Error("not implemented"); + } + + return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket; + } + + public isIP(_input: string): number { + throw new Error("not implemented"); + } + + public isIPv4(_input: string): boolean { + throw new Error("not implemented"); + } + + public isIPv6(_input: string): boolean { + throw new Error("not implemented"); + } + + public createServer( + _options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void), + _connectionListener?: (socket: net.Socket) => void, + ): net.Server { + return this.client.createServer() as net.Server; + } +} + export = new Net(client); diff --git a/packages/protocol/src/browser/modules/child_process.ts b/packages/protocol/src/browser/modules/child_process.ts deleted file mode 100644 index 10d9e85c3..000000000 --- a/packages/protocol/src/browser/modules/child_process.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as cp from "child_process"; -import { Client } from "../client"; -import { useBuffer } from "../../common/util"; - -export class CP { - public constructor( - private readonly client: Client, - ) { } - - public exec = ( - command: string, - options?: { encoding?: BufferEncoding | string | "buffer" | null } & cp.ExecOptions | null | ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void), - callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void), - ): cp.ChildProcess => { - // TODO: Probably should add an `exec` instead of using `spawn`, especially - // since bash might not be available. - const childProcess = this.client.spawn("bash", ["-c", command.replace(/"/g, "\\\"")]); - - let stdout = ""; - childProcess.stdout.on("data", (data) => { - stdout += data.toString(); - }); - - let stderr = ""; - childProcess.stderr.on("data", (data) => { - stderr += data.toString(); - }); - - childProcess.on("exit", (exitCode) => { - const error = exitCode !== 0 ? new Error(stderr) : null; - if (typeof options === "function") { - callback = options; - } - if (callback) { - // @ts-ignore not sure how to make this work. - callback( - error, - useBuffer(options) ? Buffer.from(stdout) : stdout, - useBuffer(options) ? Buffer.from(stderr) : stderr, - ); - } - }); - - // @ts-ignore TODO: not fully implemented - return childProcess; - } - - public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => { - if (options && options.env && options.env.AMD_ENTRYPOINT) { - // @ts-ignore TODO: not fully implemented - return this.client.bootstrapFork( - options.env.AMD_ENTRYPOINT, - Array.isArray(args) ? args : [], - // @ts-ignore TODO: env is a different type - Array.isArray(args) || !args ? options : args, - ); - } - - // @ts-ignore TODO: not fully implemented - return this.client.fork( - modulePath, - Array.isArray(args) ? args : [], - // @ts-ignore TODO: env is a different type - Array.isArray(args) || !args ? options : args, - ); - } - - public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => { - // @ts-ignore TODO: not fully implemented - return this.client.spawn( - command, - Array.isArray(args) ? args : [], - // @ts-ignore TODO: env is a different type - Array.isArray(args) || !args ? options : args, - ); - } -} diff --git a/packages/protocol/src/browser/modules/fs.ts b/packages/protocol/src/browser/modules/fs.ts deleted file mode 100644 index 8bd6e0d72..000000000 --- a/packages/protocol/src/browser/modules/fs.ts +++ /dev/null @@ -1,800 +0,0 @@ -import { exec, ChildProcess } from "child_process"; -import { EventEmitter } from "events"; -import * as fs from "fs"; -import * as stream from "stream"; -import { IEncodingOptions, IEncodingOptionsCallback, escapePath, useBuffer } from "../../common/util"; -import { Client } from "../client"; - -// Use this to get around Webpack inserting our fills. -// TODO: is there a better way? -declare var _require: typeof require; -declare var _Buffer: typeof Buffer; - -/** - * Implements the native fs module - * Doesn't use `implements typeof import("fs")` to remove need for __promisify__ impls - * - * TODO: For now we can't use async in the evaluate calls because they get - * transpiled to TypeScript's helpers. tslib is included but we also need to set - * _this somehow which the __awaiter helper uses. - */ -export class FS { - public constructor( - private readonly client: Client, - ) { } - - public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof mode === "function") { - callback = mode; - mode = undefined; - } - this.client.evaluate((path, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.access)(path, mode); - }, path, mode).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - // tslint:disable-next-line no-any - public appendFile = (file: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((path, data, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.appendFile)(path, data, options); - }, file, data, options).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.chmod)(path, mode); - }, path, mode).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, uid, gid) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.chown)(path, uid, gid); - }, path, uid, gid).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.close)(fd); - }, fd).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof flags === "function") { - callback = flags; - } - this.client.evaluate((src, dest, flags) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.copyFile)(src, dest, flags); - }, src, dest, typeof flags !== "function" ? flags : undefined).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - /** - * This should NOT be used for long-term writes. - * The runnable will be killed after the timeout specified in evaluate.ts - */ - public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => { - const ae = this.client.run((ae, path, options) => { - const fs = _require("fs") as typeof import("fs"); - const str = fs.createWriteStream(path, options); - ae.on("write", (d, e) => str.write(_Buffer.from(d, "utf8"))); - ae.on("close", () => str.close()); - str.on("close", () => ae.emit("close")); - str.on("open", (fd) => ae.emit("open", fd)); - str.on("error", (err) => ae.emit(err)); - }, path, options); - - return new (class WriteStream extends stream.Writable implements fs.WriteStream { - - private _bytesWritten: number = 0; - - public constructor() { - super({ - write: (data, encoding, cb) => { - this._bytesWritten += data.length; - ae.emit("write", Buffer.from(data, encoding), encoding); - cb(); - }, - }); - - ae.on("open", (a) => this.emit("open", a)); - ae.on("close", () => this.emit("close")); - } - - public get bytesWritten(): number { - return this._bytesWritten; - } - - public get path(): string | Buffer { - return ""; - } - - public close(): void { - ae.emit("close"); - } - - }) as fs.WriteStream; - } - - public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => { - this.client.evaluate((path) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.exists)(path); - }, path).then((r) => { - callback(r); - }).catch(() => { - callback(false); - }); - } - - public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.fchmod)(fd, mode); - }, fd, mode).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, uid, gid) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.fchown)(fd, uid, gid); - }, fd, uid, gid).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.fdatasync)(fd); - }, fd).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((fd) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - const tslib = _require("tslib") as typeof import("tslib"); - - return util.promisify(fs.fstat)(fd).then((stats) => { - return tslib.__assign(stats, { - _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, - _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, - _isDirectory: stats.isDirectory(), - _isFIFO: stats.isFIFO ? stats.isFIFO() : false, - _isFile: stats.isFile(), - _isSocket: stats.isSocket ? stats.isSocket() : false, - _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, - }); - }); - }, fd).then((stats) => { - callback(undefined!, new Stats(stats)); - }).catch((ex) => { - callback(ex, undefined!); - }); - } - - public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.fsync)(fd); - }, fd).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof len === "function") { - callback = len; - len = undefined; - } - this.client.evaluate((fd, len) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.ftruncate)(fd, len); - }, fd, len).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, atime, mtime) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.futimes)(fd, atime, mtime); - }, fd, atime, mtime).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.lchmod)(path, mode); - }, path, mode).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, uid, gid) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.lchown)(path, uid, gid); - }, path, uid, gid).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((existingPath, newPath) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.link)(existingPath, newPath); - }, existingPath, newPath).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((path) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - const tslib = _require("tslib") as typeof import("tslib"); - - return util.promisify(fs.lstat)(path).then((stats) => { - return tslib.__assign(stats, { - _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, - _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, - _isDirectory: stats.isDirectory(), - _isFIFO: stats.isFIFO ? stats.isFIFO() : false, - _isFile: stats.isFile(), - _isSocket: stats.isSocket ? stats.isSocket() : false, - _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, - }); - }); - }, path).then((stats) => { - callback(undefined!, new Stats(stats)); - }).catch((ex) => { - callback(ex, undefined!); - }); - } - - public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof mode === "function") { - callback = mode; - mode = undefined; - } - this.client.evaluate((path, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.mkdir)(path, mode); - }, path, mode).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((prefix, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.mkdtemp)(prefix, options); - }, prefix, options).then((folder) => { - callback!(undefined!, folder); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => { - if (typeof mode === "function") { - callback = mode; - mode = undefined; - } - this.client.evaluate((path, flags, mode) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.open)(path, flags, mode); - }, path, flags, mode).then((fd) => { - callback!(undefined!, fd); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public read = (fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => { - this.client.evaluate((fd, length, position) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - const buffer = new _Buffer(length); - - return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => { - return { - bytesRead: resp.bytesRead, - content: (resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer).toString("utf8"), - }; - }); - }, fd, length, position).then((resp) => { - const newBuf = Buffer.from(resp.content, "utf8"); - buffer.set(newBuf, offset); - callback(undefined!, resp.bytesRead, newBuf as TBuffer); - }).catch((ex) => { - callback(ex, undefined!, undefined!); - }); - } - - public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((path, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.readFile)(path, options).then((value) => value.toString()); - }, path, options).then((buffer) => { - callback!(undefined!, buffer); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - // TODO: options can also take `withFileTypes` but the types aren't working. - this.client.evaluate((path, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.readdir)(path, options); - }, path, options).then((files) => { - callback!(undefined!, files); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((path, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.readlink)(path, options); - }, path, options).then((linkString) => { - callback!(undefined!, linkString); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((path, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.realpath)(path, options); - }, path, options).then((resolvedPath) => { - callback!(undefined!, resolvedPath); - }).catch((ex) => { - callback!(ex, undefined!); - }); - } - - public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((oldPath, newPath) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.rename)(oldPath, newPath); - }, oldPath, newPath).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.rmdir)(path); - }, path).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((path) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - const tslib = _require("tslib") as typeof import("tslib"); - - return util.promisify(fs.stat)(path).then((stats) => { - return tslib.__assign(stats, { - /** - * We need to check if functions exist because nexe's implemented FS - * lib doesnt implement fs.stats properly - */ - _isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false, - _isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false, - _isDirectory: stats.isDirectory(), - _isFIFO: stats.isFIFO ? stats.isFIFO() : false, - _isFile: stats.isFile(), - _isSocket: stats.isSocket ? stats.isSocket() : false, - _isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false, - }); - }); - }, path).then((stats) => { - callback(undefined!, new Stats(stats)); - }).catch((ex) => { - callback(ex, undefined!); - }); - } - - public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof type === "function") { - callback = type; - type = undefined; - } - this.client.evaluate((target, path, type) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.symlink)(target, path, type); - }, target, path, type).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof len === "function") { - callback = len; - len = undefined; - } - this.client.evaluate((path, len) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.truncate)(path, len); - }, path, len).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.unlink)(path); - }, path).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, atime, mtime) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.utimes)(path, atime, mtime); - }, path, atime, mtime).then(() => { - callback(undefined!); - }).catch((ex) => { - callback(ex); - }); - } - - public write = (fd: number, buffer: TBuffer, offset: number | undefined, length: number | undefined, position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => { - if (typeof position === "function") { - callback = position; - position = undefined; - } - this.client.evaluate((fd, buffer, offset, length, position) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.write)(fd, _Buffer.from(buffer, "utf8"), offset, length, position).then((resp) => { - return { - bytesWritten: resp.bytesWritten, - content: resp.buffer.toString("utf8"), - }; - }); - }, fd, buffer.toString(), offset, length, position).then((r) => { - callback!(undefined!, r.bytesWritten, Buffer.from(r.content, "utf8") as TBuffer); - }).catch((ex) => { - callback!(ex, undefined!, undefined!); - }); - } - - // tslint:disable-next-line no-any - public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => { - if (typeof options === "function") { - callback = options; - options = undefined; - } - this.client.evaluate((path, data, options) => { - const fs = _require("fs") as typeof import("fs"); - const util = _require("util") as typeof import("util"); - - return util.promisify(fs.writeFile)(path, data, options); - }, path, data, options).then(() => { - callback!(undefined!); - }).catch((ex) => { - callback!(ex); - }); - } - - public watch = (filename: fs.PathLike, options: IEncodingOptions, listener?: ((event: string, filename: string) => void) | ((event: string, filename: Buffer) => void)): fs.FSWatcher => { - // TODO: can we modify `evaluate` for long-running processes like watch? - // Especially since inotifywait might not be available. - const buffer = new NewlineInputBuffer((msg): void => { - msg = msg.trim(); - const index = msg.lastIndexOf(":"); - const events = msg.substring(index + 1).split(","); - const baseFilename = msg.substring(0, index).split("/").pop(); - events.forEach((event) => { - switch (event) { - // Rename is emitted when a file appears or disappears in the directory. - case "CREATE": - case "DELETE": - case "MOVED_FROM": - case "MOVED_TO": - watcher.emit("rename", baseFilename); - break; - case "CLOSE_WRITE": - watcher.emit("change", baseFilename); - break; - } - }); - }); - - // TODO: `exec` is undefined for some reason. - const process = exec(`inotifywait ${escapePath(filename.toString())} -m --format "%w%f:%e"`); - process.on("exit", (exitCode) => { - watcher.emit("error", new Error(`process terminated unexpectedly with code ${exitCode}`)); - }); - process.stdout.on("data", (data) => { - buffer.push(data); - }); - - const watcher = new Watcher(process); - if (listener) { - const l = listener; - watcher.on("change", (filename) => { - // @ts-ignore not sure how to make this work. - l("change", useBuffer(options) ? Buffer.from(filename) : filename); - }); - watcher.on("rename", (filename) => { - // @ts-ignore not sure how to make this work. - l("rename", useBuffer(options) ? Buffer.from(filename) : filename); - }); - } - - return watcher; - } -} - -class Watcher extends EventEmitter implements fs.FSWatcher { - public constructor(private readonly process: ChildProcess) { - super(); - } - - public close(): void { - this.process.kill(); - } -} - -interface IStats { - dev: number; - ino: number; - mode: number; - nlink: number; - uid: number; - gid: number; - rdev: number; - size: number; - blksize: number; - blocks: number; - atimeMs: number; - mtimeMs: number; - ctimeMs: number; - birthtimeMs: number; - atime: Date | string; - mtime: Date | string; - ctime: Date | string; - birthtime: Date | string; - _isFile: boolean; - _isDirectory: boolean; - _isBlockDevice: boolean; - _isCharacterDevice: boolean; - _isSymbolicLink: boolean; - _isFIFO: boolean; - _isSocket: boolean; -} - -class Stats implements fs.Stats { - public readonly atime: Date; - public readonly mtime: Date; - public readonly ctime: Date; - public readonly birthtime: Date; - - public constructor(private readonly stats: IStats) { - this.atime = new Date(stats.atime); - this.mtime = new Date(stats.mtime); - this.ctime = new Date(stats.ctime); - this.birthtime = new Date(stats.birthtime); - } - - public get dev(): number { return this.stats.dev; } - public get ino(): number { return this.stats.ino; } - public get mode(): number { return this.stats.mode; } - public get nlink(): number { return this.stats.nlink; } - public get uid(): number { return this.stats.uid; } - public get gid(): number { return this.stats.gid; } - public get rdev(): number { return this.stats.rdev; } - public get size(): number { return this.stats.size; } - public get blksize(): number { return this.stats.blksize; } - public get blocks(): number { return this.stats.blocks; } - public get atimeMs(): number { return this.stats.atimeMs; } - public get mtimeMs(): number { return this.stats.mtimeMs; } - public get ctimeMs(): number { return this.stats.ctimeMs; } - public get birthtimeMs(): number { return this.stats.birthtimeMs; } - public isFile(): boolean { return this.stats._isFile; } - public isDirectory(): boolean { return this.stats._isDirectory; } - public isBlockDevice(): boolean { return this.stats._isBlockDevice; } - public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; } - public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; } - public isFIFO(): boolean { return this.stats._isFIFO; } - public isSocket(): boolean { return this.stats._isSocket; } - - public toObject(): object { - return JSON.parse(JSON.stringify(this)); - } -} - -/** - * Class for safely taking input and turning it into separate messages. - * Assumes that messages are split by newlines. - */ -export class NewlineInputBuffer { - private callback: (msg: string) => void; - private buffer: string | undefined; - - public constructor(callback: (msg: string) => void) { - this.callback = callback; - } - - /** - * Add data to be buffered. - */ - public push(data: string | Uint8Array): void { - let input = typeof data === "string" ? data : data.toString(); - if (this.buffer) { - input = this.buffer + input; - this.buffer = undefined; - } - const lines = input.split("\n"); - const length = lines.length - 1; - const lastLine = lines[length]; - if (lastLine.length > 0) { - this.buffer = lastLine; - } - lines.pop(); // This is either the line we buffered or an empty string. - for (let i = 0; i < length; ++i) { - this.callback(lines[i]); - } - } -} diff --git a/packages/protocol/src/browser/modules/net.ts b/packages/protocol/src/browser/modules/net.ts deleted file mode 100644 index 0748e27c5..000000000 --- a/packages/protocol/src/browser/modules/net.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as net from "net"; -import { Client } from "../client"; - -type NodeNet = typeof net; - -/** - * Implementation of net for the browser. - */ -export class Net implements NodeNet { - public constructor( - private readonly client: Client, - ) {} - - public get Socket(): typeof net.Socket { - // @ts-ignore - return this.client.Socket; - } - - public get Server(): typeof net.Server { - throw new Error("not implemented"); - } - - public connect(): net.Socket { - throw new Error("not implemented"); - } - - public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket { - if (typeof target === "object") { - throw new Error("not implemented"); - } - - return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket; - } - - public isIP(_input: string): number { - throw new Error("not implemented"); - } - - public isIPv4(_input: string): boolean { - throw new Error("not implemented"); - } - - public isIPv6(_input: string): boolean { - throw new Error("not implemented"); - } - - public createServer( - _options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void), - _connectionListener?: (socket: net.Socket) => void, - ): net.Server { - return this.client.createServer() as net.Server; - } -} diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index af625b475..28d6edd65 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -1,6 +1,3 @@ export * from "./browser/client"; -export * from "./browser/modules/child_process"; -export * from "./browser/modules/fs"; -export * from "./browser/modules/net"; export * from "./common/connection"; export * from "./common/util";