Skip to content

rTorrent/ruTorrent with Nginx & PHP-FPM on Entware

Note

Some assumptions:
- SSH access to device
- Entware installed
- Neither rTorrent/ruTorrent, nor Nginx nor PHP-FPM previously installed

Install required base packages

$ opkg install bash curl ffmpeg git git-http gzip htop iotop mediainfo\
mktorrent nano nginx-ssl php8 php8-cli php8-fpm php8-mod-bcmath\
php8-mod-ctype php8-mod-curl php8-mod-dom php8-mod-fileinfo php8-mod-filter\
php8-mod-mbstring php8-mod-opcache php8-mod-openssl php8-mod-phar\
php8-mod-session php8-mod-xml php8-mod-zip procps-ng-pgrep python3 python3-pip\
rtorrent-rpc socat unrar unzip wget-ssl

Note

The PHP extensions above are not optional decoration. Leaving one out disables plugins silently, or worse, throws a fatal on every call:
- mbstring - tracklabels and _getdir call mb_strtolower(). Without it the directory picker returns a 500 and tracklabels fatals on every request.
- dom - extsearch
- openssl - xmpp
- zip - source
- fileinfo - tracklabels

ruTorrent also needs a GNU stat, because php/utility/lfs.php calls it with -Lc%d:%i:%f:%h:%u:%g:%s:%X:%Y:%Z:%B:%b. On QNAP the system binary at /usr/bin/stat accepts that, which is what $pathToExternals below points at. If your device has no GNU stat there, install the coreutils-stat package and use /opt/bin/stat instead.

Configure rTorrent

Create necessary directories:

$ mkdir --parents /share/data/{.rtorrent,torrents}
$ mkdir /share/data/.rtorrent/{.session,watch}

Create rTorrent configuration file:

$ nano /share/data/.rtorrent/.rtorrent.rc
## Run rtorrent as a daemon (disables user interface, only XMLRPC control)
system.daemon.set = true

## Control rtorrent via UNIX XMLRPC socket
network.scgi.open_local = (cat, "/opt/var/run/rtorrent.sock")
execute.nothrow = chmod, 770, (cat, "/opt/var/run/rtorrent.sock")

## Listening port for incoming peer traffic, don't randomize
network.port_random.set = no
network.port_range.set = 49879-49879

## Set default data save directory, session file path
directory.default.set = /share/data/torrents
session.path.set = /share/data/.rtorrent/.session

## Disable UDP protocol to trackers, disable DHT/PEX BitTorrent protocols and enable traffic encryption if possible
trackers.use_udp.set = no
dht.mode.set = off
protocol.pex.set = no
protocol.encryption.set = allow_incoming, try_outgoing, enable_retry

## Preferred filename encoding
encoding.add = UTF-8

## Disable hash check on torrents that have finished downloading (intensive process)
pieces.hash.on_completion.set = no

## Minimum and maximum number of peers to connect to per torrent while downloading.
throttle.min_peers.normal.set = 32
throttle.max_peers.normal.set = 64

## Minimum and maximum number of peers to connect to per torrent while seeding. (-1 for same value as above)
throttle.min_peers.seed.set = -1
throttle.max_peers.seed.set = -1

## Maximum number of simultaneous downloads and uploads slots per torrent
throttle.max_downloads.set = 32
throttle.max_uploads.set = 32

## Memory address space to map file chunks.
pieces.memory.max.set = 2048M

