diff --git a/actions/admin/settings/120.system.php b/actions/admin/settings/120.system.php index 5008e700..7412f569 100644 --- a/actions/admin/settings/120.system.php +++ b/actions/admin/settings/120.system.php @@ -160,17 +160,7 @@ return array( 'default' => 90, 'save_method' => 'storeSettingField', ), - 'system_debug_cron' => array( - 'label' => $lng['serversettings']['cron']['debug'], - 'settinggroup' => 'system', - 'varname' => 'debug_cron', - 'type' => 'bool', - 'default' => false, - 'save_method' => 'storeSettingField', - ), ), ), ), ); - -?> diff --git a/actions/admin/settings/125.cronjob.php b/actions/admin/settings/125.cronjob.php new file mode 100644 index 00000000..42481669 --- /dev/null +++ b/actions/admin/settings/125.cronjob.php @@ -0,0 +1,43 @@ + (2010-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package Settings + * + */ + +return array( + 'groups' => array( + 'crond' => array( + 'title' => $lng['admin']['cronsettings'], + 'fields' => array( + 'system_cronconfig' => array( + 'label' => $lng['serversettings']['system_cronconfig'], + 'settinggroup' => 'system', + 'varname' => 'cronconfig', + 'type' => 'string', + 'string_type' => 'file', + 'default' => '/etc/cron.d/froxlor', + 'save_method' => 'storeSettingField', + ), + 'system_debug_cron' => array( + 'label' => $lng['serversettings']['cron']['debug'], + 'settinggroup' => 'system', + 'varname' => 'debug_cron', + 'type' => 'bool', + 'default' => false, + 'save_method' => 'storeSettingField', + ) + ) + ) + ) +); diff --git a/admin_cronjobs.php b/admin_cronjobs.php index d47c0778..8cd17276 100644 --- a/admin_cronjobs.php +++ b/admin_cronjobs.php @@ -99,15 +99,17 @@ if ($page == 'cronjobs' || $page == 'overview') { ); Database::pexecute($upd, array('isactive' => $isactive, 'int' => $interval, 'id' => $id)); - redirectTo($filename, Array('page' => $page, 's' => $s)); + // insert task to re-generate the cron.d-file + inserttask('99'); + + redirectTo($filename, array('page' => $page, 's' => $s)); } else { - //$isactive = makeyesno('isactive', '1', '0', $result['isactive']); + // interval $interval_nfo = explode(' ', $result['interval']); $interval_value = $interval_nfo[0]; $interval_interval = ''; - $interval_interval .= makeoption($lng['cronmgmt']['seconds'], 'SECOND', $interval_nfo[1]); $interval_interval .= makeoption($lng['cronmgmt']['minutes'], 'MINUTE', $interval_nfo[1]); $interval_interval .= makeoption($lng['cronmgmt']['hours'], 'HOUR', $interval_nfo[1]); $interval_interval .= makeoption($lng['cronmgmt']['days'], 'DAY', $interval_nfo[1]); diff --git a/install/froxlor.sql b/install/froxlor.sql index 643e3f82..7989286b 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -499,6 +499,7 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES ('system', 'mdaserver', 'dovecot'), ('system', 'mtaserver', 'postfix'), ('system', 'mailtraffic_enabled', '1'), + ('system', 'cronconfig', '/etc/cron.d/froxlor'), ('panel', 'decimal_places', '4'), ('panel', 'adminmail', 'admin@SERVERNAME'), ('panel', 'phpmyadmin_url', ''), @@ -526,7 +527,7 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES ('panel', 'phpconfigs_hidestdsubdomain', '0'), ('panel', 'allow_theme_change_admin', '1'), ('panel', 'allow_theme_change_customer', '1'), - ('panel', 'version', '0.9.32-dev4'); + ('panel', 'version', '0.9.32-dev5'); DROP TABLE IF EXISTS `panel_tasks`; @@ -737,12 +738,12 @@ CREATE TABLE IF NOT EXISTS `cronjobs_run` ( INSERT INTO `cronjobs_run` (`id`, `module`, `cronfile`, `interval`, `isactive`, `desc_lng_key`) VALUES - (1, 'froxlor/core', 'cron_tasks.php', '5 MINUTE', '1', 'cron_tasks'), - (2, 'froxlor/core', 'cron_traffic.php', '1 DAY', '1', 'cron_traffic'), - (3, 'froxlor/ticket', 'cron_used_tickets_reset.php', '1 DAY', '1', 'cron_ticketsreset'), - (4, 'froxlor/ticket', 'cron_ticketarchive.php', '1 MONTH', '1', 'cron_ticketarchive'), - (5, 'froxlor/reports', 'cron_usage_report.php', '1 DAY', '1', 'cron_usage_report'), - (6, 'froxlor/core', 'cron_mailboxsize.php', '6 HOUR', '1', 'cron_mailboxsize'); + (1, 'froxlor/core', 'tasks', '5 MINUTE', '1', 'cron_tasks'), + (2, 'froxlor/core', 'traffic', '1 DAY', '1', 'cron_traffic'), + (3, 'froxlor/ticket', 'used_tickets_reset', '1 DAY', '1', 'cron_ticketsreset'), + (4, 'froxlor/ticket', 'ticketarchive', '1 MONTH', '1', 'cron_ticketarchive'), + (5, 'froxlor/reports', 'usage_report', '1 DAY', '1', 'cron_usage_report'), + (6, 'froxlor/core', 'mailboxsize', '6 HOUR', '1', 'cron_mailboxsize'); diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index f9b30fb4..8c3e921d 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -2430,9 +2430,9 @@ if (isFroxlorVersion('0.9.31-dev1')) { INSERT INTO `".TABLE_PANEL_SETTINGS."` SET `settinggroup` = 'phpfpm', `varname` = 'fastcgi_ipcdir', `value` = :value "); $params = array(); - if (Settings::Get('system.webserver') == 'apache2') { - $params['value'] = '/var/lib/apache2/fastcgi/'; - } elseif (Settings::Get('system.webserver') == 'lighttpd') { + // set default for apache (which will suite in most cases) + $params['value'] = '/var/lib/apache2/fastcgi/'; + if (Settings::Get('system.webserver') == 'lighttpd') { $params['value'] = '/var/run/lighttpd/'; } elseif (Settings::Get('system.webserver') == 'nginx') { $params['value'] = '/var/run/nginx/'; @@ -2680,3 +2680,24 @@ if (isFroxlorVersion('0.9.32-dev3')) { updateToVersion('0.9.32-dev4'); } + +if (isFroxlorVersion('0.9.32-dev4')) { + + showUpdateStep("Updating from 0.9.32-dev4 to 0.9.32-dev5"); + lastStepStatus(0); + + showUpdateStep("Updating cronjob table"); + Database::query("UPDATE `".TABLE_PANEL_CRONRUNS."` SET `cronfile` = REPLACE( REPLACE(`cronfile`, 'cron_', ''), '.php', '')"); + lastStepStatus(0); + + showUpdateStep("Adding new settings for cron"); + // get user-chosen value + $crondfile = isset($_POST['crondfile']) ? $_POST['crondfile'] : "/etc/cron.d/froxlor"; + $crondfile = makeCorrectFile($crondfile); + Settings::AddNew("system.cronconfig", $crondfile); + // add task to generate cron.d-file + inserttask('99'); + lastStepStatus(0); + + updateToVersion('0.9.32-dev5'); +} diff --git a/install/updates/preconfig/0.9/preconfig_0.9.inc.php b/install/updates/preconfig/0.9/preconfig_0.9.inc.php index df0df94b..3573929d 100644 --- a/install/updates/preconfig/0.9/preconfig_0.9.inc.php +++ b/install/updates/preconfig/0.9/preconfig_0.9.inc.php @@ -634,4 +634,11 @@ function parseAndOutputPreconfig(&$has_preconfig, &$return, $current_version) { eval("\$return.=\"" . getTemplate("update/preconfigitem") . "\";"); } + if (versionInUpdate($current_version, '0.9.32-dev5')) { + $has_preconfig = true; + $description = 'Froxlor now generates a cron-configuration file for the cron-daemon. Please set a filename which will be included automatically by your crond (e.g. files in /etc/cron.d/)

'; + $question = 'Path to the cron-service configuration-file. This file will be updated regularly and automatically by froxlor.
'; + $question.= '
'; + eval("\$return.=\"" . getTemplate("update/preconfigitem") . "\";"); + } } diff --git a/lib/classes/settings/class.Settings.php b/lib/classes/settings/class.Settings.php index 7461d0ca..72725f32 100644 --- a/lib/classes/settings/class.Settings.php +++ b/lib/classes/settings/class.Settings.php @@ -179,7 +179,7 @@ class Settings { `value` = :value "); $ins_data = array( - 'settinggroup' => $sstr[0], + 'group' => $sstr[0], 'varname' => $sstr[1], 'value' => $value ); diff --git a/lib/cron_init.php b/lib/cron_init.php index ab0f3945..2afc0ecd 100644 --- a/lib/cron_init.php +++ b/lib/cron_init.php @@ -183,3 +183,6 @@ $idna_convert = new idna_convert_wrapper(); // Initialize logging $cronlog = FroxlorLogger::getInstanceOf(array('loginname' => 'cronjob')); fwrite($debugHandler, 'Logger has been included' . "\n"); + +// check for cron.d-generation task and create it if necessary +checkCrondConfigurationFile(); diff --git a/lib/functions/froxlor/function.CronjobFunctions.php b/lib/functions/froxlor/function.CronjobFunctions.php index 3e2d6ac9..ae67c283 100644 --- a/lib/functions/froxlor/function.CronjobFunctions.php +++ b/lib/functions/froxlor/function.CronjobFunctions.php @@ -15,98 +15,6 @@ * */ -/** - * Function getNextCronjobs - * - * checks which cronjobs have to be executed - * - * @return array array of cron-files which are to be executed - */ -function getNextCronjobs() { - - $query = "SELECT `id`, `cronfile` FROM `".TABLE_PANEL_CRONRUNS."` WHERE `interval` <> '0' AND `isactive` = '1' AND ("; - - $intervals = getIntervalOptions(); - $x = 0; - - foreach($intervals as $name => $ival) { - - if($name == '0') continue; - - if($x == 0) { - $query.= "(UNIX_TIMESTAMP(DATE_ADD(FROM_UNIXTIME(`lastrun`), INTERVAL ".$ival.")) <= UNIX_TIMESTAMP() AND `interval` = '".$ival."')"; - } else { - $query.= " OR (UNIX_TIMESTAMP(DATE_ADD(FROM_UNIXTIME(`lastrun`), INTERVAL ".$ival.")) <= UNIX_TIMESTAMP() AND `interval` = '".$ival."')"; - } - $x++; - } - $query.= ');'; - - $result = Database::query($query); - - $cron_files = array(); - // Update lastrun-timestamp - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $cron_files[] = $row['cronfile']; - $upd_stmt = Database::prepare(" - UPDATE `".TABLE_PANEL_CRONRUNS."` SET `lastrun` = UNIX_TIMESTAMP() WHERE `id` = :id;" - ); - Database::pexecute($upd_stmt, array('id' => $row['id'])); - } - - return $cron_files; -} - -function includeCronjobs($debugHandler) { - - global $cronlog; - - $cronjobs = getNextCronjobs(); - - $jobs_to_run = array(); - $cron_path = makeCorrectDir(FROXLOR_INSTALL_DIR.'/scripts/jobs/'); - - if ($cronjobs !== false - && is_array($cronjobs) - && isset($cronjobs[0]) - ) { - foreach ($cronjobs as $cronjob) { - $cron_file = makeCorrectFile($cron_path.$cronjob); - if (!file_exists($cron_file)) { - $cronlog->logAction(CRON_ACTION, LOG_ERROR, 'Wanted to include cronfile "'.$cron_file.'" but this file does not exist!!!'); - } else { - $jobs_to_run[] = $cron_file; - } - } - } - - return $jobs_to_run; -} - - -function getIntervalOptions() { - - global $lng, $cronlog; - - $query = "SELECT DISTINCT `interval` FROM `" . TABLE_PANEL_CRONRUNS . "` ORDER BY `interval` ASC;"; - $result = Database::query($query); - - $cron_intervals = array(); - $cron_intervals['0'] = $lng['panel']['off']; - - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - - if (validateSqlInterval($row['interval'])) { - $cron_intervals[$row['interval']] = $row['interval']; - } else { - $cronlog->logAction(CRON_ACTION, LOG_ERROR, "Invalid SQL-Interval ".$row['interval']." detected. Please fix this in the database."); - } - } - - return $cron_intervals; -} - - function getCronjobsLastRun() { global $lng; diff --git a/lib/functions/froxlor/function.checkCrondConfigurationFile.php b/lib/functions/froxlor/function.checkCrondConfigurationFile.php new file mode 100644 index 00000000..deacc4f1 --- /dev/null +++ b/lib/functions/froxlor/function.checkCrondConfigurationFile.php @@ -0,0 +1,81 @@ + (2010-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package Functions + * + */ + +/** + * 1st: check for task of generation + * 2nd: if task found, generate cron.d-file + * 3rd: maybe restart cron? + */ +function checkCrondConfigurationFile() { + + // check for task + $result_tasks_stmt = Database::query(" + SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99' + "); + $num_results = Database::num_rows(); + + // is there a task for re-generating the cron.d-file? + if ($num_results > 0) { + + // get all crons and their intervals + $cronfile = "# automatically generated cron-configuration by froxlor\ņ"; + $cronfile.= "# do not manually edit this file as it will be re-generated periodically.\ņ"; + $cronfile.= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n#\n"; + + // get all the crons + $result_stmt = Database::query(" + SELECT * FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `isactive` = '1' + "); + + while ($row_cronentry = $result_stmt->fetch(PDO::FETCH_ASSOC)) { + // create cron.d-entry + if (preg_match("/(\d+) (MINUTE|HOUR|DAY|WEEK|MONTH)/", $row_cronentry['interval'], $matches)) { + switch($matches[2]) { + case "MINUTE": + $cronfile .= "*/" . $matches[1] . " * * * * "; + break; + case "HOUR": + $cronfile .= "* */" . $matches[1] . " * * * "; + break; + case "DAY": + $cronfile .= "* * */" . $matches[1] . " * * "; + break; + case "WEEK": + $cronfile .= "* * * */" . $matches[1] . " * "; + break; + case "MONTH": + $cronfile .= "* * * * */" . $matches[1] . " "; + break; + } + + // create entry-line + $binpath = "/usr/bin/nice -n 5 /usr/bin/php5 -q"; + $cronfile .= "root " . $binpath." " . FROXLOR_INSTALL_DIR . "/scripts/froxlor_master_cronjob.php --" . $row_cronentry['cronfile'] . " 1 > /dev/null\n"; + } + } + + // write the file + if (file_put_contents(Settings::Get("system.cronconfig"), $cronfile) === false) { + // oh snap cannot create new crond-file + die("Oh snap, we cannot create the cron.d file. This should not happen.\nPlease check the path and permissions, the cron will keep trying if you don't stop the cron-service.\n\n"); + } + + // remove all re-generation tasks + Database::query("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99'"); + } + return true; +} diff --git a/lib/tables.inc.php b/lib/tables.inc.php index 29ab2b9b..b2ca35d9 100644 --- a/lib/tables.inc.php +++ b/lib/tables.inc.php @@ -51,6 +51,6 @@ define('TABLE_PANEL_DOMAIN_SSL_SETTINGS', 'domain_ssl_settings'); define('TABLE_DOMAINTOIP', 'panel_domaintoip'); // VERSION INFO -$version = '0.9.32-dev4'; +$version = '0.9.32-dev5'; $dbversion = '2'; $branding = ''; diff --git a/lng/english.lng.php b/lng/english.lng.php index 40eaa6bb..058aea6a 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1119,7 +1119,6 @@ $lng['crondesc']['cron_legacy'] = 'legacy (old) cronjob'; $lng['crondesc']['cron_traffic'] = 'traffic calculation'; $lng['crondesc']['cron_ticketsreset'] = 'resetting ticket-counters'; $lng['crondesc']['cron_ticketarchive'] = 'archiving old tickets'; -$lng['cronmgmt']['seconds'] = 'seconds'; $lng['cronmgmt']['minutes'] = 'minutes'; $lng['cronmgmt']['hours'] = 'hours'; $lng['cronmgmt']['days'] = 'days'; @@ -1798,3 +1797,6 @@ $lng['serversettings']['mtaserver']['description'] = "Type of the Mail Transfer $lng['serversettings']['mtalog']['title'] = "MTA log"; $lng['serversettings']['mtalog']['description'] = "Logfile of the Mail Transfer Agent"; $lng['panel']['ftpdesc'] = 'FTP description'; +$lng['admin']['cronsettings'] = 'Cronjob settings'; +$lng['serversettings']['system_cronconfig']['title'] = 'Cron configuration file'; +$lng['serversettings']['system_cronconfig']['description'] = 'Path to the cron-service configuration-file. This file will be updated regularly and automatically by froxlor.'; diff --git a/lng/german.lng.php b/lng/german.lng.php index b85aff85..c11c73f1 100644 --- a/lng/german.lng.php +++ b/lng/german.lng.php @@ -1093,7 +1093,6 @@ $lng['crondesc']['cron_legacy'] = 'Legacy (alter) Cronjob'; $lng['crondesc']['cron_traffic'] = 'Traffic-Berechnung'; $lng['crondesc']['cron_ticketsreset'] = 'Zurücksetzen der Ticket-Zähler'; $lng['crondesc']['cron_ticketarchive'] = 'Archivieren alter Tickets'; -$lng['cronmgmt']['seconds'] = 'Sekunden'; $lng['cronmgmt']['minutes'] = 'Minuten'; $lng['cronmgmt']['hours'] = 'Stunden'; $lng['cronmgmt']['days'] = 'Tage'; @@ -1506,7 +1505,7 @@ $lng['admin']['templates']['SERVER_PORT'] = 'Wird mit dem standard Port ersetzt' $lng['admin']['templates']['DOMAINNAME'] = 'Wird mit der Standardsubdomain des Kunden ersetzt (kann leer sein, wenn keine erstellt werden soll)'; $lng['admin']['show_news_feed'] = 'Zeige News-Feed im Admin-Dashboard'; -// Added in Froxlor 0.9.32 +// Added in Froxlfor 0.9.32 $lng['logger']['reseller'] = "Reseller"; $lng['logger']['admin'] = "Administrator"; $lng['logger']['cron'] = "Cronjob"; @@ -1524,3 +1523,6 @@ $lng['serversettings']['mtaserver']['description'] = "Der eingesetzte Mail Trans $lng['serversettings']['mtalog']['title'] = "Logdatei des MTA"; $lng['serversettings']['mtalog']['description'] = "Die Logdatei des Mail Transfer Agent"; $lng['panel']['ftpdesc'] = 'FTP Beschreibung'; +$lng['admin']['cronsettings'] = 'Cronjob Einstellungen'; +$lng['serversettings']['system_cronconfig']['title'] = 'Cron-Konfigurationsdatei'; +$lng['serversettings']['system_cronconfig']['description'] = 'Pfad zur Konfigurationsdatei des Cron-Dienstes. Diese Datei wird von froxlor automatisch aktualisiert.'; diff --git a/scripts/froxlor_master_cronjob.php b/scripts/froxlor_master_cronjob.php index 71b7a297..89512085 100644 --- a/scripts/froxlor_master_cronjob.php +++ b/scripts/froxlor_master_cronjob.php @@ -19,7 +19,8 @@ define('MASTER_CRONJOB', 1); include_once dirname(dirname(__FILE__)) . '/lib/cron_init.php'; -$jobs_to_run = includeCronjobs($debugHandler); +$jobs_to_run = array(); +$lastrun_update = array(); /** * check for --help @@ -27,41 +28,51 @@ $jobs_to_run = includeCronjobs($debugHandler); if (isset($argv[1]) && strtolower($argv[1]) == '--help') { echo "\n*** Froxlor Master Cronjob ***\n\n"; echo "Below are possible parameters for this file\n\n"; - echo "--force\t\t\tforces re-generating of config-files (webserver, etc.)\n"; - echo "--force-[cronname]\tforces the given cron to run, e.g. --force-mailboxsize, --force-traffic\n\n"; + echo "--[cronname]\t\t\tincludes the given cron-file\n"; + echo "--force\t\t\tforces re-generating of config-files (webserver, nameserver, etc.)\n\n"; } /** - * check for --force to include cron_tasks - * even if it's not its turn + * check for parameters + * + * --[cronname] include [cronname] + * --force to include cron_tasks even if it's not its turn */ for ($x = 1; $x < count($argv); $x++) { - if (isset($argv[$x]) && strtolower($argv[$x]) == '--force') { - $crontasks = makeCorrectFile(FROXLOR_INSTALL_DIR.'/scripts/jobs/cron_tasks.php'); - // really force re-generating of config-files by - // inserting task 1 - inserttask('1'); - if (!in_array($crontasks, $jobs_to_run)) { - array_unshift($jobs_to_run, $crontasks); + // check argument + if (isset($argv[$x])) { + // --force + if (strtolower($argv[$x]) == '--force') { + $crontasks = makeCorrectFile(FROXLOR_INSTALL_DIR.'/scripts/jobs/cron_tasks.php'); + // really force re-generating of config-files by + // inserting task 1 + inserttask('1'); + addToQueue($jobs_to_run, $crontasks); + $lastrun_update['tasks'] = crontasks; } - } - elseif (isset($argv[$x]) && substr(strtolower($argv[$x]), 0, 8) == '--force-') { - $crontasks = makeCorrectFile(FROXLOR_INSTALL_DIR.'/scripts/jobs/cron_'.substr(strtolower($argv[$x]), 8).'.php'); - if (file_exists($crontasks)) { - if (!in_array($crontasks, $jobs_to_run)) { - array_unshift($jobs_to_run, $crontasks); + // --[cronname] + elseif (substr(strtolower($argv[$x]), 0, 2) == '--') { + if (strlen($argv[$x]) > 3) { + $cronfile = makeCorrectFile(FROXLOR_INSTALL_DIR.'/scripts/jobs/cron_'.substr(strtolower($argv[$x]), 3).'.php'); + addToQueue($jobs_to_run, $cronfile); + $lastrun_update[substr(strtolower($argv[$x]), 3)] = cronfile; } } } } -foreach ($jobs_to_run as $cron) { - require_once $cron; +// do we have anything to include? +if (count($jobs_to_run) > 0) { + // include all jobs we want to execute + foreach ($jobs_to_run as $cron) { + updateLastRunOfCron($lastrun_update, $cron); + require_once $cron; + } } fwrite($debugHandler, 'Cronfiles have been included' . "\n"); -/* +/** * we have to check the system's last guid with every cron run * in case the admin installed new software which added a new user * so users in the database don't conflict with system users @@ -71,3 +82,23 @@ checkLastGuid(); // shutdown cron include_once FROXLOR_INSTALL_DIR . '/lib/cron_shutdown.php'; + +// -- helper function +function addToQueue(&$jobs_to_run, $cronfile = null, $checkExists = true) { + if ($checkExists == false || ($checkExists && file_exists($crontasks))) { + if (!in_array($cronfile, $jobs_to_run)) { + array_unshift($jobs_to_run, $cronfile); + } + } +} + +function updateLastRunOfCron($update_arr, $cronfile) { + foreach ($update_arr as $cron => $cronf) { + if ($cronf == $cronfile) { + $upd_stmt = Database::prepare(" + UPDATE `".TABLE_PANEL_CRONRUNS."` SET `lastrun` = UNIX_TIMESTAMP() WHERE `cronfile` = :cron; + "); + Database::pexecute($upd_stmt, array('cron' => $cron)); + } + } +}