The custom build type in Cabal

This story does not have a happy ending. Rather, it explores the various difficulties I encountered while trying to customize Cabal's build process.

The default setup of Cabal generally works fine … as long as you're not doing anything unusual. What I needed to do was to try and find some platform-dependent C libraries and also do a little bit of custom preprocessing on the source code. Cabal's way of finding C libraries is not very sophisticated and it only supports a limited number of known preprocessors (such as C2HS), so I had to cook up the solution myself.

The "Custom" build type provided by Cabal is very powerful, since you are given full control of the Cabal library. All you have to do is to edit your *.cabal file to say

build-type: Custom

and then homebrew your own Setup.hs. Sounds simple, right?

Unfortunately, it's not so easy because there just isn't a whole lot of documentation about writing custom Cabal builds. The User Guide doesn't say much about it other than mentioning the flag and ending with an ominous "Good luck". And there's about one example on StackOverflow.

I mean, at least the official documentation for the Cabal library isn't bad, but it's so overwhelming that you are sort of left wondering where to even begin.

The example on StackOverflow was helpful for getting started though. It turns out most of the time all you care about is writing hooks. Hooks allow existing Cabal functionality to be overriden by your own functions. The names of the hooks are relatively descriptive, but the argument lists can be somewhat daunting at first because there are quite a few record-like data types used by Cabal. (Records are also somewhat painful to work with in Haskell, especially ones that are heavily nested.)

I'll use the one that I worked with as an example: the postConf (post-configuration) hook. The others are similar (I hope), but I've not actually played with them.

postConf _ :: Args
           -> ConfigFlags
           -> PackageDescription
           -> LocalBuildInfo
           -> IO ()

I'm not entirely clear what the docs mean by "after [the] configure command" though! My guess is that it occurs immediately after parsing the *.cabal file and evaluating with the conditionals in the *.cabal file. Why? If you look at confHook instead, you will find that it has GenericPackageDescription instead of PackageDescription. The GenericPackageDescription contains the unevaluated conditionals, and thus you'll find that the package description is still incomplete (lots of Nothing) if you use confHook instead of postConf.

The arguments are relatively self-explanatory if you examine their types, so I won't say much about those.

How do you write the hook then? Here's a basic template:

myPostConf args cf pd lbi = do
  -- ... do something ...
  postConf simpleUserHooks args cf pd lbi

Simply define a function with the right signature, and then call the default hook after doing whatever you wanted to do in the hook. The default hooks are all conveniently stored in simpleUserHooks.

The main function is straightforward:

main = defaultMainWithHooks simpleUserHooks
       { postConf = myPostConf
       -- ... additional hooks ...
       }

So all in all it doesn't seem too difficult. However, it turns out I didn't fully understand what the hook does.

