Files
XC_VM/tests/Unit/ModuleCronRefreshTest.php
T
obscuremindandClaude Opus 5.5 06987df46c fix(cron): compare root's crontab itself, keep no marker of the last write
root_signals remembered the modules it had last written for as an MD5 of
config/modules.php in config/crontab.modules. Three things were wrong with
that:

- root wrote the marker with file_put_contents() in a directory xc_vm can
  write: a link planted there had root overwrite the file it pointed to;
- a `startup` or `status` holding an older module list could write it over
  a newer crontab while the marker still matched, and nothing put it right;
- the hash was MD5.

installRootCrontab() already compares the crontab with the list it wants
and writes only on a difference, so root_signals now simply calls it every
minute: an older list written by another process is corrected a minute
later, and there is no marker left to link or to hash.

The every-minute call is quiet when nothing changes, and writes nothing
when the crontab cannot be read: it runs from that crontab, so an
unreadable one is not an empty one, and writing over it would drop the
administrator's own lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:17:00 +00:00

44 lines
2.3 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Cli\Commands\StartupCommand;
use XcVm\Cli\CronJobs\RootSignalsCronJob;
/**
* root_signals brings root's crontab up to date with the modules every
* minute, so a module installed or enabled from the panel gets its cron lines
* (the anti-abuse shield's, which starts its honeypot) without a restart, and
* a `startup` or `status` that wrote an older list is put right a minute
* later. It keeps nothing of its last write: the crontab is what is compared.
*/
final class ModuleCronRefreshTest extends TestCase {
public function testEveryRunChecksTheCrontab(): void {
$rRuns = 0;
$rWrite = static function () use (&$rRuns): bool {
$rRuns++;
return true;
};
$this->assertTrue(RootSignalsCronJob::refreshModuleCrons($rWrite));
$this->assertTrue(RootSignalsCronJob::refreshModuleCrons($rWrite), 'the next minute too: nothing says "already done"');
$this->assertSame(2, $rRuns);
$this->assertFalse(RootSignalsCronJob::refreshModuleCrons(static fn(): bool => false), 'a crontab that could not be written');
}
/** Root writes no file where xc_vm could plant a link, and hashes nothing. */
public function testNothingIsKeptOfTheLastWrite(): void {
$rMethod = new ReflectionMethod(RootSignalsCronJob::class, 'refreshModuleCrons');
$rSource = implode('', array_slice(file((string) $rMethod->getFileName()), $rMethod->getStartLine() - 1, $rMethod->getEndLine() - $rMethod->getStartLine() + 1));
$this->assertStringNotContainsString('file_put_contents', $rSource);
$this->assertStringNotContainsString('md5', $rSource);
}
/** The every-minute check says nothing when the list is there: cron would mail it each minute. */
public function testTheCheckIsQuietWhenNothingChanges(): void {
$rParameters = (new ReflectionMethod(StartupCommand::class, 'installRootCrontab'))->getParameters();
$this->assertSame('rQuiet', $rParameters[0]->getName());
$this->assertFalse($rParameters[0]->getDefaultValue(), 'startup and status still say it');
$this->assertStringContainsString('StartupCommand::installRootCrontab(true)', (string) file_get_contents(MAIN_HOME . 'Cli/CronJobs/RootSignalsCronJob.php'));
$this->assertStringContainsString('if ($rQuiet && $rListed !== 0)', (string) file_get_contents(MAIN_HOME . 'Cli/Commands/StartupCommand.php'), 'an unreadable crontab is not rewritten each minute');
}
}