2018-08-18

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.

2017-01-21

Enhancing the XML generated by the DAV SVN module

Suppose you set up Apache to serve Subversion repositories. You might do this:

<Location "/svn-repos/">
  DAV svn
  SVNParentPath /var/svn-repos
  SVNIndexXSLT "/templates/svn-dir.xsl"
</Location>

The SVNIndexXSLT directive tells the Subversion module to serve any directory in a repository as XML to ordinary browsers (not SVN clients, which talk WebDAV). The XML will include a link to the XSLT stylesheet under the virtual path /templates/svn-dir.xsl, and the browser is expected to fetch that too, and use it to transform the XML into HTML. A sample XML output could be:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="/templates/svn-dir.xsl"?>
<!DOCTYPE svn [
  <!ELEMENT svn   (index)>
  <!ATTLIST svn   version CDATA #REQUIRED
                  href    CDATA #REQUIRED>
  <!ELEMENT index (updir?, (file | dir)*)>
  <!ATTLIST index name    CDATA #IMPLIED
                  path    CDATA #IMPLIED
                  rev     CDATA #IMPLIED
                  base    CDATA #IMPLIED>
  <!ELEMENT updir EMPTY>
  <!ATTLIST updir href    CDATA #REQUIRED>
  <!ELEMENT file  EMPTY>
  <!ATTLIST file  name    CDATA #REQUIRED
                  href    CDATA #REQUIRED>
  <!ELEMENT dir   EMPTY>
  <!ATTLIST dir   name    CDATA #REQUIRED
                  href    CDATA #REQUIRED>
]>
<svn version="1.9.3 (r1718519)"
     href="http://subversion.apache.org/">
  <index rev="16" path="/" base="cakes-test">
    <dir name="café" href="caf%c3%a9/" />
    <file name="test" href="test" />
  </index>
</svn>

This is fine if you only want to browse the rest of the repository in the same manner, as it contains properly escaped relative links to files and other directories in the repository. However, if you want to jump to any other pages on the site, ones that aren't under DAV control, you have to hard-wire absolute (local) links to them.

When I generate XSLT-transformable XML dynamically, I get the script to include an attribute with a configurable path prefix, and the XSLT can then generate absolute URLs from relative ones simple by attaching the prefix. But you can't do the same with the DAV/SVN-generated XML, because you don't generate it! There also seems to be a (reasonable) unwillingness to develop the DAV/SVN Apache module, so a feature request would likely be ignored.

Time for an Apache rewrite rule. It has to match only GETs within the Subversion hierarchy (/svn-repos/ in our example), and only if the first path element after the repository name is not !svn, the special marker used by WebDAV SVN clients:

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{REQUEST_URI} ^/svn-repos/
RewriteCond %{REQUEST_URI} !^/svn-repos/[^/]+/!svn/
RewriteRule ^/svn-repos(/.*)$ /scripts/svn-web.php$1 [PT]

This now intercepts (say) /svn-repos/cakes-test/, and internally redirects it to /scripts/svn-web.php/cakes-test/. The script will run with PATH_INFO set to /cakes-test/, so it will know what file or directory to look at. Then it can do its own authorization checks, and serve enhanced XML.

It would be nice if /scripts/svn-web.php could be an internal file path, rather than an exposed virtual path. RewriteRule can be given a filesystem path instead of a virtual path, and the server identifies as such by seeing if the first path element exists in the filesystem. However, this doesn't seem to work:

RewriteEngine On
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{REQUEST_URI} ^/svn-repos/
RewriteCond %{REQUEST_URI} !^/svn-repos/[^/]+/!svn/
RewriteRule ^/svn-repos(/.*)$ /usr/local/share/scripts/svn-web.php$1 [PT]

The server will respond with a 404, claiming that /usr/local/share/scripts/svn-web.php doesn't exist. It seems to require an alternative set of flags to work:

RewriteEngine On
RewriteRule ^/svn-repos(/.*)$ /usr/local/share/scripts/svn-web.php$1 [L,H=application/x-httpd-php]

So now L turns off further rewriting, and H explicitly sets the handler to PHP.

2016-12-24

Resolving directed acyclic graph with ordered edges in SQL

Suppose you have a table holding the adjacency set of a directed acyclic graph (DAG). It has two columns, parent and child, each row indicating an edge from one node to another. With a few temporary tables, it's easy enough to resolve any set of nodes into a fuller set of those nodes and all their descendants. You can use it to represent group membership, and find rules that match a given node directly or indirectly. You can also create a deterministic function to do the resolution for you, and use it in triggers to prevent cycles.

