awscli v2.34.3 on FreeBSD 15.0: _awscrt shenanigans

I had been running AWS CLI v2 on FreeBSD for a while, and it stopped working after I updated to the latest release. I checked out v2.34.3 from the awscli v2 repo, installed the dependencies from requirements.txt, making sure there were no conflicts with locally installed packages — in particular awscrt, where I used the latest available version as specified in the requirements. The aws command itself ran fine:

» aws --version
aws-cli/2.34.3 Python/3.11.14 FreeBSD/15.0-RELEASE-p2 source/amd64

But as soon as it needed to do anything real, it fell apart:

» aws configure sso

aws: [ERROR]: module '_awscrt' has no attribute 'set_log_level'

A bit of digging showed awscrt/io.py calling _awscrt.set_log_level(log_level).

For some context, the awscrt package is the AWS Common Runtime binding for Python, and it’s split into two parts. The pure Python layer lives in the awscrt package and handles the high-level API. The actual work is done by _awscrt, a C extension compiled as a library (a .so file) which on a typical FreeBSD + Python 3.11 setup would live at /usr/local/lib/python3.11/site-packages/_awscrt.abi3.so.

I had manually installed awscrt==0.31.2, the latest release at the time of writing, and also exactly the version pinned in awscli’s requirements.txt. Still, the error persisted. The Python wrapper was clearly calling something the .so didn’t expose.

The clue came from checking where Python was actually loading things from:

» python3 -c "import awscrt; print(awscrt.__version__, awscrt.__file__)"
0.31.2 /usr/local/lib/python3.11/site-packages/awscrt/__init__.py
» python3 -c "import _awscrt; print(_awscrt.__file__)"
/home/myuser/.local/lib/python3.11/site-packages/_awscrt.abi3.so

There it is. The Python part of awscrt was correctly loading from the system-wide site-packages. But _awscrt, the compiled extension, was being pulled from my user’s local site-packages. Probably a leftover from a previous install that I hadn’t properly cleaned up.

After removing the dangling file from ~/.local/lib/python3.11/site-packages/, the check came back clean:

» python3 -c "import _awscrt; print(_awscrt.__file__)"
/usr/local/lib/python3.11/site-packages/_awscrt.abi3.so

And with that, aws configure sso ran without complaint.

Error 500 in nextcloud polls

Today we found the poll application on one of our nextcloud instance to be completely broken. Attempting to create a new poll or add date options to an existing one resulted in HTTP 500 errors. In nextcloud logs, there were errors like:

duplicate key value violates unique constraint "oc_polls_polls_pkey"
DETAIL: Key (id)=(9) already exists.

or when adding options to an existing poll:

OCP\AppFramework\Db\DoesNotExistException: Did expect one result but found none

As a clue, this happened after a migration of the nextcloud DB from MySQL to PostgreSQL. My guess is that the PostgreSQL sequences were not updated correctly during the migration. So it was not possible to create either new rows for options in a poll or even a poll itself after a certain point, when the auto generated ID conflicted with an existing ID in the tables, which is the unique constraint violation you see in the error.

So the behavior was, we could add some options and some polls, but as soon as the sequence catches up to an existing ID, any INSERT fails with a primary key conflict. In our case only the polls tables seemed to be affected, but this could happen on other tables too because of the migration.

The fix was to reset the sequences to the current maximum ID for each table of the polls application:

SELECT setval('oc_polls_polls_id_seq',    (SELECT MAX(id) FROM oc_polls_polls));
SELECT setval('oc_polls_options_id_seq',  (SELECT MAX(id) FROM oc_polls_options));
SELECT setval('oc_polls_votes_id_seq',    (SELECT MAX(id) FROM oc_polls_votes));
SELECT setval('oc_polls_share_id_seq',    (SELECT MAX(id) FROM oc_polls_share));
SELECT setval('oc_polls_comments_id_seq', (SELECT MAX(id) FROM oc_polls_comments));
SELECT setval('oc_polls_log_id_seq',      (SELECT MAX(id) FROM oc_polls_log));

Open with and external programs on Claws-Mail

I was integrating rrr with claws-mail and use it to open external files, so I could centralize file opening configuration there. I created a mail profile in ~/.config/rrr.conf with a catch-all rule * echo "'%s'" >> /tmp/mail-rrr.log to debug which files and extensions were being passed. The plan was to set rrr -p mail -F '%s' as the default external application, but the command kept failing.

I wrote a small test.sh script to display each argument individually as passed by claws-mail. That’s when I discovered the issue: claws-mail treats each space in the command as a separate argument delimiter. So rrr -p  mail -F '%s' (note the double space between -p and mail) resulted in the argument list: ["rrr", "-p", "", "mail", "-F", "the-actual-file"]. The empty string broke everything.

