<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<updated>2026-02-07T14:57:18.703Z</updated>
<title>VinDuv’s blog</title>

<entry>
<id>upgrade-docker-psql.html</id>
<link rel="alternate" href="https://vinduv.app/upgrade-docker-psql.html"/>
<published>2026-02-07T10:30:28.888Z</published>
<title>Upgrade Docker PostgreSQL without export/import</title>
<author>
<name>vinduv</name>
<uri>https://vinduv.app/</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;upgrade-docker-psql.md&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;




&lt;p&gt;I have a &lt;a href=&quot;https://m.vinduv.app/&quot; rel=&quot;noopener noreferrer&quot;&gt;custom Mastodon instance&lt;/a&gt;, installed via three Docker containers (Mastodon itself, PostgreSQL, and Redis; I use &lt;a href=&quot;https://podman.io&quot; rel=&quot;noopener noreferrer&quot;&gt;Podman&lt;/a&gt; to run them, and a custom script to manage them).&lt;/p&gt;
&lt;p&gt;Updating the PostgreSQL container is tricky because when the major PostgreSQL version change, a manual database upgrade process is needed, which requires:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Separate database storage directories for the old and new versions&lt;/li&gt;
&lt;li&gt;Access to the PostgreSQL binaries of the old and new versions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Docker PostgreSQL image did neither of these requirements (&lt;a href=&quot;https://hub.docker.com/_/postgres#pgdata&quot; rel=&quot;noopener noreferrer&quot;&gt;storage directories are now separated&lt;/a&gt; as of version 18), so most “how to upgrade a PostgreSQL Docker image” just say “export the database, fully rebuild the container, restore the database”, but I wanted to perform a proper upgrade (and document it so I could do it again later).&lt;/p&gt;
&lt;h2&gt;Upgrade process&lt;/h2&gt;
&lt;p&gt;In the following commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_data&lt;/code&gt; is the Docker volume containing the PostgreSQL data&lt;/li&gt;
&lt;li&gt;OLD is the old PostgreSQL version, NEW is the new one&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;podman&lt;/code&gt; with &lt;code&gt;docker&lt;/code&gt; if that’s what you are using&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(The upgrade process assumes the database install user is &lt;code&gt;postgres&lt;/code&gt;; if it is not, use the -U option to pass the correct user to &lt;code&gt;pg_upgrade&lt;/code&gt;)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;(17-&amp;gt;18 migration only) Modify the docker-compose configuration/deploy script so that the PostgreSQL data volume is mounted to &lt;code&gt;/var/lib/postgresql/&lt;/code&gt; instead of &lt;code&gt;/var/lib/postgresql/data/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shut down the previous postgres container. Export the data storage volume, just to be sure.&lt;/li&gt;
&lt;li&gt;Start an ephemeral container to perform the migration (replace &lt;code&gt;pg_data&lt;/code&gt; with the PostgreSQL data volume):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ podman run --rm -ti -v pg_data:/var/lib/postgresql/ docker.io/postgres:NEW \
    /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;Install the previous version of PostgreSQL:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# apt update &amp;amp;&amp;amp; apt --no-install-recommends -y install postgresql-OLD
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Switch to postgresql user for the next commands (unset HISTFILE to avoid creating a &lt;code&gt;.bash_history&lt;/code&gt; file)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# su - postgres
$ unset HISTFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;(17-&amp;gt;18 migration only) Move all PostgreSQL data files into a &lt;code&gt;&amp;lt;version&amp;gt;/docker&lt;/code&gt; subfolder:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ mkdir -m 700 /var/lib/postgresql/17/ /var/lib/postgresql/17/docker/
$ shopt -s extglob
$ mv /var/lib/postgresql/!(17) /var/lib/postgresql/17/docker/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Initialize the new database, keep the old configuration&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ /usr/lib/postgresql/NEW/bin/initdb --no-data-checksums \
    /var/lib/postgresql/NEW/docker/
$ cp -p /var/lib/postgresql/OLD/docker/*.conf /var/lib/postgresql/NEW/docker/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;Perform the migration&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ /usr/lib/postgresql/NEW/bin/pg_upgrade -b /usr/lib/postgresql/OLD/bin/ \
    -d /var/lib/postgresql/OLD/docker/ -D /var/lib/postgresql/NEW/docker/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;Exit the temporary container (press Ctrl-D twice)&lt;/li&gt;
&lt;li&gt;Start the upgraded container normally (with docker-compose, etc). Check that everything works.&lt;/li&gt;
&lt;li&gt;Open a shell to the running container:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ podman exec -ti -u postgres -e HOME=/tmp &amp;lt;container name&amp;gt; /bin/bash
$ unset HISTFILE
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;10&quot;&gt;
&lt;li&gt;Cleanup the old database&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ /usr/lib/postgresql/NEW/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
$ /usr/lib/postgresql/NEW/bin/vacuumdb --all --analyze-in-stages
$ /var/lib/postgresql/delete_old_cluster.sh 
$ rmdir /var/lib/postgresql/OLD
$ rm /var/lib/postgresql/delete_old_cluster.sh 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Done!&lt;/p&gt;
&lt;p&gt;Note that data checksums (disabled by default in PostgreSQL 17 but enabled by default in PostgreSQL 18) are not enabled by this method. To enable them, run &lt;code&gt;/usr/lib/postgresql/NEW/bin/pg_checksums -e /var/lib/postgresql/NEW/docker/&lt;/code&gt;. When creating the new database during the next upgrade, do not pass the &lt;code&gt;--no-data-checksums&lt;/code&gt; option.&lt;/p&gt;
&lt;h2&gt;Upgrade check&lt;/h2&gt;
&lt;p&gt;Since upgrading between major PostgreSQL versions require manual intervention, I modified my Docker upgrade script to (1) only upgrade PostgreSQL to a fixed major version, (2) notify me if a newer version is available (i.e. &lt;code&gt;docker.io/postgres:latest&lt;/code&gt; is different from &lt;code&gt;docker.io/postgres:&amp;lt;specified version&amp;gt;&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PG_VER=17

podman pull -q &quot;docker.io/postgres:$PG_VER&quot; &amp;gt;/dev/null
podman pull -q &quot;docker.io/postgres:latest&quot; &amp;gt;/dev/null

pg_latest_id=&quot;$(podman image inspect &quot;docker.io/postgres:latest&quot; --format &quot;{{.Id}}&quot;)&quot; 
pg_cur_id=&quot;$(podman image inspect &quot;docker.io/postgres:$PG_VER&quot; --format &quot;{{.Id}}&quot;)&quot;

if [ &quot;${pg_latest_id}&quot; != &quot;${pg_cur_id}&quot; ]; then
	echo &quot;&quot;
	echo &quot;** PostgreSQL major version changed, perform a manual upgrade and change PG_VER&quot;
	echo &quot;&quot;
fi

# Rebuild PostgreSQL container using docker.io/postgres:$PG_VER
# Rebuild other containers…
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>thething1.html</id>
<link rel="alternate" href="https://vinduv.app/thething1.html"/>
<published>2025-01-20T07:08:57.243Z</published>
<title>“The thing”: An analysis of a weird classic Mac OS bug [Part 1]</title>
<author>
<name>vinduv</name>
<uri>https://vinduv.app/</uri>
</author>
<category term="classic mac os" /><category term="glitch" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;thething1.md&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;






&lt;p&gt;A long time ago, I read &lt;a href=&quot;https://web.archive.org/web/20031102055654fw_/http://gete.net/dossiers/thething/thething2.php&quot; rel=&quot;noopener noreferrer&quot;&gt;this article&lt;/a&gt; (in French) written by an ex-Apple support technician. It documented a weird bug that occurred in some versions of “classic” Mac OS (pre-Mac OS X) and had some interesting consequences. The author nicknamed the bug “The thing”, in reference to the 1982 film of the same name.&lt;/p&gt;
&lt;p&gt;For some reason (probably because of the weirdness of the bug), I remembered this article for all these years, and I recently decided to investigate now that I know a bit more how computers work ;)&lt;/p&gt;
&lt;p&gt;First, let’s set up the scene: Mac OS 8.5 running in the SheepShaver emulator. You will notice that I’m using the French version of Mac OS, and there is a reason for that. I’ll try to provide translations where needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/9e6bf645-448e-4df3-be4e-fa3e3ddfd3ea/desktop-normal.png&quot; alt=&quot;A screen capture of Mac OS 8., showing the desktop and the Extensions folder.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;For those who have never seen it, this is what classic Mac OS looked like. It’s not that different from modern Mac OS (now spelled macOS): you have a title bar at the top, showing the menus for the currently focused application and a clock, the desktop with the hard drive and some icons, the Trash at the bottom right corner of the screen (part of the desktop)… There is no Dock, instead you can put often-used applications in the Apple menu, and the menu in the top right is used to switch between running applications. The icon bar at the bottom left is called the Control Strip and provides access to commonly-used system settings like volume control (same thing as the icons on the right side of the menu bar in modern macOS).&lt;/p&gt;
&lt;p&gt;Unlike Mac OS X, classic Mac OS is not based on Unix and organizes its files very differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is no root volume (/); instead each volume is its own root, in a way.&lt;/li&gt;
&lt;li&gt;The system disk (called here Macintosh HD but can have any name) has a folder called “System Folder” (&lt;em&gt;Dossier Système&lt;/em&gt; in the French version) which contains most of the files needed for the system to function. Applications and user files can be put wherever on the disk (although in later versions of Mac OS it was standard practice to put applications in an Applications folders at the root of the disk).&lt;/li&gt;
&lt;li&gt;The System Folder contains various system files, most notably the System file (which hold most system code and resources) and the Finder (which is the same thing as the Finder in current Mac OS). It also contains folders, like Preferences (&lt;em&gt;Préférences&lt;/em&gt;, which holds settings files for the system and other applications) and Extensions.&lt;/li&gt;
&lt;li&gt;The Extensions folder contains files that are loaded at system startup and can extend the system in various ways (like to provide support for additional hardware, additional APIs for applications to use, or system customization).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The above capture shows the Extensions folder and the &lt;em&gt;Conversion encodages texte&lt;/em&gt; (Text Encoding Converter) extension. From what I can tell, this extension provides APIs to convert between text encodings. At this time, Unicode was relatively new, and OSes manipulated text using various incompatible encodings (in Europe, Mac OS used &lt;a href=&quot;https://en.wikipedia.org/wiki/Mac_OS_Roman&quot; rel=&quot;noopener noreferrer&quot;&gt;MacRoman&lt;/a&gt; and Windows used &lt;a href=&quot;https://en.wikipedia.org/wiki/Windows-1252&quot; rel=&quot;noopener noreferrer&quot;&gt;CP-1252&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Now, I’m going to drag this extension to the desktop, which will effectively disable it, and reboot. Since this is an extension, not a critical system file, the system should work fine without it. After all, pressing Shift at startup disables all extensions (so you can recover from a crashing extension), so disabling one of them should be fine, right?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/b1c1515c-3597-41c8-8ba6-69aabc1aa708/desktop-bad.png&quot; alt=&quot;The same Mac OS desktop, but the background is grey, there is a window in the background with a Mac OS logo and a progress bar, a Finder window showing the contents of the Macintosh HD disk, opened twice, and a row of icons on the bottom left.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;… what.&lt;/p&gt;
&lt;p&gt;You may notice several things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The desktop background is grey, there is a large “Mac OS” window with a progress bar, and a row of icons on the bottom left of the screen. What you’re seeing is the Mac OS boot screen.&lt;/li&gt;
&lt;li&gt;The “Macintosh HD” window seems to be opened twice. The classic Mac OS Finder does not allow that; what actually happened is that I opened the window and then dragged it to a different location.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What seem to be happening is that the system is failing to paint the background image. The boot screen and the phantom Finder window are just remnants of video memory that should have been replaced with the background, but weren’t.&lt;/p&gt;
&lt;p&gt;Another thing: the Control Strip is not displayed. And the Trash icon indicates that there is something in it, although it was empty before rebooting. What is there?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/716f6613-a217-4b97-87bc-a266725525f4/trash-contents.png&quot; alt=&quot;A Finder window showing the content of the Trash (named Corbeille in French): a single folder with no name. A second window shows the contents of the unnamed folder, a single file with no name.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;That’s a folder, with an empty name, containing a file (looking like a preference file), also with an empty name. Empty names are &lt;em&gt;not a thing&lt;/em&gt; on classic Mac OS; you can’t create a file named “.txt” and hide the extension, since there is no way to hide extensions in the first place (file extensions were not really used by Mac OS at that time).&lt;/p&gt;
&lt;p&gt;So this is very weird and smells like a filesystem corruption. There is also an unnamed file in the System Folder, and an unnamed folder in the Extensions folder:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/a4856037-5440-4bd0-bf76-0ba99a228ab0/unnamed-file-in-system-folder.png&quot; alt=&quot;A Finder window showing the contents of the System Folder; the selected file is unnamed and has the same icon as other system files.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/c07dfe71-44d2-4351-af8a-575f2ab6ceb8/unnamed-folder-in-extensions.png&quot; alt=&quot;A Finder window showing the contents of the Extensions folder; the selected folder is unnamed. It is opened, showing that it is empty.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;Okay, this is starting to look too much like a creepypasta. Let’s put the extension back in its place and reboot. That will fix things, right? Well, it fixes the desktop background and the Control Strip, but the unnamed files are still there.&lt;/p&gt;
&lt;p&gt;Maybe we can drop all these files into the Trash and empty it. Well, doing this in SheepShaver &lt;em&gt;crashes the emulator&lt;/em&gt;. I’m tempted to believe that on a real Mac, it would crash either the Finder or the entire system. (I’ve checked that dragging other files and folders work fine.)&lt;/p&gt;
&lt;p&gt;Let’s try to drop the files into a regular folder, instead of the Trash. I’m sure this is going to go well.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/75a515e0-e862-4a5e-ae8f-a32068306d19/drag-already-exists.png&quot; alt=&quot;A screen capture of an confirmation message, saying «&amp;nbsp;Il existe déjà un élément nommé “” à cet endroit. Souhaitez-vous le remplacer par celui que vous faites glisser&amp;nbsp;?&amp;nbsp;»&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;It says that the destination folder already contains an item with no name, that is going to be replaced. This is not going to end well, but let’s continue anyway.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/61237c93-de42-4dbc-8c43-6dbd7052e799/drag-dest-not-found.png&quot; alt=&quot;A screen capture of an error message, saying «&amp;nbsp;Impossible de déplacer “” vers le dossier “Some folder” car il est introuvable.&amp;nbsp;»&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The move failed because it cannot find the destination folder. Clicking OK reveals that &lt;em&gt;the destination folder has indeed disappeared, and its contents with it&lt;/em&gt;. Oops.&lt;/p&gt;
&lt;p&gt;Maybe we can at least get rid of the folder and the file in the Trash by emptying it?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/8d9bf84f-b89a-4bcc-abc7-e578522090b4/thrash-says-no.png&quot; alt=&quot;A screen capture of an error message, saying «&amp;nbsp;L&#x27;élément &amp;quot;&amp;quot; ne peut être supprimé car il contient des éléments en service. Souhaitez-vous continuer ?&amp;nbsp;»&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The message says that an item with no name cannot be deleted because it contains items “in use”. This usually means that an application has opened the file. I’m not sure that’s true, but that will prevent the Finder from deleting the file.&lt;/p&gt;
&lt;p&gt;What about fixing the empty names, by renaming the files? Attempting to rename the files by clicking on their names or selecting them and pressing Return fails: nothing happens. Opening their Information window does not help: the normally editable filename field is not editable there.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/db0253e7-b88a-4c87-ad39-3445c1c5a071/get-info-windows.png&quot; alt=&quot;Multiple Finder info windows, for the System file, the Finder file, and the unnamed files and folders. Only the info box for the Finder shows an editable field for the filename.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;Since this looks like filesystem corruption, let’s run the built-in filesystem checker (Disk First Aid, &lt;em&gt;S.O.S Disque&lt;/em&gt;). And…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/attachments/562b6152-5ca0-4090-bcd3-6e637f1e722d/disk-first-aid.png&quot; alt=&quot;The Disk First Aid window, after a scan of the startup disk, showing that no errors are detected.&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;It finds nothing!&lt;/p&gt;
&lt;p&gt;(I’ve also tried Norton Disk Doctor, but it hangs at startup, probably because of an issue with the emulator. Apparently, on a real Mac, it would find a weird error on the filesystem but fail to correct it.)&lt;/p&gt;
&lt;p&gt;Okay, all of this is very weird, but you know what’s weirder? How &lt;em&gt;specific&lt;/em&gt; this bug is. It only happens if the following conditions are met:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mac OS version strictly higher than 8.1 (actually Mac OS 8.1 also does weird things if the extension is removed but does not create nameless files and folders) and strictly lower than 9.0. (so basically Mac OS 8.5, 8.5.1 and 8.6, unless I’m missing something)&lt;/li&gt;
&lt;li&gt;Mac OS is installed on a volume using the HFS+ (Mac OS Extended) filesystem. If it’s installed on an HFS (Mac OS Standard) filesystem, the bug does not occur.&lt;/li&gt;
&lt;li&gt;A localized version of Mac OS is used. It happens with the French version of Mac OS (and probably with some other localized versions) but not with the US version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So what is going on? How can removing an extension create weird glitches and corrupt the file system in a way that is not detectable by some disk checkers? In the next post, I’ll do some filesystem inspection do try do determine what’s gone wrong. We’ll learn some things about HFS+ along the way.&lt;/p&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;classic mac os&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;glitch&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>10000001.html</id>
<link rel="alternate" href="https://vinduv.app/10000001.html"/>
<published>2024-11-17T17:30:22.841Z</published>
<title>How to reset Sidekiq failed tasks counter for Docker Mastodon</title>
<author>
<name>vinduv</name>
<uri>https://vinduv.app/</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;10000001.md&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;




&lt;p&gt;My Mastodon instance runs inside a Docker container, with its companion Sidekiq process. When I installed it, I messed up the network configuration, so the email sending tasks failed multiple times. After fixing the issue, I noticed that the Sidekiq control panel still read “Failed tasks: 38” and I really wanted to set this to zero (so I could more easily spot an issue in the future).&lt;/p&gt;
&lt;p&gt;I found &lt;a href=&quot;https://stackoverflow.com/a/46938936&quot; rel=&quot;noopener noreferrer&quot;&gt;some info&lt;/a&gt; on StackOverflow, but it took some tinkering to make it work in my case (Mastodon-specific sidekiq instance running in a Docker container).&lt;/p&gt;
&lt;p&gt;The first step was to open a shell in the context of the Docker container running Mastodon (note: I use &lt;code&gt;podman&lt;/code&gt;, but you can substitute &lt;code&gt;docker&lt;/code&gt; it that’s what you’re using):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ podman exec -ti mastodon /bin/bash
root@c24be7da02be:/# irb
irb(main):001&amp;gt; require &#x27;sidekiq/api&#x27;
&amp;lt;internal:/usr/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb&amp;gt;:136:in
    `require&#x27;: cannot load such file -- sidekiq/api (LoadError)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dang. Apparently, the sidekiq API is not accessible from the main Ruby process. I don’t know much about Ruby, but I guess the modules paths are set by environment variables, so I could just takes the one set by the sidekiq process…&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@c24be7da02be:/# ps -ef | grep &#x27;[s]idekiq&#x27;
root        41     1  0 Nov07 ?        00:00:00 s6-supervise svc-sidekiq
abc        712    41  0 Nov08 ?        00:30:09 sidekiq 6.5.12 www [0 of 5 busy]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, so sidekiq is PID 712. Since it’s running as user &lt;code&gt;abc&lt;/code&gt; (UID 1000 according to the &lt;code&gt;id&lt;/code&gt; command), let’s close this shell and open a new one using this user (and set HOME so bash does not complain):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ podman exec -ti --user 1000 -e HOME=/tmp  mastodon /bin/bash
abc@c24be7da02be:/$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can run &lt;code&gt;irb&lt;/code&gt; with all the environment variables set by PID 712:&lt;br&gt;
(Note: the following command will not work if any of the environment variables contain spaces; this is not the case here, fortunately)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abc@c24be7da02be:/$ env $(tr &#x27;\0&#x27; &#x27; &#x27; &amp;lt; /proc/712/environ) irb
/app/www/vendor/bundle/ruby/3.3.0/gems/irb-1.14.1/lib/irb/history.rb:51:in
    `stat&#x27;: Permission denied @ rb_file_s_stat - /root/.irb_history
    (Errno::EACCES)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So close yet so far. After looking into it, it appears that despite running as user &lt;code&gt;abc&lt;/code&gt;, process 712 has &lt;code&gt;HOME=/root&lt;/code&gt; in its environment variables, so &lt;code&gt;irb&lt;/code&gt; looks for its history file there. (I’m a bit surprised that “not being able to read the history file” is a fatal error for &lt;code&gt;irb&lt;/code&gt;, by the way)&lt;/p&gt;
&lt;p&gt;So let’s override &lt;code&gt;HOME&lt;/code&gt; again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abc@c24be7da02be:/$ env $(tr &#x27;\0&#x27; &#x27; &#x27; &amp;lt; /proc/712/environ) HOME=/tmp irb
irb(main):001&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally! Let’s import the module and run the command now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irb(main):001&amp;gt; require &#x27;sidekiq/api&#x27;
=&amp;gt; true
irb(main):002&amp;gt; Sidekiq::Stats.new.reset(&#x27;failed&#x27;)
/app/www/vendor/bundle/ruby/3.3.0/gems/redis-4.8.1/lib/redis/client.rb:398:in
    `rescue in establish_connection&#x27;: Error connecting to Redis on
    127.0.0.1:6379 (Errno::ECONNREFUSED) (Redis::CannotConnectError)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, the Sidekiq API is trying to connect to the Redis database&lt;code&gt;localhost&lt;/code&gt;, but Redis actually runs on another Docker container (creatively named &lt;code&gt;mast_redis&lt;/code&gt; in my case). I guess that when Mastodon starts sidekiq, it provides it with the Redis address in some way, and we need to emulate that.&lt;/p&gt;
&lt;p&gt;After looking in &lt;code&gt;lib/sidekiq.rb&lt;/code&gt;, I found a comment explaining how to configure the connection, so I tried it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irb(main):001&amp;gt; require &#x27;sidekiq/api&#x27;
=&amp;gt; true
irb(main):002* Sidekiq.configure_client do |config|
irb(main):003*   config.redis = { size: 1, url: &#x27;redis://mast_redis:6379/0&#x27; }
irb(main):004&amp;gt; end
=&amp;gt; {:size=&amp;gt;1, :url=&amp;gt;&quot;redis://mast_redis:6379/0&quot;}
irb(main):005&amp;gt; Sidekiq::Stats.new.reset(&#x27;failed&#x27;)
=&amp;gt; &quot;OK&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked! I now have zero failed sidekiq tasks. Let’s hope they add a reset button later…&lt;/p&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>10000000.html</id>
<link rel="alternate" href="https://vinduv.app/10000000.html"/>
<published>2024-11-17T16:06:03.034Z</published>
<title>So I finally made a blog</title>
<author>
<name>vinduv</name>
<uri>https://vinduv.app/</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;10000000.md&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;




&lt;p&gt;I’m not much of a &lt;em&gt;poster&lt;/em&gt;, but I did some posts on Cohost over the years and I enjoyed it! I wanted to keep them online after the shutdown and do more in the future (I have some drafts ready).&lt;br&gt;
I decided to use the &lt;a href=&quot;https://github.com/delan/autost&quot; rel=&quot;noopener noreferrer&quot;&gt;autost&lt;/a&gt; blog engine, as it’s compatible with Cohost posts and doesn’t require too much tinkering on my Web server.&lt;br&gt;
I’ll only do long posts here; see my &lt;a href=&quot;https://m.vinduv.app/&quot; rel=&quot;noopener noreferrer&quot;&gt;Mastodon&lt;/a&gt; for the shitposts.&lt;/p&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>6715452.html</id>
<link rel="alternate" href="https://vinduv.app/6715452.html"/>
<published>2024-07-03T19:40:14.238Z</published>
<title>Python footgun: __getattr__</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>
<category term="python" /><category term="programming" /><category term="footgun" /><category term="property getters really should not have side effects i insist" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;6715452.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;











&lt;p&gt;Python classes can implement a special method &lt;code&gt;__getattr__&lt;/code&gt; that is called during attribute access if an attribute cannot be found “normally”. This can be useful to provide fallback values if an attribute is not defined:&lt;/p&gt;
&lt;pre style=&quot;background-color:rgb(238, 238, 238);color:rgb(0, 0, 0);position:relative;padding:0;line-height:1.2rem&quot;&gt;&lt;code style=&quot;padding:0 16px;display:block;margin-bottom:9px;margin-top:9px&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;class&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;DefaultAttr&lt;/span&gt;:&lt;/span&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;__getattr__&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(self, key)&lt;/span&gt;:&lt;/span&gt;
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;try&lt;/span&gt;:
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; self.DEFAULT_VALS[key]
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;except&lt;/span&gt; KeyError:
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;raise&lt;/span&gt; AttributeError(&lt;span style=&quot;color: rgb(221, 17, 68)&quot;&gt;f&quot;&lt;span style=&quot;&quot;&gt;{self.__class__.__name__!r}&lt;/span&gt; object &quot;&lt;/span&gt;
                &lt;span style=&quot;color: rgb(221, 17, 68)&quot;&gt;f&quot;has no attribute &lt;span style=&quot;&quot;&gt;{key!r}&lt;/span&gt;&quot;&lt;/span&gt;) &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;from&lt;/span&gt; &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;None&lt;/span&gt;

&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;class&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;Sample&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(DefaultAttr)&lt;/span&gt;:&lt;/span&gt;
    DEFAULT_VALS = {&lt;span style=&quot;color: rgb(221, 17, 68)&quot;&gt;&#x27;x&#x27;&lt;/span&gt;: &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;42&lt;/span&gt;}

s = Sample()
print(s.x)  &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# 42&lt;/span&gt;
s.x = &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;25&lt;/span&gt;
print(s.x)  &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# 25&lt;/span&gt;
print(s.y)  &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# AttributeError: &#x27;Sample&#x27; object has no attribute &#x27;y&#x27;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Sample&lt;/code&gt; objects behave like normal objects, except that if a missing attribute is accessed that is present in the &lt;code&gt;DEFAULT_VALS&lt;/code&gt; class dictionary, the default value is returned instead. If the attribute is present on the object, &lt;code&gt;__getattr__&lt;/code&gt;is not called and the code proceeds normally.&lt;/p&gt;
&lt;p&gt;Note that if &lt;code&gt;DEFAULT_VALS&lt;/code&gt; does not provide a default values, &lt;code&gt;__getattr__&lt;/code&gt; raises an &lt;code&gt;AttributeError&lt;/code&gt; which mimics the normal &lt;code&gt;AttributeError&lt;/code&gt; raised by Python if the `&lt;strong&gt;getattr&lt;/strong&gt; is not defined.&lt;/p&gt;
&lt;p&gt;So all of this is fine, right? Well…&lt;/p&gt;
&lt;pre style=&quot;background-color:rgb(238, 238, 238);color:rgb(0, 0, 0);position:relative;padding:0;line-height:1.2rem&quot;&gt;&lt;code style=&quot;padding:0 16px;display:block;margin-bottom:9px;margin-top:9px&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;class&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;Sample2&lt;/span&gt;(&lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;DefaultAttr&lt;/span&gt;):&lt;/span&gt;
    DEFAULT_VALS = {&lt;span style=&quot;color: rgb(221, 17, 68)&quot;&gt;&#x27;x&#x27;&lt;/span&gt;: &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;42&lt;/span&gt;}

    &lt;a href=&quot;https://cohost.org/staticmethod&quot; rel=&quot;noopener noreferrer&quot;&gt;@staticmethod&lt;/a&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;cur_time&lt;/span&gt;&lt;span style=&quot;&quot;&gt;()&lt;/span&gt;&lt;/span&gt;:
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; datetime.daettime.now()

    &lt;a href=&quot;https://cohost.org/property&quot; rel=&quot;noopener noreferrer&quot;&gt;@property&lt;/a&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;prop1&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;:
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;123&lt;/span&gt;

    &lt;a href=&quot;https://cohost.org/property&quot; rel=&quot;noopener noreferrer&quot;&gt;@property&lt;/a&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;prop2&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;:
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;.cur_time()

print(Sample2().prop1)  &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# 123&lt;/span&gt;
print(Sample2().prop2)  &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# AttributeError: &#x27;Sample2&#x27; object &lt;/span&gt;
                        &lt;span style=&quot;color: rgb(153, 153, 136);font-style: italic&quot;&gt;# has no attribute &#x27;prop2&#x27; ??&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What is going on here? &lt;code&gt;prop2&lt;/code&gt; is a property, same as &lt;code&gt;prop1&lt;/code&gt;, they are defined on the class so why do we get an AttributeError when calling it? Well, &lt;a href=&quot;https://docs.python.org/3/reference/datamodel.html#object.__getattr__&quot; rel=&quot;noopener noreferrer&quot;&gt;the documentation&lt;/a&gt; doesn’t exactly says that &lt;code&gt;__getattr__&lt;/code&gt; is called &lt;em&gt;when the attribute is not found&lt;/em&gt; &lt;sup&gt;1&lt;/sup&gt;, it says that it’s called &lt;em&gt;when accessing the attribute fails with an AttributeError&lt;/em&gt;. And &lt;code&gt;AttributeError&lt;/code&gt; can come from many places…&lt;/p&gt;
&lt;p&gt;The issue is that when &lt;code&gt;.prop2&lt;/code&gt; is accessed on a &lt;code&gt;Sample2&lt;/code&gt; object, the &lt;code&gt;prop2&lt;/code&gt; method is called. This method calls &lt;code&gt;cur_time&lt;/code&gt;, and &lt;code&gt;cur_time&lt;/code&gt; &lt;em&gt;raises an AttributeError because of a typo&lt;/em&gt; (“daettime”). Since accessing that property raised an AttributeError, Python calls &lt;code&gt;__getattr__&lt;/code&gt;, which doesn’t find &lt;code&gt;prop2&lt;/code&gt; in the default values dictionary and raises &lt;em&gt;its own version of AttributeError&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So this results in a confusing error message (which the version of Python I’m using “helpfully” improves by adding “Did you mean: &#x27;prop1&#x27;?” at the end when displaying it), and the original AttributeError is lost with its traceback.&lt;/p&gt;
&lt;p&gt;After stumbling onto this, I found &lt;a href=&quot;https://github.com/python/cpython/issues/103936&quot; rel=&quot;noopener noreferrer&quot;&gt;this issue&lt;/a&gt; about this subject, but this is mostly a non-useful discussion about wether this is a bug or a feature request.&lt;/p&gt;
&lt;p&gt;I personally think confusing error messages are bad, especially when they cause the loss of useful debug information.&lt;/p&gt;
&lt;p&gt;I’ve found two ways to improve the situation. The first one is to not raise an AttributeError when wanting “default behavior”, but call &lt;code&gt;object.__getattribute__&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre style=&quot;background-color:rgb(238, 238, 238);color:rgb(0, 0, 0);position:relative;padding:0;line-height:1.2rem&quot;&gt;&lt;code style=&quot;padding:0 16px;display:block;margin-bottom:9px;margin-top:9px&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;class&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;DefaultAttr&lt;/span&gt;:&lt;/span&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;__getattr__&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;, key)&lt;/span&gt;&lt;/span&gt;:
        &lt;span style=&quot;color: rgb(153, 0, 115)&quot;&gt;try:&lt;/span&gt;
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;.DEFAULT_VALS[key]
        except &lt;span style=&quot;color: rgb(153, 0, 115)&quot;&gt;KeyError:&lt;/span&gt;
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; object.__getattribute_&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;_&lt;/span&gt;(&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;self&lt;/span&gt;, key)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this code, the following happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The code accesses &lt;code&gt;.prop2&lt;/code&gt;, which raises an AttributeError&lt;/li&gt;
&lt;li&gt;Python turns around and calls &lt;code&gt;__getattr__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__getattr__&lt;/code&gt; wants to perform default behavior so it calls &lt;code&gt;object.__getattribute__&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This results in &lt;code&gt;.prop2&lt;/code&gt; being accessed again, which again raises an AttributeError&lt;/li&gt;
&lt;li&gt;Since the access is done by the lower-level &lt;code&gt;__getattribute__&lt;/code&gt; function, the object &lt;code&gt;__getattr__&lt;/code&gt; is ignored&lt;/li&gt;
&lt;li&gt;The exception from the property is correctly thrown to the caller, with full traceback&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The obvious disadvantage of this method is that the property is called twice if it fails with an AttributeError. I don’t think it’s an issue because 1) property getters should not have side effects, 2) if your property getter has side effects, it should hopefully not have side effects when raising an exception, and 3) you should rarely catch AttributeError and let the program quickly die if it’s raised anyway.&lt;/p&gt;
&lt;p&gt;Another method is to use &lt;em&gt;gasp&lt;/em&gt; &lt;code&gt;__getattribute__&lt;/code&gt;:&lt;/p&gt;
&lt;pre style=&quot;background-color:rgb(238, 238, 238);color:rgb(0, 0, 0);position:relative;padding:0;line-height:1.2rem&quot;&gt;&lt;code style=&quot;padding:0 16px;display:block;margin-bottom:9px;margin-top:9px&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;class&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0);color: rgb(68, 85, 136)&quot;&gt;DefaultAttr&lt;/span&gt;:&lt;/span&gt;
    &lt;span style=&quot;&quot;&gt;&lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;def&lt;/span&gt; &lt;span style=&quot;color: rgb(153, 0, 0)&quot;&gt;__getattribute__&lt;/span&gt;&lt;span style=&quot;&quot;&gt;(self, key)&lt;/span&gt;:&lt;/span&gt;
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;try&lt;/span&gt;:
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; super().__getattribute__(key)
        &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;except&lt;/span&gt; AttributeError &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;as&lt;/span&gt; err:
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;try&lt;/span&gt;:
                &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;return&lt;/span&gt; self.DEFAULT_VALS[key]
            &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;except&lt;/span&gt; KeyError:
                &lt;span style=&quot;color: rgb(0, 153, 153)&quot;&gt;raise&lt;/span&gt; err&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this, we basically emulate what Python does when accessing a property (with the &lt;code&gt;super().__getattribute__(key)&lt;/code&gt; call), but instead of immediately throwing away any AttributeError that occurs, we store it and re-raise it if no default value is available. This has the advantage of calling the property only once even if it fails, but it requires the use of &lt;code&gt;__getattribute__&lt;/code&gt; which is called for &lt;em&gt;every&lt;/em&gt; attribute access on the object. That means that if you mess up, you will end up with it being called recursively until your program dies on a stack overflow. It probably makes every property access on the object a little bit slower, too.&lt;/p&gt;
&lt;p&gt;My conclusion is that while Python is a great programming language, some parts of its API are sub-optimal and cause issues&lt;sup&gt;2&lt;/sup&gt;, and this is one of it. I think the best way to solve this in a backwards compatible matter would be to add a constant to the language, maybe called &lt;code&gt;AttributeNotImplemented&lt;/code&gt; (to be similar with the &lt;code&gt;NotImplemented&lt;/code&gt; return value). &lt;code&gt;__getattr__&lt;/code&gt; could return this value when it wants “default processing” to occur (i.e. let the original AttributeError exception be propagated).&lt;/p&gt;
&lt;p&gt;It is overkill to add a constant just for this? Maybe, but I think it would be justified in that case.&lt;/p&gt;
&lt;hr aria-label=&quot;Footnotes&quot; style=&quot;margin-bottom: -0.5rem;&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/2.7/reference/datamodel.html#object.__getattr__&quot; rel=&quot;noopener noreferrer&quot;&gt;it used to say that in Python 2&lt;/a&gt;, but this was apparently changed for Python 3 &lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-2&quot;&gt;
&lt;p&gt;My personal list of Python footguns that I struggle with regularly: strings are iterable, bytes objects can be silently converted to strings, and f-strings require that f&quot;&quot; prefix. &lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;python&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;programming&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;footgun&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;property getters really should not have side effects i insist&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>5832382.html</id>
<link rel="alternate" href="https://vinduv.app/5832382.html"/>
<published>2024-05-04T10:32:33.892Z</published>
<title>Little Python trick: Type-safe object extensions</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>
<category term="python" /><category term="type checking" /><category term="mypy" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;5832382.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;










&lt;p&gt;I’m currently playing with type annotation and static type checking in Python (using &lt;a href=&quot;https://mypy.readthedocs.io/&quot; rel=&quot;noopener noreferrer&quot;&gt;mypy&lt;/a&gt;) and came across a somewhat interesting problem: How to allow extending an object in a type-safe manner?&lt;/p&gt;
&lt;p&gt;Suppose that you have an object where extension modules can add attributes: for instance, the Django Web framework’s &lt;a href=&quot;https://docs.djangoproject.com/en/5.0/topics/http/sessions/&quot; rel=&quot;noopener noreferrer&quot;&gt;sessions&lt;/a&gt; middleware adds a &lt;code&gt;session&lt;/code&gt; attribute on each incoming &lt;code&gt;request&lt;/code&gt; object so that views can use the session.&lt;/p&gt;
&lt;p&gt;This approach does not work with static type checking: For the the type checker, a &lt;code&gt;Request&lt;/code&gt; object has a fixed number of attributes, and &lt;code&gt;session&lt;/code&gt; is not one of them. The &lt;code&gt;session&lt;/code&gt; attribute cannot be hard-coded into the request object because anybody can write a new middleware that will also have this problem.&lt;/p&gt;
&lt;p&gt;A solution can be to add a generic dictionary to the &lt;code&gt;Request&lt;/code&gt; object so that extensions can add their data. The values of this dictionary cannot be bound to a single type, since each extension will need to add its own type of data, so it will be typed &lt;code&gt;Any&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Request:
	ext_values: dict[str, Any]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the session middleware can do &lt;code&gt;request.ext_values[&#x27;session&#x27;] = …&lt;/code&gt; and the view code can use it without issues. However, for the type checker, &lt;code&gt;request.ext_values[&#x27;session&#x27;]&lt;/code&gt; is an untyped (&lt;code&gt;Any&lt;/code&gt;) object, which means that it cannot do any typing verification on the object. Typos like &lt;code&gt;request.ext_values[&#x27;session&#x27;].usre&lt;/code&gt; will not be detected.&lt;/p&gt;
&lt;p&gt;The view code can also cast the session object from Any to its actual type, but that’s not very pretty, and could be error-prone.&lt;/p&gt;
&lt;p&gt;So how can we get the type checker to understand “If I access this dictionary with this key, it will always return an object of this exact type”? By using the type as a dictionary key!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T = TypeVar(&#x27;T&#x27;)

class Request:
    _ext_values: dict[type, Any] = {}

    def get_ext(self, val_type: Type[T]) -&amp;gt; T:
        return cast(T, self._ext_values[val_type])

    def set_ext(self, value: object) -&amp;gt; None:
        self._ext_values[type(value)] = value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may require a little explanation if you are not familiar with Python’s metaprogramming capabilities. If you write &lt;code&gt;class A:&lt;/code&gt;, the expression &lt;code&gt;A&lt;/code&gt; is itself an object (usually an instance of the &lt;code&gt;type&lt;/code&gt; class), and this object work like most objects. Most notably for our case, we can compare classes (they are all different from each other) and they have a hash value, so they can be used as dictionary keys:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class A:
 pass

class B:
 pass

d = {
 A: &#x27;a&#x27;
}

d[B] = &#x27;b&#x27; # d now contains 2 values
print(d[A], d[B]) # prints &#x27;a&#x27;, &#x27;b&#x27;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now let’s see what happen on the &lt;code&gt;Request&lt;/code&gt; object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Session middleware code
session = Session(…)
request.set_ext(session)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;set_ext&lt;/code&gt; calls &lt;code&gt;type()&lt;/code&gt; on the provided &lt;code&gt;session&lt;/code&gt; object, which returns the class object (here, &lt;code&gt;Session&lt;/code&gt;), and uses it as a key to store the session object. Now we have &lt;code&gt;_ext_values == {Session: session}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the view code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user = request.get_ext(Session).user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, the &lt;code&gt;Session&lt;/code&gt; class is passed to &lt;code&gt;get_ext&lt;/code&gt;. Since it exists in the dictionary, &lt;code&gt;get_ext&lt;/code&gt; returns the stored &lt;code&gt;session&lt;/code&gt; object, and user code can retrieve the &lt;code&gt;user&lt;/code&gt; object inside it. The important parts are the type annotations: the &lt;code&gt;-&amp;gt; T&lt;/code&gt; means “I return an object of generic type T” and the &lt;code&gt;: Type[T]&lt;/code&gt; means “I take an argument which is a class object, and this class object is T”&lt;/p&gt;
&lt;p&gt;This means that when the type checker sees the &lt;code&gt;request.get_ext(Session)&lt;/code&gt; code, it know that this expression returns an instance of &lt;code&gt;Session&lt;/code&gt;, and can check that it actually has a &lt;code&gt;user&lt;/code&gt; attribute. Type safety achieved!&lt;/p&gt;
&lt;p&gt;One cool thing is that despite the cast done on object coming out of the dictionary, this is type safe: as long as nobody messes up with &lt;code&gt;_ext_values&lt;/code&gt; directly, &lt;code&gt;get_ext&lt;/code&gt; will always return an instance of the type passed in, or a KeyError. Other extensions cannot accidentally erase a value set by another extension (as long as they use distinct types).&lt;/p&gt;
&lt;p&gt;Note that it is pretty simple to extend this scheme to multiple values per class, still in a type-safe manner: add a string key, and use a (type, string key) tuple as the dictionary key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class HTTPRequest:
    _ext_values: dict[tuple[type, str], Any] = {}

    def get_ext(self, val_type: Type[T], key: str) -&amp;gt; T:
        return cast(T, self._ext_values[(val_type, key)])

    def set_ext(self, key: str, value: object) -&amp;gt; None:
        self._ext_values[(type(value), key)] = value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the session code does &lt;code&gt;request.set_ext(&#x27;session&#x27;, Session(...))&lt;/code&gt; and the view code does &lt;code&gt;request.get_ext(Session, &#x27;session&#x27;)&lt;/code&gt;. The use of both the type and the string key to index values in the dictionary means that type safety is preserved: If another extension does `request.set_ext(&#x27;session&#x27;, AnotherObject(...)), the dictionary will contain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	(Session, &#x27;session&#x27;): &amp;lt;the session object&amp;gt;,
	(AnotherObject, &#x27;session&#x27;): &amp;lt;the other object&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and &lt;code&gt;request.get_ext(Session, &#x27;session&#x27;)&lt;/code&gt; will still return the session object.&lt;/p&gt;
&lt;p&gt;Note that I didn’t invent this technique; it’s rarely used in the wild, because it requires a programming language where types are first-class objects (so we can use them as dictionary keys) and that perform static type checking. I’m pretty sure I’ve seen it used in Swift (which has both characteristics).&lt;/p&gt;&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;python&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;type checking&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;mypy&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>5557821.html</id>
<link rel="alternate" href="https://vinduv.app/5557821.html"/>
<published>2024-04-14T08:39:37.065Z</published>
<title>Walls, Floors, Ceilings, and Game Development </title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>
<category term="pannenkoek2012" /><category term="super mario 64" /><category term="gamedev" /><category term="Going to die ripped apart by angry gamers" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;5557821.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;











&lt;p&gt;I watched the live-streaming of &lt;a href=&quot;https://www.youtube.com/@pannenkoek2012&quot; rel=&quot;noopener noreferrer&quot;&gt;pannenkoek2012&lt;/a&gt;’s latest video, &lt;a href=&quot;https://www.youtube.com/watch?v=YsXCVsDFiXA&quot; rel=&quot;noopener noreferrer&quot;&gt;SM64’s Invisible Walls Explained Once and for All&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It was a pretty fun and instructive video, and the chat’s reactions were very funny too&lt;sup&gt;1&lt;/sup&gt;. But of course, there were some negative comments, and I’m pretty sure not all of them are satire, since, unfortunately, yelling at game developpers is a pretty common Internet activity.&lt;/p&gt;
&lt;p&gt;So let’s get the record straight (I doubt people will actually read this but this helps getting it out of my system):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Computers are not like humans, things that developpers did that seem “more complicated” than an alternative (for instance, having ceilings that extend infinitely upwards instead of having a limited range) are actually easier to do (in the ceiling example, it means that the CPU has to do one comparison to detect that Mario is in the ceiling instead of two). I’ve also seen similar reactions to hardware design: “Why did they made RAM appear at two address space locations and then ask you not to touch one of the location?” Because it saved them a chip.&lt;/li&gt;
&lt;li&gt;Computers are not like humans, they have no eyeballs, they cannot “see” that a floor is higher that another floor. Iterating over all the floors ordered by height until you find the first one lower than Mario is a reasonable thing to do. (Okay, there may be more sophisticated algorithms that are more efficient, but the point is that the computer has to run algorithms to determine things, because that’s the only thing computers can do)&lt;/li&gt;
&lt;li&gt;It it said that a game receives 100 (1000?) times more QA in the first hour of its release than during all of its development; and it makes sense given when you compare the number of players with the number of QA testers. But of course, many gamers do not realize that.&lt;/li&gt;
&lt;li&gt;QA testers have more trouble seeing issues with invisible things (the invisible walls, the impossible coin, non-solid walls, misaligned loading zones in other games, etc) because &lt;em&gt;they are invisible&lt;/em&gt;. Sure, fans have written debug tools that help find them (and speedrunners sometimes found them by accident), but the original developpers did not have time for that. Bugs regarding &lt;em&gt;visible&lt;/em&gt; things have a higher change to be detected and fixed.&lt;/li&gt;
&lt;li&gt;Pannen’s video is almost 4 hours long and is about the &lt;em&gt;collision detection&lt;/em&gt; code of SM 64. And not even all about it! It’s probably not even 1% of the game code. So yeah, it’s imperfect, corners were cut, but developpers had many, many other things to make work in order to release the game. (And personally, as a non-game programmer, I find collision detection to be pretty much magic, especially in 3D. I have absolutely no idea how you check if &lt;em&gt;a point is over a triangle&lt;/em&gt;, which is the basis of everything Pannen has talked about in his video)&lt;/li&gt;
&lt;li&gt;It didn’t happen too much but some comparisons were made with a recent game, where a commentator said that it was OK for SM64 to have “bugs” but not the more other game. Guess what, it’s actually the same thing: Relatively small team of developpers, big ambition, extremely tight, non-extensible timeframe (for reasons outside of the developper’s control). There will be cut corners, and you may not appreciate them, but other people will enjoy the game anyway because of the other things that were made right and make the game enjoyable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And… that’s it! Don’t forget to watch Pannen’s video (if you have 3.75 free hours), and support him if you can.&lt;/p&gt;
&lt;p&gt;Also, to speedrunners: when talking about a glitch that helps you beat the game faster, please don’t talk about it like it’s the worst thing it the world, and especially don’t say “I have no idea why they did something this stupid”. Try to explain it in the context of game development.&lt;/p&gt;
&lt;hr aria-label=&quot;Footnotes&quot; style=&quot;margin-bottom: -0.5rem;&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-1&quot;&gt;
&lt;p&gt;Special mention to people that noticed that Pannen was repeating “So let’s look at it from the side and let’s look at it from above” and started posting it in all caps in the chat every time it was said again, it actually made me laugh out loud &lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;pannenkoek2012&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;super mario 64&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;gamedev&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;Going to die ripped apart by angry gamers&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>3640506.html</id>
<link rel="alternate" href="https://vinduv.app/3640506.html"/>
<published>2023-11-23T21:34:12.524Z</published>
<title>PSA: Python SSL wrap_socket has a bad default behavior for servers</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;3640506.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;







&lt;p&gt;The &lt;a href=&quot;https://docs.python.org/3/library/ssl.html#ssl.SSLContext.wrap_socket&quot; rel=&quot;noopener noreferrer&quot;&gt;SSLContext.wrap_socket&lt;/a&gt; method has an optional &lt;code&gt;do_handshake_on_connect&lt;/code&gt; argument. When &lt;code&gt;True&lt;/code&gt; (the default), it performs the TLS handshake (where the client and server agree on a protocol, exchange certificates, etc) directly after the socket is connected.&lt;br&gt;
This is fine for &lt;em&gt;client&lt;/em&gt; sockets (as long as you are prepared to handle SSL errors when calling &lt;code&gt;socket.connect&lt;/code&gt; — they are a subclass of &lt;code&gt;OSError&lt;/code&gt; so that is not too difficult). But on &lt;em&gt;server&lt;/em&gt; sockets, the handshake is performed on the newly connected socket returned by &lt;a href=&quot;https://docs.python.org/3/library/socket.html#socket.socket.accept&quot; rel=&quot;noopener noreferrer&quot;&gt;socket.accept&lt;/a&gt;, before it’s actually returned.&lt;br&gt;
This causes two problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a client fails the TLS handshake, &lt;code&gt;accept&lt;/code&gt; will throw a &lt;code&gt;SSLError&lt;/code&gt;. You can catch it, but you will not know the address of the client that caused the error, because this address is normally &lt;em&gt;returned&lt;/em&gt; by &lt;code&gt;accept&lt;/code&gt; and &lt;code&gt;accept&lt;/code&gt; threw instead of returning.&lt;/li&gt;
&lt;li&gt;If a client connects and then proceeds to do nothing, the SSL library will wait indefinitely for the handshake to proceed. Your code stays blocked in &lt;code&gt;accept&lt;/code&gt; and you cannot handle more clients while this happens! You may think that you could set a timeout on the listening socket, but &lt;em&gt;this timeout is not transferred to the connected socket&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you know that, the solution is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;do_handshake_on_connect=False&lt;/code&gt; when wrapping the socket&lt;/li&gt;
&lt;li&gt;Once &lt;code&gt;accept&lt;/code&gt; returns a socket, set a timeout on it (or set it to non-blocking and use asynchronous I/O), then call &lt;code&gt;do_handshake&lt;/code&gt;. If that fails with a &lt;code&gt;SSLError&lt;/code&gt; (or a socket error…), you can properly log it since &lt;code&gt;accept&lt;/code&gt; returned the address of the client.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>3011511.html</id>
<link rel="alternate" href="https://vinduv.app/3011511.html"/>
<published>2023-09-29T20:39:21.799Z</published>
<title>TIL that /bin/sh -c has subtle portability issues</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>
<category term="python" /><category term="shell" /><category term="code portability" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;3011511.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;










&lt;p&gt;I tried to use a Python auto-reloader program and found that it ran correctly under macOS but was failing to correctly restart the monitored program under Linux (Debian, to be specific).&lt;/p&gt;
&lt;p&gt;After some investigation, I found that it used &lt;code&gt;subprocess.Popen&lt;/code&gt; with &lt;code&gt;shell=True&lt;/code&gt;; this basically runs &lt;code&gt;/bin/sh -c &amp;lt;specified command&amp;gt;&lt;/code&gt;, which itself runs the command. We initially get the following process hierarchy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PID X] python
 \_ [PID Y] /bin/sh -c &amp;lt;specified command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But then things start to differ depending on shell used. If &lt;code&gt;/bin/sh&lt;/code&gt; is &lt;code&gt;zsh&lt;/code&gt; (default under macOS) or &lt;code&gt;bash&lt;/code&gt;, the shell directly replaces itself with the command being run (using &lt;code&gt;exec&lt;/code&gt;), and we get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PID X] python
 \_ [PID Y] &amp;lt;specified command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if /bin/sh is &lt;code&gt;dash&lt;/code&gt; (default under Debian), it runs the command in a subprocess and waits for it to exit. This gives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[PID X] python
 \_ [PID Y] /bin/sh -c &amp;lt;specified command&amp;gt;
     \_ [PID Z] &amp;lt;specified command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this is were things break if if the Python program calls &lt;code&gt;kill()&lt;/code&gt; to kill the started process. In both cases, it will send a &lt;code&gt;KILL&lt;/code&gt; signal to PID Y; under macOS, this will kill the process, but under Debian it will kill the intermediate shell and leave the program running!&lt;/p&gt;
&lt;p&gt;Moral of the story:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Portability is hard&lt;/li&gt;
&lt;li&gt;This is &lt;em&gt;yet another&lt;/em&gt; reason to not use shell=True in Python programs!&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;python&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;shell&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;code portability&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>2627587.html</id>
<link rel="alternate" href="https://vinduv.app/2627587.html"/>
<published>2023-08-26T08:38:04.673Z</published>
<title>Configuration management with git on Debian</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;2627587.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;







&lt;p&gt;Ah, configuration files. The place there’s a guaranteed opportunity of edit conflict (between the package author, and you), and no really satisfying solution.&lt;/p&gt;
&lt;p&gt;My understanding is that, on RPM-based distributions, if a configuration file is modified by the user, it stops being updated. Permanently. On Debian and derivatives, it asks you a question with no good answer: “Do you want to lose your configuration changes, or get a non-working software?”.&lt;/p&gt;
&lt;p&gt;With most packages, it’s annoying, but not too bad; you can often put your configuration changes in an override file, that will be untouched by package upgrades and it should keep working. But I happen to use &lt;em&gt;Dovecot&lt;/em&gt;, an IMAP server which has a complicated configuration (in many files), which cannot really be configured in overrides, and which changes pretty often.&lt;/p&gt;
&lt;p&gt;I decided to tackle the problem once and for all.&lt;/p&gt;&lt;hr&gt;
&lt;h2&gt;The trick&lt;/h2&gt;
&lt;p&gt;I want to put the Dovecot configuration in a Git repository, with two branches: one for the default distribution configuration, and one for my customized configuration. When the Dovecot package is upgraded, it will put the new configuration files on the “distribution” branch. I can them commit the changes, then merge (or rebase) them on my customized branches. That way, I have more control over the process.&lt;/p&gt;
&lt;p&gt;The problem is that I need to convince the installer to write the files on a Git branch. Fortunately, Git has a &lt;em&gt;worktree&lt;/em&gt; feature that multiple branches to be checked out simultaneously in different directories. I can have my customized branch in &lt;code&gt;/etc/dovecot&lt;/code&gt; and my distribution branch in &lt;code&gt;/etc/dovecot.dist&lt;/code&gt;. That way, Dovecot will read the customized configuration fine, but I still need to tell the installer to write the new files in &lt;code&gt;/etc/dovecot.dist&lt;/code&gt; and not &lt;code&gt;/etc/dovecot&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Fortunately, Debian has a tool to do just that: &lt;code&gt;dpkg-divert&lt;/code&gt;. It’s basically a way to tell the package system, “If a package want to install a file at &lt;code&gt;/some/location/x&lt;/code&gt;, install it at &lt;code&gt;/some/location/y&lt;/code&gt; instead”. This is called a &lt;em&gt;diversion&lt;/em&gt;, and is mainly used by packages to avoid file conflicts with other packages. You can run &lt;code&gt;dpkg-divert --list&lt;/code&gt; to see the list of active diversions on your system.&lt;/p&gt;
&lt;p&gt;A small wrinkle is that Dovecot does not use the package system to install its configuration files; it uses a tool called &lt;code&gt;ucf&lt;/code&gt; (“Update Configuration Files”), which makes configuration management a bit easier by allowing three-way merges during updates. This means that the files in &lt;code&gt;/etc/&lt;/code&gt; are created by &lt;code&gt;ucf&lt;/code&gt;, not &lt;code&gt;dpkg&lt;/code&gt;. Fortunately, &lt;code&gt;ucf&lt;/code&gt; understands and respects diversions, so the procedure should still work.&lt;/p&gt;
&lt;p&gt;So now we’re all set. Here is what I did:&lt;/p&gt;
&lt;h2&gt;Clean state configuration&lt;/h2&gt;
&lt;p&gt;Since my Dovecot configuration is pretty old (with files last modified in 2011!), I wanted to start clean with a new configuration. I looked at Dovecot post-install script and saw that it stores its default configuration in &lt;code&gt;/usr/share/dovecot/&lt;/code&gt; so I copied the files from there:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# mv /etc/dovecot /etc/dovecot.bak
# mkdir /etc/dovecot
# cp -r /usr/share/dovecot/conf.d /usr/share/dovecot/dovecot.conf /usr/share/dovecot/dovecot-dict-auth.conf.ext /usr/share/dovecot/dovecot-dict-sql.conf.ext /usr/share/dovecot/dovecot-sql.conf.ext /etc/dovecot/
# mkdir -m700 /etc/dovecot/private/
# ln -s /var/lib/dehydrated/certs/xxx/privkey.pem /etc/dovecot/private/dovecot.key 
# ln -s /var/lib/dehydrated/certs/xxx/fullchain.pem /etc/dovecot/private/dovecot.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default Dovecot configuration loads its certificate and key from &lt;code&gt;/etc/dovecot/private/dovecot.{pem,key}&lt;/code&gt;, which are symlinks to a fake certificate. Instead of changing the configuration, I decided to just make the symlinks point to my real certificate (a Let’s Encrypt certificate managed by &lt;code&gt;dehydrated&lt;/code&gt;). The post-install script doesn’t touch those symlinks once they are created, so that should be fine.&lt;/p&gt;
&lt;h2&gt;Creating the Git repository&lt;/h2&gt;
&lt;p&gt;Since I’m editing Dovecot’s configuration as &lt;code&gt;root&lt;/code&gt;, I needed to configure Git as &lt;code&gt;root&lt;/code&gt; by running &lt;code&gt;git config --global --edit&lt;/code&gt; and set a username/email.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# cd /etc/dovecot
# git init -b main .
Initialized empty Git repository in /etc/dovecot/.git/
# echo &#x27;/private/&#x27; &amp;gt; .gitignore
# git add .
# git commit -m &#x27;Initial configuration from distro&#x27;
[main (root-commit) 56c5310] Initial configuration from distro
 30 files changed, 2218 insertions(+)
…
# git branch distro
# git worktree add ../dovecot.dist distro
Preparing worktree (checking out &#x27;distro&#x27;)
HEAD is now at 56c5310 Initial configuration from distro
# git branch
+ distro
* main
# cd /etc/dovecot.dist
# git branch
* distro
+ main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we have the &lt;code&gt;main&lt;/code&gt; branch on &lt;code&gt;/etc/dovecot/&lt;/code&gt; and the &lt;code&gt;distro&lt;/code&gt; branch on &lt;code&gt;/etc/dovecot.dist&lt;/code&gt;, as expected.&lt;/p&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;I can then modify the configuration in &lt;code&gt;/etc/dovecot/&lt;/code&gt; and verify that it works. Once it’s done, I add a diversion for all files that I modified. For instance, I modified the file &lt;code&gt;conf.d/10-auth.conf&lt;/code&gt;, so I run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dpkg-divert --local --no-rename --divert /etc/dovecot.dist/conf.d/10-auth.conf --add /etc/dovecot/conf.d/10-auth.conf
Adding &#x27;local diversion of /etc/dovecot/conf.d/10-auth.conf to /etc/dovecot.dist/conf.d/10-auth.conf&#x27;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do the same for all modified files, then I commit them into Git.&lt;/p&gt;
&lt;h2&gt;Upgrade&lt;/h2&gt;
&lt;p&gt;Now, package upgrades should go smoothly, since the package manager/&lt;code&gt;ucf&lt;/code&gt; only sees unmodified files. Since not all files were diverted (maybe I should do it…), it’s possible that some files were modified/added on the customized branch. I move all these changes to the distro branch and cancel them on the main branch.&lt;br&gt;
Then, I commit all changes on the distro branch, then merge/rebase it into the customized branch. Done!&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;One issue to be aware of: file permissions. Don’t forget that the &lt;code&gt;.git&lt;/code&gt; directory contains the full history of all your configuration files, and is by default world-readable. Even if you restrict access to one configuration file (because it contains database passwords for instance), people can still read the file contents by fetching it from the Git commit data (by running &lt;code&gt;git show HEAD:thefile&lt;/code&gt;, for instance) &lt;sup&gt;1&lt;/sup&gt;. To avoid issues, you should make sure that the &lt;code&gt;.git&lt;/code&gt; repository is not world-readable.&lt;/p&gt;
&lt;hr aria-label=&quot;Footnotes&quot; style=&quot;margin-bottom: -0.5rem;&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-1&quot;&gt;
&lt;p&gt;This issue with accessible &lt;code&gt;.git&lt;/code&gt; folders is not limited to local access — If you deploy a website that is hosted on a Git repository, make sure that the &lt;code&gt;.git&lt;/code&gt; folder is not accessible from the web! If it is, people may be able to extract the raw files from your webserver, including any secrets that were committed to the repository. &lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>2451073.html</id>
<link rel="alternate" href="https://vinduv.app/2451073.html"/>
<published>2023-08-11T18:36:15.393Z</published>
<title>Filesystem exploration: the almost-FAT12 floppy disk</title>
<author>
<name>@VinDuv</name>
<uri>https://cohost.org/VinDuv</uri>
</author>
<category term="filesystem" /><category term="fat12" /><category term="floppy disk" />
<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;2451073.html&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;










&lt;p&gt;My parents have a (pretty old at that point) Yamaha Clavinova digital piano, with a floppy drive that can be used to record or play MIDI files.&lt;/p&gt;
&lt;p&gt;It came with a floppy disk named “Disk Orchestra Collection” that contains various sample songs. I randomly decided to image it. It copied fine without errors, yay! But I couldn’t mount it: “No mountable filesystems”.&lt;/p&gt;
&lt;p&gt;I was pretty sure this was some sort of “copy protection”, and not an issue with the imaging process. Given the age of the system, the protection was not going to be very complex, though. Let’s try to get it to mount!&lt;/p&gt;
&lt;p&gt;Note: For accessibility purposes, I’ve put a summary of all hexdumps in this post in figcaption tags. I don’t know if it’s the best way to do it and am open to suggestions.&lt;/p&gt;&lt;hr&gt;
&lt;p&gt;Looking through the image with hexdump revealed some interesting things:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;00000e00  4d 55 53 49 43 20 20 20  44 49 52 20 00 00 00 00  |MUSIC   DIR ....|
00000e10  00 00 00 00 00 00 29 78  94 1e 02 00 e0 0b 00 00  |......)x........|
00000e20  4e 41 4d 45 20 20 20 20  4d 44 41 27 00 00 00 00  |NAME    MDA&#x27;....|
00000e30  00 00 00 00 00 00 2a 78  94 1e 05 00 e0 0b 00 00  |......*x........|
&lt;/pre&gt;
&lt;figcaption&gt;At offset e00, an ASCII string &quot;MUSIC   DIR&quot;; at e20, &quot;NAME    MDA&quot;
&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;This looks like a directory listing on a FAT filesystem! On FAT, filenames are limited to a 8 characters names and 3 characters extension, all ASCII uppercase.&lt;br&gt;
The filesystem stores the 8 characters of the name and the 3 characters of the extension one after the other, padding them with spaces if they are shorter than that. For instance, the name &quot;A.X&quot; will be stored as &quot;A&amp;lt;7 spaces&amp;gt;X&amp;lt;2 spaces&amp;gt;&quot;. So here, we can see two names &quot;MUSIC.DIR&quot; and &quot;NAME.MDA&quot;. We can also see that the start of each filename is 0x20 bytes after the previous one, and FAT directory entries happen to be 0x20 bytes long.&lt;/p&gt;
&lt;p&gt;Since this is a floppy disk, it is probably FAT12 formatted (FAT16 and FAT32 are for larger drives). After reading through some Wikipedia pages, here’s what I understand about this filesystem: (&lt;strong&gt;note:&lt;/strong&gt; I may have got some details wrong)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first sector of the disk (sector 0) is the “boot sector”. It contains  information about the layout of the filesystem, and some code that is executed to boot the computer. It always contains boot code even if the disk does not contain an OS; in that case the boot code is a small program that writes “Non-system disk or disk error, press any key to reboot” (or a variant of it) on the screen. (Different disk format tools put different messages so you may have seen different messages, or even localized ones, when leaving a floppy in your computer’s disk drive)&lt;/li&gt;
&lt;li&gt;The boot sector can be followed by a number of “reserved sectors”. I guess that may be useful for some OSs to put additional boot code.&lt;/li&gt;
&lt;li&gt;After the boot and reserved sectors comes the File Allocation Table (FAT). All the disk space that is used to store file data (and subdirectory contents) is divided into “clusters” (which are a certain number of sectors long). The FAT contains a series of values that indicate the state of each cluster:
&lt;ul&gt;
&lt;li&gt;The cluster is free space (not used)&lt;/li&gt;
&lt;li&gt;The cluster is damaged and should not be used&lt;/li&gt;
&lt;li&gt;The cluster is “reserved”. I guess this is used to indicate that the filesystem driver should not mess with this cluster, for whatever reason.&lt;/li&gt;
&lt;li&gt;The cluster contains data for a file. The value in the FAT either indicates the index of the next cluster that contains data for this file, or a special value that indicates that it’s the last cluster that holds data for this file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;There can be multiple copy of the FAT, for redundancy purposes.&lt;/li&gt;
&lt;li&gt;After the FATs comes the “root directory”, that list the files and directories at the root of the drive. On FAT, directories are basically special files that contain a list of filenames, with attributes and the start cluster of each one. The “root directory” is special because it’s not part of the storage data, so it’s not divided into clusters; it has a fixed size.  Want to store more files at the root of the drive? Sorry, you’ll have to reformat it.&lt;/li&gt;
&lt;li&gt;Finally, the file clusters comes afterwards, until the end of the disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let’s see how a file is read on FAT. First, you need to locate it. Let’s say the file path is “\X\Y\Z.TXT”&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The first name in the path is “X”, so we look for an entry named “X&amp;lt;7 spaces&amp;gt;&amp;lt;3 spaces&amp;gt;” in the root directory. This gives us the start cluster of directory \X contents.&lt;/li&gt;
&lt;li&gt;From the start cluster, we can read directory \X contents (the same way we would read a file; see below) and find subdirectory Y. This gives us the start cluster of directory \X\Y contents.&lt;/li&gt;
&lt;li&gt;Read directory \X\Y contents until we find entry “Z&amp;lt;7 spaces&amp;gt;TXT”. This gives us the start cluster of this file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The directory entries also gives us other information, such as the file (or directory) creation date/time, last modification date/time, file size, and attributes. The attributes indicates if what we’ve found is a file, a directory, or some other weird thing (like long file names).&lt;/p&gt;
&lt;p&gt;Now that we known the start cluster of the file, we can read the value in the FAT corresponding to that cluster, and get the next file cluster. We follow this linked list to the end and hopefully get the list of all clusters that store this file’s data, so we can read it.&lt;/p&gt;
&lt;p&gt;Okay, so why does our mystery disk refuses to be mounted? Let’s look at sector 0:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000200
&lt;/pre&gt;
&lt;figcaption&gt;hexdump showing that sector 0 is filled with zeroes&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It’s empty! That’s why the disk cannot be mounted. The information about the&lt;br&gt;
filesystem layout is missing.&lt;/p&gt;
&lt;p&gt;So let’s try re-creating sector 0; maybe it will be sufficient to mount the&lt;br&gt;
the disk.&lt;/p&gt;
&lt;p&gt;The two first cluster values of the FAT are special. From what I understand, cluster 0 value is always 0xFFx with x = 0 or between 8 and F inclusive, and cluster 1 value is always 0xFFF. On FAT12, these are 12-bit values, so each value is stored into 1.5 bytes in little-endian order (which is a bit weird because of the half-byte).&lt;/p&gt;
&lt;p&gt;That means we can expect a FAT12 FAT to start with “Fx FF FF”. At sector 1, we can see:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;00000200  f9 ff ff 03 40 00 ff 6f  00 07 f0 ff 09 a0 00 0b  |....@..o........|
00000210
&lt;/pre&gt;
&lt;figcaption&gt;hexdump showing bytes f9 ff ff at the start of sector 1, then some other bytes&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;That’s probably the first FAT. Let’s look a bit further:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;00000500  01 22 20 03 42 20 05 62  20 07 82 20 ff 0f 00 00  |.&quot; .B .b .. ....|
00000510  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000800  f9 ff ff 03 40 00 ff 6f  00 07 f0 ff 09 a0 00 0b  |....@..o........|
00000810
&lt;/pre&gt;
&lt;figcaption&gt;hexdump showing zeroes starting at offset 0x508, then f9 ff ff at offset
0x800, followed by the same bytes that were at offset 0x200&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So the second FAT starts at offset 0x800. That means the FAT is 0x600 bytes, or three sectors long. So the second FAT ends at 0xe00, which is exactly the offset of the MUSIC.DIR directory entry, so root the directory is indeed right after the second FAT.&lt;/p&gt;
&lt;p&gt;Now we can determine the information we need to put into sector 0 in order to make the disk readable:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bytes per sector:&lt;/em&gt; 512 (standard for a 720K PC floppy disk)&lt;br&gt;
&lt;em&gt;Number of reserved sectors&lt;/em&gt;: 1 (sector 0 counts as a reserved sector, and the FAT immediately follows it).&lt;br&gt;
&lt;em&gt;Number of FATs:&lt;/em&gt; 2 (as seen above)&lt;br&gt;
&lt;em&gt;Total sector count:&lt;/em&gt; 1440 (720KB disk * 1024 bytes per KB / 512 bytes per sector)&lt;br&gt;
&lt;em&gt;Sectors per FAT:&lt;/em&gt; 3 (as seen above)&lt;br&gt;
&lt;em&gt;Sectors per track:&lt;/em&gt; 9 (standard for a 720K PC floppy disk)&lt;br&gt;
&lt;em&gt;Number of heads:&lt;/em&gt; 2 (standard for a 720K PC floppy disk)&lt;br&gt;
&lt;em&gt;Maximum number of root directory entries:&lt;/em&gt;&lt;br&gt;
Let’s try to locate the end of the directory. After a long list of filenames&lt;br&gt;
starting at 0xe00, we find:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;000015a0  4d 44 52 5f 35 39 20 20  45 56 54 27 00 00 00 00  |MDR_59  EVT&#x27;....|
000015b0  00 00 00 00 00 00 0a a9  95 1e fd 01 f4 2c 00 00  |.............,..|
000015c0  00 e5 e5 e5 e5 e5 e5 e5  e5 e5 e5 e5 e5 e5 e5 e5  |................|
&lt;/pre&gt;
&lt;figcaption&gt;hexdump showing a &quot;MDR_59  EVT&quot; entry at offset 0x15a0; the next entry at
0x15c0 starts with 00 then e5 repeated&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A directory entry starting with 0x00 indicates the end of the directory. After, there is a bunch of 0xe5 bytes, with a 0x00 every 32 bytes. This ends at offset 0x1c00, where something that looks like file data begins.&lt;/p&gt;
&lt;p&gt;So let’s assume the root directory is located between 0xe00 and 0x1c00. This is 3584 bytes, which is 7 sectors or 112 directory entries. Comparing this to other disk images I have, this seems common.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sectors per cluster:&lt;/em&gt;&lt;br&gt;
I’m a bit confused by this value. The disk can store 1426 sectors of cluster&lt;br&gt;
data, and the FAT has 1164 non-zero cluster values, so I assumed that there was&lt;br&gt;
one sector per cluster. This was wrong; I could mount the disk but not read&lt;br&gt;
any files. Other disks that I have seem to all have two sectors per cluster,&lt;br&gt;
so I used this value instead, and it seems to work. Maybe I’m missing something&lt;br&gt;
about the FAT layout.&lt;/p&gt;
&lt;p&gt;The other values in sector 0 are not directly linked to the filesystem&lt;br&gt;
structure. I put some reasonable values. I had a bit of trouble with the very&lt;br&gt;
first value on the disk (three-byte “boot jump”); I think this is the location&lt;br&gt;
in memory where execution is started after copying sector 0 to memory. Putting&lt;br&gt;
zero here did not work so I copied a value from another disk image (0x903ceb).&lt;/p&gt;
&lt;p&gt;Okay, let’s see what fsck_msdos thinks about our reconstructed sector 0:&lt;/p&gt;
&lt;pre&gt;% ./regen_sector_zero.py clavinova_disk_orchestra.img clavinova_disk_orchestra_fixed.img
% fsck_msdos -n clavinova_disk_orchestra_fixed.img 
** clavinova_disk_orchestra_fixed.img
** Phase 1 - Preparing FAT
FAT[0] is incorrect (is 0xFF9; should be 0xF01)
Correct? no
** Phase 2 - Checking Directories
** Phase 3 - Checking for Orphan Clusters
62 files, 194 KiB free (194 clusters)
&lt;/pre&gt;
&lt;p&gt;It looks like the FAT cluster 0 value 0xFF9 is weird enough that the filesystem checker does not like it, but the rest of file system seems OK! Let’s mount it:&lt;/p&gt;
&lt;figure&gt;
&lt;pre&gt;% hdiutil attach -readonly clavinova_disk_orchestra_fixed.img        
/dev/disk6          	                               	/Volumes/NO NAME
% ls -nT /Volumes/NO\ NAME
total 1038
-rwxrwxrwx  1 501  20  12042 Jun  7 23:29:58 1995 MDR_00.EVT
-rwxrwxrwx  1 501  20  32787 Apr 21 20:59:22 1995 MDR_01.EVT
[…] 
-rwxrwxrwx  1 501  20  11508 Apr 21 21:08:20 1995 MDR_59.EVT
-rwxrwxrwx  1 501  20   3040 Apr 20 15:01:18 1995 MUSIC.DIR
-rwxrwxrwx  1 501  20   3040 Apr 20 15:01:20 1995 NAME.MDA
&lt;/pre&gt;
&lt;figcaption&gt;The disk mounts successfully and show 60 files named MDR_XX.EVT, one file named MUSIC.DIR and another named NAME.MDA. File dates are in 1995&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Success! All the files read fine. There are no subdirectories on the disk. It looks like “NAME.MDA” contains the title of the songs on the disk, in a fixed-size format, and “MUSIC.DIR” contains the corresponding filenames “MDR_XX.EVT”. The EVT files probably contain MIDI data in a proprietary format; I could not easily find information about them or a way to convert them.&lt;/p&gt;
&lt;p&gt;And that’s it! I had quite a bit of fun exploring this disk image, and it was satisfying to mount it successfully.&lt;br&gt;
In the future, I think I’ll investigate a weird HFS+ corruption bug that affected some specific versions of Mac OS 8.&lt;/p&gt;&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;filesystem&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;fat12&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;span class=&quot;tag&quot;&gt;#&lt;span class=&quot;p-category&quot;&gt;floppy disk&lt;/span&gt;&lt;span class=&quot;actions&quot;&gt;&lt;/span&gt;
        &lt;/span&gt; 
    &lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

<entry>
<id>about.html</id>
<link rel="alternate" href="https://vinduv.app/about.html"/>
<published>2020-01-01T00:00:00.000Z</published>
<title>About me</title>
<author>
<name>vinduv</name>
<uri>https://vinduv.app/</uri>
</author>

<content type="html" xml:base="https://vinduv.app/">&lt;base href="https://vinduv.app/"&gt;
&lt;article class=&quot;thread h-entry&quot; data-original-path=&quot;about.md&quot;&gt;


&lt;article class=&quot;post cohost&quot;&gt;

    
    &lt;div class=&quot;content e-content&quot;&gt;




&lt;p&gt;Hi! I’m VinDuv.&lt;br&gt;
I’m a systems programmer, mainly embedded Linux. I’m also a Mac user. I sometimes tinker with old/weird computer things.&lt;br&gt;
You can find me on &lt;a href=&quot;https://m.vinduv.app/&quot; rel=&quot;noopener noreferrer&quot;&gt;Mastodon&lt;/a&gt; (mostly programming discussion/shitposting) and &lt;a href=&quot;https://bsky.app/profile/vinduv.app&quot; rel=&quot;noopener noreferrer&quot;&gt;Bluesky&lt;/a&gt; (mostly cute stuff and French politics).&lt;/p&gt;
&lt;/div&gt;
    
    &lt;footer&gt;&lt;div class=&quot;tags&quot;&gt;&lt;/div&gt;&lt;div class=&quot;actions&quot;&gt;&lt;/div&gt;&lt;/footer&gt;
&lt;/article&gt;

&lt;/article&gt;
</content>
</entry>

</feed>
