Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/capsule into odin

This commit is contained in:
Nathan Fisher 2023-08-18 12:49:59 -04:00
commit a70b768a97
8 changed files with 270 additions and 12 deletions

View file

@ -0,0 +1,20 @@
Meta(
title: "C: because danger is fun",
summary: Some("I have been working on a project concurrently in several languages, including C, and am surprised at how much I'm enjoying the experience"),
published: None,
tags: [
"C",
"programming",
],
)
---
If you've looked at my programming output at all it should be pretty obvious that I have bought into Rust. But truth be told, I never intended to go whole hog into any one programming language. I love fooling around in new languages and sometimes in older ones that are becoming more obscure (I'm looking at you, Fortran), but I've come to depend on Rust's ecosystem and some of it's builtin features when it comes time to get something done.
A while back I was working on a package manager for my little toy Linux distro, HitchHiker, and I decided to embed the ability to read and write Tar archives in it. The Tar capability had to be in the binary, because I wanted to be able to read a file into memory, checksum it, and create a tar node in one operation rather than reading it into memory twice, which I would have to do if Tar was external. In the process, I decided that I really don't care much for Tar and started thinking about what an archive format designed from scratch might look like, which fixed a lot of Tar's weaknesses. What I came up with is a format that I'm calling Haggis.
=> https://codeberg.org/jeang3nie/haggis/Format.md
Now, I'm not trying to replace Tar and don't recommend that people start using this. It's mostly a fun experiment at this stage, and I've been using it to re-evaluate programming languages in the process. I have simultaneous implementations in various degrees of completion in Rust, Zig, Hare and C. This post isn't even going to be about Haggis itself, or the different languages being used. It's going to be about how much fun I've been having revisiting C, which is totally surprising to me at this point considering how attached I've gotten to language features such as algabreic data types and Rust's great concurrency primitives.
## A Short aside on Zig
Zig is still pre-1.0. They released their self hosted compiler for version 0.10 and the upcoming 0.11 release is seeing some serious progress in implementing missing language features that have been accepted into the spec for the language. Things are progressing at a surprisingly fast pace considering how small their community is, but it's got a ways to go before it becomes a true competitor. That said, I feel that Zig's type system and safety checks get me about 90% of the benefits people ascribe to Rust without the overhead of a borrow checker. Manual memory management also has benefits in that you have flexibility to do things that Rust's borrow checker will never allow, and makes doing other things easy that the borrow checker makes into a nightmare of lifetime annotations and ridiculously complex type signatures. In short, when it matures I think Zig is going to absolutely rock the world.

View file

@ -0,0 +1,23 @@
Meta(
title: "Dresses, Kilts and more",
summary: None,
published: Some(Time(
year: 2023,
month: 8,
day: 16,
hour: 21,
minute: 9,
second: 22,
)),
tags: [],
)
---
There's been a bit of talk on Gemini today over the idea of mem in dresses. I loved this part from Stacksmith.
> Really, it would make a lot more sense for men to wear dresses and women, pants. Look at the engineering aspects: if you have a heat-sink dangling down there, you are a fool for covering it with layers and layers of fabric.
Kilts have been mentioned as well, and while I'm totally all for kilts and have considered getting a few, I think that's missing the point. The original topic was dresses. As in the totally feminine, not at all masculine all in one piece ensembles associated with Disney princesses. Kilts are not that. I associate the Kilt (with a capital K) with Scotsman carrying two handed swords into battle, or at the very least tossing small telephone poles across a field to prove their masculinity.
I'm not trans. I'm a pretty masculine guy in appearance. That said, I've never been conventional. I have no problem with defying gender stereotypes and totally support anyone else doing the same.
Plus, I totally agree when it comes to the mechanical design. Those bits were obviously not designed to be constrained by restictive garb. Quoting Robin Williams from The Fisher King, "Free up the little guy! Let him flap in the wind!"

View file

