diff --git a/src/commands/builtin_command_runner.ts b/src/commands/builtin_command_runner.ts index 4ec8acf..5ca69b6 100644 --- a/src/commands/builtin_command_runner.ts +++ b/src/commands/builtin_command_runner.ts @@ -21,8 +21,10 @@ export class BuiltinCommandRunner implements ICommandRunner { return; } const path = args[0] // Ignore other arguments? - // Need to handle path of "-". Maybe previous path is in an env var? - context.fileSystem.FS.chdir(path) - // Need to set PWD env var? + // Need to handle path of "-". Maybe previous path is in an env var? "OLDPWD" + + const { FS } = context.fileSystem + FS.chdir(path) + context.environment.set("PWD", FS.cwd()) } } diff --git a/src/commands/wasm_command_runner.ts b/src/commands/wasm_command_runner.ts index 0111cb1..7c180d1 100644 --- a/src/commands/wasm_command_runner.ts +++ b/src/commands/wasm_command_runner.ts @@ -17,20 +17,28 @@ export abstract class WasmCommandRunner implements ICommandRunner { noInitialRun: true, print: (text: string) => stdout.write(`${text}\n`), printErr: (text: string) => stdout.write(`${text}\n`), // Should be stderr + preRun: (module: any) => { + // 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()) + + // Copy environment variables into command. + context.environment.copyIntoCommand(module.ENV) + }, }) - - // Need to use PROXYFS so that command sees the shared FS. - const FS = wasm.FS - FS.mkdir(mountpoint, 0o777) - FS.mount(fileSystem.PROXYFS, { root: mountpoint, fs: fileSystem.FS }, mountpoint) - FS.chdir(fileSystem.FS.cwd()) const loaded = Date.now() wasm.callMain(args) + const FS = wasm.FS FS.close(FS.streams[1]) FS.close(FS.streams[2]) + // Copy environment variables back from command. + context.environment.copyFromCommand(wasm.getEnvStrings()) + const end = Date.now() console.log(`${cmdName} load time ${loaded-start} ms, run time ${end-loaded} ms`) } diff --git a/src/context.ts b/src/context.ts index 79397fb..8048357 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,3 +1,4 @@ +import { Environment } from "./environment" import { IFileSystem } from "./file_system" import { Output } from "./io" @@ -9,10 +10,15 @@ export class Context { readonly args: string[], readonly fileSystem: IFileSystem, readonly mountpoint: string, + environment: Environment, readonly stdout: Output, - ) {} + ) { + this.environment = environment + } async flush(): Promise { await this.stdout.flush() } + + environment: Environment } diff --git a/src/environment.ts b/src/environment.ts new file mode 100644 index 0000000..7cce710 --- /dev/null +++ b/src/environment.ts @@ -0,0 +1,48 @@ +/** + * Collection of environment variables that are known to a shell and are passed in and out of + * commands. + */ +export class Environment { + constructor() { + this._env.set("PS1", "\x1b[1;31mjs-shell:$\x1b[1;0m ") // red color + } + + /** + * Copy environment variables back from a command after it has run. + */ + copyFromCommand(source: string[]) { + for (const str of source) { + const split = str.split("=") + const key = split.shift() + if (key && !this._ignore.has(key)) { + this._env.set(key, split.join("=")) + } + } + } + + /** + * Copy environment variables into a command before it is run. + */ + copyIntoCommand(target: { [key: string]: string }) { + for (const [key, value] of this._env.entries()) { + target[key] = value + } + } + + get(key: string): string | null { + return this._env.get(key) ?? null + } + + getPrompt(): string { + return this._env.get("PS1") ?? "$ " + } + + set(key: string, value: string) { + this._env.set(key, value) + } + + private _env: Map = new Map() + + // Keys to ignore when copying back from a command's env vars. + private _ignore: Set = new Set(["USER", "LOGNAME", "HOME", "LANG", "_"]) +} diff --git a/src/shell.ts b/src/shell.ts index 8ff4d2f..05b5148 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -1,5 +1,6 @@ import { CommandRegistry } from "./command_registry" import { Context } from "./context" +import { Environment } from "./environment" import { IFileSystem } from "./file_system" import { FileOutput, Output, TerminalOutput } from "./io" import { IOutputCallback } from "./output_callback" @@ -14,7 +15,11 @@ export class Shell { this._outputCallback = outputCallback this._mountpoint = mountpoint; this._currentLine = "" - this._prompt = "\x1b[1;31mjs-shell:$\x1b[1;0m " // red color + this._environment = new Environment() + } + + get environment(): Environment { + return this._environment } async input(char: string): Promise { @@ -26,7 +31,7 @@ export class Shell { const cmdText = this._currentLine.trimStart() this._currentLine = "" await this._runCommands(cmdText) - await this.output(this._prompt) + await this.output(this._environment.getPrompt()) } else if (code == 127) { // Backspace if (this._currentLine.length > 0) { this._currentLine = this._currentLine.slice(0, -1) @@ -48,7 +53,7 @@ export class Shell { } else if (possibles.length > 1) { const line = possibles.join(" ") // Note keep leading whitespace on current line. - await this.output(`\r\n${line}\r\n${this._prompt}${this._currentLine}`) + await this.output(`\r\n${line}\r\n${this._environment.getPrompt()}${this._currentLine}`) } } else if (code == 27) { // Escape following by 1+ more characters const remainder = char.slice(1) @@ -68,6 +73,7 @@ export class Shell { const { FS, PATH, ERRNO_CODES, PROXYFS } = this._fsModule; FS.mkdir(this._mountpoint, 0o777) FS.chdir(this._mountpoint) + this._environment.set("PWD", FS.cwd()) this._fileSystem = { FS, PATH, ERRNO_CODES, PROXYFS } return this._fileSystem } @@ -88,7 +94,7 @@ export class Shell { } async start(): Promise { - await this.output(this._prompt) + await this.output(this._environment.getPrompt()) } // Keeping this public for tests. @@ -125,7 +131,9 @@ export class Shell { } const args = command.suffix.map((token) => token.value) - const context = new Context(args, this._fileSystem!, this._mountpoint, output) + const context = new Context( + args, this._fileSystem!, this._mountpoint, this._environment, output, + ) await runner.run(cmdName, context) await context.flush() @@ -144,7 +152,7 @@ export class Shell { private readonly _outputCallback: IOutputCallback private _currentLine: string - private _prompt: string // Should really obtain this from env + private _environment: Environment private _fsModule: any private _fileSystem?: IFileSystem diff --git a/src/wasm/grep.js b/src/wasm/grep.js index da4affc..58c0adf 100644 --- a/src/wasm/grep.js +++ b/src/wasm/grep.js @@ -1,7 +1,7 @@ var Module = (() => { var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; - + return ( function(moduleArg = {}) { @@ -450,7 +450,7 @@ function initRuntime() { checkStackCookie(); - + if (!Module["noFSInit"] && !FS.init.initialized) FS.init(); FS.ignorePermissions = false; @@ -461,7 +461,7 @@ TTY.init(); function preMain() { checkStackCookie(); - + callRuntimeCallbacks(__ATMAIN__); } @@ -720,10 +720,10 @@ function createWasm() { var exports = instance.exports; wasmExports = exports; - + wasmMemory = wasmExports['memory']; - + assert(wasmMemory, "memory not found in wasm exports"); // This assertion doesn't hold when emscripten is run in --post-link // mode. @@ -732,7 +732,7 @@ function createWasm() { updateMemoryViews(); wasmTable = wasmExports['__indirect_function_table']; - + assert(wasmTable, "table not found in wasm exports"); addOnInit(wasmExports['__wasm_call_ctors']); @@ -901,7 +901,7 @@ function dbg(text) { } }; - + /** * @param {number} ptr * @param {string} type @@ -928,7 +928,7 @@ function dbg(text) { return '0x' + ptr.toString(16).padStart(8, '0'); }; - + /** * @param {number} ptr * @param {number} value @@ -958,7 +958,7 @@ function dbg(text) { }; var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder('utf8') : undefined; - + /** * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given * array that contains uint8 values, returns a copy of that string as a @@ -977,7 +977,7 @@ function dbg(text) { // (As a tiny code save trick, compare endPtr against endIdx using a negation, // so that undefined means Infinity) while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr; - + if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) { return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr)); } @@ -1000,7 +1000,7 @@ function dbg(text) { if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!'); u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63); } - + if (u0 < 0x10000) { str += String.fromCharCode(u0); } else { @@ -1010,7 +1010,7 @@ function dbg(text) { } return str; }; - + /** * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the * emscripten HEAP, returns a copy of that string as a Javascript String object. @@ -1107,7 +1107,7 @@ function dbg(text) { return PATH.normalize(l + '/' + r); }, }; - + var initRandomFill = () => { if (typeof crypto == 'object' && typeof crypto['getRandomValues'] == 'function') { // for modern web browsers @@ -1120,9 +1120,9 @@ function dbg(text) { // Lazily init on the first invocation. return (randomFill = initRandomFill())(view); }; - - - + + + var PATH_FS = { resolve:function() { var resolvedPath = '', @@ -1176,11 +1176,11 @@ function dbg(text) { return outputParts.join('/'); }, }; - - - + + + var FS_stdin_getChar_buffer = []; - + var lengthBytesUTF8 = (str) => { var len = 0; for (var i = 0; i < str.length; ++i) { @@ -1201,14 +1201,14 @@ function dbg(text) { } return len; }; - + var stringToUTF8Array = (str, heap, outIdx, maxBytesToWrite) => { assert(typeof str === 'string'); // Parameter maxBytesToWrite is not optional. Negative values, 0, null, // undefined and false each don't write out any bytes. if (!(maxBytesToWrite > 0)) return 0; - + var startIdx = outIdx; var endIdx = outIdx + maxBytesToWrite - 1; // -1 for string null terminator. for (var i = 0; i < str.length; ++i) { @@ -1422,13 +1422,13 @@ function dbg(text) { }, }, }; - - + + var zeroMemory = (address, size) => { HEAPU8.fill(0, address, address + size); return address; }; - + var alignMemory = (size, alignment) => { assert(alignment, "alignment argument is required"); return Math.ceil(size / alignment) * alignment; @@ -1507,7 +1507,7 @@ function dbg(text) { // When the byte data of the file is populated, this will point to either a typed array, or a normal JS array. Typed arrays are preferred // for performance, and used by default. However, typed arrays are not resizable like normal JS arrays are, so there is a small disk size // penalty involved for appending file writes that continuously grow a file similar to std::vector capacity vs used -scheme. - node.contents = null; + node.contents = null; } else if (FS.isLink(node.mode)) { node.node_ops = MEMFS.ops_table.link.node; node.stream_ops = MEMFS.ops_table.link.stream; @@ -1680,11 +1680,11 @@ function dbg(text) { if (buffer.buffer === HEAP8.buffer) { canOwn = false; } - + if (!length) return 0; var node = stream.node; node.timestamp = Date.now(); - + if (buffer.subarray && (!node.contents || node.contents.subarray)) { // This write is from a typed array to a typed array? if (canOwn) { assert(position === 0, 'canOwn must imply no weird position inside the file'); @@ -1700,7 +1700,7 @@ function dbg(text) { return length; } } - + // Appending to an existing file and we need to reallocate, or source data did not come as a typed array. MEMFS.expandFileStorage(node, position+length); if (node.contents.subarray && buffer.subarray) { @@ -1770,7 +1770,7 @@ function dbg(text) { }, }, }; - + /** @param {boolean=} noRunDep */ var asyncLoad = (url, onload, onerror, noRunDep) => { var dep = !noRunDep ? getUniqueRunDependency(`al ${url}`) : ''; @@ -1787,13 +1787,13 @@ function dbg(text) { }); if (dep) addRunDependency(dep); }; - - + + var preloadPlugins = Module['preloadPlugins'] || []; var FS_handledByPreloadPlugin = (byteArray, fullname, finish, onerror) => { // Ensure plugins are ready. if (typeof Browser != 'undefined') Browser.init(); - + var handled = false; preloadPlugins.forEach((plugin) => { if (handled) return; @@ -1833,7 +1833,7 @@ function dbg(text) { processData(url); } }; - + var FS_modeStringToFlags = (str) => { var flagModes = { 'r': 0, @@ -1849,17 +1849,17 @@ function dbg(text) { } return flags; }; - + var FS_getMode = (canRead, canWrite) => { var mode = 0; if (canRead) mode |= 292 | 73; if (canWrite) mode |= 146; return mode; }; - - - - + + + + var ERRNO_MESSAGES = { 0:"Success", 1:"Arg list too long", @@ -1981,10 +1981,10 @@ function dbg(text) { 148:"No medium (in tape drive)", 156:"Level 2 not synchronized", }; - + var ERRNO_CODES = { }; - + var demangle = (func) => { warnOnce('warning: build with -sDEMANGLE_SUPPORT to link in libcxxabi demangling'); return func; @@ -2016,43 +2016,43 @@ function dbg(text) { syncFSRequests:0, lookupPath(path, opts = {}) { path = PATH_FS.resolve(path); - + if (!path) return { path: '', node: null }; - + var defaults = { follow_mount: true, recurse_count: 0 }; opts = Object.assign(defaults, opts) - + if (opts.recurse_count > 8) { // max recursive lookup of 8 throw new FS.ErrnoError(32); } - + // split the absolute path var parts = path.split('/').filter((p) => !!p); - + // start at the root var current = FS.root; var current_path = '/'; - + for (var i = 0; i < parts.length; i++) { var islast = (i === parts.length-1); if (islast && opts.parent) { // stop resolving break; } - + current = FS.lookupNode(current, parts[i]); current_path = PATH.join2(current_path, parts[i]); - + // jump to the mount's root node if this is a mountpoint if (FS.isMountpoint(current)) { if (!islast || (islast && opts.follow_mount)) { current = current.mounted.root; } } - + // by default, lookupPath will not follow a symlink if it is the final path component. // setting opts.follow = true will override this behavior. if (!islast || opts.follow) { @@ -2060,17 +2060,17 @@ function dbg(text) { while (FS.isLink(current.mode)) { var link = FS.readlink(current_path); current_path = PATH_FS.resolve(PATH.dirname(current_path), link); - + var lookup = FS.lookupPath(current_path, { recurse_count: opts.recurse_count + 1 }); current = lookup.node; - + if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). throw new FS.ErrnoError(32); } } } } - + return { path: current_path, node: current }; }, getPath(node) { @@ -2087,7 +2087,7 @@ function dbg(text) { }, hashName(parentid, name) { var hash = 0; - + for (var i = 0; i < name.length; i++) { hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; } @@ -2131,9 +2131,9 @@ function dbg(text) { createNode(parent, name, mode, rdev) { assert(typeof parent == 'object') var node = new FS.FSNode(parent, name, mode, rdev); - + FS.hashAddNode(node); - + return node; }, destroyNode(node) { @@ -2332,15 +2332,15 @@ function dbg(text) { getMounts(mount) { var mounts = []; var check = [mount]; - + while (check.length) { var m = check.pop(); - + mounts.push(m); - + check.push.apply(check, m.mounts); } - + return mounts; }, syncfs(populate, callback) { @@ -2348,22 +2348,22 @@ function dbg(text) { callback = populate; populate = false; } - + FS.syncFSRequests++; - + if (FS.syncFSRequests > 1) { err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`); } - + var mounts = FS.getMounts(FS.root.mount); var completed = 0; - + function doCallback(errCode) { assert(FS.syncFSRequests > 0); FS.syncFSRequests--; return callback(errCode); } - + function done(errCode) { if (errCode) { if (!done.errored) { @@ -2376,7 +2376,7 @@ function dbg(text) { doCallback(null); } }; - + // sync all mounts mounts.forEach((mount) => { if (!mount.type.syncfs) { @@ -2394,79 +2394,79 @@ function dbg(text) { var root = mountpoint === '/'; var pseudo = !mountpoint; var node; - + if (root && FS.root) { throw new FS.ErrnoError(10); } else if (!root && !pseudo) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - + mountpoint = lookup.path; // use the absolute path node = lookup.node; - + if (FS.isMountpoint(node)) { throw new FS.ErrnoError(10); } - + if (!FS.isDir(node.mode)) { throw new FS.ErrnoError(54); } } - + var mount = { type, opts, mountpoint, mounts: [] }; - + // create a root node for the fs var mountRoot = type.mount(mount); mountRoot.mount = mount; mount.root = mountRoot; - + if (root) { FS.root = mountRoot; } else if (node) { // set as a mountpoint node.mounted = mount; - + // add the new mount to the current mount's children if (node.mount) { node.mount.mounts.push(mount); } } - + return mountRoot; }, unmount(mountpoint) { var lookup = FS.lookupPath(mountpoint, { follow_mount: false }); - + if (!FS.isMountpoint(lookup.node)) { throw new FS.ErrnoError(28); } - + // destroy the nodes for this mount, and all its child mounts var node = lookup.node; var mount = node.mounted; var mounts = FS.getMounts(mount); - + Object.keys(FS.nameTable).forEach((hash) => { var current = FS.nameTable[hash]; - + while (current) { var next = current.name_next; - + if (mounts.includes(current.mount)) { FS.destroyNode(current); } - + current = next; } }); - + // no longer a mountpoint node.mounted = null; - + // remove this mount from the child mounts var idx = node.mount.mounts.indexOf(mount); assert(idx !== -1); @@ -2550,13 +2550,13 @@ function dbg(text) { var new_name = PATH.basename(new_path); // parents must exist var lookup, old_dir, new_dir; - + // let the errors from non existant directories percolate up lookup = FS.lookupPath(old_path, { parent: true }); old_dir = lookup.node; lookup = FS.lookupPath(new_path, { parent: true }); new_dir = lookup.node; - + if (!old_dir || !new_dir) throw new FS.ErrnoError(44); // need to be part of the same mount if (old_dir.mount !== new_dir.mount) { @@ -2854,7 +2854,7 @@ function dbg(text) { } // we've already handled these, don't pass down to the underlying vfs flags &= ~(128 | 512 | 131072); - + // register the stream with the filesystem var stream = FS.createStream({ node, @@ -3138,7 +3138,7 @@ function dbg(text) { // TODO deprecate the old functionality of a single // input / output callback and that utilizes FS.createDevice // and instead require a unique set of stream ops - + // by default, we symlink the standard streams to the // default tty devices. however, if the standard streams // have been overwritten we create a unique device for @@ -3158,7 +3158,7 @@ function dbg(text) { } else { FS.symlink('/dev/tty1', '/dev/stderr'); } - + // open default streams for the stdin, stdout and stderr devices var stdin = FS.open('/dev/stdin', 0); var stdout = FS.open('/dev/stdout', 1); @@ -3189,7 +3189,7 @@ function dbg(text) { }; this.setErrno(errno); this.message = ERRNO_MESSAGES[errno]; - + // Try to get a maximally helpful stack trace. On Node.js, getting Error.stack // now ensures it shows what we want. if (this.stack) { @@ -3208,15 +3208,15 @@ function dbg(text) { }, staticInit() { FS.ensureErrnoError(); - + FS.nameTable = new Array(4096); - + FS.mount(MEMFS, {}, '/'); - + FS.createDefaultDirectories(); FS.createDefaultDevices(); FS.createSpecialDirectories(); - + FS.filesystems = { 'MEMFS': MEMFS, }; @@ -3224,14 +3224,14 @@ function dbg(text) { init(input, output, error) { assert(!FS.init.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); FS.init.initialized = true; - + FS.ensureErrnoError(); - + // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here Module['stdin'] = input || Module['stdin']; Module['stdout'] = output || Module['stdout']; Module['stderr'] = error || Module['stderr']; - + FS.createStandardStreams(); }, quit() { @@ -3426,27 +3426,27 @@ function dbg(text) { var header; var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; var usesGzip = (header = xhr.getResponseHeader("Content-Encoding")) && header === "gzip"; - + var chunkSize = 1024*1024; // Chunk size in bytes - + if (!hasByteServing) chunkSize = datalength; - + // Function to get a range from the remote URL. var doXHR = (from, to) => { if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); if (to > datalength-1) throw new Error("only " + datalength + " bytes available! programmer error!"); - + // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. var xhr = new XMLHttpRequest(); xhr.open('GET', url, false); if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); - + // Some hints to the browser that we want binary data. xhr.responseType = 'arraybuffer'; if (xhr.overrideMimeType) { xhr.overrideMimeType('text/plain; charset=x-user-defined'); } - + xhr.send(null); if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); if (xhr.response !== undefined) { @@ -3465,7 +3465,7 @@ function dbg(text) { if (typeof lazyArray.chunks[chunkNum] == 'undefined') throw new Error('doXHR failed!'); return lazyArray.chunks[chunkNum]; }); - + if (usesGzip || !datalength) { // if the server uses gzip or doesn't supply the length, we have to download the whole file to get the (uncompressed) length chunkSize = datalength = 1; // this will force getter(0)/doXHR do download the whole file @@ -3473,7 +3473,7 @@ function dbg(text) { chunkSize = datalength; out("LazyFiles on gzip forces download of the whole file when length is accessed"); } - + this._length = datalength; this._chunkSize = chunkSize; this.lengthKnown = true; @@ -3499,12 +3499,12 @@ function dbg(text) { } } }); - + var properties = { isDevice: false, contents: lazyArray }; } else { var properties = { isDevice: false, url: url }; } - + var node = FS.createFile(parent, name, properties, canRead, canWrite); // This is a total hack, but I want to get this lazy file code out of the // core of MEMFS. If we want to keep this lazy file concept I feel it should @@ -3585,7 +3585,7 @@ function dbg(text) { abort('FS.standardizePath has been removed; use PATH.normalize instead'); }, }; - + var SYSCALLS = { DEFAULT_POLLMASK:5, calculateAt(dirfd, path, allowEmpty) { @@ -3669,7 +3669,7 @@ function dbg(text) { }; function ___syscall_chdir(path) { try { - + path = SYSCALLS.getStr(path); FS.chdir(path); return 0; @@ -3681,7 +3681,7 @@ function dbg(text) { function ___syscall_fchdir(fd) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); FS.chdir(stream.path); return 0; @@ -3695,11 +3695,11 @@ function dbg(text) { HEAP32[((___errno_location())>>2)] = value; return value; }; - + function ___syscall_fcntl64(fd, cmd, varargs) { SYSCALLS.varargs = varargs; try { - + var stream = SYSCALLS.getStreamFromFD(fd); switch (cmd) { case 0: { @@ -3753,7 +3753,7 @@ function dbg(text) { function ___syscall_fstat64(fd, buf) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); return SYSCALLS.doStat(FS.stat, stream.path, buf); } catch (e) { @@ -3766,21 +3766,21 @@ function dbg(text) { assert(typeof maxBytesToWrite == 'number', 'stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!'); return stringToUTF8Array(str, HEAPU8, outPtr, maxBytesToWrite); }; - + function ___syscall_getdents64(fd, dirp, count) { try { - + var stream = SYSCALLS.getStreamFromFD(fd) if (!stream.getdents) { stream.getdents = FS.readdir(stream.path); } - + var struct_size = 280; var pos = 0; var off = FS.llseek(stream, 0, 1); - + var idx = Math.floor(off / struct_size); - + while (idx < stream.getdents.length && pos + struct_size <= count) { var id; var type; @@ -3822,7 +3822,7 @@ function dbg(text) { function ___syscall_ioctl(fd, op, varargs) { SYSCALLS.varargs = varargs; try { - + var stream = SYSCALLS.getStreamFromFD(fd); switch (op) { case 21509: { @@ -3916,7 +3916,7 @@ function dbg(text) { function ___syscall_lstat64(path, buf) { try { - + path = SYSCALLS.getStr(path); return SYSCALLS.doStat(FS.lstat, path, buf); } catch (e) { @@ -3927,7 +3927,7 @@ function dbg(text) { function ___syscall_newfstatat(dirfd, path, buf, flags) { try { - + path = SYSCALLS.getStr(path); var nofollow = flags & 256; var allowEmpty = flags & 4096; @@ -3944,7 +3944,7 @@ function dbg(text) { function ___syscall_openat(dirfd, path, flags, varargs) { SYSCALLS.varargs = varargs; try { - + path = SYSCALLS.getStr(path); path = SYSCALLS.calculateAt(dirfd, path); var mode = varargs ? SYSCALLS.get() : 0; @@ -3957,7 +3957,7 @@ function dbg(text) { function ___syscall_stat64(path, buf) { try { - + path = SYSCALLS.getStr(path); return SYSCALLS.doStat(FS.stat, path, buf); } catch (e) { @@ -3988,7 +3988,7 @@ function dbg(text) { var _emscripten_memcpy_big = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num); - + var growMemory = (size) => { var b = wasmMemory.buffer; var pages = (size - b.byteLength + 65535) / 65536; @@ -4010,7 +4010,7 @@ function dbg(text) { // With multithreaded builds, races can happen (another thread might increase the size // in between), so return a failure, and let the caller retry. assert(requestedSize > oldSize); - + // Memory resize rules: // 1. Always increase heap size to at least the requested size, rounded up // to next page multiple. @@ -4027,7 +4027,7 @@ function dbg(text) { // over-eager decision to excessively reserve due to (3) above. // Hence if an allocation fails, cut down on the amount of excess // growth, in an attempt to succeed to perform a smaller allocation. - + // A limit is set for how much we can grow. We should not exceed that // (the wasm binary specifies it, so if we tried, we'd fail anyhow). var maxHeapSize = getHeapMax(); @@ -4035,9 +4035,9 @@ function dbg(text) { err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`); return false; } - + var alignUp = (x, multiple) => x + (multiple - x % multiple) % multiple; - + // Loop through potential heap size increases. If we attempt a too eager // reservation that fails, cut down on the attempted size and reserve a // smaller bump instead. (max 3 times, chosen somewhat arbitrarily) @@ -4045,12 +4045,12 @@ function dbg(text) { var overGrownHeapSize = oldSize * (1 + 0.2 / cutDown); // ensure geometric growth // but limit overreserving (default to capping at +96MB overgrowth at most) overGrownHeapSize = Math.min(overGrownHeapSize, requestedSize + 100663296 ); - + var newSize = Math.min(maxHeapSize, alignUp(Math.max(requestedSize, overGrownHeapSize), 65536)); - + var replacement = growMemory(newSize); if (replacement) { - + return true; } } @@ -4060,7 +4060,7 @@ function dbg(text) { var ENV = { }; - + var getExecutableName = () => { return thisProgram || './this.program'; }; @@ -4094,7 +4094,7 @@ function dbg(text) { } return getEnvStrings.strings; }; - + var stringToAscii = (str, buffer) => { for (var i = 0; i < str.length; ++i) { assert(str.charCodeAt(i) === (str.charCodeAt(i) & 0xff)); @@ -4103,7 +4103,7 @@ function dbg(text) { // Null-terminate the string HEAP8[((buffer)>>0)] = 0; }; - + var _environ_get = (__environ, environ_buf) => { var bufSize = 0; getEnvStrings().forEach((string, i) => { @@ -4115,7 +4115,7 @@ function dbg(text) { return 0; }; - + var _environ_sizes_get = (penviron_count, penviron_buf_size) => { var strings = getEnvStrings(); HEAPU32[((penviron_count)>>2)] = strings.length; @@ -4125,7 +4125,7 @@ function dbg(text) { return 0; }; - + var _proc_exit = (code) => { EXITSTATUS = code; if (!keepRuntimeAlive()) { @@ -4138,23 +4138,23 @@ function dbg(text) { /** @param {boolean|number=} implicit */ var exitJS = (status, implicit) => { EXITSTATUS = status; - + checkUnflushedContent(); - + // if exit() was called explicitly, warn the user if the runtime isn't actually being shut down if (keepRuntimeAlive() && !implicit) { var msg = `program exited (with status: ${status}), but keepRuntimeAlive() is set (counter=${runtimeKeepaliveCounter}) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)`; readyPromiseReject(msg); err(msg); } - + _proc_exit(status); }; var _exit = exitJS; function _fd_close(fd) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); FS.close(stream); return 0; @@ -4166,7 +4166,7 @@ function dbg(text) { function _fd_fdstat_get(fd, pbuf) { try { - + var rightsBase = 0; var rightsInheriting = 0; var flags = 0; @@ -4207,10 +4207,10 @@ function dbg(text) { } return ret; }; - + function _fd_read(fd, iov, iovcnt, pnum) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); var num = doReadv(stream, iov, iovcnt); HEAPU32[((pnum)>>2)] = num; @@ -4221,7 +4221,7 @@ function dbg(text) { } } - + var convertI32PairToI53Checked = (lo, hi) => { assert(lo == (lo >>> 0) || lo == (lo|0)); // lo should either be a i32 or a u32 assert(hi === (hi|0)); // hi should be a i32 @@ -4229,10 +4229,10 @@ function dbg(text) { }; function _fd_seek(fd,offset_low, offset_high,whence,newOffset) { var offset = convertI32PairToI53Checked(offset_low, offset_high);; - - + + try { - + if (isNaN(offset)) return 61; var stream = SYSCALLS.getStreamFromFD(fd); FS.llseek(stream, offset, whence); @@ -4262,10 +4262,10 @@ function dbg(text) { } return ret; }; - + function _fd_write(fd, iov, iovcnt, pnum) { try { - + var stream = SYSCALLS.getStreamFromFD(fd); var num = doWritev(stream, iov, iovcnt); HEAPU32[((pnum)>>2)] = num; @@ -4303,7 +4303,7 @@ function dbg(text) { quit_(1, e); }; - + var stringToUTF8OnStack = (str) => { var size = lengthBytesUTF8(str) + 1; var ret = stackAlloc(size); @@ -4574,6 +4574,8 @@ Module['FS_unlink'] = FS.unlink; Module['callMain'] = callMain; Module['FS_createPreloadedFile'] = FS.createPreloadedFile; Module['FS'] = FS; +Module['ENV'] = ENV; +Module['getEnvStrings'] = getEnvStrings; var missingLibrarySymbols = [ 'writeI53ToI64', 'writeI53ToI64Clamped', @@ -4771,7 +4773,6 @@ var unexportedSymbols = [ 'exitJS', 'getHeapMax', 'growMemory', - 'ENV', 'MONTH_DAYS_REGULAR', 'MONTH_DAYS_LEAP', 'MONTH_DAYS_REGULAR_CUMULATIVE', @@ -4816,7 +4817,6 @@ var unexportedSymbols = [ 'demangle', 'demangleAll', 'ExitStatus', - 'getEnvStrings', 'doReadv', 'doWritev', 'promiseMap', diff --git a/tests/commands/cd.test.ts b/tests/commands/cd.test.ts new file mode 100644 index 0000000..3899e07 --- /dev/null +++ b/tests/commands/cd.test.ts @@ -0,0 +1,11 @@ +import { shell_setup_simple } from "../shell_setup" + +describe("cd command", () => { + it("should update PWD", async () => { + const { shell } = await shell_setup_simple() + const { environment } = shell + expect(environment.get("PWD")).toEqual("/drive") + await shell._runCommands("cd dirA") + expect(environment.get("PWD")).toEqual("/drive/dirA") + }) +}) diff --git a/tests/commands/env.test.ts b/tests/commands/env.test.ts new file mode 100644 index 0000000..bdae236 --- /dev/null +++ b/tests/commands/env.test.ts @@ -0,0 +1,13 @@ +import { shell_setup_simple } from "../shell_setup" + +describe("env command", () => { + it("should write to stdout", async () => { + const { shell, output } = await shell_setup_simple() + const { environment } = shell + expect(environment.get("MYENV")).toBeNull() + + await shell._runCommands("env MYENV=23") + expect(environment.get("MYENV")).toBeNull() + expect(output.text.trim().split("\r\n").at(-1)).toEqual("MYENV=23") + }) +})