2025-04-26

Detecting your home network with SSH

Suppose you have a server in your private home network. It's called myserver.home, and you SSH into it as user me. When you're at home, all you need to get in is:

ssh me@myserver.home

But now you want to SSH from outside. You've set up a dynamic DNS service under the name mydyndns.example.org, and configured forwarding in your router on external port 9000 to 22 on myserver.home. Your command becomes:

ssh me@mydyndns.example.org -p 9000

You can set up aliases for these command in ~/.ssh/config:

Host srv-ext srv-int
User me

Host srv-int
Hostname myserver.home

Host srv-ext
Port 9000
Hostname mydyndns.example.org

Cool, except you have to think about which one to use, depending on where you are. Well, it might not be that important, as the srv-ext alias will likely also work at home. Nevertheless, it would be cooler to use just one alias srv, and have it automatically detect whether a local connection is possible. It might also be noticeably faster in some cases.

Solution

SSH configuration provides a Match directive that can detect several conditions, including the result of a generic command. You can use that to override the general case of external access with optimizations for internal access.

Detecting the local network

First, you need a way to detect your local network. Perhaps the most robust way is to detect your router's MAC address xx:xx:xx:xx:xx:xx, which you can get with:

$ arp -a _gateway
_gateway (192.168.1.254) at xx:xx:xx:xx:xx:xx [ether] on wlp4s0

You could wrap up testing for it in a script:

#!/bin/bash
# -*- sh-basic-offset: 2; indent-tabs-mode: nil -*-

declare -A mac_arg=()