@ -0,0 +1,61 @@
Meta(
title: "Just how much is really missing in C?",
summary: Some("An experiment in polyglot programming"),
published: None,
tags: [
"C",
"programming",
"haggis",
],
)
---
A while back now, I was working on a package manager for HitchHiker in Rust. The package format was based on Tar, and included checksumming for each file. In order to avoid having to re-read the same data that was being used to create tar nodes when it came time for checksumming, the tar support had to be built into the package manager itself. This gave me an up close look at the format. I didn't particularly like what I saw.
Long story short, I started thinking about how things could be done better. Rather than slicing everything up into 512 byte blocks and padding the final one for each node, why not just specify the file length and then read that number of bytes to eliminate the padding? Rather than recording numbers in ascii, why not just lay out integers as little endian bytes, just as they are represented in memory? That way we can store an integer in a much smaller space. Instead of recording all possible combinations of metadata for every node, why not specify what type of file that node represents and then only record the actually useful metadata? On it went, and on a wild hair I started implementing it, calling it the Haggis protocol because it's a bunch of ground up bytes stuffed into a rather nasty casing.
=> https://codeberg.org/jeang3nie/Haggis Haggis in Rust
=> https://codeberg.org/jeang3nie/seahag Haggis in C
Since I like to explore different programming languages, and have been wanting to do so in a bit more depth than I have in the past, I took this as an opportunity to do some polyglot programming and implement Haggis in Rust, Zig, Hare and C. The Rust implementation is nearly complete and came together quickly for me since I use it all of the time. Zig is not far off, with Hare trailing a bit behind right now. All three of those languages were a great fit for this project, but what has really surprised me is how much fun I've been having writing this in C. Now, I've never been great at C. I've used it for small projects, mostly command line utilities, but never for anything ambitious. I didn't get serious about learning to program until about four years ago in the first place, and by that time Rust was quite a viable option, so I've been using Rust for anything ambitious ever since. But I'm not one of those Rustaceans who thinks that using a language such as C is a sin. On the contrary, there are times when C is definitely the most appropriate tool. For instance if one is writing software to be included in the base system of any of the BSD's, then C is the appropriate tool due to the C compiler and runtime being the only compiler and runtime available. And while I've tried out Rust on microcontrollers I still prefer C for embedded due to the smaller binary sizes, enabling you to stuff more functionality into the very limited space.
## What do I miss most in C?
### And how hard is it to get it back?
I find algabreic types (tagged unions) to be an indispensible tool and any modern language that doesn't have them goes to the bottom of the pile for me. C obviously doesn't have them.
Both Zig and Rust have some form of growable array, HashMap, and BTreeMap right in their standard libraries. The closest you get in C in in queue.h, in the form of queue types backed up by linked lists. But Musl libc omits queue.h, so I won't ever use it as that would reduce portability.
Another big omission is testing. Zig, Rust and Hare all have integrated testing frameworks.
I could probably mention a bunch of other things, but to me these are the biggest issues. Basically, with most modern languages it's like building a house with all of the various parts you can find in a modern big box home store. You still need some specialist knowledge, but in large part you can just nail, screw and bolt ready made pieces together. In contrast C gives you a chainsaw, shovel and other basic tools with which you are expected to cut down trees, dig a foundation and basically make all of it yourself, including any special tools you might want or need. You can of course go buy tools off the random guy down the way, but it'll cost you. It might not be a monetary cost, but the tools from "down the way" are probably not completely fit for purpose, might not be maintained, have missing parts and in general maybe just aren't worth it, which is why so many C programmers just roll their own. And that's basically what I do, too, when dropping down to C.
What you get in return though, is a quite liberating "trust the programmer" philosophy. That "trust the programmer" is actually a direct quote from "The C Programming Language" by Kernighan and Ritchie. What this means is that, even though a language like Rust claims to get you as close to the hardware as C, there are still things one can do simply in C that can't be done the same way in Rust. The C compiler is so trusting that I find myself laughing sometimes at it, feeling as if I've just pulled off some great con, amazed that I just got away with murder. This has downsides of course, and when you combine it with the lack of an official testing framework one can easily paint themselves into a corner where things are crashing at runtime and it's going to take hours or days to track down the issues causing it.
### Tagged unions in C
The lack of algabreic data types turns out to not be that huge of a problem because you can roll your own. C has enums and unions. A tagged union is just a union with an enum tag. Pop an enum and a union into a struct and write functions to ensure that the union is only ever accessed after checking the enum "tag", and make sure that you only ever access that struct using those functions. Viola, you have a tagged union. If this is part of a public API, maybe don't directly expose the underlying struct, just the functions which access it safely.
```
typedef enum {
bar,
baz,
} tag;
typedef union {
unsigned long num;
char *name;
} data;
typedef struct {
tag tg;
data payload;
} foo;
void do_stuff_with_foo(foo *f) {
switch (f->tg) {
case bar:
// do stuff with f->payload as a ulong
break;
case baz:
// do different stuff with f->payload as a string
break;
};
}
```

