28 Jan 2026

feedPlanet Grep

Staf Wagemakers: Moved my blog to [blog.wagemakers.be](https://blog.wagemakers.be)

If you follow my blog posts with an RSS reader, update the rss feed to: https://blog.wagemakers.be/atom.xml
…If you want to continue to follow me off-course ;-)

I moved my blog from GitHub to my own hosting ( powered by Procolix ).
Procolix sponsored my hosting for 20 years, till I decided to start my company Mask27.dev.

One reason is that Microsoft seems to like to put "copilot everywhere", including on repositories hosted on github. While I don't dislike AI ( artificial intelligence ), LLM ( Large Language Models ) are a nice piece of technology. The security, privacy, and other issues are overlooked or even just ignored.

The migration was a bit more complicated as usual, as nothing "is easy" ;-)

You'll find the pitfalls of moving my blog below as they might be useful for somebody else ( including the future me ).

Html redirect

I use Jekyll to generate my webpages on my blog. I might switch to HUGO in the future.

While there're Jekyll plugins available to preform a redirect, I decide to keep it simple and added a http header to _includes/head.html

<meta http-equiv="refresh" content="0; url=https://blog.wagemakers.be/blog/2026/01/26/blog-wagemakers-be/" />

Hardcoded links

I had some hardcoded links for image, url, etc on my blog posts.

I used the script below to update the links in my _post directory.

#!/bin/sh

set -o errexit
set -o pipefail
set -o nounset

for file in *; do

  echo "... Processing file: ${file}"

  sed -i ${file} -e s@https://stafwag.github.io/blog/blog/@https://blog.wagemakers.be/blog/@g
  sed -i ${file} -e s@https://stafwag.github.io/blog/images/@https://blog.wagemakers.be/images/@g
  sed -i ${file} -e s@\(https://stafwag.github.io/blog\)@\(https://blog.wagemakers.be\)@

done

Disqus

I use DISQUS as the comment system on my blog. As the HTML pages got a proper redirect, I could ask Disqus to reindex the pages so the old comments became available again.

More information is available at: https://help.disqus.com/en/articles/1717126-redirect-crawler

Without a redirect, you can download the URL in a csv and add a migration URL to the csv file and upload it to Disqus. You can find information about it in the link below.

https://help.disqus.com/en/articles/1717129-url-mapper

RSS redirect

I didn't find a good way to redirect for RSS feeds, which RSS readers use correctly.
If you know a good way to handle it, please let me know.

I tried to add an XML redirect as suggested at: https://www.rssboard.org/redirect-rss-feed. But this doesn't seem to work with the RSS readers I tested (NewsFlash, Akregator).

These are the steps I took.

HTML header

I added the following headers to _includes/head.html

<link rel="self" type="application/atom+xml"  href="{{ site.url }}{{ site.baseurl }}/atom.xml" />
<link rel="alternate" type="application/atom+xml" title="Wagemakers Atom Feed" href="https://wagemakers.be/atom.xml">


<<link rel="self" type="application/rss+xml"  href="{{ site.url }}{{ site.baseurl }}/atom.xml" />
<link rel="alternate" type="application/rss+xml" title="Wagemakers Atom Feed" href="https://wagemakers.be/atom.xml">

Custom feed.xml

When I switched from Octopress to "plain jekyll" I started to use the jekyll-feedplugin. But I still had the old RSS page from Octopress available, so I decided to use it to generate atom.xml and feed.xml in the link rel=self and link rel="alternate" directives.

Full code below or on GitHub: https://github.com/stafwag/blog/blob/gh-pages/feed.xml

---
layout: null
---
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">



  <title><![CDATA[stafwag Blog]]></title>
  <link href="https://blog.wagemakers.be//atom.xml" rel="self"/>
  <link rel="alternate" href="https://blog.wagemakers.be/atom.xml" /> <link href="https://blog.wagemakers.be }}"/>
  <link rel="self" type="application/atom+xml" href="https://blog.wagemakers.be//atom.xml" />
  <link rel="alternate" type="application/atom+xml" href="https://blog.wagemakers.be/atom.xml" />
  <link rel="self" type="application/rss+xml" href="https://blog.wagemakers.be//atom.xml" />
  <link rel="alternate" type="application/rss+xml" href="https://blog.wagemakers.be/atom.xml" />
  <updated>2026-01-26T20:10:56+01:00</updated>
  <id>https://blog.wagemakers.be</id>
  <author>
    <name><![CDATA[Staf Wagemakers]]></name>
    
  </author>
  <generator uri="http://octopress.org/">Octopress</generator>

{% for post in site.posts limit: 10000 %}
  <entry>
<title type="html"><![CDATA[{% if site.titlecase %}{{ post.title | titlecase | cdata_escape }}{% else %}{{ post.title | cdata_escape }}{% endif %}]]></title>
 <link href="{{ site.url }}{{ site.baseurl }}{{ post.url }}"/>
    <updated></updated>
    <id>https://blog.wagemakers.be/</id>
    <content type="html"><![CDATA[]]></content>
  </entry>
{% endfor %}
</feed>

Notify users

I created this blog post to notify the users ;-)