So the fix was simple, just had to make sure there were no extra space everywhere.

runrunrun v0.3.0: I would use that

Another week, another release, runrunrun v0.3.0 is here. This release adds all the features I actually needed to start using this project myself on a daily basis.

What’s New

Fallback: The new -f/--fallback option (or RRR_FALLBACK=true) enables automatic fallback to previous matching rules when commands fail. This is useful when some commands aren’t found on the system:

https://* lynx
https://* chromium
https://* firefox

With fallback enabled, rrr -f https://example.com will try firefox first, then chromium, then lynx until one succeeds.

Alias redefinition: Redefining an alias now updates all rules using it, including previous ones:

[video] vlc
*.mkv [video]
*.mp4 [video]

# Now .mkv and .mp4 both use mplayer
[video] mplayer
*.avi [video]

Fix in desktop file support: Added support for more format specifiers (%f, %F, %u, %U) in desktop files (previously we only supported %U).

As always, feedback and contributions are welcome!


runrunrun is available at https://github.com/gawen947/runrunrun

runrunrun v0.2.0: desktop file import and more

Just one week after the initial release, here comes runrunrun v0.2.0 with a key feature from the roadmap: desktop file import.

What’s New

Desktop File Import: The :import directive can now read .desktop files and automatically generate rules from their MIME types:

:import /usr/local/share/applications/gimp.desktop
:import /usr/share/applications

This extracts the Exec and MimeTypes attributes, infers file extensions, and creates the appropriate glob patterns automatically. It’s a great bridge for migrating from existing desktop configurations.

Path Expansion: Configuration files now support tilde (~) and environment variable expansion:

:include ~/.config/rrr/work.conf
:include ~/$DOTFILES/rrr/common.conf

Include Loop Protection: Fixed a bug where circular includes would cause infinite loops. Now if a file has already been included, it’s simply skipped.

Performance: The config parser now only loads rules for the requested profile, improving startup time for large configurations.

Getting Started with Import

If you want to import your existing desktop file associations:

# Import a specific application
:import /usr/share/applications/firefox.desktop

# Import everything
:import /usr/share/applications
:import ~/.local/share/applications

Desktop files without Exec or MimeTypes are silently skipped, so you can safely import entire directories.

Next Steps

With desktop file import now available, migrating from traditional file association systems is much easier. As always, feedback and contributions are welcome!


runrunrun is available at https://github.com/gawen947/runrunrun

runrunrun v0.1.0 (first release)

I’m happy to announce the first release of runrunrun (v0.1.0), a new file and URL opener that runs the right thing—no surprises, no guesswork.

The Problem It Solves

If you’ve ever clicked on a text file only to have it open in Wine’s Notepad, or watched your carefully configured file associations get overridden by yet another desktop environment, you’ll understand the frustration. Every time I need to configure file associations with xdg-open and its cousins, something isn’t quite right.

Traditional desktop openers work by making assumptions—and that might be exactly what some users want. But for those who prefer explicit control, the current tools fall short. They guess your preferred browser based on your desktop environment, scatter configuration across countless .desktop files, and make debugging file associations unnecessarily complex.

The goal of runrunrun is simple: make opening files and URL schemes straightforward and consistent.

What Makes runrunrun Different

runrunrun (or rrr for short) takes a radically simple approach: explicit configuration through pattern matching. No MIME type cascades, no desktop file archaeology—just straightforward rules that you control.

Here’s what opening a PDF looks like in your config:

*.pdf    qpdf

That’s it. When you run rrr document.pdf, it opens in qpdf. No surprises.

Core Features in v0.1.0

Simple Pattern Matching: Define what opens what using glob patterns for files and URLs:

*.jpg        feh
https://*    firefox
mailto:*     thunderbird

Predictable Overrides: Later rules win, making customization straightforward:

*.txt    mousepad
*.txt    vim    # This one wins

Regex Support with Capture Groups: For more advanced matching, use regex patterns (prefixed with ~) that can even extract and reuse parts of the match:

~^IMG_[0-9]+\.png$  darktable
~mailto:([^?]+)\?subject=([^&]+)  "true %s; thunderbird --compose \"to=%1,subject=%2\""

Profiles for Different Contexts: Switch between configurations easily:

:profile work
https://*    firefox-work-profile

:profile personal
https://*    brave

Terminal-Friendly: Unlike desktop-centric tools, rrr works just as well in terminal environments as it does on a full desktop, making it suitable for servers and minimal setups.

Transparent Operation: Query mode (rrr -q file.ext) shows you exactly what would run, and dry-run mode (rrr -n) lets you test configurations safely.

Getting Started

The project is written in Rust and available on GitHub. Configuration lives in simple text files at /etc/rrr.conf or ~/.config/rrr.conf.

What’s Next