View file

@ -0,0 +1,38 @@
Meta(
title: "Manual intervention required when updating Gitea to v1.20.0 on Void",
summary: None,
published: Some(Time(
year: 2023,
month: 8,
day: 1,
hour: 4,
minute: 37,
second: 23,
)),
tags: [
"software",
"gitea",
],
)
---
My local Gitea instance runs on a Raspberry Pi 4 running Void Linux. I'm quite happy with Void itself. It's been totally rock solid and reliable since I got all of the kinks worked out about a week after the initial install. Gitea, however, has been a bit of a PITA on a few occasions prior to moving to Void, and the latest package upgrade I ran managed to break a few things. It's all back up and running now, but not without some frustration.
After some troubleshooting, I found an obsolete setting in the config file, which once removed at least allowed the daemon to start. However, connecting to the machine with the `git` user failed with a nice cryptic error message:
```
PTY allocation request failed on channel 0
2023/07/31 23:53:42 ...s/setting/setting.go:109:LoadCommonSettings() [F] Unable to load settings from config: unable to create chunked upload directory: /usr/bin/data/tmp/package-upload (mkdir /usr/bin/data: permission denied)
```
As it turns out, Gitea cannot run without certain files and directories existing, which it attempts to create on launch if they aren't there. They're supposed to be created in the home directory of the user running Gitea, in this case `git`, but if Gitea is run without having $HOME set then it will look for those paths relative to the binary, which is definitely not the desired behavior. More accurately, it needs the variable GITEA_WORK_DIRECTORY, but it derives that value from $HOME.
What does this have to do with ssh? Gitea attempts to automatically manage the .ssh directory of it's user, and sets itself as a custom command to handle keys in .ssh/authorized_keys.
```
command="/usr/bin/gitea --config=/etc/gitea/app.ini serv key-2",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 <redacted>
```
Now, since we've established that Gitea won't start without $GITEA_WORK_DIR being set, ssh can't authorize the git user using pubkey authentication because Gitea chokes, even though Gitea is running away happily as a daemon, having gotten the vars set correctly in the `run` script which runit uses to start it up. I'm not thrilled with this design, but there's the bug. And the fix is to change .ssh/authorized keys so that the handler is run with the environment var set:
```
command="env GITEA_WORK_DIR=/var/lib/gitea /usr/bin/gitea --config=/etc/gitea/app.ini serv key-2",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 <redacted>
```
Here's hoping that an upcoming release makes this all less brittle. I don't like that it's overwriting .ssh/authorized_keys. I get the reason, but messing with configs is why I hate "user friendly distros" and prefer to work with FreeBSD or Linux distros which follow the KISS principle like Void. Once I've written a config, I expect it to never change unless I edit it myself. As for getting configuration from environment vars, that's kind of stupid and redundant when your application actually has a config file.

View file