## Watch directory, saving a torrent file to this directory will automatically start the download.
schedule2 = watch_directory, 5, 5, load.start=/share/data/.rtorrent/watch/*.torrent
schedule2 = untied_directory, 5, 5, stop_untied=/share/data/.rtorrent/watch/*.torrent

## Stop rtorrent from downloading data when disk space is low.
schedule2 = low_diskspace, 15, 60, ((close_low_diskspace, 50G))

## Save rtorrent session data every 5 minutes (default: 20min)
schedule2 = session_save, 300, 300, ((session.save))

## Initialize ruTorrent plugins
execute2 = {sh,-c,/opt/bin/php-cli /opt/var/www/rutorrent/php/initplugins.php &}

## Logging:
## levels = critical error warn notice info debug
## groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_
# Log info
# log.open_file = "logfile", (cat,/opt/var/log/rtorrent.log)
# log.add_output = "debug", "logfile"
# log.add_output = "tracker_debug", "logfile"

Create rTorrent init file:

Note

This init file is heavily inspired by Entware's /opt/etc/init.d/rc.func.

$ nano /opt/etc/init.d/S85rtorrent
#!/bin/sh

PROCS=rtorrent
ARGS="-D -n -o import=/share/data/.rtorrent/.rtorrent.rc"
DESC=$PROCS
PATH=/opt/sbin:/opt/bin:/opt/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ulimit -n 8192   # rtorrent derives network.max_open_files from this

ansi_red="\033[1;31m";
ansi_white="\033[1;37m";
ansi_green="\033[1;32m";
ansi_yellow="\033[1;33m";
ansi_blue="\033[1;34m";
ansi_bell="\007";
ansi_blink="\033[5m";
ansi_std="\033[m";
ansi_rev="\033[7m";
ansi_ul="\033[4m";

ACTION=$1

start() {
    echo -e -n "$ansi_white Starting $DESC... $ansi_std"
    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_yellow already running. $ansi_std"
        return 0
    fi
    $PROC $ARGS > /dev/null 2>&1 &
    COUNTER=0
    LIMIT=10
    while [ -z "$(pgrep -x -f "$PROC $ARGS")" -a "$COUNTER" -le "$LIMIT" ]; do
        sleep 1;
        COUNTER=$((COUNTER + 1))
    done

    if [ -z "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_red failed. $ansi_std"
        logger "Failed to start $DESC from $CALLER."
        return 255
    else
        echo -e "            $ansi_green done. $ansi_std"
        logger "Started $DESC from $CALLER."
        return 0
    fi
}

stop() {
    case "$ACTION" in
        stop | restart)
            echo -e -n "$ansi_white Shutting down $PROC... $ansi_std"
            kill "$(pgrep -x -f "$PROC $ARGS")" 2>/dev/null
            COUNTER=0
            LIMIT=${RTORRENT_STOP_TIMEOUT:-300}
            while [ -n "$(pgrep -x -f "$PROC $ARGS")" -a "$COUNTER" -lt "$LIMIT" ]; do
                sleep 1;
                COUNTER=$((COUNTER + 1))
            done
            ;;
        kill)
            echo -e -n "$ansi_white Killing $PROC... $ansi_std"
            kill -9 "$(pgrep -x -f "$PROC $ARGS")" 2>/dev/null
            sleep 1
            ;;
    esac

    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "       $ansi_red failed. $ansi_std"
        return 255
    else
        rm -f /opt/var/run/rtorrent.sock
        echo -e "       $ansi_green done. $ansi_std"
        return 0
    fi
}

check() {
    echo -e -n "$ansi_white Checking $DESC... "
    if [ -n "$(pgrep -x -f "$PROC $ARGS")" ]; then
        echo -e "            $ansi_green alive. $ansi_std";
        return 0
    else
        echo -e "            $ansi_red dead. $ansi_std";
        return 1
    fi
}

for PROC in $PROCS; do
    case $ACTION in
        start)
            start
            ;;
        stop | kill )
            check && stop
            ;;
        restart)
            if check > /dev/null; then
                stop || exit 255
            fi
            start
            ;;
        check)
            check
            ;;
        *)
            echo -e "$ansi_white Usage: $0 (start|stop|restart|check)$ansi_std"
            exit 1
            ;;
    esac
done

Then run:

$ chmod +x /opt/etc/init.d/S85rtorrent

Note

The ulimit -n 8192 line matters once you are seeding more than a few hundred torrents. rTorrent does not take the file-descriptor limit directly, it scales network.max_open_files from it. Inheriting the default soft limit of 1024 leaves rTorrent capped at 128 open files, so it churns descriptors constantly reopening them. Check the effective value with:

$ grep 'open files' /proc/$(pgrep -f 'rtorrent -D -n')/limits

Match on the command line with -f. rTorrent names its main thread rtorrent main, so pgrep -x rtorrent, which matches against comm, finds nothing.

Install and configure ruTorrent

$ mkdir /opt/var/www
$ git clone https://github.com/Novik/ruTorrent.git /opt/var/www/rutorrent

Create ruTorrent 'user' config

We're not creating any ruTorrent users so we'll edit the default config.php file.

$ nano /opt/var/www/rutorrent/conf/config.php

Add the following:

<?php
        // configuration parameters

        // for snoopy client
        $httpUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
        $httpTimeOut = 30;                      // in seconds
        $httpUseGzip = true;
        $httpIP = null;                         // IP string. Or null for any.
        $httpProxy = array
        (
                'use'   => false,
                'proto' => 'http',              // 'http' or 'https'
                'host'  => 'PROXY_HOST_HERE',
                'port'  => 3128
        );

        // for xmlrpc actions
        $rpcTimeOut = 5;                        // in seconds
        $rpcLogCalls = false;
        $rpcLogFaults = true;

        // for php
        $phpUseGzip = false;
        $phpGzipLevel = 2;

        $schedule_rand = 10;                    // rand for schedulers start, +0..X seconds

        $do_diagnostic = true;                  // Diagnose ruTorrent. Recommended to keep enabled, unless otherwise required.
        $al_diagnostic = true;                  // Diagnose auto-loader. Set to "false" to make composer plugins work.

        $log_file = '/opt/var/log/rutorrent/errors.log';                // path to log file (comment or leave blank to disable logging)

        $saveUploadedTorrents = true;           // Save uploaded torrents to profile/torrents directory or not
        $overwriteUploadedTorrents = false;     // Overwrite existing uploaded torrents in profile/torrents directory or make unique name

        $topDirectory = '/share/data/torrents/';                        // Upper available directory. Absolute path with trail slash.
        $forbidUserSettings = false;

        // $scgi_port = 5000;
        // $scgi_host = "127.0.0.1";

        // For web->rtorrent link through unix domain socket
        // (scgi_local in rtorrent conf file), change variables
        // above to something like this:
        //

        $scgi_port = 0;
        $scgi_host = "unix:///opt/var/run/rtorrent.sock";

        $XMLRPCMountPoint = "/RPC2";            // DO NOT DELETE THIS LINE!!! DO NOT COMMENT THIS LINE!!!

        $throttleMaxSpeed = 327625*1024;        // DO NOT EDIT THIS LINE!!! DO NOT COMMENT THIS LINE!!!
        // Can't be greater then 327625*1024 due to limitation in libtorrent ResourceManager::set_max_upload_unchoked function.

        $pathToExternals = array(
                "curl"   => '/opt/bin/curl',
                "gzip"   => '/opt/bin/gzip',
                "id"     => '/usr/bin/id',
                "php"    => '/opt/bin/php-cli',
                "pgrep"  => '/opt/bin/pgrep',
                "python" => '/opt/bin/python3',
                "stat"   => '/usr/bin/stat'
        );

        $localHostedMode = true;                // Set to true if rTorrent is hosted on the SAME machine as ruTorrent

        $cachedPluginLoading = true;            // Set to true to enable rapid cached loading of ruTorrent plugins
                                                // Required to clear web browser cache when upgrading versions

        $localhosts = array(                    // list of local interfaces
                "127.0.0.1",
                "localhost",
        );

        $profilePath = '../../share';           // Path to user profiles
        $profileMask = 0777;                    // Mask for files and directory creation in user profiles.
                                                // Both Webserver and rtorrent users must have read-write access to it.
                                                // For example, if Webserver and rtorrent users are in the same group then the value may be 0770.

        $tempDirectory = null;                  // Temp directory. Absolute path with trail slash. If null, then autodetect will be used.

        $canUseXSendFile = false;               // If true then use X-Sendfile feature if it exist

        $locale = "UTF8";

        $enableCSRFCheck = false;               // If true then Origin and Referer will be checked
        $enabledOrigins = array();              // List of enabled domains for CSRF check (only hostnames, without protocols, port etc.).
                                                // If empty, then will retrieve domain from HTTP_HOST / HTTP_X_FORWARDED_HOST

Configure ruTorrent plugins follows:

$ nano /opt/var/www/rutorrent/conf/plugins.ini
;; Plugins' permissions.
;; If flag is not found in plugin section, corresponding flag from "default" section is used.
;; If flag is not found in "default" section, it is assumed to be "yes".
;;
;; For setting individual plugin permissions you must write something like that:
;;
;; [ratio]
;; enabled = yes                        ;; also may be "user-defined", in this case user can control plugin's state from UI
;; canChangeToolbar = yes
;; canChangeMenu = yes
;; canChangeOptions = no
;; canChangeTabs = yes
;; canChangeColumns = yes
;; canChangeStatusBar = yes
;; canChangeCategory = yes
;; canBeShutdowned = yes

[default]
enabled = user-defined
canChangeToolbar = yes
canChangeMenu = yes
canChangeOptions = yes
canChangeTabs = yes
canChangeColumns = yes
canChangeStatusBar = yes
canChangeCategory = yes
canBeShutdowned = yes

[ipad]
enabled = no

[httprpc]
enabled = no

[retrackers]
enabled = no

[rpc]
enabled = no

[rutracker_check]
enabled = no

[geoip]
enabled = no

[geoip2]
enabled = yes

[spectrogram]
enabled = no

Configure create plugin

$ nano /opt/var/www/rutorrent/plugins/create/conf.php
$useExternal = 'mktorrent';
$pathToCreatetorrent = '/opt/bin/mktorrent';

Install cloudscraper plugin

cloudscraper is a dependency of the _cloudflare plugin.

$ pip3 install cloudscraper

Install ratiocolor plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:Micdu70/rutorrent-ratiocolor.git ratiocolor

Install geoip2 plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:Micdu70/geoip2-rutorrent.git geoip2

Install nfo plugin

$ cd /opt/var/www/rutorrent/plugins
$ git clone [email protected]:phracker/ruTorrent-nfo.git nfo

Hint

You might need to "fix" tracklabels missing icons as described here.

Configure PHP/PHP-FPM

For PHP:

$ nano /opt/etc/php.ini
memory_limit = 128M
;doc_root = "/opt/share/www"
upload_max_filesize = 64M
max_file_uploads = 64
date.timezone = "Europe/Luxembourg"
sys_temp_dir = "/opt/tmp"

For PHP-FPM:

$ nano /opt/etc/php8-fpm.d/www.conf
user = admin
env[PATH] = /opt/bin:/opt/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/bin/X11:/usr/local/sbin:/usr/local/bin

pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6

slowlog = /opt/var/log/php8-fpm.slow.log
request_slowlog_timeout = 5s

Note

The packaged default is pm.max_children = 5. ruTorrent fires several requests per poll tick, so a single browser tab can occupy most of that on its own, and check_port can hold a worker for up to $httpTimeOut seconds waiting on an external site. Watch for server reached pm.max_children setting in /opt/var/log/php8-fpm.log.

Each worker is bounded by memory_limit (128M above), so 20 workers is a 2.5G worst case. Size it down on a device with little RAM.

request_slowlog_timeout is worth setting before you need it. Any request over the threshold is dumped to slowlog with a full PHP backtrace, which turns "the UI froze" into an exact function and line.

Allow PHP-FPM to run as root

$ nano /opt/etc/init.d/S79php8-fpm
ARGS="--daemonize -R --fpm-config /opt/etc/php8-fpm.conf"

Note

You'll need to edit the init file to allow php-fpm to run as root each time the package is updated.

Configure Nginx

$ nano /opt/etc/nginx/nginx.conf
user admin administrators;
worker_processes auto;
pid /opt/var/run/nginx.pid;
include /opt/etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 1024;
    use epoll;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
    keepalive_timeout 65;
    keepalive_requests 100;
    keepalive_disable msie6;
    types_hash_max_size 2048;
    server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /opt/etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # SSL Settings
    ##

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
    ssl_prefer_server_ciphers on;

    ##
    # Logging Settings
    ##

    access_log /opt/var/log/nginx/access.log;
    error_log /opt/var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";
    gzip_min_length 512;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_types
        text/css
        text/javascript
        text/xml
        text/plain
        text/x-component
        application/javascript
        application/x-javascript
        application/json
        application/xml
        application/rss+xml
        application/vnd.ms-fontobject
        font/truetype
        font/opentype
        image/svg+xml;

    ##
    # Virtual Host Configs
    ##

    include /opt/etc/nginx/conf.d/*.conf;
    include /opt/etc/nginx/sites-enabled/*;
}

Create the proper Nginx symlink

$ ln -s /opt/etc/nginx/sites-available/rutorrent.example.com.conf /opt/etc/nginx/sites-enabled/rutorrent.example.com.conf

Configure Nginx SCGI

$ nano /opt/etc/nginx/scgi_params
scgi_param  REQUEST_METHOD     $request_method;
scgi_param  REQUEST_URI        $request_uri;
scgi_param  QUERY_STRING       $query_string;
scgi_param  CONTENT_TYPE       $content_type;

scgi_param  DOCUMENT_URI       $document_uri;
scgi_param  DOCUMENT_ROOT      $document_root;
scgi_param  SCGI               1;
scgi_param  SERVER_PROTOCOL    $server_protocol;
scgi_param  REQUEST_SCHEME     $scheme;
scgi_param  HTTPS              $https if_not_empty;

scgi_param  REMOTE_ADDR        $remote_addr;
scgi_param  REMOTE_PORT        $remote_port;
scgi_param  SERVER_PORT        $server_port;
scgi_param  SERVER_NAME        $server_name;

Create Nginx vhost

$ mkdir --parent /opt/etc/nginx/{sites-available,sites-enabled}
$ nano /opt/etc/nginx/sites-available/rutorrent.example.com.conf
server {
    listen 80;
    server_name rutorrent.example.com;
    return 301 https://$server_name$request_uri;

    access_log /opt/var/log/nginx/example.com/rutorrent/access.log;
    error_log /opt/var/log/nginx/example.com/rutorrent/error.log;
}

server {
    listen 443 ssl;
    http2 on;
    server_name rutorrent.example.com;

    access_log /opt/var/log/nginx/example.com/rutorrent/access.log;
    error_log /opt/var/log/nginx/example.com/rutorrent/error.log;

    ssl_certificate /opt/etc/acme.sh/rutorrent.example.com_ecc/fullchain.cer;
    ssl_certificate_key /opt/etc/acme.sh/rutorrent.example.com_ecc/rutorrent.example.com.key;
    ssl_dhparam /opt/etc/acme.sh/rutorrent.example.com_ecc/dhparam.pem;
    ssl_session_timeout 5m;
    ssl_prefer_server_ciphers on;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    add_header Strict-Transport-Security "max-age=15768000" always; # six month

    charset utf-8;
    index index.html index.php;
    root /opt/var/www/rutorrent;

    client_max_body_size 64M;

    error_page 500 502 503 504 /50x.html;

    location /RPC2 {
        include scgi_params;
        scgi_pass unix:/opt/var/run/rtorrent.sock;
    }

    location = /50x.html {
        root /opt/share/nginx/html;
    }

    location = /favicon.ico {
        access_log off;
        log_not_found off;
    }

    location ~ ^/(conf|share)/(.+)$ {
        deny all;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        fastcgi_index  index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param HTTPS on;
        # Avoid sending the security headers twice
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass unix:/opt/var/run/php8-fpm.sock;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
    }

    location ~* \.(jpg|jpeg|gif|css|png|js|map|woff|woff2|ttf|svg|eot)$ {
        expires 30d;
        access_log off;
    }
}

Generate TLS certificates for vhost

Install acme.sh:

$ cd /tmp
$ git clone https://github.com/acmesh-official/acme.sh.git
$ cd acme.sh/
$ ./acme.sh --install --nocron --home /opt/etc/acme.sh --accountemail "[email protected]"

Configure your DNS provider API keys:

$ nano /opt/etc/acme.sh/dnsapi/dns_cf.sh
#!/opt/bin/bash

#
#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
#
#CF_Email="[email protected]"

CF_Token="foo"
CF_Account_ID="bar"
CF_Zone_ID="foobar"

CF_Api="https://api.cloudflare.com/client/v4"

Generate TLS certificate:

$ /opt/etc/acme.sh/acme.sh --home /opt/etc/acme.sh --server letsencrypt --issue -d example.com --dns dns_cf

Setup a cronjob to automatically renew your certificate:

$ crontab -e
30 3 * * 0 "/opt/etc/acme.sh/acme.sh" --cron --home "/opt/etc/acme.sh" > /dev/null

Setup a Diffie-Helmann key as follows:

$ wget https://ssl-config.mozilla.org/ffdhe4096.txt -O /opt/etc/acme.sh/rutorrent.example.com_ecc/dhparam.pem

Restart services

$ /opt/etc/init.d/S79php8-fpm restart
$ /opt/etc/init.d/S80nginx restart
$ /opt/etc/init.d/S85rtorrent start

Hint

Nginx might error out with the following message:

nginx: [error] open() "/opt/var/run/nginx.pid" failed (2: No such file or directory)

To fix it, simply run the following command:

$ nginx -c /opt/etc/nginx/nginx.conf

You should now be all set! 🎉

Maintenance

Restarting rTorrent with a large session

rTorrent flushes its session to disk on shutdown, and how long that takes depends on how much state is dirty rather than on the torrent count alone. Two measurements on the same machine at roughly 2450 torrents: a stop with a stale session took about 80 seconds, while one taken shortly after a session.save completed in 13. The init file above waits up to RTORRENT_STOP_TIMEOUT seconds (300 by default), which covers both, and removes the socket only once the process has actually exited, so restart is safe to use:

$ /opt/etc/init.d/S85rtorrent restart

Override the timeout for a very large session:

$ RTORRENT_STOP_TIMEOUT=600 /opt/etc/init.d/S85rtorrent restart

Warning

Entware's stock rc.func, and earlier revisions of this guide, wait only ten seconds and then rm the socket unconditionally. Once a shutdown runs past those ten seconds the socket is unlinked while rTorrent still holds it, stop() returns failure, the dispatch ignores that failure and calls start(), and start() sees the still-dying process and reports "already running". rTorrent ends up stopped with no socket, and the script prints success. If you are running an older copy, check that stop() removes the socket only after confirming the process is gone.

Upgrading Entware packages

Warning

opkg upgrade php8 can leave PHP unable to start. PHP links against a specific libxml2 soname, but the dependency is declared unversioned, so opkg happily upgrades PHP while leaving an older libxml2 in place. Both binaries then fail with:

error while loading shared libraries: libxml2.so.16: cannot open shared object file

Upgrade libxml2 in the same transaction. Note that libxslt and python3-lxml link the old soname, so upgrade those together or they break instead. Check what needs what before restarting anything:

$ ldd /opt/bin/php8-fpm | grep -E 'libxml2|not found'

Warning

A python3 minor-version bump (3.11 to 3.13, say) moves site-packages and strands everything installed with pip, including cloudscraper for the _cloudflare plugin. pip itself goes too, so reinstall it before anything else. This is the one case that genuinely needs --force-reinstall, because the package version is unchanged and only its install path moved, so refresh the index first (see the warning below):

$ opkg update
$ opkg install --force-reinstall python3-pip
$ python3 -m pip install cloudscraper==1.2.71

The old tree is left behind at /opt/lib/python3.<old> and is dead weight once nothing references it.

Warning

Avoid opkg install --force-reinstall unless you have checked the package is actually downloadable. It removes the installed copy before fetching the replacement, so a stale index (the .ipk listed but missing from the feed) leaves you with the package uninstalled and no way to put it back. Run opkg update first, and prefer a plain opkg install.