layout | title | hide_toc |
---|---|---|
default |
Writing Programs (and filesystems) |
true |
If you've stumbled upon this page you're probably trying to write your own programs in the simulator. There's a couple things you'll need to know:
- If possible, try writing real programs on a linux system. There's a whole lot of things to learn that our simulator doesn't support (like shebang lines).
- The maximum filesize on the default filesystem is 288B.
If you want to play around with bigger files (you'll almost certainly need to if you want to implement a filesystem),
use
MemFS
in [section 10]({{ '/pages/10-mounting.html#memfs' | relative_url }}). Click onLoad MemFS from solutions
to get a shell where the default filesystem has no restriction on filesize. - The easiest way to write content into files from the simulator is using our simulator's
edit
utility.
With that out of the way, let's get started!
Your program should be an async
function wrapped in parentheses. A sample program may look like this:
(async function (command){
await this.filesystem.write(command.output, str_to_bytes("hello world!\n"));
return 0;
})
By convention, return 0
indicates that everything went well.
To return an error, either use the throw
keyword in javascript, or call this._return_error(error_msg_str)
.
Let's take a look at the components that make this possible.
Your program will be executed under the context of the shell.
This means that this
will refer to an instance of Shell
from [/js/shell.mjs]({{ '/js/shell.mjs' | relative_url }}).
Below is a description of all availible attributes/functions you may use. Note that names with parentheses next to them are functions.
this.filesystem
this.current_dir
this.umask
this.input_path
this.output_path
this.error_path
this.stderr
this.create_extended_input(content)
To pass in some initial text to populate the <textarea>
with, pass in a string as content
.
this.get_extended_output_and_cleanup()
This function may block, so call it with await
.
this.run_command(command)
this.path_join(path1, path2)
this.expand_path(path)
Note that this doesn't check if the path actually corresponds to a file on the filesystem.
this._return_error(error)
You might have noticed that all our programs so far have been taking a parameter command
,
and some have even been grabbing a FileDescriptor
corresponding to stdout
from command.output
.
The command
object gives us a way to interact with specified command parameters.
A command is a string that (roughly) follows the regex below:
\s*PROGNAME\s*\s(ARG\s*)*(>>?\s*STREAMNAME)?\s*
where \s
represents any whitespace.
PROGNAME
represents the name of the program to be executedARG
represents an argumentSTREAMNAME
represents the name of an output stream
For each of the "variables" defined above,
it must either be any character that is not whitespace,
and not >
,
although it may contain whitespace and >
if they are either preceeded by a \
or if a substring containing them is surrounded by "
.
The presence of a "
mandates a corresponding closing "
.
To have a variable contain a literal "
, escape it with a \
.
Note that whitespace at the beginning and the end of a line will be lost (including a newline) when parsed by a shell.
(But not if parsed as contiguous input to either parse_command
or resume_parse_command
).
A single '>' implies that the output should be redirected to STREAMNAME
,
>>
implies that the output should be appended and should not overwrite the destination.
Let's look at how you can construct/use it.
static parse_command(input)
static resume_parse_command(input, state)
[Constructor] Command(input)
this.output
If you invoke a program via run_command
this object will be an instance of FileDescriptor
,
opened with O_WRONLY
and if this.append_output
is set, with O_APPEND
otherwise with O_TRUNC
.
this.append_output
this.arguments
The shell's filesystem is accessible to your function via the this.filesystem
variable.
One thing we haven't mentioned is that in our simulator all filesystem operations are assumed to be asynchronous.
This is to allow for things like simulated disk delay in the animated filesystem interactions,
or to allow for blocking reads from stdin
in the simulator.
Anytime you access the filesystem, use the keyword await
right before the function you wanted to call.
For example, the following program tries to read 5 chars from stdin
and writes the characters read to both stdout
and stderr
.
(async function(command) {
var fd = await this.filesystem.open(this.input_path, O_RDONLY);
// Let's make sure that we opened stdin correctly
if (typeof(fd) === 'string')
return this._return_error(fd);
var buffer = new Uint8Array(new ArrayBuffer(5));
// perform the read and check for errrors
var bytes_read = await this.filesystem.read(fd, buffer);
if (typeof(bytes_read) === 'string')
return this._return_error(bytes_read);
// create a "view" of the buffer that only has the initialized bytes
var read_view = new Uint8Array(buffer.buffer, 0, bytes_read);
// write some debugging messages to stderr
// I've ignored the error checking on the writes.
await this.filesystem.write(this.stderr, str_to_bytes("Read " + bytes_read + " bytes:\n"));
await this.filesystem.write(this.stderr, read_view);
await this.filesystem.write(this.stderr, "\n"));
// write our output to stdout
await this.filesystem.write(command.output, read_view));
// everything was ok!
return 0;
})
Full disclaimer, it's probably a better idea to go learn how to write a real POSIX filesystem that can run on something like linux or macOS for a number of reasons.
- It's a better learning experience and you'll either learn a ton about kernel modules or about FUSE, or generally, about some well documented, relevant, and useful software stack.
- It's cooler and easier to show off to people.
- It'll give you the experience required to use these skills to work on bigger and better projects.
- There's FUSE bindings to just about every language (like fusepy for python)
so you don't have to be locked into a gross language like
javascript
or forced to write a bunch of boiler plate code inc
(although I would reccomend doing it inc
anyway).
Assuming you don't have the necessary resources/motivation to do it with legit libraries or you've got an hour or so to kill, continue trying to implement your own filesystem for our simulator. Don't say I didn't warn you.
To implement a filesystem for mounting, you need to write a program that returns a filesystem when run.
To see more information on how to write a filesystem, read [/pages/filesystem-operations.html]({{ '/pages/filesystem-operations.html' | relative_url }}) and [/pages/10-mounting.html]({{ '/pages/10-mounting.html' | relative_url }}).
Then run mount path [path_to_fs_program_filename]
to mount your filesystem.
It may look something like this:
(async function (command) {
class SampleFS extends DefaultFS {};
SampleFS.prototype.readdir = function() {
console.log("Hello world!");
return [ new Dirent(0, '.'), new Dirent(0, '..')];
};
return (new SampleFS());
})