@ -0,0 +1,29 @@
Meta(
title: "Old phone morning project",
summary: Some("After being forced into a phone upgrade, I decide to make use of the old hardware"),
published: Some(Time(
year: 2023,
month: 8,
day: 16,
hour: 20,
minute: 23,
second: 12,
)),
tags: [
"android",
"apache",
],
)
---
We've had a family mobile plan with Verizon for a number of years now. The service wasn't bad, but the price kept creeping up no matter what we did. It always amazes me how companies these days are so willing to punish their long time customers with higher rates while offering a much better package if you should "switch" from another service. Seems like a really backwards way of doing business to me. Anyway, due to my SO being a first responder we qualified for an AT&T special plan that will prioritize her phone if the lines are crowded. The best part is that it's a pretty good savings, and not just an introductory rate. We should be able to save money for years to come, so we switched.
One of the annoying things about the switch is that we were unable to carry our current phones over due to being carrier locked. I've always hated this practice. It's a totally non-subtle way of locking you into your current carrier. Well, FU Verizon, because even after putting four new phones on a payment plan we're still saving $50 a month. But I hate to see the old hardware, which still has plenty of life left in it, just sitting in a drawer. Her phone, and my daughter's, were new enough to trade in. That leaves mine and my son's. I'll be trying to think of something cool to do with his, but this morning I put mine to use with a nice simple project.
## The Android petcam
Currently napping next to me on the couch is our four year old rat terrier mix named Spock. He's a really cool little dog who is 100% a family member, who I would take a bullet for if it came to it (and I'm pretty sure the feeling is mutual). We work overrlapping shifts, and as a consequence Spock is alone in the house for about six hours a day until I get home. I've always thought it would be cool to have a way to check in on him when I have a spare minute during the day, and since the old phone has a perfectly capable camera it seemed a perfect match.
This is a really basic setup. After removing almost everything else from the old phone I installed an app called DroidCam from the Play store. I'm going to be checking out some others because it does do some phoning home, but for now it does what I need it to and I'm relatively happy with the security. DroidCam streams a low res video feed (240p) over http on port 4747 in the local network and keeps the phone awake while it's running. That means the phone has to stay plugged in, which is totally fine since it's sitting immobile on the TV stand.
The other component of the setup is a reverse proxy using Apache. I setup a new subdomain in DNS, added the reverse proxy and protected it with a simple .htaccess. It's not perfect security. If you can gain access to the LAN you can probably turn it on and watch a video stream from my living room, but if you're in my lan I'm already screwed. At least with this setup my feed isn't going out to some cloud service in order to make it to the wider internet like almost every commercial offering does, so in that sense I'm satisfied with the security.
I have visions of a more complete system that would allow me to talk to him and dispense treats, using an MCU with network capability to drive a servo that would operate what amounts to a bubble gum dispenser adapted for dog treats. Not sure how I'd do the voice trick just yet. I'm pretty sure I could wrap the proxy up in a php application running on the same machine as Apache and add some buttons for other functions without a ton of effort, making the entire thing friendly enough for my son and SO to use. My son already got on the camera the minute I gave him the url and login info. He loves that he's going to be able to see what Spock is doing while he's at his grandpa's house and we're at work.

View file

