It Was Terrible Even Before It Was Slop, But It “Works”

Keeping Calibre running on FreeBSD

I have been running Calibre for years now as a way to manage my ebook library. I started with the desktop app on my Macs, but with the library itself saved to the cloud so I could access it from different computers. Calibre isn't really meant to be run this way, and you can run into trouble if you make changes from one computer while the app is still active on another computer, so avoiding these issues is key. To get around the problems with its single instance model, three years ago I got Calibre running on my VPS and configured it so that I could upload new books in the web interface and have the application convert them automatically to my preferred format. (I also jailbroke my Kindle and installed KOReader so I can download books directly from the Calibre server without having to sideload them, but that's outside scope for this post). At some point, however, my server quit letting me upload books, popping up a cryptic 500 error and logging a separate cryptic error. This problem seemed to be specific to FreeBSD so I filed a bug against the package and did some poking.

To be honest, I don't really like Calibre. I've looked for a better tool for the job, but unfortunately, despite its various historic issues (huge download size, confusing array of options, interface that I'm not even sure the developer's mother could love), there isn't really anything else yet that you can run for the same purpose and perform the same functions. Worse still, its developer has leaned hard into slop, adding chatbot interactions and a cover image generator, and he's using LLMs to generate code. But I use a couple plugins to redeem ACSM files and remove DRM so my books are actually my books. And because they only work if they're plugged into Calibre, I'm stuck with it.

The bug I encountered seemed connected to Calibre's own daemonize option, which does a double fork in order to detach from the parent process. Something seems to have changed with the way this works on FreeBSD, and while Calibre's main process was running, it was missing a worker process that should also have been forked when the daemon launched. While trying to figure out what was happening I remembered that FreeBSD also provides its own way to daemonize processes, which is (appropriately enough) called daemon(8). In fact, this is a common enough sort of use case that there's a pattern for the rc.d script.

This was one of those problems that I probably could have fixed more quickly if I hadn't been trying to modify the existing rc.d script that comes with the calibre package on FreeBSD, but because I don't have to write rc.d scripts often I made many, many errors while trying to get the syntax right. This solution is a little bit “draw the rest of the fucking owl” so I don't want to use the word simply, but the solution that worked for me was to change just a few lines in one file after I figured out what lines to change and how to change them, and to change one other line in /etc/rc.conf to match the new process.

The primary file to be changed on a live system is /usr/local/etc/rc.d/calibre. If you're looking at this in FreeBSD's ports tree, the file to edit is deskutils/calibre/files/calibre.in.

Here's what to change:

  1. In the header declaration, change calibre_flags to calibre_args.
  2. Find the line that starts command= (just below the line that starts pidfile=), and replace command with calibre_command.
  3. Below that line, add another line that says command=/usr/sbin/daemon.
  4. Find the line that starts command_interpreter= (it should be the next line) and replace command_interpreter with procname.
  5. Below the line that starts calibre_env= add two new lines: calibre_flags="-f -o ${calibre_logfile} --" and command_args="${calibre_args} --log ${calibre_logfile} --max-log-size ${calibre_logsize} --pidfile ${pidfile}"
  6. Lastly (for this file) there is one last line that starts command_args= after the two if blocks, before the calibre_prestart() command. Replace this line with: command_args="${calibre_command} ${command_args} ${calibre_library}".
  7. Edit /etc/rc.conf, find the line that starts calibre_flags, and change calibre_flags to calibre_args.

For reference I'm putting the whole rc.d file from my working system at the bottom of this post.

The main thing this does is use daemon(8) instead of Calibre's own daemonize option. Command line options passed to daemon(8) have to be set off from the arguments you're passing to the target process, so some of these changes are just there to separate the daemonize arguments (here defined in calibre_flags and terminated with --) from the command line arguments being passed to calibre-server (here defined in command_args). I thought changing calibre_flags to calibre_args in rc.conf helped to make the difference between flags and args readable.

I hope this helps someone else, but even if it just helps future me, he'll be happy.


# PROVIDE: calibre
# REQUIRE: DAEMON
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf to enable calibre_server:
#
# calibre_enable (bool):        Set it to "YES" to enable calibre
#                               Default is "NO".
# calibre_port (int):           port that calibre_server listens on
#                               Default is 8080
# calibre_user (string):        user that calibre_server runs as
#                               Default is calibre
# calibre_home (string):        home directory for calibre_server
#                               Default is the home directory of calibre_user
# calibre_url_prefix (string):  prefix to append to all URLs
#                               default is unset
# calibre_logfile (string):     log file location
#                               Default /var/log/calibre-server/calibre-server.log
# calibre_logsize (int):        size of log file before being rotated in MBs
#                               Default 10 MB
# calibre_args (string):        Any further flags to customize configuration
#                               Default empty
# calibre_library (string):     path to library folder to serve content from
#
#
##########################################################

. /etc/rc.subr

export PATH=$PATH:/usr/local/sbin:/usr/local/bin

name=calibre
rcvar=calibre_enable

load_rc_config $name

: ${calibre_enable:=NO}
: ${calibre_user:=calibre}
: ${calibre_library:=/nonexistent}
: ${calibre_logfile:=/var/log/calibre-server/calibre-server.log}   
: ${calibre_logsize:=10}

pidfile=/var/run/${name}/${name}.pid
calibre_command=/usr/local/bin/calibre-server
command=/usr/sbin/daemon
procname=/usr/local/bin/python3.12
required_dirs=${calibre_library}

start_precmd=calibre_prestart

calibre_home=${calibre_home:-$(getent passwd ${calibre_user} | awk -F: '{print $6}')}
calibre_env="${calibre_env} HOME=${calibre_home:-/nonexistent}"

calibre_flags="-f -o ${calibre_logfile} --"
command_args="${calibre_args} --log ${calibre_logfile} --max-log-size ${calibre_logsize} --pidfile ${pidfile}"

if [ ! -z "${calibre_port}" ]; then
        command_args="${command_args} --port=${calibre_port}"
fi
if [ ! -z "${calibre_url_prefix}" ]; then
        command_args="${command_args} --url-prefix=${calibre_url_prefix}"
fi

command_args="${calibre_command} ${command_args} ${calibre_library}"

calibre_prestart()
{
        install -d -o ${calibre_user} -m 755 /var/run/${name}
        install -d -o ${calibre_user} -m 755 `dirname ${calibre_logfile}`
}

run_rc_command "$1"

I don't have comments on here, but if you want to respond to this post, I'm on the fedi as fedward@distraction.party.