The postConf hook is not guaranteed to run if you are building or installing or doing anything else. Furthermore, even if you modify the package description and pass it into the default hook, (due to some weird reasons I've yet to figure out) it affects the configuration process but won't affect the actual build!

So I spent a few hours futilely debugging a problem only to realize the changes I made to postConf didn't actually affect the build. There is probably a way to make it work though, but it would likely involve binding it to more hooks.

After that "profound" realization, I've not really looked into this further. I settled for the simple build type instead because it's just not worth all the trouble (and I found alternative, simpler solutions). :c

Comments

Automatically disable touchpad when mouse is plugged in

This is one of those really simple things that are surprisingly tricky to do right. My goal: automatically disable the touchpad when an external mouse is plugged in. Why go through all the trouble? Because—in addition to being plain annoying—a moving cursor can alter the window focus in my current tiling window manager.

On Windows, you can either do it easily (if the driver supports it) or it's nearly impossible. For better or worse, on Linux, you can almost always do it after expending some (possibly non-trivial) effort.

Solution

In the end, I boiled it down to:

These are intended for Arch Linux. I haven't tried on any other systems yet, but it shouldn't be too hard to adapt them for other distributions. It might even work out of the box!

Details

I started out by following the Arch Linux wiki, but I've simplified the script and made some of my own discoveries along the way.

Firstly, the udev rules: these consist of two simple rules that will call /bin/touchpad-ctl whenever a mouse is added or removed. Nothing too fancy here.

# /etc/udev/rules.d/01-touchpad.rules
SUBSYSTEM=="input", KERNEL=="mouse[0-9]*", ACTION=="add",    RUN+="/bin/touchpad-ctl"
SUBSYSTEM=="input", KERNEL=="mouse[0-9]*", ACTION=="remove", RUN+="/bin/touchpad-ctl"

The udev rules would miss the opportunity to fire if the mouse is already plugged in at boot, so it's necessary to call touchpad-ctrl in .xinitrc to make sure that the touchpad is disabled in this scenario.

# ~/.xinitrc (insert this somewhere before starting your window manager)
touchpad-ctl

The touchpad-ctl script is where the magic occurs. Since I've not come across an easy to way to figure out which device is the actual touchpad, you'll have to set this variable manually in the script. To list all the mouse-like devices, run this command:

find /sys/class/input/ -name mouse\*                    \
     -exec udevadm info --attribute-walk --path={} \;   \
    | grep ATTRS{name}

(If it says it can't find udevadm, then try running it as root.)

The script does two things:

  • Check if any external mouse is plugged in. This is the easy part: simply loop through all the mice and check if it's an external mouse (i.e. not the touchpad).

    FOUND=0
    for MOUSE in `find /sys/class/input -name mouse\*`
    do
        if [ "`cat $MOUSE/device/name`" != "$TOUCHPAD" ]
        then
            FOUND=1
            break
        fi
    done
  • Enable or disable the touchpad with synclient. This is a bit more difficult because it needs to know the DISPLAY and XAUTHORITY. The DISPLAY variable is hardcoded, while the XAUTHORITY is obtained from the home directories of the users. This is repeated for every user.

    DISPLAY=:0
    export DISPLAY
    for USER in `w -h | cut -d\  -f1 | sort | uniq`
    do
        XAUTHORITY=`sudo -Hiu $USER env | grep ^HOME= | cut -d= -f2`/.Xauthority
        export XAUTHORITY
        synclient TouchpadOff=$FOUND
    done

Comments

Port forwarding with SSH

SSH port forwarding can be a very useful tool for working with remote systems. There are two common reasons for doing this:

  • to add security to an unencrypted connection, or

  • to access services behind firewalls.

Background

In networking lingo, a host generally refers to a single computer. Each host can have multiple network interfaces, each of which is assigned an (IP) address.

The address suffices to identify the network interface and hence the host, but a single host can provide a variety of services (e.g. web server, mail server, SSH server, etc). Hence, there needs to be a way to distinguish between them. This is where the notion of a port comes in. Each service is allowed to pick its own port, denoted by a 16-bit unsigned integer (0‒65535).

Services can listen to a particular port, waiting for clients to initiate connections to the port. The port numbers for many services are specified by conventions. Clients can then connect to the target port that corresponds to the one chosen by the service.

Example. An HTTP web server such as httpd would typically listen to port 80 (one can also say the service httpd "runs on" port 80).

the httpd server listens on port 80
the httpd server listens on port 80

Hence, HTTP clients such as web browsers or downloaders would typically have a target port of 80.

the wget client targets port 80
the wget client targets port 80

Note: In the process of establishing a connection to a server, the clients themselves also obtain a port on their end. This port, however, is ephemeral and generally irrelevant here. They are not shown in any of these diagrams.

Normally, given the correct port, the client can simply connect directly to the server…

wget connects to httpd directly
wget connects to httpd directly

… unless of course, there is a firewall that blocks port 80. However, if the firewall permits SSH connections, port forwarding can be used to bypass the barrier. In this case, the client would connect to, say, port 8000, which is then forwarded to port 80 on the server.

wget connects to httpd via a forwarded port
wget connects to httpd via a forwarded port

Forwarded ports are generally temporary, so conventionally one would use a large port number to avoid conflicts with the more frequently used ports. (Some systems, usually the Unix-like ones, would also reserve port numbers below 1024 so that one would require superuser privileges to forward them.)

Port forwarding

SSH supports two kinds of port forwarding: local and remote port forwarding. (There is also the so-called dynamic port forwarding, which won't be discussed here.) The first and most important question here is: what is the difference between local and remote forwarding?

In local port forwarding, the port that is being forwarded resides on the local end, i.e. the host of the SSH client, whereas …

local port forwarding
local port forwarding

… in remote port forwarding, the port that is being forwarded resides on the remote end, i.e. the host of the SSH server.

remote port forwarding
remote port forwarding

In other words, the type of port forwarding depends on the location of the service of interest (in the example earlier, the httpd server) with respect to the SSH server.

From the manual pages of SSH, the argument syntax for port forwarding is:

# local port forwarding
ssh -L [BIND_ADDRESS:]PORT:HOST:HOSTPORT HOSTNAME

# remote port forwarding
ssh -R [BIND_ADDRESS:]PORT:HOST:HOSTPORT HOSTNAME

The parameters are:

  • PORT: the port that is being forwarded. SSH will listen to this port and forward connections made to this port to the other side (i.e. HOST).

    • In the case of local port forwarding, PORT is bound to the host of the SSH client.

    • In the case of remote port forwarding, PORT is bound to the host of the SSH server.

  • HOST: the host that provides the service of interest.

    • In the case of local port forwarding, HOST is on the side of the SSH server.

    • In the case of remote port forwarding, HOST is on the side of the SSH client.

  • HOSTPORT: the port on HOST listened by the service of interest.

  • HOSTNAME: the SSH server (unrelated to HOST or HOSTPORT).

  • BIND_ADDRESS: this is an optional argument that specifies the address that PORT should be associated with. By default, this only binds to the loopback interface.

The naming of the arguments here is rather unfortunate as the word "host" is overloaded to mean several things. For this reason, the name sshd-host will be used to refer to the host of the SSH server (i.e. HOSTNAME), while the name appd-host will be used to refer to the host that provides the service of interest. Correspondingly, ssh-host will refer to the host of the SSH client.

Example. Consider the previous example involving the HTTP server and client. If the HTTP server resides on host of the SSH server and the HTTP client resides on host of the SSH client, then one can use local port forwarding to access the remote HTTP server from the local side.

example of local port forwarding: ssh -L 8000:localhost:80 sshd-host
example of local port forwarding: ssh -L 8000:localhost:80 sshd-host

On the other hand, if the HTTP server resides on the host of the SSH client and the HTTP client resides on the host of the SSH server, then one can use remote port forwarding to access the local HTTP server from the remote side.

example of remote port forwarding: ssh -R 8000:localhost:80 sshd-host
example of remote port forwarding: ssh -R 8000:localhost:80 sshd-host

Notice that for both cases, the arguments differ only by the flag (-L/-R). However, the similarity is somewhat deceptive: the address localhost is interpreted differently in each case. In the case of remote port forwarding, localhost refers to the host of the SSH client (ssh-host) as one would normally expect, whereas in the case of local port forwarding, localhost actually refers to the host of the SSH server (sshd-host)! In fact, the HOST parameter in the case of local port forwarding is always relative to the SSH server.

The above two scenarios are actually special cases of a more general scenario: appd-host could be a separate host, different from either ssh-host or sshd-host. In the more general case, local port forwarding would involve forwarding a port from ssh-host to sshd-host and then to appd-host, while remote port forwarding would involve forwarding a port from sshd-host to ssh-host and then to appd-host.

Example. If the HTTP server is accessible on the remote side and the HTTP client resides on host of the SSH client, then one can use local port forwarding to access the HTTP server from the local side.

example of more general local port forwarding: ssh -L 8000:appd-host:80 sshd-host
example of more general local port forwarding: ssh -L 8000:appd-host:80 sshd-host

On the other hand, if the HTTP server is accessible on the local side and the HTTP client resides on host of the SSH server, then one can use remote port forwarding to access the HTTP server from the remote side.

example of more general remote port forwarding: ssh -R 8000:appd-host:80 sshd-host
example of more general remote port forwarding: ssh -R 8000:appd-host:80 sshd-host

Note that in both cases, the second part of the traffic is not encrypted by SSH since the SSH tunnel only connects between ssh-host and sshd-host.

While the target host (appd-host) can be almost anywhere, the forwarded port must always reside on either ssh-host or sshd-host: this is necessary since SSH has to actually listen to the forwarded port, whereas the target port simply needs to be reachable (not blocked by a firewall).

Lastly, what about BIND_ADDRESS? By default, SSH will only listen to a forwarded port on the loopback interface, which is a virtual network interface that only responds to internal requests (from the same host). This is for security reasons, since if SSH listened on all interfaces, it could be potentially forwarding traffic from just about anybody on the Internet. Using BIND_ADDRESS one can override the default behavior so that SSH will listen on other interfaces as well. Use this cautiously! (Note: it may necessary to enable the GatewayPorts flag for this to work on anything but the loopback interface.)

Chapter 9.2 in the book SSH, The Secure Shell: The Definitive Guide goes into greater detail about port forwarding with SSH.

Comments