Have fun!

Links

28 Jan 2026 11:58am GMT

FOSDEM organizers: Guided sightseeing tours

If your non-geek partner and/or kids are joining you to FOSDEM, they may be interested in spending some time exploring Brussels while you attend the conference. Like previous years, FOSDEM is organising sightseeing tours.

28 Jan 2026 11:58am GMT

FOSDEM organizers: Call for volunteers

With FOSDEM just a few days away, it is time for us to enlist your help. Every year, an enthusiastic band of volunteers make FOSDEM happen and make it a fun and safe place for all our attendees. We could not do this without you. This year we again need as many hands as possible, especially for heralding during the conference, during the buildup (starting Friday at noon) and teardown (Sunday evening). No need to worry about missing lunch at the weekend, food will be provided. Would you like to be part of the team that makes FOSDEM tick?舰

28 Jan 2026 11:58am GMT

feedPlanet Debian

C.J. Collier: Part 2: Taming the Beast – Deep Dive into the Proxy-Aware GPU Initialization Action

Part
2: Taming the Beast - Deep Dive into the Proxy-Aware GPU Initialization
Action

In Part 1 of this series, we laid the network foundation for running
secure Dataproc clusters. Now, let's zoom in on the core component
responsible for installing and configuring NVIDIA GPU drivers in this
restricted environment: the install_gpu_driver.sh script
from the GoogleCloudDataproc/initialization-actions
repository.

This isn't just any installation script; it has been significantly
enhanced to handle the nuances of Secure Boot and to operate seamlessly
behind an HTTP/S proxy.

The
Challenge: Installing GPU Drivers Without Direct Internet

The install_gpu_driver.sh script needs to:

  1. Download NVIDIA drivers, CUDA toolkits, cuDNN, NCCL, etc.
  2. Potentially compile kernel modules.
  3. Install OS packages and dependencies.
  4. Configure the system for GPU workloads.

All of these steps traditionally require internet access, which is
blocked in our target environment. Additionally, for Secure Boot, any
newly compiled kernel modules must be signed.

Key Enhancements in
install_gpu_driver.sh

To address these challenges, the script incorporates several key
features:

Conclusion of Part 2

The install_gpu_driver.sh initialization action is more
than just an installer; it's a carefully crafted tool designed to handle
the complexities of secure, proxied environments. Its robust proxy
support, GCS caching, and awareness of the custom image build lifecycle
make it a critical enabler.

In Part 3, we'll see how the
GoogleCloudDataproc/custom-images repository uses this
initialization action as a customization script to build the Secure
Boot-ready images.

28 Jan 2026 10:45am GMT

C.J. Collier: Dataproc GPUs, Secure Boot, & Proxies

Part
1: Building a Secure Network Foundation for Dataproc with GPUs &
SWP

Welcome to the first post in our series on running GPU-accelerated
Dataproc workloads in secure, enterprise-grade environments. Many
organizations need to operate within VPCs that have no direct internet
egress, instead routing all traffic through a Secure Web Proxy (SWP).
Additionally, security mandates often require the use of Shielded VMs
with Secure Boot enabled. This series will show you how to meet these
requirements for your Dataproc GPU clusters.

In this post, we'll focus on laying the network foundation using
tools from the GoogleCloudDataproc/cloud-dataproc
repository.

The Challenge: Network
Isolation & Control