@ -74,7 +74,7 @@ I think the choice to not have generics was smart for Hare, because it has simpl
## C
I haven't written C in a while, and I wasn't very good at it when I did last. So it surprised me just how much fun I've been having dusting off my knowledge and diving in. There's so little abstraction that at times you're literally telling the compiler where to put these specific bits in memory, and in order to do so you have to know the endianness of the machine (unless you're just a lazy SOB and assume little endian, of which there are many in the open source world).
For example, when it comes to to store a 32 bit integer as a series of bytes, one has to do the following.
For example, when it comes to to store a 32 bit integer as a series of bytes, one *might* do the following.
* create an array of 4 eight-bit integers (more depth on the subject to follow)
* mask off the bits not needed for each of the four bytes you want to extract
* shift the remaining bits into position
@ -82,12 +82,66 @@ For example, when it comes to to store a 32 bit integer as a series of bytes, on
* use the resulting 8 bit int as the value of the appropriate position in the array, which depends on the endianness of the machine
* write the array into the stream
Needless to say, the above steps are all subject to human error. And since C has no official build system or test runner, you get to decide how you want to compile and test the code all by your lonesome. I'm a fan of just using Makefiles for the build. Taking that a step further, I want to ensure that my Makefile is portable between at least all of the BSD's and GNU make, which restricts the feature set available. I had to take myself back to school a little bit already, because I've been using GNU make so much for the past few years.
Now notice I said one *might* do it this way. But let's see if I can make a Rust programmer scream in horror with the following snippet.
```
#include <stdint.h> // exact sized integer types
#include <stdio.h>
Another benefit of working on this sort of low level project is that all of the Unix interfaces I need to access are programmed in C to begin with and are generally on #include directive away. You still have to account for differences like `major` and `minor` being different widths depending on the platform, but libc on that platform will have macros to derive those numbers from the file metadata. The `mode` I'm always treating as a u16 in any event, as the higher bits on platforms where `mode` is 32 bits are used to store the filetype, not the permissions. In fact, the permissions actually fit into 13 bits and in Haggis I'm using the remaining 3 bits to store the filetype as an enum value, further reducing the metadata size.
typedef uint8_t u8;
The higher level languages don't really give you any advantage when doing read or write ops, which is a lot of what Haggis code is doing. Probably the only thing I'm really missing is good error handling. Sure, there's no tagged unions or bounded arrays in C, but I've made data structures that internally have an enum flag and union field and thus fulfill the same purpose as tagged unions, and it relatively easy to do a `read_all` and use the length returned to fill out the length field in a haggis node, or get the position of the null byte in a C string to get the string's length.
union u16 {
uint16_t val;
u8 bytes[2];
};
I'm pretty sure that a couple of years ago I couldn't have done this. It seems that I've become a better programmer in the years that I've been using Rust, in spite of how much higher level it is.
int load_u16(FILE *stream, union u16 num) {
return fread(num.bytes, 1, 2, stream);
}
I'll be linking at runtime to some shared libs to get access to the cryptographic hash functions and do checksumming operations, and probably to zstd and to libpthread. I fully intend to tackle multithreading in C here and see how the result compares with Rust in terms of performance. I expect they're going to be very close, with any difference a result of the extra allocations I was talking about above. It's interesting, the Rust community has made it sound like parallelism in C is a nightmare, but honestly they Rust threading interface doesn't look much different than libpthread. Sure, in Rust when you lock a Mutex it physically locks out access to the protected data, while in C if you forgot to acquire a lock you could still access that data. It's a clever design. But the C implementation looks nowhere near as scary as it used to, and I think there's an awful lot of overselling going on when you really come down to it. Do I want to always work this way? Probably not. Do I fully trust myself to get it right every time? Again, probably not. But I'm no longer of the opinion that Rust is that huge of an advance. I'm also seeing a lot of the language as "baggage".
int store_u16(FILE *stream, union u16 num) {
return fwrite(num.bytes, 1, 2, stream);
}
```
I mean, that's perfectly valid C, and the compiler definitely doesn't have a problem with it. It won't crash at runtime, either, because all we're doing is providing a way to examine the value of an unsigned 16-bit integer while also providing a way to examine the underlying bytes. It's a perfectly valid access. It just feels sort of wrong coming from Rust to have this sort of capability. It's essentially being able to cast from a u16 to a two byte array of u8 and back again. Granted, if that was the entire implementation this would blow up in your face on a big endian machine because the bytes would be swapped, but that's why we have a preprocessor.
```
#include <endian.h>
#include <stdint.h> // exact sized integer types
#include <stdio.h>
typedef uint8_t u8;
union u16 {
uint16_t val;
u8 bytes[2];
};
#if __BYTE_ORDER__ == __LITTLE_ENDIAN
// little endian functions
#else
// big endian functions
#endif
```
The big endian versions would just make sure to swap the byte order after the read or before the write operations. But at any rate, I find it freaking hilarious that the compiler will accept these sorts of shenanigans after spending the past few years in Rust. Feels like I'm getting away with a major crime. My code is flashing gang signs at your borrow checker. How you like them bytes?
The astute will notice that I've pulled in stdint.h and did a typedef so I can call a u8 a u8. I might be laughing at the shenanigans that C allows, but I still think it's freaking stupid to have int, long, long long, double, long double, short etc and leave it completely up to the implementation what any of those numeric types actually mean. Honestly, I think any C programmer who isn't using exact width integers in 2023 is just being a bit of a dick at this point.
One of the things that all three of Rust, Zig and Hare provide is tagged unions (although in Rust they're just called enum types with associated data). This is something I wish that C had at the language level, but in practice it's possible to get most of the benefit by rolling your own. Consider Haggis' optional checksumming.
```
enum haggis_algorithm {
md5,
sha1,
sha256,
skip,
};
union haggis_sum {
u8 md5[16];
u8 sha1[20];
u8 sha256[32];
};
struct haggis_checksum {
enum haggis_algorithm tag;
union haggis_sum *sum;
};
```
The difference between this "roll your own" approach and that provided at the language level by the other three languages is that you have to remember to set the tag when initiating a `haggis_checksum` struct and to read the tag before accessing the data. The language level constructs in the other languages will enforce this so you can't screw it up. But it does provide a primitive sort of polymorphism, allowing you to do some interesting things with data structures. I wouldn't have known to even try it a few years ago.

View file

@ -0,0 +1,29 @@
Meta(
title: "Sortix: a hobby OS with potential",
summary: None,
published: None,
tags: [
"Unix",
"Sortix",
"programming",
],
)
---
I like perriodically checking out hobby OS projects and seeing what I can learn from them. Sortix is a small hobby OS with the goal of being a clean and modern POSIX implementation. It's been in development since 2011 and managed to go self hosting with it's 1.0 release in 2016, and is capable of running a fair amount of third party software now including Vim, Emacs, Nano, Python, and Perl, which makes it at this point a nice solid base on which to build. Expectations should be appropriate for a hobby system, however, as this is largely a one man project and is not production ready.
I've actually been using code from Sortix for a few years now in HitchHiker, as they have a nice cleaned up fork of Libz that went into the HitchHiker base system early on. It's pretty much a drop in replacement for the official Libz, but the code is missing a lot of the snarled mass of #ifdef..#else..#endif from the original, as support for long dead operatinig systems and platforms has been removed. This makes the code 1000% easier to follow and maintain. What I haven't done in quite a while is actually boot it up into a VM and check out what Sortix is like today.
## Sortix development environment (Linux)
As I mentioned earlier, Sortix is a self hosted system since 2016, but that is only for the base system. Ports are still being developed via cross compilation, as there are a lot of tools that developers take for granted which haven't made it into Sortix yet. So if you want to build master plus all of the ports you need to set up a cross development environment. Alternatively, you can download a nightly ISO and run that, but the nightlies aren't always up to date. Well, that and I like to poke at things and DIY, so I wanted to go the long route. There are official instructions on the website:
=> https://sortix.org/man/man7/cross-development.7.html
I got my cross toolchain set up on Linux but would suggest the following additional instructions.
* Run a clean shell, preferably Dash, by starting it with `env -i HOME=$HOME TERM=$TERM /bin/dash`
* If you have Isl installed it can confuse the gcc build. Manually disable Isl support by appending the arg `--without-isl` to the configure options
There was a small change I applied to the `gettext` port because it's build system got confused by the packages I had installed on my host system and attempted to compile in C# support. It's a common failure when authors use GNU autotools that their configure tests will run against the host libraries rather than the target libraries if you are cross compiling. In this case I just appended `--disable-csharp` to the args passed in to `configure`, which fixed the problem. I sent the tiny diff upstream in a pull request. After that, everything built without errors.
Sortix is a small system. The system itself (kernel and userland minus ports) compiles in about a minute or less. The bulk of the compoilation time is spent on Ports, but since there aren't a lot of ports it's still pretty quick.
## Sortix development environment (FreeBSD)
Cross compiling Sortix on FreeBSD requires more work because the author was obviously working from Linux using GNU tools. Rather than trying to patch all of the Linuxism's in his Makefiles I install GNU coreutils, GNU tar and GNU sed into the same prefix as the cross toolchain. Similar to building on Linux earlier, I started a clean shell, this time using `/bin/sh`.

View file

@ -3,11 +3,11 @@ Meta(
summary: None,
published: Some(Time(
year: 2023,
month: 7,
day: 31,
hour: 22,
minute: 46,
second: 20,
month: 8,
day: 15,
hour: 17,
minute: 31,
second: 40,
)),
tags: [
"tinylog",
@ -18,13 +18,17 @@ Some shorter thoughts and updates that might not warrant a full gemlog entry. I'
=> .. Home
## 2023-08-15 17:31 UTC
Working through issues with the lang/zig port on FreeBSD, trying to get it ready to merge the upgrade to 0.11.0
## 2023-07-31 22:46 UTC
My Gitea instance is currently down after a package upgrade. Considering options, as I'm not 100% satisfied with Gitea to begin with.
## 2023-06-26 23:03 UTC
Just finished adding functionality to create unique message id's for incoming mail in Dory. The id strings will consist of the Unix timestamp upon their receipt and an 8-digit random string to avoid collisions, with a dot separator. This should make sorting your inbox by most recent trivial.
There is already a function for actually reading and processing a request in the Dory codebase, and passing the mail off to storage. It's just currently a bit naive, and doesn't send any feedback back to the client and exits on failure. Once that's working, the only thing left in scope is going to be getting the server side connection setup and client certificate validated. Then it's off to incorporating it into a live server application.
=======
## 2023-06-24 14:06 UTC
Since switching back to FreeBSD on one of my computers there have been just a few packages that I have wished were a little more up to date. Generally the ports tree is extremely up to date, with the packages collection being built on a quarterly basis. Some packages have definitely gone longer than that, however, so I decided to switch from packages to ports. This is pretty easy, just a little time consuming on the first run since you get one rather large batch of upgrades. I'm using portupgrade since that is the tool I remember, but there are a few others such as portmaster.