This first release includes the core functionality along with regex patterns and aliases for common applications. Future versions will add the ability to import existing .desktop files for those who want a migration path from their current setup.

As with any first release, there are likely bugs to catch and behaviors that might need adjustment. Your feedback will help shape the direction of future releases.

If you’re tired of fighting with file associations and want something that just works the way you configure it, give runrunrun a try. Your feedback and contributions are welcome!


runrunrun is available at https://github.com/gawen947/runrunrun

Disable RustAnalyzer warnings

Currently, I mainly use Neovim to code in Rust (and pretty much any other language). The problem is that when you start a new Rust project and begin creating structs and functions all over the place, you get flooded with warnings about “unused this” and “unused that”.

Maybe many of you probably already know this, but a quick trick to avoid those warnings is to simply run:
export RUSTFLAGS="-Awarnings".

This will disable all warnings, but it can be very convenient during early development.

Unbound stub-zone for reverse private IPv6

Today I tried to configure a stub-zone on a unbound resolver. This was for the reverse resolution of some private IPv6. In unbound.conf, it looks something like this:

stub-zone:
  name: X.X.X.X.X.X.d.f.ip6.arpa.
  stub-addr: {authoritative-server-ip}

But trying a reverse resolution on any of those private IPv6 failed:

$ drill -x fdXX:XXXX::XXXX
;; AUTHORITY SECTION:
d.f.ip6.arpa.	10800	IN	SOA	localhost. nobody.invalid. 1 3600 1200 604800 10800

Found out the problem in a snippet from unbound.conf.sample:

# By default, for a number of zones a small default 'nothing here'
# reply is built-in.  Query traffic is thus blocked.  If you
# wish to serve such zone you can unblock them by uncommenting one
# of the nodefault statements below.
# You may also have to use domain-insecure: zone to make DNSSEC work,
# unless you have your own trust anchors for this zone.
# local-zone: "localhost." nodefault
# local-zone: "127.in-addr.arpa." nodefault
# local-zone: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa." nodefault
# local-zone: "home.arpa." nodefault
# local-zone: "resolver.arpa." nodefault
# local-zone: "service.arpa." nodefault
# local-zone: "onion." nodefault
# local-zone: "test." nodefault
# local-zone: "invalid." nodefault
# local-zone: "10.in-addr.arpa." nodefault
# local-zone: "16.172.in-addr.arpa." nodefault
# local-zone: "17.172.in-addr.arpa." nodefault
# local-zone: "18.172.in-addr.arpa." nodefault
# local-zone: "19.172.in-addr.arpa." nodefault
# local-zone: "20.172.in-addr.arpa." nodefault
# local-zone: "21.172.in-addr.arpa." nodefault
# local-zone: "22.172.in-addr.arpa." nodefault
# local-zone: "23.172.in-addr.arpa." nodefault
# local-zone: "24.172.in-addr.arpa." nodefault
# local-zone: "25.172.in-addr.arpa." nodefault
# local-zone: "26.172.in-addr.arpa." nodefault
# local-zone: "27.172.in-addr.arpa." nodefault
# local-zone: "28.172.in-addr.arpa." nodefault
# local-zone: "29.172.in-addr.arpa." nodefault
# local-zone: "30.172.in-addr.arpa." nodefault
# local-zone: "31.172.in-addr.arpa." nodefault
# local-zone: "168.192.in-addr.arpa." nodefault
# local-zone: "0.in-addr.arpa." nodefault
# local-zone: "254.169.in-addr.arpa." nodefault
# local-zone: "2.0.192.in-addr.arpa." nodefault
# local-zone: "100.51.198.in-addr.arpa." nodefault
# local-zone: "113.0.203.in-addr.arpa." nodefault
# local-zone: "255.255.255.255.in-addr.arpa." nodefault
# local-zone: "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa." nodefault
# local-zone: "d.f.ip6.arpa." nodefault
# local-zone: "8.e.f.ip6.arpa." nodefault
# local-zone: "9.e.f.ip6.arpa." nodefault
# local-zone: "a.e.f.ip6.arpa." nodefault
# local-zone: "b.e.f.ip6.arpa." nodefault
# local-zone: "8.b.d.0.1.0.0.2.ip6.arpa." nodefault
# And for 64.100.in-addr.arpa. to 127.100.in-addr.arpa.

As you can see d.f.ip6.arpa. is blocked by default, so just had to add this line to unblock it:

local-zone: "d.f.ip6.arpa." nodefault

Broken Campag

Status

Today I just broke a Campagnolo shifter in the dumbest crash possible, on a bike that was just fixed less than 10 days ago after weeks of relentless investigation by my LBS, on what was supposed to be a relaxing Sunday afternoon spin.

I feel a weird mix of stupid, bad and sad.