Suspend on lid closed + battery

If you use FreeBSD 11 on the good ol’ ThinkPad X201, you’ve probably noticed now that suspend and resume work perfectly flawlessly with the latest FreeBSD release.

You probably wish to take advantage of this and suspend your laptop automatically when the lid is closed. Nothing could be easier:

sysctl hw.acpi.lid_switch_state=S3

Save this option /etc/sysctl.conf for it to be permanent.
Close the lid, the laptop goes to sleep, you’re done!
It’s that simple.

But, with this method it will go to sleep each time the lid is closed, completely ignoring whether the laptop is on battery or not. And if you’re like me you probably don’t want this. Instead you want it to suspend itself when the lid is closed and the laptop is on battery. Easy enough! We just have to invoke the mighty power of devd along with a very little shell script.

First we need to tell devd how to react when the lid is closed. Put this in /etc/devd/lid.conf, then restart (service devd restart):

# Notify lid close/open events.
notify 10 {
  match  "system"    "ACPI";
  match  "subsystem" "Lid";
  action "/etc/acpi/lid.sh $notify";
};

Now we will make a script that checks the lid and AC line states and chooses to suspend the laptop when both the lid is closed and AC line is disconnected. This goes in /etc/acpi/lid.sh:

#!/bin/sh

lid="$1" # 0x00 is closed, 0x01 is open (see devd.conf)
acline=$(sysctl -n hw.acpi.acline) # 0 is battery, 1 is online (man acpi)

if [ \( "$lid" = "0x00" \) -a \( "$acline" -eq 0 \) ]
then
  logger "Lid closed on battery. Going to sleep."
  acpiconf -s3
fi

Try it out! Disconnect the AC line, close the lid, it should go to sleep within seconds. However if it doesn’t work you might want to modify the script above a little to check whether lid events are received correctly or not.

Now you may use a modified version of this script to lock xscreensaver when the lid is closed and before the actual suspend. Well OK, I’ve done that for you:

#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop to be
# shown for a few seconds when the system resumes from sleep.
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

lid="$1" # 0x00 is closed, 0x01 is open (see devd.conf)
acline=$(sysctl -n hw.acpi.acline) # 0 is battery, 1 is online (man acpi)

lock_display() (
  socket="$1"
  display=$(echo "$socket" | tr "X" ":")

  # Temporary pid file for the watching command
  tpid=$(mktemp)

  # Wait until the display is actually locked.
  (timeout 2s xscreensaver-command -display "$display" -watch & echo $! > $tpid) | (
    # Issue the lock command only when we know that
    # the watching pipe is ready.
    xscreensaver-command -display "$display" -lock

    while read line
    do
      line=$(echo $line | cut -d' ' -f 1)

      if [ "$line" = LOCK ]
      then
        # We have to kill the watching command manually before breaking.
        kill -TERM $(cat $tpid)
        break
      fi
    done
  )

  rm $tpid
)

# The X server may not be running
if [ ! -d /tmp/.X11-unix ]
then
  exit 0
fi

# Lock each available display
for socket in $(ls /tmp/.X11-unix)
do
  # Lock the display
  logger "Locking xscreensaver on $socket"
  lock_display $socket &
done

# Wait until every displays are locked
wait

# Now we can suspend if needed
if [ \( "$lid" = "0x00" \) -a \( "$acline" -eq 0 \) ]
then
  logger "Lid closed on battery. Going to sleep."
  acpiconf -s3
fi

Start XScreensaver before going to sleep

It seems rational to request XScreensaver to lock your screen when you suspend your machine. This is possible in Debian via the /etc/default/acpi-support file, especially with this line :

# Comment this out to disable screen locking on resume
LOCK_SCREEN=true

However for some obscure reason this will lock the screen (i.e. issue the “xscreensaver-command -lock” command) after suspend, that is on resume. Since the locking process is not immediate your desktop will be available for anyone to watch (and use) for a duration of about one or two second. There is no need to say that this is unacceptable.

It is possible to avoid that by disabling the default screen locking mechanism and hooking it manually to PM. So you should add a script into /etc/pm/sleep.d. The following script is the first version of the script I used (beware it doesn’t work, see below) :

#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop
# to be shown for a few seconds when the system resumes from sleep.

case "$1" in
  hibernate|suspend)
    xscreensaver-command -lock
    sleep 1 # annoying sleep
    ;;
  *)
    exit 0;;
esac

