Kamin's Corner

GBD-200

I've always been a watch wearer, and with the advent of smartwatches I was keen to hop on the train and explore the benefits of having a small connected device on my wrist. My first “smart” watch was the original Pebble which was an amazing device with an e-ink display that lasted about a week on a single charge. However, I wanted to see what a full system on a wrist could do and bought the first Moto 360 and later the Apple Watch Series 4.

Both the Moto 360 and the Series 4 were great wrist computers – they showed notifications, did health and fitness tracking, as well had a variety of apps that extended functionality to things like navigation, calls, and productivity. But the one thing that they weren't great at ironically, was being watches. Both the 360 and the Series 4 had motion activated displays that only turned on when you twisted your wrist. And the battery – only a day's charge at most – meant I had to put them on a charger every night, and always had to make sure to bring a charger with me if I was going out of town. Finally, they were not very durable – the minimal water resistance and glass screens on both devices meant they weren't designed to withstand even some of the everyday activities that a fitness tracker or even a normal watch would go through, like bathing or swimming or manual labor. Over time, these limitations overcame the other benefits these watches brought. I yearned more for something like a modern successor the original Pebble that did some basic smarts like notifications, but actually functioned more like a real watch.

Enter the G-Shock

The G-Shock brand has been known for decades as a reliable digital watch designed to withstand the motions and elements of normal human activity and beyond. That's why I was excited to learn that Casio started dipping its toe into the smarts category with some of its models. I eventually decided to purchase the GBD-200, a variant of the classic G-Shock design with some nifty new features like an always-on MIP digital display, a step counter, and Bluetooth connectivity for time, step, and notification syncing. Plus, with the lower power usage – it promised up to 2 years of battery life on a single standard CR2032 battery. Was this the watch I was looking for?

The Good

At a high level, the GBD-200 has everything I was looking for. Bluetooth syncing so that notifications from my phone showed up on my watch with a quick buzz or beep. A basic step counter meant I could leave my phone at home and go for a quick walk or run and still track my progress. And the MIP display is beautiful – easily legible in ambient light and most importantly – always on. That brings me to the biggest benefit – battery life. Not having to worry about battery life whatsoever felt like a chain was released from me. I can throw it in a drawer for the night, or go an an extended trip and never worry about needing to charge it. And worst case if the battery did run low, it just takes a standard CR2032 battery that can be found at nearly any convenience store. Finally, even with all these features, it's still a G-shock so it carries the same durability as its predecessors – 200 meter water resistance, a rugged durable case, and shock resistance from bumps and drops. And of course it has the standard digital watch features – date tracking, alarms, and timers.

The Bad

Being a semi-smart watch, the GBD-200 does have its drawbacks. The MIP display, while power efficient, can only refresh about once per second – so millisecond-precise timers are out of the question. The notification syncing, while nifty, has its limits. There's little to no granularity to watch you can or can't sync to the watch. It's either phone calls, all other notifications, both, or nothing. There's no way to forward only specific notifications like messages or emails to the watch. And there's no customization of notifications either – everything is just a single beep or buzz.

Speaking of notifications, there is a bug where accumulating too many notifications on your phone without dismissing them can cause the watch to lose connectivity with the phone. Casio seems to acknowledge this and on the companion app suggests the only fix is to regularly dismiss your notifications. There doesn't seem to be any fix planned for it, so it's a big FYI if you plan on relying on this for critical notifications.

In Summary

Overall, the GBD-200 fits the niche that I really was looking for in a watch – straddling the line well between the features that we've come to appreciate from today's smartwatches while also holding on to the reliability that we've been used to from regular watches of the past. It's a good sign that I hope other watchmakers will take notice. Rather than trying to cram a small computer on our wrists that do everything at the expense of battery life and durability – rather carefully choosing the features that complement the profile well, making the humble watch continue to be a useful companion in an ever more digital world.

I've decided to take another step into the Fediverse and migrate my blog to Writefreely. This means you can now follow me through your favorite ActivityPub platform as well! You can always find me here or through the AP handle for this site – @kamin@kghorvath.com

Note: This post was originally published on August 20, 2024

Modems in 86Box

With the release of version 4.2, 86Box, the vintage x86 PC emulator, now has support for serial modem emulation. It supports acting as a Telnet client or dial-in server, but can also emulate a connection to the internet.