Now add another column, seqno, that orders the direct children of a node. Now you want to resolve a node to a sequence with depth-first traversal. SQL doesn't do recursion, so how do you go about it? Here's some example data:

parent child seqno
1 2 0
1 3 1
1 4 2
2 4 0
4 5 0
4 6 1

So 1 imports 2, 3 and 4, but 2 also imports 4, which also imports 5 and 6. Although 1 directly imports 4 after 3, 4 should appear before 3 in the result. The expected sequence is 1, 2, 4, 5, 6, 3, 4, 5, 6, from which we remove the later duplicates to give 1, 2, 4, 5, 6, 3.

Here's a technique that scans the graph in two passes. In the first, we descend level by level, looking for the highest seqno of any node's outgoing edges (the maximum fanout) at that level. In our example, that gives us 3, 2, 2, 0. We then work back, starting with a counter of zero, multiplying the number in the table by the counter and adding one, which becomes the new counter: 0×0+1=1; 2×1+1=3; 2×3+1=7. Note that we don't use the first fanout 3. We reverse the result 1, 3, 7, and associate these factors with each level of the graph, so level 1 maps to 7, 2 to 3, and 3 to 1.

Now we start the second pass, this time keeping a computed sequence number. The root element 1 is assigned 0 as its sequence number. Then we find its children are 2, 3 and 4 in sequence 0, 1 and 2. We assign their sequence numbers as the root's assigned number 0, plus 1, plus the original sequence number times the factor at the current level: so 2 is placed at 0+1+0×7=1, 3 is placed at 0+1+1×7=8, and 4 is placed at 0+1+2×7=15.

When we move to the next level, using factor 3, 2 at 1 imports 4 with seqno 0, so 4 is placed at 1+1+0×3=2. We now have 4 at both 2 and 15, so we can lose the later one at 15. That also puts 4 (at 2) before 3 (at 8).

Essentially, what we're doing is leaving a space at each level for a full tree (but with potentially different fanout at each level). We then use the highest factor at the top level to spread the first generation out, leaving enough gap between two siblings for the descendants of the 'older' sibling.

Here's the body of a stored function that takes one node id, and resolves it to a sequence that represents depth-first traversal, with later duplicates removed. Let's hope I've got it right!:

BEGIN
DECLARE idlist TEXT DEFAULT '';
DECLARE scl INT UNSIGNED;
DECLARE lyr INT UNSIGNED;
DECLARE prv INT UNSIGNED;

-- Create a table to hold fan-outs for each level of the graph.
DROP TEMPORARY TABLE IF EXISTS fanouts;
CREATE TEMPORARY TABLE fanouts
   (layer INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    max_fanout INT UNSIGNED,
    factor INT UNSIGNED DEFAULT NULL);

-- Create a table to hold candidates, and populate it with the root
-- node.
DROP TEMPORARY TABLE IF EXISTS cands;
CREATE TEMPORARY TABLE cands (id INT UNSIGNED);
INSERT INTO cands (id) VALUES (root);

-- Create a table to hold parents of the current candidates.
DROP TEMPORARY TABLE IF EXISTS parents;
CREATE TEMPORARY TABLE parents (id INT UNSIGNED);

-- Work out the fanouts for each level.
WHILE EXISTS (SELECT id FROM cands) DO
   -- Get the maximum fan-out at this level.
   INSERT INTO fanouts
      SELECT NULL AS layer,
             MAX(seqno) + 1 AS max_fanout,
             NULL AS factor
         FROM edges
         WHERE parent IN (SELECT id FROM cands)
         GROUP BY parent
         ORDER BY max_fanout DESC
   LIMIT 1;

   -- Work out what's at the next level.
   INSERT INTO parents
      SELECT edges.child AS id
         FROM cands
   INNER JOIN edges
     ON edges.parent = cands.id;

   -- Make the next level the current level, and reset the next level.
   DELETE FROM cands;
   INSERT INTO cands
      SELECT DISTINCT id FROM parents;
   DELETE FROM parents;
END WHILE;