while [ $# -gt 0 ] ; do
  arg="$1" ; shift
  case "$arg" in
    (-h|--host)
      host_arg="$1" ; shift
      ;;

    (--host=*)
      host_arg="${arg#--host=}"
      ;;

    (-m|--mac)
      arg="$1" ; shift
      arg="${arg,,}"
      mac_arg["$arg"]=yes
      ;;

    (--mac=*)
      arg="${arg#--mac=}"
      arg="${arg,,}"
      mac_arg["$arg"]=yes
      ;;

    (-*|+*)
      printf >&2 '%s: unknown switch: %s\n' "$0" "$arg"
      exit 1
      ;;

    (*)
      printf >&2 '%s: unknown arg: %s\n' "$0" "$arg"
      exit 1
      ;;
  esac
done

if [ -z "$host_arg" -o "${#mac_arg[@]}" -eq 0 ] ; then
  printf >&2 'usage: %s -h host -m mac\n' "$0"
  exit 1
fi

read canon ipbr at got_mac rest < <(arp -a "$host_arg" 2> /dev/null)
test -n "$got_mac" && test -n "${mac_arg["$got_mac"]}"

(You can surely get away with something a lot simpler.) Drop the script in (say) /usr/local/bin/host-is-mac and make it executable (chmod 755 /usr/local/bin/host-is-mac). Now you can see whether you're in your own network:

$ host-is-mac -h _gateway -m xx:xx:xx:xx:xx && echo internal
internal
$ 

If you test a hostname which doesn't resolve, the command quietly fails:

$ host-is-mac -h made-up -m xx:xx:xx:xx:xx && echo internal
$ 

A special case for your home network

Now you can configure SSH to test whether the local network is your home network, to decide whether to connect using internal or external parameters. Conflicting options are resolved by choosing the first instance, so you should define the parameters for external access as the general case, and then precede them with the specific case of being in the same network:

## specific (internal) case
Match originalhost="srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Port 22
Hostname myserver.home

## general (external) case
Host srv
User me
HostName mydyndns.example.org
Port 9000

The Match directive enables its following directives only if srv is given as the SSH destination, and then only if the host-is-mac command succeeds. They are treated as a logical AND with short-circuiting, so the external command is only invoked when necessary.

Test the configuration by using ssh -v srv echo yes, and look for lines with Connecting to in them. If you're connecting locally, you'll see:

debug1: Connecting to myserver.home [192.168.1.100] port 22.

If you're connecting from outside:

debug1: Connecting to mydyndns.example.org [10.10.10.10] port 9000.

Proxying

Suppose you use myserver.home as a proxy to another server otherserver.home, and you want the alias alt-srv to conditionally go direct when local. Its specialization must disable proxying:

Match originalhost="alt-srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
ProxyJump none

Host alt-srv
User me
Hostname otherserver.home
ProxyJump srv

Handling multiple aliases

If you have several aliases for a single server, you can list them in the Host clause, separating them with spaces. However, to list them in the originalhost condition, separate them with commas:

Match originalhost="srv1,srv2" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Port 22
Hostname myserver.home

Host srv1 srv2
User me
HostName mydyndns.example.org
Port 9000

You could, of course, just use Match for both clauses.

Things that didn't work

Trying to make transclusion of configuration files conditional doesn't work:

Match originalhost="srv,alt-srv" exec "host-is-mac -h _gateway -m xx:xx:xx:xx:xx"
Include site1-specializations.conf

The Include directive applies unconditionally, and the Match directive applies only to the initial directives of the transcluded file, up until the next Match or Host.

2024-05-27

Reading pressure from a QMP6988

I got hold of an envIII sensor for the indoor humidity, temperature and pressure readings, and bunged it on an RPi via a Grove HAT. This device incorporates an SHT30 for humidity and temperature, and a QMP6988 for pressure (but it also measures temperature for performing some compensation on the pressure). I had no trouble interpreting the SHT30's datasheet, and got the readings out with two I²C calls. The procedure for the QMP6988 is a bit more involved, and its datasheet required some guesswork, so I'm documenting the steps I took in case someone else is having trouble.

Reading the raw coefficients

To perform compensation, you need to read 12 raw integer coefficients, then scale and translate them as real numbers, before combining them with the raw pressure/temperature readings. The raw coefficients are expressed as 25 1-byte constant read-only registers within the device, so you only need to fetch them once, even if you're going to take multiple readings. I used the I2C_RDWR ioctl to write the register being requested, read the value, cancel the request, and confirm the cancellation, in sequence for each register. Each call (re-)used a single buffer:

uint8_t buf;
struct i2c_msg msg = {
  .addr = addr,
  .len = 1,
  .buf = &buf,
};
struct i2c_rdwr_ioctl_data pyld = {
  .msgs = &msg,
  .nmsgs = 1,
};

With fd open on the I²C device, I could request register reg_idx like this:

buf = reg_idx;
msg.flags = 0; // write
if (ioctl(fd, I2C_RDWR, &pyld) < 0)
  throw std::system_error(errno, std::generic_category());

To read, set msg.flags = I2C_M_RD, and call ioctl again. I kept reading as long as ioctl returned negative with errno == EIO.

My understanding of the datasheet is that one should then request register 0xff (as if to cancel the prior request), and keep reading until one gets 0. In fact, my code stopped if it got EIO or a zero, though I don't think I've seen the latter:

buf = 0;
msg.flags = I2C_M_RD;
do {
  if (ioctl(fd, I2C_RDWR, &pyld) < 0) {
    if (errno == EIO) break;
    throw std::system_error(errno, std::generic_category());
  }
  if (buf != 0x00) continue;
  break;
} while (true);

Coefficients' signedness

Ten of the coefficients are 16-bit integers, and the other two are 20-bit. I couldn't find anywhere in the datasheet about their signedness, but I only get reasonable readings if they are treated as signed. I used a wider unsigned type to compose the value from bytes, reinterpreted as the corresponding signed type, then subtracted if the ‘top’ bit was set:

uint_fast32_t val = low_byte;
val |= high_byte << 8;
int_fast32_t ival = val;
if (val & 0x8000)
  ival -= 0x10000;

Scaling and translating the coefficients

Each of the 16-bit integers must be divided by an integer constant, then multipled by a real constant, and then offset by another real. In the datasheet, these real constants are provided under Conversion factor in a table, and a general equation shows how to use them. However, the information for the 20-bit coefficients looks potentially contradictory. In the corresponding table, the Conversion factor column says Offset value (20Q16), while the equation simply says to divide by 16 (so no offset?). I haven't found any definition of this notation, but I think it implies that the original value is 20 bits, with the unit being 1/16. In other words, all you have to do is divide the signed integer by 16, as the equation states.

Taking the raw readings

I used a one-off write to one of the registers to initialize the device (a 2-byte <register, value> message), but I send another 2-byte message to force each reading. After waiting a moment, I read each of the 6 bytes separately, in the same way as reading the coefficients (request, read, cancel, confirm).

The datasheet states that each 24-bit reading should have 223 subtracted from it, but at 24bits[sic] output mode. I thought maybe this meant that the result should be masked with 0xffffff, but that would create a considerable discontinuity, and indeed it does not yield correct results. Simply treat the raw 24-bit value as unsigned, convert it to a signed value (with no sign extension), and do the subtraction.

Units

After applying compensation, the pressure is expressed in Pa, which is stated in the datasheet. Divide by 100 to get hPa or mbar.

The datasheet mentions 256 degreeC as the unit for the compensated temperature. I got meaningful readings by dividing by 256, so I guess it means that the unit is one 256th of a degree C. When you use the compensated temperature to compensate the pressure, just use the value as is; don't divide.

WS3085 wind speed codes

I've been examining the raw signals from several Aercus Instruments weather stations, mainly the WS3085 and similar. Two bytes of the long (80-bit) messages appear to carry wind speed, one for the average, and one for gust.

By recording the signals and simultaneously observing the console, I could get a mapping between the signal and reported wind speed. Here are some plain speeds:

byte 1 (wind speed, bits 32-39) console speed (km/hr)
00000000 0.0
00000001 1.1 (corrected signal after possible misreading)
00000010 2.5
00000011 3.6
00000100 5.0
00000101 6.1
00000110 7.2
00000111 8.6

Here are some gust speeds (on a windier day):

byte 2 (gust speed; bits 40-47) console gust speed (km/hr)
00000110 7.2
00001000 9.7
00001001 11.2
00001110 17.3
00001111 18.4
00010001 20.9
00010010 22.0
00011101 35.6
00100000 39.2

Where they overlap, gust speeds and plain wind speeds appear to use the same representation, and larger numbers correspond to greater speeds, so I'm going to assume that they indeed use the same representation. However, there's no consistent ratio shown in the recordings above, but it's always (so far) between 1.1 and 1.25. The mean is ~1.218, which works closely for codes 5 and 8, but over-reports for 1, 3 and 6, and under-reports for 2, 4, 7, 9, 14, 15, 17, 18, 29 and 32. Perhaps using different units would have yielded a more consistent ratio, e.g., the code is first multiplied and rounded to get the speed in another unit, then multiplied again and rounded again to get the speed in km/hr. Other units are m/s (÷3.6), mi/hr (÷1.609) and knots (÷1.852), and none of these are going to yield a nicer ratio.

To get a more intuitive understanding, here's a plot of speeds against raw values, but with a couple of anticipated scales subtracted:

Those drops are all by the same amount. The increments aren't, but some are similar. What's going on?

Here's the Gnuplot script:

set title 'Wind ratio'
set datafile sep ','
set xlabel 'signal'
set ylabel 'speed (km/hr)'
set term pdf monochrome linewidth 0.1
set output 'windratio.pdf'
set key left bottom
set grid xtics
set xtics 1
show grid
plot 'windratio.csv' using 1:($2-$1*1.25) with linespoints title 'observed - 1.25x', \
  'windratio.csv' using 1:($2-$1*1.225) with linespoints title 'observed - 1.225x'

And here's windratio.csv:

0,0
1,1.1
2,2.5
3,3.6
4,5.0
5,6.1
6,7.2
7,8.6
8,9.7
9,11.2
14,17.3
15,18.4
17,20.9
18,22
29,35.6
32,39.2

Looks like you can reproduce that table with something like this:

def conv(i):
    return i * 1.1 + \
        ((i + 3) // 5 + (i + 1) // 5) * 0.3 + \
        ((i + 16) // 25) * 0.1

for i in range(0, 33):
    print('%2d: %4.2f' % (i, conv(i)))
    continue

In other words, add 1.1 per unit, then add 0.3 every 5 units from positions 1 and 4, and add a further 0.1 at 9 (and I'm guessing that's every 25 units, but it must be at least 24).

According to Kevin, just multiply by 0.34, and round to the nearest tenth, to get metres per second. Converting to km/h and rounding again gives all the reported values. Try the following, and you'll see all the reported values matching:

def conv(i):
    return i * 1.1 + \
        ((i + 3) // 5 + (i + 1) // 5) * 0.3 + \
        ((i + 15) // 24) * 0.1

def conv2(i):
    return int(i * 3.4 + 0.5) / 10 * 3600 / 1000

for i in range(0, 33):
    print('%2d: %4.1f %4.1f' % (i, conv(i), conv2(i)))
    continue

[2024-06-10 Minor corrections to table; inferred expression]
[2024-06-12 Linked to Kevin's post with "the answer"; corrected bit positions]

2023-08-19

Two logical interfaces on one physical, with Netplan

In my home network, I have a server which I want to appear under two hostnames, mainly so I can later move the functionality associated with one of them around to other hosts. I'm just using my ISP-supplied broadband router/modem to manage the network, but it doesn't exactly bristle with configuration options to make this directly possible with, say, a DNS alias. Nevertheless, I want to stick with it, as other solutions might involve duplicating a lot of its functionality, or splitting it across multiple hosts, both of which introduce their own risks.

The router provides local DNS resolution (in the .home domain), and it honours the hostnames specified by DHCP requests. By presenting two interfaces to it, a single host can get two IP addresses and so two distinct names. Yes, it's ugly and hacky, but it's a solution within the constraints.

Approach

In this specific example, enp3s0 is the physical interface, and the second hostname is media-centre. The approach is to create two virtual interface pairs (faux0-faux0br and faux1-faux1br), connect one end of each (faux0br and faux1br) to a virtual bridge (br0), and connect this to the physical interface enp3s0. The other two ends of the pairs (faux0 and faux1) are now on the same Ethernet network, and running DHCP on them causes them to acquire distinct IP addresses, and registers them under distinct DNS names.

IPv4 ARP

For IPv4, it's essential to prevent the two interfaces stepping on each other's toes regarding ARPs, and a Server Fault answer shows how. Put this in your /etc/sysctl.d/local.conf (or create a numbered file for it, say 99-dualiface.conf):

net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.all.rp_filter=2

That will apply on boot, but you can apply it immediately with sudo sysctl -p/etc/sysctl.d/local.conf.

Creating virtual interface pairs

At the time of writing, and as far as I can tell, Netplan can set up bridges, but not the veth pairs used in the previous solution. This Ask Ubuntu answer explains how to do it another way. For our case specifically, create /etc/systemd/network/25-faux0.netdev:

[NetDev]
Name=faux0
Kind=veth
[Peer]
Name=faux0br

Create /etc/systemd/network/25-faux1.netdev similarly:

[NetDev]
Name=faux1
Kind=veth
[Peer]
Name=faux1br

Connecting with a bridge

We create and define the bridge in the /network/bridges section of a YAML file in /etc/netplan/. I've called this one 99-bridgehack.yaml:

network:
  ethernets:
    enp3s0:
      dhcp4: false
    faux0:
      dhcp4: true
    faux0br: {}
    faux1:
      dhcp4: true
      dhcp4-overrides:
        hostname: media-centre
    faux1br: {}
  bridges:
    br0:
      link-local: []
      interfaces:
        - faux0br
        - faux1br
        - enp3s0

We enable DHCP on faux0 and faux1. The former announces itself using the server's own name by default, but we set the name explicitly for the latter. Note that we also disable DHCP on our original interface enp3s0, overriding the setting in /etc/netplan/00-installer-config.yaml:

# This is the network config written by 'subiquity'
network:
  ethernets:
    enp3s0:
      dhcp4: true
  version: 2

The section /network/bridges/br0/interfaces binds the backends of the veth pairs together with the physical interface. faux0br and faux1br must have some presence in /network/ethernets in order to reference them here, so they are set empty.

[Edit 2024-12-07] /network/bridges/br0/link-local is set to an empty list to prevent IPv6 addresses being assigned to the bridge. This isn't vital, but it might save you some head scratching about strange entries in your router's network device list.

Deployment

With /etc/netplan/99-bridgehack.yaml in place, you just need to tell Netplan about it. Any remote network reconfiguration risks you losing the very connection you're using to do it over, so this is best done on the server's console:

sudo netplan generate
sudo netplan apply

Maybe I did something wrong, but I would often find that Netplan would create new entities as requested, but not tear down old ones. A reboot ensures you're starting from a clean slate. If you make a mistake, you can always rename 99-bridgehack.yaml to disable it.

Déjà vu

I did this before, but without Netplan. I turned it off, and enabled legacy ifupdown functionality still available in Ubuntu 18.04. However, it's less clear how to do that on 22.04, so I had to find a way with Netplan. There was no need to mess with /etc/dhcp/dhclient.conf this time, which is good, as it didn't seem to make any difference. (Is dhclient being used any more?) The IPv4/ARP advice remains largely the same.

2022-08-25

The Brexit Song

To the tune of “Thank you for the music” by ABBA:

Thank you for the Brexit that keeps on giving
To the EU. You've lost your living.
Thank you for the workforce,
The jobs and all your money,
For sovereignty,
And for some bad trade deals you will see
Aren't worth the loss of all your farming,
Fishing and industry.

Feel free to develop.

2021-04-06

BT email rules not working

So, I just spent the evening rejigging my parents' email rules with BT. They seemed to stop working sometime in January 2021, and I've just worked out why.

BT have changed how comparisons like is and ends with work on the From: field (and possibly others). Previously, the email address was extracted from the field, so it didn't matter whether the whole text of the field read any of these ways:

From: j.bloggs@example.com
From: Joe Bloggs <j.bloggs@example.com>
From: "Joe Bloggs" j.bloggs@example.com

Can't be certain that I've remembered that third form correctly; it's in an RFC somewhere anyway. However, I don't think I've seen it for a long time, so I've going to assume it's fallen out of favour, and focus on the other two.

Under the new mechanism, From: is j.bloggs@example.com will only match the first form. You'll now also need a From: contains <j.bloggs@example.com> to guarantee a match. You can't use multiple operators like is and contains on the same field in the same rule, so you must duplicate the rule, and maintain it. You could, of course, match both j.bloggs@example.com and <j.bloggs@example.com> in the same rule with contains, and you'll probably get away with it, but you'll be left scratching your head when bob.j.bloggs@example.computing.invalid ends up in the same place. Also, if they change it back without notice, your is rule will continue to work.

From: ends with @example.com will also fail to match the second form. You need From: ends with @example.com> too now. Fortunately, you can do that with an extra entry in the same rule; you don't need a duplicate rule. However, bear in mind that you can only have 15 From: entries in a single rule.

To: and CC: can have multiple addresses. Some experimentation is required to determine whether they are automatically split and tested separately.

While I'm in gripe mode, BT rules could do with a few other features:

  • Match on List-Id: to pick out mailing-list posts unambiguously.
  • Filter out those damn subject-line tags like [zarquon users] that needn't pollute mailing lists when they've already been sorted into the right folder.
  • Mark messages as read.

2021-01-07

“Wrong __data_start/_end pair” work-around

I was getting Wrong __data_start/_end pair from my Mokvino Web scripts when converting ODG to SVG, since upgrading to Ubuntu 20.04 (though I've used Mokvino Web so little lately, I can't be sure that that's the start of the problem). It was an inkscape command that was failing. When I ran the command manually, I got no error. I found few differences in environment variables between running directly and running via make, and when I forced them to be the same in the script as in the console, it still failed within make and worked in the console.

A StackExchange question pointed towards a work-around. I checked the resource limit for the stack size (ulimit -s), and it was unlimited when run from make, but 8192 in the console. I bunged in a ulimit -s 8192 before the command, and it worked!

$ ulimit -s unlimited 
$ inkscape -z --query-all "example.svg" | head -2
Wrong __data_start/_end pair
$ ulimit -s 8192
$ inkscape -z --query-all "example.svg" | head -2
svg805,5848,8815,14472,4305.111
rect2,0,0,29700,21000
$ 

Can't say I understand what's happening here; just hope it helps.

2020-05-25

Weather station woes and fixes

I've set up some Raspberry Pis to export data from various FineOffset weather stations, using WeeWX as the server software periodically downloading records and presenting them graphically in a webpage. Each weather station comes with a dedicated console to receive transmissions from the outdoor sensors every minute or so, and the console has a USB socket to allow a host to configure it and extract data.

This console is known to have a “USB lock-up” bug, whereby it refuses to talk to the host after some random period (from a few days to a couple of months), even though it had been interacting successfully prior to that point. The only robust work-around is to power-cycle the console, which is not easy to automate. Here's what I had to do.

Detection

The lock-up bug now appears as Operation timed out in the WeeWX log, as given by sudo /bin/systemctl status weewx:

fousb: get_records failed: [Errno 110] Operation timed out

You can get essentially the same lines from grep weewx /var/log/syslog. Four of these appear (about 45 seconds apart), and then WeeWX seems to reconnect in vain, and gets another four, and so on. This cycle lasts about 3½ minutes.

Note that the WeeWX documentation on the matter identifies a different error:

could not detach kernel driver from interface

Maybe that means this isn't really the lock-up bug I'm getting, but the symptoms and treatment seem to be the same.

Recovery

You have to take the batteries out of the console, and ensure it is disconnected over USB. You can run the console off USB power alone, so for an unattended power cycle, you just need a USB hub that can depower its sockets. The big disadvantage of leaving the batteries out is that no readings are taken during a power cut; with the batteries in, you could at least pull them off the console when power returned, as it can store several days' worth.

Here are the Pis I'm using with each weather station:

Hostname Host model Weather station model WeeWX version
fish RPi 3B+ WH3083 3.9.2
ruscoe RPi 3B WH3083 3.9.2
kettley RPi 3B WH1080 3.9.1

All are running some version of Raspbian.

I've used uhubctl to check for and invoke the power-cycling feature, and can confirm that both the RPi 3B and 3B+ can control the power on their USB sockets. I also tried an RPi Zero W, which would have had the ideal amount of grunt for the task, but it's unable to control power on its sockets. Since I've not seen the problem on the WH1080, it could be used there, or indeed on any similar set-up with a different type of weather station. I was using an older RPi model at some point (with no built-in Wi-Fi); it could power-cycle its entire USB hub, although this included the USB Wi-Fi chip!

The output of sudo uhubctl looks something like this (on a 3B; it's marginally different on the 3B+):

$ sudo uhubctl 
Current status for hub 1-1 [0424:9514]
  Port 1: 0503 power highspeed enable connect [0424:ec00]
  Port 2: 0100 power
  Port 3: 0100 power
  Port 4: 0303 power lowspeed enable connect [1941:8021]
  Port 5: 0100 power

1941:8021 is the weather station console:

$ lsusb 
Bus 001 Device 014: ID 1941:8021 Dream Link WH1080 Weather Station / USB Missile Launcher
Bus 001 Device 013: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

This means that a command of the following form will power-cycle the console:

sudo uhubctl -l 1-1 -p 4 -a 2 -d 30 -R

Update: On fish, I had to set -p to one less than the reported port! sudo uhubctl said it was in port 3, but the command only worked on port 2. It helps to have someone looking at the console to confirm when you're doing it remotely!

  • -l 1-1 -p 4 are taken from the output of uhubctl, identifying the hub and port.
  • -a 2 causes a power cycle, rather than switching on or off.
  • -d 30 keeps it off for a generous 30 seconds. That maybe could be trimmed a bit.
  • -R resets the hub, forcing devices to re-associate. I found this to be essential, and wonder if it would be effective without the power cycle. Update: It isn't; you must remove the batteries.

Putting it together

A script, in ~/.local/bin/check-weather-station:

#!/bin/bash

count=0
while read line ; do
    if [[ "$line" == *"get_records failed: [Errno 110] Operation timed out"* ]] ; then
        ((count++))
    elif [[ "$line" == *"Stopping LSB: weewx weather system"* ]] ; then
        count=0
    fi
done < <(grep weewx /var/log/syslog | tail -50)

if [ $count -ge 4 ] ; then
    printf >&2 'Fault detected, power-cycling...\n'
    echo >&2 'Stopping station software'
    sudo /bin/systemctl stop weewx
    echo >&2 'Power-cycling hub'
    sudo /usr/sbin/uhubctl -l 1-1 -p 4 -a 2 -d 30 -R
    echo >&2 'Waiting for end of sensor-learning period'
    sleep 180
    echo >&2 'Setting time'
    sudo /usr/bin/wee_device -y --set-time
    echo >&2 'Setting interval'
    sudo /usr/bin/wee_device -y --set-interval=5
    echo >&2 'Restarting station software'
    sudo /bin/systemctl start weewx
fi

A cron job then checks every few minutes:

*/3 * * * * $HOME/.local/bin/check-weather-station

That should pick up the fault within one 3½-minute cycle.

Other aspects of the script:

  • Four “timed out” messaged are awaited. Maybe I could get away with two, or even one!

  • The weewx service is suspended during the reset. This ensures there's no interaction with the console shortly after it comes back on.

  • While the service is suspended, we don't want overlapping invocations of the script to do anything. This is detected by resetting the message count whenever we see that the service has been stopped. Only “timed out” messages that are not followed by a “stopping” message are counted.

    (There's a potential race condition here, but it's not going to happen unless parsing the log and stopping the service take more than 3 minutes.)

  • Waiting three minutes after the reset ensures that the console's sensor-learning mode is not jeopardized by external activity. The weather-station manual warns about key activity on the console during this time, and I suspect it actually extends to USB activity too. I've been very cautious, so it might be possible to trim the timing a bit.

  • The console's time is synchronized with the host's. This can only be done while the service is stopped. (Unfortunately, this does not seem to update the clock displayed in the console.)

  • The logging interval is set to 5 minutes. Apparently, the console can sometimes forget this after a power cycle, but this setting is thought to reduce the likelihood of lock-ups.

Other issues

  • One Pi wouldn't come back on after a power cut. Changing the power supply fixed that.

  • Another Pi seems to lose its Internet connection, but continued gathering data from the console. Being headless, the simplest thing for a non-technical person to do is to power-cycle the Pi, but that's a bit drastic, and undermines the goal of unattended operation. I tried the following to prevent the Wi-Fi from going to sleep, but it still happened:

    sudo iw dev wlan0 set power_save off
    

    I've resorted to pinging the router once a day.

Results

With a cruder detection mechanism (one that took about four or five log cycles to get lucky), I've seen the script work twice in just a couple of days. I'm trying out this new detection mechanism above, which should be safe to use as often as every three minutes, and so it should be able to detect the first cycle. I'll update this article as things develop.

2020-02-02

Removing variable prefixes and suffixes from other variables in Bash

Just been bitten by this…

If you have a variable txt in Bash, you can strip a given prefix or suffix from it like this:

$ txt=a/b.d/c.jpg
$ echo "${txt%.*}"
a/b.d/c
$ echo "${txt##*.}"
jpg
$ echo "${txt%%.*}"
a/b
$ echo "${txt#*.}"
d/c.jpg

The % operator strips of the shortest matching suffix, and .* matches .jpg, so that gets removed. %% strips off the longest matching suffix. Similarly, # and ## strip off the shortest and longest matching prefix, respectively. Asterisks, square brackets and other characters are special, probably following the same rules as Pattern Matching in the Bash manual page.

You can also use literal strings as the patterns, i.e., no special characters:

$ txt=a/b.d/c.jpg
$ echo "${txt%.jpg}"
a/b.d/c
$ echo "${txt%.png}"
a/b.d/c.jpg
$ echo "${txt#a/b.d/}"
c.jpg
$ echo "${txt#c/b.d/}"
a/b.d/c.jpg

Note that, if the prefix or suffix doesn't match (whether you use special characters or not), you get the whole string returned.

These operations are useful for traversing pathnames:

$ path="/home/john/file.jpg"
$ echo Leaf is "${path%%*/}"
Leaf is file.jpg
$ echo Dir is "${path#/*}"
Dir is /home/john

You have to be careful if your input doesn't contain the separator:

$ input1=path/to/file.jpg
$ input2=file.jpg
$ echo Input 1 dir "[${input1%/*}]" leaf "[${input1##*/}]"
Input 1 dir [path/to] leaf [file.jpg]
$ echo Input 2 dir "[${input2%/*}]" leaf "[${input2##*/}]"
Input 2 dir [file.jpg] leaf [file.jpg]

To avoid this special case, I thought I could do this:

input1=path/to/file.jpg
input2=file.jpg
input1leaf="${input1##*/}"
input1dir="${input1%${input1leaf}}"
input2leaf="${input2##*/}"
input2dir="${input2%${input2leaf}}"
echo "[${input1dir}]" "[${input1leaf}]"
echo "[${input2dir}]" "[${input2leaf}]"

…which leads to this:

[path/to/] [file1.jpg]
[] [file1.jpg]

However, I hadn't noticed that special characters are still interpreted after the partial expansion:

input3="path/to/file [2002].jpg"
input3leaf="${input3##*/}"
input3dir="${input3%${input3leaf}}"
echo "[${input3dir}]" "[${input3leaf}]"

The square brackets are taken as a wildcard, and fail to match the literal value:

[path/to/file [2002].jpg] [file1 [2002].jpg]

The trick is to quote again:

input3="path/to/file [2002].jpg"
input3leaf="${input3##*/}"
input3dir="${input3%"${input3leaf}"}"
echo "[${input3dir}]" "[${input3leaf}]"

Now you get the intended result:

[path/to/] [file1 [2002].jpg]

An alternative technique would be to use the length of your prefix/suffix in a substring operations, but it's less convenient and more error-prone if you want to do small adjustments to a prefix or suffix before applying it.

Anyway, in summary, if you're going to use Bash's prefix/suffix removal with a computed pattern, put the result in quotes!

Bash redirection with descriptor in variable, and locking

A recommended way to acquire a lock in Bash is to open the lock file for a group command, and call flock on the open descriptor before doing anything dangerous:

{
  echo waiting
  flock -x 9
  echo in
  sleep 10
  echo done
} 9> /tmp/lock

Try it in two independent terminals. The second command will run only as the first finishes.

However, one should never have to pick an arbitrary file descriptor (9 in this case). Fortunately, you can get Bash to choose an available descriptor, using {var} in place of the literal descriptor number:

unset lfd
{
  echo waiting
  flock -x $lfd
  echo in
  sleep 10
  echo done
} {lfd}> /tmp/lock

Problem solved!

No, wait. The descriptor doesn't get closed at the end of the group command, so your second invocation will hang indefinitely. Once the first terminal has finished, if you manually close the descriptor, the second proceeds:

exec {lfd}>&-

Looks like you have to do things more explicitly (and the group command is no longer useful):

echo waiting
unset lfd
exec {lfd}> /tmp/lock
flock -x $lfd
echo in
sleep 10
echo done
exec {lfd}>&-

This is inconvenient if you want to break or continue out of an enclosing loop:

for i in $(seq 1 10)
do
  {
    echo waiting
    flock -x 9
    echo in
    sleep 5
    if something_went_wrong ; then continue ; fi
    sleep 5
    echo done
  } 9> /tmp/lock
done

Is this a bug, a feature, or a mistake on my part? (Bash version 4.4.20(1)-release.)

2019-08-17

procmail to IMAP

My employer provides an IMAP server for email. Historically, it behaved badly with Message-Id headers, overwriting the original (the first sin, breaking all threading), and overwriting it with a globally indistinct value. It also had limited filtering capabilities. So I avoided it, and eventually set up my own home IMAP server (using Dovecot, Postfix and Procmail). I still had to get emails forwarded from the employer-provided server, and was forced to use ‘forward as attachment’ to ensure that the message id was preserved. An earlier post was on how to extract the email attachment to a pipe for interfacing with Procmail, but the latest incarnation of the employer-provided server has a proper redirection facilty, so that's redundant for me now.

Problem

Running your own IMAP server at home is no fun, especially with a dynamic IP. You have to have a Dynamic DNS service, and hope no-one else gets your mail while it's re-syncing. I need the filtering with procmail, but it only delivers to mailboxes in mbox and Maildirs formats directly (and maybe one other…?); everything else has to be handled through an external command. Is there a beast that will deliver to an IMAP server? If so, I could redirect the mail via SMTP to my own host running procmail, and have it deposit the mail in specific folders back on the server via IMAP.

Stack Overflow has one question on procmail and delivering to an IMAP server?, and suggests the use of mailtool, which is part of the Courier mail server. However, I don't want to install all of Courier to get it, and an apt-file search can't seem to find it, so I suspect it's also ‘historical’.

Solution

Python to the rescue! (I hate Python syntax, by the way. Indentation for syntax is a step backwards, in my opinion. Still, it works…) Here's a small program pushimap, which uses imaplib to write a file to an IMAPS server:

#!/usr/bin/env python

import imaplib
import time
import ConfigParser
import os
import sys
import email.message
import email

import getopt
from pprint import pprint

if __name__ == '__main__':
    ## Parse arguments.
    cfg_name = os.environ['PUSHIMAP_CONFIG'] \
               if 'PUSHIMAP_CONFIG' in os.environ \
                  else os.path.expanduser('~/.config/pushimap/conf.ini')
    acc_name = os.environ['PUSHIMAP_ACCOUNT'] \
               if 'PUSHIMAP_ACCOUNT' in os.environ \
                  else 'default'
    msg_file = None
    mb_name = 'INBOX'
    flags = ''
    opts, args = getopt.getopt(sys.argv[1:], "f:a:d:s")
    for opt, val in opts:
        if opt == '-f':
            cfg_name = val
        elif opt == '-d':
            mb_name = val
        elif opt == '-a':
            acc_name = val
        elif opt == '-s':
            flags += ' \Seen'
    flags = flags[1:]
    if len(args) == 0:
        args.append('/dev/stdin')

    ## Read the account details.  Should include port too.
    config = ConfigParser.ConfigParser()
    config.read([os.path.expanduser(cfg_name)])
    hostname = config.get('account %s' % acc_name, 'hostname')
    username = config.get('account %s' % acc_name, 'username')
    password = config.get('account %s' % acc_name, 'password')

    ## Connect to the server.
    c = imaplib.IMAP4_SSL(hostname)
    try:
        ## Authenticate with the server.
        c.login(username, password)

        ## Process the plain arguments as filenames.
        for fn in args:
            if fn is None or fn == '':
                continue

            ## Read in the message.
            fp = open(fn, "r")
            try:
                msg = email.message_from_file(fp)
            finally:
                fp.close()

            ## Attempt to add the file's contents as a message, or
            ## create the folder and try again.
            created = False
            while True:
                typ, erk = c.append(mb_name, flags,
                                    imaplib.Time2Internaldate(time.time()),
                                    str(msg))
                if typ != 'NO':
                    sys.exit()
                if created:
                    sys.exit(1)
                typ, erk = c.create(mb_name)
                created = True
    finally:
        c.logout()

-f file specifies an INI file to hold credentials and the server address, and defaults to $PUSHIMAP_CONFIG, then to ~/.config/pushimap/conf.ini. The file should contain the likes of:

[account default]
hostname = imap.example.org
username = bloggsj
password = mind-your-own-sodding-business

-a acc specifies a section [account acc] to read fields from, and defaults to default.

-d mailbox specifies a mailbox on the server to add a message to, and defaults to INBOX. Some servers seem to use / (U+002F) as a folder-name separator, and others use . (U+002E, full stop). You could check by temporarily modifying the code to run typ, data = c.list() ; pprint(data).

-s says that messages should be flagged as ‘seen’, i.e., read.

Remaining arguments are filenames. If none are given, /dev/stdin is assumed. Each file is read as an mbox-formatted message (what will it do with an mbox with more than one message?), and appended to the specified mailbox. If that fails, an attempt is made to create the mailbox, then the appending operation is tried again. If that fails, the program exits with a non-zero status. This can be used with Procmail's W flag, in which the exit status of the piped command determines whether to continue filtering.

See imaplib - IMAP4 client library - Python Module of the Week for some example uses of imaplib. The c.list() call is useful in determining what the folder-name separator is.

Now let's Procmail it up:

PUSHIMAP_CONFIG = $HOME/.myimapstuff

:0
* ^List-Id:.*<some\.list\.example\.org>
{
  :0 W
  | pushimap -d "Work/My Employer/Mailing list"

  :0
  ".Work.My Employer.Mailing list/"
}

Caveats

  • As a precaution, I'm keeping the old Maildirs action, but it's only done if the pushimap command fails.

  • I used email.message_from_file in solving the earlier problem (extracting an attachment from a pipe), and it seemed to cope okay with fairly big messages, but I'm not sure. Ideally, it would transparently cache them on disc when they went over a threshold. It's quite possible that the script will simply terminate abnormally, so the :0 W flag is being relied upon to ensure some back-up form of delivery.

2019-04-12

Notes on bug JDK-8162455

I submitted bug JDK-8162455 some time ago (2016, so it says), and it's not a very important bug, so I can reasonably expect its fixing to be at a very low priority. Nevertheless, I'd like to comment on it, especially as it seems my original report wasn't terribly clear.

What's the problem?

As the bug report describes, if you specify an annotation-processor option to the Java compiler, and you correctly provide the corresponding processor to the compiler, you can get still a warning of the form:

The following options were not recognized by any processor: a list including that option

…if a certain part of the processor is never invoked (because none of the annotations it recognizes are present in the source). You also get it if you fail to specify the processor correctly (e.g., you get the classpath or the class's name wrong, or specify the service incorrectly, etc), so it's useful there. And you get it if you spell the option wrongly, so that's useful too. The problem is that either:

  • the logic that determines which options are to be reported is faulty, or
  • the message is faulty.

Which fault applies depends on what the intent of the warning is.

What's the intent?

The class com.sun.tools.javac.processing.JavacProcessingEnvironment implements this behaviour. It creates a set of the names of all provided options before annotation processing properly begins. Then, during that processing, it selectively removes entries matching those recognized by processors that it has just called to process a round. When all processing is complete, it generates the warning if the set is not empty.

Assuming its implementation accurately expresses its intent, a more accurate message would be:

The following options were not recognized by any engaged processor: options

…where engaged means having a processing round submitted to it.

But what use is knowing the options of an unengaged processor? If a processor had not been engaged, all of its options would be reported this way, so you're never going to be told about some of its options but not others (unless another processor was engaged, and unusually happened to use some of the same options). If this isn't the real intent, the logic must be faulty.

What if the intent is to inform about unengaged processors?:

  • Why not simply identify the processors directly?
  • If the options of an unengaged processor are also all recognized by an engaged processor, you won't be informed of the unengaged processor.
  • If an unengaged processor has no options, again you won't be informed.

The current logic doesn't robustly achieve this intent either. I have to presume that the message already expresses the intent.

What's the solution?

How should the logic be fixed? In JavacProcessingEnvironment, don't bother gradually eating away at the set of option names each time a processor is engaged. Instead, just before you check whether the set is empty at the end, go through all processors, and remove the options they support. The remainder is the set to report. A private method checks the set of remaining options, and reports if non-empty. You just have to eliminate the recognized ones before checking:

private void warnIfUnmatchedOptions() {
    for (ProcessorState ps : discoveredProcs)
        ps.removeSupportedOptions(unmatchedProcessorOptions);
    if (!unmatchedProcessorOptions.isEmpty())
        ...
}

So, although it's a fairly inconsequential bug, the fix is also pretty trivial, assuming that the iteration over the processors changes no other state, and that the intent is as I've presumed.

2019-02-15

Genuinely scalable SVGs with width and height attributes

It seems some software doesn't know what to do with an SVG that has no width and height attributes. These attributes have always bothered me a bit. What's so scalable about forcing your vector graphic to be a specific number of pixels or inches wide or high?

Or is it only to be taken as a hint? Firefox (65.0) doesn't think so. It fixes the image at the specified size, and treats unitless values as pixels (or maybe virtual pixels).

The desktop background under Kubuntu (18.04 as I write) does seem to take it as a hint, and a mandatory one at that. If the icon to be used for a *.desktop file is an SVG, it might display it. If you specify width and height in millimetres (say), it seems to rasterize at the size you specify, then scales the bitmap up or down as required. But if you don't specify width and height, you end up with a carefully labelled blank space on your screen.

How do you satisfy both requirements? The solution seems to be to use percentages. Check the last two fields of the viewBox attribute:

<svg viewBox='68932 -240980 190516 153908'
     ... >

Divide the smaller by the larger, and express as a percentage:

$ bc -lq
153908/190516*100
80.78481597346154653600

If the last number is smaller, use the computed value as the height; otherwise, use it as the width. Set the other attribute to 100%:

<svg viewBox='68932 -240980 190516 153908'
     width="100%" height="80.78%"
     ... >

This seems to allow Firefox to scale according to available space, while the desktop deigns to display a decently detailed icon.

I've updated Mokvino Web to re-include width and height with these computed values. I've updated my earlier article too.

Update 2021-04-06: It now looks like you should set both width and height to 100%, for the Kubuntu desktop and for Firefox at least. Bloody hell. More investigation…

2018-09-10

Issues mounting MTP on Kubuntu

I've had difficulty getting Kubuntu to mount a Samsung Galaxy S8 reliably. Using the Device Notifier gets as far as showing directory structure and thumbnails, but the MTP process dies if you try to read a file properly. Also, the phone asks the user whether it should be accessed via USB only after the first mount attempt. If you say “Allow”, it withdraws its current configuration (thereby invalidating the first mount), and re-offers it (thereby causing the Device Notifier to pop up again, and requiring the user to open another window). Perhaps it's a clash between USB and Android requirements: (say) the phone must respond to a mount-triggered USB request at once, but Android also has to wait for user authorization, and has no way to asynchronously inform the host of new files appearing on an existing mount? To get anywhere, I've had to abandon the Device Notifier, install jmtpfs on the host, and run it manually, and twice. I've also had to enable Developer Options on the phone(?!).

Now I'm trying to write an auto-mounting script for a headless machine, so that the latest photos and videos I've recorded on my devices can be automatically moved off the device simply by plugging it in. The files will later be dropped into an ingest process to make them ready for presentation over DLNA. I use this to watch for USB devices being plugged and unplugged:

$ inotifywait -m -r /dev/bus/usb -e CREATE -e DELETE
/dev/bus/usb/001 CREATE 010
/dev/bus/usb/001 DELETE 010
/dev/bus/usb/001 CREATE 011
/dev/bus/usb/001 DELETE 011

CREATE and DELETE events specify the bus number and device number (e.g., 001:010) when the phone is plugged in or unplugged. The output of lsusb -v -s 001:010 provides the vendor id and serial number of the device, and whether an MTP interface is provided, so events for non-MTP devices can be ignored.

On plugging in, the CREATE event is received. The phone lights up, but doesn't yet ask the user if the host has permission to access its files. I ensure a mount point exists, and run jmtpfs on it, specifying the device id:

mkdir -p "/var/mtp/$vendid-$serialno"
jmtpfs -device=001,010 "/var/mtp/$vendid-$serialno"

This triggers the phone to ask for authorization from the user. Although the response is still pending, the mount appears to succeed, so I proceed to scan the mount point for interesting files with find. All attempts to scan or access fail with I/O errors, so there's nothing to do but unmount with:

fusermount -u "/var/mtp/$vendid-$serialno"

Now I tap “Allow” on the phone, and I get a DELETE for 001:010, immediately followed by CREATE for 001:011. The device id has changed, but the vendor id and serial number are the same, so the same mount point is used, and mounts without error as before. This time, a scan of files succeeds, and unmounting can take place when they have been processed.

So, the trick seems to be:

  • Expect failure and assume a retry will occur. (If it doesn't, it obviously wasn't that important.)
  • Use the vendor id and serial number to avoid treating the retry as new device. (You don't actually need to remember that there was an earlier failure, just make sure that your action in the second cycle tries to do exactly what it tried to do before.)

2018-09-09

Two logical interfaces on one physical, on Ubuntu 18.04 without Netplan

If my ISP-provided home gateway allowed DNS aliases to be configured, I'd get it to map foo.home to bar.home, a headless server. foo.home is meant to be present in my home network and in my relatives', to identify a host providing write access at each site to a library of photos, videos and music that are synchronized between sites. The home gateway has no such aliasing feature, so I've done it by adding an interface on the bar.home host. The new interface looks like a different host to the gateway, so it can have a different name. It happens to get a different IP too.

I could have achieved largely the same with the spare wireless interface, but why use up airwaves to travel 1 foot between a static host and the access point? I could have bought a USB Ethernet dongle, but I did it without any extra hardware or using up a socket on the gateway by creating a virtual interface faux0 piggybacked on the physical wired interface enp3s0. Here's what I did on Ubuntu Server 18.04.

From Netplan to Ifupdown

Ubuntu 18.04 uses Netplan by default. Its configuration files match /etc/netplan/*.yaml. Older Ubuntus use ifup and ifdown which read configuration from /etc/network/interfaces, and ifup -a is run at boot to bring up all marked interfaces. I'd hoped to configure Netplan to set up two logical interfaces on one physical one, with different MAC addresses, and each making DHCP requests with different names, but it doesn't seem to have any way to do that. According to Netplan documentation, installing the package ifupdown is sufficient to disable Netplan:

sudo apt-get install ifupdown

Now you need configuration to make ifupdown perform Netplan's duties:

# In /etc/network/interfaces
auto lo
iface lo inet loopback

auto enp3s0
iface enp3s0 inet dhcp

The interface name enp3s0 is the host's sole wired Ethernet device. Yours might have a different name, perhaps the traditional eth0. You can list all interface names with:

ip link show

Just to make sure, I also renamed 01-netcfg.yaml to 01-netcfg.yaml-disabled. That's the only file I found in /etc/netplan/, so that really should render it inert, as Netplan doesn't modify interfaces it does not match in its configuration.

Things that didn't work

I also investigated removing the package netplan.io, but was told that that would also remove ubuntu-minimal. I suspected that might be a bad idea. There's also a package netplan, which also provides the /usr/sbin/netplan binary, but it was not installed.

Creating the second interface

With ifupdown now responsible for interface configuration at boot, define the new interface:

# In /etc/network/interfaces
auto lo
iface lo inet loopback

auto enp3s0
iface enp3s0 inet dhcp

auto faux0
iface faux0 inet dhcp
pre-up ip link add faux0 link enp3s0 address XX:XX:XX:XX:XX:XX type macvlan
pre-up /sbin/sysctl -w net.ipv6.conf.faux0.autoconf=0
post-down ip link delete faux0

I named the new, virtual interface faux0. It's created with an ip link command just before the interface comes up, and similarly deleted just after being taken down, using the pre-up and post-down directives.

The pre-up /sbin/sysctl is not essential, but disables SLAAC on the interface, which is appropriate for virtual interfaces. Don't know whether I'll need it, but the interface seemed to be accumulating a lot of IPv6 addresses, so I'll try it and see.

The new interface has a distinct MAC address XX:XX:XX:XX:XX:XX, specified as it is created. I've borrowed one from a device I know will not be seen on my home network, but there's probably a better strategy, something that a virtualization system employs, perhaps. It's not something I've looked into yet. Maybe someone will explain in a comment, because I get a lot of those. The interfaces file format also has a hwaddress setting, but it seemed to have no effect.

The new interface is configured to request a DHCP lease with the name foo:

# In /etc/dhcp/dhclient.conf
interface "faux0" {
  send host-name "foo";
  send dhcp-client-identifier 1:XX:XX:XX:XX:XX:XX;
}

I've thrown in a dhcp-client-identifier setting, but I'm not sure how vital it is. It seems you can use any string (with quotes if necessary), and it's just to stop the gateway from thinking the two DHCP clients are the same, leading to both interfaces coming up as under the same name in the gateway's web interface, making it less clear what you're port-forwarding to. However, that could have been caused in my case by bad data cached in the gateway, flushed out by leaving the server off while deleting the entries in the gateway. I'm going to leave them the setting in for now, as it seems harmless. I explicitly set an identifier for the main interface too, for completeness.

Things that didn't work

Setting the hostname for the interface with a hostname directive in /etc/network/interfaces didn't work because dhclient doesn't recognize it. Hence, it is set in dhclient's own configuration.

Setting the MAC address with a hwaddress also didn't work.

ARP flux

ARP flux can be a problem. Both interfaces can respond to ARP requests for either of their IPs. My home gateway then detects that both IPs map to the same MAC, and therefore to the same hostname, so both names end up resolving to the same IP. The other address, though it gets properly assigned to the right interface, never gets used. Functionally, this is fine, and actually meets the goal of having DNS aliases. However, it messes up the rendition and editing of port-forwarding rules in the gateway. If you have a rule forwarding to the disused MAC, its IP has no name, so the IP is displayed as the destination, not the hostname. It's also impossible to select that IP as a destination, because you can only select by name on this particular gateway.

To fix this, it's possible to disable the interfaces responding to ARP requests on behalf of each other, and the following seems to the right combination of settings to avoid one of the interfaces going dead (according to this serverfault article):

# In /etc/sysctl.conf
net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.all.rp_filter=2

You can test these temporarily with the likes of:

sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
sudo sysctl -w net.ipv4.conf.all.arp_announce=2
sudo sysctl -w net.ipv4.conf.all.rp_filter=2

Things that didn't work

Not setting rp_filter results in one of the interfaces being unable to receive traffic, effectively leaving it dead.


That should be it. Rebooting should put that into effect, but without rebooting, this should be enough (from the machine's console, not remotely!):

sudo ifdown enp3s0
sudo ifup enp3s0
sudo ifup faux0

In summary:

  • Install ifupdown.
  • Remove or rename Netplan files matching /etc/netplan/*.yaml to disable Netplan.
  • Create entries in /etc/network/interfaces to take over Netplan's duties, and augment to set up the extra interface.
  • Tell dhclient to use foo instead of the machine's hostname.
  • Take steps to prevent ARP flux.

Happy now?


[Edited to include notes on ARP flux.]

2018-08-18

JDK9 doclet API frustration

The new Javadoc doclet API promises a better view of Javadoc comments than before, one consistent and integrated with other source-related tools. I recently decided that my old doclet (“ssdoc”) based on the old API was becoming unmaintainable, and that I should start writing afresh against the new API (“Polydoclot” at the same location).

One way that the new API helps is that HTML tags and entity/character references in Javadoc comments are distinctly parsed along with Javadoc tags, important if your doclet is generating something other than HTML. If you were writing XHTML, you'd have to recognize empty HTML tags, and infer implicit closing of (say) <p> by a <div>, so that you could meet the strict requirement of XHTML that all elements are properly closed. For LaTeX output, references like &amp; would first have to be decoded into & before being re-escaped as \&.

So, that's a big improvement. However, I've found a few faults (at least, as I deem them) in the new API/implementation:

  1. It does not resolve understood HTML entity/character references, even though the new API obviates retaining them in their original form. (The old API did not have this option.)
  2. It does not resolve context-sensitive signatures in {@link}, {@linkplain}, {@value} and @see tags any more. (It used to!)
  3. It does not recursively parse the content of unknown in-line tags. (It used to!)
  4. Unknown in-line tags have their own class UnknownInlineTagTree, instead of simply being of the supertype InlineTagTree. Similarly, unknown block tags have their own class UnknownBlockTagTree, instead of simply being of the supertype BlockTagTree. This causes problems when tags defined in future JDKs are supplied to doclets compiled against older APIs.
  5. By now, there ought to be a formal way of determining how to link to elements within a Javadoc installation. (It keeps changing, and pinning it down would be too restrictive for alternative doclets.)

Here are those points in detail.

Lack of HTML reference resolution

The new API parses Javadoc source looking for Javadoc tags, HTML tags, and HTML entity/character references, and has distinct classes to represent each of these three groups. Since the HTML tags are distinctly represented from plain text by StartElementTree and EndElementTree, HTML references no longer need to remain escaped, and could just appear as the resolved character in a TextTree. The only times that can't happen are when the referenced entity is not recognized, or when it maps to a character not expressible in a Java string. Otherwise, why not just resolve them away? Whether you're generating HTML or something else, the escaping is only required within the source. The resolution has to be done whatever the output, and it is the same whatever the output.

Lack of signature resolution

The old API modelled {@link}, {@linkplain}, {@value} and @see tags with the SeeTag class. Javadoc would parse (say) {@link Service#close()}, work out that Service referred to (say) org.example.Service based on imports, on nested class declarations of the file containing the {@link}, or on the enclosing package, pick the zero-argument method called close from it, and then provide references to the modelled method through SeeTag.referencedMember().

In the new API, {@link}/{@linkplain}, {@value} and @see tags are modelled with LinkTree, ValueTree and SeeTree respectively. The first two each provide a ReferenceTree directly, and SeeTree provides one as the first element of its content, as it's meant to cope with other kinds of references. In turn, ReferenceTree provides just a flat string taken unchanged from the tag. This requires the doclet author to write some 300 lines of code to meet this contract:

/**
 * Resolve a signature in a given element context.
 *
 * @param context the element whose documentation
 * provided the signature
 *
 * @param signature the flat, unresolved signature,
 * as provided by ReferenceTree.getSignature()
 *
 * @return the corresponding element, or null if
 * not found
 */
Element resolveSignature(Element context, String signature);

I imagine this design decision is based on not wanting the Javadoc tool to do things that are doclet-specific. But how else should {@link} be interpreted? The output might be different between (say) HTML and LaTeX, but it still fundamentally refers to the same program element, independently of how the doclet will choose to use it!

Lack of recursive parsing of in-line tag content

In the old API, if an unknown in-line tag was encountered, it would be modelled as a plain (unspecialized) Tag, but its content would be parsed as a sequence of inner tags, accessible through inlineTags(). In the new API, the content is just a flat string! Yet UnknownInlineTagTree.getContent() returns a list of documentation tree nodes, implying that the content should have been recursively parsed. Instead, it returns a list of exactly one TextTree. This requires an explicit parsing routine that reparses arbitrary text according to Javadoc rules, and the only way I could find to do that was to spoof a FileObject with the content wrapped in <body>.

Again, this looks like a design decision to avoid the Javadoc tool from doing something doclet-specific, but Javadoc already has to impose some basic structure on the content, i.e., braces of nested tags have to match up, so it can't be left as free-format for the doclet. And, if Javadoc is going so far as to parse the braces, it might as well finish the job, especially since &#123; and &#125; will be needed to escape any braces to be passed literally to the doclet, which means &amp; will also be needed. Then, the documenter shouldn't have to remember which characters need to be escaped based on context (especially if the doclet doesn't recognize a tag), and the doclet author shouldn't have to re-escape & or re-piece together the parsed components just so that the rest of it can be re-interpreted as Javadoc+HTML again.

An alternative might be for the doclet to be able to declare which tags it recognizes, which ones should have their content parsed, etc. A method declareTags(TagTypes tts) on Doclet could be invoked at a sufficiently early stage to collect that information. It would be an opportunity to specify argument syntax in general too, as you might want to define {@link}-like tags that take an element reference as an argument, for example. However, that forces that documenter to be over-conscious of whether an extension tag will be recognized.

Special classes for unknown tags

So, there's a class BlockTagTree, the base type for all block tags. It also has a subtype UnknownBlockTagTree, which adds a method to get the parsed content of the block tag. What if a previously unknown block tag @foo starts being recognized by a new Javadoc implementation and API? You'd have a new FooTagTree class extending BlockTagTree, but now the object representing the tag can't go to the same places as it did when it was unknown. Sure, the visitor type probably has a new method on it to accept the new type, but if the doclet was compiled against the old API, it cannot override that, and it won't go through visitUnknownBlockTag(), because it's the wrong type. Fortunately, the doclet can specify the most recent version of Java (and Javadoc, implicitly?) it recognizes, allowing Javadoc to deliberately fail to recognize the new tag. Does it do that for block tags? Not sure yet.

It doesn't do that for in-line tags! JDK10 introduces a {@summary} in-line tag to be used to explicitly delimit the “first sentence” of an element's description, when application of the default rules (“Look for the first dot and whitespace.”) leads to the wrong result. It also defines a SummaryTree class to represent this. However, even though my doclet's highest language version is declared as 9, the {@summary} tag doesn't come through as UnknownInlineTagTree, so it is ignored, and the most important content of the documentation goes missing. My doclet is compiled against 9, so SummaryTree is not available, so the doclet has no option to provide a special visitor for that case. If I compile against 10, it won't be runnable against 9, because SummaryTree will be unavailable at runtime.

If UnknownInlineTagTree were to be abolished, with InlineTagTree subsuming its functions, a JDK10 default implementation of visitSummary(...) (which no JDK9 doclet can override) could call visitUnknownInlineTag(...) (which would now take an InlineTagTree instead of UnknownInlineTagTree), and some sensible default action could be taken, leading to some future-proofing for doclet implementations.

(This ties in with the generic, recursive parsing of in-line tags. The method getContent() is on UnknownInlineTagTree, but moving it to InlineTagTree kind-of implies that you unconditionally parse all tags' content, whether the tag type is known or not.)

Not distinguishing between block and in-line tags

Now that JDK10 recognizes the in-line {@summary}, it tramples on my own @summary, even though it's a block tag. These are syntactically distinguishable!

Lack of mechanism to derive URI for element documentation

The original Javadoc mapped methods to simple fragment identifiers, so foo(String,int) became #foo(java.lang.String, int). Later versions of Javadoc changed the scheme to avoid brackets and spaces, possibly to make it more compatible with (say) the more limited XML fragment-identifier syntax. It also used to erase parameter types, but later versions do not, and varargs are no longer flattened into arrays. (And wtf? Brackets are back in 10!) This makes linking to an installation generated by a different doclet awkward.

By now, there ought to be a formal way of determining how to link into a Javadoc installation without having to be the doclet that created it. For both the old and new versions of my doclet, I came up with the following. The doclet should generate (say) doc-properties.xml alongside package-list or element-list. This would be the XML representation of a Properties object, a property of which describes how to mechanically generate links to the documentation of specific elements, relative to the documentation's base address. Another doclet, told to -link to such an installation, would look up doc-properties.xml (in the same way it must already look up package-list/element-list), extract a well-known property, and use its value in a MacroFormatter. This would automatically tell it how to link within the site, while independently using its own scheme, which it can express to other doclets through the same mechanism. The format string would be arcane, e.g.:

{?PACKAGE:{${PACKAGE}:\\.:/}{?CLASS:/{${CLASS}:\\.:\\$}{?FIELD:-field-{FIELD}:{?EXEC:-{?CONSTR:constr:method-{EXEC}}{@PARAMETER:I:/{?PARAMETER.{I}.DIMS:{PARAMETER.{I}.DIMS}:0}{${PARAMETER.{I}}:\\.:\\$}}}}:/package-summary}:{${MODULE}:\\.:\\$}-module}

…but it's only meant to be machine-readable.

I chose XML as it obviates charset issues. Simply serve as application/xml. A Properties object leaves room for expansion, and you could probably deprecate package-list/element-list altogether by incorporating their information into the same doc-properties.xml, although retaining the simpler format could still be useful for interfacing with other languages.

Summary

Please, authors of javadoc:

  • Specify contractually that the documentation author shall write literal text, HTML element tags, HTML references, and Javadoc in-line tags (recursively containing such structured content) in the bodies and block-tag content of Javadoc comments, regardless of the documentation output format. Javadoc shall supply literal text, HTML element tags, unrecognized HTML references, and Javadoc in-line tags to the doclet, regardless of the documentation output format.
  • Resolve HTML references into their corresponding unescaped text, if possible, and merge with adjacent literal text.
  • Specify that unrecognized in-line tags should be interpreted as if only their content existed.
  • If you're going to make the effort of recognizing @see, {@link} and {@value} tags, bother to resolve the signatures within them to Elements too.
  • Either uniformly parse all tag's content recursively, or introduce a means for the doclet to declare tags whose content should be recursively parsed. Failing that, at least expose the routine to do the parsing directly, rather than forcing the doclet author to draw such a routine out of the API's own rectum.
  • Introduce a means for a doclet to declare tags whose arguments should be resolved as element references.
  • Move the methods of UnknownBlockTagTree and UnknownInlineTagTree to BlockTagTree and InlineTagTree respectively, and deprecate Unknown*TagTree.
  • Devise and specify a technique for expressing how to link with documentation elements, something that can be statically served with the documentation just like package-list already is.

Fixing SDDM scale on 4K screens

I'm running Kubuntu 18.04 on a 4K screen*, and everything is tiny. I can fix the desktop when I'm logged in by scaling the display in the “Display and Monitor” settings. This doesn't affect the display manager's screen before you log in, though. As a note to myself if I have to do this again, I modified /usr/share/sddm/scripts/Xsetup, adding this to the end:

xrandr --output eDP-1-1 --fbmm 346x194

That file is obviously for SDDM only. Other display managers might have a similar script in a different location.

The string eDP-1-1 and the screen's physical size are given by xrandr:

$ xrandr --query | grep ' connected'
eDP-1-1 connected primary 3840x2160+0+0 (normal left inverted right x axis y axis) 346mm x 194mm

I suspect that the reported dimensions might only be accurate after you've applied scaling in the desktop.

*(Why did I get a 4K screen? Twenty years ago, I might actually have been able to see the difference…)

2018-03-21

Effective defaults for equals and hashCode in Java?

So is it not possible to do this:

package java.lang;

public interface RootInterface {
  default boolean equals(Object other) {
    return this == other;
  }

  default int hashCode() {
    return System.identityHashCode(this);
  }
}

Then interpret all interfaces that don't extend anything as implicitly extending RootInterface? Then remove equals and hashCode from java.lang.Object, and get it to implement RootInterface?

package java.lang.Object;

public class Object implements RootInterface {
  ... // no hashCode or equals
}

Result: Interfaces can provide effective defaults for equals and hashCode? Nothing else breaks (except that RootInterface might be better off in a package not implicitly imported)?

This round tuit was brought to you by avoiding real work.


Update: It's possibly a bad idea for interfaces not to implicitly extend Object, as <?> and <? extends Object> then wouldn't be able to match any interface type, even though you could be sure the underlying object was certainly an Object.

2018-01-02

EU Cookie Law dumbness

I've wanted to say something about this for a long time, but never got a round tuit.

The “EU Cookie Law” is supposed to give website visitors the right to refuse the use of cookies. The way this seems to be interpreted is that sites that use cookies must place an intrusive warning over their content for new visitors, advising them that cookies are in use, possibly offering some cookie settings and a policy for the site, and generally obtaining consent to use cookies. After some explicit or implicit action by the visitor, the warning goes away, and that particular visitor is never bothered with them again.

But there's a problem. The site remembers that the visitor has seen the warning by using a cookie! This means that you cannot use the site without using a cookie!

And it's all so pointless. Visitors already have the ability to refuse the use of cookies by configuring their browsers. Granted, not everyone is aware of this, and knows how, and browsers' configuration capabilities may vary, but it's a browser problem.

The worst part is that the cookie law prevents this browser problem being solved in the browser. If you turn cookies off, the site can't remember that you've already been warned, and always puts up the warning, often obscuring essential parts of the content.

Here's a site that seems to explain the Cookie Law, but also looks like it offers cookie compliance services (despite its .org suffix): The Cookie Law Explained The Cookie Law is a piece of privacy legislation that requires websites to obtain consent from visitors to store or retrieve any information on a computer or any other web connected device, like a smartphone or tablet.


Here are some more details, updated 2022-04-02.

Exascerbations

There are several variations to the way cookie consent is obtained, and these can make the problem worse:

  • The cookie consent form often pops up over the page content, and sometimes prevents scrolling, making the content inaccessible until the form is submitted.

    (I suspect the law requires the consent request to be ‘prominent’, and no site wants to risk being regarded as less than that. A visitor is likely more motivated to click it away as soon as possible too, the more intrusive it is.)

  • The consent form often dazzles with hundreds of options. Many sites will fortunately show all consent turned off (where possible) by default, but some don't. Most sites display the ‘Consent to all’ submission button much more prominently than the ‘Save current options’ button. Few have a ‘Reject all’ button, and are misleading anyway, since a cookie will be used to record the lack of consent.

  • JavaScript is often required to submit the consent form, so the user has to whitelist the site for JavaScript before he has had an opportunity to check the content, and judge whether it's worth the risk.

  • When the consent rejection cookie expires, you go through it all again. I dare say, sites are not motivated to renew it automatically.

Alternative solution

A better solution would be to allow visitors to exploit the fact that not retaining a cookie is sufficient to implement lack of consent, and then it's a matter of having browser functionality that lets the user choose which cookies to retain. The law should work more like this:

  1. As with the current law, require sites to classify their cookies by purpose. Cookie consent pop-ups often indicate that some of the site's cookies are essential for the functioning of the site, some are for performance, and some for marketing; there might be other classes, such as function enhancement. These broad classifications must have already been deemed good determinants for whether to retain a cookie, so they should continue in the new law.

  2. Require sites to attribute their cookies according to purpose classification. For example, if it's a performance cookie, set an attribute such as cookie-name=cookie-value; Complience=http://cookie.law.eu/performance. A site is then legally (or at least enforceably, or reputationally) required to ensure that the cookie is not used for other purposes. The purpose of a cookie is now available and machine-readable in its delivery.

This approach has the following benefits:

  1. Browsers can offer (say) whitelisting of cookies based on site and cookie purpose. When visiting a new site, the user is assured that no new cookies will be stored, unless the site is making an enforceable declaration that they will only be used for the declared purposes, and only if those purposes are whitelisted. Cookies that do not follow the attribution convention will be deemed to have unknown purpose, and can be automatically discarded.

    No pop-ups are required, because the site is not required to obtain consent. The browser simply refuses to give it by not storing the cookie. Notification of cookie policy can just be a discreet link.

    No JavaScript is required, because no pop-up is required.

  2. If a site is suspected of misusing a cookie, there must already be a way under the current law to investigate it and enforce the rules (or the law has no teeth!). Use the same mechanism here. The only difference is that the purpose of a cookie that is under investigation is embedded in its delivery, rather than in some separate policy declaration made by the site.

    This, of course, is a mechanism to be used rarely. The threat of its use should ensure compliance, and underpins the assurance that the visitor has about cookie use.


Note on EU membership and Brexit

I am not a Brexiteer. Brexit was dumb, is no real solution to anything, and has probably committed the UK to self-destruction. Being able to replace the EU Cookie Law is barely a Brexit benefit, and it could have been done while in the EU by persuading MEPs to vote on it. Even if the UK unilaterally changes it now, it hardly has the clout by itself to enforce it.

2017-12-09

OpenTTD under systemd

Just got OpenTTD running satisfactorily on Ubuntu 17.10 Server using systemd, and thought I'd make a note for future reference. When the system is rebooted, OpenTTD shuts down gracefully, saving the game state. Then it comes back up resumed from the saved state.

I happen to have installed OpenTTD from source, just to ensure it has the right version to match current Android apps (1.7.1), and installed in /usr/local, borrowing data from apt-installed packages:

sudo apt install openttd-{data,opengfx,openmsx}
ln -s /usr/share/games/openttd/baseset ~/.openttd/baseset

(That's probably not critical, and some of those packages might be unnecessary for a headless server.)

I run the whole thing in an openttd account to isolate it from anything else. It includes a script, which I've called ~/.install/share/server-process.sh, but you can call it what you like. It's meant to be run under the openttd account:

#!/bin/bash

## List the target file and all autosaves.
files=(~/.openttd/save/esp-main.sav ~/.openttd/save/autosave/*.sav)

## Choose the most recent file.
best="${files[0]}"
bestdate="$(date +'%s%N' -r "$best")"
files=("${files[@]:1}")
while [ ${#files[@]} -gt 0 ]
do
    cand="${files[0]}"

    ## Skip an unmatched wildcard.
    if [ "$cand" = ~/.openttd/save/autosave/\*.sav ]
    then
        continue
    fi

    ## Choose this candidate if it is newer than the best so far.
    canddate="$(date +'%s%N' -r "$cand")"
    if [ "$canddate" -gt "$bestdate" ]
    then
        best="$cand"
        bestdate="$canddate"
    fi

    ## Move on to next file.
    files=("${files[@]:1}")
done

## Save the best file just in case.
printf 'Best file is %s\n' "$best"
cp --reflink=auto "$best" ~/.openttd/save/best.sav

## Run a dedicated server with the best file.
exec /usr/local/games/openttd -g "$best" -D

The intention is to use the latest .sav from among the original file and all autosaves. If the server dies suddenly, it ought to be the last periodic autosave; otherwise, it will take the exit.sav file saved automatically on exit. I'm assuming that the server doesn't save any inconsistent files.

As root, create /etc/systemd/system/openttd.service:

[Unit]
Description=Open Transport Tycoon Deluxe
After=network.target

[Service]
User=openttd
Type=simple
ExecStart=/home/openttd/.install/share/server-process.sh

[Install]
WantedBy=multi-user.target

You might initially need to run this, or after every edit of openttd.service:

sudo systemctl daemon-reload

Test it with:

sudo systemctl start openttd.service
sudo systemctl status openttd.service
sudo systemctl stop openttd.service

Enable it to start on boot with:

sudo systemctl enable openttd.service

I tried using openttd -f, and Type=forking or Type=oneshot, but I think it had trouble killing it. Maybe it needed an explicit ExecStop directive.

Probably a lot more could be done with this to make it more robust, but it's a start.