Origin: Customer support ticket. Suggested Fix reflects ticket agent
investigation — do not derive fix from title or symptom alone.
Summary
userTransfer.php's post-transfer convergence starts rTorrent unconditionally. It never consults the .rtorrentDisable per-user opt-out marker, so a user who has explicitly opted out of rTorrent has it started for them the moment their migration completes.
Symptom
A user opts out of rTorrent (~/.rtorrentDisable present — the GH#470 per-user opt-out that checkRtorrent.php honours). Their account is then migrated to another host with userTransfer.php. At the end of the transfer rTorrent starts on the target, loads the migrated session and begins announcing. It is stopped again on the next checkRtorrent.php tick, so the window is bounded by the cron interval (currently */2), but the client does start and does announce.
Where the source account is still live and seeding the same torrents — the normal state during a staged migration — both hosts announce the same torrents for that window.
Root Cause
scripts/lib/userTransfer/postSetup.php:
pmssUserTransferPostSetup() calls pmssUserTransferRequestRtorrentRestart($home, $localUser) unconditionally (~line 44).
pmssUserTransferRequestRtorrentRestart() touches www/.rtorrentRestart, then pmssUserTransferRunRtorrentRestart() executes ~/.rtorrentRestart.php immediately ("so migration is not cron-latent").
etc/skel/.rtorrentRestart.php gates only on file_exists('www/.rtorrentRestart'). There is no .rtorrentDisable check anywhere on that path.
scripts/cron/checkRtorrent.php (~line 123-129) DOES honour the marker — "stop and stay stopped" — which is why the state self-corrects rather than persisting. So the opt-out contract is enforced in one code path and ignored in another: the same condition, two rules, guaranteed to drift.
Reproduction
- Create
~/.rtorrentDisable for a test user; confirm checkRtorrent.php keeps rTorrent stopped.
- On a target host run
php /scripts/util/userTransfer.php <user> <user> <source-host> for that user.
- When the transfer converges, observe rTorrent running on the target despite the marker.
- Wait one
checkRtorrent.php tick — it is stopped again.
Environment
PMSS git/main, Debian 12 (bookworm). The code path is suite-independent.
Suggested Fix
Guard the restart request on the same predicate checkRtorrent.php already uses, at the top of pmssUserTransferRequestRtorrentRestart():
if (is_file($home.'/.rtorrentDisable')) {
logMessage('[INFO] Skipping rTorrent restart (user opt-out marker .rtorrentDisable present)');
return;
}
so neither the marker file nor the immediate execution happens.
Preferred variant: extract the predicate into one shared helper (e.g. pmssRtorrentUserOptedOut(string $home): bool) called from BOTH checkRtorrent.php and postSetup.php, so the opt-out contract lives in a single place and cannot drift again. The inline check is the minimal fix; the shared predicate is the correct one.
Related but distinct: #717 (.rtorrentRestart.php comm matching) and #836 (checkRtorrent.php killall comm matching) concern how the restart/stop identifies the process. This issue is about the restart being requested at all.
(The cron stops it two minutes later; it should never have started.)
Tier-3 declared at filing: --why public-surface — GH#631 tier gate. Declared fix size: 25 lines vs 52-line body — size gate (operator directive 2026-07-29).
Summary
userTransfer.php's post-transfer convergence starts rTorrent unconditionally. It never consults the.rtorrentDisableper-user opt-out marker, so a user who has explicitly opted out of rTorrent has it started for them the moment their migration completes.Symptom
A user opts out of rTorrent (
~/.rtorrentDisablepresent — the GH#470 per-user opt-out thatcheckRtorrent.phphonours). Their account is then migrated to another host withuserTransfer.php. At the end of the transfer rTorrent starts on the target, loads the migrated session and begins announcing. It is stopped again on the nextcheckRtorrent.phptick, so the window is bounded by the cron interval (currently*/2), but the client does start and does announce.Where the source account is still live and seeding the same torrents — the normal state during a staged migration — both hosts announce the same torrents for that window.
Root Cause
scripts/lib/userTransfer/postSetup.php:pmssUserTransferPostSetup()callspmssUserTransferRequestRtorrentRestart($home, $localUser)unconditionally (~line 44).pmssUserTransferRequestRtorrentRestart()toucheswww/.rtorrentRestart, thenpmssUserTransferRunRtorrentRestart()executes~/.rtorrentRestart.phpimmediately ("so migration is not cron-latent").etc/skel/.rtorrentRestart.phpgates only onfile_exists('www/.rtorrentRestart'). There is no.rtorrentDisablecheck anywhere on that path.scripts/cron/checkRtorrent.php(~line 123-129) DOES honour the marker — "stop and stay stopped" — which is why the state self-corrects rather than persisting. So the opt-out contract is enforced in one code path and ignored in another: the same condition, two rules, guaranteed to drift.Reproduction
~/.rtorrentDisablefor a test user; confirmcheckRtorrent.phpkeeps rTorrent stopped.php /scripts/util/userTransfer.php <user> <user> <source-host>for that user.checkRtorrent.phptick — it is stopped again.Environment
PMSS
git/main, Debian 12 (bookworm). The code path is suite-independent.Suggested Fix
Guard the restart request on the same predicate
checkRtorrent.phpalready uses, at the top ofpmssUserTransferRequestRtorrentRestart():so neither the marker file nor the immediate execution happens.
Preferred variant: extract the predicate into one shared helper (e.g.
pmssRtorrentUserOptedOut(string $home): bool) called from BOTHcheckRtorrent.phpandpostSetup.php, so the opt-out contract lives in a single place and cannot drift again. The inline check is the minimal fix; the shared predicate is the correct one.Related but distinct: #717 (
.rtorrentRestart.phpcomm matching) and #836 (checkRtorrent.phpkillall comm matching) concern how the restart/stop identifies the process. This issue is about the restart being requested at all.(The cron stops it two minutes later; it should never have started.)
Tier-3 declared at filing:
--why public-surface— GH#631 tier gate. Declared fix size:25lines vs 52-line body — size gate (operator directive 2026-07-29).