To configure it, there's a new adapter option in the Network settings tab of 86Box for a Standard Hayes-compliant Modem. From the settings, you'll be able to choose a free COM port as well as the baud rate. You can also point it to a phonebook file (more on this later). For this example, I'm using COM1 and 9600. You'll also need to go to the Ports (COM & LPT) tab and make sure your selected serial port is enabled, and that it is not being passed through.

Now you'll be able to add it within your 86Box VM. In my case, I'm emulating Windows 95 OSR2. From Control Panel > Modems > Modem Properties, add a new modem. Be sure to select “Don't detect my modem”. On the next page, select the model as a “Standard 9600 bps Modem”, replacing the baud rate with whatever you configured for 86Box.

You'll also need to select whichever COM port you assigned it in your configuration as well.

Once installed, you can add a new Dial-Up connection from the Accessories folder in the Start Menu. In Windows 98 and later, you can do this directly, but for Windows 95, you'll have to install SLIP and Dial-Up Scripting support first. You can do this through the Windows Setup tab of Add/Remove Programs. Select “Have Disk”, and then browse to the Admin/Apptools/Dscript folder on your Windows 95 CD-ROM and select “Rnaplus.inf”, and install the “SLIP and Scripting for Dial-Up Networking” option.

When adding a new dial-up connection, you can either leave the telephone number blank and enter 0.0.0.0 later as your connection number, or for more realism, you can point 86Box to a phonebook file on your host with a map of telephone numbers to hosts. In this case, if I want to use the number 404-555-1234 as my dial-in number to connect to the internet, I would add this to my phonebook.txt file:

404-555-1234 0.0.0.0:0

After creating the new connection, go into its properties and under the “Server Types” tab, change the Type of Dial-Up Server from PPP to SLIP. Under “TCP/IP settings”, make sure all addresses and gateways are assigned by the server.

Finally, you can go ahead and test your connection. Leave the username and password blank, and either put in your phonebook defined number, or 0.0.0.0 as the Phone number.

If all is successful, you'll be greeted with a blank terminal screen dialog. Hit continue and you'll be connected!

Note: This post was originally published on March 25, 2024

This is a short post that I hope helps someone else that ran into a similar problem I did.

I created a new Windows 10 virtual machine on a Linux host, using QEMU/KVM as the backend and Libvirt/virt-manager as the frontend. As I've done in the past, I used VirtIO devices, including a virtual QXL display adapter, and installed the SPICE guest tools after installation, in one part to allow for auto-resizing the guest resolution with the host window size.

However upon install, I found that the auto-resize feature was not working. After some trial and error, I found that auto-resizing worked when the host window was below a certain size – about half the size of my display. Upon trying to resize the window larger than that, it just stopped working altogether.

After some head scratching and comparing it to my desktop machine where a near-identical configuration worked just fine, I realized that the main difference was that this machine had a HiDPI display. It seems that the problem is that default video memory allocated to the VM (16MB) isn't enough to support guest resolutions above a certain size.

The fix was simple. In the virt-manager details page for the VM, I selected the Video QXL option, and switched to the XML view. I then changed the vgamem field from the default of 16384 to 65536 (64MB). A quick reboot of the VM and now guest autoresizing works perfectly.

Note: This post was originally published in two parts on March 11, 2024 and March 18, 2024

The creators of Bluesky recently introduced early-access to federation of their AT Protocol. This means that individuals can now self-host their own so-called Bluesky instances, called Personal Data Servers, or (PDS)es.

The process of setting up a PDS is fairly straightfoward. An installation script is provided on the official repository but it's really just a wrapper around installing Docker and spinning up a container hosting the service. The script also spins up a Caddy container for reverse-proxying, and a Watchtower container for updates – but the PDS itself is self-contained in a single container, which is a nice change from Mastodon.

Since I already have a Caddy container running as a reverse proxy, and I can handle updates manually, I'll just spin up the lone PDS container. I'm using Podman so this is the incantation I used:

podman run -d --name sky -p 8080:3000 --env-file=pds.env -v /containers/pds:/pds:Z ghcr.io/bluesky-social/pds:0.4