-- Work out the scale factor for each level.
SET scl = 0;
SET prv = 0;
SELECT MAX(layer) FROM fanouts INTO lyr;
WHILE lyr > 0 DO
   UPDATE fanouts
      SET factor = scl * prv + 1
      WHERE layer = lyr;
   SELECT max_fanout, factor
      FROM fanouts
      WHERE layer = lyr
      INTO prv, scl;
   SET lyr = lyr - 1;
END WHILE;



-- SECOND PHASE


-- This table holds the final result.  seqno defines the order.
DROP TEMPORARY TABLE IF EXISTS done;
CREATE TEMPORARY TABLE done
   (id INT UNSIGNED, seqno INT UNSIGNED);

-- This table lists nodes that we have to add to the result.
DROP TEMPORARY TABLE IF EXISTS cands;
CREATE TEMPORARY TABLE cands
   (id INT UNSIGNED, seqno INT UNSIGNED);

DROP TEMPORARY TABLE IF EXISTS parents;
CREATE TEMPORARY TABLE parents
   (id INT UNSIGNED, seqno INT UNSIGNED);

INSERT INTO cands (id, seqno) VALUES (root, 0);

SET lyr = 1;
WHILE EXISTS (SELECT id FROM cands) DO
   SELECT factor FROM fanouts WHERE layer = lyr INTO scl;
   SET lyr = lyr + 1;

   INSERT INTO parents
      SELECT child AS id,
             cands.seqno + 1 + scl * edges.seqno AS seqno FROM cands
   JOIN edges
     ON edges.parent = cands.id
   ORDER BY cands.seqno ASC, edges.seqno ASC;

   -- Acknowledge that we have the parents of the current candidates.
   INSERT INTO done
      SELECT id, seqno FROM cands;
   DELETE FROM cands;

   -- Keep the earliest entry from done and parents.
   DELETE FROM parents
      WHERE id IN (SELECT id FROM done WHERE done.seqno < parents.seqno);
   DELETE FROM done
      WHERE id IN (SELECT id FROM parents WHERE done.seqno > parents.seqno);

   -- Make the parents the new candidates.
   INSERT INTO cands
      SELECT id, seqno FROM parents;
   DELETE FROM parents;
END WHILE;

SELECT GROUP_CONCAT(id ORDER BY seqno ASC SEPARATOR ',')
    FROM done
    INTO idlist;
RETURN idlist;
END;

Happy now?

2016-11-20

Extracting an email attachment to a pipe in procmail

At work, some of my emails come through some Microsoft mail server, which I don't use. However, through its web interface, I can set up redirection to the address I do actually use. But there's a problem: the redirected email is identical to the original, except that the Message-Id header field is overwritten with one the server chooses locally. What the hell could this be useful for?!! It completely messes up threading, and achieves nothing in its place! Worse, the new id ends in .local!! I thought these things were supposed to be globally unique…?

My own emails run through a procmail script that I control, so I already get it to fix things like restoring the subject line when mangled by spam filters. This does the job for that:

:0 fhw
* ^X-Spam-Prev-Subject:
| formail -R X-Spam-Prev-Subject Subject -U Subject

Can I do something similar for Message-Id? No, not with what gets redirected. However, there is another option: forward email as attachment. I checked, and the message id within the attachment is preserved!

I hunted around for a tool to unpack the attachment and print it to standard output (so it could be used as a procmail filter), but tools like munpack and ripmime only unpack to a directory. With nothing ready-made, I decided to pick a language with some hopefully mature RFC822 library:

#!/usr/bin/env python

"""Unpack a MIME message onto STDOUT."""

# Distilled from https://docs.python.org/2/library/email-examples.html

import sys
import email

def main():

    msg = email.message_from_file(sys.stdin)

    if not(msg.is_multipart()):
        sys.stderr.write('Not multipart\n')
        sys.exit(1)
    attachment = msg.get_payload(1)
    print attachment.get_payload(0).as_string(False)
    sys.exit(0)

if __name__ == '__main__':
    main()

The procmail configuration just has to be certain that the message has come through the mindlessly mangling server. I used three lines that should prevent mishaps:

:0 fw
* ^From:.*<my@email\.address\.example\.com>
* ^X-MS-Has-Attach: yes
* ^X-MS-Exchange-Inbox-Rules-Loop: my@email\.address\.example\.com
| /path/to/the/python/script

The encapsulating message should always come from me, even if the original is from someone else.

It seems to do the job! Just tried it with an image attachment (within the attached message), and it was fine. I wonder how well it will cope with very big emails. Will the Python library automatically save to disc when the size is above a threshold?