VanadiumOS: Portable, multi-user Unix-like OS
github.com
github.com
An example is that if we move to shells where everything is an object with a toString method for stdout, we would avoid many bash bugs in piped commands or shell scripts. I’m sure many concepts such as file io etc could achieve different but worthwhile benefits if reengineered from a modern perspective, even tcp/ip has its warts (but I’ll acknowledge we will never get away from tcp/ ip).
Am I just a clueless outsider?
Nowadays PowerShell is the only experience that ships in the box that is somehow similar to that.
Not only you have structured data, by having first class access to .NET, COM and DLLs, it means any OS API or modules from other applications, are directly accessible for scripting, besides some type declarations.
Food for thoughts: you take a step away from byte-array files. Why stick with the notion of using pipes for composition, or using the same language for the (command-line) UI and for scripting?
Or, for that matter, with a universal "file" concept that tries to treat videos, pictures, executables, application-internal data files, database files and system configuration files the same (even before taking into account Unix's "special files" that aren't persistent, and are just high-level APIs sharing the same low-level APIs as actual files).
> Or, for that matter, with a universal "file" concept that tries to treat videos, pictures, executables, application-internal data files, database files and system configuration files the same
POSIX and Windows have what are called "stream-oriented" or "bytestream" files – as far as the filesystem is concerned, the file is just an unstructured stream of bytes; applications can require those bytes to be in a certain format, but the filesystem is ignorant of that and won't enforce it.
Many mainframe/minicomputer operating systems support additional types of files – such as record-oriented files, in which a file is divided into records – either fixed length (every record in file must be same size) or variable length (every record is preceded by a header which stores its length in bytes, sometimes also flags). The filesystem forces all reads/writes to be of whole records (or groups of them), partial reads/writes are prohibited. Some also had indexed files, where you basically have a key-value store built into the filesystem, with each record divided into a key and a value, the filesystem had an API to retrieve records by key instead of location in the file; commonly the file was internally structured as a B-tree or hashtable, but that was hidden from applications, the filesystem looked after those details.
The designers of Unix knew about this complexity – they had extensive experience with IBM's OS/360 mainframe operating system which included it – and intentionally decided to avoid it, and give Unix only byte-stream files. The same design choice was made by designers of microcomputer/PC operating systems such as CP/M, MS-DOS and OS/2. Dave Cutler, chief architect of Windows NT, knew all about record-oriented files (OpenVMS has them), but decided against including them in Windows NT.
We can question whether those were the right decisions – or, even if they were the right decisions at the time, they are not guaranteed to forever more remain so.
That all said, I'm not disagree with your point about smarter pipelines (rather than dumb byte streams) would be advantageous too.
Though to address your $PATH point directly, in the literal decades that I've been using Linux and UNIX systems, I can't recall a single time when I've ran into a problem with $PATH values being colon delimited. And the reason it is structured that way is again, to work around a limitation of the shells of that time (ie they didn't support arrays).
Though if we are going to talk about problems with environmental variables then $PATH would be somewhere at the bottom of the list while the structure those values are stored at would be top (you think it's a pain parsing $PATH, trying parsing raw env var "structs" -- I've seen far more bugs introduced there) along with the fact that it was never intended to be secure yet that passing secrets seems to be one of it's primary use cases these days.
I see nushell is written in Rust, so it doesn't have (2), although I'm less sure about (1).
I think it would be cool if you could add structured pipeline support to traditional Unix tools (bash, coreutils, etc). The problem is, Unix pipes don't have any support for passing metadata (performing content negotiation between the two ends of the pipe). Given they are a facility provided by the kernel, adding that would likely require kernel changes.
One day I started daydreaming about prototyping content negotiation support for Unix pipes. I was thinking of using CUSE (Linux character device driver in userspace) to do it. Never actually got around to trying though, other more pressing things to work on. Maybe some day I will get around with it, or maybe someone else will feel more enthusiastic about that than I do.
The way it works is it uses fd3 to communicate schema information so it can natively support all the existing "dumb" pipes without any modification but any new tools can be written to send objects instead (albeit byte encoded).
It's not as elegant as PowerShell sending .NET objects natively, but then PowerShell doesn't work with existing CLI tools natively (it needs wrapper scripts to convert them into PowerShell commands). Whereas my shell is fully backwards compatible while still supporting a suite of additional functionality too.
An idea I've had before: the shell should listen on a Unix domain socket, and put the path to that in an environment variable. Then commands could talk to the shell which invoked them. That could be used for this kind of content negotiation stuff. It could also be used to do other things – for example, a subprocess could modify the current directory of the user's shell, or its environment – there are scenarios in which being able to do that would be useful. A Unix domain socket path in an environment variable seems less likely to cause unexpected issues than inheriting an extra file descriptor.
Please don't take this as a dismissive comment though. I'm definitely not suggesting that your method has more problems nor that my method is better. There is a lot I do like about your suggestion and it might be an option I pivot to if/when I do run into issues with fd3.
> I did consider that myself but I didn't want to manage sockets in addition to processes, plus file descriptors would be idiomatic to pipelines.
A socket is just a file descriptor at the end of the day. Some shells (I think some variants of ksh?) have actually used socketpair() to build pipelines instead of pipe().
> the environmental variables might also be carried over, so how does that grandchild process get restricted from writing to the socket?
Unix domain sockets let you know the PID/UID/GID at the other end (SO_PEERCRED, SCM_CREDENTIALS, etc). So when the shell gets a connection, it can tell whether it is coming from one of its direct children, or a grandchild.
However, I'm not sure if one actually should restrict subprocess. If a child process spawns a subprocess, why shouldn't the subprocess be allowed to negotiate the input/output format, etc? The parent process might just be some kind of launcher and the child might be the one that does all the actual work.
In any event, what's to stop fd3 being inherited by grandchild processes too?
They are. But I don’t have code to manage sockets whereas I already do manage STDOUT et al so adding another pipe there doesn’t increase the code complexity.
It’s a bit of a lazy excuse I’ll grant you. But it also wasn’t the deciding reason, just an added incentive.
> Unix domain sockets let you know the PID/UID/GID at the other end (SO_PEERCRED, SCM_CREDENTIALS, etc). So when the shell gets a connection, it can tell whether it is coming from one of its direct children, or a grandchild.
I wasn’t aware of this. That’s handy to know. Thank you.
> However, I'm not sure if one actually should restrict subprocess. If a child process spawns a subprocess, why shouldn't the subprocess be allowed to negotiate the input/output format, etc?
There’s a lot of scope for accidental data corruption. If you’ve just got a byte stream of text then it’s very easy for data to be safely concatenated. But if you’re streaming objects then the last thing you want is is those objects concatenated by dumb POSIX pipes.
An example of this is if one data type aware shell script forks a bash shell script, which in turn forks processes that are again aware of data types. You’d end up having each process forked from bash writing schemas to the socket (because they inherited the env cars from bash) while their actual data is being written in the pipeline. This would lead to corrupted data.
If the kernel was object aware then this wouldn’t be an issue. But since we are hacking objects into a byte stream you have to be a little more careful about how the data is segmented.
> In any event, what's to stop fd3 being inherited by grandchild processes too?
Indeed. Just as you can write directly to fd{0..2} on any given process if you wanted. But the developer would have to go out of their way to expose (or locate another processes) fd3.
My goal wasn’t to prevent deliberate exposure but rather to prevent accidental unwanted exposure.
I do agree there is the risk that some processes might intentionally create an fd3, unaware of my shell. But at the time I felt that risk was lower than the risk of processes accidentally misusing a socket…though I will look into the point you made about the PID of connecting process being available as that might swing my decision the other way.
The same thing could happen with fd3: Your shell spawns programA, which knows nothing about fd3. programA then spawns children programB and programC in parallel, both of which do. If programA does nothing to stop programB and programC from inheriting fd3, they both will, and then you are in the same situation as with a Unix domain socket path in an environment variable.
Actually, here's a way in which fd3 is worse than a domain socket path in an environment variable: with a Unix domain socket, you'll get two independent connections (on different FDs) from programB and programC, so even if their output gets mixed up, at least their control messages won't be. Whereas, they'll share the same fd3, and any message you receive, you won't be able to tell which one it came from–there is even some risk you'll get part of a message from one and then part of a message from another (although whether that happens or not depends on the details of buffering).
> But since we are hacking objects into a byte stream you have to be a little more careful about how the data is segmented.
Linux supports SOCK_SEQPACKET Unix domain sockets, in which the kernel is aware of explicit message boundaries. Unfortunately, most other Unix-like systems don't (most significantly, macOS).
This isn’t something I’ve been able to replicate in any of my tests but I’ll take your point into consideration and take another look in case I’ve missed something.
> Actually, here's a way in which fd3 is worse than a domain socket path in an environment variable: with a Unix domain socket, you'll get two independent connections (on different FDs) from programB and programC, so even if their output gets mixed up, at least their control messages won't be. Whereas, they'll share the same fd3, and any message you receive, you won't be able to tell which one it came from–there is even some risk you'll get part of a message from one and then part of a message from another (although whether that happens or not depends on the details of buffering).
Good point.
Do you think it would be practical to reuse the same socket for everything then and use the incoming PID as a filter rather than creating a new file per invocation?
> Linux supports SOCK_SEQPACKET Unix domain sockets, in which the kernel is aware of explicit message boundaries. Unfortunately, most other Unix-like systems don't (most significantly, macOS).
Yeah. This is a cross platform project which has unfortunately placed a few restrictions on its design (though thankfully not many).
Thanks for the feedback by the way. It’s constructive comments like these which help improve indie projects :)
I was thinking you could have two environment variables:
SHELL_SOCKET=<path to the Unix domain socket the shell is listening on> SHELL_CLIENT_ID=<some kind of unique ID, like a UUID>
Now, SHELL_SOCKET is the same for every subprocess. But every new process you fork, you set a different value for SHELL_CLIENT_ID. And part of the protocol, is that upon connecting, the client has to send you SHELL_CLIENT_ID, and you check it is a valid one in your memory, and you reject the connection if it isn't. Since you control the protocol, you could also insist the client sends you the PID, and match that too. I wouldn't even bother trying to stop clients lying about their own PID – if someone wants to play those kind of silly games, why stop them? If the socket file ownership/permissions make it accessible by the user only, then either the user themselves is being silly, or else their account is already compromised by an attacker/malware.
So then you don't even need to bother with SO_PEERCRED/SCM_CREDENTIALS/etc if you don't want to.
And, with respect to the "multiple subprocesses" problem–the simplest thing you could do, is decide to only accept one connection per a SHELL_CLIENT_ID, the first one wins, the second gets rejected. There's many more complex solutions (like have some kind of locking so subprocesses could take turns at output but doing it in a way which wouldn't corrupt the output), but that would be the simplest.
You could even encode both into a single environment variable, e.g: SHELL_SOCKET=<client UUID>:<path to shell socket>
Additionally the way Xerox and ETHZ workstations did their REPLs was native code all the way, as Interlisp, Smalltalk, Mesa XDE, Mesa/Cedar, Oberon, Oberon-2, AOS all had AOT/JIT workflows.
I realize this is HN, but not everything needs to be an industry disrupter. Honestly, I'm here more for the weird, impractical experiments than anything else.
One of the reasons why we have all this mess with UWP, WinRT, WinUI and WinAppSDK.
They could have followed Android's approach where everyone works together for that incremental upgrade, but alas.
Genode OS
SerenityOS
BeOS/Haiku
From Oberon linage, AOS is still around at ETHZ
Inferno
Erlang, Go, Oberon, Java and .NET targeting bare metal workloads where the runtime plays the role of the OS.
And most likely a few others I have forgotten.
Nobody should start to undertake a large project. You start with a small trivial project, and you should never expect it to get large. If you do, you'll just overdesign and generally think it is more important than it likely is at that stage. Or worse, you might be scared away by the sheer size of the work you envision. So start small, and think about the details. Don't think about some big picture and fancy design. If it doesn't solve some fairly immediate need, it's almost certainly over-designed. And don't expect people to jump in and help you. That's not how these things work. You need to get something half-way useful first, and then others will say "hey, that almost works for me", and they'll get involved in the project.
“Linus is a good developer, but is a terrible engineer. I'm sure that he'll agree with me.”
:)
I've noticed, and I'm not the only one, that HN has developed a particular reputation in the tech community. A number of people I know don't actually like having their work show up on "the orange site" because of that Comic-Book-Guy culture.
Just seems counter to the spirit of hacking to judge somebody's experiments through the lens of a VC pitch.
And it's a reaction I've seen on here before, so I guess I'm a little sensitive to it, maybe because my personal projects are the kind that would attract similar criticism if they ever showed up here.
You could build compatibility layers for porting the most complex stuff, but the idea is that an advanced, better OS would make it trivial to build the most commonly used applications for everyday use, and provide benefits on top of them that would make the switch to the new system worthwhile.
You just have to use it.
When Cutler went to Microsoft the restrictions where clearly the slow and small PC's (the main customers of MS), so that really "cut" the system down (pun intended). Like the Idea to implement NT as a pure microkernel, but it was just too slow for the normal home PC, and that's why NT is kind of a hybrid.
https://en.wikipedia.org/wiki/Dave_Cutler#Microsoft_(1988_-_...
I'd be interested to know if anyone is using this OS, and for what use case.
Amazing bit of work!
https://github.com/Zeal-Operating-System/ZealOS
a try. It's 64-bit fork of TempleOS...