You may notice that the script issues a sleep just after the xscreensaver-command has returned. It ensures that the screen will be really locked when the system effectively enters into sleep. This is needed because the xscreensaver-command will not lock the screen immediately, that is it is non-blocking in a certain way and you cannot ensure that the screen is effectively locked as soon as the command has returned.

However the script above doesn’t work. As Marcus Moeller commented, the above script won’t work by default on Debian and probably with most other distributions. That is because we don’t issue the xscreensaver lock command as the user owning the xscreensaver daemon. I quote his solution here :

#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop
# to be shown for a few seconds when the system resumes from sleep.

IS_ACTIVE="$( pidof /usr/bin/xscreensaver )"

case "$1" in 
  hibernate|suspend) 
    # check if xscreensaver is running. if not, just skip on. 
    if [ -z "$IS_ACTIVE" ] 
      then : 
      else 
      # run the lock command as the user who owns xscreensaver process, 
      # and not as root, which won't work.
      su "$( ps aux | grep xscreensaver | grep -v grep | grep $IS_ACTIVE | awk '{print $1}' )" 
             -c "/usr/bin/xscreensaver-command -lock" &
      sleep 1
    fi
    ;;
  *)
    exit 0;;
esac

Digging in xscreensaver’s code shows that what the command actually needs is a connection to the X server. If xscreensaver-command cannot find the display from either command line or environment variables, it will fall back to “:0.0“. But this will fail if root cannot connect to the X server (which is generally the case). That’s how the ‘user approach’ fixes it. However this won’t work anymore if there are multiple instance of xscreensaver running on different displays (only one of them will be locked). Another solution would be to issue the command on each display where root can connect to. However this poses two problems :

  1. It is not as easy as it seems to reliably list all available displays. (see http://stackoverflow.com/questions/11367354/obtaining-list-of-all-xorg-displays).
  2. It requires that each lockable session allows connections from root with “xhost si:localuser:root“.
Here is the modification I posted in response which uses the ‘display approach’ instead:
#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop to be shown
# for a few seconds when the system resumes from sleep.

case "$1" in
  hibernate|suspend)
  # The X server may not be running
  if [ ! -d /tmp/.X11-unix ]
  then
    exit 0
  fi 

  # Lock each available display
  for socket in $(ls /tmp/.X11-unix)
  do
    display=$(echo "$socket" | tr "X" ":")
    xscreensaver-command -display "$display" -lock
  done

  sleep 1 # annoying sleep
  ;;
  *)
   exit 0;;
esac

However we are not done yet. As you can see we still rely on sleep to ensure that the screen is locked before our script returns control to the suspend procedure. With usage it became clear that one second was not sufficient as the script would return too early from time to time. Incrementing the duration of the sleep would be more than annoying and it doesn’t offer any real guarantee anyway. The only solution would be to find a way to exit the script when we are sure that the display is effectively locked. This is possible by watching at the changes of states of the screensaver while issuing the lock command. There is a slight last problem however. If multiple displays are present we want to issue that “lock ‘n watch” procedure in paralell to avoid accumulating the locking delays. That’s the solution I use in the script below, note that we don’t rely on sleep anymore:

#!/bin/sh
# XScreensaver should be called BEFORE going to sleep to avoid the desktop to be
# shown for a few seconds when the system resumes from sleep.
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin

lock_display() (
  socket="$1"
  display=$(echo "$socket" | tr "X" ":")

  # Temporary pid file for the watching command
  tpid=$(mktemp)

  # Wait until the display is actually locked.
  (timeout 2s xscreensaver-command -display "$display" -watch & echo $! > $tpid) | (
    # Issue the lock command only when we know that
    # the watching pipe is ready.
    xscreensaver-command -display "$display" -lock

    while read line
    do
      line=$(echo $line | cut -d' ' -f 1)

      if [ "$line" = LOCK ]
      then
        # We have to kill the watching command manually before breaking.
        kill -TERM $(cat $tpid)
        break
      fi
    done
  )

  rm $tpid
)

case "$1" in
  hibernate|suspend)
    # The X server may not be running
    if [ ! -d /tmp/.X11-unix ]
    then
      exit 0
    fi

    # Lock each available display
    for socket in $(ls /tmp/.X11-unix)
    do
        # Lock the display
        lock_display $socket &
    done

    # Wait until every displays are locked
    wait
    ;;
  *)
    exit 0;;
esac

As stated above you still need to allow connections from root to your display. You may for example use this command when your session start :

xhost si:localuser:root

Or, as the man page of xhost states, use the file /etc/X*.hosts to do that globally.