Here, I'm using the path /containers/pds as my stateful volume for data storage, and 8080 is the external port I'm passing to Caddy to proxy. The pds.env file contains the necessary environment variables needed to initially configure the container. The repo contains a full list of options you can customize, but here's the bare minimum I started with.

PDS_HOSTNAME=pds.example.com
PDS_JWT_SECRET=<openssl rand --hex 16>
PDS_ADMIN_PASSWORD=<secretpasswordhere>
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32>
PDS_DATA_DIRECTORY=/pds
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
PDS_DID_PLC_URL=https://plc.directory
PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
PDS_REPORT_SERVICE_URL=https://mod.bsky.app
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
PDS_CRAWLERS=https://bsky.network
LOG_ENABLED=true

Most of these can remain unchanged. The key ones to set are obviously the PDSHOSTNAME and the PDSADMINPASSWORD. PDSJWTSECRET and PDSPLCROTATIONKEYK256PRIVATEKEYHEX can be generated with the commands listed, which are pulled straight from the installer script.

If all goes well, you should be able to browse to your public facing PDS domain and see this:

This is an AT Protocol Personal Data Server (PDS): https://github.com/bluesky-social/atproto
Most API routes are under /xrpc/

Creating a new account

Since we did not set up our PDS with any sort of email verification, we'll need to first create an invite code in order to be able to create an account. To do this, we'll need to construct a POST request to the server as follows, using the admin credentials you specified when you created the PDS in part 1.

curl -X POST -L "https://pds.example.com/xrpc/com.atproto.server.createInviteCode" \
    -H "Content-Type: application/json" \
	--user "admin:<password>" \
	-d '{"useCount": 1}' \
	| jq --raw-output '.code'

If all goes well, it should spit on an invite code. From here, you can do another POST request to create your user, but it's probably easier just to head to bsky.app and create a new account there, pointing towards your PDS.

Migrating an account

Migrating an existing account over is a bit more complicated. There's currently no user-friendly way to do so, so the only way is again through the RESTful API. The account migration doc on the official repository does a good job of explaining the process, as well as the risks and implications of doing so. To make things easier, I've also created a shell script that semi-automates the process. I encourage you to read through it to get an understanding of what steps are taking place before following through.

Note: This post was originally published on January 18, 2024

MicroVMs are a new type of virtual machine that sit somewhere between containers and full VMs. Lighter on resources and faster to boot than standard VMs, but better isolation than a standard container. They've been popularized with Firecracker, which is designed for short-lived disposable microVMs, but QEMU has also added a microvm machine type that we can play around with as well.

At a minimum to get up and running with QEMU microVMs, we'll need to feed it a kernel image and an initial ramdisk. You could probably just use the kernel that's on your system now, but for the sake of this exercise, we'll compile and build a minimal kernel image. Download and extract the kernel sources, start from a minimal slate with make allnoconfig and select your options with make menuconfig or make xconfig

xconfig

A copy of the .config I used is available here. Essentially, I added 64-bit kernel, binary executable support, initramfs support, printk support for console messages (along with timing information), and serial TTY support. This should result in a relatively shorter build time than a standard kernel.

