call cron asynchronously + tests

This commit is contained in:
odain
2026-02-25 17:28:15 +01:00
parent 259c82b88e
commit 393643b6f9
12 changed files with 375 additions and 143 deletions

View File

@@ -9,15 +9,15 @@
*/
interface iTokenLoginUIExtension
{
/**
* @return array
* @api
*/
public function GetTokenInfo() : array;
/**
* @return array
* @api
*/
public function GetTokenInfo(): array;
/**
* @return array
* @api
*/
public function GetUserLogin(array $aTokenInfo) : string;
}
public function GetUserLogin(array $aTokenInfo): string;
}

View File

@@ -130,8 +130,7 @@ class LoginBasic extends AbstractLoginFSMExtension implements iTokenLoginUIExten
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'basic';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal'))
{
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}

View File

@@ -33,10 +33,8 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension,
{
if (!Session::IsSet('login_mode') || Session::Get('login_mode') == 'form') {
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
if ($this->bForceFormOnError || empty($sAuthUser) || empty($sAuthPwd))
{
if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER))
{
if ($this->bForceFormOnError || empty($sAuthUser) || empty($sAuthPwd)) {
if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER)) {
// X-Combodo-Ajax is a special header automatically added to all ajax requests
// Let's reply that we're currently logged-out
header('HTTP/1.0 401 Unauthorized');
@@ -65,11 +63,9 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension,
*/
protected function OnCheckCredentials(&$iErrorCode)
{
if (Session::Get('login_mode') == 'form')
{
if (Session::Get('login_mode') == 'form') {
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal'))
{
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) {
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}
@@ -159,8 +155,8 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension,
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'form';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal'))
{
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}

View File

@@ -28,8 +28,7 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi
protected function OnModeDetection(&$iErrorCode)
{
if (!Session::IsSet('login_mode') && !$this->bErrorOccurred)
{
if (!Session::IsSet('login_mode') && !$this->bErrorOccurred) {
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
{
Session::Set('login_mode', 'url');
@@ -48,11 +47,9 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi
protected function OnCheckCredentials(&$iErrorCode)
{
if (Session::Get('login_mode') == 'url')
{
if (Session::Get('login_mode') == 'url') {
list($sAuthUser, $sAuthPwd) = $this->GetTokenInfo();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal'))
{
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, Session::Get('login_mode'), 'internal')) {
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}
@@ -97,8 +94,7 @@ class LoginURL extends AbstractLoginFSMExtension implements iTokenLoginUIExtensi
{
$sLogin = $aTokenInfo[0];
$sLoginMode = 'url';
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal'))
{
if (UserRights::CheckCredentials($sLogin, $aTokenInfo[1], $sLoginMode, 'internal')) {
return $sLogin;
}

View File

@@ -530,18 +530,15 @@ class LoginWebPage extends NiceWebPage
return $aPlugins;
}
public static function GetCurrentLoginPlugin(string $sCurrentLoginMode) : iLoginExtension
public static function GetCurrentLoginPlugin(string $sCurrentLoginMode): iLoginExtension
{
/** @var iLoginExtension $oLoginExtensionInstance */
foreach (MetaModel::EnumPlugins('iLoginFSMExtension') as $oLoginExtensionInstance)
{
foreach (MetaModel::EnumPlugins('iLoginFSMExtension') as $oLoginExtensionInstance) {
$aLoginModes = $oLoginExtensionInstance->ListSupportedLoginModes();
$aLoginModes = (is_array($aLoginModes) ? $aLoginModes : array());
foreach ($aLoginModes as $sLoginMode)
{
$aLoginModes = (is_array($aLoginModes) ? $aLoginModes : []);
foreach ($aLoginModes as $sLoginMode) {
// Keep only the plugins for the current login mode + before + after
if ($sLoginMode == $sCurrentLoginMode)
{
if ($sLoginMode == $sCurrentLoginMode) {
return $oLoginExtensionInstance;
}
}

View File

@@ -3226,4 +3226,43 @@ TXT
return $aTrace;
}
/**
* @author Ain Tohvri <https://mstdn.social/@tekkie>
*
* @since ???
*/
public static function ReadTail($sFilename, $iLines = 1): array
{
$handle = fopen($sFilename, "r");
if (false === $handle) {
throw new \Exception("Cannot read file $sFilename");
}
$iLineCounter = $iLines;
$iPos = -2;
$bBeginning = false;
$aLines = [];
while ($iLineCounter > 0) {
$sChar = " ";
while ($sChar != "\n") {
if (fseek($handle, $iPos, SEEK_END) == -1) {
$bBeginning = true;
break;
}
$sChar = fgetc($handle);
$iPos--;
}
$iLineCounter--;
if ($bBeginning) {
rewind($handle);
}
$aLines[$iLines - $iLineCounter - 1] = fgets($handle);
if ($bBeginning) {
break;
}
}
fclose($handle);
return array_reverse($aLines);
}
}

View File

@@ -976,7 +976,7 @@ abstract class ItopDataTestCase extends ItopTestCase
protected function AssertLastErrorLogEntryContains(string $sNeedle, string $sMessage = ''): void
{
$aLastLines = self::ReadTail(APPROOT.'/log/error.log');
$aLastLines = Utils::ReadTail(APPROOT.'/log/error.log');
$this->assertStringContainsString($sNeedle, $aLastLines[0], $sMessage);
}
@@ -1470,4 +1470,21 @@ abstract class ItopDataTestCase extends ItopTestCase
@chmod($sConfigPath, 0440);
@unlink($this->sConfigTmpBackupFile);
}
protected function AddLoginModeAndSaveConfiguration($sLoginMode)
{
$aAllowedLoginTypes = $this->oiTopConfig->GetAllowedLoginTypes();
if (!in_array($sLoginMode, $aAllowedLoginTypes)) {
$aAllowedLoginTypes[] = $sLoginMode;
$this->oiTopConfig->SetAllowedLoginTypes($aAllowedLoginTypes);
$this->SaveItopConfFile();
}
}
private function SaveItopConfFile()
{
@chmod($this->oiTopConfig->GetLoadedFile(), 0770);
$this->oiTopConfig->WriteToFile();
@chmod($this->oiTopConfig->GetLoadedFile(), 0440);
}
}

View File

@@ -15,6 +15,8 @@ use SetupUtils;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpKernel\KernelInterface;
use Utils;
use const DEBUG_BACKTRACE_IGNORE_ARGS;
/**
@@ -89,7 +91,7 @@ abstract class ItopTestCase extends KernelTestCase
if (method_exists('utils', 'GetConfig')) {
// Reset the config by forcing the load from disk
$oConfig = \utils::GetConfig(true);
$oConfig = utils::GetConfig(true);
if (method_exists('MetaModel', 'SetConfig')) {
\MetaModel::SetConfig($oConfig);
}
@@ -625,32 +627,7 @@ abstract class ItopTestCase extends KernelTestCase
*/
protected static function ReadTail($sFilename, $iLines = 1)
{
$handle = fopen($sFilename, "r");
$iLineCounter = $iLines;
$iPos = -2;
$bBeginning = false;
$aLines = [];
while ($iLineCounter > 0) {
$sChar = " ";
while ($sChar != "\n") {
if (fseek($handle, $iPos, SEEK_END) == -1) {
$bBeginning = true;
break;
}
$sChar = fgetc($handle);
$iPos--;
}
$iLineCounter--;
if ($bBeginning) {
rewind($handle);
}
$aLines[$iLines - $iLineCounter - 1] = fgets($handle);
if ($bBeginning) {
break;
}
}
fclose($handle);
return array_reverse($aLines);
return Utils::ReadTail($sFilename, $iLines);
}
/**

View File

@@ -4,6 +4,7 @@ namespace Combodo\iTop\Test\UnitTest\Webservices;
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use Exception;
use iTopMutex;
use MetaModel;
use utils;
@@ -17,8 +18,8 @@ class CronTest extends ItopDataTestCase
public const USE_TRANSACTION = false;
public const CREATE_TEST_ORG = false;
private static $sLogin;
private static $sPassword = "Iuytrez9876543ç_è-(";
public static $sLogin;
public static $sPassword = "Iuytrez9876543ç_è-(";
/**
* This method is called before the first test of this test class is run (in the current process).
@@ -36,20 +37,43 @@ class CronTest extends ItopDataTestCase
parent::tearDownAfterClass();
}
public function ModeProvider()
{
$aModes = ['form', 'url', 'basic'];
$aUsecases = [];
foreach ($aModes as $sMode) {
$aUsecases[$sMode] = [$sMode];
}
return $aUsecases;
}
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
$this->BackupConfiguration();
static::$sLogin = "rest-user-".date('dmYHis');
static::$sLogin = "rest-user-";//.date('dmYHis');
$this->CreateTestOrganization();
}
public function testListOperationsAndJSONPCallback()
/**
* @throws Exception
*/
protected function tearDown(): void
{
parent::tearDown();
$this->ReleaseCronIfBusy();
}
public function testRestWithFormMode()
{
$this->AddLoginModeAndSaveConfiguration('form');
$this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]);
$aPostFields = [
'version' => '1.3',
@@ -58,27 +82,206 @@ class CronTest extends ItopDataTestCase
'json_data' => '{"operation": "list_operations"}',
];
// Test regular JSON result
$sJSONResult = $this->CallItopUri("/webservices/rest.php", $aPostFields);
$sExpected = <<<JSON
{"code":0,"message":"Operations: 7","version":"1.3","operations":[{"verb":"core\/create","description":"Create an object","extension":"CoreServices"},{"verb":"core\/update","description":"Update an object","extension":"CoreServices"},{"verb":"core\/apply_stimulus","description":"Apply a stimulus to change the state of an object","extension":"CoreServices"},{"verb":"core\/get","description":"Search for objects","extension":"CoreServices"},{"verb":"core\/delete","description":"Delete objects","extension":"CoreServices"},{"verb":"core\/get_related","description":"Get related objects through the specified relation","extension":"CoreServices"},{"verb":"core\/check_credentials","description":"Check user credentials","extension":"CoreServices"}]}
JSON;
$this->assertEquals($sExpected, $sJSONResult);
$this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult);
}
private function CreateUserWithProfiles(array $aProfileIds): void
public function testRestWithBasicMode()
{
$this->AddLoginModeAndSaveConfiguration('basic');
$this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]);
$aPostFields = [
'version' => '1.3',
'json_data' => '{"operation": "list_operations"}',
];
$sToken = base64_encode(sprintf("%s:%s", static::$sLogin, static::$sPassword));
$aCurlOptions = [
CURLOPT_HTTPHEADER => ["Authorization: Basic $sToken"],
];
// Test regular JSON result
$sJSONResult = $this->CallItopUri("/webservices/rest.php", $aPostFields, $aCurlOptions);
$this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult);
}
public function testRestWithUrlMode()
{
$this->AddLoginModeAndSaveConfiguration('url');
$this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator'], self::$aURP_Profiles['REST Services User']]);
$aPostFields = [
'version' => '1.3',
'json_data' => '{"operation": "list_operations"}',
];
$aGetFields = [
'auth_user' => static::$sLogin,
'auth_pwd' => static::$sPassword,
];
$sJSONResult = $this->CallItopUri("/webservices/rest.php?".http_build_query($aGetFields), $aPostFields);
$this->assertEquals($this->GetExpectedRestResponse(), $sJSONResult);
}
public function testLaunchCronWithFormModeFailWhenNotAdmin()
{
$this->ForceCronBusyError();
$this->AddLoginModeAndSaveConfiguration('form');
$this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]);
$sLogFileName = "crontest_".uniqid();
$aPostFields = [
'version' => '1.3',
'auth_user' => static::$sLogin,
'auth_pwd' => static::$sPassword,
'verbose' => 1,
'debug' => 1,
'cron_log_file' => $sLogFileName,
];
$sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php", $aPostFields);
$this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult);
$sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName);
$this->CheckAdminAccessIssueWithCron($sLogFile);
}
public function testLaunchCronWithBasicModeFailWhenNotAdmin()
{
$this->ForceCronBusyError();
$this->AddLoginModeAndSaveConfiguration('basic');
$this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]);
$sLogFileName = "crontest_".uniqid();
$aPostFields = [
'version' => '1.3',
'verbose' => 1,
'debug' => 1,
'cron_log_file' => $sLogFileName,
];
$sToken = base64_encode(sprintf("%s:%s", static::$sLogin, static::$sPassword));
$aCurlOptions = [
CURLOPT_HTTPHEADER => ["Authorization: Basic $sToken"],
];
$sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php", $aPostFields, $aCurlOptions);
$this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult);
$sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName);
$this->CheckAdminAccessIssueWithCron($sLogFile);
}
public function testLaunchCronWithUrlModeFailWhenNotAdmin()
{
$this->ForceCronBusyError();
$this->AddLoginModeAndSaveConfiguration('url');
$this->CreateUserWithProfiles([self::$aURP_Profiles['REST Services User']]);
$sLogFileName = "crontest_".uniqid();
$aPostFields = [
'version' => '1.3',
'verbose' => 1,
'debug' => 1,
'cron_log_file' => $sLogFileName,
];
$aGetFields = [
'auth_user' => static::$sLogin,
'auth_pwd' => static::$sPassword,
];
$sJSONResult = $this->CallItopUri("/webservices/launch_cron_asynchronously.php?".http_build_query($aGetFields), $aPostFields);
$this->assertEquals($this->GetExpectedCronResponse(), $sJSONResult);
$sLogFile = $this->CheckLogFileIsGeneratedAndGetFullPath($sLogFileName);
$this->CheckAdminAccessIssueWithCron($sLogFile);
;
}
/**
* @dataProvider ModeProvider
*/
public function testGetUserLoginWithFormMode($sMode)
{
$this->AddLoginModeAndSaveConfiguration($sMode);
$this->CreateUserWithProfiles([self::$aURP_Profiles['Administrator']]);
$oLoginMode = new \LoginForm();
$sUserLogin = $oLoginMode->GetUserLogin([static::$sLogin, static::$sPassword]);
$this->assertEquals(static::$sLogin, $sUserLogin);
}
private ?iTopMutex $oMutex = null;
private function ForceCronBusyError(): void
{
try {
$oMutex = new iTopMutex('cron');
if ($oMutex->TryLock()) {
$this->oMutex = $oMutex;
}
} catch (Exception $e) {
}
}
private function ReleaseCronIfBusy(): void
{
try {
if (! is_null($this->oMutex)) {
$this->oMutex->Unlock();
}
} catch (Exception $e) {
}
}
private function CreateUserWithProfiles(array $aProfileIds): ?string
{
if (count($aProfileIds) > 0) {
$oUser = null;
foreach ($aProfileIds as $oProfileId) {
foreach ($aProfileIds as $iProfileId) {
if (is_null($oUser)) {
$oUser = $this->CreateUser(static::$sLogin, $oProfileId, static::$sPassword);
$oUser = $this->CreateContactlessUser(static::$sLogin, $iProfileId, static::$sPassword);
} else {
$this->AddProfileToUser($oUser, $oProfileId);
$this->AddProfileToUser($oUser, $iProfileId);
}
$oUser->DBWrite();
}
return $oUser->GetKey();
}
return null;
}
private function GetExpectedRestResponse(): string
{
return <<<JSON
{"code":0,"message":"Operations: 7","version":"1.3","operations":[{"verb":"core\/create","description":"Create an object","extension":"CoreServices"},{"verb":"core\/update","description":"Update an object","extension":"CoreServices"},{"verb":"core\/apply_stimulus","description":"Apply a stimulus to change the state of an object","extension":"CoreServices"},{"verb":"core\/get","description":"Search for objects","extension":"CoreServices"},{"verb":"core\/delete","description":"Delete objects","extension":"CoreServices"},{"verb":"core\/get_related","description":"Get related objects through the specified relation","extension":"CoreServices"},{"verb":"core\/check_credentials","description":"Check user credentials","extension":"CoreServices"}]}
JSON;
}
private function GetExpectedCronResponse(): string
{
return '{"message":"OK"}';
}
private function CheckLogFileIsGeneratedAndGetFullPath(string $sLogFileName): string
{
$sLogFile = APPROOT."log/$sLogFileName";
$this->assertTrue(is_file($sLogFile));
$this->aFileToClean[] = $sLogFile;
return $sLogFile;
}
private function CheckAdminAccessIssueWithCron(string $sLogFile)
{
$aLines = Utils::ReadTail($sLogFile);
$sLastLine = array_shift($aLines);
$this->assertMatchesRegularExpression('/^Access restricted to administrators/', $sLastLine, "@$sLastLine@");
}
}