Before we can even think about custom images or GPU drivers, we need
a network environment that:

  1. Prevents direct internet access from Dataproc cluster nodes.
  2. Forces all egress traffic through a manageable and auditable
    SWP.
  3. Provides the necessary connectivity for Dataproc to function and for
    us to build images.

The Toolkit:
GoogleCloudDataproc/cloud-dataproc

To make setting up and tearing down these complex network
environments repeatable and consistent, we've developed a set of bash
scripts within the gcloud directory of the
cloud-dataproc repository. These scripts handle the
creation of VPCs, subnets, firewall rules, service accounts, and the
Secure Web Proxy itself.

Key Script:
bin/create-dpgce-private

This script is the cornerstone for creating the private, proxied
environment. It automates:

Configuration via env.json

While this script is in the cloud-dataproc repo, we've
designed it to be driven by a single env.json file that
will also be used by the custom-images scripts in Part 2.
This env.json should reside in your
custom-images repository clone.

Example env.json Snippet for
Networking:

{
  "PROJECT_ID": "YOUR_GCP_PROJECT_ID",
  "REGION": "YOUR_GCP_REGION",
  "ZONE": "YOUR_GCP_ZONE",
  "SUBNET": "main-subnet",
  "BUCKET": "YOUR_GCS_BUCKET",
  "TEMP_BUCKET": "YOUR_GCS_TEMP_BUCKET",
  "RANGE": "10.10.0.0/24",
  "PRIVATE_RANGE": "10.11.0.0/24",
  "SWP_RANGE": "10.12.0.0/24",
  "SWP_IP": "10.11.0.250",
  "SWP_PORT": "3128",
  "SWP_HOSTNAME": "swp.your.domain.com",
  "PROXY_CERT_GCS_PATH": "gs://YOUR_PROXY_CERT_BUCKET/proxy.cer"
}

Running the Setup:

# Assuming you have cloud-dataproc and custom-images cloned side-by-side
cd cloud-dataproc/gcloud
# Symlink to the env.json in custom-images
ln -sf ../../custom-images/env.json env.json
# Run the creation script, but don't create a cluster yet
bash bin/create-dpgce-private --no-create-cluster
cd ../../custom-images

Node
Configuration: The Metadata Startup Script

One of the key learnings from developing this solution was that the
proxy settings need to be applied to the Virtual Machines very early in
the boot process, even before the Dataproc agent starts. To achieve
this, we use a GCE metadata startup script.

The script startup_script/gce-proxy-setup.sh (from the
custom-images repository) is designed to be run on each
cluster node at boot. It reads metadata like http-proxy and
http-proxy-pem-uri to configure the OS environment, package
managers, and other tools to use the SWP.

Upload this script to your GCS bucket:

# Run from the custom-images repository root
gsutil cp startup_script/gce-proxy-setup.sh gs://$(jq -r .BUCKET env.json)/custom-image-deps/

We will specify this script in the
--metadata startup-script-url flag when creating the
Dataproc cluster in Part 4.

Conclusion of Part 1

With the cloud-dataproc scripts, we've laid the
groundwork by provisioning a secure VPC with controlled egress through
an SWP. We've also prepared the essential node-level proxy configuration
script in GCS.

Stay tuned for Part 2, where we'll dive into the
install_gpu_driver.sh initialization action and how it's
been adapted to thrive in this proxied world.

28 Jan 2026 10:37am GMT

Sven Hoexter: Decrypt TLS Connection with wireshark and curl

With TLS 1.3 more parts of the handshake got encrypted (e.g. the certificate), but sometimes it's still helpful to look at the complete handshake.

curl uses the somewhat standardized env variable for the key log file called SSLKEYLOGFILE, which is also supported by Firefox and Chrome. wireshark hides the setting in the UI behind Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename which is uncomfortable to reach. Looking up the config setting in the Advanced settings one can learn that it's called internally tls.keylog_file. Thus we can set it up with:

sudo wireshark -o "tls.keylog_file:/home/sven/curl.keylog"

SSLKEYLOGFILE=/home/sven/curl.keylog curl -v https://www.cloudflare.com/cdn-cgi/trace

Depending on the setup root might be unable to access the wayland session, that can be worked around by letting sudo keep the relevant env variables:

$ cat /etc/sudoers.d/wayland 
Defaults   env_keep += "XDG_RUNTIME_DIR"
Defaults   env_keep += "WAYLAND_DISPLAY"

Or setup wireshark properly and use the wireshark group to be able to dump traffic. Might require a sudo dpkg-reconfigure wireshark-common.

Regarding curl: In some situations it could be desirable to force a specific older TLS version for testing, which requires a minimal and maximal version. E.g. to force TLS 1.2 only:

curl -v --tlsv1.2 --tls-max 1.2 https://www.cloudflare.com/cdn-cgi/trace

28 Jan 2026 10:32am GMT

26 Jan 2026

feedFOSDEM 2026

Guided sightseeing tours

If your non-geek partner and/or kids are joining you to FOSDEM, they may be interested in spending some time exploring Brussels while you attend the conference. Like previous years, FOSDEM is organising sightseeing tours.

26 Jan 2026 11:00pm GMT

Call for volunteers

With FOSDEM just a few days away, it is time for us to enlist your help. Every year, an enthusiastic band of volunteers make FOSDEM happen and make it a fun and safe place for all our attendees. We could not do this without you. This year we again need as many hands as possible, especially for heralding during the conference, during the buildup (starting Friday at noon) and teardown (Sunday evening). No need to worry about missing lunch at the weekend, food will be provided. Would you like to be part of the team that makes FOSDEM tick?舰

26 Jan 2026 11:00pm GMT

feedPlanet Lisp

TurtleWare: McCLIM and 7GUIs - Part 1: The Counter

Table of Contents

  1. Version 1: Using Gadgets and Layouts
  2. Version 2: Using the CLIM Command Loop
  3. Conclusion

For the last two months I've been polishing the upcoming release of McCLIM. The most notable change is the rewriting of the input editing and accepting-values abstractions. As it happens, I got tired of it, so as a breather I've decided to tackle something I had in mind for some time to improve the McCLIM manual - namely the 7GUIs: A GUI Programming Benchmark.

This challenge presents seven distinct tasks commonly found in graphical interface requirements. In this post I'll address the first challenge - The Counter. It is a fairly easy task, a warm-up of sorts. The description states:

Challenge: Understanding the basic ideas of a language/toolkit.

The task is to build a frame containing a label or read-only textfield T and a button B. Initially, the value in T is "0" and each click of B increases the value in T by one.

Counter serves as a gentle introduction to the basics of the language, paradigm and toolkit for one of the simplest GUI applications imaginable. Thus, Counter reveals the required scaffolding and how the very basic features work together to build a GUI application. A good solution will have almost no scaffolding.

In this first post, to make things more interesting, I'll solve it in two ways:

In CLIM it is possible to mix both paradigms for defining graphical interfaces. Layouts and gadgets are predefined components that are easy to use, while using application streams enables a high degree of flexibility and composability.

First, we define a package shared by both versions:

(eval-when (:compile-toplevel :load-toplevel :execute)
  (unless (member :mcclim *features*)
    (ql:quickload "mcclim")))

(defpackage "EU.TURTLEWARE.7GUIS/TASK1"
  (:use  "CLIM-LISP" "CLIM" "CLIM-EXTENSIONS")
  (:export "COUNTER-V1" "COUNTER-V2"))
(in-package "EU.TURTLEWARE.7GUIS/TASK1")

Note that "CLIM-EXTENSIONS" package is McCLIM-specific.

Version 1: Using Gadgets and Layouts

Assuming that we are interested only in the functionality and we are willing to ignore the visual aspect of the program, the definition will look like this:

(define-application-frame counter-v1 ()
  ((value :initform 0 :accessor value))
  (:panes
   ;;      v type v initarg
   (tfield :label :label (princ-to-string (value *application-frame*))
                  :background +white+)
   (button :push-button :label "Count"
                        :activate-callback (lambda (gadget)
                                             (declare (ignore gadget))
                                             (with-application-frame (frame)
                                               (incf (value frame))
                                               (setf (label-pane-label (find-pane-named frame 'tfield))
                                                     (princ-to-string (value frame)))))))
  (:layouts (default (vertically () tfield button))))

;;; Start the application (if not already running).
;; (find-application-frame 'counter-v1)

The macro define-application-frame is like defclass with additional clauses. In our program we store the current value as a slot with an accessor.

The clause :panes is responsible for defining named panes (sub-windows). The first element is the pane name, then we specify its type, and finally we specify initargs for it. Panes are created in a dynamic context where the application frame is already bound to *application-frame*, so we can use it there.

The clause :layouts allows us to arrange panes on the screen. There may be multiple layouts that can be changed at runtime, but we define only one. The macro vertically creates another (anonymous) pane that arranges one gadget below another.

Gadgets in CLIM operate directly on top of the event loop. When the pointer button is pressed, it is handled by activating the callback, that updates the frame's value and the label. Effects are visible immediately.

Now if we want the demo to look nicer, all we need to do is to fiddle a bit with spacing and bordering in the :layouts section:

(define-application-frame counter-v1 ()
  ((value :initform 0 :accessor value))
  (:panes
   (tfield :label :label (princ-to-string (value *application-frame*))
                  :background +white+)
   (button :push-button :label "Count"
                        :activate-callback (lambda (gadget)
                                             (declare (ignore gadget))
                                             (with-application-frame (frame)
                                               (incf (value frame))
                                               (setf (label-pane-label (find-pane-named frame 'tfield))
                                                     (princ-to-string (value frame)))))))
  (:layouts (default
             (spacing (:thickness 10)
              (horizontally ()
                (100
                 (bordering (:thickness 1 :background +black+)
                   (spacing (:thickness 4 :background +white+) tfield)))
                15
                (100 button))))))

;;; Start the application (if not already running).
;; (find-application-frame 'counter-v1)

This gives us a layout that is roughly similar to the example presented on the 7GUIs page.

Version 2: Using the CLIM Command Loop

Unlike gadgets, stream panes in CLIM operate on top of the command loop. A single command may span multiple events after which we redisplay the stream to reflect the new state of the model. This is closer to the interaction type found in the command line interfaces:

  (define-application-frame counter-v2 ()
    ((value :initform 0 :accessor value))
    (:pane :application
     :display-function (lambda (frame stream)
                         (format stream "~d" (value frame)))))

  (define-counter-v2-command (com-incf-value :name "Count" :menu t)
      ()
    (with-application-frame (frame)
      (incf (value frame))))

;; (find-application-frame 'counter-v2)

Here we've used :pane option this is a syntactic sugar for when we have only one named pane. Skipping :layouts clause means that named panes will be stacked vertically one below another.

Defining the application frame defines a command-defining macro. When we define a command with define-counter-v2-command, then this command will be inserted into a command table associated with the frame. Passing the option :menu t causes the command to be available in the frame menu as a top-level entry.

After the command is executed (in this case it modifies the counter value), the application pane is redisplayed; that is a display function is called, and its output is captured. In more demanding scenarios it is possible to refine both the time of redisplay and the scope of changes.

Now we want the demo to look nicer and to have a button counterpart placed beside the counter value, to resemble the example more:

(define-presentation-type counter-button ())

(define-application-frame counter-v2 ()
  ((value :initform 0 :accessor value))
  (:menu-bar nil)
  (:pane :application
   :width 250 :height 32
   :borders nil :scroll-bars nil
   :end-of-line-action :allow
   :display-function (lambda (frame stream)
                       (formatting-item-list (stream :n-columns 2)
                         (formatting-cell (stream :min-width 100 :min-height 32)
                           (format stream "~d" (value frame)))
                         (formatting-cell (stream :min-width 100 :min-height 32)
                           (with-output-as-presentation (stream nil 'counter-button :single-box t)
                             (surrounding-output-with-border (stream :padding-x 20 :padding-y 0
                                                                     :filled t :ink +light-grey+)
                               (format stream "Count"))))))))

(define-counter-v2-command (com-incf-value :name "Count" :menu t)
    ()
  (with-application-frame (frame)
    (incf (value frame))))

(define-presentation-to-command-translator act-incf-value
    (counter-button com-incf-value counter-v2)
    (object)
  `())

;; (find-application-frame 'counter-v2)

The main addition is the definition of a new presentation type counter-button. This faux button is printed inside a cell and surrounded with a background. Later we define a translator that converts clicks on the counter button to the com-incf-value command. The translator body returns arguments for the command.

Presenting an object on the stream associates a semantic meaning with the output. We can now extend the application with new gestures (names :scroll-up and :scroll-down are McCLIM-specific):

(define-counter-v2-command (com-scroll-value :name "Increment")
    ((count 'integer))
  (with-application-frame (frame)
    (if (plusp count)
        (incf (value frame) count)
        (decf (value frame) (- count)))))

(define-presentation-to-command-translator act-scroll-up-value
    (counter-button com-scroll-value counter-v2 :gesture :scroll-up)
    (object)
  `(10))

(define-presentation-to-command-translator act-scroll-dn-value
    (counter-button com-scroll-value counter-v2 :gesture :scroll-down)
    (object)
  `(-10))

(define-presentation-action act-popup-value
    (counter-button nil counter-v2 :gesture :describe)
    (object frame)
  (notify-user frame (format nil "Current value: ~a" (value frame))))

A difference between presentation to command translators and presentation actions is that the latter does not automatically progress the command loop. Actions are often used for side effects, help, inspection etc.

Conclusion

In this short post we've solved the first task from the 7GUIs challenge. We've used two techniques available in CLIM - using layouts and gadgets, and using display and command tables. Both techniques can be combined, but differences are visible at a glance:

This post only scratched the capabilities of the latter, but the second version demonstrates why the command loop and presentations scale better than gadget-only solutions.

Following tasks have gradually increasing level of difficulty that will help us to emphasize how useful are presentations and commands when we want to write maintainable applications with reusable user-defined graphical metaphors.

26 Jan 2026 12:00am GMT

25 Jan 2026

feedFOSDEM 2026

Present a lightning lightning talk

The same as last year: come and take part in a very rapid set of talks! Thought of a last minute topic you want to share? Got your interesting talk rejected? Has something exciting happened in the last few weeks you want to talk about? Get that talk submitted to Lightning Lightning Talks! We have two sessions for participants to speak about subjects which are interesting, amusing, or just something the FOSDEM audience would appreciate: Saturday Sunday Selected speakers line up and present in one continuous automated stream, with an SLO of 99% talk uptime. To submit your talk for舰

25 Jan 2026 11:00pm GMT

20 Jan 2026

feedPlanet Lisp

Joe Marshall: Filter

One of the core ideas in functional programming is to filter a set of items by some criterion. It may be somewhat suprising to learn that lisp does not have a built-in function named "filter" "select", or "keep" that performs this operation. Instead, Common Lisp provides the "remove", "remove-if", and "remove-if-not" functions, which perform the complementary operation of removing items that satisfy or do not satisfy a given predicate.

The remove function, like similar sequence functions, takes an optional keyword :test-not argument that can be used to specify a test that must fail for an item to be considered for removal. Thus if you invert your logic for inclusion, you can use the remove function as a "filter" by specifying the predicate with :test-not.

> (defvar *nums* (map 'list (λ (n) (format nil "~r" n)) (iota 10)))
*NUMS*

;; Keep *nums* with four letters
> (remove 4 *nums* :key #'length :test-not #'=)
("zero" "four" "five" "nine")

;; Keep *nums* starting with the letter "t"
> (remove #\t *nums* :key (partial-apply-right #'elt 0) :test-not #'eql)
("two" "three")

20 Jan 2026 11:46am GMT

16 Jan 2026

feedPlanet Lisp

Scott L. Burson: FSet v2.2.0: JSON parsing/printing using Jzon

FSet v2.2.0, which is the version included in the recent Quicklisp release, has a new Quicklisp-loadable system, FSet/Jzon. It extends the Jzon JSON parser/printer to construct FSet collections when reading, and to be able to print them.

On parsing, JSON arrays produce FSet seqs; JSON objects produce FSet replay maps by default, but the parser can also be configured to produce ordinary maps or FSet tuples. For printing, any of these can be handled, as well as the standard Jzon types. The tuple representation provides a way to control the printing of `nil`, depending on the type of the corresponding key.

For details, see the GitLab MR.

NOTE: unfortunately, the v2.1.0 release had some bugs in the new seq code, and I didn't notice them until after v2.2.0 was in Quicklisp. If you're using seqs, I strongly recommend you pick up v2.2.2 or newer from GitLab or GitHub.

16 Jan 2026 8:05am GMT