Kernel: arch/x86/boot/bzImage is ready  (#7)
make -j5  231.06s user 27.03s system 304% cpu 1:24.78 total

Copy the built kernel from arch/x86_64/boot/bzImage into our working directory.

Next, we'll want to build a basic initramfs so that we can muck around once the system has booted. Create a new working folder and populate it with our standard LFS directories:

mkdir -p bin dev etc lib mnt proc sys tmp var

Download the latest busybox binary from busybox.net and place it in the bin folder.

In the root of our working folder, we'll create a very basic init script that will populate our /bin and mount our pseudofilesystems. It should look something like this:

#!/bin/busybox sh
/bin/busybox --install /bin
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
sh

Mark our new /init executable with chmod +x init and compress our filesystem into an initramfs.

find . | cpio -ov --format=newc | gzip --best > ../initramfz

Now that we have our kernel and our initramfs ready, we can finally create our microVM. The QEMU incantation is similar to a standard VM:

qemu-system-x86_64 -M microvm,x-option-roms=off,rtc=off \
           -bios /usr/share/qemu/qboot.rom \
		   -cpu host \
		   -m 512 \
		   -enable-kvm \
		   -nographic \
		   -nodefaults \
		   -no-user-config \
		   -no-reboot \
		   -serial mon:stdio \
		   -append 'console=ttyS0 acpi=off reboot=t panic=-1 init=/init' \
		   -kernel bzImage \
		   -initrd initramfz

With any luck, you should see a flurry of kernel messages scroll by and eventually land at a shell prompt:

[    0.276000] Unpacking initramfs...
[    0.280000] workingset: timestamp_bits=62 max_order=17 bucket_order=0
[    0.280000] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[    0.280000] serial8250: ttyS0 at I/O 0x3f8 (irq = 4, base_baud = 115200) is a 16550A
[    0.288000] Freeing initrd memory: 684K
[    0.288000] Freeing unused kernel image (initmem) memory: 636K
[    0.288000] Write protecting the kernel read-only data: 6144k
[    0.288000] Freeing unused kernel image (rodata/data gap) memory: 1756K
[    0.288000] Run /init as init process
sh: can't access tty; job control turned off
/ #

If you list the contents of bin, you'll find that busybox as hardlinked most of the common utilities you'd find in a base Linux distribution. Feel free to play around and when you're done, just type exit. Since microVMs don't have ACPI support, we set our kernel boot parameters to trigger a triple-fault upon reboot, gracefully exiting QEMU when we're done.

Note: This post was originally published on January 16, 2024

Running Silverblue on my primary machine, one of the things that I miss most in the base distribution is Libvirt and QEMU/KVM. There are a few simple options for this – one would be to simply layer the necessary packages with rpm-ostree. However, I try to layer as few packages as possible and keep my system as close to the base tree as possible. There's also a flatpak for GNOME Boxes that can get you quickly up and running, but it's fairly bare bones and makes it difficult to do any configuration beyond basic parameters without diving into XML editing.

However, what Silverblue does have is Podman. Podman and Toolbox are recommended for running containerized software that you either don't want to layer, or isn't available in Flatpak form, and Libvirt is no exception.

To start I create a new privileged container:

$ sudo podman run -d --name virtbox --privileged --net=host -v /proc/modules:/proc/modules -v /var/lib/libvirt/:/var/lib/libvirt:z -v /sys/fs/cgroup:/sys/fs/cgroup:rw registry.fedoraproject.org/fedora-minimal:latest

I make sure to pass --net=host so that we can SSH into the container later on and that our VMs can talk out. We pass through /proc/modules and /sys/fs/cgroup as well so that our container has access to full KVM virtualization. Finally, I pass through a directory from the host, in this case /var/lib/libvirt where we can store our configuration and our VM images.

Once the container is created, enter it with:

$ sudo podman exec -it virbox /bin/bash

Inside the container, we have access to microdnf, a lighter version of the DNF package manager. We'll use it to install the packages we need for virtualization.

# microdnf install passwd libvirt qemu-kvm systemd nc openssh-server passt --nodocs

In order to manage libvirt, we'll SSH into the container from the host. To do that, we'll set up sshd on the container to listen on an alternate port. Within the container's /etc/ssh/sshd_config, add the following lines:

Port 2222 ListenAddress 127.0.0.1 PermitRootLogin yes

Be sure to set a root password as well.

Finally, enable your services:

# systemctl enable --now sshd libvirtd

From your host, or another container on the system, run virt-manager and connect to your libvirt container with root and localhost:2222. You may also want to copy your SSH keys to the container so it doesn't constantly ask for your password with $ ssh-copy-id -p 2222 root@localhost

Mac OS CD desktop

Note: This post was originally published on January 7, 2024

For a long time, the best options for emulating Old World Macs was either Basilisk II or Mini vMac. Now thanks to progress with the qemu-system-m68k emulator, it’s now possible to emulate a Quadra 800 in QEMU, with support for System 7 and Mac OS 8, as well as A/UX 3.0.

As of QEMU 8.2.0, Quadra 800 support is now part of upstream builds, with support for running System 7.1 through Mac OS 8.1 and A/UX 3.0.1.

Starting off, you'll need to create two disk images, one PRAM image to hold firmware variables like resolution and startup disk, and your hard disk image.

qemu-img create -f raw pram-q800.img 256b

qemu-img create -f qcow2 macintoshhd.qcow2 1G

You'll also need a System Software installation CD for System 7.1-7.6 or Mac OS 8.0-8.1 and a Quadra 800 ROM dump (available if you look hard enough). Once you have everything, something similar to the following incantaction should get you going:

qemu-system-m68k.exe -M q800 -m 128 -bios q800.rom-drive file=pramq800.img,format=raw,if=mtd -device scsi-hd,scsi-id=0,drive=hd0 -drive file=macintoshhd.qcow2,media=disk,format=qcow2,if=none,id=hd0 -device scsi-cd,scsi-id=3,drive=cd0 -drive file=system761.iso,media=cdrom,if=none,id=cd0

If everything goes well, you should hear the startup chime, see the Happy Mac, and soon be greeted with a Macintosh desktop.

You’ll have to initialize the disk first before you can install Mac OS. Since we’re emulating a SCSI disk, we'll have to use Apple HD SC Setup to initialize and partition the disk. In my experience, automatic partitioning didn’t properly utilize the whole disk, so you may have to manually tweak the partition layout.

From there, go ahead and run the installer and reboot (don't forget to change your startup disk).

Note: This post was originally published on July 5, 2023

With the launch of Threads tonight, and after having had the chance to use Mastodon and now Bluesky for a bit, I wanted to write my thoughts on the landscape.

Ever since I created a Mastodon instance and joined the Fediverse, I've felt a new love and sense of hope for social media that I had not felt in a long time. Compared to the cesspools of Facebook and Twitter, Mastodon has been a breath of fresh air. The community and level of genuineness in interaction I found there was something I had not experienced since the early days of being on the web. It made me realize that Mastodon didn’t need to be the successor to Twitter to be great – it was able to do that by carving out a niche of its own.

Where Bluesky comes in is that “next Twitter”. I’ve heard comparisons of it to early Twitter and I can see it, where there’s a level of enthusiasm to being on a new platform. I do like some of the ideas Bluesky has put forth with the AT protocol as well, such as unique domain handles that are DNS TXT verified. It’s a great solution to the verification problem, and unlike Mastodon doesn’t require you to spin up your own instance. That being said, there are rough edges. Bsky.social, the only instance available so far, is still missing fundamental features like 2FA authentication. It also seems to have hints of algorithmic suggestions, which Mastodon has vehemently been against. Not an issue yet, but as history has shown, algorithms can be weaponized easily. Still, it does seem like Bluesky is the best positioned to be the Twitter replacement that everyone has held their breath for.

Finally that brings us to Threads. From what I can tell so far, it’s a real mess. Built by Meta on the shoulders of Facebook and Twitter, it shows it’s origins easily. The app is a privacy nightmare, as expected. And in just a few hours, it seems to be already overrun with spam accounts and bad actors. It’s like jumping straight to when Twitter went to shit. What makes Threads interesting though is that they apparently plan to incorporate ActivityPub and join the Fediverse sometime in the future. Whether this is good or bad has been discussed to death already, with no clear conclusions. I will say that personally, I plan to block federation with Threads until the network effects of such a move are a bit clearer. With that in mind, I have no desire and no plan to sign up with Threads anytime soon. It's just not clear what the end goal of Threads is. Is it a Twitter-killer, launched quickly to capitalize on Elon's missteps? Is it another venue for Meta to suck up personal data and monetize it? Or is it just another attempt by Meta to expand back into a landscape that they've struggled to win back for some time?

So there you have it, my two cents on three social media platforms each with differing philosophies and implementations. Only time will tell how this plays out, but my only hope is that the good parts I’ve found stay, and we learn our lessons of the past and minimize the bad that has plagued the platforms we fled from.

Microsoft provides a free version of their Windows Server 2019 operating system called Hyper-V Server 2019. It's essentially a version of Server Core stripped down to only being able to run the Hyper-V role, as a lightweight hypervisor. But with some tooling, we can coax it to run a GUI with enough to run most Win32 applications – and use it as a somewhat lightweight, free version of Windows.

Installation

The first step is to install Hyper-V server. If you've installed Windows in the past, the setup process should be familiar, minus the product key portion of course.

Post-install, we'll be dropped to a command prompt, with sconfig open ready to configure the system, similar to a fresh Server Core install. At this point, I'd recommend enabling Remote Desktop and connecting to the machine that way, so that we can cut and paste text into the prompt.

Sconfig

Initial Steps

The first thing we need to do is enable the App Compatibility Feature on Demand (FOD). This enables compatibility with some desktop programs on Server Core without needing to install the full desktop experience. Fortunately, it's available here as well on Hyper-V Server and will get us some of the essentials we need like explorer.exe and mmc.exe.

To do so, open up a Powershell prompt (you can do so by running powershell from the command prompt), and run the following command:

Add-WindowsCapability -Online -Name ServerCore.AppCompatibility~~~~0.0.1.0

Once that's done, reboot the system and you'll now be able to open up Explorer and a few other familiar tools. But we're not quite there yet – we want the system to log on to a desktop, instead of the barebones command prompt. For that, we'll need to make some registry edits.

Open up regedit and browse to HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\AlternateShells\AvailableShells. This key lists what shell Hyper-V server will load on logon. The default with value 30000 is to load cmd.exe like we've seen. We need to add a string with a higher number to load the shell that we want – in this case explorer.exe.

You'll notice that if you try and do so now, you'll be stopped by an error message, like shown below.

We need to take ownership of the AvailableShells key before we can make any edits. Right click on AvailableShells in the sidebar, select Permissions, and then click Advanced.

At the top, you'll see it says Owner: TrustedInstaller. Click Change, type Administrator in the field, and click Check Names. It should populate with the current Administrator user, as shown below.

Click OK, and before we close out of this, select Administrators under the Permission Entries: list below, click Edit, and make sure to check Full Control. Close out of all the permissions dialog boxes and we should now have the ability to create new objects in this key.

Right click anywhere on the right pane, and select New > String Value. Give it a number higher than the default 30000 (in this case I chose 90000). Double click it and under Value data, type in explorer.exe.

Editing the registry

There's no need to restart after this. Go ahead and log out and log back in, and you should be greeted with the familiar Windows desktop.

Adding the Start Menu back in

It's not quite the full desktop experience though. Most notably, the start menu does not work at all. We can work around this by installing a 3rd party program called Open-Shell.

Open a Powershell window (you can do this by hitting Meta+R and running powershell) and enter in the following command:

wget https://github.com/Open-Shell/Open-Shell-Menu/releases/download/v4.4.190/OpenShellSetup_4_4_190.exe -outfile OpenShell.exe

Then type OpenShell.exe and run the installer. If you don't want the Explorer tweaks, make sure to deselect that feature before you proceed.

Now, when you click the Start Menu, it'll bring up the Open-Shell settings window. Make the changes you want, and you'll now have a classic, but fully functional Start Menu.

Start Menu

Installing a browser

If we want to be able to really use this as a desktop, we'll need a browser. The easiest one that I found I was able to get up and running was Vivaldi. Open up your Powershell window again and run the following command:

wget https://downloads.vivaldi.com/stable/Vivaldi.6.1.3035.257.x64.exe -outfile VivaldiSetup.exe

and then same as with Open-Shell, run VivaldiSetup.exe (be sure to select Advanced and in the drop down select Install for all users). After a short install, we'll now have a full web browser available to us.

Adding Users

We're still running as the default Administrator user, so you may want to create your own user profile instead. The usual Control Panel and Settings aren't available to us here, but we can create one by opening mmc.exe, clicking File > Add/Remove Snap In..., selecting Local Users and Groups in the left pane, and clicking Add >. Select Local Computer in the pop up dialog and hit Finish. You may get an error message that MMC could not initialize the snap-in, but I was just able to hit OK and it worked anyway. In the left pane, select Local Users and Groups, and then browse to Users. In the right pane, right click and select New User.... Fill out the fields as you'd like and click Create. The new user has been created, and you'll now be able to log out and log back in as your new user.

Final thoughts and additional steps

From here out, you should be fully equipped with a working desktop and a browser which will allow you to download and install whatever additional programs you'd like. Since our desktop is still missing some core functionality, I'd recommend some 3rd party tools like 7-zip to open archives, and Everything for local file search.

But there you have it, a (mostly) fully functional Windows desktop on a free Hyper-V Server 2019 machine. Special thanks to This reply on this Open-Shell issue for discovering the preliminary steps necessary to get this off the ground.

Hyper-V Server desktop