View File

@@ -457,12 +457,12 @@ if ($bIsModeCLI) {
}
try {
utils::UseParamFile();
$bVerbose = utils::ReadParam('verbose', false, true /* Allow CLI */);
$bDebug = utils::ReadParam('debug', false, true /* Allow CLI */);
if ($bIsModeCLI) {
utils::UseParamFile();
// Next steps:
// specific arguments: 'csv file'
//
@@ -478,17 +478,16 @@ try {
$oP->output();
exit(EXIT_CODE_ERROR);
}
} else
{
} else {
$oLoginFSMExtensionInstance = LoginWebPage::GetCurrentLoginPlugin($sLoginMode);
if ($oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension){
if ($oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension) {
$aTokenInfo = json_decode(base64_decode($sTokenInfo), true);
/** @var iTokenLoginUIExtension $oLoginFSMExtensionInstance */
$sAuthUser = $oLoginFSMExtensionInstance->GetUserLogin($aTokenInfo);
UserRights::Login($sAuthUser); // Login & set the user's language
} else {
$oP->p("cannot call cron asynchronously via current login mode $sLoginMode");
$oP->p("Access wrong credentials via current login mode $sLoginMode");
$oP->output();
exit(EXIT_CODE_ERROR);
}

View File

@@ -11,38 +11,31 @@ const ERROR = "error";
try {
$oCtx = new ContextTag(ContextTag::TAG_CRON);
LoginWebPage::ResetSession(true);
LoginWebPage::ResetSession();
$iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN);
if ($iRet != LoginWebPage::EXIT_CODE_OK){
if ($iRet != LoginWebPage::EXIT_CODE_OK) {
throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED);
}
$sLogFilename = ReadParam("cron_log_file", "cron.log");
$sStatus = STOPPED;
$sStatus = RUNNING;
$sMsg = "";
$sLogFile = APPROOT."log/$sLogFilename";
if (is_file($sLogFile)) {
$sContent = exec("tail -n 1 $sLogFile");
if (0 === strpos($sContent, 'Exiting: ')) {
exec("tail -n 2 $sLogFile", $aContent);
//var_dump($aContent);
$sContent = implode("\n", $aContent);
if (false !== strpos($sContent, 'Already running')) {
$sStatus = ERROR_ALREADY_RUNNING;
} else if (preg_match('/ERROR: (.*)\\n/', $sContent, $aMatches)) {
$sMsg = "$aMatches[1]";
$sStatus = ERROR;
} else {
$sMsg = "$sContent";
$sStatus = STOPPED;
}
$aLines = Utils::ReadTail($sLogFile, 2);
$sLastLine = $aLines[1] ?? '';
if (0 === strpos($sLastLine, 'Exiting: ')) {
$sContent = $aLines[0];
if (false !== strpos($sContent, 'Already running')) {
$sStatus = ERROR_ALREADY_RUNNING;
} elseif (preg_match('/ERROR: (.*)\\n/', $sContent, $aMatches)) {
$sMsg = $aMatches[1];
$sStatus = ERROR;
} else {
$sStatus = RUNNING;
$sMsg = "$sContent";
$sStatus = STOPPED;
}
} else {
$sMsg = "missing $sLogFile";
$sStatus = ERROR;
}
http_response_code(200);
@@ -51,9 +44,8 @@ try {
$oP->SetData(["status" => $sStatus, 'message' => $sMsg]);
$oP->SetOutputDataOnly(true);
$oP->Output();
}
catch (Exception $e) {
\IssueLog::Error("Cannot cron status", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]);
} catch (Exception $e) {
\IssueLog::Error("Cannot get cron status", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]);
http_response_code(500);
$oP = new JsonPage();
$oP->add_header('Access-Control-Allow-Origin: *');
@@ -70,4 +62,4 @@ function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils:
}
return trim($sValue);
}
}

View File

@@ -6,23 +6,48 @@ require_once(__DIR__.'/../approot.inc.php');
require_once(APPROOT.'/application/application.inc.php');
require_once(APPROOT.'/application/startup.inc.php');
function GetCliCommand(string $sPHPExec, string $sLogFile, array $aCronValues): string
{
$sCliParams = implode(" ", $aCronValues);
return sprintf("$sPHPExec %s/cron.php $sCliParams 2>&1 >>$sLogFile &", __DIR__);
}
function IsErrorLine(string $sLine): bool
{
if (preg_match('/^Access wrong credentials/', $sLine)) {
return true;
}
if (preg_match('/^Access restricted/', $sLine)) {
return true;
}
return false;
}
function IsCronStartingLine(string $sLine): bool
{
return preg_match('/^Starting: /', $sLine);
}
try {
$oCtx = new ContextTag(ContextTag::TAG_CRON);
LoginWebPage::ResetSession(true);
LoginWebPage::ResetSession();
$iRet = LoginWebPage::DoLogin(false, false, LoginWebPage::EXIT_RETURN);
if ($iRet != LoginWebPage::EXIT_CODE_OK){
if ($iRet != LoginWebPage::EXIT_CODE_OK) {
throw new Exception("Unknown authentication error (retCode=$iRet)", RestResult::UNAUTHORIZED);
}
$sCurrentLoginMode = \Combodo\iTop\Application\Helper\Session::Get('login_mode', '');
$oLoginFSMExtensionInstance = LoginWebPage::GetCurrentLoginPlugin($sCurrentLoginMode);
if (! $oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension){
if (! $oLoginFSMExtensionInstance instanceof iTokenLoginUIExtension) {
throw new \Exception("cannot call cron asynchronously via current login mode $sCurrentLoginMode");
}
$aCronValues = [];
foreach ([ 'status_only', 'verbose', 'debug'] as $sParam){
foreach ([ 'verbose', 'debug'] as $sParam) {
$value = ReadParam($sParam, false);
$aCronValues[] = "--$sParam=".escapeshellarg($value);
}
@@ -30,41 +55,34 @@ try {
/** @var iTokenLoginUIExtension $oLoginFSMExtensionInstance */
$aTokenInfo = $oLoginFSMExtensionInstance->GetTokenInfo();
$sTokenInfo = base64_encode(json_encode($aTokenInfo));
$aCronValues[] = "--auth_info=".escapeshellarg($sTokenInfo);
$aCronValues[] = "--login_mode=".escapeshellarg($sCurrentLoginMode);
$sCliParams=implode(" ", $aCronValues);
$sLogFilename = ReadParam("cron_log_file", "cron.log");
$sLogFile = APPROOT."log/$sLogFilename";
touch($sLogFile);
if (! touch($sLogFile)) {
throw new \Exception("Cannot touch $sLogFile");
}
$sPHPExec = trim(\MetaModel::GetConfig()->Get('php_path'));
$sCliForLogs = GetCliCommand($sPHPExec, $sLogFile, $aCronValues).PHP_EOL;
file_put_contents("$sLogFile", $sCliForLogs);
if (! is_file($sLogFile)) {
throw new \Exception("Cannot write in $sLogFile");
}
if ($aCronValues['status_only']) {
//still synchronous
$sCli = sprintf("$sPHPExec %s/cron.php $sCliParams 2>&1 >>$sLogFile &", __DIR__);
file_put_contents($sLogFile, $sCli);
$process = popen($sCli, 'r');
} else {
//asynchronous
$sCli = sprintf("\n $sPHPExec %s/cron.php $sCliParams", __DIR__);
$fp = fopen($sLogFile, 'a+');
fwrite($fp, $sCli);
$aCronValues[] = "--auth_info=".escapeshellarg($sTokenInfo);
$sCli = GetCliCommand($sPHPExec, $sLogFile, $aCronValues);
$process = popen($sCli, 'r');
$aDescriptorSpec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
];
$rProcess = proc_open($sCli, $aDescriptorSpec, $aPipes, __DIR__, null);
$sStdOut = stream_get_contents($aPipes[1]);
fclose($aPipes[1]);
$iCode = proc_close($rProcess);
fwrite($fp, $sStdOut);
fwrite($fp, "Exiting: ".time().' ('.date('Y-m-d H:i:s').')');
fclose($fp);
$i = 0;
while ($aLines = Utils::ReadTail($sLogFile)) {
$sLastLine = array_shift($aLines);
if (IsErrorLine($sLastLine) || IsCronStartingLine($sLastLine)) {
//return answer once we are sure cron is starting or did not pass authentication
break;
}
usleep(100);
}
http_response_code(200);
@@ -73,8 +91,7 @@ try {
$oP->SetData(["message" => "OK"]);
$oP->SetOutputDataOnly(true);
$oP->Output();
}
catch (Exception $e) {
} catch (Exception $e) {
\IssueLog::Error("Cannot run cron", null, ['msg' => $e->getMessage(), 'stack' => $e->getTraceAsString()]);
http_response_code(500);
$oP = new JsonPage();
@@ -92,4 +109,4 @@ function ReadParam($sParam, $sDefaultValue = null, $sSanitizationFilter = utils:
}
return trim($sValue);
}
}