diff --git a/composer.json b/composer.json index f7dd56b9cf6cf577d393897bbbf5d0697d533ca5..3ab95dc2ff9c9b6f80e8984f67e3b6ea39052d75 100644 --- a/composer.json +++ b/composer.json @@ -165,6 +165,7 @@ "drupal/token": "1.9", "drupal/twig_tweak": "2.9", "drupal/twitter_block": "3.0-alpha1", + "drupal/ultimate_cron": "^2.0@alpha", "drupal/userprotect": "1.1", "drupal/video_embed_field": "2.4", "drupal/view_unpublished": "1.0", diff --git a/composer.lock b/composer.lock index cdcf77dde73b120bdd4fff287c28246358ff89dd..070408ff58948fd79d5bb53415ec098bc5c37f4b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "39b546aa80d3dd2636b7dd5693e7c94e", + "content-hash": "dee17df1727a8fa0b885fa33c86cafba", "packages": [ { "name": "alchemy/zippy", @@ -7380,6 +7380,75 @@ "source": "https://git.drupalcode.org/project/twitter_block" } }, + { + "name": "drupal/ultimate_cron", + "version": "2.0.0-alpha5", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/ultimate_cron.git", + "reference": "8.x-2.0-alpha5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/ultimate_cron-8.x-2.0-alpha5.zip", + "reference": "8.x-2.0-alpha5", + "shasum": "0f10464fff29eca89024e7afa5b6d8d07bd52f75" + }, + "require": { + "drupal/core": "^8.7.7 || ^9" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-2.0-alpha5", + "datestamp": "1600928948", + "security-coverage": { + "status": "not-covered", + "message": "Alpha releases are not covered by Drupal security advisories." + } + }, + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "Dane Powell", + "homepage": "https://www.drupal.org/user/339326" + }, + { + "name": "Primsi", + "homepage": "https://www.drupal.org/user/282629" + }, + { + "name": "arnested", + "homepage": "https://www.drupal.org/user/245635" + }, + { + "name": "gielfeldt", + "homepage": "https://www.drupal.org/user/366993" + }, + { + "name": "miro_dietiker", + "homepage": "https://www.drupal.org/user/227761" + } + ], + "description": "Ultimate cron", + "homepage": "https://www.drupal.org/project/ultimate_cron", + "support": { + "source": "https://git.drupalcode.org/project/ultimate_cron" + } + }, { "name": "drupal/userprotect", "version": "1.1.0", @@ -15512,7 +15581,8 @@ "minimum-stability": "dev", "stability-flags": { "drupal/cache_control_override": 15, - "drupal/multiple_fields_remove_button": 15 + "drupal/multiple_fields_remove_button": 15, + "drupal/ultimate_cron": 15 }, "prefer-stable": true, "prefer-lowest": false, diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index deb1af93f8513c0b1e5e175d29465c3859968a77..d63e3b3beab3ee7f9d5ee62c98b3c1992618afec 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -7682,6 +7682,78 @@ }, "install-path": "../../web/modules/twitter_block" }, + { + "name": "drupal/ultimate_cron", + "version": "2.0.0-alpha5", + "version_normalized": "2.0.0.0-alpha5", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/ultimate_cron.git", + "reference": "8.x-2.0-alpha5" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/ultimate_cron-8.x-2.0-alpha5.zip", + "reference": "8.x-2.0-alpha5", + "shasum": "0f10464fff29eca89024e7afa5b6d8d07bd52f75" + }, + "require": { + "drupal/core": "^8.7.7 || ^9" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-2.0-alpha5", + "datestamp": "1600928948", + "security-coverage": { + "status": "not-covered", + "message": "Alpha releases are not covered by Drupal security advisories." + } + }, + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "Dane Powell", + "homepage": "https://www.drupal.org/user/339326" + }, + { + "name": "Primsi", + "homepage": "https://www.drupal.org/user/282629" + }, + { + "name": "arnested", + "homepage": "https://www.drupal.org/user/245635" + }, + { + "name": "gielfeldt", + "homepage": "https://www.drupal.org/user/366993" + }, + { + "name": "miro_dietiker", + "homepage": "https://www.drupal.org/user/227761" + } + ], + "description": "Ultimate cron", + "homepage": "https://www.drupal.org/project/ultimate_cron", + "support": { + "source": "https://git.drupalcode.org/project/ultimate_cron" + }, + "install-path": "../../web/modules/ultimate_cron" + }, { "name": "drupal/userprotect", "version": "1.1.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 9677e42e695059a08db80e246180bd1385a3928e..00ac08d532572f3fa9f2117af81c6af305763b4f 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'd2ba02375d2963b4d365129930f11ab04573ee3e', + 'reference' => '2bc2e7748e927bf884a27dc8ab9beca9910c1aad', 'name' => 'osu-asc-webservices/d8-upstream', 'dev' => true, ), @@ -1723,6 +1723,15 @@ 'reference' => '8.x-3.0-alpha1', 'dev_requirement' => false, ), + 'drupal/ultimate_cron' => array( + 'pretty_version' => '2.0.0-alpha5', + 'version' => '2.0.0.0-alpha5', + 'type' => 'drupal-module', + 'install_path' => __DIR__ . '/../../web/modules/ultimate_cron', + 'aliases' => array(), + 'reference' => '8.x-2.0-alpha5', + 'dev_requirement' => false, + ), 'drupal/update' => array( 'dev_requirement' => false, 'replaced' => array( @@ -2095,7 +2104,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'd2ba02375d2963b4d365129930f11ab04573ee3e', + 'reference' => '2bc2e7748e927bf884a27dc8ab9beca9910c1aad', 'dev_requirement' => false, ), 'pantheon-systems/quicksilver-pushback' => array( diff --git a/web/modules/ultimate_cron/.travis.yml b/web/modules/ultimate_cron/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..4281883026ce83baddfe6bbd0f26d4c2be278fb7 --- /dev/null +++ b/web/modules/ultimate_cron/.travis.yml @@ -0,0 +1,117 @@ +# @file +# .travis.yml - Drupal for Travis CI Integration +# +# Template provided by https://github.com/LionsAd/drupal_ti. +# +# Based for simpletest upon: +# https://github.com/sonnym/travis-ci-drupal-module-example + +language: php + +sudo: false + +php: + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + fast_finish: true + allow_failures: + - php: 7 + - php: hhvm + +env: + global: + # add composer's global bin directory to the path + # see: https://github.com/drush-ops/drush#install---composer + - PATH="$PATH:$HOME/.composer/vendor/bin" + + # Configuration variables. + - DRUPAL_TI_MODULE_NAME="ultimate_cron" + - DRUPAL_TI_SIMPLETEST_GROUP="ultimate_cron" + + # Define runners and environment vars to include before and after the + # main runners / environment vars. + #- DRUPAL_TI_SCRIPT_DIR_BEFORE="./.drupal_ti/before" + #- DRUPAL_TI_SCRIPT_DIR_AFTER="./drupal_ti/after" + + # The environment to use, supported are: drupal-7, drupal-8 + - DRUPAL_TI_ENVIRONMENT="drupal-8" + + # Drupal specific variables. + - DRUPAL_TI_DB="drupal_travis_db" + - DRUPAL_TI_DB_URL="mysql://root:@127.0.0.1/drupal_travis_db" + # Note: Do not add a trailing slash here. + - DRUPAL_TI_WEBSERVER_URL="http://127.0.0.1" + - DRUPAL_TI_WEBSERVER_PORT="8080" + + # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. + - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 4 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT" + + # === Behat specific variables. + # This is relative to $TRAVIS_BUILD_DIR + - DRUPAL_TI_BEHAT_DIR="./tests/behat" + # These arguments are passed to the bin/behat command. + - DRUPAL_TI_BEHAT_ARGS="" + # Specify the filename of the behat.yml with the $DRUPAL_TI_DRUPAL_DIR variables. + - DRUPAL_TI_BEHAT_YML="behat.yml.dist" + # This is used to setup Xvfb. + - DRUPAL_TI_BEHAT_SCREENSIZE_COLOR="1280x1024x16" + # The version of seleniumthat should be used. + - DRUPAL_TI_BEHAT_SELENIUM_VERSION="2.44" + # Set DRUPAL_TI_BEHAT_DRIVER to "selenium" to use "firefox" or "chrome" here. + - DRUPAL_TI_BEHAT_DRIVER="phantomjs" + - DRUPAL_TI_BEHAT_BROWSER="firefox" + + # PHPUnit specific commandline arguments. + - DRUPAL_TI_PHPUNIT_ARGS="--verbose --debug" + # Specifying the phpunit-core src/ directory is useful when e.g. a vendor/ + # directory is present in the module directory, which phpunit would then + # try to find tests in. This option is relative to $TRAVIS_BUILD_DIR. + #- DRUPAL_TI_PHPUNIT_CORE_SRC_DIRECTORY="./tests/src" + + # Code coverage via coveralls.io + - DRUPAL_TI_COVERAGE="satooshi/php-coveralls:0.6.*" + # This needs to match your .coveralls.yml file. + - DRUPAL_TI_COVERAGE_FILE="build/logs/clover.xml" + + # Debug options + #- DRUPAL_TI_DEBUG="-x -v" + # Set to "all" to output all files, set to e.g. "xvfb selenium" or "selenium", + # etc. to only output those channels. + #- DRUPAL_TI_DEBUG_FILE_OUTPUT="selenium xvfb webserver" + + matrix: + # [[[ SELECT ANY OR MORE OPTIONS ]]] + #- DRUPAL_TI_RUNNERS="phpunit" + #- DRUPAL_TI_RUNNERS="simpletest" + #- DRUPAL_TI_RUNNERS="behat" + - DRUPAL_TI_RUNNERS="phpunit-core simpletest" + +# This will create the database +mysql: + database: drupal_travis_db + username: root + encoding: utf8 + +# To be able to run a webbrowser +# If we need anything more powerful +# than e.g. phantomjs +before_install: + - composer self-update + - composer global require "lionsad/drupal_ti:1.*" + - drupal-ti before_install + +install: + - drupal-ti install + +before_script: + - drupal-ti before_script + +script: + - drupal-ti script + +after_script: + - drupal-ti after_script diff --git a/web/modules/ultimate_cron/INSTALL.txt b/web/modules/ultimate_cron/INSTALL.txt new file mode 100644 index 0000000000000000000000000000000000000000..bd74c964288c48de9335076f2a2395d80c23b175 --- /dev/null +++ b/web/modules/ultimate_cron/INSTALL.txt @@ -0,0 +1,2 @@ +Set your crontab to wget cron.php once per minute (* * * * *). If this is not possible, activate the Poormans cron function in the Ultimate Cron settings. + diff --git a/web/modules/ultimate_cron/LICENSE.txt b/web/modules/ultimate_cron/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..d159169d1050894d3ea3b98e1c965c4058208fe1 --- /dev/null +++ b/web/modules/ultimate_cron/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/ultimate_cron/README.txt b/web/modules/ultimate_cron/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..a785b5d2af855a0ae6c415ad5a0359885159bc87 --- /dev/null +++ b/web/modules/ultimate_cron/README.txt @@ -0,0 +1,118 @@ +CONTENTS OF THIS FILE +--------------------- + + * Introduction + * Requirements + * Installation + * Configuration + * Maintainers + + +INTRODUCTION +------------ + +The Ultimate Cron module runs cron jobs individually in parallel using +configurable rules, pool management and load balancing. + + * For a full description of the module, visit the project page: + https://www.drupal.org/project/ultimate_cron + + * To submit bug reports and feature suggestions, or to track changes: + https://www.drupal.org/project/issues/ultimate_cron + + +REQUIREMENTS +------------ + +This module requires no modules outside of Drupal core. + + +INSTALLATION +------------ + + * Install the Ultimate Cron module as you would normally install a + contributed Drupal module. Visit + https://www.drupal.org/node/1897420 for further information. + + +CONFIGURATION +------------- + +To add a cron job you can use either hook_cron() or use configuration files with +custom parameters for multiple/additional cron jobs in a module. + +The easiest way to declare a cron job is to use hook_cron() and then configure +the cron job through the UI and export it, then change the cron jobs callback +method. + +Another way to register a cron job is to add a cron configuration object in a +custom module. In your custom module add in the sub directory +my_module/config/optional a yaml file named +ultimate_cron.job.my_custom_cron_job_name.yml + +For an example see the cron configuration of the simplenews module: +http://cgit.drupalcode.org/simplenews/tree/config/optional/ultimate_cron.job.simplenews_cron.yml + +After installing the custom module the configuration will become available. +During development you can use the config_devel module to import configuration. + +The cron configuration yaml file could look like: + +``` +langcode: en +status: true +dependencies: + module: + - user +title: 'Pings users' +id: user_ping +module: my_module +callback: _my_module_user_ping_cron +scheduler: + id: simple + configuration: + rules: + - '*/5@ * * * *' +launcher: + id: serial + configuration: + timeouts: + lock_timeout: 3600 + max_execution_time: 3600 + launcher: + max_threads: 1 +logger: + id: database + configuration: + method: '3' + expire: 1209600 + retain: 1000 +``` + +The following details of the cron job can be specified: + - "title": The title of the cron job. If not provided, the + name of the cron job will be used. + - "module": The module where this job lives. + - "callback": The callback to call when running the job. + Defaults to the job name. + - "scheduler": Default scheduler (plugin type) for this job. + - "launcher": Default launcher (plugin type) for this job. + - "logger": Default logger (plugin type) for this job. + + +MAINTAINERS +----------- + + * Sascha Grossenbacher (Berdir) - https://www.drupal.org/u/berdir + * Arne Jørgensen (arnested) - https://www.drupal.org/u/arnested + * Thomas Gielfeldt (gielfeldt) - https://www.drupal.org/u/gielfeldt + * Lukas Schneider (LKS90) - https://www.drupal.org/u/lks90 + +Supporting organizations: + + * MD Systems - https://www.drupal.org/md-systems + * Reload! - https://www.drupal.org/reload + +Thanks to Mark James for the icons: + + * http://www.famfamfam.com/lab/icons/silk/ diff --git a/web/modules/ultimate_cron/composer.json b/web/modules/ultimate_cron/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..7fc89ffd922116154e4e22e94b56b568f4e173b7 --- /dev/null +++ b/web/modules/ultimate_cron/composer.json @@ -0,0 +1,16 @@ +{ + "name": "drupal/ultimate_cron", + "type": "drupal-module", + "license": "GPL-2.0+", + "description": "Ultimate cron", + "require": { + "drupal/core": "^8.7.7 || ^9" + }, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9 || ^10" + } + } + } +} diff --git a/web/modules/ultimate_cron/config/install/ultimate_cron.settings.yml b/web/modules/ultimate_cron/config/install/ultimate_cron.settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..4b8d42e43fb71bc8a0e389cea9107efe171191d4 --- /dev/null +++ b/web/modules/ultimate_cron/config/install/ultimate_cron.settings.yml @@ -0,0 +1,33 @@ +bypass_transactional_safe_connection: FALSE +queue: + enabled: FALSE + timeouts: + lease_time: 30 + time: 15 + delays: + empty_delay: 0 + item_delay: 0 + throttle: + enabled: TRUE + threads: 4 + threshold: 10 +launcher: + thread: 'any' + max_threads: 1 + lock_timeout: 3600 + max_execution_time: 3600 +logger: + cache: + bin: 'ultimate_cron_logger' + timeout: -1 + database: + method: 3 + expire: 1209600 + retain: 1000 +scheduler: + crontab: + catch_up: 86400 + rules: + - '*/10+@ * * * *' + simple: + rule: '*/15+@ * * * *' diff --git a/web/modules/ultimate_cron/config/schema/ultimate_cron.schema.yml b/web/modules/ultimate_cron/config/schema/ultimate_cron.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..f2d1da2afef7602e04a0442423739ed9a3365253 --- /dev/null +++ b/web/modules/ultimate_cron/config/schema/ultimate_cron.schema.yml @@ -0,0 +1,236 @@ +ultimate_cron.settings: + type: config_object + label: Ultimate cron settings + mapping: + bypass_transactional_safe_connection: + type: boolean + label: 'Bypass transactional connection' + queue: + type: mapping + label: Queue settings + mapping: + enabled: + type: boolean + label: Toggle queing jobs + timeouts: + type: mapping + label: Timeouts + mapping: + lease_time: + type: float + label: Time to claim item + time: + type: float + label: Time to process item + delays: + type: mapping + label: Delays + mapping: + empty_delay: + type: float + lebel: Time to idle when queue is empty + item_delay: + type: float + label: Time to idle when finished with item + throttle: + type: mapping + label: Timeouts + mapping: + enabled: + type: boolean + label: Toggles throttling of queues + threads: + type: integer + label: Number of threads for queues + threshold: + type: integer + label: Number of items needed for a queue + launcher: + type: mapping + label: Launcher + mapping: + thread: + type: string + label: Which thread to use + max_threads: + type: integer + label: Maximum number of threads + lock_timeout: + type: integer + label: Time to lock job + max_execution_time: + type: integer + label: Maximum time for the job to run + logger: + type: mapping + label: Logger + mapping: + cache: + type: mapping + label: Cache + mapping: + bin: + type: string + label: The cache bin to use + timeout: + type: integer + label: Time until cache expires + database: + type: mapping + label: Database + mapping: + method: + type: integer + label: The method used for log retention + expire: + type: integer + label: Time until expiration + retain: + type: integer + label: Maximum number of logs to keep + scheduler: + type: mapping + label: Schedulr + mapping: + crontab: + type: mapping + label: Crontab + mapping: + catch_up: + type: integer + label: Stop between job runs + rules: + type: sequence + label: Array with rules + sequence: + type: string + label: Formatted rule + simple: + type: mapping + label: Simple + mapping: + rule: + type: string + label: Rule for Simple scheduler + +ultimate_cron.job.*: + type: config_entity + label: 'Cron Job' + mapping: + title: + type: label + label: 'Title' + id: + type: string + label: 'Machine-readable name' + weight: + type: integer + label: 'Weight' + module: + type: string + label: 'Module Name' + callback: + type: string + label: 'Callback' + scheduler: + type: mapping + label: 'Scheduler' + mapping: + id: + type: string + label: 'Scheduler ID' + configuration: + type: ultimate_cron.plugin.scheduler.[%parent.id] + launcher: + type: mapping + label: 'Scheduler' + mapping: + id: + type: string + label: 'Launcher ID' + configuration: + type: ultimate_cron.plugin.launcher.[%parent.id] + logger: + type: mapping + label: 'Scheduler' + mapping: + id: + type: string + label: 'Logger ID' + configuration: + type: ultimate_cron.plugin.logger.[%parent.id] + +ultimate_cron.plugin.scheduler.simple: + type: mapping + label: 'Scheduler configuration' + mapping: + rules: + type: sequence + label: 'Scheduler rules' + sequence: + type: string + label: 'Scheduling rule' + +ultimate_cron.plugin.scheduler.crontab: + type: mapping + label: 'Scheduler configuration' + mapping: + rules: + type: sequence + label: 'Scheduler rules' + sequence: + type: string + label: 'Scheduling rule' + catch_up: + type: integer + label: 'Timeout (s) after job run' + +ultimate_cron.plugin.launcher.serial: + type: mapping + label: 'Scheduler configuration' + mapping: + timeouts: + type: mapping + label: 'Timeout settings' + mapping: + lock_timeout: + type: integer + label: 'Lock timeout' + max_execution_time: + type: integer + label: 'Max execution time' + launcher: + type: mapping + label: 'Launcher settings' + mapping: + max_threads: + type: integer + label: 'Max threads' + thread: + type: integer + label: 'Thread' + +ultimate_cron.plugin.logger.database: + type: mapping + label: 'Scheduler configuration' + mapping: + method: + type: string + label: 'Method' + expire: + type: integer + label: 'Expiration' + retain: + type: integer + label: 'Retain X amount of logs' + +ultimate_cron.plugin.logger.cache: + type: mapping + label: 'Scheduler configuration' + mapping: + bin: + type: string + label: 'Cache bin to use for storing logs' + timeout: + type: integer + label: 'Timeout (s) before cache entry expires' diff --git a/web/modules/ultimate_cron/css/ultimate_cron.admin.css b/web/modules/ultimate_cron/css/ultimate_cron.admin.css new file mode 100755 index 0000000000000000000000000000000000000000..9f63e04319ee18df7b312a35e82422f83c4363e4 --- /dev/null +++ b/web/modules/ultimate_cron/css/ultimate_cron.admin.css @@ -0,0 +1,65 @@ +.ultimate-cron-admin-status a { + width: 16px; + height: 16px; + display: block; +} + +.ultimate-cron-admin-status span { + display: none; +} + +.ultimate-cron-admin-status-info, +.ultimate-cron-admin-status-error, +.ultimate-cron-admin-status-success, +.ultimate-cron-admin-status-warning, +.ultimate-cron-admin-status-running { + background-position: center; + background-repeat: no-repeat; +} + +a.ultimate-cron-admin-status-info, +a.ultimate-cron-admin-status-info:hover, +a.ultimate-cron-admin-status-error, +a.ultimate-cron-admin-status-error:hover, +a.ultimate-cron-admin-status-success, +a.ultimate-cron-admin-status-success:hover, +a.ultimate-cron-admin-status-warning, +a.ultimate-cron-admin-status-warning:hover, +a.ultimate-cron-admin-status-running, +a.ultimate-cron-admin-status-running:hover { + padding-left: 24px !important; + background-position: 5px center !important; + background-repeat: no-repeat !important; +} + + +.ultimate-cron-admin-status-info { + background-image: url(../icons/message-16-info.png) !important; +} + +.ultimate-cron-admin-status-error { + background-image: url(../icons/message-16-error.png) !important; +} + +.ultimate-cron-admin-status-success { + background-image: url(../icons/message-16-ok.png) !important; +} + +.ultimate-cron-admin-status-warning { + background-image: url(../icons/message-16-warning.png) !important; +} + +.ultimate-cron-admin-status-running { + background-image: url(../icons/hourglass.png) !important; +} + +.ultimate-cron-admin-start, +.ultimate-cron-admin-end, +.ultimate-cron-admin-duration, +.ultimate-cron-admin-rules { + white-space: nowrap; +} + +.ultimate-cron-admin-message { + white-space: pre-line; +} diff --git a/web/modules/ultimate_cron/drush.services.yml b/web/modules/ultimate_cron/drush.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..daa66a5005e7a44e708704e11d316d16c9b3d1af --- /dev/null +++ b/web/modules/ultimate_cron/drush.services.yml @@ -0,0 +1,6 @@ +services: + ultimate_cron.commands: + class: Drupal\ultimate_cron\Commands\UltimateCronCommands + arguments: ['@logger.factory'] + tags: + - { name: drush.command } diff --git a/web/modules/ultimate_cron/help/about.html b/web/modules/ultimate_cron/help/about.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/about.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/hooks.html b/web/modules/ultimate_cron/help/hooks.html new file mode 100644 index 0000000000000000000000000000000000000000..eec3d3966fd5a9f789d5cb3103aed8bace4acf03 --- /dev/null +++ b/web/modules/ultimate_cron/help/hooks.html @@ -0,0 +1 @@ +<p>This section explains how to use the hooks implemented by Ultimate Cron.</p> diff --git a/web/modules/ultimate_cron/help/launchers-background-process.html b/web/modules/ultimate_cron/help/launchers-background-process.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/launchers-background-process.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/launchers-create.html b/web/modules/ultimate_cron/help/launchers-create.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/launchers-create.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/launchers-serial.html b/web/modules/ultimate_cron/help/launchers-serial.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/launchers-serial.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/launchers.html b/web/modules/ultimate_cron/help/launchers.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/launchers.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/loggers-cache.html b/web/modules/ultimate_cron/help/loggers-cache.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/loggers-cache.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/loggers-create.html b/web/modules/ultimate_cron/help/loggers-create.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/loggers-create.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/loggers-database.html b/web/modules/ultimate_cron/help/loggers-database.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/loggers-database.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/loggers.html b/web/modules/ultimate_cron/help/loggers.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/loggers.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/plugins.html b/web/modules/ultimate_cron/help/plugins.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/plugins.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/rules.html b/web/modules/ultimate_cron/help/rules.html new file mode 100644 index 0000000000000000000000000000000000000000..6abf8e874b360182a6cbf7a9485b13d7b97d85d4 --- /dev/null +++ b/web/modules/ultimate_cron/help/rules.html @@ -0,0 +1,41 @@ +<h3>Fields order</h3> +<pre> + +---------------- minute (0 - 59) + | +------------- hour (0 - 23) + | | +---------- day of month (1 - 31) + | | | +------- month (1 - 12) + | | | | +---- day of week (0 - 7) (Sunday=0) + | | | | | + * * * * * +</pre> +<p>Each of the patterns from the first five fields may be either * (an asterisk), +which matches all legal values, or a list of elements separated by commas (see below).</p> +<p>For "day of the week" (field 5), 0 is considered Sunday, 6 is Saturday and 7 is also +considered Sunday. Literal names (e.g. mon, tue, wed, etc.) are also accepted.</p> +<p>Literal names are also accepted for months (e.g. jan, feb, mar, etc.). +<p>A job is executed when the time/date specification fields all match the current +time and date. There is one exception: if both "day of month" and "day of week" +are restricted (not "*"), then either the "day of month" field (3) or the "day of week" +field (5) must match the current day (even though the other of the two fields +need not match the current day).</p> + +<h3>Fields operators</h3> +<p>There are several ways of specifying multiple date/time values in a field:</p> +<ul> +<li>The comma (',') operator specifies a list of values, for example: "1,3,4,7,8"</li> +<li>The dash ('-') operator specifies a range of values, for example: "1-6", which is equivalent to "1,2,3,4,5,6"</li> +<li>The asterisk ('*') operator specifies all possible values for a field. For example, an asterisk in the hour time field would be equivalent to 'every hour' (subject to matching other specified fields).</li> +<li>The slash ('/') operator (called "step") can be used to skip a given number of values. For example, "*/3" in the hour time field is equivalent to "0,3,6,9,12,15,18,21".</li> +<li>The plus ('+') operator (called "offset") can be used as an offset to a given range. For example, "*/10+2" in the hour minute field is equivalent to "2,12,22,32,42,52".</li> +<li>The at ('@') operator (called "skew") can be used as an auto calculated value for a given range. The value is calculated per job and is fixed per job. For example, if one job has the skew "2", the rule "*/10+@" in the hour minute field is equivalent to "2,12,22,32,42,52".</li> +</ul> + +<h3>Examples</h3> +<pre> + */15 * * * : Execute job every 15 minutes + */15+@ * * * : Execute job every 15 minutes, but at a different offset for each job + 0 2,14 * * *: Execute job every day at 2:00 and 14:00 + 0 2 * * 1-5: Execute job at 2:00 of every working day + 0 12 1 */2 1: Execute job every 2 month, at 12:00 of first day of the month OR at every monday. +</pre> + diff --git a/web/modules/ultimate_cron/help/schedulers-create.html b/web/modules/ultimate_cron/help/schedulers-create.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/schedulers-create.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/schedulers-crontab.html b/web/modules/ultimate_cron/help/schedulers-crontab.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/schedulers-crontab.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/schedulers-simple.html b/web/modules/ultimate_cron/help/schedulers-simple.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/schedulers-simple.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/schedulers.html b/web/modules/ultimate_cron/help/schedulers.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/schedulers.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/settings-create.html b/web/modules/ultimate_cron/help/settings-create.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/settings-create.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/settings-general.html b/web/modules/ultimate_cron/help/settings-general.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/settings-general.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/settings-queue.html b/web/modules/ultimate_cron/help/settings-queue.html new file mode 100644 index 0000000000000000000000000000000000000000..1901b3dcd5a067f61460cb234b177782f874da76 --- /dev/null +++ b/web/modules/ultimate_cron/help/settings-queue.html @@ -0,0 +1 @@ +<p>To be written</p> diff --git a/web/modules/ultimate_cron/help/settings.html b/web/modules/ultimate_cron/help/settings.html new file mode 100644 index 0000000000000000000000000000000000000000..b21a39d9efeca9973bc465169c7a494b29f92bee --- /dev/null +++ b/web/modules/ultimate_cron/help/settings.html @@ -0,0 +1 @@ +<p>This section describes the settings plugin type</p> diff --git a/web/modules/ultimate_cron/help/ultimate_cron.help.ini b/web/modules/ultimate_cron/help/ultimate_cron.help.ini new file mode 100644 index 0000000000000000000000000000000000000000..58c596362c1f851c01f93e476420789623748612 --- /dev/null +++ b/web/modules/ultimate_cron/help/ultimate_cron.help.ini @@ -0,0 +1,94 @@ +[advanced help settings] +line break = TRUE + +[about] +title = About Ultimate Cron +weight = -100 + +[plugins] +title = Plugins +weight = 10 + +[schedulers] +title = Schedulers +parent = plugins +weight = 10 + +[schedulers-crontab] +title = Crontab Scheduler +parent = schedulers +weight = 10 + +[schedulers-simple] +title = Simple Scheduler +parent = schedulers +weight = 20 + +[schedulers-create] +title = Creating a Scheduler plugin +parent = schedulers +weight = 30 + +[launchers] +title = Launchers +parent = plugins +weight = 20 + +[launchers-serial] +title = Serial Launcher +parent = launchers +weight = 10 + +[launchers-background-process] +title = Background Process Launcher +parent = launchers +weight = 20 + +[launchers-create] +title = Creating a Launcher plugin +parent = launchers +weight = 30 + +[loggers] +title = Loggers +parent = plugins +weight = 30 + +[loggers-database] +title = Database Logger +parent = loggers +weight = 10 + +[loggers-cache] +title = Cache Logger +parent = loggers +weight = 20 + +[loggers-create] +title = Creating a Logger plugin +parent = loggers +weight = 30 + +[settings] +title = Settings +parent = plugins +weight = 40 + +[settings-general] +title = General Settings +parent = settings +weight = 10 + +[settings-queue] +title = Queue Settings +parent = settings +weight = 20 + +[settings-create] +title = Creating a Settings plugin +parent = settings +weight = 30 + +[hooks] +title = Hooks +weight = 20 diff --git a/web/modules/ultimate_cron/icons/add.png b/web/modules/ultimate_cron/icons/add.png new file mode 100644 index 0000000000000000000000000000000000000000..6332fefea4be19eeadf211b0b202b272e8564898 --- /dev/null +++ b/web/modules/ultimate_cron/icons/add.png @@ -0,0 +1,4 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��oIDAT8˥��K�a��[�/���Y(�)%X(o�l��Nۖsk���n.���-����h�;8�f���E��P��"jï��MGˈ�}yພ羹�$�I���.t�u���lu��� AX��:�Z�H�h1�D�nZJ�OJB��{�Z����?�`2`��S���=�N$��ő�=;��a��&j�w��q�JG�#��<"N���2h8�`��6���x�ցn_+~��Zto��}`���x%XЛ͈ hXѿ�ƻ/��}���B�J�_G�&�|Q�r-��6��AރEL�⬡\�U3:WUh[�C6+� 6.f� �*���KܝF��q�����ou4܄?�d�|X���ҥ�Mv��D`� *_���[ ���#A���2��0li��R�|x�q`4w=\�������u�Q ��m+G��|%$��5��Թ���5�RO*��YGM��UO��G�qj4ְ(X�& +s1�c�˭(LV�f� R���d�j��Q '-1��A�TA>U �j4,�p�V�"4L$e�@.ArB���Y a~m�y����Y])Q8tN�L����ܞt2��"��I ��� �o=C�S��d�)�_��_�AF��(������IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/application_form_edit.png b/web/modules/ultimate_cron/icons/application_form_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..af486c940c60b746d56e757686d51801bf18ae72 --- /dev/null +++ b/web/modules/ultimate_cron/icons/application_form_edit.png @@ -0,0 +1,5 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��\IDAT8˥��KSQ�����U/"��/���HA���L͕bk���b���miI�bk������֦��Ӷ��v�c�nw�w���v�݊4 +��s��=�9��(�eSM�5�7=@��`�Ճ\�C�Y4�yQo��%�(n��V����E�d��sڑ5lE�XN2o��;F!��~���m#o�� T���祹���� ���j0�w��E�X�d2���ORd- +~mcW"�}�{-p�8�F��Np��K"��@����7cc�)���g�-�����%>���ǥ(gId����^C.�D:X�u�E��p���he{���i�b1D��X4&e�u��Q!�qAT"�R����Ө�ޛ/��g�HdS ����v0�ʂ��Og��6a�p�ω�FKޠN��0��Bd�{g���F��d����r �I@_9Z:s���7�վG8L �� �1���ݥ�˱>u�����8 ��ѐ՛�Ln:��U�f���(�a�P�z'��)�p�!Ԩ�$�'�f�F�$l����E� ��[pk�\�+5����ݸu����Lk�ެ��8�����3��k��=�!��;L[����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/application_go.png b/web/modules/ultimate_cron/icons/application_go.png new file mode 100644 index 0000000000000000000000000000000000000000..5cc2b0dd36978513f3ca009c68ebc4150976fc80 --- /dev/null +++ b/web/modules/ultimate_cron/icons/application_go.png @@ -0,0 +1,7 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��IDAT��͋Nq����;�4È�fb�x��)2��XX�HY���b�(Ij��ac��$aa#/%�R3ϋs~��5gbor]&��a���ݲ�u�{G��"�p"ȝ��;������S7��+[��k�����9�D��׀��S+u�O2S�Z�)�\�?���:|�=�R +Uv(�"Bx��g� +*ˇ���4J$��P2d��H�0K�9V�"e��N#y8����"�0,A�`VQp��nN�܌��Fx�H +1͌F�)Ea)��\�xp��7R�N��p�23�I��� )SE�ʙEV�jx#�6{�FX��4�\�L3��I�9*v�9�+�p���G�-e���{����/�A���3��� +W�����䨩��� 6,��d����ʈ�/�)f�r�왡���p\��X8w�����:N��Z�h��;���#���**z�b��j6����G���e���\>��$�/֞K����}�^��T;>_�+��$���Yk{}���?~��4{<,Lp����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/chart_line.png b/web/modules/ultimate_cron/icons/chart_line.png new file mode 100644 index 0000000000000000000000000000000000000000..85020f3205adc903896aae3ac8b2431d81d25a92 --- /dev/null +++ b/web/modules/ultimate_cron/icons/chart_line.png @@ -0,0 +1,4 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<���IDAT8�c���?%�,M_�|��z<�l�\@�_����9��'K���kvrc�l�t������Ɩ�����c�Gs�]�,� ��k����s��N�5[;~4�y���\���3��NO��+9i=��5g���x�������מ}a���7���<{[�iƦ�i� +~�����u�����DD�!4��w�<�����������o�sM�f��u�I���=�7HZ���gb���11��G\�r��qM��ʏ=�+;b2X3?�n�n���� i�#������/[���g|�>}�u~�k����^���{ ~� �_ ǪS��ˏ-�-9� ,``��7m��w��p�D@�>�� п�Ȓ﵍ֿS���VI{=N���<�����_��*�ؕ~`S���n��������A�������IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/cog.png b/web/modules/ultimate_cron/icons/cog.png new file mode 100644 index 0000000000000000000000000000000000000000..67de2c6ccbeac17742f56cf7391e72b2bf5033ba --- /dev/null +++ b/web/modules/ultimate_cron/icons/cog.png @@ -0,0 +1,7 @@ +�PNG + +��� IHDR�����������7����gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<���IDAT(�UQMKa^��� ���t��T�2� +�.E���QС:�EeQ�PR����麖QR�q��%!O^w��m!c3��3�3���To^�%�ۚ�\�Y���|���g��������z�)�,{�b,�پ���7�g|� �fʨ�'��T�ǂ���L�GUMW�0F�DN�C \`��BA���S�J�k��4$0���S��rI ���ZH�d���(c��O�:/B&6��9I#M�:�X6��1đ�|���hK���5AJ�T���Ȑ�9��Z�f) +9+�<�OQ +� ����**4K��rH�k�����?�WDj +�L�>c�!�i��������9BL%�^���0��n9ֈs�>i֓����i붻����k>�Gj����� �]������IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/delete.png b/web/modules/ultimate_cron/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..08f249365afd29594b51210c6e21ba253897505d --- /dev/null +++ b/web/modules/ultimate_cron/icons/delete.png @@ -0,0 +1,5 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��]IDAT8˥��KSa��[���n��QP��2w��ܦγL�[,bi��a�A��\�C��v��_2Ml�ZFjס���NMjm��kʷ�`&.#z�������<ϓ ���bV��P��T3�%�I��{G��qRiv�ȅ� +�tz�#E��6����Edd���J�`���DR�2<]N��;�4�Ѿ;���m>�7��8��ɀQe6�L�I���t��殷c�q!z�|v��j�/Xi���@�� �%1|h���l� !���|������! �Y#�u�U�N�w]�˼H3��u� t]E��>k%�I�f��o���R��D:�0��`�~�|� ���(r� +�on�3oG0!�$����V�� �*[W0_������-+���� d��W�&�2�ZfMF��VJp�iF&B�� >��R���g�-� �~ C�m��ڴ���ER� ឫ� p�5ްy����+��21���K�aw�h�`� ��#���a�Z�T�Zo���L��ѓ���`"�(?��'��ˎJv�K�ކ��|�:�G9[�a�w8�2 Jw��f'��y����m�zsӘ��Tsw��_��_��ιIr�����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/error.png b/web/modules/ultimate_cron/icons/error.png new file mode 100644 index 0000000000000000000000000000000000000000..628cf2dae3d419ae220c8928ac71393b480745a3 --- /dev/null +++ b/web/modules/ultimate_cron/icons/error.png @@ -0,0 +1,4 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��,IDAT8˥SKH�������k�TC[R�L��5 o=��c�k�)�c�.$B��D�P��,,��NY(FFf�����������k��af>�o�1Hb=f�U�~�Ϣ�=US�l.��Z���kP�(9X(>�H�3kR �����x_�Oqg覆8�t����]�t�i����X�a��s������_��j'{��Љ_��袺I~}����^�OT��j5՟�羇}v�M��悥�]b�7�(�V�l9��o� XoCn��%M���+ci�ѐ��+�C�i@��I�z���W���^��5� +@E��e�6dV�K>@dW������2�U���/zW���ѳ'B�O��lYxo�T3#��Yd�(,���a��PG�+�_��Rr�\:�m;gS� �o�*����0�>N� @���aΣ��5�;�5�0�?�k��kz65�ß} /qoI��0- R`Z�Uކ�1̌�U�� rj�1B �C�eqƊ������@�H \�):�xu�4E���3�'ǃ�xǃ8>��!�@}����X�;��Lc٧�S/����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/hourglass.png b/web/modules/ultimate_cron/icons/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..57b03ce7a61aa3b47d20235987a1c1918c5cb535 --- /dev/null +++ b/web/modules/ultimate_cron/icons/hourglass.png @@ -0,0 +1,6 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��zIDAT8ˍ��OSa�;28�����ĕ8�����J�1&�F�xO" P(- +ZiK +�G�^h�^i)(�^��j��������b�|��>�|�� ��GEM$m$��!a]�w`_�«ˬ��3q�<������d[�M��``h;Y� �x�����1Ė���"���t'<�v�70��"�Xx�\���MN2��y?�Jн��ƺ�9�������q�)W`Z +m�ElieXS>Ʋ��W@� �l��>i�i���U��00���,l�u�p �l�2�b�C:W�Z��k0�:Y=��9�n�W�By�(o��a��"��`=Z��M?v1D���Y���N��c�yڑ���Y�~���3�A*��7�k><�`?ʥ��d`�g!��G� �X���a6��� P���+��j(�4SEt��Yk]�[/T����SOZ�/Q�7Q�r�-����:�x��ŵ�:����Qjޝ�v��\�E���,���<����)�h��ˍkTͻ�����ح�e���|�cE�l���Aǘ�n���%��l�y#�p2��Ya4�?�}p�l�!z�yz�х�2ׅÀ��Da�(��)̻@f��)�$������B������IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/hourglass_go.png b/web/modules/ultimate_cron/icons/hourglass_go.png new file mode 100644 index 0000000000000000000000000000000000000000..b2d3a98bc4688cdfa1dc3ae3723a5a65e0e0df9b --- /dev/null +++ b/web/modules/ultimate_cron/icons/hourglass_go.png @@ -0,0 +1,7 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<���IDAT8˅�[H�a����PARRI�B�Yօ$u�P�P2I-A���H*O�Ӝ�t�5��N�a��Ι:χȦN�4�<l��ӷ?94�.����}~���X�LHH�$2%��+��n�}~]���,:-����3�����������r�HU&��Pt�a�����L��b\��������� E#�%!�@��@?'�k0-�%y�II6��31"H������ht�>��; `��w��n��T\�oU9�+�D��q. g�� ��Qdy[ ��$�bo"'=��*k@��j�15f�ְ��auM�9�:�&V�&_���5��g�^9,�(����K��*�YV�j11����U����"2���W�%��[���:��:�e�eK���˕�s���*���,ol��I��.W�mx L�\���sΩ����gc�_@qӴ�sL )��5�g�,;U4�6\��Åy����B��U!�]B �o�Ï}^y��(��w��~�+)��I{Ͷ�8 S��Ӻ!� +��d��3�Q�N�[`| +[�?�3��kmb�v��Ѥ��g����Zh�u '����@$���Q|/�T�"K����zB�SE5=�܆ ����bq�.ku���x�����%�M����Cd�b�ý +�HJq0���]�f�Q"���,�f�����F�\K=������,d�G����桔�6����ӝ�d;�T�1B<vI7�hЙ(,# +++L�@���V��"|��Fo��;��E,�\�����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/information.png b/web/modules/ultimate_cron/icons/information.png new file mode 100644 index 0000000000000000000000000000000000000000..12cd1aef900803abba99b26920337ec01ad5c267 --- /dev/null +++ b/web/modules/ultimate_cron/icons/information.png @@ -0,0 +1,6 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<���IDAT8˥��k�W�?wL�dL&1 +��6�Ѩ(h��R,�M!R;TA�.\t�m\� +>V���t���Z�����0��4��3�$������BH|�=�/_>8�U�}T��!su4-�WNV��8 (��w����O��o^u�ŕ���r�#�ɞ�F֮`!r�pzy����e�����Hn�V��Z�Ԝ��გ�[C*��³2??\��S���+��K�;E�С�����zrc%���5�*��cb�]���3_槻�i4|��v�Q@���h�ԎdÅ���"�@��Iz�S�lՒ�,Ѿ1A�ֆ�F��ޟ����Xq A��Ǐ�d�'�b�βE��.r`o��+)�ȶ6��P�)�G��!�w��G���Cqn���f�G��� �S��J����y��8�ux�8q8+��g�~jn�Bs��14(�{^&�xq�X�x�XƘ0� +���`~��Mq�rd;;?l�n]-�G� "8:Z � ��&V��#�_������M�����_G���_�8T����-��y�/L��Z��O��r���_�wn����Yf�� .����m[�/-q�_�1��r�dߪr����^L�J&�KӼ~-�<]�0��(�1������n��+iU ' �4`�)7 r珁s�?�w��?�{Y�!�����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/lock_open.png b/web/modules/ultimate_cron/icons/lock_open.png new file mode 100644 index 0000000000000000000000000000000000000000..a471765ff1432092e7113e56b42f2798968b443f --- /dev/null +++ b/web/modules/ultimate_cron/icons/lock_open.png @@ -0,0 +1,10 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<��iIDAT8�u��kQ��;i��� +)R5��p�B�7�+) +��a�����܈�@@Ņ�ʕ�4�j����h�>�3�q\��!��{8�����{����j�z�Z{+��c�c�c̽ ^�Pi@�R�������!�y�+++���I�պ199��Зz|(��O�J%�h4h6����O�TRaN����z�q��)���b������l6/A�� P����fgg) +����;� +�T�3ư���A�Osjj��:���oh�Xk�{ibbb� B��WW���Q�SXk9s� ш5�IRk��u�O�� +9���w*Q ���c(<Tf��ip +�6V�z�,p`���=��ςX������ʃ뇁���#'F��y�Y�8J�'`cڵ�x� +�����]��dx���5�p �vD,��4@�b5���;@9ldy�`��¾�������<��V��a�v�5x���$������Įs��)Z'TSؿ���"x1�㔵����"��O8����Ĵk52�}ؔ��,8�6��O��2��V��ND,�]W؝Aes�z��,�!~����l2/cR�8���v2c���{bm�X�&�K���qW.rOX����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/message-16-error.png b/web/modules/ultimate_cron/icons/message-16-error.png new file mode 100755 index 0000000000000000000000000000000000000000..486390c9b0ae6870eb81f39ca8206ad93dadd84d --- /dev/null +++ b/web/modules/ultimate_cron/icons/message-16-error.png @@ -0,0 +1,7 @@ +�PNG + +��� IHDR�����������a���IDATxڥ�?HBQƣ��r�� ��#�p(2h���Ak +�!G�����P�������2ISiъ +�:�W_O"���s��w����^���g���H�^��3���כT(P��������pc�"31!��h�>_�af��� Epd����!����H�������߯�!��LNj M��p7<��ǃ�p�l��2��L�����Ilo#6;��a�!���vp�˅r4 +u +�Rp�NE��>�yHn���l�G.���RWR�B?�%a�<�a2#=4�}��D�rO�u_� TW�XT���P��jm�������A(`%�x�ZZj�KjFU�tNjX���''�;GWX��C�W83�p~z�o�$�B{;���r�t��Aj�X)� |��!uws��ֆ��h�x-=2��ة������Q-�riI�M��Q��(�r?6�{L�~�_�ͧ��G�����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/message-16-help.png b/web/modules/ultimate_cron/icons/message-16-help.png new file mode 100755 index 0000000000000000000000000000000000000000..fc44326e46b705778b66c9aa27994fb703d7c775 --- /dev/null +++ b/web/modules/ultimate_cron/icons/message-16-help.png @@ -0,0 +1,5 @@ +�PNG + +��� IHDR�����������a��cIDATxڥ��KSQNj( ��"�|�;��ܩ�Y��'��f��zkOnn�Z�a�SI�P��&ڋ�fsk�����E�0� �I귻�-�����8���|8p~g�m�6<(��%��BZ�c�]��;?�2��_r�]M]�%�c1zP`Ua�iE�� +r/��օ���3:P��P��D�yL��`�)${� +�],��B٭0�w���c}}��kx��P��P��u.�:�[R�}]1�\l �L�L��'�&����"~���(��An ��YY��)AAu>����ʉȯs�Y78����_��uU�C� vEUw���k�Z�և�&n��C(�:0�����=q��E$:�ء��a�6����s����㐭���|㰢=&d!P��!J�\3`K T���;�}9�B�@������%(��o�'PY�d)Aoo 3�f������C�0��G�is�4D6�a�G�@�[�Qi��2ZST�č<�(�O-�1� Y�Q�B��lM7H{���D�AncҖ7XH���]Bn�k�{�����Q�Wum���~��+� i�c�h�`.9���a[��_�i��w�tOW��ʫ���m5��$k��¶� qa��Tbn����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/message-16-info.png b/web/modules/ultimate_cron/icons/message-16-info.png new file mode 100755 index 0000000000000000000000000000000000000000..f47924fc8ab0c800363774a555ce2da7186cf787 --- /dev/null +++ b/web/modules/ultimate_cron/icons/message-16-info.png @@ -0,0 +1,5 @@ +�PNG + +��� IHDR�����������a���IDATx�m��KSa���PBP�� z�v.;�RѴ�AT:�Ew�&N��t�Hc-�\��y�,I����奝3�S��*_ 60�o������<����s�yd���JI�q��Ih�^���P+t��˦}Pw<�im蛼�5Ȋ �1���*F!���.y1�*�=V����"��{�=�/� ��Ϩh��!�,� =J߽���i�J��5,B� +x#����F�M���i]Ȑ�Ȟ���$`��L�,�z9���M������}h�i=!�������_��'�c�FF�$�>�|Q8���;ǯs4q(�s�Ad�v^���=�u��X ]`3����XCI�{�C"d��[BY�9<Lٿ#����DT.��$( &q��C�;q@C�}ٖ��|k��6��ig���C^GQ�$ky5߳�|�v$@�pȱt��nWE����x��$(�o�K��f������?�JZށp,����� Z�@:WY!�oaj�B���ʵ��Q�%P�k"�s�Ф$p��BY� +ڵ~�/�4w���9h����4��f]�m��Ē)�������P�m�*���v�0iG��p)��)�0�cz�q e����8z�N��L�f����i�}AR���;X��ȴD�ڢ�7���_�tNw��������}�B��h�9��Q�N(`Y����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/message-16-ok.png b/web/modules/ultimate_cron/icons/message-16-ok.png new file mode 100755 index 0000000000000000000000000000000000000000..9edebe6bd22fa0cbc17a3d68507b47142458aa24 --- /dev/null +++ b/web/modules/ultimate_cron/icons/message-16-ok.png @@ -0,0 +1,7 @@ +�PNG + +��� IHDR�����������a��FIDATxڥ��KSaǍ( ���K�9s�c?���D<8v1s[��8�""j�����<�ۜ�rg� �6kV&���M]+[Taԅ��o����}���>��|x�}�:��f��r�1݄Τ��N�y�8�� M����t�;��]�it���_�C{G:H˲�� +tS� �����n�¾i1?1C��@��6j +�<]�ԼC�C`��`�q������P�*��z�>�u�z���w �k�?���tڏ�H����1B�iv����TQ����E]p�_�ʀI38{��mA�|�bʨ}������" +^oj�R ��۸>w ����1�x��:W:Ѿ�E\�^�7$ +L��� Oȱ�rI(x��-n�8�������@f���r^yL#g�E�ph�tK���R�p.����1RDvK[�vJ���MeQH�V�?��T���yP��)�:�Z�~�6�{Fv��ȦeB1G8�N�2��>\E꓂ ��Z�t��1�f3�Y�&��D��'��O�||$4�ƨ"�&!��@2)�%@]���[�}�C;�wN��]-���``0�����Q9���O[x�>P�d����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/message-16-warning.png b/web/modules/ultimate_cron/icons/message-16-warning.png new file mode 100755 index 0000000000000000000000000000000000000000..ffc43177abdc983dd2d9d46c25cb0ad697e2b3f0 --- /dev/null +++ b/web/modules/ultimate_cron/icons/message-16-warning.png @@ -0,0 +1,6 @@ +�PNG + +��� IHDR�����������a���IDAT8Oc���?%���_��r��@�a��P� �op��|���W���58�%ɀ�K������<���7M���Pl,B�]UvG>��~��+8�>������Q4�k˭�����1��^.Z��]�������k(�P h@s����G��N� +�c����x h�4�Xѯ�{Q����T��0������ +��qP�k|��an���r����p��#�+2�oa5�2[�|i� +�v^ة�Y��d�=���sU,0(N�{��0��K���_]ˇ"�����d͇(Td��-hS@Q©��`�.��C�i��܀�X���r`(\?I����`����n��������1�¯����tT�7���aIQ��=l�0;n)�[���`�£4g������IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/icons/tick.png b/web/modules/ultimate_cron/icons/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..a9925a06ab02db30c1e7ead9c701c15bc63145cb --- /dev/null +++ b/web/modules/ultimate_cron/icons/tick.png @@ -0,0 +1,5 @@ +�PNG + +��� IHDR�����������a���gAMA����7�����tEXtSoftware�Adobe ImageReadyq�e<���IDAT8˽��.Ca�{�8�bn�SB��T'��)E)����V�CJǥj��� �ZՆg/����h �ݿ����k����n�^k�[�ꝿ2P�6�c=�XH�*�G�`?xԅ�{7�7V�Ԩ�پ%V�Hy��q��Ntn���[���J2^�5�3��X�,�S-OƜ�o����D�X��x����2Oܵ �r�]L`�}�Z����T��U�(��Si������P��/�a:6͖,A` �%S�=���[ +���b[�a�='�L�a�W�{����x���D����[�u9J�B�GqzfGN��0��os��6�"f��fh�ZR��".��2H-[��{���(7�h�@`%E��[I��W�u3��e�+� ����l�GQ&���'� ������������k|���<R +H����IEND�B`� \ No newline at end of file diff --git a/web/modules/ultimate_cron/js/jquery.tableSort.js b/web/modules/ultimate_cron/js/jquery.tableSort.js new file mode 100644 index 0000000000000000000000000000000000000000..601d46cd17f0fc8cd10cc520aa9773873e660eaa --- /dev/null +++ b/web/modules/ultimate_cron/js/jquery.tableSort.js @@ -0,0 +1,211 @@ +/* =============================== +| TABLESORT.JS +| Copyright, Andy Croxall (mitya@mitya.co.uk) +| For documentation and demo see http://mitya.co.uk/scripts/Animated-table-sort-REGEXP-friendly-111 +| +| USAGE +| This script may be used, distributed and modified freely but this header must remain in tact. +| For usage info and demo, including info on args and params, see www.mitya.co.uk/scripts +=============================== */ + + +jQuery.fn.sortTable = function(params) { + + + /*----------- + | STOP right now if anim already in progress + -----------*/ + + if ($(this).find(':animated').length > 0) return; + + /*----------- + | VALIDATE TABLE & PARAMS + | - if no col to sort on passed, complain and return + | - if table doesn't contain requested col, complain and return + | If !sortType or invalid sortType, assume ascii sort + -----------*/ + + var error = null; + var complain = null; + if (!params.onCol) { error = "No column specified to search on"; complain = true; } + else if ($(this).find('td:nth-child('+params.onCol+')').length == 0) { error = "The requested column wasn't found in the table"; complain = true; } + if (error) { if (complain) alert(error); return; } + if (!params.sortType || params.sortType != 'numeric') params.sortType = 'ascii'; + + + /*----------- + | PREP + | - declare array to store the contents of each <td>, or, if sorting on regexp, the pattern match of the regexp in each <td> + | - Give the <table> position: relative to aid animation + | - Mark the col we're sorting on with an identifier class + -----------*/ + + var valuesToSort = []; + $(this).css('position', 'relative'); + var doneAnimating = 0; + var tdSelectorText = 'td'+(!params.onCol ? '' : ':nth-child('+params.onCol+')'); + $(this).find('td:nth-child('+params.onCol+')').addClass('sortOnThisCol'); + var thiss = this; + + + /*----------- + | Iterate over table and. For each: + | - append its content / regexp match (see above) to valuesToSort[] + | - create a new <div>, give it position: absolute and copy over the <td>'s content into it + | - fix the <td>'s width/height to its offset width/height so that, when we remove its html, it won't change shape + | - clear the <td>'s content + | - clear the <td>'s content + | There is no visual effect in this. But it means each <td>'s content is now 'animatable', since it's position: absolute. + -----------*/ + + var counter = 0; + $(this).find('td').each(function() { + if ($(this).is('.sortOnThisCol') || (!params.onCol && !params.keepRelationships)) { + var valForSort = !params.child ? $(this).text() : (params.child != 'input' ? $(this).find(params.child).text() : $(this).find(params.child).val()); + if (params.regexp) { + valForSort = valForSort.match(new RegExp(params.regexp))[!params.regexpIndex ? 0 : params.regexpIndex]; + } + valuesToSort.push(valForSort); + } + var thisTDHTMLHolder = document.createElement('div'); + with($(thisTDHTMLHolder)) { + html($(this).html()); + if (params.child && params.child == 'input') html(html().replace(/<input /, "<input value='"+$(this).find(params.child).val()+"'", html())); + css({position: 'relative', left: 0, top: 0}); + } + $(this).html(''); + $(this).append(thisTDHTMLHolder); + counter++; + }); + + + /*----------- + | Sort values array. + | - Sort (either simply, on ascii, or numeric if sortNumeric == true) + | - If descending == true, reverse after sort + -----------*/ + + params.sortType == 'numeric' ? valuesToSort.sort(function(a, b) { return (a.replace(/[^\d\.]/g, '', a)-b.replace(/[^\d\.]/g, '', b)); }) : valuesToSort.sort(); + if (params.sortDesc) { + valuesToSort_tempCopy = []; + for(var u=valuesToSort.length; u--; u>=0) valuesToSort_tempCopy.push(valuesToSort[u]); + valuesToSort = valuesToSort_tempCopy; + delete(valuesToSort_tempCopy) + } + + + + /*----------- + | Now, for each: + -----------*/ + + for(var k in valuesToSort) { + + //establish current <td> relating to this value of the array + var currTD = $($(this).find(tdSelectorText).filter(function() { + return ( + ( + !params.regexp + && + ( + ( + params.child + && + ( + ( + params.child != 'input' + && + valuesToSort[k] == $(this).find(params.child).text() + ) + || + params.child == 'input' + && + valuesToSort[k] == $(this).find(params.child).val() + ) + ) + || + ( + !params.child + && + valuesToSort[k] == $(this).children('div').html() + ) + ) + ) + || + ( + params.regexp + && + $(this).children('div').html().match(new RegExp(params.regexp))[!params.regexpIndex ? 0 : params.regexpIndex] == valuesToSort[k] + ) + ) + && + !$(this).hasClass('tableSort_TDRepopulated'); + }).get(0)); + + //give current <td> a class to mark it as having been used, so we don't get confused with duplicate values + currTD.addClass('tableSort_TDRepopulated'); + + //establish target <td> for this value and store as a node reference on this <td> + var targetTD = $($(this).find(tdSelectorText).get(k)); + currTD.get(0).toTD = targetTD; + + //if we're sorting on a particular column and maintaining relationships, also give the other <td>s in rows a node reference + //denoting ITS target, so they move with their lead siibling + if (params.keepRelationships) { + var counter = 0; + $(currTD).parent().children('td').each(function() { + $(this).get(0).toTD = $(targetTD.parent().children().get(counter)); + counter++; + }); + } + + //establish current relative positions for the current and target <td>s and use this to calculate how far each <div> needs to move + var currPos = currTD.position(); + var targetPos = targetTD.position(); + var moveBy_top = targetPos.top - currPos.top; + + //invert values if going backwards/upwards + if (targetPos.top > currPos.top) moveBy_top = Math.abs(moveBy_top); + + /*----------- + | ANIMATE + | - work out what to animate on. + | - if !keepRelationships, animate only <td>s in the col we're sorting on (identified by .sortOnThisCol) + | - if keepRelationships, animate all cols but <td>s that aren't .sortOnThisCol follow lead sibiling with .sortOnThisCol + | - run animation. On callback, update each <td> with content of <div> that just moved into it and remove <div>s + | - If noAnim, we'll still run aniamte() but give it a low duration so it appears instant + -----------*/ + + var animateOn = params.keepRelationships ? currTD.add(currTD.siblings()) : currTD; + var done = 0; + animateOn.children('div').animate({top: moveBy_top}, !params.noAnim ? 500 : 0, null, function() { + if ($(this).parent().is('.sortOnThisCol') || !params.keepRelationships) { + done++; + if (done == valuesToSort.length-1) thiss.tableSort_cleanUp(); + } + }); + + } + +}; + + +jQuery.fn.tableSort_cleanUp = function() { + + /*----------- + | AFTER ANIM + | - assign each <td> its new content as property of it (DON'T populate it yet - this <td> may still need to be read by + | other <td>s' toTD node references + | - once new contents for each <td> gathered, populate + | - remove some identifier classes and properties + -----------*/ + $(this).find('td').each(function() { + if($(this).get(0).toTD) $($(this).get(0).toTD).get(0).newHTML = $(this).children('div').html(); + }); + $(this).find('td').each(function() { $(this).html($(this).get(0).newHTML); }); + $('td.tableSort_TDRepopulated').removeClass('tableSort_TDRepopulated'); + $(this).find('.sortOnThisCol').removeClass('sortOnThisCol'); + $(this).find('td[newHTML]').attr('newHTML', ''); + $(this).find('td[toTD]').attr('toTD', ''); + +}; \ No newline at end of file diff --git a/web/modules/ultimate_cron/js/jquery.tablesorter.js b/web/modules/ultimate_cron/js/jquery.tablesorter.js new file mode 100644 index 0000000000000000000000000000000000000000..a14d071a8a728706c8af1d456c50d7a9af2cf05d --- /dev/null +++ b/web/modules/ultimate_cron/js/jquery.tablesorter.js @@ -0,0 +1,1031 @@ +/* + * + * TableSorter 2.0 - Client-side table sorting with ease! + * Version 2.0.5b + * @requires jQuery v1.2.3 + * + * Copyright (c) 2007 Christian Bach + * Examples and docs at: http://tablesorter.com + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ +/** + * + * @description Create a sortable table with multi-column sorting capabilitys + * + * @example $('table').tablesorter(); + * @desc Create a simple tablesorter interface. + * + * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); + * @desc Create a tablesorter interface and sort on the first and secound column column headers. + * + * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); + * + * @desc Create a tablesorter interface and disableing the first and second column headers. + * + * + * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); + * + * @desc Create a tablesorter interface and set a column parser for the first + * and second column. + * + * + * @param Object + * settings An object literal containing key/value pairs to provide + * optional settings. + * + * + * @option String cssHeader (optional) A string of the class name to be appended + * to sortable tr elements in the thead of the table. Default value: + * "header" + * + * @option String cssAsc (optional) A string of the class name to be appended to + * sortable tr elements in the thead on a ascending sort. Default value: + * "headerSortUp" + * + * @option String cssDesc (optional) A string of the class name to be appended + * to sortable tr elements in the thead on a descending sort. Default + * value: "headerSortDown" + * + * @option String sortInitialOrder (optional) A string of the inital sorting + * order can be asc or desc. Default value: "asc" + * + * @option String sortMultisortKey (optional) A string of the multi-column sort + * key. Default value: "shiftKey" + * + * @option String textExtraction (optional) A string of the text-extraction + * method to use. For complex html structures inside td cell set this + * option to "complex", on large tables the complex option can be slow. + * Default value: "simple" + * + * @option Object headers (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortList (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortForce (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * prepended to user-selected rules. Default value: null + * + * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever + * to use String.localeCampare method or not. Default set to true. + * + * + * @option Array sortAppend (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * appended to user-selected rules. Default value: null + * + * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter + * should apply fixed widths to the table columns. This is usefull when + * using the pager companion plugin. This options requires the dimension + * jquery plugin. Default value: false + * + * @option Boolean cancelSelection (optional) Boolean flag indicating if + * tablesorter should cancel selection of the table headers text. + * Default value: true + * + * @option Boolean debug (optional) Boolean flag indicating if tablesorter + * should display debuging information usefull for development. + * + * @type jQuery + * + * @name tablesorter + * + * @cat Plugins/Tablesorter + * + * @author Christian Bach/christian.bach@polyester.se + */ + +(function ($) { + $.extend({ + tablesorter: new + function () { + + var parsers = [], + widgets = []; + + this.defaults = { + cssHeader: "header", + cssAsc: "headerSortUp", + cssDesc: "headerSortDown", + cssChildRow: "expand-child", + sortInitialOrder: "asc", + sortMultiSortKey: "shiftKey", + sortForce: null, + sortAppend: null, + sortLocaleCompare: true, + textExtraction: "simple", + parsers: {}, widgets: [], + widgetZebra: { + css: ["even", "odd"] + }, headers: {}, widthFixed: false, + cancelSelection: true, + sortList: [], + headerList: [], + dateFormat: "us", + decimal: '/\.|\,/g', + onRenderHeader: null, + selectorHeaders: 'thead th', + debug: false + }; + + /* debuging utils */ + + function benchmark(s, d) { + log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); + } + + this.benchmark = benchmark; + + function log(s) { + if (typeof console != "undefined" && typeof console.debug != "undefined") { + console.log(s); + } else { + alert(s); + } + } + + /* parsers utils */ + + function buildParserCache(table, $headers) { + + if (table.config.debug) { + var parsersDebug = ""; + } + + if (table.tBodies.length == 0) return; // In the case of empty tables + var rows = table.tBodies[0].rows; + + if (rows[0]) { + + var list = [], + cells = rows[0].cells, + l = cells.length; + + for (var i = 0; i < l; i++) { + + var p = false; + + if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { + + p = getParserById($($headers[i]).metadata().sorter); + + } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { + + p = getParserById(table.config.headers[i].sorter); + } + if (!p) { + + p = detectParserForColumn(table, rows, -1, i); + } + + if (table.config.debug) { + parsersDebug += "column:" + i + " parser:" + p.id + "\n"; + } + + list.push(p); + } + } + + if (table.config.debug) { + log(parsersDebug); + } + + return list; + }; + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var l = parsers.length, + node = false, + nodeValue = false, + keepLooking = true; + while (nodeValue == '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); + nodeValue = trimAndGetNodeText(table.config, node); + if (table.config.debug) { + log('Checking if value was empty on row:' + rowIndex); + } + } else { + keepLooking = false; + } + } + for (var i = 1; i < l; i++) { + if (parsers[i].is(nodeValue, table, node)) { + return parsers[i]; + } + } + // 0 is always the generic parser (text) + return parsers[0]; + } + + function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { + return rows[rowIndex].cells[cellIndex]; + } + + function trimAndGetNodeText(config, node) { + return $.trim(getElementText(config, node)); + } + + function getParserById(name) { + var l = parsers.length; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == name.toLowerCase()) { + return parsers[i]; + } + } + return false; + } + + /* utils */ + + function buildCache(table) { + + if (table.config.debug) { + var cacheTime = new Date(); + } + + var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, + totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, + parsers = table.config.parsers, + cache = { + row: [], + normalized: [] + }; + + for (var i = 0; i < totalRows; ++i) { + + /** Add the table data to main data array */ + var c = $(table.tBodies[0].rows[i]), + cols = []; + + // if this is a child row, add it to the last row's children and + // continue to the next row + if (c.hasClass(table.config.cssChildRow)) { + cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); + // go to the next for loop + continue; + } + + cache.row.push(c); + + for (var j = 0; j < totalCells; ++j) { + cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); + } + + cols.push(cache.normalized.length); // add position for rowCache + cache.normalized.push(cols); + cols = null; + }; + + if (table.config.debug) { + benchmark("Building cache for " + totalRows + " rows:", cacheTime); + } + + return cache; + }; + + function getElementText(config, node) { + + var text = ""; + + if (!node) return ""; + + if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; + + if (config.textExtraction == "simple") { + if (config.supportsTextContent) { + text = node.textContent; + } else { + if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { + text = node.childNodes[0].innerHTML; + } else { + text = node.innerHTML; + } + } + } else { + if (typeof(config.textExtraction) == "function") { + text = config.textExtraction(node); + } else { + text = $(node).text(); + } + } + return text; + } + + function appendToTable(table, cache) { + + if (table.config.debug) { + var appendTime = new Date() + } + + var c = cache, + r = c.row, + n = c.normalized, + totalRows = n.length, + checkCell = (n[0].length - 1), + tableBody = $(table.tBodies[0]), + rows = []; + + + for (var i = 0; i < totalRows; i++) { + var pos = n[i][checkCell]; + + rows.push(r[pos]); + + if (!table.config.appender) { + + //var o = ; + var l = r[pos].length; + for (var j = 0; j < l; j++) { + tableBody[0].appendChild(r[pos][j]); + } + + // + } + } + + + + if (table.config.appender) { + + table.config.appender(table, rows); + } + + rows = null; + + if (table.config.debug) { + benchmark("Rebuilt table:", appendTime); + } + + // apply table widgets + applyWidget(table); + + // trigger sortend + setTimeout(function () { + $(table).trigger("sortEnd"); + }, 0); + + }; + + function buildHeaders(table) { + + if (table.config.debug) { + var time = new Date(); + } + + var meta = ($.metadata) ? true : false; + + var header_index = computeTableHeaderCellIndexes(table); + + $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { + + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + // this.column = index; + this.order = formatSortingOrder(table.config.sortInitialOrder); + + + this.count = this.order; + + if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; + if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); + + if (!this.sortDisabled) { + var $th = $(this).addClass(table.config.cssHeader); + if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); + } + + // add cell to headerList + table.config.headerList[index] = this; + }); + + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + + return $tableHeaders; + + }; + + // from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + + + function computeTableHeaderCellIndexes(t) { + var matrix = []; + var lookup = {}; + var thead = t.getElementsByTagName('THEAD')[0]; + var trs = thead.getElementsByTagName('TR'); + + for (var i = 0; i < trs.length; i++) { + var cells = trs[i].cells; + for (var j = 0; j < cells.length; j++) { + var c = cells[j]; + + var rowIndex = c.parentNode.rowIndex; + var cellId = rowIndex + "-" + c.cellIndex; + var rowSpan = c.rowSpan || 1; + var colSpan = c.colSpan || 1 + var firstAvailCol; + if (typeof(matrix[rowIndex]) == "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (var k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) == "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + for (var k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) == "undefined") { + matrix[k] = []; + } + var matrixrow = matrix[k]; + for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function checkCellColSpan(table, rows, row) { + var arr = [], + r = table.tHead.rows, + c = r[row].cells; + + for (var i = 0; i < c.length; i++) { + var cell = c[i]; + + if (cell.colSpan > 1) { + arr = arr.concat(checkCellColSpan(table, headerArr, row++)); + } else { + if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { + arr.push(cell); + } + // headerArr[row] = (i+row); + } + } + return arr; + }; + + function checkHeaderMetadata(cell) { + if (($.metadata) && ($(cell).metadata().sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptions(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptionsSortingLocked(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; + return false; + } + + function applyWidget(table) { + var c = table.config.widgets; + var l = c.length; + for (var i = 0; i < l; i++) { + + getWidgetById(c[i]).format(table); + } + + } + + function getWidgetById(name) { + var l = widgets.length; + for (var i = 0; i < l; i++) { + if (widgets[i].id.toLowerCase() == name.toLowerCase()) { + return widgets[i]; + } + } + }; + + function formatSortingOrder(v) { + if (typeof(v) != "Number") { + return (v.toLowerCase() == "desc") ? 1 : 0; + } else { + return (v == 1) ? 1 : 0; + } + } + + function isValueInArray(v, a) { + var l = a.length; + for (var i = 0; i < l; i++) { + if (a[i][0] == v) { + return true; + } + } + return false; + } + + function setHeadersCss(table, $headers, list, css) { + // remove all header information + $headers.removeClass(css[0]).removeClass(css[1]); + + var h = []; + $headers.each(function (offset) { + if (!this.sortDisabled) { + h[this.column] = $(this); + } + }); + + var l = list.length; + for (var i = 0; i < l; i++) { + h[list[i][0]].addClass(css[list[i][1]]); + } + } + + function fixColumnWidth(table, $headers) { + var c = table.config; + if (c.widthFixed) { + var colgroup = $('<colgroup>'); + $("tr:first td", table.tBodies[0]).each(function () { + colgroup.append($('<col>').css('width', $(this).width())); + }); + $(table).prepend(colgroup); + }; + } + + function updateHeaderSortCount(table, sortList) { + var c = table.config, + l = sortList.length; + for (var i = 0; i < l; i++) { + var s = sortList[i], + o = c.headerList[s[0]]; + o.count = s[1]; + o.count++; + } + } + + /* sorting methods */ + + function multisort(table, sortList, cache) { + + if (table.config.debug) { + var sortTime = new Date(); + } + + var dynamicExp = "var sortWrapper = function(a,b) {", + l = sortList.length; + + // TODO: inline functions. + for (var i = 0; i < l; i++) { + + var c = sortList[i][0]; + var order = sortList[i][1]; + // var s = (getCachedSortType(table.config.parsers,c) == "text") ? + // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? + // "sortNumeric" : "sortNumericDesc"); + // var s = (table.config.parsers[c].type == "text") ? ((order == 0) + // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? + // makeSortNumeric(c) : makeSortNumericDesc(c)); + var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); + var e = "e" + i; + + dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c + // + "]); "; + dynamicExp += "if(" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + + } + + // if value is the same keep orignal order + var orgOrderCol = cache.normalized[0].length - 1; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + + for (var i = 0; i < l; i++) { + dynamicExp += "}; "; + } + + dynamicExp += "return 0; "; + dynamicExp += "}; "; + + if (table.config.debug) { + benchmark("Evaling expression:" + dynamicExp, new Date()); + } + + eval(dynamicExp); + + cache.normalized.sort(sortWrapper); + + if (table.config.debug) { + benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); + } + + return cache; + }; + + function makeSortFunction(type, direction, index) { + var a = "a[" + index + "]", + b = "b[" + index + "]"; + if (type == 'text' && direction == 'asc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; + } else if (type == 'text' && direction == 'desc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; + } else if (type == 'numeric' && direction == 'asc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; + } else if (type == 'numeric' && direction == 'desc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; + } + }; + + function makeSortText(i) { + return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; + }; + + function makeSortTextDesc(i) { + return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; + }; + + function makeSortNumeric(i) { + return "a[" + i + "]-b[" + i + "];"; + }; + + function makeSortNumericDesc(i) { + return "b[" + i + "]-a[" + i + "];"; + }; + + function sortText(a, b) { + if (table.config.sortLocaleCompare) return a.localeCompare(b); + return ((a < b) ? -1 : ((a > b) ? 1 : 0)); + }; + + function sortTextDesc(a, b) { + if (table.config.sortLocaleCompare) return b.localeCompare(a); + return ((b < a) ? -1 : ((b > a) ? 1 : 0)); + }; + + function sortNumeric(a, b) { + return a - b; + }; + + function sortNumericDesc(a, b) { + return b - a; + }; + + function getCachedSortType(parsers, i) { + return parsers[i].type; + }; /* public methods */ + this.construct = function (settings) { + return this.each(function () { + // if no thead or tbody quit. + if (!this.tHead || !this.tBodies) return; + // declare + var $this, $document, $headers, cache, config, shiftDown = 0, + sortOrder; + // new blank config object + this.config = {}; + // merge and extend. + config = $.extend(this.config, $.tablesorter.defaults, settings); + // store common expression for speed + $this = $(this); + // save the settings where they read + $.data(this, "tablesorter", config); + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + this.config.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + cache = buildCache(this); + // get the css class names, could be done else where. + var sortCSS = [config.cssDesc, config.cssAsc]; + // fixate columns if the users supplies the fixedWidth option + fixColumnWidth(this); + // apply event handling to headers + // this is to big, perhaps break it out? + $headers.click( + + function (e) { + var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; + if (!this.sortDisabled && totalRows > 0) { + // Only call sortStart if sorting is + // enabled. + $this.trigger("sortStart"); + // store exp, for speed + var $cell = $(this); + // get current column index + var i = this.column; + // get current column sort order + this.order = this.count++ % 2; + // always sort on the locked order. + if(this.lockedOrder) this.order = this.lockedOrder; + + // user only whants to sort on one + // column + if (!e[config.sortMultiSortKey]) { + // flush the sort list + config.sortList = []; + if (config.sortForce != null) { + var a = config.sortForce; + for (var j = 0; j < a.length; j++) { + if (a[j][0] != i) { + config.sortList.push(a[j]); + } + } + } + // add column to sort list + config.sortList.push([i, this.order]); + // multi column sorting + } else { + // the user has clicked on an all + // ready sortet column. + if (isValueInArray(i, config.sortList)) { + // revers the sorting direction + // for all tables. + for (var j = 0; j < config.sortList.length; j++) { + var s = config.sortList[j], + o = config.headerList[s[0]]; + if (s[0] == i) { + o.count = s[1]; + o.count++; + s[1] = o.count % 2; + } + } + } else { + // add column to sort list array + config.sortList.push([i, this.order]); + } + }; + setTimeout(function () { + // set css for headers + setHeadersCss($this[0], $headers, config.sortList, sortCSS); + appendToTable( + $this[0], multisort( + $this[0], config.sortList, cache) + ); + }, 1); + // stop normal event by returning false + return false; + } + // cancel selection + }).mousedown(function () { + if (config.cancelSelection) { + this.onselectstart = function () { + return false + }; + return false; + } + }); + // apply easy methods that trigger binded events + $this.bind("update", function () { + var me = this; + setTimeout(function () { + // rebuild parsers. + me.config.parsers = buildParserCache( + me, $headers); + // rebuild the cache map + cache = buildCache(me); + }, 1); + }).bind("updateCell", function (e, cell) { + var config = this.config; + // get position from the dom. + var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; + // update cache + cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( + getElementText(config, cell), cell); + }).bind("sorton", function (e, list) { + $(this).trigger("sortStart"); + config.sortList = list; + // update and store the sortlist + var sortList = config.sortList; + // update header count index + updateHeaderSortCount(this, sortList); + // set css for headers + setHeadersCss(this, $headers, sortList, sortCSS); + // sort the table and append it to the dom + appendToTable(this, multisort(this, sortList, cache)); + }).bind("appendCache", function () { + appendToTable(this, cache); + }).bind("applyWidgetId", function (e, id) { + getWidgetById(id).format(this); + }).bind("applyWidgets", function () { + // apply widgets + applyWidget(this); + }); + if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { + config.sortList = $(this).metadata().sortlist; + } + // if user has supplied a sort list to constructor. + if (config.sortList.length > 0) { + $this.trigger("sorton", [config.sortList]); + } + // apply widgets + applyWidget(this); + }); + }; + this.addParser = function (parser) { + var l = parsers.length, + a = true; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + parsers.push(parser); + }; + }; + this.addWidget = function (widget) { + widgets.push(widget); + }; + this.formatFloat = function (s) { + var i = parseFloat(s); + return (isNaN(i)) ? 0 : i; + }; + this.formatInt = function (s) { + var i = parseInt(s); + return (isNaN(i)) ? 0 : i; + }; + this.isDigit = function (s, config) { + // replace all an wanted chars and match. + return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); + }; + this.clearTableBody = function (table) { + if ($.browser.msie) { + function empty() { + while (this.firstChild) + this.removeChild(this.firstChild); + } + empty.apply(table.tBodies[0]); + } else { + table.tBodies[0].innerHTML = ""; + } + }; + } + }); + + // extend plugin scope + $.fn.extend({ + tablesorter: $.tablesorter.construct + }); + + // make shortcut + var ts = $.tablesorter; + + // add default parsers + ts.addParser({ + id: "text", + is: function (s) { + return true; + }, format: function (s) { + return $.trim(s.toLocaleLowerCase()); + }, type: "text" + }); + + ts.addParser({ + id: "digit", + is: function (s, table) { + var c = table.config; + return $.tablesorter.isDigit(s, c); + }, format: function (s) { + return $.tablesorter.formatFloat(s); + }, type: "numeric" + }); + + ts.addParser({ + id: "currency", + is: function (s) { + return /^[£$€?.]/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function (s) { + return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); + }, format: function (s) { + var a = s.split("."), + r = "", + l = a.length; + for (var i = 0; i < l; i++) { + var item = a[i]; + if (item.length == 2) { + r += "0" + item; + } else { + r += item; + } + } + return $.tablesorter.formatFloat(r); + }, type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function (s) { + return /^(https?|ftp|file):\/\/$/.test(s); + }, format: function (s) { + return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); + }, type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function (s) { + return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( + new RegExp(/-/g), "/")).getTime() : "0"); + }, type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function (s) { + return /\%$/.test($.trim(s)); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function (s) { + return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + + ts.addParser({ + id: "shortDate", + is: function (s) { + return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); + }, format: function (s, table) { + var c = table.config; + s = s.replace(/\-/g, "/"); + if (c.dateFormat == "us") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); + } else if (c.dateFormat == "uk") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); + } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); + } + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "time", + is: function (s) { + return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "metadata", + is: function (s) { + return false; + }, format: function (s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, type: "numeric" + }); + // add default widgets + ts.addWidget({ + id: "zebra", + format: function (table) { + if (table.config.debug) { + var time = new Date(); + } + var $tr, row = -1, + odd; + // loop through the visible rows + $("tr:visible", table.tBodies[0]).each(function (i) { + $tr = $(this); + // style children rows the same way the parent + // row was styled + if (!$tr.hasClass(table.config.cssChildRow)) row++; + odd = (row % 2 == 0); + $tr.removeClass( + table.config.widgetZebra.css[odd ? 0 : 1]).addClass( + table.config.widgetZebra.css[odd ? 1 : 0]) + }); + if (table.config.debug) { + $.tablesorter.benchmark("Applying Zebra widget", time); + } + } + }); +})(jQuery); diff --git a/web/modules/ultimate_cron/js/jquery.tablesorter.min.js b/web/modules/ultimate_cron/js/jquery.tablesorter.min.js new file mode 100644 index 0000000000000000000000000000000000000000..b8605df1e7277e2f88237eb7bee7c7e38d80a232 --- /dev/null +++ b/web/modules/ultimate_cron/js/jquery.tablesorter.min.js @@ -0,0 +1,4 @@ + +(function($){$.extend({tablesorter:new +function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i<l;i++){var p=false;if($.metadata&&($($headers[i]).metadata()&&$($headers[i]).metadata().sorter)){p=getParserById($($headers[i]).metadata().sorter);}else if((table.config.headers[i]&&table.config.headers[i].sorter)){p=getParserById(table.config.headers[i].sorter);}if(!p){p=detectParserForColumn(table,rows,-1,i);}if(table.config.debug){parsersDebug+="column:"+i+" parser:"+p.id+"\n";}list.push(p);}}if(table.config.debug){log(parsersDebug);}return list;};function detectParserForColumn(table,rows,rowIndex,cellIndex){var l=parsers.length,node=false,nodeValue=false,keepLooking=true;while(nodeValue==''&&keepLooking){rowIndex++;if(rows[rowIndex]){node=getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex);nodeValue=trimAndGetNodeText(table.config,node);if(table.config.debug){log('Checking if value was empty on row:'+rowIndex);}}else{keepLooking=false;}}for(var i=1;i<l;i++){if(parsers[i].is(nodeValue,table,node)){return parsers[i];}}return parsers[0];}function getNodeFromRowAndCellIndex(rows,rowIndex,cellIndex){return rows[rowIndex].cells[cellIndex];}function trimAndGetNodeText(config,node){return $.trim(getElementText(config,node));}function getParserById(name){var l=parsers.length;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==name.toLowerCase()){return parsers[i];}}return false;}function buildCache(table){if(table.config.debug){var cacheTime=new Date();}var totalRows=(table.tBodies[0]&&table.tBodies[0].rows.length)||0,totalCells=(table.tBodies[0].rows[0]&&table.tBodies[0].rows[0].cells.length)||0,parsers=table.config.parsers,cache={row:[],normalized:[]};for(var i=0;i<totalRows;++i){var c=$(table.tBodies[0].rows[i]),cols=[];if(c.hasClass(table.config.cssChildRow)){cache.row[cache.row.length-1]=cache.row[cache.row.length-1].add(c);continue;}cache.row.push(c);for(var j=0;j<totalCells;++j){cols.push(parsers[j].format(getElementText(table.config,c[0].cells[j]),table,c[0].cells[j]));}cols.push(cache.normalized.length);cache.normalized.push(cols);cols=null;};if(table.config.debug){benchmark("Building cache for "+totalRows+" rows:",cacheTime);}return cache;};function getElementText(config,node){var text="";if(!node)return"";if(!config.supportsTextContent)config.supportsTextContent=node.textContent||false;if(config.textExtraction=="simple"){if(config.supportsTextContent){text=node.textContent;}else{if(node.childNodes[0]&&node.childNodes[0].hasChildNodes()){text=node.childNodes[0].innerHTML;}else{text=node.innerHTML;}}}else{if(typeof(config.textExtraction)=="function"){text=config.textExtraction(node);}else{text=$(node).text();}}return text;}function appendToTable(table,cache){if(table.config.debug){var appendTime=new Date()}var c=cache,r=c.row,n=c.normalized,totalRows=n.length,checkCell=(n[0].length-1),tableBody=$(table.tBodies[0]),rows=[];for(var i=0;i<totalRows;i++){var pos=n[i][checkCell];rows.push(r[pos]);if(!table.config.appender){var l=r[pos].length;for(var j=0;j<l;j++){tableBody[0].appendChild(r[pos][j]);}}}if(table.config.appender){table.config.appender(table,rows);}rows=null;if(table.config.debug){benchmark("Rebuilt table:",appendTime);}applyWidget(table);setTimeout(function(){$(table).trigger("sortEnd");},0);};function buildHeaders(table){if(table.config.debug){var time=new Date();}var meta=($.metadata)?true:false;var header_index=computeTableHeaderCellIndexes(table);$tableHeaders=$(table.config.selectorHeaders,table).each(function(index){this.column=header_index[this.parentNode.rowIndex+"-"+this.cellIndex];this.order=formatSortingOrder(table.config.sortInitialOrder);this.count=this.order;if(checkHeaderMetadata(this)||checkHeaderOptions(table,index))this.sortDisabled=true;if(checkHeaderOptionsSortingLocked(table,index))this.order=this.lockedOrder=checkHeaderOptionsSortingLocked(table,index);if(!this.sortDisabled){var $th=$(this).addClass(table.config.cssHeader);if(table.config.onRenderHeader)table.config.onRenderHeader.apply($th);}table.config.headerList[index]=this;});if(table.config.debug){benchmark("Built headers:",time);log($tableHeaders);}return $tableHeaders;};function computeTableHeaderCellIndexes(t){var matrix=[];var lookup={};var thead=t.getElementsByTagName('THEAD')[0];var trs=thead.getElementsByTagName('TR');for(var i=0;i<trs.length;i++){var cells=trs[i].cells;for(var j=0;j<cells.length;j++){var c=cells[j];var rowIndex=c.parentNode.rowIndex;var cellId=rowIndex+"-"+c.cellIndex;var rowSpan=c.rowSpan||1;var colSpan=c.colSpan||1 +var firstAvailCol;if(typeof(matrix[rowIndex])=="undefined"){matrix[rowIndex]=[];}for(var k=0;k<matrix[rowIndex].length+1;k++){if(typeof(matrix[rowIndex][k])=="undefined"){firstAvailCol=k;break;}}lookup[cellId]=firstAvailCol;for(var k=rowIndex;k<rowIndex+rowSpan;k++){if(typeof(matrix[k])=="undefined"){matrix[k]=[];}var matrixrow=matrix[k];for(var l=firstAvailCol;l<firstAvailCol+colSpan;l++){matrixrow[l]="x";}}}}return lookup;}function checkCellColSpan(table,rows,row){var arr=[],r=table.tHead.rows,c=r[row].cells;for(var i=0;i<c.length;i++){var cell=c[i];if(cell.colSpan>1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i<l;i++){getWidgetById(c[i]).format(table);}}function getWidgetById(name){var l=widgets.length;for(var i=0;i<l;i++){if(widgets[i].id.toLowerCase()==name.toLowerCase()){return widgets[i];}}};function formatSortingOrder(v){if(typeof(v)!="Number"){return(v.toLowerCase()=="desc")?1:0;}else{return(v==1)?1:0;}}function isValueInArray(v,a){var l=a.length;for(var i=0;i<l;i++){if(a[i][0]==v){return true;}}return false;}function setHeadersCss(table,$headers,list,css){$headers.removeClass(css[0]).removeClass(css[1]);var h=[];$headers.each(function(offset){if(!this.sortDisabled){h[this.column]=$(this);}});var l=list.length;for(var i=0;i<l;i++){h[list[i][0]].addClass(css[list[i][1]]);}}function fixColumnWidth(table,$headers){var c=table.config;if(c.widthFixed){var colgroup=$('<colgroup>');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('<col>').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i<l;i++){var s=sortList[i],o=c.headerList[s[0]];o.count=s[1];o.count++;}}function multisort(table,sortList,cache){if(table.config.debug){var sortTime=new Date();}var dynamicExp="var sortWrapper = function(a,b) {",l=sortList.length;for(var i=0;i<l;i++){var c=sortList[i][0];var order=sortList[i][1];var s=(table.config.parsers[c].type=="text")?((order==0)?makeSortFunction("text","asc",c):makeSortFunction("text","desc",c)):((order==0)?makeSortFunction("numeric","asc",c):makeSortFunction("numeric","desc",c));var e="e"+i;dynamicExp+="var "+e+" = "+s;dynamicExp+="if("+e+") { return "+e+"; } ";dynamicExp+="else { ";}var orgOrderCol=cache.normalized[0].length-1;dynamicExp+="return a["+orgOrderCol+"]-b["+orgOrderCol+"];";for(var i=0;i<l;i++){dynamicExp+="}; ";}dynamicExp+="return 0; ";dynamicExp+="}; ";if(table.config.debug){benchmark("Evaling expression:"+dynamicExp,new Date());}eval(dynamicExp);cache.normalized.sort(sortWrapper);if(table.config.debug){benchmark("Sorting on "+sortList.toString()+" and dir "+order+" time:",sortTime);}return cache;};function makeSortFunction(type,direction,index){var a="a["+index+"]",b="b["+index+"]";if(type=='text'&&direction=='asc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+a+" < "+b+") ? -1 : 1 )));";}else if(type=='text'&&direction=='desc'){return"("+a+" == "+b+" ? 0 : ("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : ("+b+" < "+a+") ? -1 : 1 )));";}else if(type=='numeric'&&direction=='asc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+a+" - "+b+"));";}else if(type=='numeric'&&direction=='desc'){return"("+a+" === null && "+b+" === null) ? 0 :("+a+" === null ? Number.POSITIVE_INFINITY : ("+b+" === null ? Number.NEGATIVE_INFINITY : "+b+" - "+a+"));";}};function makeSortText(i){return"((a["+i+"] < b["+i+"]) ? -1 : ((a["+i+"] > b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((a<b)?-1:((a>b)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((b<a)?-1:((b>a)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j<a.length;j++){if(a[j][0]!=i){config.sortList.push(a[j]);}}}config.sortList.push([i,this.order]);}else{if(isValueInArray(i,config.sortList)){for(var j=0;j<config.sortList.length;j++){var s=config.sortList[j],o=config.headerList[s[0]];if(s[0]==i){o.count=s[1];o.count++;s[1]=o.count%2;}}}else{config.sortList.push([i,this.order]);}};setTimeout(function(){setHeadersCss($this[0],$headers,config.sortList,sortCSS);appendToTable($this[0],multisort($this[0],config.sortList,cache));},1);return false;}}).mousedown(function(){if(config.cancelSelection){this.onselectstart=function(){return false};return false;}});$this.bind("update",function(){var me=this;setTimeout(function(){me.config.parsers=buildParserCache(me,$headers);cache=buildCache(me);},1);}).bind("updateCell",function(e,cell){var config=this.config;var pos=[(cell.parentNode.rowIndex-1),cell.cellIndex];cache.normalized[pos[0]][pos[1]]=config.parsers[pos[1]].format(getElementText(config,cell),cell);}).bind("sorton",function(e,list){$(this).trigger("sortStart");config.sortList=list;var sortList=config.sortList;updateHeaderSortCount(this,sortList);setHeadersCss(this,$headers,sortList,sortCSS);appendToTable(this,multisort(this,sortList,cache));}).bind("appendCache",function(){appendToTable(this,cache);}).bind("applyWidgetId",function(e,id){getWidgetById(id).format(this);}).bind("applyWidgets",function(){applyWidget(this);});if($.metadata&&($(this).metadata()&&$(this).metadata().sortlist)){config.sortList=$(this).metadata().sortlist;}if(config.sortList.length>0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i<l;i++){if(parsers[i].id.toLowerCase()==parser.id.toLowerCase()){a=false;}}if(a){parsers.push(parser);};};this.addWidget=function(widget){widgets.push(widget);};this.formatFloat=function(s){var i=parseFloat(s);return(isNaN(i))?0:i;};this.formatInt=function(s){var i=parseInt(s);return(isNaN(i))?0:i;};this.isDigit=function(s,config){return/^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g,'')));};this.clearTableBody=function(table){if($.browser.msie){function empty(){while(this.firstChild)this.removeChild(this.firstChild);}empty.apply(table.tBodies[0]);}else{table.tBodies[0].innerHTML="";}};}});$.fn.extend({tablesorter:$.tablesorter.construct});var ts=$.tablesorter;ts.addParser({id:"text",is:function(s){return true;},format:function(s){return $.trim(s.toLocaleLowerCase());},type:"text"});ts.addParser({id:"digit",is:function(s,table){var c=table.config;return $.tablesorter.isDigit(s,c);},format:function(s){return $.tablesorter.formatFloat(s);},type:"numeric"});ts.addParser({id:"currency",is:function(s){return/^[£$€?.]/.test(s);},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g),""));},type:"numeric"});ts.addParser({id:"ipAddress",is:function(s){return/^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s);},format:function(s){var a=s.split("."),r="",l=a.length;for(var i=0;i<l;i++){var item=a[i];if(item.length==2){r+="0"+item;}else{r+=item;}}return $.tablesorter.formatFloat(r);},type:"numeric"});ts.addParser({id:"url",is:function(s){return/^(https?|ftp|file):\/\/$/.test(s);},format:function(s){return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//),''));},type:"text"});ts.addParser({id:"isoDate",is:function(s){return/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s);},format:function(s){return $.tablesorter.formatFloat((s!="")?new Date(s.replace(new RegExp(/-/g),"/")).getTime():"0");},type:"numeric"});ts.addParser({id:"percent",is:function(s){return/\%$/.test($.trim(s));},format:function(s){return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g),""));},type:"numeric"});ts.addParser({id:"usLongDate",is:function(s){return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/));},format:function(s){return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"shortDate",is:function(s){return/\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s);},format:function(s,table){var c=table.config;s=s.replace(/\-/g,"/");if(c.dateFormat=="us"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$1/$2");}else if(c.dateFormat=="uk"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/,"$3/$2/$1");}else if(c.dateFormat=="dd/mm/yy"||c.dateFormat=="dd-mm-yy"){s=s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/,"$1/$2/$3");}return $.tablesorter.formatFloat(new Date(s).getTime());},type:"numeric"});ts.addParser({id:"time",is:function(s){return/^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s);},format:function(s){return $.tablesorter.formatFloat(new Date("2000/01/01 "+s).getTime());},type:"numeric"});ts.addParser({id:"metadata",is:function(s){return false;},format:function(s,table,cell){var c=table.config,p=(!c.parserMetadataName)?'sortValue':c.parserMetadataName;return $(cell).metadata()[p];},type:"numeric"});ts.addWidget({id:"zebra",format:function(table){if(table.config.debug){var time=new Date();}var $tr,row=-1,odd;$("tr:visible",table.tBodies[0]).each(function(i){$tr=$(this);if(!$tr.hasClass(table.config.cssChildRow))row++;odd=(row%2==0);$tr.removeClass(table.config.widgetZebra.css[odd?0:1]).addClass(table.config.widgetZebra.css[odd?1:0])});if(table.config.debug){$.tablesorter.benchmark("Applying Zebra widget",time);}}});})(jQuery); \ No newline at end of file diff --git a/web/modules/ultimate_cron/js/ultimate_cron.job.js b/web/modules/ultimate_cron/js/ultimate_cron.job.js new file mode 100644 index 0000000000000000000000000000000000000000..fa34358654a5189864129a71732b6f134e707080 --- /dev/null +++ b/web/modules/ultimate_cron/js/ultimate_cron.job.js @@ -0,0 +1,18 @@ +(function ($) { + 'use strict'; + + Drupal.behaviors.ultimateCronJobFieldsetSummaries = { + attach: function (context) { + $('#edit-settings-scheduler', context).drupalSetSummary(function (context) { + return $('#edit-settings-scheduler-name', context).find(':selected').text(); + }); + $('#edit-settings-launcher', context).drupalSetSummary(function (context) { + return $('#edit-settings-launcher-name', context).find(':selected').text(); + }); + $('#edit-settings-logger', context).drupalSetSummary(function (context) { + return $('#edit-settings-logger-name', context).find(':selected').text(); + }); + } + }; + +})(jQuery); diff --git a/web/modules/ultimate_cron/js/ultimate_cron.js b/web/modules/ultimate_cron/js/ultimate_cron.js new file mode 100644 index 0000000000000000000000000000000000000000..d463cc918846b94f96a6fbbd99bd2a913d761fd0 --- /dev/null +++ b/web/modules/ultimate_cron/js/ultimate_cron.js @@ -0,0 +1,9 @@ +jQuery(document).ready(function ($) { + 'use strict'; + + /* + setInterval(function() { + $("#ctools-export-ui-list-items-reload").click(); + }, 1000); + */ +}); diff --git a/web/modules/ultimate_cron/js/ultimate_cron.nodejs.js b/web/modules/ultimate_cron/js/ultimate_cron.nodejs.js new file mode 100644 index 0000000000000000000000000000000000000000..683b8b34005607d7a651834fca19fd8a29ead716 --- /dev/null +++ b/web/modules/ultimate_cron/js/ultimate_cron.nodejs.js @@ -0,0 +1,76 @@ +(function ($) { + 'use strict'; + Drupal.Nodejs.callbacks.nodejsUltimateCron = { + disabled: false, + runningJobs: {}, + callback: function (message) { + if (this.disabled) {return;} + var action = message.data.action; + var job = message.data.job; + var elements = message.data.elements; + + switch (action) { + case 'lock': + job.started = new Date().getTime(); + this.runningJobs[job.name] = job; + break; + + case 'unlock': + delete this.runningJobs[job.name]; + break; + + case 'progress': + if (!this.runningJobs[job.name]) { + $('#ctools-export-ui-list-items-reload').click(); + return; + } + break; + + } + + for (var key in elements) { + if (elements.hasOwnProperty(key)) { + var value = elements[key]; + $(key).replaceWith(value); + Drupal.attachBehaviors($(key)); + } + } + } + }; + + Drupal.behaviors.ultimateCronJobNodejs = { + attach: function (context) { + $('tr td.ctools-export-ui-status', context).each(function () { + var row = $(this).parent('tr'); + var name = $(row).attr('id'); + if ($(this).attr('title') === 'running') { + var duration = $('tr#' + name + ' td.ctools-export-ui-duration span.duration-time').attr('data-src'); + Drupal.Nodejs.callbacks.nodejsUltimateCron.runningJobs[name] = { + started: (new Date().getTime()) - (duration * 1000) + }; + } + else { + delete Drupal.Nodejs.callbacks.nodejsUltimateCron.runningJobs[name]; + } + }); + } + }; + + setInterval(function () { + var time = new Date().getTime(); + var jobs = Drupal.Nodejs.callbacks.nodejsUltimateCron.runningJobs; + + for (var name in jobs) { + if (jobs.hasOwnProperty(name)) { + var job = jobs[name]; + var date = new Date(time - job.started); + var minutes = '00' + date.getUTCMinutes(); + var seconds = '00' + date.getUTCSeconds(); + var formatted = minutes.substring(minutes.length - 2) + ':' + seconds.substring(seconds.length - 2); + $('tr#' + name + ' td.ctools-export-ui-duration .duration-time').html(formatted); + } + } + }, 1000); + +}(jQuery)); + diff --git a/web/modules/ultimate_cron/src/Annotation/LauncherPlugin.php b/web/modules/ultimate_cron/src/Annotation/LauncherPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..6af0ace6203abc91efb1eaeacf60fd8a1eae102e --- /dev/null +++ b/web/modules/ultimate_cron/src/Annotation/LauncherPlugin.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\ultimate_cron\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines a Launcher plugin annotation object. + * + * @Annotation + * + * @see \Drupal\ultimate_cron\LauncherManager + */ +class LauncherPlugin extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The human-readable title of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $title; + + /** + * A short description of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $description; + + + + public $weight = 0; + +} diff --git a/web/modules/ultimate_cron/src/Annotation/LoggerPlugin.php b/web/modules/ultimate_cron/src/Annotation/LoggerPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..85e16498977d84db67cca6dba350684f31069ace --- /dev/null +++ b/web/modules/ultimate_cron/src/Annotation/LoggerPlugin.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\ultimate_cron\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines a Logger plugin annotation object. + * + * @Annotation + * + * @see \Drupal\ultimate_cron\LoggerManager + */ +class LoggerPlugin extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The human-readable title of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $title; + + /** + * A short description of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $description; + +} diff --git a/web/modules/ultimate_cron/src/Annotation/SchedulerPlugin.php b/web/modules/ultimate_cron/src/Annotation/SchedulerPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..5642953eaaaa3168931f4a2b4ed8f727390777bf --- /dev/null +++ b/web/modules/ultimate_cron/src/Annotation/SchedulerPlugin.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\ultimate_cron\Annotation; + +use Drupal\Component\Annotation\Plugin; + +/** + * Defines a Scheduler plugin annotation object. + * + * @Annotation + * + * @see \Drupal\ultimate_cron\SchedulerManager + */ +class SchedulerPlugin extends Plugin { + + /** + * The plugin ID. + * + * @var string + */ + public $id; + + /** + * The human-readable title of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $title; + + /** + * A short description of the scheduler. + * + * @ingroup plugin_translatable + * + * @var \Drupal\Core\Annotation\Translation + */ + public $description; + +} diff --git a/web/modules/ultimate_cron/src/Commands/UltimateCronCommands.php b/web/modules/ultimate_cron/src/Commands/UltimateCronCommands.php new file mode 100644 index 0000000000000000000000000000000000000000..243c5c21e4f0dd535f38ee11d507625f6dc2d90d --- /dev/null +++ b/web/modules/ultimate_cron/src/Commands/UltimateCronCommands.php @@ -0,0 +1,428 @@ +<?php + +namespace Drupal\ultimate_cron\Commands; + +use Consolidation\OutputFormatters\StructuredData\RowsOfFields; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\Entity\CronJob; +use Drush\Commands\DrushCommands; + +/** + * Class UltimateCronCommands. + * + * @package Drupal\ultimate_cron\Commands + */ +class UltimateCronCommands extends DrushCommands { + + /** + * Logger object. + * + * @var \Drupal\Core\Logger\LoggerChannelInterface + */ + protected $logger; + + /** + * Constructs an UltimateCronCommands object. + * + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger + * Logger factory object. + */ + public function __construct(LoggerChannelFactoryInterface $logger) { + $this->logger = $logger->get('ultimate_cron'); + } + + /** + * Show a cron jobs logs. + * + * @param string $name + * Job to show logs for. + * @param array $options + * Options array. + * + * @command cron:logs + * + * @option limit Number of log entries to show + * @option compact Only show the first line of each log entry + * @usage drush cron-logs node_cron --limit=20 + * Show 20 last logs for the node_cron job + * @aliases cron-logs + * @format table + */ + public function logs($name, array $options = ['limit' => NULL, 'compact' => NULL]) { + if (!$name) { + throw new \Exception(dt('No job specified?')); + } + + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + $job = CronJob::load($name); + + if (!$job) { + throw new \Exception(dt('@name not found', ['@name' => $name])); + } + + $compact = $options['compact']; + $limit = $options['limit']; + $limit = $limit ? $limit : 10; + + $table = []; + $table[] = [ + '', + dt('Started'), + dt('Duration'), + dt('User'), + dt('Initial message'), + dt('Message'), + dt('Status'), + ]; + + $lock_id = $job->isLocked(); + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, $limit); + + /** @var \Drupal\ultimate_cron\Logger\LogEntry $log_entry */ + foreach ($log_entries as $log_entry) { + $progress = ''; + if ($log_entry->lid && $lock_id && $log_entry->lid === $lock_id) { + $progress = $job->getProgress(); + $progress = is_numeric($progress) ? sprintf(' (%d%%)', round($progress * 100)) : ''; + } + + $legend = ''; + if ($lock_id && $log_entry->lid == $lock_id) { + $legend .= 'R'; + list(, $status) = $job->getPlugin('launcher')->formatRunning($job); + } + elseif ($log_entry->start_time && !$log_entry->end_time) { + list(, $status) = $job->getPlugin('launcher')->formatUnfinished($job); + } + else { + list(, $status) = $log_entry->formatSeverity(); + } + + $table[$log_entry->lid][] = $legend; + $table[$log_entry->lid][] = $log_entry->formatStartTime(); + $table[$log_entry->lid][] = $log_entry->formatDuration() . $progress; + $table[$log_entry->lid][] = $log_entry->formatUser(); + if ($compact) { + $table[$log_entry->lid][] = trim(reset(explode("\n", $log_entry->init_message)), "\n"); + $table[$log_entry->lid][] = trim(reset(explode("\n", $log_entry->message)), "\n"); + } + else { + $table[$log_entry->lid][] = trim($log_entry->init_message, "\n"); + $table[$log_entry->lid][] = trim($log_entry->message, "\n"); + } + $table[$log_entry->lid][] = $status; + } + + return new RowsOfFields($table); + } + + /** + * List cron jobs. + * + * @param array $options + * Options array. + * + * @command cron:list + * @option module Comma separated list of modules to show jobs from + * @option enabled Show enabled jobs + * @option disabled Show enabled jobs + * @option behind Show jobs that are behind schedule + * @option status Comma separated list of statuses to show jobs from + * @option extended Show extended information + * @option name Show name instead of title + * @option scheduled Show scheduled jobs + * @usage drush cron-list --status=running --module=node + * Show jobs from the node module that are currently running + * @aliases crl cron-list + * @format table + */ + public function cronList( + array $options = [ + 'module' => NULL, + 'enabled' => NULL, + 'disabled' => NULL, + 'behind' => NULL, + 'status' => NULL, + 'extended' => NULL, + 'name' => NULL, + 'scheduled' => NULL, + ] + ) { + $modules = $options['module']; + $enabled = $options['enabled']; + $disabled = $options['disabled']; + $behind = $options['behind']; + $extended = $options['extended']; + $statuses = $options['status']; + $scheduled = $options['scheduled']; + $showname = $options['name']; + + $modules = $modules ? explode(',', $modules) : []; + $statuses = $statuses ? explode(',', $statuses) : []; + + $title = $showname ? dt('Name') : dt('Title'); + + $table = []; + $table[] = [ + '', + dt('ID'), + dt('Module'), + $title, + dt('Scheduled'), + dt('Started'), + dt('Duration'), + dt('Status'), + ]; + + $print_legend = FALSE; + + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + foreach (CronJob::loadMultiple() as $name => $job) { + if ($modules && !in_array($job->getModule(), $modules)) { + continue; + } + + if ($enabled && FALSE === $job->status()) { + continue; + } + + if ($disabled && TRUE === $job->status()) { + continue; + } + + if ($scheduled && !$job->isScheduled()) { + continue; + } + + $legend = ''; + + if (FALSE === $job->status()) { + $legend .= 'D'; + $print_legend = TRUE; + } + + $lock_id = $job->isLocked(); + $log_entry = $job->loadLogEntry($lock_id); + + if ($time = $job->isBehindSchedule()) { + $legend .= 'B'; + $print_legend = TRUE; + } + + if ($behind && !$time) { + continue; + } + + if ($lock_id && $log_entry->lid == $lock_id) { + $legend .= 'R'; + list(, $status) = $job->getPlugin('launcher')->formatRunning($job); + $print_legend = TRUE; + } + elseif ($log_entry->start_time && !$log_entry->end_time) { + list(, $status) = $job->getPlugin('launcher')->formatUnfinished($job); + } + else { + list(, $status) = $log_entry->formatSeverity(); + } + + if ($statuses && !in_array($status, $statuses)) { + continue; + } + + $progress = $lock_id ? $job->formatProgress() : ''; + + $table[$name][] = $legend; + $table[$name][] = $job->id(); + $table[$name][] = $job->getModuleName(); + $table[$name][] = $showname ? $job->id() : $job->getTitle(); + $table[$name][] = $job->getPlugin('scheduler')->formatLabel($job); + $table[$name][] = $log_entry->formatStartTime(); + $table[$name][] = $log_entry->formatDuration() . ' ' . $progress; + $table[$name][] = $status; + + if ($extended) { + $table['extended:' . $name][] = ''; + $table['extended:' . $name][] = ''; + $table['extended:' . $name][] = $job->id(); + $table['extended:' . $name][] = $job->getPlugin('scheduler')->formatLabelVerbose($job); + $table['extended:' . $name][] = $log_entry->init_message; + $table['extended:' . $name][] = $log_entry->message; + } + } + + if ($print_legend) { + $this->output->writeln("\n" . dt('Legend: D = Disabled, R = Running, B = Behind schedule')); + } + + return new RowsOfFields($table); + } + + /** + * Run cron job. + * + * @param string $name + * Job to run. + * @param array $options + * Options array. + * + * @command cron:run + * + * @option force Skip the schedule check for each job. Locks are still respected. + * @option options Custom options for plugins, e.g. --options=thread=1 for serial launcher + * @usage drush cron-run node_cron + * Run the node_cron job + * @aliases crun cron-run + */ + public function run($name = NULL, array $options = ['force' => NULL, 'options' => NULL]) { + if ($o = $options['options']) { + $pairs = explode(',', $o); + foreach ($pairs as $pair) { + list($key, $value) = explode('=', $pair); + CronPlugin::setGlobalOption(trim($key), trim($value)); + } + } + + $force = $options['force']; + + if (!$name) { + throw new \Exception(dt("Running all cronjobs is not supported by Ultimate Cron's cron:run - please use Drupal Core's core:cron command!")); + } + + // Run a specific job. + $job = CronJob::load($name); + + if (!$job) { + throw new \Exception(dt('@name not found', ['@name' => $name])); + } + + if ($force || $job->isScheduled()) { + $job->run(t('Launched by drush')); + } + + } + + /** + * Enable cron job. + * + * @param string $name + * Job to enable. + * @param array $options + * Options array. + * + * @command cron:enable + * + * @option all Enabled all jobs + * @usage drush cron-enable node_cron + * Enable the node_cron job + * @aliases cre cron-enable + */ + public function enable($name, array $options = ['all' => NULL]) { + if (!$name) { + if (!$options['all']) { + throw new \Exception(dt('No job specified?')); + } + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + foreach (CronJob::loadMultiple() as $job) { + $job->enable()->save(); + } + return; + } + + $job = CronJob::load($name); + if ($job->enable()->save()) { + $this->output->writeln(dt('@name enabled', ['@name' => $name])); + } + } + + /** + * Disable cron job. + * + * @param string $name + * Job to disable. + * @param array $options + * Options array. + * + * @command cron:disable + * + * @option all Enabled all jobs + * @usage drush cron-disable node_cron + * Disable the node_cron job + * @aliases crd cron-disable + */ + public function disable($name, array $options = ['all' => NULL]) { + if (!$name) { + if (!$options['all']) { + throw new \Exception(dt('No job specified?')); + } + foreach (CronJob::loadMultiple() as $job) { + $job->disable()->save(); + } + return; + } + + $job = CronJob::load($name); + if ($job->disable()->save()) { + $this->output->writeln(dt('@name disabled', ['@name' => $name])); + } + } + + /** + * Unlock cron job. + * + * @param string $name + * Job to unlock. + * @param array $options + * Options array. + * + * @command cron:unlock + * + * @option all Enabled all jobs + * @usage drush cron-unlock node_cron + * Unlock the node_cron job + * @aliases cru cron-unlock + */ + public function unlock($name, array $options = ['all' => NULL]) { + if (!$name) { + if (!$options['all']) { + throw new \Exception(dt('No job specified?')); + } + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + foreach (CronJob::loadMultiple() as $job) { + if ($job->isLocked()) { + $job->unlock(); + } + } + return; + } + + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + $job = CronJob::load($name); + if (!$job) { + throw new \Exception(dt('@name not found', ['@name' => $name])); + } + + $lock_id = $job->isLocked(); + if (!$lock_id) { + throw new \Exception(dt('@name is not running', ['@name' => $name])); + } + + // Unlock the process. + if ($job->unlock($lock_id, TRUE)) { + $log_entry = $job->resumeLog($lock_id); + global $user; + $this->logger->warning('@name manually unlocked by user @username (@uid)', [ + '@name' => $job->id(), + '@username' => $user->getDisplayName(), + '@uid' => $user->id(), + ]); + $log_entry->finish(); + + $this->output->writeln(dt('Cron job @name unlocked', ['@name' => $name])); + } + else { + throw new \Exception(dt('Could not unlock cron job @name', ['@name' => $name])); + } + } + +} diff --git a/web/modules/ultimate_cron/src/Controller/JobController.php b/web/modules/ultimate_cron/src/Controller/JobController.php new file mode 100644 index 0000000000000000000000000000000000000000..2ce189df3607265dbfadc654f1789f854661d842 --- /dev/null +++ b/web/modules/ultimate_cron/src/Controller/JobController.php @@ -0,0 +1,124 @@ +<?php + +namespace Drupal\ultimate_cron\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * A controller to interact with CronJob entities. + */ +class JobController extends ControllerBase { + + /** + * Runs a single cron job. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $ultimate_cron_job + * The cron job which will be run. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * Redirects to the job listing after running a job. + */ + public function runCronJob(CronJob $ultimate_cron_job) { + $ultimate_cron_job->run($this->t('Launched manually')); + $this->messenger() + ->addStatus($this->t('Cron job @job_label (@module) was successfully run.', [ + '@job_label' => $ultimate_cron_job->label(), + '@module' => $ultimate_cron_job->getModuleName(), + ])); + return $this->redirect('entity.ultimate_cron_job.collection'); + } + + /** + * Discovers new default cron jobs. + */ + public function discoverJobs() { + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); + $this->messenger() + ->addStatus($this->t('Completed discovery for new cron jobs.')); + return $this->redirect('entity.ultimate_cron_job.collection'); + } + + /** + * Displays a detailed cron job logs table. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $ultimate_cron_job + * The cron job which will be run. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function showLogs(CronJob $ultimate_cron_job) { + + $header = array( + $this->t('Severity'), + $this->t('Start Time'), + $this->t('End Time'), + $this->t('Message'), + $this->t('Duration'), + ); + + $build['ultimate_cron_job_logs_table'] = [ + '#type' => 'table', + '#header' => $header, + '#empty' => $this->t('No log information available.'), + ]; + + $log_entries = $ultimate_cron_job->getLogEntries(); + foreach ($log_entries as $log_entry) { + list($status, $title) = $log_entry->formatSeverity(); + $title = $log_entry->message ? $log_entry->message : $title; + + $row = []; + $row['severity'] = $status; + $row['severity']['#wrapper_attributes']['title'] = strip_tags($title); + $row['start_time']['#markup'] = $log_entry->formatStartTime(); + $row['end_time']['#markup'] = $log_entry->formatEndTime(); + $row['message']['#markup'] = $log_entry->message ?: $log_entry->formatInitMessage(); + $row['duration']['#markup'] = $log_entry->formatDuration(); + + $build['ultimate_cron_job_logs_table'][] = $row; + } + $build['#title'] = $this->t('Logs for %label', ['%label' => $ultimate_cron_job->label()]); + return $build; + + } + + /** + * Unlocks a single cron job. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $ultimate_cron_job + * The cron job which will be unlocked. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * Redirects to the job listing after running a job. + */ + public function unlockCronJob(CronJob $ultimate_cron_job) { + $lock_id = $ultimate_cron_job->isLocked(); + $name = $ultimate_cron_job->label(); + + // Unlock the process. + if ($ultimate_cron_job->unlock($lock_id, TRUE)) { + $user = \Drupal::currentUser(); + \Drupal::logger('ultimate_cron')->warning('@name manually unlocked by user @username (@uid)', array( + '@name' => $ultimate_cron_job->id(), + '@username' => $user->getDisplayName(), + '@uid' => $user->id(), + )); + + $this->messenger() + ->addStatus($this->t('Cron job @name unlocked successfully.', [ + '@name' => $name, + ])); + } + else { + $this->messenger() + ->addError($this->t('Could not unlock cron job @name', [ + '@name' => $name, + ])); + } + + return $this->redirect('entity.ultimate_cron_job.collection'); + } +} diff --git a/web/modules/ultimate_cron/src/CronJobAccessControlHandler.php b/web/modules/ultimate_cron/src/CronJobAccessControlHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..3f0015fe792df0fad717bdc710a8ff198a33b89d --- /dev/null +++ b/web/modules/ultimate_cron/src/CronJobAccessControlHandler.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Session\AccountInterface; + +/** + * Defines a class to check whether a cron job is valid and should be deletable. + */ +class CronJobAccessControlHandler extends EntityAccessControlHandler { + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + if ($operation === 'delete') { + if (!$entity->isValid()) { + return AccessResult::allowedIfHasPermission($account, 'administer ultimate cron'); + } + return AccessResult::forbidden(); + } + if ($operation === 'update') { + if ($entity->isValid()) { + return AccessResult::allowedIfHasPermission($account, 'administer ultimate cron'); + } + return AccessResult::forbidden(); + } + return parent::checkAccess($entity, $operation, $account); + } +} diff --git a/web/modules/ultimate_cron/src/CronJobDiscovery.php b/web/modules/ultimate_cron/src/CronJobDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..0f8a0d6a312acbdf4f3b0a27d270338bdfe2cf7c --- /dev/null +++ b/web/modules/ultimate_cron/src/CronJobDiscovery.php @@ -0,0 +1,201 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Extension\ModuleExtensionList; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Queue\QueueWorkerManagerInterface; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Discovery and instantiation of default cron jobs. + */ +class CronJobDiscovery { + + /** + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * @var \Drupal\Core\Queue\QueueWorkerManagerInterface + */ + protected $queueManager; + + /** + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The module extension list service. + * + * @var \Drupal\Core\Extension\ModuleExtensionList + */ + protected $moduleExtensionList; + + /** + * CronJobDiscovery constructor. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Queue\QueueWorkerManagerInterface $queue_manager + * The queue manager. + * @param \Drupal\Core\Extension\ModuleExtensionList $module_extension_list + * The module extension list service. + */ + public function __construct(ModuleHandlerInterface $module_handler, QueueWorkerManagerInterface $queue_manager, ConfigFactoryInterface $config_factory, ModuleExtensionList $module_extension_list) { + $this->moduleHandler = $module_handler; + $this->queueManager = $queue_manager; + $this->configFactory = $config_factory; + $this->moduleExtensionList = $module_extension_list; + } + + /** + * Automatically discovers and creates default cron jobs. + */ + public function discoverCronJobs() { + // Create cron jobs for hook_cron() implementations. + foreach ($this->getHooks() as $id => $info) { + $this->ensureCronJobExists($info, $id); + } + + if (!$this->configFactory->get('ultimate_cron.settings')->get('queue.enabled')) { + return; + } + + // Create cron jobs for queue plugins. + foreach ($this->queueManager->getDefinitions() as $id => $definition) { + if (!isset($definition['cron'])) { + continue; + } + + $job_id = str_replace(':', '__', CronJobInterface::QUEUE_ID_PREFIX . $id); + if (!CronJob::load($job_id)) { + $values = [ + 'title' => t('Queue: @title', ['@title' => $definition['title']]), + 'id' => $job_id, + 'module' => $definition['provider'], + // Process queue jobs later by default. + 'weight' => 10, + 'callback' => 'ultimate_cron.queue_worker:queueCallback', + 'scheduler' => [ + 'id' => 'simple', + 'configuration' => [ + 'rules' => ['* * * * *'], + ], + ] + ]; + + $job = CronJob::create($values); + $job->save(); + } + } + } + + /** + * Creates a new cron job with specific values. + * + * @param array $info + * Module info. + * @param string $id + * Module name. + */ + protected function ensureCronJobExists($info, $id) { + $job = NULL; + if (!CronJob::load($id)) { + $values = array( + 'title' => $this->getJobTitle($id), + 'id' => $id, + 'module' => $info['module'], + 'callback' => $info['callback'], + ); + + $job = CronJob::create($values); + + $job->save(); + } + } + + /** + * Returns the job title for a given ID. + * + * @param string $id + * The default cron job ID. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + * The default job title. + */ + protected function getJobTitle($id) { + $titles = array(); + + $titles['comment_cron'] = t('Store the maximum possible comments per thread'); + $titles['dblog_cron'] = t('Remove expired log messages and flood control events'); + $titles['field_cron'] = t('Purges deleted Field API data'); + $titles['file_cron'] = t('Deletes temporary files'); + $titles['history_cron'] = t('Deletes history'); + $titles['search_cron'] = t('Updates indexable active search pages'); + $titles['system_cron'] = t('Cleanup (caches, batch, flood, temp-files, etc.)'); + $titles['update_cron'] = t('Update indexes'); + $titles['node_cron'] = t('Updates search rankings for nodes'); + $titles['aggregator_cron'] = t('Refresh feeds'); + $titles['ultimate_cron_cron'] = t('Runs internal cleanup operations'); + $titles['statistics_cron'] = t('Reset counts and clean up'); + $titles['tracker_cron'] = t('Update tracker index'); + + if (isset($titles[$id])) { + return $titles[$id]; + } + return t('Default cron handler'); + } + + /** + * Get all cron hooks defined. + * + * @return array + * All hook definitions available. + */ + protected function getHooks() { + $hooks = array(); + // Generate list of jobs provided by modules. + $modules = array_keys($this->moduleHandler->getModuleList()); + foreach ($modules as $module) { + $hooks += $this->getModuleHooks($module); + } + + return $hooks; + } + + /** + * Get cron hooks declared by a module. + * + * @param string $module + * Name of module. + * + * @return array + * Hook definitions for the specified module. + */ + protected function getModuleHooks($module) { + $items = array(); + + // Add hook_cron() if applicable. + if ($this->moduleHandler->implementsHook($module, 'cron')) { + $info = $this->moduleExtensionList->getExtensionInfo($module); + $callback = "{$module}_cron"; + $items[$callback] = array( + 'module' => $module, + 'title' => isset($titles[$callback]) ? $titles[$callback] : 'Default cron handler', + 'configure' => empty($info['configure']) ? NULL : $info['configure'], + 'callback' => $callback, + 'tags' => array(), + 'pass job argument' => FALSE, + ); + $items["{$module}_cron"]['tags'][] = 'core'; + } + + return $items; + } + +} + diff --git a/web/modules/ultimate_cron/src/CronJobInterface.php b/web/modules/ultimate_cron/src/CronJobInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..7dc4daefac935084f96589ba7c0d3857dea0d4fe --- /dev/null +++ b/web/modules/ultimate_cron/src/CronJobInterface.php @@ -0,0 +1,356 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\ultimate_cron\Logger\LogEntry; +use Drupal\ultimate_cron\Logger\LoggerBase; + +interface CronJobInterface extends ConfigEntityInterface { + + /** + * Cron job ID prefix for queue jobs. + */ + const QUEUE_ID_PREFIX = 'ultimate_cron_queue_'; + + /** + * Get locked state for multiple jobs. + * + * @param array $jobs + * Jobs to check locks for. + */ + public static function isLockedMultiple($jobs); + + /** + * Load latest log entries. + * + * @param array $jobs + * Jobs to load log entries for. + * + * @return array + * Array of UltimateCronLogEntry objects. + */ + public static function loadLatestLogEntries($jobs, $log_types = array(ULTIMATE_CRON_LOG_TYPE_NORMAL)); + + /** + * Get multiple job progresses. + * + * @param array $jobs + * Jobs to get progress for. + * + * @return array + * Progress of jobs, keyed by job name. + */ + public static function getProgressMultiple($jobs); + + /** + * Gets the title of the created cron job. + * + * @return mixed + * Cron job title. + */ + public function getTitle(); + + /** + * Gets the cron job callback string. + * + * @return string + * Callback string. + */ + public function getCallback(); + + /** + * Gets the cron job module name used for the callback string. + * + * @return string + * Module name. + */ + public function getModule(); + + /** + * Gets scheduler array which holds info about the scheduler settings. + * + * @return array + * Scheduler settings + */ + public function getSchedulerId(); + + /** + * Gets launcher array which holds info about the launcher settings. + * + * @return array + * Launcher settings + */ + public function getLauncherId(); + + /** + * Gets logger array which holds info about the logger settings. + * + * @return array + * Logger settings + */ + public function getLoggerId(); + + /** + * Sets the title of the created cron job. + * + * @param $title + * @return mixed + * Cron job title. + */ + public function setTitle($title); + + /** + * Sets the cron job callback string. + * + * @param $callback + * @return string + * Callback string. + */ + public function setCallback($callback); + + /** + * Sets the cron job module name used for the callback string. + * + * @param $module + * @return string + * Module name. + */ + public function setModule($module); + + /** + * Sets scheduler array which holds info about the scheduler settings. + * + * @param $scheduler_id + * @return array + * Scheduler settings + */ + public function setSchedulerId($scheduler_id); + + /** + * Sets launcher array which holds info about the launcher settings. + * + * @param $launcher_id + * @return array + * Launcher settings + */ + public function setLauncherId($launcher_id); + + /** + * Sets logger array which holds info about the logger settings. + * + * @param $logger_id + * @return array + * Logger settings + */ + public function setLoggerId($logger_id); + + /** + * Check if the cron job is callable. + * + * @return bool + * TRUE if the job is callable, FALSE otherwise. + */ + public function isValid(); + + /** + * Get a signal without affecting it. + * + * @see UltimateCronSignal::peek() + */ + public function peekSignal($signal); + + /** + * Get a signal and clear it if found. + * + * @see UltimateCronSignal::get() + */ + public function getSignal($signal); + + /** + * Send a signal. + * + * @see UltimateCronSignal::set() + */ + public function sendSignal($signal, $persist = FALSE); + + /** + * Clear a signal. + * + * @see UltimateCronSignal::clear() + */ + public function clearSignal($signal); + + /** + * Send all signal for the job. + * + * @see UltimateCronSignal::flush() + */ + public function clearSignals(); + + /** + * Check job schedule. + */ + public function isScheduled(); + + /** + * Check if job is behind its schedule. + */ + public function isBehindSchedule(); + + /** + * Lock job. + */ + public function lock(); + + /** + * Unlock job. + * + * @param string $lock_id + * The lock id to unlock. + * @param boolean $manual + * Whether or not this is a manual unlock. + */ + public function unlock($lock_id = NULL, $manual = FALSE); + + /** + * Get locked state of job. + */ + public function isLocked(); + + /** + * Run job. + * + * @param string $init_message + * (optional) The launch message. If left NULL, a default message will be + * displayed. + * + * @return bool + * TRUE if the job is ran, FALSE otherwise. + */ + public function run($init_message = NULL); + + /** + * Get log entries. + * + * @param integer $limit + * (optional) Number of log entries per page. + * + * @return array + * Array of UltimateCronLogEntry objects. + */ + public function getLogEntries($log_types = ULTIMATE_CRON_LOG_TYPE_ALL, $limit = 10); + + /** + * Load log entry. + * + * @param string $lock_id + * The lock id of the log entry. + * + * @return LogEntry + * The log entry. + */ + public function loadLogEntry($lock_id); + + /** + * Load latest log. + * + * @return LogEntry + * The latest log entry for this job. + */ + public function loadLatestLogEntry($log_types = array(ULTIMATE_CRON_LOG_TYPE_NORMAL)); + + /** + * Start logging. + * + * @param string $lock_id + * The lock id to use. + * @param string $init_message + * Initial message for the log. + * + * @return \Drupal\ultimate_cron\Logger\LogEntry + * The log object. + */ + public function startLog($lock_id, $init_message = '', $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL); + + /** + * Resume a previosly saved log. + * + * @param string $lock_id + * The lock id of the log to resume. + * + * @return LogEntry + * The log entry object. + */ + public function resumeLog($lock_id); + + /** + * Get module name for this job. + */ + public function getModuleName(); + + /** + * Get module description for this job. + */ + public function getModuleDescription(); + + /** + * Initialize progress. + */ + public function initializeProgress(); + + /** + * Finish progress. + */ + public function finishProgress(); + + /** + * Get job progress. + * + * @return float + * The progress of this job. + */ + public function getProgress(); + + /** + * Set job progress. + * + * @param float $progress + * The progress (0 - 1). + */ + public function setProgress($progress); + + /** + * Format progress. + * + * @param float $progress + * (optional) The progress to format. Uses the progress on the object + * if not specified. + * + * @return string + * Formatted progress. + */ + public function formatProgress($progress = NULL); + + /** + * Get a "unique" id for a job. + */ + public function getUniqueID(); + + /** + * Get job plugin. + * + * If no plugin name is provided current plugin of the specified type will + * be returned. + * + * @param string $plugin_type + * Name of plugin type. + * @param string $name + * (optional) The name of the plugin. + * + * @return \Drupal\ultimate_cron\CronPlugin + * Plugin instance of the specified type. + */ + public function getPlugin($plugin_type, $name = NULL); + +} diff --git a/web/modules/ultimate_cron/src/CronJobListBuilder.php b/web/modules/ultimate_cron/src/CronJobListBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..e6e9c72854423fd03b93dc07607e4397a21f285f --- /dev/null +++ b/web/modules/ultimate_cron/src/CronJobListBuilder.php @@ -0,0 +1,133 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Config\Entity\DraggableListBuilder; +use Drupal\Core\Entity\EntityInterface; + +/** + * Defines a class to build a listing of cron jobs. + * + * @see \Drupal\ultimate_cron\Entity\CronJob + */ +class CronJobListBuilder extends DraggableListBuilder { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ultimate_cron_job_list'; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header = array(); + $header['label'] = array('data' => t('Title')); + $header['module'] = array('data' => t('Module')); + $header['scheduled'] = array('data' => t('Scheduled')); + $header['started'] = array('data' => t('Last Run')); + $header['duration'] = array('data' => t('Duration')); + $header['status'] = array('data' => t('Status')); + return $header + parent::buildHeader(); + } + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var \Drupal\ultimate_cron\CronJobInterface $entity */ + $icon = drupal_get_path('module', 'ultimate_cron') . '/icons/hourglass.png'; + $behind_icon = ['#prefix' => ' ', '#theme' => 'image', '#uri' => file_create_url($icon), '#title' => t('Job is behind schedule!')]; + + $log_entry = $entity->loadLatestLogEntry(); + $row['label'] = $entity->label(); + $row['module']['#markup'] = $entity->getModuleName(); + $row['module']['#wrapper_attributes']['title'] = $entity->getModuleDescription(); + $row['scheduled']['label']['#markup'] = $entity->getPlugin('scheduler')->formatLabel($entity); + if ($entity->isScheduled()) { + $row['scheduled']['behind'] = $behind_icon; + } + // If the start time is 0, the jobs have never been run. + $row['started']['#markup'] = $log_entry->start_time ? \Drupal::service('date.formatter')->format($log_entry->start_time, "short") : $this->t('Never'); + + // Display duration + $progress = $entity->isLocked() ? $entity->formatProgress() : ''; + $row['duration'] = [ + '#markup' => '<span class="duration-time" data-src="' . $log_entry->getDuration() . '">' . $log_entry->formatDuration() . '</span> <span class="duration-progress">' . $progress . '</span>', + '#wrapper_attributes' => ['title' => $log_entry->formatEndTime()], + ]; + + if (!$entity->isValid()) { + $row['status']['#markup'] = $this->t('Missing'); + } + elseif (!$entity->status()) { + $row['status']['#markup'] = $this->t('Disabled'); + } + else { + // Get the status from the launcher when running, otherwise use the last + // log entry. + if ($entity->isLocked() && $log_entry->lid == $entity->isLocked()) { + list($status, $title) = $entity->getPlugin('launcher')->formatRunning($entity); + } + elseif ($log_entry->start_time && !$log_entry->end_time) { + list($status, $title) = $entity->getPlugin('launcher')->formatUnfinished($entity); + } + else { + list($status, $title) = $log_entry->formatSeverity(); + $title = $log_entry->message ? $log_entry->message : $title; + } + + $row['status'] = $status; + $row['status']['#wrapper_attributes']['title'] = $title; + } + + $row += parent::buildRow($entity); + $row['weight']['#delta'] = 50; + return $row; + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + if ($entity->status() && $entity->isValid()) { + if (!$entity->isLocked()) { + $operations += [ + 'run' => [ + 'title' => t('Run'), + 'weight' => 9, + 'url' => $entity->toUrl('run'), + ] + ]; + } + else { + $operations += [ + 'unlock' => [ + 'title' => t('Unlock'), + 'weight' => 9, + 'url' => $entity->toUrl('unlock'), + ] + ]; + } + } + + $operations += [ + 'logs' => [ + 'title' => t('Logs'), + 'weight' => 10, + 'url' => $entity->toUrl('logs'), + ], + ]; + + // Invalid jobs can not be enabled nor disabled. + if (!$entity->isValid()) { + unset($operations['disable']); + unset($operations['enable']); + } + + return $operations; + } + +} diff --git a/web/modules/ultimate_cron/src/CronPlugin.php b/web/modules/ultimate_cron/src/CronPlugin.php new file mode 100644 index 0000000000000000000000000000000000000000..7d9863617e7a8aaf8761c786dae2282ea4563337 --- /dev/null +++ b/web/modules/ultimate_cron/src/CronPlugin.php @@ -0,0 +1,272 @@ +<?php + +namespace Drupal\ultimate_cron; +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Plugin\PluginFormInterface; + +/** + * This is the base class for all Ultimate Cron plugins. + * + * This class handles all the load/save settings for a plugin as well as the + * forms, etc. + */ +class CronPlugin extends PluginBase implements PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface, PluginFormInterface { + static public $multiple = FALSE; + static public $instances = array(); + public $weight = 0; + static public $globalOptions = array(); + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->setConfiguration($configuration); + } + + /** + * Returns a list of plugin types. + * + * @return array + */ + public static function getPluginTypes() { + return array( + 'scheduler' => t('Scheduler'), + 'launcher' => t('Launcher'), + 'logger' => t('Logger') + ); + } + + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = array_merge( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return array(); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + + } + + /** + * Get global plugin option. + * + * @param string $name + * Name of global plugin option to get. + * + * @return mixed + * Value of option if any, NULL if not found. + */ + static public function getGlobalOption($name) { + return isset(static::$globalOptions[$name]) ? static::$globalOptions[$name] : NULL; + } + + /** + * Get all global plugin options. + * + * @return array + * All options currently set, keyed by name. + */ + static public function getGlobalOptions() { + return static::$globalOptions; + } + + /** + * Set global plugin option. + * + * @param string $name + * Name of global plugin option to get. + * @param string $value + * The value to give it. + */ + static public function setGlobalOption($name, $value) { + static::$globalOptions[$name] = $value; + } + + /** + * Get label for a specific setting. + */ + public function settingsLabel($name, $value) { + if (is_array($value)) { + return implode(', ', $value); + } + else { + return $value; + } + } + + /** + * Default plugin valid for all jobs. + */ + public function isValid($job = NULL) { + return TRUE; + } + + /** + * Modified version drupal_array_get_nested_value(). + * + * Removes the specified parents leaf from the array. + * + * @param array $array + * Nested associative array. + * @param array $parents + * Array of key names forming a "path" where the leaf will be removed + * from $array. + */ + public function drupal_array_remove_nested_value(array &$array, array $parents) { + $ref = & $array; + $last_parent = array_pop($parents); + foreach ($parents as $parent) { + if (is_array($ref) && array_key_exists($parent, $ref)) { + $ref = & $ref[$parent]; + } + else { + return; + } + } + unset($ref[$last_parent]); + } + + /** + * Clean form of empty fallback values. + */ + public function cleanForm($elements, &$values, $parents) { + if (empty($elements)) { + return; + } + + foreach (element_children($elements) as $child) { + if (empty($child) || empty($elements[$child]) || is_numeric($child)) { + continue; + } + // Process children. + $this->cleanForm($elements[$child], $values, $parents); + + // Determine relative parents. + $rel_parents = array_diff($elements[$child]['#parents'], $parents); + $key_exists = NULL; + $value = drupal_array_get_nested_value($values, $rel_parents, $key_exists); + + // Unset when applicable. + if (!empty($elements[$child]['#markup'])) { + static::drupal_array_remove_nested_value($values, $rel_parents); + } + elseif ( + $key_exists && + empty($value) && + !empty($elements[$child]['#fallback']) && + $value !== '0' + ) { + static::drupal_array_remove_nested_value($values, $rel_parents); + } + } + } + + /** + * Process fallback form parameters. + * + * @param array $elements + * Elements to process. + * @param array $defaults + * Default values to add to description. + * @param bool $remove_non_fallbacks + * If TRUE, non fallback elements will be removed. + */ + public function fallbackalize(&$elements, &$values, $defaults, $remove_non_fallbacks = FALSE) { + if (empty($elements)) { + return; + } + foreach (element_children($elements) as $child) { + $element = & $elements[$child]; + if (empty($element['#tree'])) { + $param_values = & $values; + $param_defaults = & $defaults; + } + else { + $param_values = & $values[$child]; + $param_defaults = & $defaults[$child]; + } + $this->fallbackalize($element, $param_values, $param_defaults, $remove_non_fallbacks); + + if (empty($element['#type']) || $element['#type'] == 'fieldset') { + continue; + } + + if (!empty($element['#fallback'])) { + if (!$remove_non_fallbacks) { + if ($element['#type'] == 'radios') { + $label = $this->settingsLabel($child, $defaults[$child]); + $element['#options'] = array( + '' => t('Default (@default)', array('@default' => $label)), + ) + $element['#options']; + } + elseif ($element['#type'] == 'select' && empty($element['#multiple'])) { + $label = $this->settingsLabel($child, $defaults[$child]); + $element['#options'] = array( + '' => t('Default (@default)', array('@default' => $label)), + ) + $element['#options']; + } + elseif ($defaults[$child] !== '') { + $element['#description'] .= ' ' . t('(Blank = @default).', array('@default' => $this->settingsLabel($child, $defaults[$child]))); + } + unset($element['#required']); + } + } + elseif (!empty($element['#type']) && $remove_non_fallbacks) { + unset($elements[$child]); + } + elseif (!isset($element['#default_value']) || $element['#default_value'] === '') { + $empty = $element['#type'] == 'checkbox' ? FALSE : ''; + $values[$child] = !empty($defaults[$child]) ? $defaults[$child] : $empty; + $element['#default_value'] = $values[$child]; + } + } + } + +} diff --git a/web/modules/ultimate_cron/src/CronPluginMultiple.php b/web/modules/ultimate_cron/src/CronPluginMultiple.php new file mode 100644 index 0000000000000000000000000000000000000000..ca8f9d5955c0a162e1b51231d388508fb6f4c057 --- /dev/null +++ b/web/modules/ultimate_cron/src/CronPluginMultiple.php @@ -0,0 +1,117 @@ +<?php + +namespace Drupal\ultimate_cron; + +class CronPluginMultiple extends \Drupal\ultimate_cron\CronPlugin { + static public $multiple = TRUE; + + /** + * Default settings form. + */ + static public function defaultSettingsForm(&$form, &$form_state, $plugin_info) { + $plugin_type = $plugin_info['type']; + foreach (ultimate_cron_plugin_load_all($plugin_type) as $name => $plugin) { + if ($plugin->isValid()) { + $plugins[] = l($plugin->title, "admin/config/system/cron/$plugin_type/$name"); + } + } + $form['available'] = array( + '#markup' => theme('item_list', array( + 'title' => $plugin_info['defaults']['static']['title plural proper'] . ' available', + 'items' => $plugins + )) + ); + } + + /** + * Job settings form. + */ + static public function jobSettingsForm(&$form, &$form_state, $plugin_type, $job) { + // Check valid plugins. + $plugins = ultimate_cron_plugin_load_all($plugin_type); + foreach ($plugins as $name => $plugin) { + if (!$plugin->isValid($job)) { + unset($plugins[$name]); + } + } + + // No plugins = no settings = no vertical tabs for you mister! + if (empty($plugins)) { + return; + } + + $weight = 10; + $form_state['default_values']['settings'][$plugin_type] = array(); + $form['settings'][$plugin_type]['#tree'] = TRUE; + foreach ($plugins as $name => $plugin) { + $form_state['default_values']['settings'][$plugin_type][$name] = array(); + if (empty($form_state['values']['settings'][$plugin_type][$name])) { + $form_state['values']['settings'][$plugin_type][$name] = array(); + } + $form['settings'][$plugin_type][$name] = array( + '#title' => $plugin->title, + '#group' => 'settings_tabs', + '#type' => 'fieldset', + '#tree' => TRUE, + '#visible' => TRUE, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => $weight++, + ); + + $defaults = $plugin->getDefaultSettings($job); + + $form_state['default_values']['settings'][$plugin_type][$name] += $defaults; + $form_state['values']['settings'][$plugin_type][$name] += ultimate_cron_blank_values($defaults); + + $plugin->settingsForm($form, $form_state, $job); + if (empty($form['settings'][$plugin_type][$name]['no_settings'])) { + $plugin->fallbackalize( + $form['settings'][$plugin_type][$name], + $form_state['values']['settings'][$plugin_type][$name], + $form_state['default_values']['settings'][$plugin_type][$name], + FALSE + ); + } + else { + unset($form['settings'][$plugin_type][$name]); + } + } + } + + /** + * Job settings form validate handler. + */ + static public function jobSettingsFormValidate($form, &$form_state, $plugin_type, $job = NULL) { + $plugins = ultimate_cron_plugin_load_all($plugin_type); + foreach ($plugins as $plugin) { + if ($plugin->isValid($job)) { + $plugin->settingsFormValidate($form, $form_state, $job); + } + } + } + + /** + * Job settings form submit handler. + */ + static public function jobSettingsFormSubmit($form, &$form_state, $plugin_type, $job = NULL) { + $plugins = ultimate_cron_plugin_load_all($plugin_type); + foreach ($plugins as $name => $plugin) { + if ($plugin->isValid($job)) { + $plugin->settingsFormSubmit($form, $form_state, $job); + + // Weed out blank values that have fallbacks. + $elements = & $form['settings'][$plugin_type][$name]; + $values = & $form_state['values']['settings'][$plugin_type][$name]; + $plugin->cleanForm($elements, $values, array( + 'settings', + $plugin_type, + $name + )); + } + else { + unset($form_state['values']['settings'][$plugin_type][$name]); + } + } + } +} diff --git a/web/modules/ultimate_cron/src/CronRule.php b/web/modules/ultimate_cron/src/CronRule.php new file mode 100644 index 0000000000000000000000000000000000000000..8b0ecab1214cfb45f568bfe6eca2d82ef5146c3f --- /dev/null +++ b/web/modules/ultimate_cron/src/CronRule.php @@ -0,0 +1,475 @@ +<?php + +namespace Drupal\ultimate_cron; + +class CronRule { + + public $rule = NULL; + public $time = NULL; + public $skew = 0; + + public $allow_shorthand = FALSE; + private static $ranges = array( + 'minutes' => array(0, 59), + 'hours' => array(0, 23), + 'days' => array(1, 31), + 'months' => array(1, 12), + 'weekdays' => array(0, 6), + ); + + private $type = NULL; + static private $cache = array(); + static private $instances = array(); + private $last_run; + private $next_run; + + /** + * Factory method for CronRule instance. + * + * @param string $rule + * The crontab rule to use. + * @param integer $time + * The time to test against. + * @param integer $skew + * Skew for @ flag. + * + * @return CronRule + * CronRule object. + */ + static public function factory($rule, $time = NULL, $skew = 0) { + if (strpos($rule, '@') === FALSE) { + $skew = 0; + } + + $time = isset($time) ? (int) $time : time(); + + $key = "$rule:$time:$skew"; + if (isset(self::$instances[$key])) { + return self::$instances[$key]; + } + self::$instances[$key] = new CronRule($rule, $time, $skew); + return self::$instances[$key]; + } + + /** + * Constructor. + * + * @param string $rule + * The crontab rule to use. + * @param integer $time + * The time to test against. + * @param integer $skew + * Skew for @ flag. + */ + public function __construct($rule, $time, $skew) { + $this->rule = $rule; + $this->time = $time; + $this->skew = $skew; + } + + /** + * Expand interval from cronrule part. + * + * @param array $matches + * (e.g. 4-43/5+2). + * array of matches: + * [1] = lower + * [2] = upper + * [5] = step + * [7] = offset + * + * @return string + * Comma-separated list of values. + */ + public function expandInterval($matches) { + $result = array(); + + $lower = $matches[1]; + $upper = isset($matches[2]) && $matches[2] != '' ? $matches[2] : $lower; + $step = isset($matches[5]) && $matches[5] != '' ? $matches[5] : 1; + $offset = isset($matches[7]) && $matches[7] != '' ? $matches[7] : 0; + + if ($step <= 0) { + return ''; + } + + $step = ($step > 0) ? $step : 1; + for ($i = $lower; $i <= $upper; $i += $step) { + $result[] = ($i + $offset) % (self::$ranges[$this->type][1] + 1); + } + return implode(',', $result); + } + + /** + * Prepare part + * + * @param string $part + * The part. + * @param string $type + * Type of part. + * + * @return string + * The prepared part. + */ + public function preparePart($part, $type) { + $max = implode('-', self::$ranges[$type]); + $part = str_replace("*", $max, $part); + $part = str_replace("@", $this->skew % (self::$ranges[$type][1] + 1), $part); + return $part; + } + + /** + * Expand range from cronrule part. + * + * @param string $rule + * Cronrule part, e.g.: 1,2,3,4-43/5. + * @param string $type + * Type of range (minutes, hours, etc.) + * + * @return array + * Valid values for this range. + */ + public function expandRange($part, $type) { + $this->type = $type; + $part = preg_replace_callback('!(\d+)(?:-(\d+))?((/(\d+))?(\+(\d+))?)?!', array( + $this, + 'expandInterval' + ), $part); + if (!preg_match('/([^0-9\,])/', $part)) { + $part = explode(',', $part); + rsort($part); + } + else { + $part = array(); + } + return $part; + } + + /** + * Pre process rule. + * + * @param array &$parts + * Parts of rules to pre process. + */ + public function preProcessRule(&$parts) { + // Allow JAN-DEC. + $months = array( + 1 => 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec' + ); + $parts[3] = strtr(strtolower($parts[3]), array_flip($months)); + + // Allow SUN-SUN. + $days = array('sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'); + $parts[4] = strtr(strtolower($parts[4]), array_flip($days)); + $parts[4] = str_replace('7', '0', $parts[4]); + + $i = 0; + foreach (self::$ranges as $type => $range) { + $part =& $parts[$i++]; + $max = implode('-', $range); + $part = str_replace("*", $max, $part); + $part = str_replace("@", $this->skew % ($range[1] + 1), $part); + } + } + + /** + * Post process rule. + * + * @param array &$intervals + * Intervals to post process. + */ + public function postProcessRule(&$intervals) { + } + + /** + * Generate regex rules. + * + * @return array + * Date and time regular expression for mathing rule. + */ + public function getIntervals() { + if (isset(self::$cache['intervals'][$this->rule][$this->skew])) { + return self::$cache['intervals'][$this->rule][$this->skew]; + } + + $parts = preg_split('/\s+/', $this->rule); + if ($this->allow_shorthand) { + // Allow short rules by appending wildcards? + $parts += array('*', '*', '*', '*', '*'); + $parts = array_slice($parts, 0, 5); + } + if (count($parts) != 5) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $this->preProcessRule($parts); + $intervals = array(); + $intervals['parts'] = $parts; + $intervals['minutes'] = $this->expandRange($parts[0], 'minutes'); + if (empty($intervals['minutes'])) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $intervals['hours'] = $this->expandRange($parts[1], 'hours'); + if (empty($intervals['hours'])) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $intervals['days'] = $this->expandRange($parts[2], 'days'); + if (empty($intervals['days'])) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $intervals['months'] = $this->expandRange($parts[3], 'months'); + if (empty($intervals['months'])) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $intervals['weekdays'] = $this->expandRange($parts[4], 'weekdays'); + if (empty($intervals['weekdays'])) { + return self::$cache['intervals'][$this->rule][$this->skew] = FALSE; + } + $intervals['weekdays'] = array_flip($intervals['weekdays']); + $this->postProcessRule($intervals); + + return self::$cache['intervals'][$this->rule][$this->skew] = $intervals; + } + + /** + * Convert intervals back into crontab rule format. + * + * @param array $intervals + * Intervals to convert. + * + * @return string + * Crontab rule. + */ + public function rebuildRule($intervals) { + return implode(' ', $intervals['parts']); + } + + /** + * Parse rule. Run through parser expanding expression, and recombine into crontab syntax. + */ + public function parseRule() { + if (isset($this->parsed)) { + return $this->parsed; + } + $this->parsed = $this->rebuildRule($this->getIntervals()); + return $this->parsed; + } + + /** + * Get last schedule time of rule in UNIX timestamp format. + * + * @return integer + * UNIX timestamp of last schedule time. + */ + public function getLastSchedule() { + if (isset($this->last_ran)) { + return $this->last_ran; + } + + // Current time round to last minute. + $time = floor($this->time / 60) * 60; + + // Generate regular expressions from rule. + $intervals = $this->getIntervals(); + if ($intervals === FALSE) { + return FALSE; + } + + // Get starting points. + $start_year = date('Y', $time); + // Go back max 28 years (leapyear * weekdays). + $end_year = $start_year - 28; + $start_month = date('n', $time); + $start_day = date('j', $time); + $start_hour = date('G', $time); + $start_minute = (int) date('i', $time); + + // If both weekday and days are restricted, then use either or + // otherwise, use and ... when using or, we have to try out all the days in the month + // and not just to the ones restricted. + $check_weekday = count($intervals['weekdays']) != 7; + $check_both = $check_weekday && (count($intervals['days']) != 31); + $days = $check_both ? range(31, 1) : $intervals['days']; + + // Find last date and time this rule was run. + for ($year = $start_year; $year > $end_year; $year--) { + foreach ($intervals['months'] as $month) { + if ($month < 1 || $month > 12) { + continue; + } + if ($year >= $start_year && $month > $start_month) { + continue; + } + + foreach ($days as $day) { + if ($day < 1 || $day > 31) { + continue; + } + if ($year >= $start_year && $month >= $start_month && $day > $start_day) { + continue; + } + if (!checkdate($month, $day, $year)) { + continue; + } + + // Check days and weekdays using and/or logic. + if ($check_weekday) { + $date_array = getdate(mktime(0, 0, 0, $month, $day, $year)); + if ($check_both) { + if ( + !in_array($day, $intervals['days']) && + !isset($intervals['weekdays'][$date_array['wday']]) + ) { + continue; + } + } + else { + if (!isset($intervals['weekdays'][$date_array['wday']])) { + continue; + } + } + } + + if ($day != $start_day || $month != $start_month || $year != $start_year) { + $start_hour = 23; + $start_minute = 59; + } + foreach ($intervals['hours'] as $hour) { + if ($hour < 0 || $hour > 23) { + continue; + } + if ($hour > $start_hour) { + continue; + } + if ($hour < $start_hour) { + $start_minute = 59; + } + foreach ($intervals['minutes'] as $minute) { + if ($minute < 0 || $minute > 59) { + continue; + } + if ($minute > $start_minute) { + continue; + } + break 5; + } + } + } + } + } + + // Create UNIX timestamp from derived date+time. + $this->last_ran = mktime($hour, $minute, 0, $month, $day, $year); + + return $this->last_ran; + } + + /** + * Get next schedule time of rule in UNIX timestamp format. + * + * @return integer + * UNIX timestamp of next schedule time. + */ + public function getNextSchedule() { + if (isset($this->next_run)) { + return $this->next_run; + } + + $intervals = $this->getIntervals(); + $last_schedule = $this->getLastSchedule(); + + $next['minutes'] = (int) date('i', $last_schedule); + $next['hours'] = date('G', $last_schedule); + $next['days'] = date('j', $last_schedule); + $next['months'] = date('n', $last_schedule); + $year = date('Y', $last_schedule); + + $check_weekday = count($intervals['weekdays']) != 7; + $check_both = $check_weekday && (count($intervals['days']) != 31) ? TRUE : FALSE; + $days = $intervals['days']; + $intervals['days'] = $check_both ? range(31, 1) : $intervals['days']; + + $ranges = self::$ranges; + unset($ranges['weekdays']); + + foreach ($ranges as $type => $range) { + $found = array_keys($intervals[$type], $next[$type]); + $idx[$type] = reset($found); + } + + reset($ranges); + while ($type = key($ranges)) { + next($ranges); + $idx[$type]--; + if ($idx[$type] < 0) { + $found = array_keys($intervals[$type], end($intervals[$type])); + $idx[$type] = reset($found); + if ($type == 'months') { + $year--; + reset($ranges); + } + continue; + } + + if ($type == 'days' && $check_weekday) { + // Check days and weekdays using and/or logic. + $date_array = getdate(mktime( + $intervals['hours'][$idx['hours']], + $intervals['minutes'][$idx['minutes']], + 0, + $intervals['months'][$idx['months']], + $intervals['days'][$idx['days']], + $year + )); + if ($check_both) { + if ( + !in_array($intervals['days'][$idx['days']], $days) && + !isset($intervals['weekdays'][$date_array['wday']]) + ) { + reset($ranges); + next($ranges); + next($ranges); + continue; + } + } + else { + if (!isset($intervals['weekdays'][$date_array['wday']])) { + reset($ranges); + next($ranges); + next($ranges); + continue; + } + } + } + + break; + } + + $this->next_run = mktime( + $intervals['hours'][$idx['hours']], + $intervals['minutes'][$idx['minutes']], + 0, + $intervals['months'][$idx['months']], + $intervals['days'][$idx['days']], + $year + ); + return $this->next_run; + } + + /** + * Check if a rule is valid. + */ + public function isValid() { + return $this->getLastSchedule() === FALSE ? FALSE : TRUE; + } +} diff --git a/web/modules/ultimate_cron/src/CronSignal.php b/web/modules/ultimate_cron/src/CronSignal.php new file mode 100644 index 0000000000000000000000000000000000000000..344669e996694df5176af4cba35e0e7613af887d --- /dev/null +++ b/web/modules/ultimate_cron/src/CronSignal.php @@ -0,0 +1,109 @@ +<?php + +namespace Drupal\ultimate_cron; + +class CronSignal { + /** + * Get a signal without claiming it. + * + * @param string $name + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return string + * The signal if any. + */ + static public function peek($name, $signal) { + $database = \Drupal::service('ultimate_cron.database_factory'); + return $database->select('ultimate_cron_signal', 's') + ->fields('s', array('job_name')) + ->condition('job_name', $name) + ->condition('signal_name', $signal) + ->condition('claimed', 0) + ->execute() + ->fetchField(); + } + + /** + * Get and claim signal. + * + * @param string $name + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return string + * The signal if any. If a signal is found, it is "claimed" and therefore + * cannot be claimed again. + */ + static public function get($name, $signal) { + $database = \Drupal::service('ultimate_cron.database_factory'); + $claimed = $database->update('ultimate_cron_signal') + ->fields(array('claimed' => 1)) + ->condition('job_name', $name) + ->condition('signal_name', $signal) + ->condition('claimed', 0) + ->execute(); + if ($claimed) { + $database->delete('ultimate_cron_signal') + ->condition('job_name', $name) + ->condition('signal_name', $signal) + ->condition('claimed', 1) + ->execute(); + } + return $claimed; + } + + /** + * Set signal. + * + * @param string $name + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return boolean + * TRUE if the signal was set. + * @throws \Exception + */ + static public function set($name, $signal) { + $database = \Drupal::service('ultimate_cron.database_factory'); + return $database->merge('ultimate_cron_signal') + ->keys(array( + 'job_name' => $name, + 'signal_name' => $signal, + )) + ->fields(array('claimed' => 0)) + ->execute(); + } + + /** + * Clear signal. + * + * @param string $name + * The name of the job. + * @param string $signal + * The name of the signal. + */ + static public function clear($name, $signal) { + $database = \Drupal::service('ultimate_cron.database_factory'); + $database->delete('ultimate_cron_signal') + ->condition('job_name', $name) + ->condition('signal_name', $signal) + ->execute(); + } + + /** + * Clear signals. + * + * @param string $name + * The name of the job. + */ + static public function flush($name) { + $database = \Drupal::service('ultimate_cron.database_factory'); + $database->delete('ultimate_cron_signal') + ->condition('job_name', $name) + ->execute(); + } +} diff --git a/web/modules/ultimate_cron/src/Entity/CronJob.php b/web/modules/ultimate_cron/src/Entity/CronJob.php new file mode 100644 index 0000000000000000000000000000000000000000..837e8775d3d872f20a66d210ed0acb0805c083d6 --- /dev/null +++ b/web/modules/ultimate_cron/src/Entity/CronJob.php @@ -0,0 +1,888 @@ +<?php + +namespace Drupal\ultimate_cron\Entity; + +use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Session\AnonymousUserSession; +use Drupal\Core\Utility\Error; +use Drupal\ultimate_cron\CronJobInterface; + +/** + * Class for handling cron jobs. + * + * This class represents the jobs available in the system. + * + * @ConfigEntityType( + * id = "ultimate_cron_job", + * label = @Translation("Cron Job"), + * handlers = { + * "access" = "Drupal\ultimate_cron\CronJobAccessControlHandler", + * "list_builder" = "Drupal\ultimate_cron\CronJobListBuilder", + * "form" = { + * "default" = "Drupal\ultimate_cron\Form\CronJobForm", + * "delete" = "\Drupal\Core\Entity\EntityDeleteForm", + * "disable" = "Drupal\ultimate_cron\Form\CronJobDisableForm", + * "enable" = "Drupal\ultimate_cron\Form\CronJobEnableForm", + * } + * }, + * config_prefix = "job", + * admin_permission = "administer ultimate cron", + * entity_keys = { + * "id" = "id", + * "label" = "title", + * "status" = "status", + * "weight" = "weight", + * }, + * config_export = { + * "title", + * "id", + * "status", + * "weight", + * "module", + * "callback", + * "scheduler", + * "launcher", + * "logger", + * }, + * links = { + * "edit-form" = "/admin/config/system/cron/jobs/manage/{ultimate_cron_job}", + * "delete-form" = "/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/delete", + * "collection" = "/admin/config/system/cron/jobs", + * "run" = "/admin/config/system/cron/jobs/{ultimate_cron_job}/run", + * "disable" = "/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/disable", + * "enable" = "/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/enable", + * "logs" = "/admin/config/system/cron/jobs/logs/{ultimate_cron_job}", + * "unlock" = "/admin/config/system/cron/jobs/{ultimate_cron_job}/unlock", + * } + * ) + */ +class CronJob extends ConfigEntityBase implements CronJobInterface { + static public $signals; + static public $currentJob; + public $progressUpdated = 0; + public $settings; + + /** + * @var int + */ + protected $id; + + /** + * @var int + */ + protected $uuid; + + /** + * @var bool + */ + protected $status = TRUE; + + /** + * The weight. + * + * @var int + */ + protected $weight = 0; + + /** + * @var string + */ + protected $title; + + /** + * @var string + */ + protected $callback; + + /** + * @var string + */ + protected $module; + + /** + * @var array + */ + protected $scheduler = array('id' => 'simple'); + + /** + * @var array + */ + protected $launcher = array('id' => 'serial'); + + /** + * @var array + */ + protected $logger = array('id' => 'database'); + + /** + * @var \Drupal\ultimate_cron\CronPlugin + */ + protected $plugins = []; + + /** + * The class resolver. + * + * @var \Drupal\Core\DependencyInjection\ClassResolverInterface + */ + protected $classResolver; + + /** + * The module extension list service. + * + * @var \Drupal\Core\Extension\ModuleExtensionList + */ + protected $moduleExtensionList; + + /** + * CronJob constructor. + * + * @param array $values + * @param string $entity_type + */ + public function __construct(array $values, $entity_type) { + parent::__construct($values, $entity_type); + $this->classResolver = \Drupal::service('class_resolver'); + $this->moduleExtensionList = \Drupal::service('extension.list.module'); + + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + if ($update && empty($this->dont_log)) { + $log = $this->startLog(uniqid($this->id(), TRUE), '', ULTIMATE_CRON_LOG_TYPE_ADMIN); + $log->log('Job modified by ' . $log->formatUser(), array(), RfcLogLevel::INFO); + $log->finish(); + } + } + + /** + * Set configuration for a given plugin type. + * + * @param string $plugin_type + * launcher, logger or scheduler. + * @param array $configuration + * The configuration array. + */ + public function setConfiguration($plugin_type, array $configuration) { + $this->{$plugin_type}['configuration'] = $configuration; + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + foreach ($entities as $entity) { + if (empty($entity->dont_log)) { + /** @var \Drupal\ultimate_cron\Entity\CronJob $entity */ + $log = $entity->startLog(uniqid($entity->id(), TRUE), 'modification', ULTIMATE_CRON_LOG_TYPE_ADMIN); + $log->log('Job deleted by ' . $log->formatUser(), array(), RfcLogLevel::INFO); + $log->finish(); + } + } + } + + /** + * {@inheritdoc} + */ + public function isValid() { + return is_callable($this->getCallback()); + } + + /** + * Get a signal without affecting it. + * + * @see UltimateCronSignal::peek() + */ + public function peekSignal($signal) { + if (isset(self::$signals[$this->id()][$signal])) { + return TRUE; + } + $signal = \Drupal::service('ultimate_cron.signal');; + return $signal->peek($this->id(), $signal); + } + + /** + * Get a signal and clear it if found. + * + * @see UltimateCronSignal::get() + */ + public function getSignal($signal) { + if (isset(self::$signals[$this->id()][$signal])) { + unset(self::$signals[$this->id()][$signal]); + return TRUE; + } + $service = \Drupal::service('ultimate_cron.signal');; + return $service->get($this->id(), $signal); + } + + /** + * Send a signal. + * + * @see UltimateCronSignal::set() + */ + public function sendSignal($signal, $persist = FALSE) { + if ($persist) { + $signal = \Drupal::service('ultimate_cron.signal');; + $signal->set($this->id(), $signal); + } + else { + self::$signals[$this->id()][$signal] = TRUE; + } + } + + /** + * Clear a signal. + * + * @see UltimateCronSignal::clear() + */ + public function clearSignal($signal) { + unset(self::$signals[$this->id()][$signal]); + $signal = \Drupal::service('ultimate_cron.signal');; + $signal->clear($this->id(), $signal); + } + + /** + * Send all signal for the job. + * + * @see UltimateCronSignal::flush() + */ + public function clearSignals() { + unset(self::$signals[$this->id()]); + $signal = \Drupal::service('ultimate_cron.signal');; + $signal->flush($this->id()); + } + + /** + * Get job plugin. + * + * If no plugin name is provided current plugin of the specified type will + * be returned. + * + * @param string $plugin_type + * Name of plugin type. + * @param string $name + * (optional) The name of the plugin. + * + * @return mixed + * Plugin instance of the specified type. + */ + public function getPlugin($plugin_type, $name = NULL) { + if ($name) { + return ultimate_cron_plugin_load($plugin_type, $name); + } + // @todo: enable static cache, needs unset when values change. + // if (isset($this->plugins[$plugin_type])) { + // return $this->plugins[$plugin_type]; + // } + if ($name) { + } + elseif (!empty($this->{$plugin_type}['id'])) { + $name = $this->{$plugin_type}['id']; + } + else { + $name = $this->hook[$plugin_type]['name']; + } + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.' . $plugin_type); + $this->plugins[$plugin_type] = $manager->createInstance($name, isset($this->{$plugin_type}['configuration']) ? $this->{$plugin_type}['configuration'] : array()); + return $this->plugins[$plugin_type]; + } + + /** + * Gets this plugin's configuration. + * + * @param $plugin_type + * The type of plugin. + * @return array + * An array of this plugin's configuration. + */ + public function getConfiguration($plugin_type) { + if (!isset($this->{$plugin_type}['configuration'])) { + $this->{$plugin_type}['configuration'] = $this->getPlugin($plugin_type)->defaultConfiguration(); + } + + return $this->{$plugin_type}['configuration']; + } + + /** + * Signal page for plugins. + */ + public function signal($item, $plugin_type, $plugin_name, $signal) { + $plugin = ultimate_cron_plugin_load($plugin_type, $plugin_name); + return $plugin->signal($item, $signal); + } + + /** + * Invokes the jobs callback. + */ + protected function invokeCallback() { + $callback = $this->getCallback(); + return call_user_func($callback, $this); + } + + /** + * Returns a callable for the given controller. + * + * @param string $callback + * A callback string. + * + * @return mixed + * A PHP callable. + * + * @throws \InvalidArgumentException + * If the callback class does not exist. + */ + protected function resolveCallback($callback) { + // Controller in the service:method notation. + $count = substr_count($callback, ':'); + if ($count == 1) { + list($class_or_service, $method) = explode(':', $callback, 2); + } + // Controller in the class::method notation. + elseif (strpos($callback, '::') !== FALSE) { + list($class_or_service, $method) = explode('::', $callback, 2); + } + else { + return $callback; + } + + $callback = $this->classResolver->getInstanceFromDefinition($class_or_service); + + return array($callback, $method); + } + + /** + * Check job schedule. + */ + public function isScheduled() { + \Drupal::moduleHandler()->invokeAll('cron_pre_schedule', array($this)); + $result = $this->status() && !$this->isLocked() && $this->getPlugin('scheduler') + ->isScheduled($this); + \Drupal::moduleHandler()->invokeAll('cron_post_schedule', array($this)); + return $result; + } + + /** + * Check if job is behind its schedule. + * + * @return bool|int + * FALSE if job is behind its schedule or number of seconds behind. + */ + public function isBehindSchedule() { + return $this->getPlugin('scheduler')->isBehind($this); + } + + /** + * Lock job. + */ + public function lock() { + $launcher = $this->getPlugin('launcher'); + $lock_id = $launcher->lock($this); + if (!$lock_id) { + \Drupal::logger('ultimate_cron')->error('Could not get lock for job @name', array( + '@name' => $this->id(), + )); + return FALSE; + } + $this->sendMessage('lock', array( + 'lock_id' => $lock_id, + )); + return $lock_id; + } + + /** + * Unlock job. + * + * @param string $lock_id + * The lock id to unlock. + * @param bool $manual + * Whether or not this is a manual unlock. + */ + public function unlock($lock_id = NULL, $manual = FALSE) { + $result = NULL; + if (!$lock_id) { + $lock_id = $this->isLocked(); + } + if ($lock_id) { + $result = $this->getPlugin('launcher')->unlock($lock_id, $manual); + } + $this->sendMessage('unlock', array( + 'lock_id' => $lock_id, + )); + return $result; + } + + /** + * Get locked state of job. + */ + public function isLocked() { + return $this->getPlugin('launcher')->isLocked($this); + } + + /** + * Get locked state for multiple jobs. + * + * @param array $jobs + * Jobs to check locks for. + */ + static public function isLockedMultiple($jobs) { + $launchers = array(); + foreach ($jobs as $job) { + $launchers[$job->getPlugin('launcher')->name][$job->id()] = $job; + } + $locked = array(); + foreach ($launchers as $launcher => $jobs) { + $locked += ultimate_cron_plugin_load('launcher', $launcher)->isLockedMultiple($jobs); + } + return $locked; + } + + /** + * {@inheritdoc} + */ + public function run($init_message = NULL) { + if (!$init_message) { + $init_message = t('Launched manually'); + } + + $lock_id = $this->lock(); + if (!$lock_id) { + return FALSE; + } + $log_entry = $this->startLog($lock_id, $init_message); + + $accountSwitcher = \Drupal::service('account_switcher'); + + try { + $this->clearSignals(); + $this->initializeProgress(); + \Drupal::moduleHandler()->invokeAll('cron_pre_run', array($this)); + + // Force the current user to anonymous to ensure consistent permissions + // on cron runs. + $accountSwitcher->switchTo(new AnonymousUserSession()); + + self::$currentJob = $this; + $this->invokeCallback(); + } + catch (\Error $e) { + // PHP 7 throws Error objects in case of a fatal error. It will also call + // the finally block below and close the log entry. Because of that, + // the global fatal error catching will not work and we have to log it + // explicitly here instead. The advantage is that this will not + // interrupt the process. + $variables = Error::decodeException($e); + unset($variables['backtrace']); + $log_entry->log('%type: @message in %function (line %line of %file).', $variables, RfcLogLevel::ERROR); + return FALSE; + } + catch (\Exception $e) { + $variables = Error::decodeException($e); + unset($variables['backtrace']); + $log_entry->log('%type: @message in %function (line %line of %file).', $variables, RfcLogLevel::ERROR); + return FALSE; + } + finally { + self::$currentJob = NULL; + \Drupal::moduleHandler()->invokeAll('cron_post_run', array($this)); + $this->finishProgress(); + + // Restore original user account. + $accountSwitcher->switchBack(); + $log_entry->finish(); + $this->unlock($lock_id); + } + return TRUE; + } + + /** + * Get log entries. + * + * @param int $limit + * (optional) Number of log entries per page. + * + * @return \Drupal\ultimate_cron\Logger\LogEntry[] + * Array of UltimateCronLogEntry objects. + */ + public function getLogEntries($log_types = ULTIMATE_CRON_LOG_TYPE_ALL, $limit = 10) { + $log_types = $log_types == ULTIMATE_CRON_LOG_TYPE_ALL ? _ultimate_cron_define_log_type_all() : $log_types; + return $this->getPlugin('logger') + ->getLogEntries($this->id(), $log_types, $limit); + } + + /** + * Load log entry. + * + * @param string $lock_id + * The lock id of the log entry. + * + * @return LogEntry + * The log entry. + */ + public function loadLogEntry($lock_id) { + return $this->getPlugin('logger')->load($this->id(), $lock_id); + } + + /** + * Load latest log. + * + * @return LogEntry + * The latest log entry for this job. + */ + public function loadLatestLogEntry($log_types = array(ULTIMATE_CRON_LOG_TYPE_NORMAL)) { + return $this->getPlugin('logger')->load($this->id(), NULL, $log_types); + } + + /** + * Load latest log entries. + * + * @param array $jobs + * Jobs to load log entries for. + * + * @return array + * Array of UltimateCronLogEntry objects. + */ + static public function loadLatestLogEntries($jobs, $log_types = array(ULTIMATE_CRON_LOG_TYPE_NORMAL)) { + $loggers = array(); + foreach ($jobs as $job) { + $loggers[$job->getPlugin('logger')->name][$job->id()] = $job; + } + $log_entries = array(); + foreach ($loggers as $logger => $jobs) { + $log_entries += ultimate_cron_plugin_load('logger', $logger)->loadLatestLogEntries($jobs, $log_types); + } + return $log_entries; + } + + /** + * {@inheritdoc} + */ + public function startLog($lock_id, $init_message = '', $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL) { + $logger = $this->getPlugin('logger'); + $log_entry = $logger->createEntry($this->id(), $lock_id, $init_message, $log_type); + \Drupal::service('logger.ultimate_cron')->catchMessages($log_entry); + return $log_entry; + } + + /** + * Resume a previosly saved log. + * + * @param string $lock_id + * The lock id of the log to resume. + * + * @return LogEntry + * The log entry object. + */ + public function resumeLog($lock_id) { + $logger = $this->getPlugin('logger'); + $log_entry = $logger->load($this->id(), $lock_id); + $log_entry->finished = FALSE; + \Drupal::service('logger.ultimate_cron')->catchMessages($log_entry); + return $log_entry; + } + + /** + * Get module name for this job. + */ + public function getModuleName() { + static $names = array(); + if (!isset($names[$this->module])) { + $info = $this->moduleExtensionList->getExtensionInfo($this->module); + $names[$this->module] = $info && !empty($info['name']) ? $info['name'] : $this->module; + } + return $names[$this->module]; + } + + /** + * Get module description for this job. + */ + public function getModuleDescription() { + static $descs = array(); + if (!isset($descs[$this->module])) { + $info = $this->moduleExtensionList->getExtensionInfo($this->module); + $descs[$this->module] = $info && !empty($info['description']) ? $info['description'] : ''; + } + return $descs[$this->module]; + } + + /** + * Initialize progress. + */ + public function initializeProgress() { + return $this->getPlugin('launcher')->initializeProgress($this); + } + + /** + * Finish progress. + */ + public function finishProgress() { + return $this->getPlugin('launcher')->finishProgress($this); + } + + /** + * Get job progress. + * + * @return float + * The progress of this job. + */ + public function getProgress() { + return $this->getPlugin('launcher')->getProgress($this); + } + + /** + * Get multiple job progresses. + * + * @param array $jobs + * Jobs to get progress for. + * + * @return array + * Progress of jobs, keyed by job name. + */ + static public function getProgressMultiple($jobs) { + $launchers = array(); + foreach ($jobs as $job) { + $launchers[$job->getPlugin('launcher')->name][$job->id()] = $job; + } + $progresses = array(); + foreach ($launchers as $launcher => $jobs) { + $progresses += ultimate_cron_plugin_load('launcher', $launcher)->getProgressMultiple($jobs); + } + return $progresses; + } + + /** + * Set job progress. + * + * @param float $progress + * The progress (0 - 1). + */ + public function setProgress($progress) { + if ($this->getPlugin('launcher')->setProgress($this, $progress)) { + $this->sendMessage('setProgress', array( + 'progress' => $progress, + )); + return TRUE; + } + return FALSE; + } + + /** + * Format progress. + * + * @param float $progress + * (optional) The progress to format. Uses the progress on the object + * if not specified. + * + * @return string + * Formatted progress. + */ + public function formatProgress($progress = NULL) { + if (!isset($progress)) { + $progress = isset($this->progress) ? $this->progress : $this->getProgress(); + } + return $this->getPlugin('launcher')->formatProgress($this, $progress); + } + + /** + * Get a "unique" id for a job. + */ + public function getUniqueID() { + return isset($this->ids[$this->id()]) ? $this->ids[$this->id()] : $this->ids[$this->id()] = hexdec(substr(sha1($this->id()), -8)); + } + + /** + * Send a nodejs message. + * + * @param string $action + * The action performed. + * @param array $data + * Data blob for the given action. + */ + public function sendMessage($action, $data = array()) { + // @TODO: Nodejs integration has not been ported to 8.x yet. + if (FALSE && \Drupal::moduleHandler()->moduleExists('nodejs')) { + $settings = ultimate_cron_plugin_load('settings', 'general')->getDefaultSettings(); + if (empty($settings['nodejs'])) { + return; + } + + $elements = array(); + + $build = clone $this; + + $cell_idxs = array(); + + switch ($action) { + case 'lock': + $logger = $build->getPlugin('logger'); + if (empty($data['log_entry'])) { + $build->lock_id = $data['lock_id']; + $build->log_entry = $logger->factoryLogEntry($build->name); + $build->log_entry->setData(array( + 'lid' => $data['lock_id'], + 'start_time' => microtime(TRUE), + )); + } + else { + $build->log_entry = $data['log_entry']; + } + $cell_idxs = array( + 'tr#' . $build->name . ' .ctools-export-ui-start-time' => 3, + 'tr#' . $build->name . ' .ctools-export-ui-duration' => 4, + 'tr#' . $build->name . ' .ctools-export-ui-status' => 5, + 'tr#' . $build->name . ' .ctools-export-ui-operations' => 7, + ); + break; + + case 'unlock': + $build->log_entry = $build->loadLogEntry($data['lock_id']); + $build->lock_id = FALSE; + $cell_idxs = array( + 'tr#' . $build->name . ' .ctools-export-ui-start-time' => 3, + 'tr#' . $build->name . ' .ctools-export-ui-duration' => 4, + 'tr#' . $build->name . ' .ctools-export-ui-status' => 5, + 'tr#' . $build->name . ' .ctools-export-ui-operations' => 7, + ); + break; + + case 'setProgress': + $build->lock_id = $build->isLocked(); + $build->log_entry = $build->loadLogEntry($build->lock_id); + $cell_idxs = array( + 'tr#' . $build->name . ' .ctools-export-ui-start-time' => 3, + 'tr#' . $build->name . ' .ctools-export-ui-duration' => 4, + 'tr#' . $build->name . ' .ctools-export-ui-status' => 5, + ); + break; + } + $cells = $build->rebuild_ctools_export_ui_table_row(); + foreach ($cell_idxs as $selector => $cell_idx) { + $elements[$selector] = $cells[$cell_idx]; + } + + $message = (object) array( + 'channel' => 'ultimate_cron', + 'data' => (object) array( + 'action' => $action, + 'job' => $build, + 'timestamp' => microtime(TRUE), + 'elements' => $elements, + ), + 'callback' => 'nodejsUltimateCron', + ); + nodejs_send_content_channel_message($message); + } + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + $this->addDependency('module', $this->getModule()); + + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function getCallback() { + if (is_callable($this->callback)) { + return $this->callback; + } + else { + return $this->resolveCallback($this->callback); + } + } + + /** + * {@inheritdoc} + */ + public function getModule() { + return $this->module; + } + + /** + * {@inheritdoc} + */ + public function getSchedulerId() { + return $this->scheduler; + } + + /** + * {@inheritdoc} + */ + public function getLauncherId() { + return $this->launcher['id']; + } + + /** + * {@inheritdoc} + */ + public function getLoggerId() { + return $this->logger['id']; + } + + /** + * {@inheritdoc} + */ + public function setTitle($title) { + $this->set('title', $title); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setCallback($callback) { + $this->set('callback', $callback); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setModule($module) { + $this->set('module', $module); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setSchedulerId($scheduler_id) { + $this->scheduler['id'] = $scheduler_id; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setLauncherId($launcher_id) { + $this->launcher['id'] = $launcher_id; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setLoggerId($logger_id) { + $this->logger['id'] = $logger_id; + return $this; + } + +} diff --git a/web/modules/ultimate_cron/src/Form/CronJobDisableForm.php b/web/modules/ultimate_cron/src/Form/CronJobDisableForm.php new file mode 100644 index 0000000000000000000000000000000000000000..6eacf01a29eca173fc691780ec7427c44aaed48c --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/CronJobDisableForm.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\ultimate_cron\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; + + +class CronJobDisableForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Do you really want to disable @cronjob_id cron job?', array( + '@cronjob_id' => $this->getEntity()->label(), + )); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This cron job will no longer be executed.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->getEntity()->toUrl('collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Disable'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->disable()->save(); + $this->messenger() + ->addStatus($this->t('Disabled cron job %cronjob.', ['%cronjob' => $this->entity->label()])); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/ultimate_cron/src/Form/CronJobEnableForm.php b/web/modules/ultimate_cron/src/Form/CronJobEnableForm.php new file mode 100644 index 0000000000000000000000000000000000000000..16b23c63cfb2f04cf9b5c89bc7ce6d5440b57572 --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/CronJobEnableForm.php @@ -0,0 +1,51 @@ +<?php + +namespace Drupal\ultimate_cron\Form; + +use Drupal\Core\Entity\EntityConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; + + +class CronJobEnableForm extends EntityConfirmFormBase { + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Do you really want to enable @cronjob_id cron job?', array( + '@cronjob_id' => $this->getEntity()->label(), + )); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This cron job will be executed again.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->getEntity()->toUrl('collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Enable'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->enable()->save(); + $this->messenger() + ->addStatus($this->t('Enabled cron job %cronjob.', ['%cronjob' => $this->entity->label()])); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/web/modules/ultimate_cron/src/Form/CronJobForm.php b/web/modules/ultimate_cron/src/Form/CronJobForm.php new file mode 100644 index 0000000000000000000000000000000000000000..9819bd176b8e0a30c93533c03614249229634d80 --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/CronJobForm.php @@ -0,0 +1,174 @@ +<?php + +namespace Drupal\ultimate_cron\Form; + +use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Form\FormStateInterface; +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\CronRule; + +/** + * Base form controller for cron job forms. + */ +class CronJobForm extends EntityForm { + + protected $selected_option; + + /** + * @var \Drupal\ultimate_cron\CronJobInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + /* @var \Drupal\ultimate_cron\Entity\CronJob $job */ + $job = $this->entity; + + $form['title'] = array( + '#title' => t('Title'), + '#description' => t('This will appear in the administrative interface to easily identify it.'), + '#type' => 'textfield', + '#default_value' => $job->getTitle(), + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $job->id(), + '#machine_name' => array( + 'exists' => '\Drupal\ultimate_cron\Entity\CronJob::load', + 'source' => array('title'), + ), + '#disabled' => !$job->isNew(), + ); + + $form['status'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled'), + '#default_value' => $job->status(), + '#description' => t('This checkbox enables the cron job. Disabled Cron jobs are not run.'), + ); + + $form['module_info'] = array( + '#type' => 'item', + '#title' => $this->t('Module'), + '#markup' => $job->getModule(), + ); + + $callback = $job->getCallback(); + if (is_array($callback)) { + $callback = get_class($callback[0]) . '::' . $callback[1]; + } + + $form['callback_info'] = array( + '#type' => 'item', + '#title' => $this->t('Callback'), + '#markup' => $callback, + ); + + // Setup vertical tabs. + $form['settings_tabs'] = array( + '#type' => 'vertical_tabs', + ); + + // Load settings for each plugin in its own vertical tab. + $plugin_types = CronPlugin::getPluginTypes(); + foreach ($plugin_types as $plugin_type => $plugin_label) { + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.' . $plugin_type); + $plugins = $manager->getDefinitions(); + + $plugin_settings = $job->get($plugin_type); + + // Generate select options. + $options = array(); + foreach ($plugins as $value => $key) { + if (!empty($key['default']) && $key['default'] == TRUE) { + $options = array($value => t('@title (Default)', array('@title' => $key['title']))) + $options; + } + else { + $options[$value] = $key['title']; + } + } + + $form[$plugin_type] = array( + '#type' => 'details', + '#title' => $plugin_label, + '#group' => 'settings_tabs', + '#tree' => TRUE, + ); + + $form[$plugin_type]['id'] = array( + '#type' => 'select', + '#title' => $plugin_label, + '#options' => $options, + '#plugin_type' => $plugin_type, + '#default_value' => $plugin_settings['id'], + '#description' => $this->t("Select which @plugin to use for this job.", array('@plugin' => $plugin_type)), + '#group' => 'settings_tabs', + '#executes_submit_callback' => TRUE, + '#ajax' => array( + 'callback' => array($this, 'updateSelectedPluginType'), + 'wrapper' => $plugin_type . '_settings', + 'method' => 'replace', + ), + '#submit' => array('::submitForm', '::rebuild'), + '#limit_validation_errors' => array(array($plugin_type, 'id')), + ); + + $form[$plugin_type]['select'] = array( + '#type' => 'submit', + '#name' => $plugin_type . '_select', + '#value' => t('Select'), + '#submit' => array('::submitForm', '::rebuild'), + '#limit_validation_errors' => array(array($plugin_type, 'id')), + '#attributes' => array('class' => array('js-hide')), + ); + + $plugin = $job->getPlugin($plugin_type); + $temp_form = array(); + $form[$plugin_type]['configuration'] = $plugin->buildConfigurationForm($temp_form, $form_state); + $form[$plugin_type]['configuration']['#prefix'] = '<div id="' . $plugin_type . '_settings' . '">'; + $form[$plugin_type]['configuration']['#suffix'] = '</div>'; + } + + //$form['#attached']['js'][] = drupal_get_path('module', 'ultimate_cron') . '/js/ultimate_cron.job.js'; + + return $form; + } + + public function updateSelectedPluginType(array $form, FormStateInterface $form_state) { + return $form[$form_state->getTriggeringElement()['#plugin_type']]['configuration']; + } + + /** + * {@inheritdoc} + */ + public function rebuild(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(TRUE); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $this->entity->getPlugin('scheduler')->validateConfigurationForm($form, $form_state); + + } + + /** + * Overrides Drupal\Core\Entity\EntityForm::save(). + */ + public function save(array $form, FormStateInterface $form_state) { + parent::save($form, $form_state); + $this->messenger() + ->addStatus(t('job %label has been updated.', ['%label' => $this->entity->label()])); + $form_state->setRedirect('entity.ultimate_cron_job.collection'); + + } + +} diff --git a/web/modules/ultimate_cron/src/Form/GeneralSettingsForm.php b/web/modules/ultimate_cron/src/Form/GeneralSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b1f24b1dad18824b34cf1b34d614f05a95da22a7 --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/GeneralSettingsForm.php @@ -0,0 +1,223 @@ +<?php + +namespace Drupal\ultimate_cron\Form; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Datetime\DateFormatter; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\State\StateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Form for general cron settings. + */ +class GeneralSettingsForm extends ConfigFormBase { + + /** + * Stores the state storage service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * The cron service. + * + * @var \Drupal\Core\CronInterface + */ + protected $cron; + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatter + */ + protected $dateFormatter; + + /** + * Constructs a GeneralSettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\State\StateInterface $state + * The state key value store. + * @param \Drupal\Core\Datetime\DateFormatter $date_formatter + * The date formatter service. + */ + public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state, DateFormatter $date_formatter) { + parent::__construct($config_factory); + $this->state = $state; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('state'), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ultimate_cron_general_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ultimate_cron.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('ultimate_cron.settings'); + // Setup vertical tabs. + $form['settings_tabs'] = [ + '#type' => 'vertical_tabs', + ]; + + // @todo enable this when supported again + $form['nodejs'] = array( + '#type' => 'checkbox', + '#title' => t('nodejs'), + '#default_value' => $config->get('nodejs'), + '#description' => t('Enable nodejs integration (Live reload on jobs page. Requires the nodejs module to be installed and configured).'), + '#fallback' => TRUE, + + '#access' => FALSE, + ); + + // Queue settings. Visual hierarchy disabled since this is currently + // the only general settings group. + $form['queue'] = [ + //'#type' => 'details', + //'#title' => 'queue', + //'#group' => 'settings_tabs', + '#tree' => TRUE, + ]; + + $form['queue']['enabled'] = array( + '#title' => t('Override cron queue processing'), + '#description' => t('If enabled, queue workers are exposed as cron jobs and can be configured separately. When disabled, the standard queue processing is used. <strong>This feature is currently experimental, do not enable unless you need it.</strong>'), + '#type' => 'checkbox', + '#default_value' => $config->get('queue.enabled'), + '#fallback' => TRUE, + ); + + $queue_states = array( + '#states' => array( + 'visible' => array(':input[name="queue[enabled]"]' => array('checked' => TRUE)), + ), + ); + + $form['queue']['timeouts'] = array( + '#type' => 'fieldset', + '#title' => t('Timeouts'), + ) + $queue_states; + $form['queue']['timeouts']['lease_time'] = array( + '#title' => t("Queue lease time"), + '#type' => 'number', + '#default_value' => $config->get('queue.timeouts.lease_time'), + '#description' => t('Seconds to claim a cron queue item.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + '#step' => 0.01 + ); + $form['queue']['timeouts']['time'] = array( + '#title' => t('Time'), + '#type' => 'number', + '#default_value' => $config->get('queue.timeouts.time'), + '#description' => t('Time in seconds to process items during a cron run.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + '#step' => 0.01 + ); + + $form['queue']['delays'] = array( + '#type' => 'fieldset', + '#title' => t('Delays'), + ) + $queue_states; + $form['queue']['delays']['empty_delay'] = array( + '#title' => t("Empty delay"), + '#type' => 'number', + '#default_value' => $config->get('queue.delays.empty_delay'), + '#description' => t('Seconds to delay processing of queue if queue is empty (0 = end job).'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + '#step' => 0.01 + ); + $form['queue']['delays']['item_delay'] = array( + '#title' => t("Item delay"), + '#type' => 'number', + '#default_value' => $config->get('queue.delays.item_delay'), + '#description' => t('Seconds to wait between processing each item in a queue.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + '#step' => 0.01 + ); + + $throttle_states = array( + '#states' => array( + 'visible' => array(':input[name="queue[throttle][enabled]"]' => array('checked' => TRUE)), + ), + ); + + $form['queue']['throttle'] = array( + '#type' => 'fieldset', + '#title' => t('Throttling'), + // @todo Show when throttling is implemented. + '#access' => FALSE, + ) + $queue_states; + $form['queue']['throttle']['enabled'] = array( + '#title' => t('Throttle'), + '#type' => 'checkbox', + '#default_value' => $config->get('queue.throttle.enabled'), + '#description' => t('Throttle queues using multiple threads.'), + ); + $form['queue']['throttle']['threads'] = array( + '#title' => t('Threads'), + '#type' => 'number', + '#default_value' => $config->get('queue.throttle.threads'), + '#description' => t('Number of threads to use for queues.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + ) + $throttle_states; + $form['queue']['throttle']['threshold'] = array( + '#title' => t('Threshold'), + '#type' => 'number', + '#default_value' => $config->get('queue.throttle.threshold'), + '#description' => t('Number of items in queue required to activate the next cron job.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#min' => 0, + ) + $throttle_states; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('ultimate_cron.settings') + ->set('queue', $form_state->getValue('queue')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/ultimate_cron/src/Form/LauncherSettingsForm.php b/web/modules/ultimate_cron/src/Form/LauncherSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..9c0c528ecf3f8571801c1e25dd5f11824ea703ec --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/LauncherSettingsForm.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\ultimate_cron\Form; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Form for launcher settings. + */ +class LauncherSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ultimate_cron_launcher_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ultimate_cron.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $values = $this->config('ultimate_cron.settings'); + + $form['timeouts'] = [ + '#type' => 'fieldset', + '#title' => t('Timeouts'), + ]; + $form['launcher'] = [ + '#type' => 'fieldset', + '#title' => t('Launching options'), + ]; + $form['timeouts']['lock_timeout'] = [ + '#title' => t('Job lock timeout'), + '#type' => 'textfield', + '#default_value' => $values->get('launcher.lock_timeout'), + '#description' => t('Number of seconds to keep lock on job.'), + '#fallback' => TRUE, + '#required' => TRUE, + ]; + $form['timeouts']['max_execution_time'] = [ + '#title' => t('Maximum execution time'), + '#type' => 'textfield', + '#default_value' => $values->get('launcher.max_execution_time'), + '#description' => t('Maximum execution time for a cron run in seconds.'), + '#fallback' => TRUE, + '#required' => TRUE, + ]; + $form['launcher']['max_threads'] = [ + '#title' => t('Maximum number of launcher threads'), + '#type' => 'textfield', + '#default_value' => $values->get('launcher.max_threads'), + '#description' => t('The maximum number of launch threads that can be running at any given time.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#weight' => 1, + ]; + + $options = ['any', '-- fixed --', '1']; + + $form['launcher']['thread'] = [ + '#title' => t('Run in thread'), + '#type' => 'select', + '#default_value' => $values->get('launcher.thread'), + '#options' => $options, + '#description' => t('Which thread to run jobs in.') . '<br/>' . + t('<strong>Any</strong>: Just use any available thread') . '<br/>' . + t('<strong>Fixed</strong>: Only run in one specific thread. The maximum number of threads is spread across the jobs.') . '<br/>' . + t('<strong>1-?</strong>: Only run when a specific thread is invoked. This setting only has an effect when cron is run through cron.php with an argument ?thread=N or through Drush with --options=thread=N.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#weight' => 2, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('ultimate_cron.settings') + ->set('launcher.lock_timeout', $form_state->getValue('lock_timeout')) + ->set('launcher.max_execution_time', $form_state->getValue('max_execution_time')) + ->set('launcher.max_threads', $form_state->getValue('max_threads')) + ->set('launcher.thread', $form_state->getValue('thread')) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/ultimate_cron/src/Form/LoggerSettingsForm.php b/web/modules/ultimate_cron/src/Form/LoggerSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..8c56dbbbe2ebd92f4c04ad54b7106880304e9903 --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/LoggerSettingsForm.php @@ -0,0 +1,126 @@ +<?php + +namespace Drupal\ultimate_cron\Form; +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Form for logger settings. + */ +class LoggerSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ultimate_cron_logger_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ultimate_cron.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('ultimate_cron.settings'); + + // Setup vertical tabs. + $form['settings_tabs'] = [ + '#type' => 'vertical_tabs', + ]; + + // Settings for Cache logger. + $form['cache'] = [ + '#type' => 'details', + '#title' => t('Cache'), + '#group' => 'settings_tabs', + '#tree' => TRUE, + ]; + + $form['cache']['bin'] = array( + '#type' => 'textfield', + '#title' => t('Cache bin'), + '#description' => t('Select which cache bin to use for storing logs.'), + '#default_value' => $config->get('logger.cache.bin'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + $form['cache']['timeout'] = array( + '#type' => 'textfield', + '#title' => t('Cache timeout'), + '#description' => t('Seconds before cache entry expires (0 = never, -1 = on next general cache wipe).'), + '#default_value' => $config->get('logger.cache.timeout'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + // Settings for Database logger. + $form['database'] = [ + '#type' => 'details', + '#title' => t('Database'), + '#group' => 'settings_tabs', + '#tree' => TRUE, + ]; + $options['method'] = [ + 1 => t('Disabled'), + 2 => t('Remove logs older than a specified age'), + 3 => t('Retain only a specific amount of log entries'), + ]; + $form['database']['method'] = array( + '#type' => 'select', + '#title' => t('Log entry cleanup method'), + '#description' => t('Select which method to use for cleaning up logs.'), + '#options' => $options['method'], + '#default_value' => $config->get('logger.database.method'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + $states = array('expire' => array(), 'retain' => array()); + $form['database']['method_expire'] = array( + '#type' => 'fieldset', + '#title' => t('Remove logs older than a specified age'), + ) + $states['expire']; + $form['database']['method_expire']['expire'] = array( + '#type' => 'textfield', + '#title' => t('Log entry expiration'), + '#description' => t('Remove log entries older than X seconds.'), + '#default_value' => $config->get('logger.database.method_expire.expire'), + '#fallback' => TRUE, + '#required' => TRUE, + ) + $states['expire']; + + $form['database']['method_retain'] = array( + '#type' => 'fieldset', + '#title' => t('Retain only a specific amount of log entries'), + ) + $states['retain']; + $form['database']['method_retain']['retain'] = array( + '#type' => 'textfield', + '#title' => t('Retain logs'), + '#description' => t('Retain X amount of log entries.'), + '#default_value' => $config->get('logger.database.method_retain.retain'), + '#fallback' => TRUE, + '#required' => TRUE, + ) + $states['retain']; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('ultimate_cron.settings') + ->set('logger.cache', $form_state->getValue('cache')) + ->set('logger.database', $form_state->getValue('database')) + ->save(''); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/ultimate_cron/src/Form/SchedulerSettingsForm.php b/web/modules/ultimate_cron/src/Form/SchedulerSettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c9bdc716b7c4cfb5f73dfc49b75304ff4d993791 --- /dev/null +++ b/web/modules/ultimate_cron/src/Form/SchedulerSettingsForm.php @@ -0,0 +1,119 @@ +<?php + +namespace Drupal\ultimate_cron\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Form for scheduler settings. + */ +class SchedulerSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'ultimate_cron_scheduler_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['ultimate_cron.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $values = $this->config('ultimate_cron.settings'); + $rules = is_array($values->get('rules')) ? implode(';', $values->get('rules')) : ''; + + // Setup vertical tabs. + $form['settings_tabs'] = array( + '#type' => 'vertical_tabs', + ); + + // Settings for crontab. + $form['crontab'] = [ + '#type' => 'details', + '#title' => 'Crontab', + '#group' => 'settings_tabs', + '#tree' => TRUE, + ]; + + $form['crontab']['catch_up'] = array( + '#title' => t("Catch up"), + '#type' => 'textfield', + '#default_value' => $values->get('catch_up'), + '#description' => t("Don't run job after X seconds of rule."), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + $form['crontab']['rules'] = array( + '#title' => t("Rules"), + '#type' => 'textfield', + '#default_value' => $rules, + '#description' => t('Semi-colon separated list of crontab rules.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#element_validate' => array('ultimate_cron_plugin_crontab_element_validate_rule'), + ); + $form['crontab']['rules_help'] = array( + '#type' => 'fieldset', + '#title' => t('Rules help'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['crontab']['rules_help']['info'] = array( + '#markup' => file_get_contents(drupal_get_path('module', 'ultimate_cron') . '/help/rules.html'), + ); + + // Settings for Simple scheduler. + $form['simple'] = [ + '#type' => 'details', + '#title' => t('Simple'), + '#group' => 'settings_tabs', + '#tree' => TRUE, + ]; + + $options = [ + '* * * * *' => 'Every minute', + '*/15+@ * * * *' => 'Every 15 minutes', + '*/30+@ * * * *' => 'Every 30 minutes', + '0+@ * * * *' => 'Every hour', + '0+@ */3 * * *' => 'Every 3 hours', + '0+@ */6 * * *' => 'Every 6 hours', + '0+@ */12 * * *' => 'Every 12 hours', + '0+@ 0 * * *' => 'Every day', + '0+@ 0 * * 0' => 'Every week', + ]; + $form['simple']['rule'] = array( + '#type' => 'select', + '#title' => t('Run cron every'), + '#default_value' => $values->get('rule'), + '#description' => t('Select the interval you wish cron to run on.'), + '#options' => $options, + '#fallback' => TRUE, + '#required' => TRUE, + ); + + parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->config('ultimate_cron.settings') + ->set('scheduler.crontab', $form_state->getValue('crontab')) + ->set('scheduler.simple', explode(';', $form_state->getValue('simple'))) + ->save(); + + parent::submitForm($form, $form_state); + } + +} diff --git a/web/modules/ultimate_cron/src/Launcher/LauncherBase.php b/web/modules/ultimate_cron/src/Launcher/LauncherBase.php new file mode 100644 index 0000000000000000000000000000000000000000..f83f695f060a50fe1a9ed37128edc791157550b3 --- /dev/null +++ b/web/modules/ultimate_cron/src/Launcher/LauncherBase.php @@ -0,0 +1,129 @@ +<?php + +namespace Drupal\ultimate_cron\Launcher; + +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\CronJobInterface; + +/** + * Abstract class for Ultimate Cron launchers. + * + * A launcher is responsible for locking and launching/running a job. + * + * Abstract methods: + * lock($job) + * - Lock a job. This method must return the lock_id on success + * or FALSE on failure. + * + * unlock($lock_id, $manual = FALSE) + * - Release a specific lock id. If $manual is set, then the release + * was triggered manually by a user. + * + * isLocked($job) + * - Check if a job is locked. This method must return the current + * - lock_id for the given job, or FALSE if it is not locked. + * + * launch($job) + * - This method launches/runs the given job. This method must handle + * the locking of job before launching it. Returns TRUE on successful + * launch, FALSE if not. + * + * Important methods: + * isLockedMultiple($jobs) + * - Check locks for multiple jobs. Each launcher should implement an + * optimized version of this method if possible. + * + * launchJobs($jobs) + * - Launches the jobs provided to it. A default implementation of this + * exists, but can be overridden. It is assumed that this function + * checks the jobs schedule before launching and that it also handles + * locking wrt concurrency for the launcher itself. + * + */ +abstract class LauncherBase extends CronPlugin implements LauncherInterface { + + /** + * {@inheritdoc} + */ + public function isLockedMultiple(array $jobs) { + $lock_ids = array(); + foreach ($jobs as $name => $job) { + $lock_ids[$name] = $this->isLocked($job); + } + } + + /** + * {@inheritdoc} + */ + public function launchJobs(array $jobs) { + foreach ($jobs as $job) { + if ($job->isScheduled()) { + $job->launch(); + } + } + } + + /** + * {@inheritdoc} + */ + public function formatRunning(CronJobInterface $job) { + $file = drupal_get_path('module', 'ultimate_cron') . '/icons/hourglass.png'; + $status = ['#theme' => 'image', '#uri' => $file]; + $title = t('running'); + return array($status, $title); + } + + /** + * {@inheritdoc} + */ + public function formatUnfinished(CronJobInterface $job) { + $file = drupal_get_path('module', 'ultimate_cron') . '/icons/lock_open.png'; + $status = ['#theme' => 'image', '#uri' => $file]; + $title = t('unfinished but not locked?'); + return array($status, $title); + } + + /** + * {@inheritdoc} + */ + public function formatProgress(CronJobInterface $job, $progress) { + $progress = $progress ? sprintf("(%d%%)", round($progress * 100)) : ''; + return $progress; + } + + /** + * {@inheritdoc} + */ + public function initializeProgress(CronJobInterface $job) { + \Drupal::service('ultimate_cron.progress')->setProgress($job->id(), FALSE); + } + + /** + * {@inheritdoc} + */ + public function finishProgress(CronJobInterface $job) { + \Drupal::service('ultimate_cron.progress')->setProgress($job->id(), FALSE); + } + + /** + * {@inheritdoc} + */ + public function getProgress(CronJobInterface $job) { + return \Drupal::service('ultimate_cron.progress')->getProgress($job->id()); + } + + /** + * {@inheritdoc} + */ + public function getProgressMultiple(array $jobs) { + return \Drupal::service('ultimate_cron.progress')->getProgressMultiple(array_keys($jobs)); + } + + /** + * {@inheritdoc} + */ + public function setProgress(CronJobInterface $job, $progress) { + \Drupal::service('ultimate_cron.progress')->setProgress($job->id(), $progress); + } + +} diff --git a/web/modules/ultimate_cron/src/Launcher/LauncherInterface.php b/web/modules/ultimate_cron/src/Launcher/LauncherInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ab2b10e582d638bd0373b81712587060dd73b374 --- /dev/null +++ b/web/modules/ultimate_cron/src/Launcher/LauncherInterface.php @@ -0,0 +1,166 @@ +<?php + +namespace Drupal\ultimate_cron\Launcher; + +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\ultimate_cron\CronJobInterface; + +/** + * Defines a launcher method. + */ +interface LauncherInterface extends PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface, PluginFormInterface { + + /** + * Default settings. + * + * @return array + * Returns array with default configuration of the object. + */ + public function defaultConfiguration(); + + /** + * Lock job. + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * The job to lock. + * + * @return string|FALSE + * Lock ID or FALSE. + */ + public function lock(CronJobInterface $job); + + /** + * Unlock a lock. + * + * @param string $lock_id + * The lock id to unlock. + * @param bool $manual + * Whether this is a manual unlock or not. + * + * @return bool + * TRUE on successful unlock. + */ + public function unlock($lock_id, $manual = FALSE); + + /** + * Check if a job is locked. + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * The job to check. + * + * @return string + * Lock ID of the locked job, FALSE if not locked. + */ + public function isLocked(CronJobInterface $job); + + /** + * Launch job. + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * The job to launch. + * + * @return bool + * TRUE on successful launch. + */ + public function launch(CronJobInterface $job); + + /** + * Fallback implementation of multiple lock check. + * + * Each launcher should implement an optimized version of this method + * if possible. + * + * @param \Drupal\ultimate_cron\CronJobInterface[] $jobs + * Array of UltimateCronJobs to check. + * + * @return array + * Array of lock ids, keyed by job name. + */ + public function isLockedMultiple(array $jobs); + + /** + * Default implementation of jobs launcher. + * + * @param \Drupal\ultimate_cron\CronJobInterface[] $jobs + * Array of UltimateCronJobs to launch. + */ + public function launchJobs(array $jobs); + + /** + * Format running state. + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * The running job to format. + */ + public function formatRunning(CronJobInterface $job); + + /** + * Format unfinished state. + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * The running job to format. + */ + public function formatUnfinished(CronJobInterface $job); + + /** + * Default implementation of formatProgress(). + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * Job to format progress for. + * @param string $progress + * Progress value for the Job. + */ + public function formatProgress(CronJobInterface $job, $progress); + + /** + * Default implementation of initializeProgress(). + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * Job to initialize progress for. + */ + public function initializeProgress(CronJobInterface $job); + + /** + * Default implementation of finishProgress(). + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * Job to finish progress for. + */ + public function finishProgress(CronJobInterface $job); + + /** + * Default implementation of getProgress(). + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * Job to get progress for. + * + * @return float + * Progress for the job. + */ + public function getProgress(CronJobInterface $job); + + /** + * Default implementation of getProgressMultiple(). + * + * @param \Drupal\ultimate_cron\CronJobInterface[] $jobs + * Jobs to get progresses for, keyed by job name. + * + * @return array + * Progresses, keyed by job name. + */ + public function getProgressMultiple(array $jobs); + + /** + * Default implementation of setProgress(). + * + * @param \Drupal\ultimate_cron\CronJobInterface $job + * Job to set progress for. + * @param float $progress + * Progress (0-1). + */ + public function setProgress(CronJobInterface $job, $progress); + +} diff --git a/web/modules/ultimate_cron/src/Launcher/LauncherManager.php b/web/modules/ultimate_cron/src/Launcher/LauncherManager.php new file mode 100644 index 0000000000000000000000000000000000000000..bea3cb46c5b6ce9b30e07af5bcc4b7e1087a638e --- /dev/null +++ b/web/modules/ultimate_cron/src/Launcher/LauncherManager.php @@ -0,0 +1,31 @@ +<?php + +namespace Drupal\ultimate_cron\Launcher; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; + +/** + * A plugin manager for launcher plugins. + */ +class LauncherManager extends DefaultPluginManager { + + /** + * Constructs a LauncherManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct('Plugin/ultimate_cron/Launcher', $namespaces, $module_handler, '\Drupal\ultimate_cron\Launcher\LauncherInterface', 'Drupal\ultimate_cron\Annotation\LauncherPlugin'); + $this->alterInfo('ultimate_cron_launcher_info'); + $this->setCacheBackend($cache_backend, 'ultimate_cron_launcher'); + } + +} diff --git a/web/modules/ultimate_cron/src/Lock/Lock.php b/web/modules/ultimate_cron/src/Lock/Lock.php new file mode 100644 index 0000000000000000000000000000000000000000..5e5e7c2a98a0059da162efef0949da3ac7fe819b --- /dev/null +++ b/web/modules/ultimate_cron/src/Lock/Lock.php @@ -0,0 +1,248 @@ +<?php + +namespace Drupal\ultimate_cron\Lock; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\IntegrityConstraintViolationException; +use PDOException; + +/** + * Class for handling lock functions. + * + * This is a pseudo namespace really. Should probably be refactored... + */ +class Lock implements LockInterface { + public $locks = NULL; + + public $killable = TRUE; + + private $connection; + + public function __construct(Connection $connection) { + $this->connection = $connection; + } + + /** + * Shutdown handler for releasing locks. + */ + public function shutdown() { + if ($this->locks) { + foreach (array_keys($this->locks) as $lock_id) { + $this->unlock($lock_id); + } + } + } + + /** + * Dont release lock on shutdown. + * + * @param string $lock_id + * The lock id to persist. + */ + public function persist($lock_id) { + if (isset($this->locks)) { + unset($this->locks[$lock_id]); + } + } + + /** + * Acquire lock. + * + * @param string $job_id + * The name of the lock to acquire. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return string + * The lock id acquired. + */ + public function lock($job_id, $timeout = 30.0) { + // First, ensure cleanup. + if (!isset($this->locks)) { + $this->locks = array(); + ultimate_cron_register_shutdown_function(array( + $this, + 'shutdown' + )); + } + + $this->connection->setTarget(_ultimate_cron_get_transactional_safe_connection()); + + try { + // First we ensure that previous locks are "removed" + // if they are expired. + $this->expire($job_id); + + // Ensure that the timeout is at least 1 ms. + $timeout = max($timeout, 0.001); + $expire = microtime(TRUE) + $timeout; + + // Now we try to acquire the lock. + $lock_id = $this->connection->insert('ultimate_cron_lock') + ->fields(array( + 'name' => $job_id, + 'current' => 0, + 'expire' => $expire, + )) + ->execute(); + + $this->locks[$lock_id] = TRUE; + + return $lock_id; + } catch (PDOException $e) { + return FALSE; + } catch (IntegrityConstraintViolationException $e) { + return FALSE; + } + } + + /** + * Release lock if expired. + * + * Checks if expiration time has been reached, and releases the lock if so. + * + * @param string $job_id + * The name of the lock. + */ + public function expire($job_id) { + if ($lock_id = $this->isLocked($job_id, TRUE)) { + $now = microtime(TRUE); + $this->connection->update('ultimate_cron_lock') + ->expression('current', 'lid') + ->condition('lid', $lock_id) + ->condition('expire', $now, '<=') + ->execute(); + } + } + + /** + * Release lock. + * + * @param string $lock_id + * The lock id to release. + */ + public function unlock($lock_id) { + $unlocked = $this->connection->update('ultimate_cron_lock') + ->expression('current', 'lid') + ->condition('lid', $lock_id) + ->condition('current', 0) + ->execute(); + $this->persist($lock_id); + return $unlocked; + } + + /** + * Relock. + * + * @param string $lock_id + * The lock id to relock. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return boolean + * TRUE if relock was successful. + */ + public function reLock($lock_id, $timeout = 30.0) { + // Ensure that the timeout is at least 1 ms. + $timeout = max($timeout, 0.001); + $expire = microtime(TRUE) + $timeout; + return (bool) $this->connection->update('ultimate_cron_lock') + ->fields(array( + 'expire' => $expire, + )) + ->condition('lid', $lock_id) + ->condition('current', 0) + ->execute(); + } + + /** + * Check if lock is taken. + * + * @param string $job_id + * Name of the lock. + * @param boolean $ignore_expiration + * Ignore expiration, just check if it's present. + * Used for retrieving the lock id of an expired lock. + * + * @return mixed + * The lock id if found, otherwise FALSE. + */ + public function isLocked($job_id, $ignore_expiration = FALSE) { + $now = microtime(TRUE); + $result = $this->connection->select('ultimate_cron_lock', 'l') + ->fields('l', array('lid', 'expire')) + ->condition('name', $job_id) + ->condition('current', 0) + ->execute() + ->fetchObject(); + return $result && ($result->expire > $now || $ignore_expiration) ? $result->lid : FALSE; + } + + /** + * Check multiple locks. + * + * @param array $job_ids + * The names of the locks to check. + * + * @return array + * Array of lock ids. + */ + public function isLockedMultiple($job_ids) { + $now = microtime(TRUE); + $result = $this->connection->select('ultimate_cron_lock', 'l') + ->fields('l', array('lid', 'name', 'expire')) + ->condition('name', $job_ids, 'IN') + ->condition('current', 0) + ->execute() + ->fetchAllAssoc('name'); + foreach ($job_ids as $job_id) { + if (!isset($result[$job_id])) { + $result[$job_id] = FALSE; + } + else { + $result[$job_id] = $result[$job_id]->expire > $now ? $result[$job_id]->lid : FALSE; + } + } + return $result; + } + + /** + * Cleanup expired locks. + */ + public function cleanup() { + $count = 0; + $class = \Drupal::entityTypeManager()->getDefinition('ultimate_cron_job')->getClass(); + $now = microtime(TRUE); + + $this->connection->update('ultimate_cron_lock') + ->expression('current', 'lid') + ->condition('expire', $now, '<=') + ->execute(); + + do { + $lids = $this->connection->select('ultimate_cron_lock', 'l') + ->fields('l', array('lid')) + ->where('l.current = l.lid') + ->range(0, 100) + ->execute() + ->fetchCol(); + + if ($lids) { + $count += count($lids); + $this->connection->delete('ultimate_cron_lock') + ->condition('lid', $lids, 'IN') + ->execute(); + } + if ($job = $class::$currentJob) { + if ($job->getSignal('kill')) { + \Drupal::logger('ultimate_cron')->warning('kill signal recieved'); + return; + } + } + } while ($lids); + + if ($count) { + \Drupal::logger('ultimate_cron_lock')->info('Cleaned up @count expired locks', ['@count' => $count]); + } + } +} diff --git a/web/modules/ultimate_cron/src/Lock/LockInterface.php b/web/modules/ultimate_cron/src/Lock/LockInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..cfd3544ba87a3a16a86b0082995cbec25753e269 --- /dev/null +++ b/web/modules/ultimate_cron/src/Lock/LockInterface.php @@ -0,0 +1,98 @@ +<?php + +namespace Drupal\ultimate_cron\Lock; + + +/** + * Class for handling lock functions. + * + * This is a pseudo namespace really. Should probably be refactored... + */ +interface LockInterface { + /** + * Shutdown handler for releasing locks. + */ + public function shutdown(); + + /** + * Relock. + * + * @param string $lock_id + * The lock id to relock. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return boolean + * TRUE if relock was successful. + */ + public function reLock($lock_id, $timeout = 30.0); + + /** + * Dont release lock on shutdown. + * + * @param string $lock_id + * The lock id to persist. + */ + public function persist($lock_id); + + /** + * Check multiple locks. + * + * @param array $job_ids + * The names of the locks to check. + * + * @return array + * Array of lock ids. + */ + public function isLockedMultiple($job_ids); + + /** + * Acquire lock. + * + * @param string $job_id + * The name of the lock to acquire. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return string + * The lock id acquired. + */ + public function lock($job_id, $timeout = 30.0); + + /** + * Check if lock is taken. + * + * @param string $job_id + * Name of the lock. + * @param boolean $ignore_expiration + * Ignore expiration, just check if it's present. + * Used for retrieving the lock id of an expired lock. + * + * @return mixed + * The lock id if found, otherwise FALSE. + */ + public function isLocked($job_id, $ignore_expiration = FALSE); + + /** + * Release lock. + * + * @param string $lock_id + * The lock id to release. + */ + public function unlock($lock_id); + + /** + * Release lock if expired. + * + * Checks if expiration time has been reached, and releases the lock if so. + * + * @param string $job_id + * The name of the lock. + */ + public function expire($job_id); + + /** + * Cleanup expired locks. + */ + public function cleanup(); +} diff --git a/web/modules/ultimate_cron/src/Lock/LockMemcache.php b/web/modules/ultimate_cron/src/Lock/LockMemcache.php new file mode 100644 index 0000000000000000000000000000000000000000..07ad11441e6091f22af6509453ccd266b1ad5c54 --- /dev/null +++ b/web/modules/ultimate_cron/src/Lock/LockMemcache.php @@ -0,0 +1,205 @@ +<?php + +namespace Drupal\ultimate_cron\Lock; +/** + * Class for handling lock functions. + * + * This is a pseudo namespace really. Should probably be refactored... + */ +class LockMemcache { + private static $locks = NULL; + + /** + * Shutdown handler for releasing locks. + */ + static public function shutdown() { + if (self::$locks) { + foreach (array_keys(self::$locks) as $lock_id) { + self::unlock($lock_id); + } + } + } + + /** + * Dont release lock on shutdown. + * + * @param string $lock_id + * The lock id to persist. + */ + static public function persist($lock_id) { + if (isset(self::$locks)) { + unset(self::$locks[$lock_id]); + } + } + + /** + * Acquire lock. + * + * @param string $name + * The name of the lock to acquire. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return string + * The lock id acquired. + */ + static public function lock($name, $timeout = 30.0) { + // First, ensure cleanup. + if (!isset(self::$locks)) { + self::$locks = array(); + ultimate_cron_register_shutdown_function(array( + 'Drupal\ultimate_cron\Lock\LockMemcache', + 'shutdown' + )); + } + + // Ensure that the timeout is at least 1 sec. This is a limitation + // imposed by memcached. + $timeout = (int) max($timeout, 1); + + $bin = variable_get('ultimate_cron_lock_memcache_bin', 'semaphore'); + $lock_id = $name . ':' . uniqid('memcache-lock', TRUE); + if (dmemcache_add($name, $lock_id, $timeout, $bin)) { + self::$locks[$lock_id] = $lock_id; + return $lock_id; + } + return FALSE; + } + + /** + * Release lock if expired. + * + * Checks if expiration time has been reached, and releases the lock if so. + * + * @param string $name + * The name of the lock. + */ + static public function expire($name) { + // Nothing to do here. Memcache handles this internally. + } + + /** + * Release lock. + * + * @param string $lock_id + * The lock id to release. + */ + static public function unlock($lock_id) { + if (!preg_match('/(.*):memcache-lock.*/', $lock_id, $matches)) { + return FALSE; + } + $name = $matches[1]; + + $result = FALSE; + if (!($semaphore = self::lock("lock-$name"))) { + return FALSE; + } + + if (self::isLocked($name) == $lock_id) { + $result = self::unlockRaw($name); + unset(self::$locks[$lock_id]); + } + else { + unset(self::$locks[$lock_id]); + } + self::unlockRaw("lock-$name", $semaphore); + + return $result; + } + + /** + * Real unlock. + * + * @param string $name + * Name of lock. + * + * @return boolean + * Result of unlock. + */ + static private function unlockRaw($name, $lock_id = NULL) { + $bin = variable_get('ultimate_cron_lock_memcache_bin', 'semaphore'); + if ($lock_id) { + unset(self::$locks[$lock_id]); + } + return dmemcache_delete($name, $bin); + } + + /** + * Relock. + * + * @param string $lock_id + * The lock id to relock. + * @param float $timeout + * The timeout in seconds for the lock. + * + * @return boolean + * TRUE if relock was successful. + */ + static public function reLock($lock_id, $timeout = 30.0) { + if (!preg_match('/(.*):memcache-lock.*/', $lock_id, $matches)) { + return FALSE; + } + $name = $matches[1]; + + if (!($semaphore = self::lock("lock-$name"))) { + return FALSE; + } + + $bin = variable_get('ultimate_cron_lock_memcache_bin', 'semaphore'); + $current_lock_id = self::isLocked($name); + if ($current_lock_id === FALSE || $current_lock_id === $lock_id) { + if (dmemcache_set($name, $lock_id, $timeout, $bin)) { + self::unlockRaw("lock-$name", $semaphore); + self::$locks[$lock_id] = TRUE; + return self::$locks[$lock_id]; + } + } + self::unlockRaw("lock-$name", $semaphore); + return FALSE; + } + + /** + * Check if lock is taken. + * + * @param string $name + * Name of the lock. + * @param boolean $ignore_expiration + * Ignore expiration, just check if it's present. + * Used for retrieving the lock id of an expired lock. + * + * @return mixed + * The lock id if found, otherwise FALSE. + */ + static public function isLocked($name, $ignore_expiration = FALSE) { + $bin = variable_get('ultimate_cron_lock_memcache_bin', 'semaphore'); + $result = dmemcache_get($name, $bin); + return $result ? $result : FALSE; + } + + /** + * Check multiple locks. + * + * @param array $names + * The names of the locks to check. + * + * @return array + * Array of lock ids. + */ + static public function isLockedMultiple($names) { + $bin = variable_get('ultimate_cron_lock_memcache_bin', 'semaphore'); + $locks = dmemcache_get_multi($names, $bin); + foreach ($names as $name) { + if (!isset($locks[$name])) { + $locks[$name] = FALSE; + } + } + return $locks; + } + + /** + * Cleanup expired locks. + */ + static public function cleanup() { + // Nothing to do here. Memcache handles this internally. + } +} diff --git a/web/modules/ultimate_cron/src/Logger/LogEntry.php b/web/modules/ultimate_cron/src/Logger/LogEntry.php new file mode 100644 index 0000000000000000000000000000000000000000..b7e6d4622f106fbd6bca3aa9df8c0700f9636db5 --- /dev/null +++ b/web/modules/ultimate_cron/src/Logger/LogEntry.php @@ -0,0 +1,252 @@ +<?php + +namespace Drupal\ultimate_cron\Logger; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\user\Entity\User; + +/** + * Class for Ultimate Cron log entries. + * + * Important properties: + * $log_entry_size + * - The maximum number of characters of the message in the log entry. + */ +class LogEntry { + public $lid = NULL; + public $name = ''; + public $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL; + public $uid = NULL; + public $start_time = 0; + public $end_time = 0; + public $init_message = ''; + public $message = ''; + public $severity = -1; + + // Default 1MiB log entry. + public $log_entry_size = 1048576; + + public $log_entry_fields = array( + 'lid', + 'uid', + 'log_type', + 'start_time', + 'end_time', + 'init_message', + 'message', + 'severity', + ); + + public $logger; + public $job; + public $finished = FALSE; + + /** + * Constructor. + * + * @param string $name + * Name of log. + * @param \Drupal\ultimate_cron\Logger\LoggerBase $logger + * A logger object. + */ + public function __construct($name, $logger, $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL) { + $this->name = $name; + $this->logger = $logger; + $this->log_type = $log_type; + if (!isset($this->uid)) { + $this->uid = \Drupal::currentUser()->id(); + } + } + + /** + * Get current log entry data as an associative array. + * + * @return array + * Log entry data. + */ + public function getData() { + $result = array(); + foreach ($this->log_entry_fields as $field) { + $result[$field] = $this->$field; + } + return $result; + } + + /** + * Set current log entry data from an associative array. + * + * @param array $data + * Log entry data. + */ + public function setData($data) { + foreach ($this->log_entry_fields as $field) { + if (array_key_exists($field, $data)) { + $this->$field = $data[$field]; + } + } + } + + /** + * Finish a log and save it if applicable. + */ + public function finish() { + if (!$this->finished) { + \Drupal::service('logger.ultimate_cron')->unCatchMessages($this); + $this->end_time = microtime(TRUE); + $this->finished = TRUE; + $this->save(); + } + } + + /** + * Logs a message. + * + * @param string $message + * The message to log. + * @param array $variables + * Replacement variables for t(). + * @param int $level + * The log level, see \Drupal\Core\Logger\RfcLogLevel. + */ + public function log($message, $variables = array(), $level = RfcLogLevel::NOTICE) { + + if ($variables !== NULL && gettype($message) === 'string') { + $message = t($message, $variables); + } + + $this->message .= $message; + if ($this->severity < 0 || $this->severity > $level) { + $this->severity = $level; + } + // Make sure that message doesn't become too big. + if (mb_strlen($this->message) > $this->log_entry_size) { + while (mb_strlen($this->message) > $this->log_entry_size) { + $firstline = mb_strpos(rtrim($this->message, "\n"), "\n"); + if ($firstline === FALSE || $firstline == mb_strlen($this->message)) { + // Only one line? That's a big line ... truncate it without mercy! + $this->message = mb_substr($this->message, -$this->log_entry_size); + break; + } + $this->message = mb_substr($this->message, $firstline + 1); + } + $this->message = '.....' . $this->message; + } + } + + /** + * Get duration. + */ + public function getDuration() { + $duration = 0; + if ($this->start_time && $this->end_time) { + $duration = (int) ($this->end_time - $this->start_time); + } + elseif ($this->start_time) { + $duration = (int) (microtime(TRUE) - $this->start_time); + } + return $duration; + } + + /** + * Format duration. + */ + public function formatDuration() { + $duration = $this->getDuration(); + switch (TRUE) { + case $duration >= 86400: + $format = 'd H:i:s'; + break; + + case $duration >= 3600: + $format = 'H:i:s'; + break; + + default: + $format = 'i:s'; + } + return isset($duration) ? gmdate($format, $duration) : t('N/A'); + } + + /** + * Format start time. + */ + public function formatStartTime() { + return $this->start_time ? \Drupal::service('date.formatter')->format((int) $this->start_time, 'custom', 'Y-m-d H:i:s') : t('Never'); + } + + /** + * Format end time. + */ + public function formatEndTime() { + return $this->end_time ? \Drupal::service('date.formatter')->format((int) $this->end_time, 'custom', 'Y-m-d H:i:s') : ''; + } + + /** + * Format user. + */ + public function formatUser() { + $username = t('anonymous') . ' (0)'; + if ($this->uid) { + $user = User::load($this->uid); + $username = $user ? new FormattableMarkup('@username (@uid)', array('@username' => $user->getDisplayName(), '@uid' => $user->id())) : t('N/A'); + } + return $username; + } + + /** + * Format initial message. + */ + public function formatInitMessage() { + if ($this->start_time) { + return $this->init_message ? $this->init_message . ' ' . t('by') . ' ' . $this->formatUser() : t('N/A'); + } + else { + $registered = variable_get('ultimate_cron_hooks_registered', array()); + return !empty($registered[$this->name]) ? t('Registered at @datetime', array( + '@datetime' => \Drupal::service('date.formatter')->format($registered[$this->name], 'custom', 'Y-m-d H:i:s'), + )) : t('N/A'); + } + } + + /** + * Format severity. + */ + public function formatSeverity() { + switch ($this->severity) { + case RfcLogLevel::EMERGENCY: + case RfcLogLevel::ALERT: + case RfcLogLevel::CRITICAL: + case RfcLogLevel::ERROR: + $file = 'core/misc/icons/e32700/error.svg'; + break; + + case RfcLogLevel::WARNING: + $file = 'core/misc/icons/e29700/warning.svg'; + break; + + case RfcLogLevel::NOTICE: + // @todo Look for a better icon. + $file = 'core/misc/icons/008ee6/twistie-up.svg'; + break; + + case RfcLogLevel::INFO: + case RfcLogLevel::DEBUG: + default: + $file = 'core/misc/icons/73b355/check.svg'; + } + $status = ['#theme' => 'image', '#uri' => $file]; + $severity_levels = array( + -1 => t('no info'), + ) + RfcLogLevel::getLevels(); + $title = $severity_levels[$this->severity]; + return array($status, $title); + } + + /** + * Save log entry. + */ + public function save() { + $this->logger->save($this); + } +} diff --git a/web/modules/ultimate_cron/src/Logger/LoggerBase.php b/web/modules/ultimate_cron/src/Logger/LoggerBase.php new file mode 100644 index 0000000000000000000000000000000000000000..1e8f97d29703c685b7bc51b8e0379587948c26f9 --- /dev/null +++ b/web/modules/ultimate_cron/src/Logger/LoggerBase.php @@ -0,0 +1,56 @@ +<?php + +namespace Drupal\ultimate_cron\Logger; + +use Drupal\ultimate_cron\CronPlugin; + +/** + * Abstract class for Ultimate Cron loggers + * + * Each logger must implement its own functions for getting/setting data + * from the its storage backend. + * + * Abstract methods: + * load($name, $lock_id = NULL) + * - Load a log entry. If no $lock_id is provided, this method should + * load the latest log entry for $name. + * + * "Abstract" properties: + * $logEntryClass + * - The class name of the log entry class associated with this logger. + */ +abstract class LoggerBase extends CronPlugin implements LoggerInterface { + static public $log_entries = NULL; + + /** + * {@inheritdoc} + */ + public function factoryLogEntry($name) { + return new LogEntry($name, $this); + } + + /** + * {@inheritdoc} + */ + public function createEntry($name, $lock_id, $init_message = '', $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL) { + $log_entry = new LogEntry($name, $this, $log_type); + + $log_entry->lid = $lock_id; + $log_entry->start_time = microtime(TRUE); + $log_entry->init_message = $init_message; + //$log_entry->save(); + return $log_entry; + } + + /** + * {@inheritdoc} + */ + public function loadLatestLogEntries(array $jobs, array $log_types) { + $logs = array(); + foreach ($jobs as $job) { + $logs[$job->id()] = $job->loadLatestLogEntry($log_types); + } + return $logs; + } + +} diff --git a/web/modules/ultimate_cron/src/Logger/LoggerInterface.php b/web/modules/ultimate_cron/src/Logger/LoggerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..b567f0a559d439a15db5beb794369ae00ae86826 --- /dev/null +++ b/web/modules/ultimate_cron/src/Logger/LoggerInterface.php @@ -0,0 +1,99 @@ +<?php + +namespace Drupal\ultimate_cron\Logger; + +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Plugin\PluginFormInterface; + +/** + * Defines a logger method. + */ +interface LoggerInterface extends PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface, PluginFormInterface { + + /** + * Returns the default configuration. + * + * @return mixed + */ + public function defaultConfiguration(); + + /** + * Factory method for creating a new unsaved log entry object. + * + * @param string $name + * Name of the log entry (name of the job). + * + * @return LogEntry + * The log entry. + */ + public function factoryLogEntry($name); + + /** + * Create a new log entry. + * + * @param string $name + * Name of the log entry (name of the job). + * @param string $lock_id + * The lock id. + * @param string $init_message + * (optional) The initial message for the log entry. + * @param int $log_type + * (optional) The log_type for the log entry. + * + * @return LogEntry + * The log entry created. + */ + public function createEntry($name, $lock_id, $init_message = '', $log_type = ULTIMATE_CRON_LOG_TYPE_NORMAL); + + /** + * Load latest log entry for multiple jobs. + * + * This is the fallback method. Loggers should implement an optimized + * version if possible. + * + * @param array $jobs + * Jobs for which the log entries should be loaded. + * @param array $log_types + * Type of log messages to load. + */ + public function loadLatestLogEntries(array $jobs, array $log_types); + + /** + * Load a log. + * + * @param string $name + * Name of log. + * @param string $lock_id + * Specific lock id. + * + * @return \Drupal\ultimate_cron\Logger\LogEntry + * Log entry + */ + public function load($name, $lock_id = NULL, array $log_types = [ULTIMATE_CRON_LOG_TYPE_NORMAL]); + + /** + * Get page with log entries for a job. + * + * @param string $name + * Name of job. + * @param array $log_types + * Log types to get. + * @param int $limit + * (optional) Number of log entries per page. + * + * @return array + * Log entries. + */ + public function getLogEntries($name, array $log_types, $limit = 10); + + /** + * Saves a log entry. + * + * @param \Drupal\ultimate_cron\Logger\LogEntry $log_entry + * The log entry to save. + */ + public function save(LogEntry $log_entry); + +} diff --git a/web/modules/ultimate_cron/src/Logger/LoggerManager.php b/web/modules/ultimate_cron/src/Logger/LoggerManager.php new file mode 100644 index 0000000000000000000000000000000000000000..0ac89d9c467585fe4d56355464940224a8379fb1 --- /dev/null +++ b/web/modules/ultimate_cron/src/Logger/LoggerManager.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\ultimate_cron\Logger; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Plugin\DefaultPluginManager; + +/** + * A plugin manager for logger plugins. + */ +class LoggerManager extends DefaultPluginManager { + + /** + * Constructs a LoggerManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct('Plugin/ultimate_cron/Logger', $namespaces, $module_handler, '\Drupal\ultimate_cron\Logger\LoggerInterface', 'Drupal\ultimate_cron\Annotation\LoggerPlugin'); + $this->alterInfo('ultimate_cron_logger_info'); + $this->setCacheBackend($cache_backend, 'ultimate_cron_logger'); + } + +} diff --git a/web/modules/ultimate_cron/src/Logger/WatchdogLogger.php b/web/modules/ultimate_cron/src/Logger/WatchdogLogger.php new file mode 100644 index 0000000000000000000000000000000000000000..56d69ec37eedcd49878ae776c1436562f06bd513 --- /dev/null +++ b/web/modules/ultimate_cron/src/Logger/WatchdogLogger.php @@ -0,0 +1,148 @@ +<?php + +namespace Drupal\ultimate_cron\Logger; + +use Drupal\Core\Logger\LogMessageParserInterface; +use Drupal\Core\Logger\RfcLoggerTrait; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Psr\Log\LoggerInterface as PsrLoggerInterface; + +/** + * Logs events in currently running cronjobs. + */ +class WatchdogLogger implements PsrLoggerInterface { + use RfcLoggerTrait; + use StringTranslationTrait; + + /** + * Log entries for currently running cron jobs. + * + * @var \Drupal\ultimate_cron\Logger\LogEntry[] + */ + protected $logEntries = []; + + /** + * The message's placeholders parser. + * + * @var \Drupal\Core\Logger\LogMessageParserInterface + */ + protected $parser; + + /** + * Whether the shutdown handler is registered or not. + * + * @var bool + */ + protected $shutdownRegistered = FALSE; + + /** + * Constructs a WatchdogLogger object. + * + * @param \Drupal\Core\Logger\LogMessageParserInterface $parser + * The parser to use when extracting message variables. + */ + public function __construct(LogMessageParserInterface $parser) { + $this->parser = $parser; + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = array()) { + + if ($this->logEntries) { + + // Remove any backtraces since they may contain an unserializable variable. + unset($context['backtrace']); + + // Convert PSR3-style messages to + // \Drupal\Component\Render\FormattableMarkup style, so they can be + // translated too in runtime. + $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context); + + foreach ($this->logEntries as $log_entry) { + $log_entry->log($message, $message_placeholders, $level); + } + } + } + + /** + * Begin capturing messages. + * + * @param LogEntry $log_entry + * The log entry that should capture messages. + */ + public function catchMessages(LogEntry $log_entry) { + // Since we may already be inside a drupal_register_shutdown_function() + // we cannot use that. Use PHPs register_shutdown_function() instead. + if (!$this->shutdownRegistered) { + ultimate_cron_register_shutdown_function( + array( + $this, + 'catchMessagesShutdownWrapper' + ), 'catch_messages' + ); + $this->shutdownRegistered = TRUE; + } + + $this->logEntries[$log_entry->lid] = $log_entry; + } + + /** + * End message capturing. + * + * Effectively disables the shutdown function for the given log entry. + * + * @param \Drupal\ultimate_cron\Logger\LogEntry $log_entry + * The log entry. + */ + public function unCatchMessages(LogEntry $log_entry) { + unset($this->logEntries[$log_entry->lid]); + } + + /** + * Shutdown handler wrapper for catching messages. + */ + public function catchMessagesShutdownWrapper() { + foreach ($this->logEntries as $log_entry) { + $this->catchMessagesShutdown($log_entry); + } + } + + /** + * Shutdown function callback for a single log entry. + * + * Ensures that a log entry has been closed properly on shutdown. + * + * @param LogEntry $log_entry + * The log entry to close. + */ + public function catchMessagesShutdown(LogEntry $log_entry) { + $this->unCatchMessages($log_entry); + + if ($log_entry->finished) { + return; + } + + // Get error messages. + $error = error_get_last(); + if ($error) { + $message = $error['message'] . ' (line ' . $error['line'] . ' of ' . $error['file'] . ').' . "\n"; + $severity = RfcLogLevel::INFO; + if ($error['type'] && (E_NOTICE || E_USER_NOTICE || E_USER_WARNING)) { + $severity = RfcLogLevel::NOTICE; + } + if ($error['type'] && (E_WARNING || E_CORE_WARNING || E_USER_WARNING)) { + $severity = RfcLogLevel::WARNING; + } + if ($error['type'] && (E_ERROR || E_CORE_ERROR || E_USER_ERROR || E_RECOVERABLE_ERROR)) { + $severity = RfcLogLevel::ERROR; + } + + $log_entry->log($message, NULL, $severity); + } + $log_entry->finish(); + } + +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Launcher/SerialLauncher.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Launcher/SerialLauncher.php new file mode 100644 index 0000000000000000000000000000000000000000..7c1beee922e263c9e51cc2ac7db8052bc6274661 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Launcher/SerialLauncher.php @@ -0,0 +1,355 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Launcher; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\ultimate_cron\CronJobInterface; +use Drupal\ultimate_cron\Launcher\LauncherBase; +use Drupal\ultimate_cron\PluginCleanupInterface; + +/** + * Ultimate Cron launcher plugin class. + * + * @LauncherPlugin( + * id = "serial", + * title = @Translation("Serial"), + * description = @Translation("Launches scheduled jobs in the same thread and runs them consecutively."), + * ) + */ +class SerialLauncher extends LauncherBase implements PluginCleanupInterface { + + public $currentThread = NULL; + + /** + * Implements hook_cron_alter(). + */ + public function cron_alter(&$jobs) { + $lock = \Drupal::service('ultimate_cron.lock'); + if (!empty($lock->{$killable})) { + $jobs['ultimate_cron_plugin_launcher_serial_cleanup']->hook['tags'][] = 'killable'; + } + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'timeouts' => array( + 'lock_timeout' => 3600, + 'max_execution_time' => 3600, + ), + 'launcher' => array( + 'max_threads' => 1, + 'thread' => 'any', + ), + ) + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['timeouts'] = array( + '#type' => 'fieldset', + '#title' => t('Timeouts'), + ); + + $form['launcher'] = array( + '#type' => 'fieldset', + '#title' => t('Launching options'), + ); + + $form['timeouts']['lock_timeout'] = array( + '#title' => t("Job lock timeout"), + '#type' => 'textfield', + '#default_value' => $this->configuration['timeouts']['lock_timeout'], + '#description' => t('Number of seconds to keep lock on job.'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + // @todo: Figure this out when converting global settings to use plugin + // classes. + if (FALSE) { + $form['timeouts']['max_execution_time'] = array( + '#title' => t("Maximum execution time"), + '#type' => 'textfield', + '#default_value' => $this->configuration['timeouts']['max_execution_time'], + '#description' => t('Maximum execution time for a cron run in seconds.'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + $form['launcher']['max_threads'] = array( + '#title' => t("Maximum number of launcher threads"), + '#type' => 'number', + '#default_value' => $this->configuration['launcher']['max_threads'], + '#description' => t('The maximum number of launch threads that can be running at any given time.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#weight' => 1, + ); + + return $form; + } + else { + $max_threads = isset($this->configuration['launcher']['max_threads']) ? $this->configuration['launcher']['max_threads'] : 1; + } + + $options = array( + 'any' => t('-- Any -- '), + 'fixed' => t('-- Fixed -- '), + ); + for ($i = 1; $i <= $max_threads; $i++) { + $options[$i] = $i; + } + + + $form['launcher']['thread'] = array( + '#title' => t("Run in thread"), + '#type' => 'select', + '#default_value' => isset($this->configuration['launcher']['thread']) ? $this->configuration['launcher']['thread'] : 'any', + '#options' => $options, + '#description' => t('Which thread to run in when invoking with ?thread=N. Note: This setting only has an effect when cron is run through cron.php with an argument ?thread=N or through Drush with --options=thread=N.'), + '#fallback' => TRUE, + '#required' => TRUE, + '#weight' => 2, + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function settingsFormValidate(&$form, &$form_state, $job = NULL) { + $elements = & $form['configuration'][$this->type][$this->name]; + $values = & $form_state['values']['configuration'][$this->type][$this->name]; + if (!$job) { + if (intval($values['max_threads']) <= 0) { + form_set_error("settings[$this->type][$this->name", t('%title must be greater than 0', array( + '%title' => $elements['launcher']['max_threads']['#title'] + ))); + } + } + } + + /** + * {@inheritdoc} + */ + public function lock(CronJobInterface $job) { + if (array_key_exists('timeouts', $this->configuration)) { + $timeout = $this->configuration['timeouts']['lock_timeout']; + } + else { + $timeout = 0; + } + + $lock = \Drupal::service('ultimate_cron.lock'); + if ($lock_id = $lock->lock($job->id(), $timeout)) { + $lock_id = $this->getPluginId() . '-' . $lock_id; + return $lock_id; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function unlock($lock_id, $manual = FALSE) { + list($launcher, $lock_id) = explode('-', $lock_id, 2); + $lock = \Drupal::service('ultimate_cron.lock'); + return $lock->unlock($lock_id); + } + + /** + * {@inheritdoc} + */ + public function isLocked(CronJobInterface $job) { + $lock = \Drupal::service('ultimate_cron.lock'); + $lock_id = $lock->isLocked($job->id()); + return $lock_id ? $this->pluginId . '-' . $lock_id : $lock_id; + } + + /** + * {@inheritdoc} + */ + public function isLockedMultiple(array $jobs) { + $names = array(); + foreach ($jobs as $job) { + $names[] = $job->id(); + } + $lock = \Drupal::service('ultimate_cron.lock'); + $lock_ids = $lock->isLockedMultiple($names); + foreach ($lock_ids as &$lock_id) { + $lock_id = $lock_id ? $this->pluginId . '-' . $lock_id : $lock_id; + } + return $lock_ids; + } + + /** + * {@inheritdoc} + */ + public function cleanup() { + $lock = \Drupal::service('ultimate_cron.lock'); + $lock->cleanup(); + } + + /** + * {@inheritdoc} + */ + public function launch(CronJobInterface $job) { + \Drupal::moduleHandler()->invokeAll('cron_pre_launch', array($this)); + + if ($this->currentThread) { + $init_message = t('Launched in thread @current_thread', array( + '@current_thread' => $this->currentThread, + )); + } + else { + $init_message = t('Launched manually'); + } + + // Run job. + $job_launch = $job->run($init_message); + \Drupal::moduleHandler()->invokeAll('cron_post_launch', array($this)); + + return $job_launch; + } + + /** + * {@inheritdoc} + */ + public function findFreeThread($lock, $lock_timeout = NULL, $timeout = 3) { + $configuration = $this->getConfiguration(); + + // Find a free thread, try for 3 seconds. + $delay = $timeout * 1000000; + $sleep = 25000; + + $lock_service = \Drupal::service('ultimate_cron.lock'); + do { + for ($thread = 1; $thread <= $configuration['launcher']['max_threads']; $thread++) { + if ($thread != $this->currentThread) { + $lock_name = 'ultimate_cron_serial_launcher_' . $thread; + if (!$lock_service->isLocked($lock_name)) { + if ($lock) { + if ($lock_id = $lock_service->lock($lock_name, $lock_timeout)) { + return array($thread, $lock_id); + } + } + else { + return array($thread, FALSE); + } + } + } + } + if ($delay > 0) { + usleep($sleep); + // After each sleep, increase the value of $sleep until it reaches + // 500ms, to reduce the potential for a lock stampede. + $delay = $delay - $sleep; + $sleep = min(500000, $sleep + 25000, $delay); + } + } while ($delay > 0); + return array(FALSE, FALSE); + } + + /** + * {@inheritdoc} + */ + public function launchJobs(array $jobs) { + $lock = \Drupal::service('ultimate_cron.lock'); + $configuration = $this->getConfiguration(); + + // Set proper max execution time. + $max_execution_time = ini_get('max_execution_time'); + $lock_timeout = max($max_execution_time, $configuration['timeouts']['max_execution_time']); + + // We only lock for 55 seconds at a time, to give room for other cron + // runs. + // @todo: Why hard-code this? + $lock_timeout = 55; + + if (!empty($_GET['thread'])) { + self::setGlobalOption('thread', $_GET['thread']); + } + + if ($thread = intval(self::getGlobalOption('thread'))) { + if ($thread < 1 || $thread > $configuration['launcher']['max_threads']) { + \Drupal::logger('serial_launcher')->warning("Invalid thread available for starting launch thread"); + return; + } + + $lock_name = 'ultimate_cron_serial_launcher_' . $thread; + $lock_id = NULL; + if (!$lock->isLocked($lock_name)) { + $lock_id = $lock->lock($lock_name, $lock_timeout); + } + if (!$lock_id) { + \Drupal::logger('serial_launcher')->warning("Thread @thread is already running", array( + '@thread' => $thread, + )); + } + } + else { + $timeout = 1; + list($thread, $lock_id) = $this->findFreeThread(TRUE, $lock_timeout, $timeout); + } + $this->currentThread = $thread; + + if (!$thread) { + \Drupal::logger('serial_launcher')->warning("No free threads available for launching jobs"); + return; + } + + if ($max_execution_time && $max_execution_time < $configuration['timeouts']['max_execution_time']) { + set_time_limit($configuration['timeouts']['max_execution_time']); + } + + $this->runThread($lock_id, $thread, $jobs); + $lock->unlock($lock_id); + } + + /** + * {@inheritdoc} + */ + public function runThread($lock_id, $thread, $jobs) { + $lock = \Drupal::service('ultimate_cron.lock'); + $lock_name = 'ultimate_cron_serial_launcher_' . $thread; + foreach ($jobs as $job) { + $configuration = $job->getConfiguration('launcher'); + $configuration['launcher'] += array( + 'thread' => 'any', + ); + switch ($configuration['launcher']['thread']) { + case 'any': + $configuration['launcher']['thread'] = $thread; + break; + + case 'fixed': + $configuration['launcher']['thread'] = ($job->getUniqueID() % $configuration['launcher']['max_threads']) + 1; + break; + } + if ((!self::getGlobalOption('thread') || $configuration['launcher']['thread'] == $thread) && $job->isScheduled()) { + $this->launch($job); + // Be friendly, and check if someone else has taken the lock. + // If they have, bail out, since someone else is now handling + // this thread. + if ($current_lock_id = $lock->isLocked($lock_name)) { + if ($current_lock_id !== $lock_id) { + return; + } + } + else { + // If lock is free, then take the lock again. + $lock_id = $lock->lock($lock_name); + if (!$lock_id) { + // Race-condition, someone beat us to it. + return; + } + } + } + } + } + +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/CacheLogger.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/CacheLogger.php new file mode 100644 index 0000000000000000000000000000000000000000..a97070c223682337927b8c907c1f6cb41f734c69 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/CacheLogger.php @@ -0,0 +1,125 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Logger; + +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\ultimate_cron\Logger\LogEntry; +use Drupal\ultimate_cron\Logger\LoggerBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Cache Logger. + * + * @LoggerPlugin( + * id = "cache", + * title = @Translation("Cache"), + * description = @Translation("Stores the last log entry (and only the last) in the cache."), + * ) + */ +class CacheLogger extends LoggerBase implements ContainerFactoryPluginInterface { + + /** + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $bin = isset($configuration['bin']) ? $configuration['bin'] : 'ultimate_cron_logger'; + return new static ($configuration, $plugin_id, $plugin_definition, $container->get('cache.' . $bin)); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'bin' => 'ultimate_cron_logger', + 'timeout' => Cache::PERMANENT, + ); + } + + /** + * {@inheritdoc} + */ + public function load($name, $lock_id = NULL, array $log_types = [ULTIMATE_CRON_LOG_TYPE_NORMAL]) { + $log_entry = new LogEntry($name, $this); + if (!$lock_id) { + $cache = $this->cache->get('uc-name:' . $name, TRUE); + if (empty($cache) || empty($cache->data)) { + return $log_entry; + } + $lock_id = $cache->data; + } + $cache = $this->cache->get('uc-lid:' . $lock_id, TRUE); + + if (!empty($cache->data)) { + $log_entry->setData((array) $cache->data); + $log_entry->finished = TRUE; + } + return $log_entry; + } + + /** + * {@inheritdoc} + */ + public function getLogEntries($name, array $log_types, $limit = 10) { + $log_entry = $this->load($name); + return $log_entry->lid ? array($log_entry) : array(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['bin'] = array( + '#type' => 'textfield', + '#title' => t('Cache bin'), + '#description' => t('Select which cache bin to use for storing logs.'), + '#default_value' => $this->configuration['bin'], + '#fallback' => TRUE, + '#required' => TRUE, + ); + + $form['timeout'] = array( + '#type' => 'textfield', + '#title' => t('Cache timeout'), + '#description' => t('Seconds before cache entry expires (0 = never, -1 = on next general cache wipe).'), + '#default_value' => $this->configuration['timeout'], + '#fallback' => TRUE, + '#required' => TRUE, + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(LogEntry $log_entry) { + if (!$log_entry->lid) { + return; + } + + $settings = $this->getConfiguration(); + + $expire = $settings['timeout'] != Cache::PERMANENT ? REQUEST_TIME + $settings['timeout'] : $settings['timeout']; + + $this->cache->set('uc-name:' . $log_entry->name, $log_entry->lid, $expire); + $this->cache->set('uc-lid:' . $log_entry->lid, $log_entry->getData(), $expire); + } + +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/DatabaseLogger.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/DatabaseLogger.php new file mode 100644 index 0000000000000000000000000000000000000000..dbd5dfba77a1ca96f9824ce37fe599a38807bfa6 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Logger/DatabaseLogger.php @@ -0,0 +1,400 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Logger; + +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Database\IntegrityConstraintViolationException; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\ultimate_cron\CronJobInterface; +use Drupal\ultimate_cron\Entity\CronJob; +use Drupal\ultimate_cron\Logger\LogEntry; +use Drupal\ultimate_cron\Logger\LoggerBase; +use Drupal\ultimate_cron\PluginCleanupInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Database logger. + * + * @LoggerPlugin( + * id = "database", + * title = @Translation("Database"), + * description = @Translation("Stores logs in the database."), + * default = TRUE, + * ) + */ +class DatabaseLogger extends LoggerBase implements PluginCleanupInterface, ContainerFactoryPluginInterface { + + const CLEANUP_METHOD_DISABLED = 1; + const CLEANUP_METHOD_EXPIRE = 2; + const CLEANUP_METHOD_RETAIN = 3; + + /** + * Max length for message and init message fields. + */ + const MAX_TEXT_LENGTH = 5000; + + /** + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * Constructor. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $connection) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static ($configuration, $plugin_id, $plugin_definition, $container->get('database')); + } + + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'method' => static::CLEANUP_METHOD_RETAIN, + 'expire' => 86400 * 14, + 'retain' => 1000, + ); + } + + /** + * {@inheritdoc} + */ + public function cleanup() { + $jobs = CronJob::loadMultiple(); + $current = 1; + $max = 0; + $counter = 0; + $count_deleted = []; + foreach ($jobs as $job) { + if ($job->getLoggerId() === $this->getPluginId()) { + $max++; + } + } + foreach ($jobs as $job) { + if ($job->getLoggerId() === $this->getPluginId()) { + // Get the plugin through the job so it has the right configuration. + $counter = $job->getPlugin('logger')->cleanupJob($job); + $class = \Drupal::entityTypeManager()->getDefinition('ultimate_cron_job')->getClass(); + if ($class::$currentJob) { + $class::$currentJob->setProgress($current / $max); + $current++; + } + if ($counter) { + // Store number of deleted messages for each job. + $count_deleted[$job->id()] = $counter; + } + } + } + if ($count_deleted) { + \Drupal::logger('database_logger') + ->info('@count_entries log entries removed for @jobs_count jobs', array( + '@count_entries' => array_sum($count_deleted), + '@jobs_count' => count($count_deleted), + )); + } + } + + /** + * {@inheritdoc} + */ + public function cleanupJob(CronJobInterface $job) { + switch ($this->configuration['method']) { + case static::CLEANUP_METHOD_DISABLED: + return; + + case static::CLEANUP_METHOD_EXPIRE: + $expire = $this->configuration['expire']; + // Let's not delete more than ONE BILLION log entries :-o. + $max = 10000000000; + $chunk = 100; + break; + + case static::CLEANUP_METHOD_RETAIN: + $expire = 0; + $max = $this->connection->query("SELECT COUNT(lid) FROM {ultimate_cron_log} WHERE name = :name", array( + ':name' => $job->id(), + ))->fetchField(); + $max -= $this->configuration['retain']; + if ($max <= 0) { + return; + } + $chunk = min($max, 100); + break; + + default: + \Drupal::logger('ultimate_cron')->warning('Invalid cleanup method: @method', array( + '@method' => $this->configuration['method'], + )); + return; + } + + // Chunked delete. + $count = 0; + do { + $lids = $this->connection->select('ultimate_cron_log', 'l') + ->fields('l', array('lid')) + ->condition('l.name', $job->id()) + ->condition('l.start_time', microtime(TRUE) - $expire, '<') + ->range(0, $chunk) + ->orderBy('l.start_time', 'ASC') + ->orderBy('l.end_time', 'ASC') + ->execute() + ->fetchCol(); + if ($lids) { + $count += count($lids); + $max -= count($lids); + $chunk = min($max, 100); + $this->connection->delete('ultimate_cron_log') + ->condition('lid', $lids, 'IN') + ->execute(); + } + } while ($lids && $max > 0); + return $count; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['method'] = array( + '#type' => 'select', + '#title' => t('Log entry cleanup method'), + '#description' => t('Select which method to use for cleaning up logs.'), + '#options' => $this->getMethodOptions(), + '#default_value' => $this->configuration['method'], + ); + + $form['expire'] = array( + '#type' => 'textfield', + '#title' => t('Log entry expiration'), + '#description' => t('Remove log entries older than X seconds.'), + '#default_value' => $this->configuration['expire'], + '#fallback' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="logger[settings][method]"]' => array('value' => static::CLEANUP_METHOD_EXPIRE), + ), + 'required' => array( + ':input[name="logger[settings][method]"]' => array('value' => static::CLEANUP_METHOD_EXPIRE), + ), + ), + ); + + $form['retain'] = array( + '#type' => 'textfield', + '#title' => t('Retain logs'), + '#description' => t('Retain X amount of log entries.'), + '#default_value' => $this->configuration['retain'], + '#fallback' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="logger[settings][method]"]' => array('value' => static::CLEANUP_METHOD_RETAIN), + ), + 'required' => array( + ':input[name="logger[settings][method]"]' => array('value' => static::CLEANUP_METHOD_RETAIN), + ), + ), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function load($name, $lock_id = NULL, array $log_types = [ULTIMATE_CRON_LOG_TYPE_NORMAL]) { + if ($lock_id) { + $log_entry = $this->connection->select('ultimate_cron_log', 'l') + ->fields('l') + ->condition('l.lid', $lock_id) + ->execute() + ->fetchObject(LogEntry::class, array($name, $this)); + } + else { + $log_entry = $this->connection->select('ultimate_cron_log', 'l') + ->fields('l') + ->condition('l.name', $name) + ->condition('l.log_type', $log_types, 'IN') + ->orderBy('l.start_time', 'DESC') + ->orderBy('l.end_time', 'DESC') + ->range(0, 1) + ->execute() + ->fetchObject(LogEntry::class, array($name, $this)); + } + if ($log_entry) { + $log_entry->finished = TRUE; + } + else { + $log_entry = new LogEntry($name, $this); + } + return $log_entry; + } + + /** + * {@inheritdoc} + */ + public function loadLatestLogEntries(array $jobs, array $log_types) { + if ($this->connection->databaseType() !== 'mysql') { + return parent::loadLatestLogEntries($jobs, $log_types); + } + + $result = $this->connection->query("SELECT l.* + FROM {ultimate_cron_log} l + JOIN ( + SELECT l3.name, ( + SELECT l4.lid + FROM {ultimate_cron_log} l4 + WHERE l4.name = l3.name + AND l4.log_type IN (:log_types) + ORDER BY l4.name desc, l4.start_time DESC + LIMIT 1 + ) AS lid FROM {ultimate_cron_log} l3 + GROUP BY l3.name + ) l2 on l2.lid = l.lid", array(':log_types' => $log_types)); + + $log_entries = array(); + while ($object = $result->fetchObject()) { + if (isset($jobs[$object->name])) { + $log_entries[$object->name] = new LogEntry($object->name, $this); + $log_entries[$object->name]->setData((array) $object); + } + } + foreach ($jobs as $name => $job) { + if (!isset($log_entries[$name])) { + $log_entries[$name] = new LogEntry($name, $this); + } + } + + return $log_entries; + } + + /** + * {@inheritdoc} + */ + public function getLogEntries($name, array $log_types, $limit = 10) { + $result = $this->connection->select('ultimate_cron_log', 'l') + ->fields('l') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender') + ->condition('l.name', $name) + ->condition('l.log_type', $log_types, 'IN') + ->limit($limit) + ->orderBy('l.start_time', 'DESC') + ->execute(); + + $log_entries = array(); + while ($object = $result->fetchObject(LogEntry::class, array( + $name, + $this + ))) { + $log_entries[$object->lid] = $object; + } + + return $log_entries; + } + + /** + * {@inheritdoc} + */ + public function save(LogEntry $log_entry) { + if (!$log_entry->lid) { + return; + } + + try { + $this->connection->insert('ultimate_cron_log') + ->fields([ + 'lid' => $log_entry->lid, + 'name' => $log_entry->name, + 'log_type' => $log_entry->log_type, + 'start_time' => $log_entry->start_time, + 'end_time' => $log_entry->end_time, + 'uid' => $log_entry->uid, + 'init_message' => Unicode::truncate((string) $log_entry->init_message, static::MAX_TEXT_LENGTH, FALSE, TRUE), + 'message' => Unicode::truncate((string) $log_entry->message, static::MAX_TEXT_LENGTH, FALSE, TRUE), + 'severity' => $log_entry->severity, + ]) + ->execute(); + } + catch (IntegrityConstraintViolationException $e) { + // Row already exists. Let's update it, if we can. + $updated = $this->connection->update('ultimate_cron_log') + ->fields([ + 'name' => $log_entry->name, + 'log_type' => $log_entry->log_type, + 'start_time' => $log_entry->start_time, + 'end_time' => $log_entry->end_time, + 'init_message' => Unicode::truncate((string) $log_entry->init_message, static::MAX_TEXT_LENGTH, FALSE, TRUE), + 'message' => Unicode::truncate((string) $log_entry->message, static::MAX_TEXT_LENGTH, FALSE, TRUE), + 'severity' => $log_entry->severity, + ]) + ->condition('lid', $log_entry->lid) + ->condition('end_time', 0) + ->execute(); + if (!$updated) { + // Row was not updated, someone must have beaten us to it. + // Let's create a new log entry. + $lid = $log_entry->lid . '-' . uniqid('', TRUE); + $log_entry->message = (string) t('Lock #@original_lid was already closed and logged. Creating a new log entry #@lid', [ + '@original_lid' => $log_entry->lid, + '@lid' => $lid, + ]) . "\n" . $log_entry->message; + $log_entry->severity = $log_entry->severity >= 0 && $log_entry->severity < RfcLogLevel::ERROR ? $log_entry->severity : RfcLogLevel::ERROR; + $log_entry->lid = $lid; + + $this->save($log_entry); + } + } + catch (\Exception $e) { + // In case the insert statement above results in a database exception. + // To ensure that the causal error is written to the log, + // we try once to open a dedicated connection and write again. + if ( + // Only handle database related exceptions. + ($e instanceof DatabaseException || $e instanceof \PDOException) && + // Avoid an endless loop of re-write attempts. + $this->connection->getTarget() != 'ultimate_cron' && + !\Drupal::config('ultimate_cron')->get('bypass_transactional_safe_connection') + ) { + + $key = $this->connection->getKey(); + $info = Database::getConnectionInfo($key); + Database::addConnectionInfo($key, 'ultimate_cron', $info['default']); + $this->connection = Database::getConnection('ultimate_cron', $key); + + // Now try once to log the error again. + $this->save($log_entry); + } + else { + throw $e; + } + } + } + + /** + * Returns the method options. + * + * @return array + */ + protected function getMethodOptions() { + return array( + static::CLEANUP_METHOD_DISABLED => t('Disabled'), + static::CLEANUP_METHOD_EXPIRE => t('Remove logs older than a specified age'), + static::CLEANUP_METHOD_RETAIN => t('Retain only a specific amount of log entries'), + ); + } + +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Crontab.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Crontab.php new file mode 100644 index 0000000000000000000000000000000000000000..1795ad55a873ffaf26be5e55dad58c55724c70c1 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Crontab.php @@ -0,0 +1,193 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\ultimate_cron\CronJobInterface; +use Drupal\ultimate_cron\CronRule; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Crontab scheduler. + * + * @SchedulerPlugin( + * id = "crontab", + * title = @Translation("Crontab"), + * description = @Translation("Use crontab rules for scheduling jobs."), + * ) + */ +class Crontab extends SchedulerBase { + /** + * Default settings. + * @todo: $catch_up is randomly failing when value is low in some situation. 0 value is ignoring catch_up checks. + */ + public function defaultConfiguration() { + return array( + 'rules' => array('0+@ */3 * * *'), + 'catch_up' => '0', + ); + } + + /** + * {@inheritdoc} + */ + public function formatLabel(CronJob $job) { + return implode("\n", $this->configuration['rules']); + } + + /** + * {@inheritdoc} + */ + public function formatLabelVerbose(CronJob $job) { + $parsed = ''; + $next_schedule = NULL; + $time = REQUEST_TIME; + $skew = $this->getSkew($job); + foreach ($this->configuration['rules'] as $rule) { + $cron = CronRule::factory($rule, $time, $skew); + $parsed .= $cron->parseRule() . "\n"; + $result = $cron->getNextSchedule(); + $next_schedule = is_null($next_schedule) || $next_schedule > $result ? $result : $next_schedule; + $result = $cron->getLastSchedule(); + if ($time < $result + $this->configuration['catch_up']) { + $result = floor($time / 60) * 60 + 60; + $next_schedule = $next_schedule > $result ? $result : $next_schedule; + } + } + $parsed .= t('Next scheduled run at @datetime', array( + '@datetime' => \Drupal::service('date.formatter')->format($next_schedule, 'custom', 'Y-m-d H:i:s') + )); + return $parsed; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['rules'][0] = array( + '#title' => t("Rules"), + '#type' => 'textfield', + '#default_value' => $this->configuration['rules'], + '#description' => t('Comma separated list of crontab rules.'), + '#fallback' => TRUE, + '#required' => TRUE, + // @todo: check this out. + //'#element_validate' => array('ultimate_cron_plugin_crontab_element_validate_rule'), + ); + + $form['rules_help'] = array( + '#type' => 'fieldset', + '#title' => t('Rules help'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $form['rules_help']['info'] = array( + '#markup' => file_get_contents(drupal_get_path('module', 'ultimate_cron') . '/help/rules.html'), + ); + + $form['catch_up'] = array( + '#title' => t("Catch up"), + '#type' => 'textfield', + '#default_value' => $this->configuration['catch_up'], + '#description' => t("Don't run job after X seconds of rule."), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + $rule = $form_state->getValues()['scheduler']['configuration']['rules'][0]; + $cron = CronRule::factory($rule); + if (!$cron->isValid()) { + $form_state->setErrorByName('scheduler][configuration][rules][0', t('Rule is invalid')); + } + } + + /** + * {@inheritdoc} + */ + public function settingsFormSubmit(&$form, &$form_state, CronJob $job = NULL) { + $values = & $form_state['values']['settings'][$this->type][$this->name]; + + if (!empty($values['rules'])) { + $rules = explode(',', $values['rules']); + $values['rules'] = array_map('trim', $rules); + } + } + + /** + * {@inheritdoc} + */ + public function isScheduled(CronJob $job) { + $log_entry = isset($job->log_entry) ? $job->log_entry : $job->loadLatestLogEntry(); + $skew = $this->getSkew($job); + $class = get_class($this); + return $class::shouldRun($this->configuration['rules'], $log_entry->start_time, NULL, $this->configuration['catch_up'], $skew) ? TRUE : FALSE; + } + + /** + * {@inheritdoc} + */ + static public function shouldRun($rules, $job_last_ran, $time = NULL, $catch_up = 0, $skew = 0) { + $time = is_null($time) ? time() : $time; + foreach ($rules as $rule) { + $cron = CronRule::factory($rule, $time, $skew); + $cron_last_ran = $cron->getLastSchedule(); + + // @todo: Right now second test is failing randomly on low $catch_up value. + if ($job_last_ran < $cron_last_ran && $cron_last_ran <= $time) { + if ($time <= $cron_last_ran + $catch_up || $catch_up == 0) { + return $time - $job_last_ran; + } + } + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function isBehind(CronJob $job) { + // Disabled jobs are not behind! + if (!$job->status()) { + return FALSE; + } + + $log_entry = isset($job->log_entry) ? $job->log_entry : $job->loadLatestLogEntry(); + // If job hasn't run yet, then who are we to say it's behind its schedule? + // Check the registered time, and use that if it's available. + $job_last_ran = $log_entry->start_time; + if (!$job_last_ran) { + $registered = \Drupal::config('ultimate_cron')->get('ultimate_cron_hooks_registered'); + if (empty($registered[$job->id()])) { + return FALSE; + } + $job_last_ran = $registered[$job->id()]; + } + + $skew = $this->getSkew($job); + $next_schedule = NULL; + foreach ($this->configuration['rules'] as $rule) { + $cron = CronRule::factory($rule, $job_last_ran, $skew); + $time = $cron->getNextSchedule(); + $next_schedule = is_null($next_schedule) || $time < $next_schedule ? $time : $next_schedule; + } + $behind = REQUEST_TIME - $next_schedule; + + return $behind > $this->configuration['catch_up'] ? $behind : FALSE; + } + + /** + * Get a "unique" skew for a job. + */ + protected function getSkew(CronJob $job) { + return $job->getUniqueID() & 0xff; + } +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/SchedulerBase.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/SchedulerBase.php new file mode 100644 index 0000000000000000000000000000000000000000..8411c6e883ee72031dd730f21191419732c6de15 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/SchedulerBase.php @@ -0,0 +1,46 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler; +use Drupal\ultimate_cron\Entity\CronJob; +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\Scheduler\SchedulerInterface; + +/** + * Abstract class for Ultimate Cron schedulers + * + * A scheduler is responsible for telling Ultimate Cron whether a job should + * run or not. + * + * Abstract methods: + * isScheduled($job) + * - Check if the given job is scheduled for launch at this time. + * TRUE if it's scheduled for launch, otherwise FALSE. + * + * isBehind($job) + * - Check if the given job is behind its schedule. + * FALSE if not behind, otherwise the amount of time it's behind + * in seconds. + */ +abstract class SchedulerBase extends CronPlugin implements SchedulerInterface { + /** + * Check job schedule. + * + * @param CronJob $job + * The job to check schedule for. + * + * @return boolean + * TRUE if job is scheduled to run. + */ + abstract public function isScheduled(CronJob $job); + + /** + * Check if job is behind schedule. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $job + * The job to check schedule for. + * + * @return bool|int + * FALSE if job is behind its schedule or number of seconds behind. + */ + abstract public function isBehind(CronJob $job); +} diff --git a/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Simple.php b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Simple.php new file mode 100644 index 0000000000000000000000000000000000000000..b65b217104705991beb15c899c766405fa60b941 --- /dev/null +++ b/web/modules/ultimate_cron/src/Plugin/ultimate_cron/Scheduler/Simple.php @@ -0,0 +1,80 @@ +<?php + +namespace Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler; +use Drupal\Core\Form\FormStateInterface; +use Drupal\ultimate_cron\CronRule; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Simple scheduler. + * + * @SchedulerPlugin( + * id = "simple", + * title = @Translation("Simple"), + * description = @Translation("Provides a set of predefined intervals for scheduling."), + * ) + */ +class Simple extends Crontab { + + public $presets = array( + '* * * * *' => 60, + '*/5+@ * * * *' => 300, + '*/15+@ * * * *' => 900, + '*/30+@ * * * *' => 1800, + '0+@ * * * *' => 3600, + '0+@ */3 * * *' => 10800, + '0+@ */6 * * *' => 21600, + '0+@ */12 * * *' => 43200, + '0+@ 0 * * *' => 86400, + '0+@ 0 * * 0' => 604800, + ); + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return array( + 'rules' => array('*/15+@ * * * *'), + ) + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function settingsLabel($name, $value) { + switch ($name) { + case 'rules': + return isset($value[0]) ? \Drupal::service('date.formatter')->formatInterval($this->presets[$value[0]]) : $value; + } + return parent::settingsLabel($name, $value); + } + + /** + * {@inheritdoc} + */ + public function formatLabel(CronJob $job) { + return t('Every @interval', array( + '@interval' => \Drupal::service('date.formatter')->formatInterval($this->presets[$this->configuration['rules'][0]]), + )); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $date_formatter = \Drupal::service('date.formatter'); + $intervals = array_map(array($date_formatter, 'formatInterval'), $this->presets); + + $form['rules'][0] = array( + '#type' => 'select', + '#title' => t('Run cron every'), + '#default_value' => $this->configuration['rules'][0], + '#description' => t('Select the interval you wish cron to run on.'), + '#options' => $intervals, + '#fallback' => TRUE, + '#required' => TRUE, + ); + + return $form; + } +} diff --git a/web/modules/ultimate_cron/src/PluginCleanupInterface.php b/web/modules/ultimate_cron/src/PluginCleanupInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..4df5eee6f9c3bd1db4df0aba140dd881a08ce9c9 --- /dev/null +++ b/web/modules/ultimate_cron/src/PluginCleanupInterface.php @@ -0,0 +1,15 @@ +<?php + +namespace Drupal\ultimate_cron; + +/** + * Plugins can implement this to be invoked for regular cleanup tasks. + */ +interface PluginCleanupInterface { + + /** + * Cleans and purges data stored by this plugin. + */ + function cleanup(); + +} diff --git a/web/modules/ultimate_cron/src/Progress/Progress.php b/web/modules/ultimate_cron/src/Progress/Progress.php new file mode 100644 index 0000000000000000000000000000000000000000..859a370c6ef4fde30dea915a1462ae3e91919132 --- /dev/null +++ b/web/modules/ultimate_cron/src/Progress/Progress.php @@ -0,0 +1,57 @@ +<?php + +namespace Drupal\ultimate_cron\Progress; + +use Drupal\Core\KeyValueStore\KeyValueFactory; +use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; + +class Progress implements ProgressInterface { + protected $progressUpdated = 0; + protected $interval = 1; + + /** + * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface + */ + protected $keyValue; + + /** + * Constructor. + * + * @param float $interval + * How often the database should be updated with the progress. + */ + public function __construct(KeyValueFactoryInterface $key_value_factory, $interval = 1) { + $this->keyValue = $key_value_factory->get('uc-progress'); + $this->interval = $interval; + } + + /** + * {@inheritdoc} + */ + public function getProgress($job_id) { + $value = $this->keyValue->get($job_id); + return $value; + } + + /** + * {@inheritdoc} + */ + public function getProgressMultiple($job_ids) { + $values = $this->keyValue->getMultiple($job_ids); + + return $values; + } + + /** + * {@inheritdoc} + */ + public function setProgress($job_id, $progress) { + if (microtime(TRUE) >= $this->progressUpdated + $this->interval) { + $this->keyValue->set($job_id, $progress); + + $this->progressUpdated = microtime(TRUE); + return TRUE; + } + return FALSE; + } +} diff --git a/web/modules/ultimate_cron/src/Progress/ProgressInterface.php b/web/modules/ultimate_cron/src/Progress/ProgressInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..e4661c1f36017d7fdf75a53b103e5b465e81df50 --- /dev/null +++ b/web/modules/ultimate_cron/src/Progress/ProgressInterface.php @@ -0,0 +1,39 @@ +<?php + +namespace Drupal\ultimate_cron\Progress; + +interface ProgressInterface { + + /** + * Set job progress. + * + * @param string $job_id + * Cron Job id. + * + * @param float $progress + * The progress (0 - 1). + */ + public function setProgress($job_id, $progress); + + /** + * Get multiple job progresses. + * + * @param array $job_ids + * Job ids to get progress for. + * + * @return array + * Progress of jobs, keyed by job name. + */ + public function getProgressMultiple($job_ids); + + /** + * Get job progress. + * + * @param string $job_id + * Cron Job id. + * + * @return float + * The progress of this job. + */ + public function getProgress($job_id); +} diff --git a/web/modules/ultimate_cron/src/Progress/ProgressMemcache.php b/web/modules/ultimate_cron/src/Progress/ProgressMemcache.php new file mode 100644 index 0000000000000000000000000000000000000000..dedb9c3b4319c1ecd175f699b28180f06154fcde --- /dev/null +++ b/web/modules/ultimate_cron/src/Progress/ProgressMemcache.php @@ -0,0 +1,97 @@ +<?php + +namespace Drupal\ultimate_cron\Progress; + +use Drupal\ultimate_cron\Progress; + +class ProgressMemcache { + public $name; + public $progressUpdated = 0; + public $interval = 1; + static public $instances = array(); + + /** + * Constructor. + * + * @param string $name + * Name of job. + * @param float $interval + * How often the database should be updated with the progress. + */ + public function __construct($name, $interval = 1) { + $this->name = $name; + $this->interval = $interval; + } + + /** + * Singleton factory. + * + * @param string $name + * Name of job. + * @param float $interval + * How often the database should be updated with the progress. + * + * @return Progress + * The object. + */ + static public function factory($name, $interval = 1) { + if (!isset(self::$instances[$name])) { + self::$instances[$name] = new ProgressMemcache($name, $interval); + } + self::$instances[$name]->interval = $interval; + return self::$instances[$name]; + } + + /** + * Get job progress. + * + * @return float + * The progress of this job. + */ + public function getProgress() { + $name = 'uc-progress:' . $this->name; + $bin = variable_get('ultimate_cron_progress_memcache_bin', 'progress'); + return dmemcache_get($name, $bin); + } + + /** + * Get multiple job progresses. + * + * @param array $names + * Job names to get progress for. + * + * @return array + * Progress of jobs, keyed by job name. + */ + static public function getProgressMultiple($names) { + $keys = array(); + foreach ($names as $name) { + $keys[] = 'uc-progress:' . $name; + } + $bin = variable_get('ultimate_cron_progress_memcache_bin', 'progress'); + $values = dmemcache_get_multi($keys, $bin); + + $result = array(); + foreach ($names as $name) { + $result[$name] = isset($values['uc-progress:' . $name]) ? $values['uc-progress:' . $name] : FALSE; + } + return $result; + } + + /** + * Set job progress. + * + * @param float $progress + * The progress (0 - 1). + */ + public function setProgress($progress) { + if (microtime(TRUE) >= $this->progressUpdated + $this->interval) { + $name = 'uc-progress:' . $this->name; + $bin = variable_get('ultimate_cron_progress_memcache_bin', 'progress'); + dmemcache_set($name, $progress, 0, $bin); + $this->progressUpdated = microtime(TRUE); + return TRUE; + } + return FALSE; + } +} diff --git a/web/modules/ultimate_cron/src/ProxyClass/UltimateCron.php b/web/modules/ultimate_cron/src/ProxyClass/UltimateCron.php new file mode 100644 index 0000000000000000000000000000000000000000..ec959a614c94774c4e9036d2b611060119545c22 --- /dev/null +++ b/web/modules/ultimate_cron/src/ProxyClass/UltimateCron.php @@ -0,0 +1,75 @@ +<?php + +namespace Drupal\ultimate_cron\ProxyClass { + + /** + * Provides a proxy class for \Drupal\ultimate_cron\UltimateCron. + * + * @see \Drupal\Component\ProxyBuilder + */ + class UltimateCron implements \Drupal\Core\CronInterface + { + + use \Drupal\Core\DependencyInjection\DependencySerializationTrait; + + /** + * The id of the original proxied service. + * + * @var string + */ + protected $drupalProxyOriginalServiceId; + + /** + * The real proxied service, after it was lazy loaded. + * + * @var \Drupal\ultimate_cron\UltimateCron + */ + protected $service; + + /** + * The service container. + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * Constructs a ProxyClass Drupal proxy object. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The container. + * @param string $drupal_proxy_original_service_id + * The service ID of the original service. + */ + public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id) + { + $this->container = $container; + $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; + } + + /** + * Lazy loads the real service from the container. + * + * @return object + * Returns the constructed real service. + */ + protected function lazyLoadItself() + { + if (!isset($this->service)) { + $this->service = $this->container->get($this->drupalProxyOriginalServiceId); + } + + return $this->service; + } + + /** + * {@inheritdoc} + */ + public function run() + { + return $this->lazyLoadItself()->run(); + } + + } + +} diff --git a/web/modules/ultimate_cron/src/QueueSettings.php b/web/modules/ultimate_cron/src/QueueSettings.php new file mode 100644 index 0000000000000000000000000000000000000000..cb334d4b554483f019686b28eca0af6fcd696b26 --- /dev/null +++ b/web/modules/ultimate_cron/src/QueueSettings.php @@ -0,0 +1,377 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\ultimate_cron\Entity\CronJob; +use Drupal\ultimate_cron\TaggedSettings; + +/** + * Queue settings plugin class. + */ +class QueueSettings extends TaggedSettings { + static private $throttled = array(); + static private $queues = NULL; + + /** + * Get cron queues and static cache them. + * + * Works like module_invoke_all('cron_queue_info'), but adds + * a 'module' to each item. + * + * @return array + * Cron queue definitions. + */ + private function get_queues() { + if (!isset(self::$queues)) { + $queues = array(); + foreach (module_implements('cron_queue_info') as $module) { + $items = module_invoke($module, 'cron_queue_info'); + if (is_array($items)) { + foreach ($items as &$item) { + $item['module'] = $module; + } + $queues += $items; + } + } + drupal_alter('cron_queue_info', $queues); + self::$queues = $queues; + } + return $queues; + } + + /** + * Implements hook_cronapi(). + */ + public function cronapi() { + $items = array(); + if (!variable_get($this->key . '_enabled', TRUE)) { + return $items; + } + + // Grab the defined cron queues. + $queues = self::get_queues(); + + foreach ($queues as $name => $info) { + if (!empty($info['skip on cron'])) { + continue; + } + + $items['queue_' . $name] = array( + 'title' => t('Queue: @name', array('@name' => $name)), + 'callback' => array(get_class($this), 'worker_callback'), + 'scheduler' => array( + 'simple' => array( + 'rules' => array('* * * * *'), + ), + 'crontab' => array( + 'rules' => array('* * * * *'), + ), + ), + 'settings' => array( + 'queue' => array( + 'name' => $name, + 'worker callback' => $info['worker callback'], + ), + ), + 'tags' => array('queue', 'core', 'killable'), + 'module' => $info['module'], + ); + if (isset($info['time'])) { + $items['queue_' . $name]['settings']['queue']['time'] = $info['time']; + } + } + + return $items; + } + + /** + * Process a cron queue. + * + * This is a wrapper around the cron queues "worker callback". + * + * @param CronJob $job + * The job being run. + */ + static public function worker_callback($job) { + $settings = $job->getPluginSettings('settings'); + $queue = DrupalQueue::get($settings['queue']['name']); + $function = $settings['queue']['worker callback']; + + $end = microtime(TRUE) + $settings['queue']['time']; + $items = 0; + while (microtime(TRUE) < $end) { + if ($job->getSignal('kill')) { + \Drupal::logger('ultimate_cron')->warning('kill signal recieved'); + break; + } + + $item = $queue->claimItem($settings['queue']['lease_time']); + if (!$item) { + if ($settings['queue']['empty_delay']) { + usleep($settings['queue']['empty_delay'] * 1000000); + continue; + } + else { + break; + } + } + try { + if ($settings['queue']['item_delay']) { + if ($items == 0) { + // Move the boundary if using a throttle, to avoid waiting for nothing. + $end -= $settings['queue']['item_delay'] * 1000000; + } + else { + // Sleep before retrieving. + usleep($settings['queue']['item_delay'] * 1000000); + } + } + $function($item->data); + $queue->deleteItem($item); + $items++; + } + catch (Exception $e) { + // Just continue ... + \Drupal::logger($job->hook['module'])->error("Queue item @item_id from queue @queue failed with message @message", array( + '@item_id' => $item->item_id, + '@queue' => $settings['queue']['name'], + '@message' => $e->getMessage() + )); + } + } + \Drupal::logger($job->hook['module'])->info('Processed @items items from queue @queue', array( + '@items' => $items, + '@queue' => $settings['queue']['name'], + )); + + // Re-throttle. + $job->getPlugin('settings', 'queue')->throttle($job); + + return; + } + + /** + * Implements hook_cron_alter(). + */ + public function cron_alter(&$jobs) { + $new_jobs = array(); + foreach ($jobs as $job) { + if (!$this->isValid($job)) { + continue; + } + $settings = $job->getSettings(); + if (isset($settings['settings']['queue']['name'])) { + if ($settings['settings']['queue']['throttle']) { + for ($i = 2; $i <= $settings['settings']['queue']['threads']; $i++) { + $name = $job->id() . '_' . $i; + $hook = $job->hook; + $hook['settings']['queue']['master'] = $job->id(); + $hook['settings']['queue']['thread'] = $i; + $hook['name'] = $name; + $hook['title'] .= " (#$i)"; + $hook['immutable'] = TRUE; + $new_jobs[$name] = ultimate_cron_prepare_job($name, $hook); + $new_jobs[$name]->settings = $settings + $new_jobs[$name]->settings; + $new_jobs[$name]->title = $job->title . " (#$i)"; + } + } + } + } + $jobs += $new_jobs; + } + + /** + * Implements hook_cron_alter(). + */ + public function cron_pre_schedule($job) { + $queue_name = !empty($job->hook['settings']['queue']['name']) ? $job->hook['settings']['queue']['name'] : FALSE; + if ($queue_name) { + if (empty(self::$throttled[$job->id()])) { + self::$throttled[$job->id()] = TRUE; + $this->throttle($job); + } + } + } + + /** + * Default settings. + */ + public function defaultSettings() { + return array( + 'lease_time' => 30, + 'empty_delay' => 0, + 'item_delay' => 0, + 'throttle' => FALSE, + 'threads' => 4, + 'threshold' => 10, + 'time' => 15, + ); + } + + /** + * Settings form. + */ + public function settingsForm(&$form, &$form_state, $job = NULL) { + $elements = &$form['settings'][$this->type][$this->name]; + $values = &$form_state['values']['settings'][$this->type][$this->name]; + + $states = array(); + if (!$job) { + $elements['enabled'] = array( + '#title' => t('Enable cron queue processing'), + '#description' => t('If enabled, cron queues will be processed by this plugin. If another cron queue plugin is installed, it may be necessary/beneficial to disable this plugin.'), + '#type' => 'checkbox', + '#default_value' => variable_get($this->key . '_enabled', TRUE), + '#fallback' => TRUE, + ); + $states = array( + '#states' => array( + 'visible' => array( + ':input[name="settings[' . $this->type . '][' . $this->name . '][enabled]"]' => array( + 'checked' => TRUE, + ), + ), + ), + ); + } + + $elements['timeouts'] = array( + '#type' => 'fieldset', + '#title' => t('Timeouts'), + ) + $states; + $elements['timeouts']['lease_time'] = array( + '#parents' => array('settings', $this->type, $this->name, 'lease_time'), + '#title' => t("Queue lease time"), + '#type' => 'textfield', + '#default_value' => $values['lease_time'], + '#description' => t('Seconds to claim a cron queue item.'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + $elements['timeouts']['time'] = array( + '#parents' => array('settings', $this->type, $this->name, 'time'), + '#title' => t('Time'), + '#type' => 'textfield', + '#default_value' => $values['time'], + '#description' => t('Time in seconds to process items during a cron run.'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + $elements['delays'] = array( + '#type' => 'fieldset', + '#title' => t('Delays'), + ) + $states; + $elements['delays']['empty_delay'] = array( + '#parents' => array('settings', $this->type, $this->name, 'empty_delay'), + '#title' => t("Empty delay"), + '#type' => 'textfield', + '#default_value' => $values['empty_delay'], + '#description' => t('Seconds to delay processing of queue if queue is empty (0 = end job).'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + $elements['delays']['item_delay'] = array( + '#parents' => array('settings', $this->type, $this->name, 'item_delay'), + '#title' => t("Item delay"), + '#type' => 'textfield', + '#default_value' => $values['item_delay'], + '#description' => t('Seconds to wait between processing each item in a queue.'), + '#fallback' => TRUE, + '#required' => TRUE, + ); + + $elements['throttle'] = array( + '#title' => t('Throttle'), + '#type' => 'checkbox', + '#default_value' => $values['throttle'], + '#description' => t('Throttle queues using multiple threads.'), + ); + + $states = !$job ? $states : array( + '#states' => array( + 'visible' => array(':input[name="settings[' . $this->type . '][' . $this->name . '][throttle]"]' => array('checked' => TRUE)) + ), + ); + + $elements['throttling'] = array( + '#type' => 'fieldset', + '#title' => t('Throttling'), + ) + $states; + $elements['throttling']['threads'] = array( + '#parents' => array('settings', $this->type, $this->name, 'threads'), + '#title' => t('Threads'), + '#type' => 'textfield', + '#default_value' => $values['threads'], + '#description' => t('Number of threads to use for queues.'), + '#states' => array( + 'visible' => array(':input[name="settings[' . $this->type . '][' . $this->name . '][throttle]"]' => array('checked' => TRUE)) + ), + '#fallback' => TRUE, + '#required' => TRUE, + ); + $elements['throttling']['threshold'] = array( + '#parents' => array('settings', $this->type, $this->name, 'threshold'), + '#title' => t('Threshold'), + '#type' => 'textfield', + '#default_value' => $values['threshold'], + '#description' => t('Number of items in queue required to activate the next cron job.'), + '#states' => array( + 'visible' => array(':input[name="settings[' . $this->type . '][' . $this->name . '][throttle]"]' => array('checked' => TRUE)) + ), + '#fallback' => TRUE, + '#required' => TRUE, + ); + } + + /** + * Form submit handler. + */ + public function settingsFormSubmit(&$form, &$form_state, $job = NULL) { + if (!$job) { + $values = &$form_state['values']['settings'][$this->type][$this->name]; + variable_set($this->key . '_enabled', $values['enabled']); + unset($values['enabled']); + } + } + + /** + * Throttle queues. + * + * Enables or disables queue threads depending on remaining items in queue. + */ + public function throttle($job) { + if (!empty($job->hook['settings']['queue']['master'])) { + // We always base the threads on the master. + $master_job = ultimate_cron_job_load($job->hook['settings']['queue']['master']); + $settings = $master_job->getSettings('settings'); + } + else { + return; + } + if ($settings['queue']['throttle']) { + $queue = DrupalQueue::get($settings['queue']['name']); + $items = $queue->numberOfItems(); + $thread = $job->hook['settings']['queue']['thread']; + + $name = $master_job->name . '_' . $thread; + $status = empty($master_job->disabled) && ($items >= ($thread - 1) * $settings['queue']['threshold']); + $new_status = !$status ? TRUE : FALSE; + $old_status = ultimate_cron_job_get_status($name) ? TRUE : FALSE; + if ($old_status !== $new_status) { + $log_entry = $job->startLog(uniqid($job->id(), TRUE), 'throttling', ULTIMATE_CRON_LOG_TYPE_ADMIN); + $log_entry->log('Job @status by queue throttling (items:@items, boundary:@boundary, threshold:@threshold)', array( + '@status' => $new_status ? t('disabled') : t('enabled'), + '@items' => $items, + '@boundary' => ($thread - 1) * $settings['queue']['threshold'], + '@threshold' => $settings['queue']['threshold'], + ), RfcLogLevel::INFO); + $log_entry->finish(); + $job->dont_log = TRUE; + ultimate_cron_job_set_status($job, $new_status); + $job->disabled = $new_status; + } + } + } +} diff --git a/web/modules/ultimate_cron/src/QueueWorker.php b/web/modules/ultimate_cron/src/QueueWorker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5a819e53097b9e221a27ca9a0085225a3a1e490 --- /dev/null +++ b/web/modules/ultimate_cron/src/QueueWorker.php @@ -0,0 +1,133 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Queue\QueueWorkerManager; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Queue\RequeueException; +use Drupal\Core\Queue\SuspendQueueException; + +/** + * Defines the queue worker. + */ +class QueueWorker { + + /** + * Queue worker plugin manager + * + * @var Drupal\Core\Queue\QueueWorkerManager + */ + protected $pluginManagerQueueWorker; + + /** + * Queue Factory. + * + * @var Drupal\Core\Queue\QueueFactory + */ + protected $queue; + + /** + * Config Factory. + * + * @var Drupal\Core\Config\ConfigFactory + */ + protected $configFactory; + + /** + * Constructs a QueueWorker object. + * + * @param \Drupal\Core\Queue\QueueWorkerManager $plugin_manager_queue_worker + * @param \Drupal\Core\Queue\QueueFactory $queue + * @param \Drupal\Core\Config\ConfigFactory $config_factory + */ + public function __construct(QueueWorkerManager $plugin_manager_queue_worker, QueueFactory $queue, ConfigFactory $config_factory) { + $this->pluginManagerQueueWorker = $plugin_manager_queue_worker; + $this->queue = $queue; + $this->configFactory = $config_factory; + } + + /** + * Cron callback for queue worker cron jobs. + */ + public function queueCallback(CronJobInterface $job) { + $queue_name = str_replace(CronJobInterface::QUEUE_ID_PREFIX, '', $job->id()); + + $queue_manager = $this->pluginManagerQueueWorker; + $queue_factory = $this->queue; + + $config = $this->configFactory->get('ultimate_cron.settings'); + + $info = $queue_manager->getDefinition($queue_name); + + // Make sure every queue exists. There is no harm in trying to recreate + // an existing queue. + $queue_factory->get($queue_name)->createQueue(); + + /** @var \Drupal\Core\Queue\QueueWorkerInterface $queue_worker */ + $queue_worker = $queue_manager->createInstance($queue_name); + $end = microtime(TRUE) + (isset($info['cron']['time']) ? $info['cron']['time'] : $config->get('queue.timeouts.time')); + + /** @var \Drupal\Core\Queue\QueueInterface $queue */ + $queue = $queue_factory->get($queue_name); + $items = 0; + while (microtime(TRUE) < $end) { + // Check kill signal. + if ($job->getSignal('kill')) { + \Drupal::logger('ultimate_cron')->warning('Kill signal received for job @job_id', ['@job_id' => $job->id()]); + break; + } + + $item = $queue->claimItem($config->get('queue.timeouts.lease_time')); + + // If there is no item, check the empty delay setting and wait if + // configured. + if (!$item) { + if ($config->get('queue.delays.empty_delay')) { + usleep($config->get('queue.delays.empty_delay') * 1000000); + continue; + } + else { + break; + } + } + + try { + // We have an item, check if we need to wait. + if ($config->get('queue.delays.item_delay')) { + if ($items == 0) { + // Move the boundary if using a throttle, + // to avoid waiting for nothing. + $end -= $config->get('queue.delays.item_delay'); + } + else { + // Sleep before retrieving. + usleep($config->get('queue.delays.item_delay') * 1000000); + } + } + + $queue_worker->processItem($item->data); + $queue->deleteItem($item); + $items++; + } + catch (RequeueException $e) { + // The worker requested the task be immediately requeued. + $queue->releaseItem($item); + } + catch (SuspendQueueException $e) { + // If the worker indicates there is a problem with the whole queue, + // release the item and skip to the next queue. + $queue->releaseItem($item); + + watchdog_exception('cron', $e); + + } + catch (\Exception $e) { + // In case of any other kind of exception, log it and leave the item + // in the queue to be processed again later. + watchdog_exception('ultimate_cron_queue', $e); + } + } + } + +} diff --git a/web/modules/ultimate_cron/src/Scheduler/SchedulerInterface.php b/web/modules/ultimate_cron/src/Scheduler/SchedulerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6b878c9550ccc835ffbb7ebe92c07428ab2d8141 --- /dev/null +++ b/web/modules/ultimate_cron/src/Scheduler/SchedulerInterface.php @@ -0,0 +1,60 @@ +<?php + +namespace Drupal\ultimate_cron\Scheduler; + +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DependentPluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Defines a scheduler method. + */ +interface SchedulerInterface extends PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface, PluginFormInterface { + + /** + * Returns the default configuration. + * + * @return mixed + */ + public function defaultConfiguration(); + /** + * Label for schedule. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $job + * The job whose label should be formatted. + */ + public function formatLabel(CronJob $job); + + /** + * Label for schedule. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $job + * The job whose label should be formatted. + */ + public function formatLabelVerbose(CronJob $job); + + /** + * Check job schedule. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $job + * The job to check schedule for. + * + * @return bool + * TRUE if job is scheduled to run. + */ + public function isScheduled(CronJob $job); + + /** + * Check if job is behind schedule. + * + * @param \Drupal\ultimate_cron\Entity\CronJob $job + * The job to check schedule for. + * + * @return bool|int + * FALSE if job is behind its schedule or number of seconds behind. + */ + public function isBehind(CronJob $job); + +} diff --git a/web/modules/ultimate_cron/src/Scheduler/SchedulerManager.php b/web/modules/ultimate_cron/src/Scheduler/SchedulerManager.php new file mode 100644 index 0000000000000000000000000000000000000000..a115234d2b09e0e8695f98da60c2d389dfdf5983 --- /dev/null +++ b/web/modules/ultimate_cron/src/Scheduler/SchedulerManager.php @@ -0,0 +1,34 @@ +<?php + +namespace Drupal\ultimate_cron\Scheduler; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\payment\Plugin\Payment\OperationsProviderPluginManagerTrait; + +/** + * A plugin manager for scheduler plugins. + * + * @see \Drupal\ultimate_cron\Scheduler\SchedulerInterface + */ +class SchedulerManager extends DefaultPluginManager { + + /** + * Constructs a SchedulerManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + parent::__construct('Plugin/ultimate_cron/Scheduler', $namespaces, $module_handler, '\Drupal\ultimate_cron\Scheduler\SchedulerInterface', 'Drupal\ultimate_cron\Annotation\SchedulerPlugin'); + $this->alterInfo('ultimate_cron_scheduler_info'); + $this->setCacheBackend($cache_backend, 'ultimate_cron_scheduler'); + } + +} diff --git a/web/modules/ultimate_cron/src/Settings.php b/web/modules/ultimate_cron/src/Settings.php new file mode 100644 index 0000000000000000000000000000000000000000..b31aee3fe2b22e3d0b63ba0e625b3c00537e5bea --- /dev/null +++ b/web/modules/ultimate_cron/src/Settings.php @@ -0,0 +1,13 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Form\FormBase; + +/** + * Base class for settings. + * + * There's nothing special about this plugin. + */ +class Settings extends CronPluginMultiple { +} diff --git a/web/modules/ultimate_cron/src/Signal/SignalCache.php b/web/modules/ultimate_cron/src/Signal/SignalCache.php new file mode 100644 index 0000000000000000000000000000000000000000..6542f02a10d9328413f37c5ca8d05126693282db --- /dev/null +++ b/web/modules/ultimate_cron/src/Signal/SignalCache.php @@ -0,0 +1,106 @@ +<?php + +namespace Drupal\ultimate_cron\Signal; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\ultimate_cron\Signal\SignalInterface; + +class SignalCache implements SignalInterface { + + /** + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + public $cacheBackend; + + /** + * @var \Drupal\Core\Lock\LockBackendInterface + */ + public $lockBackend; + + public function __construct(CacheBackendInterface $cache_backend, LockBackendInterface $lock_backend) { + $this->cacheBackend = $cache_backend; + $this->lockBackend = $lock_backend; + } + + /** + * Get a signal without claiming it. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return string + * The signal if any. + */ + public function peek($job_id, $signal) { + $cache = $this->cacheBackend->get("signal-$job_id-$signal"); + if ($cache) { + $flushed = $this->cacheBackend->get("flushed-$job_id"); + if (!$flushed || $cache->created > $flushed->created) { + return $cache->data; + } + } + return FALSE; + } + + /** + * Get and claim signal. + * + * @param string $name + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return string + * The signal if any. If a signal is found, it is "claimed" and therefore + * cannot be claimed again. + */ + public function get($job_id, $signal) { + if ($this->lockBackend->acquire("signal-$job_id-$signal")) { + $result = self::peek($job_id, $signal); + self::clear($job_id, $signal); + $this->lockBackend->release("signal-$job_id-$signal"); + return $result; + } + return FALSE; + } + + /** + * Set signal. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return boolean + * TRUE if the signal was set. + */ + public function set($job_id, $signal) { + $this->cacheBackend->set("signal-$job_id-$signal", TRUE); + } + + /** + * Clear signal. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + */ + public function clear($job_id, $signal) { + $this->cacheBackend->delete("signal-$job_id-$signal"); + } + + /** + * Clear signals. + * + * @param string $job_id + * The name of the job. + */ + public function flush($job_id) { + $this->cacheBackend->set("flushed-$job_id", microtime(TRUE)); + } +} diff --git a/web/modules/ultimate_cron/src/Signal/SignalInterface.php b/web/modules/ultimate_cron/src/Signal/SignalInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..3da154001ebbe64a896f164140658aed38f77308 --- /dev/null +++ b/web/modules/ultimate_cron/src/Signal/SignalInterface.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\ultimate_cron\Signal; + +interface SignalInterface { + /** + * Get a signal without claiming it. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return string + * The signal if any. + */ + public function peek($job_id, $signal); + + /** + * Set signal. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + * + * @return boolean + * TRUE if the signal was set. + */ + public function set($job_id, $signal); + + /** + * Get and claim signal. + * + * @param $job_id + * @param string $signal + * The name of the signal. + * + * @internal param string $name The name of the job.* The name of the job. + * @return string + * The signal if any. If a signal is found, it is "claimed" and therefore + * cannot be claimed again. + */ + public function get($job_id, $signal); + + /** + * Clear signals. + * + * @param string $job_id + * The name of the job. + */ + public function flush($job_id); + + /** + * Clear signal. + * + * @param string $job_id + * The name of the job. + * @param string $signal + * The name of the signal. + */ + public function clear($job_id, $signal); +} diff --git a/web/modules/ultimate_cron/src/TaggedSettings.php b/web/modules/ultimate_cron/src/TaggedSettings.php new file mode 100644 index 0000000000000000000000000000000000000000..4cae649a736a126d553fee5c81092cdd7a23c11d --- /dev/null +++ b/web/modules/ultimate_cron/src/TaggedSettings.php @@ -0,0 +1,20 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Base class for tagged settings. + * + * Settings plugins using this as a base class, will only be available + * to jobs having the same tag as the name of the plugin. + */ +class TaggedSettings extends Settings { + /** + * Only valid for jobs tagged with the proper tag. + */ + public function isValid($job = NULL) { + return $job ? in_array($this->name, $job->hook['tags']) : \Drupal\ultimate_cron\parent::isValid(); + } +} diff --git a/web/modules/ultimate_cron/src/UltimateCron.php b/web/modules/ultimate_cron/src/UltimateCron.php new file mode 100644 index 0000000000000000000000000000000000000000..c8010621e5846ef63419840934410c3def50f094 --- /dev/null +++ b/web/modules/ultimate_cron/src/UltimateCron.php @@ -0,0 +1,76 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Cron; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Queue\QueueWorkerManagerInterface; +use Drupal\Core\Session\AccountSwitcherInterface; +use Drupal\Core\State\StateInterface; +use Drupal\ultimate_cron\Entity\CronJob; +use Psr\Log\LoggerInterface; + +/** + * The Ultimate Cron service. + */ +class UltimateCron extends Cron { + + /** + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Sets the config factory for ultimate cron service. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + */ + public function setConfigFactory(ConfigFactoryInterface $config_factory) { + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function run() { + + // Load the cron jobs in the right order. + $job_ids = \Drupal::entityQuery('ultimate_cron_job') + ->condition('status', TRUE) + ->sort('weight', 'ASC') + + ->execute(); + + $launcher_jobs = array(); + foreach (CronJob::loadMultiple($job_ids) as $job) { + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.' . 'launcher'); + $launcher = $manager->createInstance($job->getLauncherId()); + $launcher_definition = $launcher->getPluginDefinition(); + + if (!isset($launchers) || in_array($launcher->getPluginId(), $launchers)) { + $launcher_jobs[$launcher_definition['id']]['launcher'] = $launcher; + $launcher_jobs[$launcher_definition['id']]['sort'] = array($launcher_definition['weight']); + $launcher_jobs[$launcher_definition['id']]['jobs'][$job->id()] = $job; + $launcher_jobs[$launcher_definition['id']]['jobs'][$job->id()]->sort = array($job->loadLatestLogEntry()->start_time); + } + } + + foreach ($launcher_jobs as $name => $launcher_job) { + $launcher_job['launcher']->launchJobs($launcher_job['jobs']); + } + + // Run standard queue processing if our own handling is disabled. + if (!$this->configFactory->get('ultimate_cron.settings')->get('queue.enabled')) { + $this->processQueues(); + } + + $this->setCronLastTime(); + + return TRUE; + } +} diff --git a/web/modules/ultimate_cron/src/UltimateCronDatabaseFactory.php b/web/modules/ultimate_cron/src/UltimateCronDatabaseFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..7dad5ff3925e712d2dd740264332086924b2c007 --- /dev/null +++ b/web/modules/ultimate_cron/src/UltimateCronDatabaseFactory.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\Database\Database; + +/** + * Class DatabaseFactory + */ +class UltimateCronDatabaseFactory { + /** + * Factory method that returns a Connection object with the correct target. + * + * @return \Drupal\Core\Database\Connection + * The connection object. + */ + public static function getConnection() { + $target = _ultimate_cron_get_transactional_safe_connection(); + return Database::getConnection($target); + } + +} diff --git a/web/modules/ultimate_cron/src/UltimateCronServiceProvider.php b/web/modules/ultimate_cron/src/UltimateCronServiceProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..2c4fcb5a47cd56112832f8e5ca0558a106604def --- /dev/null +++ b/web/modules/ultimate_cron/src/UltimateCronServiceProvider.php @@ -0,0 +1,24 @@ +<?php + +namespace Drupal\ultimate_cron; + +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DependencyInjection\ServiceProviderBase; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Defines service provider for ultimate cron. + */ +class UltimateCronServiceProvider extends ServiceProviderBase { + + /** + * {@inheritdoc} + */ + public function alter(ContainerBuilder $container) { + // Overrides cron class to use our own cron manager. + $container->getDefinition('cron') + ->setClass('Drupal\ultimate_cron\UltimateCron') + ->addMethodCall('setConfigFactory', [new Reference('config.factory')]); + } + +} diff --git a/web/modules/ultimate_cron/templates/page-admin-settings-cron-log.tpl.php b/web/modules/ultimate_cron/templates/page-admin-settings-cron-log.tpl.php new file mode 100644 index 0000000000000000000000000000000000000000..c8a49fa4a0d496c186607eeef9f238d87b4a0983 --- /dev/null +++ b/web/modules/ultimate_cron/templates/page-admin-settings-cron-log.tpl.php @@ -0,0 +1,17 @@ +<?php +/** + * @file + */ +?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php print $language->language ?>" lang="<?php print $language->language ?>" dir="<?php print $language->dir ?>"> + <head> + <?php print $head ?> + <title><?php print $head_title ?></title> + <?php print $styles ?> + <?php print $scripts ?> + </head> + <body> + <?php print $content ?> + </body> +</html> diff --git a/web/modules/ultimate_cron/tests/src/Functional/CronJobFormTest.php b/web/modules/ultimate_cron/tests/src/Functional/CronJobFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5c0d13109ea30abd886f71e6fbedcd9e127f38a6 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Functional/CronJobFormTest.php @@ -0,0 +1,218 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\Traits\Core\CronRunTrait; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Cron Job Form Testing. + * + * @group ultimate_cron + */ +class CronJobFormTest extends BrowserTestBase { + + use CronRunTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron', 'block', 'cron_queue_test'); + + /** + * A user with permission to create and edit books and to administer blocks. + * + * @var \Drupal\user\Entity\User + */ + protected $adminUser; + + /** + * Cron job name. + * + * @var string + */ + protected $jobName; + + /** + * Cron job machine id. + * + * @var string + */ + protected $jobId = 'system_cron'; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'classy'; + + /** + * Tests adding and editing a cron job. + */ + public function testManageJob() { + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + + // Create user with correct permission. + $this->adminUser = $this->drupalCreateUser(array('administer ultimate cron', 'administer site configuration')); + $this->drupalLogin($this->adminUser); + + // Cron Jobs overview. + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertResponse('200'); + + // Check for the default schedule message in Job list. + $this->assertText('Every 15 min'); + // Check for the Last Run default value. + $this->assertText('Never'); + + // Start editing added job. + $this->drupalGet('admin/config/system/cron/jobs/manage/' . $this->jobId); + $this->assertResponse('200'); + + // Set new cron job configuration and save the old job name. + $job = CronJob::load($this->jobId); + $old_job_name = $job->label(); + $this->jobName = 'edited job name'; + $edit = array('title' => $this->jobName); + + // Save the new job. + $this->drupalPostForm(NULL, $edit, t('Save')); + // Assert the edited Job hasn't run yet. + $this->assertText('Never'); + // Assert messenger service message for successful updated job. + $this->assertText(t('job @name has been updated.', array('@name' => $this->jobName))); + + // Run the Jobs. + $this->cronRun(); + + // Assert the cron jobs have been run by checking the time. + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertText(\Drupal::service('date.formatter')->format(\Drupal::state()->get('system.cron_last'), 'short'), 'Created Cron jobs have been run.'); + + // Check that all jobs have been run. + $this->assertNoText("Never"); + + // Assert cron job overview for recently updated job. + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertNoText($old_job_name); + $this->assertText($this->jobName); + + // Change time when cron runs, check the 'Scheduled' label is updated. + $this->clickLink(t('Edit')); + $this->drupalPostForm(NULL, ['scheduler[configuration][rules][0]' => '0+@ */6 * * *'], t('Save')); + $this->assertText('Every 6 hours'); + + // Test disabling a job. + $this->clickLink(t('Disable'), 0); + $this->assertText('This cron job will no longer be executed.'); + $this->drupalPostForm(NULL, NULL, t('Disable')); + + // Assert messenger service message for successful disabled job. + $this->assertText(t('Disabled cron job @name.', array('@name' => $this->jobName))); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[6]', 'Disabled'); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Enable'); + $this->assertNoFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Run'); + + // Test enabling a job. + $this->clickLink(t('Enable'), 0); + $this->assertText('This cron job will be executed again.'); + $this->drupalPostForm(NULL, NULL, t('Enable')); + + // Assert messenger service message for successful enabled job. + $this->assertText(t('Enabled cron job @name.', array('@name' => $this->jobName))); + $this->drupalGet('admin/config/system/cron/jobs'); + $expected_checkmark_image_url = file_url_transform_relative(file_create_url('core/misc/icons/73b355/check.svg')); + $this->assertEquals($expected_checkmark_image_url, $this->xpath('//table/tbody/tr[1]/td[6]/img')[0]->getAttribute('src')); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Run'); + + // Test disabling a job with the checkbox on the edit page. + $edit = array( + 'status' => FALSE, + ); + $this->drupalPostForm('admin/config/system/cron/jobs/manage/' . $this->jobId, $edit, t('Save')); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[6]', 'Disabled'); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Enable'); + $this->assertNoFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Run'); + + // Test enabling a job with the checkbox on the edit page. + $edit = array( + 'status' => TRUE, + ); + $this->drupalPostForm('admin/config/system/cron/jobs/manage/' . $this->jobId, $edit, t('Save')); + $this->assertEquals($expected_checkmark_image_url, $this->xpath('//table/tbody/tr[1]/td[6]/img')[0]->getAttribute('src')); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li[1]/a', 'Run'); + + $this->drupalGet('admin/config/system/cron/jobs'); + + // Save new job. + $this->clickLink(t('Edit'), 0); + $job_configuration = array( + 'scheduler[id]' => 'crontab', + ); + $this->drupalPostForm(NULL, $job_configuration, t('Save')); + $this->drupalPostForm('admin/config/system/cron/jobs/manage/' . $this->jobId, ['scheduler[configuration][rules][0]' => '0+@ * * * *'], t('Save')); + $this->assertText('0+@ * * * *'); + + // Try editing the rule to an invalid one. + $this->clickLink('Edit'); + $this->drupalPostForm(NULL, ['scheduler[configuration][rules][0]' => '*//15+@ *-2 * * *'], t('Save')); + $this->assertText('Rule is invalid'); + $this->assertTitle('Edit job | Drupal'); + + // Assert that there is no Delete link on the details page. + $this->assertNoLink('Delete'); + + // Force a job to be invalid by changing the callback. + $job = CronJob::load($this->jobId); + $job->setCallback('non_existing_function') + ->save(); + $this->drupalGet('admin/config/system/cron/jobs'); + + // Assert that the invalid cron job is displayed properly. + $this->assertFieldByXPath('//table/tbody/tr[1]/td[6]', 'Missing'); + $this->assertFieldByXPath('//table/tbody/tr[1]/td[8]/div/div/ul/li/a', 'Delete'); + + // Test deleting a job (only possible if invalid cron job). + $this->clickLink(t('Delete'), 0); + $this->drupalPostForm(NULL, NULL, t('Delete')); + $this->assertText(t('The cron job @name has been deleted.', array('@name' => $job->label()))); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertNoText($job->label()); + + $job = CronJob::load('ultimate_cron_cron'); + + $log_entries = $job->getLogEntries(); + $log_entry = reset($log_entries); + + // Test logs details page. + $this->drupalGet('admin/config/system/cron/jobs'); + $this->clickLink('Logs'); + $xpath = $this->xpath('//tbody/tr[@class="odd"]/td'); + $start_time = \Drupal::service('date.formatter')->format($log_entry->start_time, 'custom', 'Y-m-d H:i:s'); + $end_time = \Drupal::service('date.formatter')->format($log_entry->end_time, 'custom', 'Y-m-d H:i:s'); + $this->assertEqual($xpath[1]->getText(), $start_time); + $this->assertEqual($xpath[2]->getText(), $end_time); + // The message logged depends on timing, do not hardcode that. + $this->assertEqual($xpath[3]->getText(), $log_entry->message ?: $log_entry->formatInitMessage()); + $this->assertEqual($xpath[4]->getText(), '00:00'); + + // Assert queue cron jobs. + $this->config('ultimate_cron.settings') + ->set('queue.enabled', TRUE) + ->save(); + + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertText('Queue: Broken queue test'); + + $this->drupalGet('admin/config/system/cron/jobs/manage/ultimate_cron_queue_cron_queue_test_broken_queue'); + $this->assertFieldByName('title', 'Queue: Broken queue test'); + $this->drupalPostForm(NULL, [], 'Save'); + $this->assertText('job Queue: Broken queue test has been updated.'); + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Functional/CronJobInstallTest.php b/web/modules/ultimate_cron/tests/src/Functional/CronJobInstallTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f89dd34c12b9c1cf9b73b9d7f858573b8777d3fe --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Functional/CronJobInstallTest.php @@ -0,0 +1,107 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Functional; + +use Drupal\Core\Url; +use Drupal\Tests\BrowserTestBase; +use Drupal\ultimate_cron\CronRule; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Cron Job Form Testing + * + * @group ultimate_cron + */ +class CronJobInstallTest extends BrowserTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron'); + + /** + * A user with permission to create and edit books and to administer blocks. + * + * @var \Drupal\user\Entity\User + */ + protected $adminUser; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Tests adding and editing a cron job. + */ + public function testManageJob() { + // Create user with correct permission. + $this->adminUser = $this->drupalCreateUser(array('administer ultimate cron')); + $this->drupalLogin($this->adminUser); + + // Check default modules + \Drupal::service('module_installer')->install(array('field')); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertText('Purges deleted Field API data'); + $this->assertText('Cleanup (caches, batch, flood, temp-files, etc.)'); + $this->assertNoText('Deletes temporary files'); + + // Install new module. + \Drupal::service('module_installer')->install(array('file')); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertText('Deletes temporary files'); + + // Uninstall new module. + \Drupal::service('module_installer')->uninstall(array('file')); + $this->drupalGet('admin/config/system/cron/jobs'); + $this->assertNoText('Deletes temporary files'); + } + + /** + * Tests the requirements checking of ultimate_cron. + */ + public function testRequirements() { + $element = ultimate_cron_requirements('runtime')['cron_jobs']; + $this->assertEqual($element['value'], t("Cron is running properly.")); + $this->assertEqual($element['severity'], REQUIREMENT_OK); + + + $values = array( + 'title' => 'ultimate cron fake cronjob title', + 'id' => 'ultimate_cron_fake_job', + 'module' => 'ultimate_cron_fake', + 'callback' => 'ultimate_cron_fake_cron', + ); + + $job = new CronJob($values, 'ultimate_cron_job'); + $job->save(); + + \Drupal::service('cron')->run(); + + // Generate an initial scheduled cron time. + $cron = CronRule::factory('*/15+@ * * * *', time(), $job->getUniqueID() & 0xff); + $scheduled_cron_time = $cron->getLastSchedule(); + // Generate a new start time by adding two seconds to the initial scheduled cron time. + $log_entry_past = $scheduled_cron_time - 10000; + \Drupal::database()->update('ultimate_cron_log') + ->fields([ + 'start_time' => $log_entry_past, + ]) + ->condition('name', $values['id']) + ->execute(); + + // Check run counter, at this point there should be 0 run. + $this->assertEqual(1, \Drupal::state()->get('ultimate_cron.cron_run_counter'), 'Job has run once.'); + $this->assertNotEmpty($job->isBehindSchedule(), 'Job is behind schedule.'); + + $element = ultimate_cron_requirements('runtime')['cron_jobs']; + $this->assertEqual($element['value'], '1 job is behind schedule', '"1 job is behind schedule." is displayed'); + $this->assertEqual($element['description']['#markup'], 'Some jobs are behind their schedule. Please check if <a href="' . + Url::fromRoute('system.cron', ['key' => \Drupal::state()->get('system.cron_key')])->toString() . + '">Cron</a> is running properly.', 'Description is correct.'); + $this->assertEqual($element['severity'], REQUIREMENT_WARNING, 'Severity is of level "Error"'); + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Functional/LoggerWebTest.php b/web/modules/ultimate_cron/tests/src/Functional/LoggerWebTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1b274551ec19dcb9e5659a347fb624a5cc439228 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Functional/LoggerWebTest.php @@ -0,0 +1,208 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Functional; + +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\Listeners\DeprecationListenerTrait; +use Drupal\Tests\Traits\Core\CronRunTrait; + +/** + * Tests that scheduler plugins are discovered correctly. + * + * @group ultimate_cron + */ +class LoggerWebTest extends BrowserTestBase { + + use CronRunTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['ultimate_cron', 'ultimate_cron_logger_test']; + + /** + * A user with permissions to administer and run cron jobs. + * + * @var \Drupal\user\Entity\User + */ + protected $user; + + /** + * Flag to control if errors should be ignored or not. + * + * @var bool + */ + protected $ignoreErrors = FALSE; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->user = $this->createUser([ + 'administer ultimate cron', + 'view cron jobs', + 'run cron jobs', + ]); + $this->drupalLogin($this->user); + } + + /** + * Tests that the logger handles an exception correctly. + */ + public function testLoggerException() { + + \Drupal::state()->set('ultimate_cron_logger_test_cron_action', 'exception'); + + // Run cron to get an exception from ultimate_cron_logger_test module. + $this->cronRun(); + + // Check that the error message is displayed in its log page. + $this->drupalGet('admin/config/system/cron/jobs/logs/ultimate_cron_logger_test_cron'); + $this->assertRaw('/core/misc/icons/e32700/error.svg'); + $this->assertRaw('<em class="placeholder">Exception</em>: Test cron exception in <em class="placeholder">ultimate_cron_logger_test_cron()</em> (line'); + } + + /** + * Tests that the logger handles an exception correctly. + */ + public function testLoggerFatal() { + + \Drupal::state()->set('ultimate_cron_logger_test_cron_action', 'fatal'); + + // Run cron to get an exception from ultimate_cron_logger_test module. + $this->ignoreErrors = TRUE; + $this->cronRun(); + $this->ignoreErrors = FALSE; + + // Check that the error message is displayed in its log page. + $this->drupalGet('admin/config/system/cron/jobs/logs/ultimate_cron_logger_test_cron'); + $this->assertRaw('/core/misc/icons/e32700/error.svg'); + $this->assertRaw('Call to undefined function call_to_undefined_function'); + + // Empty the logfile, our fatal errors are expected. + $filename = DRUPAL_ROOT . '/' . $this->siteDirectory . '/error.log'; + file_put_contents($filename, ''); + } + + /** + * Tests that the logger handles long message correctly. + */ + public function testLoggerLongMessage() { + + \Drupal::state()->set('ultimate_cron_logger_test_cron_action', 'long_message'); + + // Run cron to get a long message log from ultimate_cron_logger_test. + $this->cronRun(); + + // Check that the long log message is properly trimmed. + $this->drupalGet('admin/config/system/cron/jobs/logs/ultimate_cron_logger_test_cron'); + $xpath = $this->xpath('//table/tbody/tr/td[4]'); + // The last 2 chars from xpath are not related to the message. + $this->assertTrue(strlen(substr($xpath[0]->getText(), 0, -2)) == 5000); + $this->assertRaw('This is a v…'); + } + + /** + * Tests that the logger handles an exception correctly. + */ + public function testLoggerLogWarning() { + + \Drupal::state()->set('ultimate_cron_logger_test_cron_action', 'log_warning'); + + // Run cron to get an exception from ultimate_cron_logger_test module. + $this->cronRun(); + + // Check that the error message is displayed in its log page. + $this->drupalGet('admin/config/system/cron/jobs/logs/ultimate_cron_logger_test_cron'); + $this->assertRaw('/core/misc/icons/e29700/warning.svg'); + $this->assertRaw('This is a warning message'); + } + + + /** + * Tests that the logger handles an exception correctly. + */ + public function testLoggerNormal() { + // Run cron to get an exception from ultimate_cron_logger_test module. + $this->cronRun(); + + // Check that the error message is displayed in its log page. + $this->drupalGet('admin/config/system/cron/jobs/logs/ultimate_cron_logger_test_cron'); + $this->assertRaw('/core/misc/icons/73b355/check.svg'); + $this->assertText('Launched in thread 1'); + } + + /** + * Reads headers and registers errors received from the tested site. + * + * Overriden to not report fatal errors if $this->ignoreErrors is set to TRUE. + * + * @param $curlHandler + * The cURL handler. + * @param $header + * An header. + * + * @see _drupal_log_error() + */ + protected function curlHeaderCallback($curlHandler, $header) { + // Header fields can be extended over multiple lines by preceding each + // extra line with at least one SP or HT. They should be joined on receive. + // Details are in RFC2616 section 4. + if ($header[0] == ' ' || $header[0] == "\t") { + // Normalize whitespace between chucks. + $this->headers[] = array_pop($this->headers) . ' ' . trim($header); + } + else { + $this->headers[] = $header; + } + + // Errors are being sent via X-Drupal-Assertion-* headers, + // generated by _drupal_log_error() in the exact form required + // by \Drupal\simpletest\WebTestBase::error(). + if (!$this->ignoreErrors && preg_match('/^X-Drupal-Assertion-[0-9]+: (.*)$/', $header, $matches)) { + $parameters = unserialize(urldecode($matches[1])); + // Handle deprecation notices triggered by system under test. + if ($parameters[1] === 'User deprecated function') { + if (getenv('SYMFONY_DEPRECATIONS_HELPER') !== 'disabled') { + $message = (string) $parameters[0]; + if (!in_array($message, DeprecationListenerTrait::getSkippedDeprecations())) { + call_user_func_array([&$this, 'error'], $parameters); + } + } + } + else { + // Call \Drupal\simpletest\WebTestBase::error() with the parameters from + // the header. + call_user_func_array([&$this, 'error'], $parameters); + } + } + + // Save cookies. + if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) { + $name = $matches[1]; + $parts = array_map('trim', explode(';', $matches[2])); + $value = array_shift($parts); + $this->cookies[$name] = ['value' => $value, 'secure' => in_array('secure', $parts)]; + if ($name === $this->getSessionName()) { + if ($value != 'deleted') { + $this->sessionId = $value; + } + else { + $this->sessionId = NULL; + } + } + } + + // This is required by cURL. + return strlen($header); + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/CronJobKernelTest.php b/web/modules/ultimate_cron/tests/src/Kernel/CronJobKernelTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2124810dccbc393c3676024da998150d08a9e59f --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/CronJobKernelTest.php @@ -0,0 +1,57 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Tests CRUD for cron jobs. + * + * @group ultimate_cron + */ +class CronJobKernelTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('system', 'ultimate_cron'); + + protected function setup() { + parent::setUp(); + + $this->installSchema('ultimate_cron', [ + 'ultimate_cron_log', + 'ultimate_cron_lock', + ]); + } + + /** + * Tests CRUD operations for cron jobs. + */ + public function testCRUD() { + $values = array( + 'id' => 'example', + 'title' => $this->randomMachineName(), + 'description' => $this->randomMachineName(), + ); + + /** @var \Drupal\ultimate_cron\Entity\CronJob $cron_job */ + $cron_job = CronJob::create($values); + $cron_job->save(); + + $this->assertEquals('example', $cron_job->id()); + $this->assertEquals($values['title'], $cron_job->label()); + $this->assertTrue($cron_job->status()); + + $cron_job->disable(); + $cron_job->save(); + + $cron_job = CronJob::load('example'); + $this->assertEquals('example', $cron_job->id()); + $this->assertFalse($cron_job->status()); + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/CronJobTest.php b/web/modules/ultimate_cron/tests/src/Kernel/CronJobTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fdea092712160af725745cd4f6e286ae269225b9 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/CronJobTest.php @@ -0,0 +1,98 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\ultimate_cron\CronRule; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Cron Job Testing if a job runs when it is supposed to. + * + * @group ultimate_cron + */ +class CronJobTest extends KernelTestBase { + + public static $modules = array('ultimate_cron'); + + public function setup() { + parent::setUp(); + + $this->installSchema('ultimate_cron', array( + 'ultimate_cron_log', + 'ultimate_cron_lock' + )); + } + + /** + * Tests adding and editing a cron job. + */ + function testGeneratedJob() { + $values = array( + 'title' => 'ultimate cron fake cronjob title', + 'id' => 'ultimate_cron_fake_job', + 'module' => 'ultimate_cron_fake', + 'callback' => 'ultimate_cron_fake_cron', + ); + + $job = CronJob::create($values); + $job->save(); + + // Check the latest log entry, there should be none. + $this->assertEquals(0, CronJob::load($values['id'])->loadLatestLogEntry()->start_time); + + // Check run counter, at this point there should be 0 runs. + $this->assertEquals(0, \Drupal::state()->get('ultimate_cron.cron_run_counter')); + + // Run cron manually for the first time. + \Drupal::service('cron')->run(); + + // Check run counter, at this point there should be 1 run. + $this->assertEquals(1, \Drupal::state()->get('ultimate_cron.cron_run_counter')); + + // Generate an initial scheduled cron time. + $cron = CronRule::factory('*/15+@ * * * *', time(), $job->getUniqueID() & 0xff); + $scheduled_cron_time = $cron->getLastSchedule(); + + // Load Latest log entry time. + $latest_log_entry = CronJob::load($values['id'])->loadLatestLogEntry()->start_time; + + // Latest log entry should not be 0 because it ran already. + $this->assertNotEquals(0, $latest_log_entry); + + // Generate a new start time by adding two seconds to the initial scheduled cron time. + $log_entry_future = $scheduled_cron_time + 2; + + // Update new start_time in the future so the next cron job should not run. + \Drupal::database()->update('ultimate_cron_log') + ->fields(array('start_time' => $log_entry_future)) + ->condition('name', $values['id']) + ->execute(); + + // Cron job should not run. + \Drupal::service('cron')->run(); + + // Load latest log entry. + $cron_last_ran = CronJob::load($values['id'])->loadLatestLogEntry()->start_time; + + // Check if job ran, it shouldn't have. + $this->assertEquals($log_entry_future, $cron_last_ran); + $this->assertEquals(1, \Drupal::state()->get('ultimate_cron.cron_run_counter')); + + // Generate a new start time by deducting two seconds from the initial scheduled cron time. + $log_entry_past = $scheduled_cron_time - 2; + + // Update new start_time in the past so the next cron job should run. + \Drupal::database()->update('ultimate_cron_log') + ->fields(array('start_time' => $log_entry_past)) + ->condition('name', $values['id']) + ->execute(); + + // Cron job should run. + \Drupal::service('cron')->run(); + + // Check if the cron job has run, it should have. + $this->assertNotEquals($log_entry_past, $latest_log_entry); + $this->assertEquals(2, \Drupal::state()->get('ultimate_cron.cron_run_counter')); + } +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/LauncherPluginTest.php b/web/modules/ultimate_cron/tests/src/Kernel/LauncherPluginTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c170a8c88abffa75504d9412003dbfcd9cd11f4 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/LauncherPluginTest.php @@ -0,0 +1,36 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Launcher\SerialLauncher; + +/** + * Tests the default scheduler plugins. + * + * @group ultimate_cron + */ +class LauncherPluginTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron'); + + /** + * Tests that scheduler plugins are discovered correctly. + */ + function testDiscovery() { + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.launcher'); + + $plugins = $manager->getDefinitions(); + $this->assertCount(1, $plugins); + + $serial = $manager->createInstance('serial'); + $this->assertTrue($serial instanceof SerialLauncher); + $this->assertEquals('serial', $serial->getPluginId()); + } +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/LoggerPluginTest.php b/web/modules/ultimate_cron/tests/src/Kernel/LoggerPluginTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3d0f2f8059e61899af9d7059a5bf2cd29e28c4f8 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/LoggerPluginTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\ultimate_cron\Entity\CronJob; +use Drupal\ultimate_cron\Logger\LogEntry; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Logger\CacheLogger; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Logger\DatabaseLogger; + +/** + * Tests the default scheduler plugins. + * + * @group ultimate_cron + */ +class LoggerPluginTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron', 'ultimate_cron_logger_test', 'system'); + + /** + * Tests that scheduler plugins are discovered correctly. + */ + function testDiscovery() { + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.logger'); + + $plugins = $manager->getDefinitions(); + $this->assertCount(2, $plugins); + + $cache = $manager->createInstance('cache'); + $this->assertTrue($cache instanceof CacheLogger); + $this->assertEquals('cache', $cache->getPluginId()); + + $database = $manager->createInstance('database'); + $this->assertTrue($database instanceof DatabaseLogger); + $this->assertEquals('database', $database->getPluginId()); + } + + /** + * Tests log cleanup of the database logger. + */ + function testCleanup() { + + $this->installSchema('ultimate_cron', ['ultimate_cron_log', 'ultimate_cron_lock']); + + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); + + $job = CronJob::load('ultimate_cron_logger_test_cron'); + $job->setConfiguration('logger', [ + 'retain' => 10, + ]); + $job->save(); + + // Run the job 12 times. + for ($i = 0; $i < 12; $i++) { + $job->getPlugin('launcher')->launch($job); + } + + // There are 12 run log entries and one from the modified job. + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, 15); + $this->assertCount(13, $log_entries); + + // Run cleanup. + ultimate_cron_cron(); + + // There should be exactly 10 log entries now. + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, 15); + $this->assertCount(10, $log_entries); + + // Switch to expire-based cleanup. + $job->setConfiguration('logger', [ + 'expire' => 60, + 'method' => DatabaseLogger::CLEANUP_METHOD_EXPIRE, + ]); + $job->save(); + + $ids = array_slice(array_keys($log_entries), 5); + + // Date back 5 log entries. + \Drupal::database()->update('ultimate_cron_log') + ->expression('start_time', 'start_time - 65') + ->condition('lid', $ids, 'IN') + ->execute(); + + // Run cleanup. + ultimate_cron_cron(); + + // There should be exactly 6 log entries now, as saving caused another + // modified entry to be saved. + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, 15); + $this->assertCount(6, $log_entries); + } + + /** + * Tests cache logger. + */ + function testCacheLogger() { + // @todo Set default logger and do not enable the log table. + $this->installSchema('ultimate_cron', ['ultimate_cron_log', 'ultimate_cron_lock']); + + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); + + $job = CronJob::load('ultimate_cron_logger_test_cron'); + $job->setLoggerId('cache'); + $job->save(); + + // Launch the job twice. + $job->getPlugin('launcher')->launch($job); + $job->getPlugin('launcher')->launch($job); + + // There is only one log entry. + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, 3); + $this->assertCount(1, $log_entries); + + $log_entry = reset($log_entries); + $this->assertTrue($log_entry instanceof LogEntry); + $this->assertEquals('ultimate_cron_logger_test_cron', $log_entry->name); + $this->assertEquals('Launched manually by anonymous (0)', (string) $log_entry->formatInitMessage()); + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/SchedulerPluginTest.php b/web/modules/ultimate_cron/tests/src/Kernel/SchedulerPluginTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9ea0fe88cc1bcae1621e118dd683b5161a3c3e07 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/SchedulerPluginTest.php @@ -0,0 +1,41 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\KernelTests\KernelTestBase; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler\Crontab; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler\Simple; + +/** + * Tests the default scheduler plugins. + * + * @group ultimate_cron + */ +class SchedulerPluginTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron'); + + /** + * Tests that scheduler plugins are discovered correctly. + */ + function testDiscovery() { + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.scheduler'); + + $plugins = $manager->getDefinitions(); + $this->assertCount(2, $plugins); + + $simple = $manager->createInstance('simple'); + $this->assertTrue($simple instanceof Simple); + $this->assertEquals('simple', $simple->getPluginId()); + + $crontab = $manager->createInstance('crontab'); + $this->assertTrue($crontab instanceof Crontab); + $this->assertEquals('crontab', $crontab->getPluginId()); + } +} diff --git a/web/modules/ultimate_cron/tests/src/Kernel/UltimateCronQueueTest.php b/web/modules/ultimate_cron/tests/src/Kernel/UltimateCronQueueTest.php new file mode 100644 index 0000000000000000000000000000000000000000..39cbfb8fd542af397d8a967f7440a7b036445443 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Kernel/UltimateCronQueueTest.php @@ -0,0 +1,157 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Kernel; + +use Drupal\Tests\system\Kernel\System\CronQueueTest; +use Drupal\ultimate_cron\CronJobInterface; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Update feeds on cron. + * + * @group ultimate_cron + */ +class UltimateCronQueueTest extends CronQueueTest { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('ultimate_cron'); + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + module_load_install('ultimate_cron'); + ultimate_cron_install(); + $this->installSchema('ultimate_cron', [ + 'ultimate_cron_log', + 'ultimate_cron_lock', + ]); + } + + /** + * {@inheritdoc} + */ + public function testExceptions() { + // Get the queue to test the normal Exception. + $queue = $this->container->get('queue')->get('cron_queue_test_exception'); + + // Enqueue an item for processing. + $queue->createItem(array($this->randomMachineName() => $this->randomMachineName())); + + // Run cron; the worker for this queue should throw an exception and handle + // it. + $this->cron->run(); + $this->assertEquals(1, \Drupal::state()->get('cron_queue_test_exception')); + + // The item should be left in the queue. + $this->assertEquals(1, $queue->numberOfItems(), 'Failing item still in the queue after throwing an exception.'); + + // Expire the queue item manually. system_cron() relies in REQUEST_TIME to + // find queue items whose expire field needs to be reset to 0. This is a + // Kernel test, so REQUEST_TIME won't change when cron runs. + // @see system_cron() + // @see \Drupal\Core\Cron::processQueues() + $this->connection->update('queue') + ->condition('name', 'cron_queue_test_exception') + ->fields(['expire' => REQUEST_TIME - 1]) + ->execute(); + + // Has to be manually called for Ultimate Cron. + system_cron(); + + $this->cron->run(); + $this->assertEquals(2, \Drupal::state()->get('cron_queue_test_exception')); + + $this->assertEquals(0, $queue->numberOfItems(), 'Item was processed and removed from the queue.'); + // Get the queue to test the specific SuspendQueueException. + $queue = $this->container->get('queue')->get('cron_queue_test_broken_queue'); + + // Enqueue several item for processing. + $queue->createItem('process'); + $queue->createItem('crash'); + $queue->createItem('ignored'); + + // Run cron; the worker for this queue should process as far as the crashing + // item. + $this->cron->run(); + + // Only one item should have been processed. + $this->assertEquals(2, $queue->numberOfItems(), 'Failing queue stopped processing at the failing item.'); + + // Check the items remaining in the queue. The item that throws the + // exception gets released by cron, so we can claim it again to check it. + $item = $queue->claimItem(); + $this->assertEquals('crash', $item->data, 'Failing item remains in the queue.'); + $item = $queue->claimItem(); + $this->assertEquals('ignored', $item->data, 'Item beyond the failing item remains in the queue.'); + + // Test the requeueing functionality. + $queue = $this->container->get('queue')->get('cron_queue_test_requeue_exception'); + $queue->createItem([]); + $this->cron->run(); + + $this->assertEquals(2, \Drupal::state()->get('cron_queue_test_requeue_exception')); + $this->assertEquals(0, $queue->numberOfItems()); + } + + + /** + * Tests behavior when ultimate_cron overrides the cron processing. + */ + public function testOverriddenProcessing() { + + $job = CronJob::load(CronJobInterface::QUEUE_ID_PREFIX . 'cron_queue_test_broken_queue'); + $this->assertNull($job); + + $this->config('ultimate_cron.settings') + ->set('queue.enabled', TRUE) + ->save(); + + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); + + $job = CronJob::load(CronJobInterface::QUEUE_ID_PREFIX . 'cron_queue_test_broken_queue'); + $this->assertTrue($job instanceof CronJobInterface); + + /** @var \Drupal\Core\Queue\QueueInterface $queue */ + $queue = $this->container->get('queue')->get('cron_queue_test_broken_queue'); + + // Enqueue several item for processing. + $queue->createItem('process'); + $queue->createItem('process'); + $queue->createItem('process'); + $this->assertEquals(3, $queue->numberOfItems()); + + // Run the job, this should process them. + $job->run(t('Test launch')); + $this->assertEquals(0, $queue->numberOfItems()); + + // Check item delay feature. + $this->config('ultimate_cron.settings') + ->set('queue.delays.item_delay', 0.5) + ->save(); + + $queue->createItem('process'); + $queue->createItem('process'); + $queue->createItem('process'); + $this->assertEquals(3, $queue->numberOfItems()); + + // There are 3 items, the implementation doesn't wait for the first, that + // means this should between 1 and 1.5 seconds. + $before = microtime(TRUE); + $job->run(t('Test launch')); + $after = microtime(TRUE); + + $this->assertEquals(0, $queue->numberOfItems()); + $this->assertTrue($after - $before > 1); + $this->assertTrue($after - $after < 1.5); + + // @todo Test empty delay, causes a wait of 60 seconds with the test queue + // worker. + } + +} diff --git a/web/modules/ultimate_cron/tests/src/Unit/RulesUnitTest.php b/web/modules/ultimate_cron/tests/src/Unit/RulesUnitTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5fafc4a0a9848b6c7da7bed8de84b35a13263203 --- /dev/null +++ b/web/modules/ultimate_cron/tests/src/Unit/RulesUnitTest.php @@ -0,0 +1,769 @@ +<?php + +namespace Drupal\Tests\ultimate_cron\Unit; + +use Drupal\Tests\UnitTestCase; +use Drupal\ultimate_cron\CronRule; +use Drupal\ultimate_cron\Plugin\ultimate_cron\Scheduler\Crontab; + +/** + * Tests Drupal\ultimate_cron\CronRule. + * + * @group ultimate_cron + */ +class RulesUnitTest extends UnitTestCase { + + private function getIntervals($rule) { + $cron = CronRule::factory($rule, $_SERVER['REQUEST_TIME']); + return $cron->getIntervals(); + } + + private function assertRule($options) { + // Setup values + $options['rules'] = is_array($options['rules']) ? $options['rules'] : array($options['rules']); + $options['catch_up'] = isset($options['catch_up']) ? $options['catch_up'] : 86400 * 365; // @todo Adapting Elysia Cron test cases with a catchup of 1 year + + // Generate result message + $message = array(); + foreach ($options['rules'] as $rule) { + $cron = CronRule::factory($rule, strtotime($options['now'])); + $intervals = $cron->getIntervals(); + $parsed_rule = ''; + foreach ($intervals as $key => $value) { + $parsed_rule .= "$key: " . implode(',', $value) . "\n"; + } + #$parsed_rule = str_replace(" ", "\n", $cron->rebuildRule($cron->getIntervals())); + $last_scheduled = $cron->getLastSchedule(); + $message[] = "<span title=\"$parsed_rule\">$rule</span> @ " . date('Y-m-d H:i:s', $last_scheduled); + } + $message[] = 'now @ ' . $options['now']; + $message[] = 'last-run @ ' . $options['last_run']; + $message[] = 'catch-up @ ' . $options['catch_up']; + $message[] = ($options['result'] ? '' : 'not ') . 'expected to run'; + + // Do the actual test + $result = Crontab::shouldRun($options['rules'], strtotime($options['last_run']), strtotime($options['now']), $options['catch_up']); + + return array($options['result'] == $result, implode('<br/>', $message)); + } + + function testIntervals2MinuteRange() { + $intervals = $this->getIntervals('10-11 12 * * *'); + $this->assertEquals(range(11, 10, -1), $intervals['minutes'], 'Expected minutes to be 10, 11'); + $intervals = $this->getIntervals('0-1 12 * * *'); + $this->assertEquals(range(1, 0, -1), $intervals['minutes'], 'Expected minutes to be 0, 1'); + $intervals = $this->getIntervals('58-59 12 * * *'); + $this->assertEquals(range(59, 58, -1), $intervals['minutes'], 'Expected minutes to be 58, 59'); + } + + function testIntervals2MinuteRangeWithOffset() { + $intervals = $this->getIntervals('0-1+1 12 * * *'); + $this->assertEquals(range(2, 1, -1), $intervals['minutes'], 'Expected minutes to be 1, 2'); + $intervals = $this->getIntervals('10-11+1 12 * * *'); + $this->assertEquals(range(12, 11, -1), $intervals['minutes'], 'Expected minutes to be 11, 12'); + // Note, this test is testing for correct behaviour when the minutes wrap around + // Previously, this test would generate 43, 0 due to a bug in expandRange/expandInterval + $intervals = $this->getIntervals('42-43+1 12 * * *'); + $this->assertEquals(array(44, 43), $intervals['minutes'], 'Expected minutes to be 43, 44'); + // Note, this test is testing for correct behaviour when the minutes wrap around + $intervals = $this->getIntervals('58-59+1 12 * * *'); + $this->assertEquals(array(59, 0), $intervals['minutes'], 'Expected minutes to be 59, 0'); + } + + function testIntervalsSpecificMinute() { + $intervals = $this->getIntervals('0 12 * * *'); + $this->assertEquals(array(0), $intervals['minutes'], 'Expected minutes to be 0'); + $intervals = $this->getIntervals('10 12 * * *'); + $this->assertEquals(array(10), $intervals['minutes'], 'Expected minutes to be 10'); + $intervals = $this->getIntervals('59 12 * * *'); + $this->assertEquals(array(59), $intervals['minutes'], 'Expected minutes to be 59'); + } + + function testRules() { + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '0 12 * * *', + 'last_run' => '2008-01-02 12:00:00', + 'now' => '2008-01-02 12:01:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '0 12 * * *', + 'last_run' => '2008-01-02 12:00:00', + 'now' => '2008-01-02 15:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '0 12 * * *', + 'last_run' => '2008-01-02 12:00:00', + 'now' => '2008-01-03 11:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '0 12 * * *', + 'last_run' => '2008-01-02 12:00:00', + 'now' => '2008-01-03 12:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * * *', + 'last_run' => '2008-01-02 23:59:00', + 'now' => '2008-01-03 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * *', + 'last_run' => '2008-01-02 23:59:00', + 'now' => '2008-01-03 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * *', + 'last_run' => '2008-01-02 23:59:00', + 'now' => '2008-01-04 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * *', + 'last_run' => '2008-01-02 23:58:00', + 'now' => '2008-01-02 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * *', + 'last_run' => '2008-01-02 23:58:00', + 'now' => '2008-01-03 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-07 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 23:29:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 23:58:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:58:00', + 'now' => '2008-01-06 23:28:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:28:00', + 'now' => '2008-01-05 23:29:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:28:00', + 'now' => '2008-01-05 23:30:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:28:00', + 'now' => '2008-01-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 0', + 'last_run' => '2008-01-05 23:28:00', + 'now' => '2008-01-06 23:29:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '29,59 23 * * 5', + 'last_run' => '2008-02-22 23:59:00', + 'now' => '2008-02-28 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 5', + 'last_run' => '2008-02-22 23:59:00', + 'now' => '2008-02-29 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '29,59 23 * * 5', + 'last_run' => '2008-02-22 23:59:00', + 'now' => '2008-03-01 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * * 3', + 'last_run' => '2008-12-31 23:59:00', + 'now' => '2009-01-01 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * * 3', + 'last_run' => '2008-12-31 23:59:00', + 'now' => '2009-01-07 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * * 3', + 'last_run' => '2008-12-31 23:59:00', + 'now' => '2009-01-07 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * 2 5', + 'last_run' => '2008-02-22 23:59:00', + 'now' => '2008-02-29 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * 2 5', + 'last_run' => '2008-02-22 23:59:00', + 'now' => '2008-03-01 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * 2 5', + 'last_run' => '2008-02-29 23:59:00', + 'now' => '2008-03-07 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 * 2 5', + 'last_run' => '2008-02-29 23:59:00', + 'now' => '2009-02-06 23:58:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 * 2 5', + 'last_run' => '2008-02-29 23:59:00', + 'now' => '2009-02-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 */10 * *', + 'last_run' => '2008-01-10 23:58:00', + 'now' => '2008-01-10 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 */10 * *', + 'last_run' => '2008-01-10 23:59:00', + 'now' => '2008-01-11 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 */10 * *', + 'last_run' => '2008-01-10 23:59:00', + 'now' => '2008-01-20 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-04 23:59:00', + 'now' => '2008-01-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-04 23:59:00', + 'now' => '2008-01-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-05 23:59:00', + 'now' => '2008-01-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-05 23:59:00', + 'now' => '2008-01-10 23:58:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-05 23:59:00', + 'now' => '2008-01-10 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5,10-15 * *', + 'last_run' => '2008-01-05 23:59:00', + 'now' => '2008-01-16 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-01-04 23:59:00', + 'now' => '2008-01-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-01-05 23:59:00', + 'now' => '2008-01-06 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-01-06 23:59:00', + 'now' => '2008-01-07 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-01-06 23:59:00', + 'now' => '2008-01-13 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-02-04 23:59:00', + 'now' => '2008-02-05 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-02-05 23:59:00', + 'now' => '2008-02-10 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59 23 1-5 1 0', + 'last_run' => '2008-02-10 23:59:00', + 'now' => '2008-02-17 23:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 08:58:00', + 'now' => '2008-02-10 08:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 08:59:00', + 'now' => '2008-02-10 09:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 08:59:00', + 'now' => '2008-02-10 17:59:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 08:59:00', + 'now' => '2008-02-10 18:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 18:00:00', + 'now' => '2008-02-10 18:01:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 18:00:00', + 'now' => '2008-02-10 19:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '* 0,1,2,3,4,5,6,7,8,18,19,20,21,22,23 * * *', + 'last_run' => '2008-02-10 18:00:00', + 'now' => '2008-03-10 09:00:00' + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRules1MinuteRange() { + // Test a 1 minute range + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-10 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:09:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-10 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:10:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-10 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:11:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRules2MinuteRange() { + // Test a 1 minute range + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-11 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:09:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-11 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:10:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-11 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:11:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-11 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:12:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRules2MinuteRangeWithOffset() { + // Test a 1 minute range + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-11+1 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:10:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-11+1 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:11:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-11+1 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:12:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-11+1 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:13:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRules5MinuteRange() { + // Test a 5 minute range + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:09:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:10:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:11:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:12:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:13:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:14:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:15:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + // This should not run, as it last ran one minute ago + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:15:00', + 'now' => '2008-01-03 12:16:00' + )); + $this->assertTrue($result[0], $result[1]); + // This should run, as catch_up defaults to 1 year and it last ran 16 minutes ago. + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10-15 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:16:00' + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRules5MinuteStep() { + // Test a 5 minute step + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '*/5 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:01:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '*/5 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:02:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '*/5 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:03:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '*/5 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:04:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '*/5 12 * * *', + 'last_run' => '2008-01-03 12:00:00', + 'now' => '2008-01-03 12:05:00' + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRulesExtended() { + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '0 0 * jan,oct *', + 'last_run' => '2008-01-31 00:00:00', + 'now' => '2008-03-10 09:00:00' + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '0 0 * jan,oct *', + 'last_run' => '2008-01-31 00:00:00', + 'now' => '2008-10-01 00:00:00' + )); + $this->assertTrue($result[0], $result[1]); + } + + function testRulesMinuteWithOffset() { + // Test a 1 minute range + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 12:10:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '10+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 12:11:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '10+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 12:12:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 12:59:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => True, + 'rules' => '59+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 12:00:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 13:00:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + $result = $this->assertRule(array( + 'result' => FALSE, + 'rules' => '59+1 12 * * *', + 'last_run' => '2008-01-01 12:00:00', + 'now' => '2008-01-03 13:01:00', + 'catch_up' => 1 + )); + $this->assertTrue($result[0], $result[1]); + } +} + diff --git a/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.info.yml b/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..9081ead804845be08bb5a49d7ccc6fd384ef0639 --- /dev/null +++ b/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.info.yml @@ -0,0 +1,12 @@ +name: Ultimate Cron Logger Test +type: module +description: Ultimate Cron Logger Test +core_version_requirement: ^8.7.7 || ^9 +package: Tests +dependencies: + - ultimate_cron + +# Information added by Drupal.org packaging script on 2020-09-24 +version: '8.x-2.0-alpha5' +project: 'ultimate_cron' +datestamp: 1600928951 diff --git a/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.module b/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.module new file mode 100644 index 0000000000000000000000000000000000000000..bb36bc7c84c49fff0eb050d07752b03f5282637b --- /dev/null +++ b/web/modules/ultimate_cron/tests/ultimate_cron_logger_test/ultimate_cron_logger_test.module @@ -0,0 +1,27 @@ +<?php +/** + * @file + * Contains ultimate_cron_logger_test.module.. + */ + +/** + * Implements hook_cron(). + */ +function ultimate_cron_logger_test_cron() { + $action = \Drupal::state()->get('ultimate_cron_logger_test_cron_action'); + + if ($action == 'exception') { + throw new Exception('Test cron exception'); + } + elseif ($action == 'fatal') { + call_to_undefined_function(); + } + elseif ($action == 'long_message') { + // This long message text length is 5800 long. + $long_message = str_repeat('This is a very long message. ', 200); + \Drupal::logger('ultimate_cron_logger_test_cron')->notice($long_message); + } + elseif ($action == 'log_warning') { + \Drupal::logger('ultimate_cron_logger_test_cron')->warning('This is a warning message'); + } +} diff --git a/web/modules/ultimate_cron/ultimate_cron.api.php b/web/modules/ultimate_cron/ultimate_cron.api.php new file mode 100644 index 0000000000000000000000000000000000000000..8f3e7d7a643437dca99a7b22930a9e94135f8eb0 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.api.php @@ -0,0 +1,274 @@ +<?php +/** + * @file + * Hooks provided by Ultimate Cron. + */ +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * @addtogroup hooks + * @{ + */ + +/** + * Inform Ultimate Cron about cron jobs. + * + * To add additional/multiple cron jobs from a custom module, provide + * a default configuration in the module with the needed settings. + * Example: ultimate_cron.job.custom_module_cron.yml + * + * Note that the result of this hook is cached. + * + * @return array + * Array of cron jobs, keyed by name. + * - "title": (optional) The title of the cron job. If not provided, the + * name of the cron job will be used. + * - "file": (optional) The file where the callback lives. + * - "module": The module where this job lives. + * - "file path": (optional) The path to the directory containing the file + * specified in "file". This defaults to the path to the module + * implementing the hook. + * - "callback": (optional) The callback to call when running the job. + * Defaults to the job name. + * - "callback arguments": (optional) Arguments for the callback. Defaults + * to array(). + * - "enabled": (optional) Initial state of the job. Defaults to TRUE. + * - "tags": (optional) Tags for the job. Defaults to array(). + * - "settings": (optional) Default settings (plugin type) for this job. + * Example of a job declaring some default settings for a plugin called + * "some_plugin": + * 'settings' => array( + * 'some_plugin' => array( + * 'some_value' => 60, + * ), + * ), + * - "scheduler": (optional) Default scheduler (plugin type) for this job. + * Example of a job using the crontab scheduler as default: + * 'scheduler' => array( + * 'name' => 'crontab', + * 'crontab' => array( + * 'rules' => array('* * * * *'), + * ), + * ), + * - "launcher": (optional) Default launcher (plugin type) for this job. + * Example of a job using the serial launcher as default: + * 'launcher' => array( + * 'name' => 'serial', + * 'serial' => array( + * 'thread' => 'any', + * ), + * ), + * - "logger": (optional) Default logger (plugin type) for this job. + * Example of a job using the cache logger as default: + * 'logger' => array( + * 'name' => 'cache', + * 'cache' => array( + * 'bin' => 'mycachebin', + * ), + * ), + */ +function hook_cronapi() { + $items = array(); + + $items['example_my_cron_job_1'] = array( + 'title' => t('This is my cron job #1'), + 'file' => 'example.jobs.inc', + 'file path' => drupal_get_path('module', 'example') . '/cron', + 'callback' => 'example_my_cron_job_callback', + 'callback arguments' => array('cronjob1'), + 'enabled' => FALSE, + 'tags' => array('example'), + 'settings' => array( + 'example_plugin' => array( + 'example_setting' => 'example_value', + ), + ), + 'scheduler' => array( + 'name' => 'crontab', + 'crontab' => array( + 'rules' => array('* * * * *'), + ), + ), + 'launcher' => array( + 'name' => 'serial', + 'serial' => array( + 'thread' => 'any', + ), + ), + 'logger' => array( + 'name' => 'cache', + 'cache' => array( + 'bin' => 'my_cache_bin', + ), + ), + ); + + return $items; +} + +/** + * Alter the output of hook_cronapi() and hook_cron(). + * + * Note that the result of this hook is cached just like hook_cronapi(). + * + * This can hook can also be implemented inside a plugin, but with a + * slight difference. Inside the plugin, the hook is not cached and it operates + * on an array of UltimateCronJob objects instead of hook definitions. + * + * @param array &$items + * Hooks defined in the system. + */ +function hook_cron_alter(&$items) { + $items['example_my_cron_job_1']['title'] = 'NEW TITLE FOR EXAMPLE CRON JOB #1! HA!'; +} + +/** + * Provide easy hooks for Ultimate Cron. + * + * Ultimate Cron has a built-in set of easy hooks: + * - hook_cron_hourly(). + * - hook_cron_daily(). + * - hook_cron_nightly(). + * - hook_cron_weekly(). + * - hook_cron_monthly(). + * - hook_cron_yearly(). + * + * This hook makes it possible to provide custom easy hooks. + * + * @return array + * Array of easy hook definitions. + */ +function hook_cron_easy_hooks() { + return array( + 'cron_fullmoonly' => array( + 'title' => 'Run at full moon', + 'scheduler' => array( + 'name' => 'moonphase', + 'moonphase' => array( + 'phase' => 'full', + ), + ), + ) + ); +} + +/** + * Alter easy hooks. + * + * @param array &$easy_hooks + * Easy hook definitions. + */ +function hook_cron_easy_hooks_alter(&$easy_hooks) { + $easy_hooks['cron_fullmoonly']['scheduler']['moonphase']['phase'] = 'new'; +} + +/** + * The following hooks are invoked during the jobs life cycle, + * from schedule to finish. The chronological order is: + * + * cron_pre_schedule + * cron_post_schedule + * cron_pre_launch + * cron_pre_launch(*) + * cron_pre_run + * cron_pre_invoke + * cron_post_invoke + * cron_post_run + * cron_post_launch(*) + * + * Depending on how the launcher works, the hook_cron_post_launch() may be + * invoked before or after hook_cron_post_run() or somewhere in between. + * An example of this is the Background Process launcher, which launches + * the job in a separate thread. After the launch, hook_cron_post_launch() + * is invoked, but the run/invoke hooks are invoked simultaneously in a + * separate thread. + * + * All of these hooks can also be implemented inside a plugin as a method. + */ + +/** + * Invoked just before a job is asked for its schedule. + * + * @param CronJob $job + * The job being queried. + */ +function hook_pre_schedule($job) { +} + +/** + * Invoked after a job has been asked for its schedule. + * + * @param CronJob $job + * The job being queried. + */ +function hook_post_schedule($job) { +} + +/** + * Invoked just before a job is launched. + * + * @param CronJob $job + * The job being launched. + */ +function hook_pre_launch($job) { +} + +/** + * Invoked after a job has been launched. + * + * @param CronJob $job + * The job that was launched. + */ +function hook_post_launch($job) { +} + +/** + * Invoked just before a job is being run. + * + * @param CronJob $job + * The job being run. + */ +function hook_pre_run($job) { +} + +/** + * Invoked after a job has been run. + * + * @param CronJob $job + * The job that was run. + */ +function hook_post_run($job) { +} + +/** + * Invoked just before a job is asked for its schedule. + * + * @param CronJob $job + * The job being invoked. + */ +function hook_pre_invoke($job) { +} + +/** + * Invoked after a job has been invoked. + * + * @param CronJob $job + * The job that was invoked. + */ +function hook_post_invoke($job) { +} + +/** + * Alter the allowed operations for a given job on the export UI page. + * + * This hook can also be implemented inside a plugin as a method: + * build_operations_alter($job, &$allowed_operations). It will only be + * run for the currently active plugin for the job. + * + * @param CronJob $job + * The job in question. + * @param array &$allowed_operations + * Allowed operations for this job. + */ +function hook_ultimate_cron_plugin_build_operations($job, &$allowed_operations) { +} diff --git a/web/modules/ultimate_cron/ultimate_cron.drush.inc b/web/modules/ultimate_cron/ultimate_cron.drush.inc new file mode 100644 index 0000000000000000000000000000000000000000..06100834ba55be0f6b79cae1a4594bc533cb960c --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.drush.inc @@ -0,0 +1,471 @@ +<?php +/** + * @file + * Drush commands for Ultimate Cron! + */ +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Implements hook_drush_command(). + */ +function ultimate_cron_drush_command() { + $items = array(); + + $items['cron-logs'] = array( + 'description' => 'Show a cron jobs logs', + 'arguments' => array( + 'name' => 'Job to show logs for', + ), + 'options' => array( + 'limit' => 'Number of log entries to show', + 'compact' => 'Only show the first line of each log entry', + ), + 'examples' => array( + 'drush cron-logs node_cron --limit=20' => 'Show 20 last logs for the node_cron job', + ), + ); + + $items['cron-list'] = array( + 'description' => 'List cron jobs', + 'options' => array( + 'module' => 'Comma separated list of modules to show jobs from', + 'enabled' => 'Show enabled jobs', + 'disabled' => 'Show enabled jobs', + 'behind' => 'Show jobs that are behind schedule', + 'status' => 'Comma separated list of statuses to show jobs from', + 'extended' => 'Show extended information', + 'name' => 'Show name instead of title', + 'scheduled' => 'Show scheduled jobs', + ), + 'examples' => array( + 'drush cron-list --status=running --module=node' => 'Show jobs from the node module that are currently running', + ), + 'aliases' => array('cl'), + ); + + $items['cron-run'] = array( + 'description' => 'Run cron job', + 'arguments' => array( + 'name' => 'Job to run', + ), + 'options' => array( + 'force' => 'Skip the schedule check for each job. Locks are still respected.', + 'options' => 'Custom options for plugins, e.g. --options=thread=1 for serial launcher', + ), + 'examples' => array( + 'drush cron-run node_cron' => 'Run the node_cron job', + 'drush cron-run --options=thread=1' => 'Run all scheduled jobs and instruct serial launcher only to launch thread 1 jobs', + ), + 'aliases' => array('crun'), + ); + + $items['cron-is-running'] = array( + 'description' => 'Tell whether cron is running. Exit status is set in concordance with the cron running status.', + 'examples' => array( + 'drush cron-is-running' => 'Check if cron is running.', + 'drush cron-is-running --quiet' => 'Check if cron is running and don\'t show an informative message.', + 'while `drush cron-is-running --quiet`; do echo "Waiting cron to finish"; sleep 1; done' => 'Bash loop to wait until cron finishes.', + ), + 'aliases' => array('cir'), + ); + + $items['cron-enable'] = array( + 'description' => 'Enable cron job', + 'arguments' => array( + 'name' => 'Job to enable', + ), + 'options' => array( + 'all' => 'Enabled all jobs', + ), + 'examples' => array( + 'drush cron-enable node_cron' => 'Enable the node_cron job', + ), + 'aliases' => array('ce'), + ); + + $items['cron-disable'] = array( + 'description' => 'Disable cron job', + 'arguments' => array( + 'name' => 'Job to disable', + ), + 'options' => array( + 'all' => 'Enabled all jobs', + ), + 'examples' => array( + 'drush cron-disable node_cron' => 'Disable the node_cron job', + ), + 'aliases' => array('cd'), + ); + + $items['cron-unlock'] = array( + 'description' => 'Unlock cron job', + 'arguments' => array( + 'name' => 'Job to unlock', + ), + 'options' => array( + 'all' => 'Enabled all jobs', + ), + 'examples' => array( + 'drush cron-unlock node_cron' => 'Unlock the node_cron job', + ), + 'aliases' => array('cu'), + ); + + return $items; +} + +/** + * Implements hook_drush_help(). + */ +function ultimate_cron_drush_help($section) { + switch ($section) { + case 'drush:cron-list': + return dt('This command will list cron jobs'); + + case 'drush:cron-run': + return dt('This command will run a cron job'); + + case 'drush:cron-enable': + return dt('This command will enable a cron job'); + + case 'drush:cron-disable': + return dt('This command will disable a cron job'); + + case 'drush:cron-unlock': + return dt('This command will unlock a cron job'); + } +} + +/** + * List cron jobs. + */ +function drush_ultimate_cron_cron_list() { + $modules = drush_get_option('module'); + $enabled = drush_get_option('enabled'); + $disabled = drush_get_option('disabled'); + $behind = drush_get_option('behind'); + $extended = drush_get_option('extended'); + $statuses = drush_get_option('status'); + $scheduled = drush_get_option('scheduled'); + $showname = drush_get_option('name'); + + $modules = $modules ? explode(',', $modules) : array(); + $statuses = $statuses ? explode(',', $statuses) : array(); + + $title = $showname ? dt('Name') : dt('Title'); + + $table = array(); + $table[] = array( + '', + dt('ID'), + dt('Module'), + $title, + dt('Scheduled'), + dt('Started'), + dt('Duration'), + dt('Status'), + ); + + $print_legend = FALSE; + + /** @var \Drupal\ultimate_cron\Entity\CronJob $job */ + foreach (CronJob::loadMultiple() as $name => $job) { + if ($modules && !in_array($job->getModule(), $modules)) { + continue; + } + + if ($enabled && FALSE === $job->status()) { + continue; + } + + if ($disabled && TRUE === $job->status()) { + continue; + } + + if ($scheduled && !$job->isScheduled()) { + continue; + } + + $legend = ''; + + if (FALSE === $job->status()) { + $legend .= 'D'; + $print_legend = TRUE; + } + + $lock_id = $job->isLocked(); + $log_entry = $job->loadLogEntry($lock_id); + + if ($time = $job->isBehindSchedule()) { + $legend .= 'B'; + $print_legend = TRUE; + } + + if ($behind && !$time) { + continue; + } + + if ($lock_id && $log_entry->lid == $lock_id) { + $legend .= 'R'; + list(, $status) = $job->getPlugin('launcher')->formatRunning($job); + $print_legend = TRUE; + } + elseif ($log_entry->start_time && !$log_entry->end_time) { + list(, $status) = $job->getPlugin('launcher')->formatUnfinished($job); + } + else { + list(, $status) = $log_entry->formatSeverity(); + } + + if ($statuses && !in_array($status, $statuses)) { + continue; + } + + $progress = $lock_id ? $job->formatProgress() : ''; + + $table[$name][] = $legend; + $table[$name][] = $job->id(); + $table[$name][] = $job->getModuleName(); + $table[$name][] = $showname ? $job->id() : $job->getTitle(); + $table[$name][] = $job->getPlugin('scheduler')->formatLabel($job); + $table[$name][] = $log_entry->formatStartTime(); + $table[$name][] = $log_entry->formatDuration() . ' ' . $progress; + $table[$name][] = $status; + + if ($extended) { + $table['extended:' . $name][] = ''; + $table['extended:' . $name][] = ''; + $table['extended:' . $name][] = $job->id(); + $table['extended:' . $name][] = $job->getPlugin('scheduler')->formatLabelVerbose($job); + $table['extended:' . $name][] = $log_entry->init_message; + $table['extended:' . $name][] = $log_entry->message; + } + } + drush_print_table($table); + if ($print_legend) { + drush_print("\n" . dt('Legend: D = Disabled, R = Running, B = Behind schedule')); + } +} + +/** + * List cron jobs. + */ +function drush_ultimate_cron_cron_logs($name = NULL) { + if (!$name) { + return drush_set_error(dt('No job specified?')); + } + + /** @var CronJob $job */ + $job = Cronjob::load($name); + + if (!$job) { + return drush_set_error(dt('@name not found', array('@name' => $name))); + } + + $compact = drush_get_option('compact'); + $limit = drush_get_option('limit'); + $limit = $limit ? $limit : 10; + + $table = array(); + $table[] = array( + '', + dt('Started'), + dt('Duration'), + dt('User'), + dt('Initial message'), + dt('Message'), + dt('Status'), + ); + + $lock_id = $job->isLocked(); + $log_entries = $job->getLogEntries(ULTIMATE_CRON_LOG_TYPE_ALL, $limit); + + /** @var \Drupal\ultimate_cron\Logger\LogEntry $log_entry */ + foreach ($log_entries as $log_entry) { + $progress = ''; + if ($log_entry->lid && $lock_id && $log_entry->lid === $lock_id) { + $progress = $job->getProgress(); + $progress = is_numeric($progress) ? sprintf(' (%d%%)', round($progress * 100)) : ''; + } + + $legend = ''; + if ($lock_id && $log_entry->lid == $lock_id) { + $legend .= 'R'; + list(, $status) = $job->getPlugin('launcher')->formatRunning($job); + } + elseif ($log_entry->start_time && !$log_entry->end_time) { + list(, $status) = $job->getPlugin('launcher')->formatUnfinished($job); + } + else { + list(, $status) = $log_entry->formatSeverity(); + } + + $table[$log_entry->lid][] = $legend; + $table[$log_entry->lid][] = $log_entry->formatStartTime(); + $table[$log_entry->lid][] = $log_entry->formatDuration() . $progress; + $table[$log_entry->lid][] = $log_entry->formatUser(); + if ($compact) { + $table[$log_entry->lid][] = trim(reset(explode("\n", $log_entry->init_message)), "\n"); + $table[$log_entry->lid][] = trim(reset(explode("\n", $log_entry->message)), "\n"); + } + else { + $table[$log_entry->lid][] = trim($log_entry->init_message, "\n"); + $table[$log_entry->lid][] = trim($log_entry->message, "\n"); + } + $table[$log_entry->lid][] = $status; + } + drush_print_table($table); +} + +/** + * Run cron job(s). + */ +function drush_ultimate_cron_cron_run($name = NULL) { + + if ($options = drush_get_option('options')) { + $pairs = explode(',', $options); + foreach ($pairs as $pair) { + list($key, $value) = explode('=', $pair); + CronPlugin::setGlobalOption(trim($key), trim($value)); + } + } + + $force = drush_get_option('force'); + + if (!$name) { + // Run all jobs. + $jobs = CronJob::loadMultiple(); + + /** @var CronJob $job */ + foreach($jobs as $job) { + if ($force || $job->isScheduled()) { + $job->run(t('Launched by drush')); + } + + } + } + else { + // Run a specific job. + $job = CronJob::load($name); + + if (!$job) { + return drush_set_error(dt('@name not found', array('@name' => $name))); + } + + if ($force || $job->isScheduled()) { + $job->run(t('Launched by drush')); + } + } +} + +/** + * Tell whether cron is running. + */ +function drush_ultimate_cron_cron_is_running() { + $locked = FALSE; + foreach (CronJob::loadMultiple() as $name => $job) { + if ($job->isLocked()) { + $locked = TRUE; + break; + } + } + + if ($locked) { + $msg = dt('Cron is running.'); + drush_set_context('DRUSH_EXIT_CODE', 0); + } + else { + $msg = dt('Cron is not running.'); + drush_set_context('DRUSH_EXIT_CODE', 1); + } + + return $msg; +} + +/** + * Enable a cron job. + */ +function drush_ultimate_cron_cron_enable($name = NULL) { + if (!$name) { + if (!drush_get_option('all')) { + return drush_set_error(dt('No job specified?')); + } + /** @var CronJob $job */ + foreach (CronJob::loadMultiple() as $job) { + $job->enable()->save(); + } + return; + } + + $job = CronJob::load($name); + if ($job->enable()->save()) { + drush_print(dt('@name enabled', array('@name' => $name))); + } +} + +/** + * Disable a cron job. + */ +function drush_ultimate_cron_cron_disable($name = NULL) { + if (!$name) { + if (!drush_get_option('all')) { + return drush_set_error(dt('No job specified?')); + } + foreach (CronJob::loadMultiple() as $job) { + $job->disable()->save(); + } + return; + } + + $job = CronJob::load($name); + if ($job->disable()->save()) { + drush_print(dt('@name disabled', array('@name' => $name))); + } +} + +/** + * Unlock a cron job. + */ +function drush_ultimate_cron_cron_unlock($name = NULL) { + if (!$name) { + if (!drush_get_option('all')) { + return drush_set_error(dt('No job specified?')); + } + /** @var CronJob $job */ + foreach (CronJob::loadMultiple() as $job) { + if ($job->isLocked()) { + $job->unlock(); + } + } + return; + } + + /** @var CronJob $job */ + $job = CronJob::load($name); + if (!$job) { + return drush_set_error(dt('@name not found', array('@name' => $name))); + } + + $lock_id = $job->isLocked(); + if (!$lock_id) { + return drush_set_error(dt('@name is not running', array('@name' => $name))); + } + + // Unlock the process. + if ($job->unlock($lock_id, TRUE)) { + $log_entry = $job->resumeLog($lock_id); + global $user; + \Drupal::logger('ultimate_cron')->warning('@name manually unlocked by user @username (@uid)', array( + '@name' => $job->id(), + '@username' => $user->getDisplayName(), + '@uid' => $user->id(), + )); + $log_entry->finish(); + + drush_print(dt('Cron job @name unlocked', array('@name' => $name))); + } + else { + drush_set_error(dt('Could not unlock cron job @name', array('@name' => $name))); + } +} diff --git a/web/modules/ultimate_cron/ultimate_cron.info.yml b/web/modules/ultimate_cron/ultimate_cron.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..ef825f2a5a67a7fa1637ea409f352628df6aa752 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.info.yml @@ -0,0 +1,10 @@ +type: module +name: "Ultimate Cron" +description: "Cron" +core_version_requirement: ^8.7.7 || ^9 +configure: entity.ultimate_cron_job.collection + +# Information added by Drupal.org packaging script on 2020-09-24 +version: '8.x-2.0-alpha5' +project: 'ultimate_cron' +datestamp: 1600928951 diff --git a/web/modules/ultimate_cron/ultimate_cron.install b/web/modules/ultimate_cron/ultimate_cron.install new file mode 100755 index 0000000000000000000000000000000000000000..2e6ed179e607b3c82ba52bebe23f991a13dcf894 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.install @@ -0,0 +1,200 @@ +<?php +/** + * @file + * Installation file for Ultimate Cron + */ + +use Drupal\Core\Url; +use Drupal\ultimate_cron\Entity\CronJob; + +/** + * Implements hook_schema(). + */ +function ultimate_cron_schema() { + $schema = array(); + + $schema['ultimate_cron_log'] = array( + 'description' => 'Logs', + 'fields' => array( + 'lid' => array( + 'description' => 'Lock ID', + 'type' => 'varchar', + 'length' => 176, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'Name', + 'type' => 'varchar', + 'length' => 166, + 'not null' => TRUE, + ), + 'log_type' => array( + 'description' => 'Log type', + 'type' => 'int', + 'size' => 'normal', + 'not null' => TRUE, + 'default' => 0, + ), + 'start_time' => array( + 'description' => 'Timestamp of execution start', + 'type' => 'float', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 0, + ), + 'end_time' => array( + 'description' => 'Timestamp of execution end', + 'type' => 'float', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 0, + ), + 'uid' => array( + 'description' => 'User ID', + 'type' => 'int', + 'size' => 'normal', + 'not null' => TRUE, + 'default' => 0, + ), + 'init_message' => array( + 'description' => 'Initial message', + 'type' => 'text', + 'not null' => FALSE, + ), + 'message' => array( + 'description' => 'Message', + 'type' => 'text', + 'not null' => FALSE, + ), + 'severity' => array( + 'description' => 'Max severity level of the execution', + 'type' => 'int', + 'size' => 'normal', + 'not null' => FALSE, + 'default' => -1, + ), + ), + 'primary key' => array('lid'), + 'indexes' => array( + 'idx_last' => array( + array('name', 80), + 'start_time', + 'end_time', + 'log_type', + ), + ), + ); + + $schema['ultimate_cron_lock'] = array( + 'description' => 'Locks', + 'fields' => array( + 'lid' => array( + 'description' => 'Lock ID', + 'type' => 'serial', + 'size' => 'big', + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'Name', + 'type' => 'varchar', + 'length' => 166, + 'not null' => TRUE, + ), + 'current' => array( + 'description' => 'Current lock', + 'type' => 'int', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 0, + ), + 'expire' => array( + 'description' => 'Expiration time of lock', + 'type' => 'float', + 'size' => 'big', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('lid'), + 'unique keys' => array( + 'idx_name' => array('name', 'current'), + ), + ); + + $schema['ultimate_cron_signal'] = array( + 'description' => 'Signals', + 'fields' => array( + 'job_name' => array( + 'description' => 'Name of job', + 'type' => 'varchar', + 'length' => 166, + 'not null' => TRUE, + ), + 'signal_name' => array( + 'description' => 'Name of signal', + 'type' => 'varchar', + 'length' => 166, + 'not null' => TRUE, + ), + 'claimed' => array( + 'description' => 'Is signal claimed', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('job_name', 'signal_name'), + ); + + return $schema; +} + +/** + * Implements hook_install(). + */ +function ultimate_cron_install() { + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); +} + +/** + * Implements hook_requirements(). + */ +function ultimate_cron_requirements($phase) { + $requirements = array(); + + switch ($phase) { + case 'runtime': + $requirements['cron_jobs']['title'] = 'Ultimate Cron'; + $requirements['cron_jobs']['severity'] = REQUIREMENT_OK; + + // Check if any jobs are behind. + $jobs_behind = 0; + $jobs = CronJob::loadMultiple(); + + foreach ($jobs as $job) { + if ($job->isBehindSchedule()) { + $jobs_behind++; + } + } + + if ($jobs_behind) { + $requirements['cron_jobs']['severity'] = REQUIREMENT_WARNING; + $requirements['cron_jobs']['value'] = \Drupal::translation()->formatPlural( + $jobs_behind, + '@count job is behind schedule', + '@count jobs are behind schedule' + ); + $requirements['cron_jobs']['description'] = [ + '#markup' => t('Some jobs are behind their schedule. Please check if <a href=":system_cron_url">Cron</a> is running properly.', [ + ':system_cron_url' => Url::fromRoute('system.cron', ['key' => \Drupal::state()->get('system.cron_key')])->toString() + ]) + ]; + } + else { + $requirements['cron_jobs']['value'] = t('Cron is running properly.'); + } + } + + return $requirements; +} diff --git a/web/modules/ultimate_cron/ultimate_cron.links.action.yml b/web/modules/ultimate_cron/ultimate_cron.links.action.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe8c5580f5fb8f7129c731c3eb2c3ce013bcf643 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.links.action.yml @@ -0,0 +1,6 @@ +ultimate_cron.job_add: + route_name: ultimate_cron.discover_jobs + title: 'Discover jobs' + weight: 1 + appears_on: + - entity.ultimate_cron_job.collection diff --git a/web/modules/ultimate_cron/ultimate_cron.links.menu.yml b/web/modules/ultimate_cron/ultimate_cron.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..b0c967263141f353709aceeaa26c14d2f52f0ae5 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.links.menu.yml @@ -0,0 +1,5 @@ +system.cron_settings: + title: 'Cron' + description: 'Manage automatic site maintainenance tasks.' + route_name: entity.ultimate_cron_job.collection + parent: system.admin_config_system diff --git a/web/modules/ultimate_cron/ultimate_cron.links.task.yml b/web/modules/ultimate_cron/ultimate_cron.links.task.yml new file mode 100644 index 0000000000000000000000000000000000000000..54e0a3755330d89cdc2a7574d44bd75d070cc431 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.links.task.yml @@ -0,0 +1,34 @@ +ultimate_cron.job_list: + title: 'Cron Jobs' + route_name: entity.ultimate_cron_job.collection + base_route: entity.ultimate_cron_job.collection + +ultimate_cron.run_cron: + title: 'Run cron' + route_name: system.cron_settings + base_route: entity.ultimate_cron_job.collection + +ultimate_cron.settings: + title: 'Cron settings' + route_name: ultimate_cron.settings + base_route: entity.ultimate_cron_job.collection + +ultimate_cron.general_settings: + title: Queue + route_name: ultimate_cron.general_settings + parent_id: ultimate_cron.settings + +ultimate_cron.launcher_settings: + title: Launcher + route_name: ultimate_cron.launcher_settings + parent_id: ultimate_cron.settings + +ultimate_cron.logger_settings: + title: Logger + route_name: ultimate_cron.logger_settings + parent_id: ultimate_cron.settings + +ultimate_cron.scheduler_settings: + title: Scheduler + route_name: ultimate_cron.scheduler_settings + parent_id: ultimate_cron.settings diff --git a/web/modules/ultimate_cron/ultimate_cron.module b/web/modules/ultimate_cron/ultimate_cron.module new file mode 100755 index 0000000000000000000000000000000000000000..15035785d26094d267d9b95b5b83ca5aa98a4e56 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.module @@ -0,0 +1,260 @@ +<?php +/** + * @file + * Ultimate Cron. Extend cron functionality in Drupal. + */ + +use Drupal\Core\Queue\RequeueException; +use Drupal\ultimate_cron\CronPlugin; +use Drupal\ultimate_cron\PluginCleanupInterface; + +define('ULTIMATE_CRON_LOG_TYPE_NORMAL', 0); +define('ULTIMATE_CRON_LOG_TYPE_ADMIN', 1); +define('ULTIMATE_CRON_LOG_TYPE_ALL', -1); + +/** + * Pseudo define. + */ +function _ultimate_cron_define_log_type_all() { + return array(ULTIMATE_CRON_LOG_TYPE_NORMAL, ULTIMATE_CRON_LOG_TYPE_ADMIN); +} + +$GLOBALS['ultimate_cron_shutdown_functions'] = array(); + +/** + * The shutdown function itself is also overridable. + * + * In case it is necessary to add it earlier, say settings.php. + * Remeber to invoke the registered ultimate cron shutdown handlers. + * If the function exists, we assume that the register_shutdown_handler() has + * also been setup correctly. + * + * @todo: Move _ultimate_cron_out_of_memory_protection() to a service. + */ +if (!function_exists('_ultimate_cron_out_of_memory_protection')) { + /** + * Shutdown hander that unleash the memory reserved. + */ + function _ultimate_cron_out_of_memory_protection() { + // error_log("RELEASING MEMORY"); + unset($GLOBALS['__RESERVED_MEMORY']); + // error_log(print_r($GLOBALS['ultimate_cron_shutdown_functions'], TRUE)); + foreach ($GLOBALS['ultimate_cron_shutdown_functions'] as $function) { + call_user_func_array($function['callback'], $function['arguments']); + } + } + + + // The minor overhead in _drupal_shutdown_function() can mean the + // difference between life and death for our shutdown handlers in + // a memory exhaust situation. We want our shutdown handler to be + // called as early as possible. If no callbacks have been registrered + // yet, we use PHPs built-in register_shutdown_function() otherwise + // we ensure, that we are the first in the list of Drupals registered + // shutdown functions. + $callbacks = &drupal_register_shutdown_function(); + if (empty($callbacks)) { + register_shutdown_function('_ultimate_cron_out_of_memory_protection'); + } + else { + array_unshift($callbacks, array('callback' => '_ultimate_cron_out_of_memory_protection', 'arguments' => array())); + // Reset internal array pointer just in case ... + reset($callbacks); + } +} +/** + * Registers a function for execution on shutdown. + * + * Wrapper for register_shutdown_function() that catches thrown exceptions to + * avoid "Exception thrown without a stack frame in Unknown". + * + * This is a duplicate of the built-in functionality in Drupal, however we + * need to perform our tasks before that. + * + * @param callback $callback + * The shutdown function to register. + * @param ... + * Additional arguments to pass to the shutdown function. + * + * @see register_shutdown_function() + * + * @ingroup php_wrappers + */ +function ultimate_cron_register_shutdown_function($callback) { + $args = func_get_args(); + array_shift($args); + $GLOBALS['ultimate_cron_shutdown_functions'][] = array( + 'callback' => $callback, + 'arguments' => $args, + ); +} + +/** + * Load callback for plugins. + * + * @param string $type + * Type of the plugin (settings, scheduler, launcher, logger). + * @param string $name + * Name of the plugin (general, queue, serial, database, etc.). + * + * @return object + * The instance of the plugin (singleton). + */ +function ultimate_cron_plugin_load($type, $name) { + $cache = &drupal_static('ultimate_cron_plugin_load_all', array()); + if (!isset($cache[$type][$name])) { + ultimate_cron_plugin_load_all($type); + $cache[$type][$name] = isset($cache[$type][$name]) ? $cache[$type][$name] : FALSE; + } + return $cache[$type][$name]; +} + + +function ultimate_cron_fake_cron() { + $counter = \Drupal::state()->get('ultimate_cron.cron_run_counter', 0); + $counter++; + \Drupal::state()->set('ultimate_cron.cron_run_counter', $counter); +} + +/** + * Load all callback for plugins. + * + * @param string $type + * Type of the plugin (settings, scheduler, launcher, logger). + * + * @return array + * The instances of the plugin type (singletons). + */ +function ultimate_cron_plugin_load_all($type, $reset = FALSE) { + $cache = &drupal_static('ultimate_cron_plugin_load_all', array()); + if (!$reset && isset($cache[$type])) { + return $cache[$type]; + } + /* @var \Drupal\Core\Plugin\DefaultPluginManager $manager */ + $manager = \Drupal::service('plugin.manager.ultimate_cron.' . $type); + $plugins = $manager->getDefinitions(); + + + foreach ($plugins as $plugin_id => $definition) { + if ($object = $manager->createInstance($plugin_id)) { + $plugins[$plugin_id] = $object; + } + } + $cache[$type] = $plugins; + return $cache[$type]; +} +// ---------- HOOKS ---------- +/** + * Implements hook_hook_info(). + */ +function ultimate_cron_hook_info() { + $hooks = array(); + $hooks['background_process_shutdown'] = array( + 'group' => 'background_process_legacy', + ); + return $hooks; +} + +/** + * Implements hook_help(). + * + */ +function ultimate_cron_help($route_name, \Drupal\Core\Routing\RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.ultimate_cron': + // Return a line-break version of the module README. + return '<pre>' . file_get_contents(dirname(__FILE__) . '/README.txt') . '</pre>'; + } +} + +/** + * Implements hook_cronapi(). + * + * Adds clean up jobs for plugins. + * */ +function ultimate_cron_cron() { + $plugin_types = CronPlugin::getPluginTypes(); + foreach ($plugin_types as $plugin_type => $info) { + foreach (ultimate_cron_plugin_load_all($plugin_type) as $name => $plugin) { + if ($plugin->isValid() && ($plugin instanceof PluginCleanupInterface)) { + $plugin->cleanup(); + } + } + } +} + +/** + * * Implements hook_modules_installed(). + */ +function ultimate_cron_modules_installed($modules) { + \Drupal::service('ultimate_cron.discovery')->discoverCronJobs(); +} + +/** + * Implements hook_form_FORM_ID_alter() for the system_cron_settings() form. + */ +function ultimate_cron_form_system_cron_settings_alter(&$form, &$form_state) { + $options = [60, 300, 900, 1800, 3600, 10800, 21600, 43200, 86400, 604800]; + $form['automated_cron']['interval']['#options'] = [0 => t('Never')] + array_map([ + \Drupal::service('date.formatter'), + 'formatInterval', + ], array_combine($options, $options)); + +} + +// ---------- CRON RULE FUNCTIONS ---------- +/** + * Return blank values for all keys in an array. + * + * @param array $array + * Array to generate blank values from. + * + * @return array + * Array with same keys as input, but with blank values (empty string). + */ +function ultimate_cron_blank_values($array) { + $result = array(); + foreach ($array as $key => $value) { + switch (gettype($value)) { + case 'array': + $result[$key] = array(); + break; + + default: + $result[$key] = ''; + } + } + return $result; +} + +/** + * Custom sort callback for sorting cron jobs by start time. + */ +function _ultimate_cron_sort_jobs_by_start_time($a, $b) { + return $a->log_entry->start_time == $b->log_entry->start_time ? 0 : ($a->log_entry->start_time > $b->log_entry->start_time ? 1 : -1); +} + +/** + * Sort callback for multiple column sort. + */ +function _ultimate_cron_multi_column_sort($a, $b) { + $a = (array) $a; + $b = (array) $b; + foreach ($a['sort'] as $i => $sort) { + if ($a['sort'][$i] == $b['sort'][$i]) { + continue; + } + return $a['sort'][$i] < $b['sort'][$i] ? -1 : 1; + } + return 0; +} + +/** + * Get transactional safe connection. + * + * @return string + * Connection target. + */ +function _ultimate_cron_get_transactional_safe_connection() { + return !\Drupal::config('ultimate_cron.settings')->get('bypass_transactional_safe_connection') && \Drupal\Core\Database\Database::getConnection()->inTransaction() ? 'ultimate_cron' : 'default'; +} diff --git a/web/modules/ultimate_cron/ultimate_cron.nagios.inc b/web/modules/ultimate_cron/ultimate_cron.nagios.inc new file mode 100644 index 0000000000000000000000000000000000000000..e600c93425272fdc8deaa42831abd7fba20bd118 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.nagios.inc @@ -0,0 +1,245 @@ +<?php +use Drupal\Core\Logger\RfcLogLevel; +use Drupal\ultimate_cron\CronJobDiscovery; + +/** + * Implements hook_nagios_info(). + */ +function ultimate_cron_nagios_info() { + return array( + 'name' => t('Ultimate Cron Monitoring'), + 'id' => 'ULTIMATE_CRON', + ); +} + +/** + * Implementation of hook_nagios(). + */ +function ultimate_cron_nagios($check = 'nagios') { + $status = array(); + foreach(ultimate_cron_nagios_functions() as $function => $description) { + if (variable_get('ultimate_cron_nagios_func_' . $function, TRUE) && ($check == 'nagios' || $check == $function)) { + $func = $function . '_check'; + $result = $func(); + $status[$result['key']] = $result['data']; + } + } + + return $status; +} + +/** + * Implementation of hook_nagios_settings(). + */ +function ultimate_cron_nagios_settings() { + $form = array(); + + foreach(ultimate_cron_nagios_functions() as $function => $description) { + $var = 'ultimate_cron_nagios_func_' . $function; + $form[$var] = array( + '#type' => 'checkbox', + '#title' => $function, + '#default_value' => variable_get($var, TRUE), + '#description' => $description, + ); + } + + $group = 'thresholds'; + $form[$group] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#title' => t('Thresholds'), + '#description' => t('Thresholds for reporting critical alerts to Nagios.'), + ); + + $form[$group]['ultimate_cron_nagios_running_threshold'] = array( + '#type' => 'textfield', + '#title' => t('Running jobs count'), + '#default_value' => variable_get('ultimate_cron_nagios_running_threshold', 50), + '#description' => t('Issue a critical alert when more than this number of jobs are running. Default is 50.'), + ); + + $form[$group]['ultimate_cron_nagios_failed_threshold'] = array( + '#type' => 'textfield', + '#title' => t('Failed jobs count'), + '#default_value' => variable_get('ultimate_cron_nagios_failed_threshold', 10), + '#description' => t('Issue a critical alert when more than this number of jobs failed their last run. Default is 10.'), + ); + + $form[$group]['ultimate_cron_nagios_longrunning_threshold'] = array( + '#type' => 'textfield', + '#title' => t('Long running jobs'), + '#default_value' => variable_get('ultimate_cron_nagios_longrunning_threshold', 0), + '#description' => t('Issue a critical alert when more than this number of jobs are running longer than usual. Default is 0.') + ); + + return $form; +} + +/** + * Implementation of hook_nagios_checks(). + */ +function ultimate_cron_nagios_checks() { + return ultimate_cron_nagios_functions(); +} + +/** + * Implementation of drush hook_nagios_check(). + */ +function ultimate_cron_nagios_check($function) { + // We don't bother to check if the function has been enabled by the user. + // Since this runs via drush, web security is not an issue. + $func = $function . '_check'; + $result = $func(); + $status[$result['key']] = $result['data']; + return $status; +} + +/************** HELPER FUNCTIONS ***********************************/ +/** + * Return a list of nagios check functions + * @see ultimate_cron_nagios() + */ +function ultimate_cron_nagios_functions() { + return array( + 'ultimate_cron_running' => t('Check number of currently running jobs'), + 'ultimate_cron_failed' => t('Check the number of jobs that failed last run'), + 'ultimate_cron_longrunning' => t('Check the number of jobs that are running longer than usual'), + ); +} + +/** + * Get information about running jobs - currently running or failed. + * + * @staticvar array $overview + * @param string $mode Which mode to get info about; 'running' or 'error' + * @return int + */ +function ultimate_cron_nagios_get_job_info($mode = 'running') { + // Ensure valid mode + if (!in_array($mode, array('running', 'error'))) { + $mode = 'running'; + } + static $overview = array(); + + if (!isset($overview[$mode])) { + $overview[$mode] = 0; + // Get hooks and their data + $hooks = CronJobDiscovery::getHooks(); + + $modules = array(); + foreach ($hooks as $name => $hook) { + if (!$module || $module == $hook['module']) { + $log = ultimate_cron_get_log($name); + + if ($hook['background_process']) { + $overview['running']++; + } + $severity_type = $log['severity'] < 0 ? 'success' : ($log['severity'] >= RfcLogLevel::NOTICE ? 'info' : ($log['severity'] >= RfcLogLevel::NOTICE ? 'warning' : 'error')); + $overview[$severity_type]++; + } + } + } + + return $overview[$mode]; +} + +/*************** NAGIOS CHECK FUNCTIONS ********************************/ +/** + * Check number of running jobs. + * + * @return array + */ +function ultimate_cron_running_check() { + $running = ultimate_cron_nagios_get_job_info('running'); + $threshold = variable_get('ultimate_cron_nagios_running_threshold', 50); + if (count($running) > $threshold) { + $data = array( + 'status' => NAGIOS_STATUS_CRITICAL, + 'type' => 'state', + 'text' => t('@jobs currently running - it is more than @threshold', array('@jobs' => $running, '@threshold' => $threshold)), + ); + } + else { + $data = array( + 'status' => NAGIOS_STATUS_OK, + 'type' => 'state', + 'text' => t('@jobs currently running', array('@jobs' => $running)), + ); + } + + return array( + 'key' => 'ULTIMATE_CRON_RUNNING', + 'data' => $data, + ); +} + +/** + * Check number of jobs that failed last run. + * + * @return array + */ +function ultimate_cron_failed_check() { + $failed = ultimate_cron_nagios_get_job_info('errors'); + $threshold = variable_get('ultimate_cron_nagios_failed_threshold', 10); + if (count($failed) > $threshold) { + $data = array( + 'status' => NAGIOS_STATUS_CRITICAL, + 'type' => 'state', + 'text' => t('@jobs failed their last run - it is more than @threshold', array('@jobs' => $failed, '@threshold' => $threshold)), + ); + } + else { + $data = array( + 'status' => NAGIOS_STATUS_OK, + 'type' => 'state', + 'text' => t('@jobs failed their last run', array('@jobs' => $failed)), + ); + } + + return array( + 'key' => 'ULTIMATE_CRON_FAILED', + 'data' => $data, + ); +} + +/** + * Check number of jobs running longer than usual. + * + * @return array + * + * @todo Implement the logic + */ +function ultimate_cron_longrunning_check() { + $longrunning = 0; + + // Get running jobs + + // Find out how long they have been running + + // Calculate average run time per job (over a threshold? E.g. queues run very fast if there is nothing to process) + + // If + + $threshold = variable_get('ultimate_cron_nagios_longrunning_threshold', 0); + if ($longrunning > $threshold) { + $data = array( + 'status' => NAGIOS_STATUS_CRITICAL, + 'type' => 'state', + 'text' => t('@jobs jobs are running longer than usual - it is more than @threshold', array('@jobs' => $longrunning, '@threshold' => $threshold)), + ); + } + else { + $data = array( + 'status' => NAGIOS_STATUS_OK, + 'type' => 'state', + 'text' => t('@jobs jobs are running longer than usual', array('@jobs' => $longrunning)), + ); + } + + return array( + 'key' => 'ULTIMATE_CRON_LONGRUNNING', + 'data' => $data, + ); +} diff --git a/web/modules/ultimate_cron/ultimate_cron.permissions.yml b/web/modules/ultimate_cron/ultimate_cron.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..760a45275e80edd361ec55fb806c18822d36455d --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.permissions.yml @@ -0,0 +1,9 @@ +administer ultimate cron: + title: 'Administer Ultimate Cron' + description: 'Lets you configure everything in Ultimate Cron' +view cron jobs: + title: 'View cron jobs' + description: 'Lets you view cron jobs and their logs' +run cron jobs: + title: 'Run cron jobs' + description: 'Lets you run cron jobs' diff --git a/web/modules/ultimate_cron/ultimate_cron.routing.yml b/web/modules/ultimate_cron/ultimate_cron.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..1185f49688db1744b0b13a2c343a1f7df08b0904 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.routing.yml @@ -0,0 +1,112 @@ +ultimate_cron.settings: + path: '/admin/config/system/cron/settings' + defaults: + _form: '\Drupal\ultimate_cron\Form\GeneralSettingsForm' + _title: 'Manage Cron settings' + requirements: + _permission: 'administer ultimate cron' + +ultimate_cron.general_settings: + path: '/admin/config/system/cron/settings' + defaults: + _form: '\Drupal\ultimate_cron\Form\GeneralSettingsForm' + _title: 'General Cron Settings' + requirements: + _permission: 'administer ultimate cron' + +ultimate_cron.launcher_settings: + path: '/admin/config/system/cron/settings/launcher' + defaults: + _form: '\Drupal\ultimate_cron\Form\LauncherSettingsForm' + _title: 'Launcher settings' + requirements: + _permission: 'administer ultimate cron' + +ultimate_cron.logger_settings: + path: '/admin/config/system/cron/settings/logger' + defaults: + _form: '\Drupal\ultimate_cron\Form\LoggerSettingsForm' + _title: 'Logger settings' + requirements: + _permission: 'administer ultimate cron' + +ultimate_cron.scheduler_settings: + path: '/admin/config/system/cron/settings/scheduler' + defaults: + _form: '\Drupal\ultimate_cron\Form\SchedulerSettingsForm' + _title: 'Scheduler settings' + requirements: + _permission: 'administer ultimate cron' + +entity.ultimate_cron_job.collection: + path: '/admin/config/system/cron/jobs' + defaults: + _entity_list: 'ultimate_cron_job' + _title: 'Cron jobs' + requirements: + _permission: 'administer ultimate cron' + +ultimate_cron.discover_jobs: + path: '/admin/config/system/cron/jobs/discover' + defaults: + _controller: '\Drupal\ultimate_cron\Controller\JobController::discoverJobs' + _title: 'Discover jobs' + requirements: + _permission: 'administer ultimate cron' + _csrf: 'TRUE' + +entity.ultimate_cron_job.edit_form: + path: '/admin/config/system/cron/jobs/manage/{ultimate_cron_job}' + defaults: + _entity_form: 'ultimate_cron_job.default' + _title: 'Edit job' + requirements: + _entity_access: 'ultimate_cron_job.update' + +entity.ultimate_cron_job.run: + path: '/admin/config/system/cron/jobs/{ultimate_cron_job}/run' + defaults: + _controller: '\Drupal\ultimate_cron\Controller\JobController::runCronJob' + _title: Run Cron job + requirements: + _permission: 'run cron jobs' + +entity.ultimate_cron_job.delete_form: + path: '/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/delete' + defaults: + _entity_form: 'ultimate_cron_job.delete' + _title: 'Delete job' + requirements: + _entity_access: 'ultimate_cron_job.delete' + +entity.ultimate_cron_job.disable: + path: '/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/disable' + defaults: + _entity_form: 'ultimate_cron_job.disable' + _title: 'Disable cron job' + requirements: + _entity_access: 'ultimate_cron_job.disable' + +entity.ultimate_cron_job.enable: + path: '/admin/config/system/cron/jobs/manage/{ultimate_cron_job}/enable' + defaults: + _entity_form: 'ultimate_cron_job.enable' + _title: 'Enable cron job' + requirements: + _entity_access: 'ultimate_cron_job.enable' + +entity.ultimate_cron_job.logs: + path: '/admin/config/system/cron/jobs/logs/{ultimate_cron_job}' + defaults: + _controller: '\Drupal\ultimate_cron\Controller\JobController::showLogs' + _title: 'Cron jobs logs' + requirements: + _entity_access: 'ultimate_cron_job.views' + +entity.ultimate_cron_job.unlock: + path: '/admin/config/system/cron/jobs/{ultimate_cron_job}/unlock' + defaults: + _controller: '\Drupal\ultimate_cron\Controller\JobController::unlockCronJob' + _title: Unlock Cron job + requirements: + _permission: 'run cron jobs' diff --git a/web/modules/ultimate_cron/ultimate_cron.services.yml b/web/modules/ultimate_cron/ultimate_cron.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..3863d99cc9cace033499184f7183e37b14d64286 --- /dev/null +++ b/web/modules/ultimate_cron/ultimate_cron.services.yml @@ -0,0 +1,45 @@ +services: + cache.signal: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [signal] + cache.ultimate_cron_logger: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory: cache_factory:get + arguments: [ultimate_cron_logger] + plugin.manager.ultimate_cron.launcher: + class: Drupal\ultimate_cron\Launcher\LauncherManager + parent: default_plugin_manager + plugin.manager.ultimate_cron.logger: + class: Drupal\ultimate_cron\Logger\LoggerManager + parent: default_plugin_manager + plugin.manager.ultimate_cron.scheduler: + class: Drupal\ultimate_cron\Scheduler\SchedulerManager + parent: default_plugin_manager + ultimate_cron.lock: + class: Drupal\ultimate_cron\Lock\Lock + arguments: ['@ultimate_cron.database_factory'] + ultimate_cron.progress: + class: Drupal\ultimate_cron\Progress\Progress + arguments: ['@keyvalue'] + ultimate_cron.signal: + class: Drupal\ultimate_cron\Signal\SignalCache + arguments: ['@cache.signal', '@lock'] + ultimate_cron.discovery: + class: Drupal\ultimate_cron\CronJobDiscovery + arguments: ['@module_handler', '@plugin.manager.queue_worker', '@config.factory', '@extension.list.module'] + logger.ultimate_cron: + class: Drupal\ultimate_cron\Logger\WatchdogLogger + arguments: ['@logger.log_message_parser'] + tags: + - { name: logger } + ultimate_cron.queue_worker: + class: Drupal\ultimate_cron\QueueWorker + arguments: ["@plugin.manager.queue_worker", "@queue", "@config.factory"] + ultimate_cron.database_factory: + class: Drupal\Core\Database\Connection + factory: Drupal\ultimate_cron\UltimateCronDatabaseFactory::getConnection