├── LICENCE.rst ├── README.md ├── composer.json └── lib └── Treffynnon └── At ├── Job.php ├── JobAddException.php ├── JobNotFoundException.php ├── UndefinedPropertyException.php └── Wrapper.php /LICENCE.rst: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | ==================== 3 | 4 | Copyright (c) 2012, Simon Holywell 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP at Job Queue Wrapper 2 | 3 | ![build](https://github.com/treffynnon/PHP-at-Job-Queue-Wrapper/workflows/build/badge.svg) [![Latest Stable Version](https://poser.pugx.org/treffynnon/php-at-job-queue-wrapper/v)](//packagist.org/packages/treffynnon/php-at-job-queue-wrapper) [![Total Downloads](https://poser.pugx.org/treffynnon/php-at-job-queue-wrapper/downloads)](//packagist.org/packages/treffynnon/php-at-job-queue-wrapper) [![Latest Unstable Version](https://poser.pugx.org/treffynnon/php-at-job-queue-wrapper/v/unstable)](//packagist.org/packages/treffynnon/php-at-job-queue-wrapper) [![License](https://poser.pugx.org/treffynnon/php-at-job-queue-wrapper/license)](//packagist.org/packages/treffynnon/php-at-job-queue-wrapper) 4 | 5 | A PHP class to wrap the Unix/Linux at/atd job queue. At allows you to specify a job that the system should run at certain point in time. For more information on at either run `man at` in your console or visit [man page][man-page] This class lets you add new items to the queue either as a command or a path to a file and it can also give you back a list of the jobs already in the queue. You have the option to supply a queue letter that the wrapper should use so you can seperate out your jobs. For example:: 6 | 7 | # Migrate to v2 8 | 9 | **Since the PHP 7.x version has been released for about whiles, the `1.x` version will not be active and please concern about `2.x` version.** 10 | 11 | ```php 12 | require 'vendor/autoload.php'; 13 | use Treffynnon\At\Wrapper as At 14 | 15 | // create a command job 16 | $job = 'echo "hello" | wall'; 17 | $time = 'now + 1min'; 18 | // save command job to queue letter c 19 | $queue = 'c'; 20 | At::cmd($job, $time, $queue); 21 | 22 | // create a file job 23 | $job = '/home/example/example.sh'; 24 | // save file job to queue letter f 25 | $queue = 'f'; 26 | At::file($job, $time, $queue); 27 | 28 | // create a file job and send it to 29 | // the default queue (determined by at) 30 | At::file($job, $time); 31 | 32 | // get a list of all the jobs in the queue 33 | var_dump(At::lq()); 34 | 35 | // get a list of all the jobs in the f queue 36 | var_dump(At::lq('f')); 37 | 38 | // get a list of all the jobs in the c queue 39 | var_dump(At::listQueue('c')); 40 | ``` 41 | 42 | PHP 5.3 is required to support the use of the namespace, but this could easily be removed to make it backwards compatible with PHP 5.1 and above. 43 | 44 | 45 | ## Requires 46 | 47 | * Unix/Linux 48 | * at (you will already have this installed as it comes with Linux distributions) 49 | * If using PHP 5.3 and above due to the use of namespaces in the code, please use `1.x` versions. 50 | * If using PHP 7.2+ version, please use `2.x` versions. **It will not support PHP 7.0 and PHP 7.1 versions**. 51 | 52 | ## Installation 53 | 54 | With composer already setup/init in your project you can add this project with: 55 | 56 | composer require treffynnon/php-at-job-queue-wrapper 57 | 58 | ## Running the Tests 59 | 60 | Clone this repository and install the development dependencies using composer: 61 | 62 | composer install 63 | 64 | Once complete you can simply run: 65 | 66 | composer run-script test 67 | 68 | from the root directory of the at Job Queue Wrapper. 69 | 70 | 71 | ## Troubleshooting Failed Tests 72 | 73 | The tests may fail if your version of `at` outputs differently when a new job is added or when the `at` queue is listed. The class has to long but simple regular expressions defined as properties in it. With a little knowledge of regex you can modify these to suit the output of the `at` binary on your operating system. 74 | 75 | If you find that you need to modify these regexs then please lodge a ticket on [github][github]. 76 | 77 | 78 | ## Currently Tested Output Styles 79 | 80 | ### Redhat 81 | 82 | #### Add job 83 | 84 | [root@server home]# echo 'echo "hello" \| wall' | at now +10min 85 | job 3 at 2010-11-15 10:54 86 | 87 | 88 | #### List queue 89 | 90 | [root@server home]# at -l 91 | 2 2010-11-15 10:53 a root 92 | 3 2010-11-15 10:54 a root 93 | 94 | 95 | ### Ubuntu 96 | 97 | #### Add job 98 | 99 | user@server:~$ echo 'echo "hello" \| wall' | at now +10min 100 | warning: commands will be executed using /bin/sh 101 | job 17 at Mon Nov 15 10:55:00 2010 102 | 103 | 104 | #### List queue 105 | 106 | user@server:~$ at -l 107 | 17 Mon Nov 15 10:55:00 2010 a simon 108 | 18 Mon Nov 15 10:55:00 2010 a simon 109 | 110 | 111 | [github]: https://github.com/treffynnon/PHP-at-Job-Queue-Wrapper/issues 112 | [man-page]: http://unixhelp.ed.ac.uk/CGI/man-cgi?at 113 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treffynnon/php-at-job-queue-wrapper", 3 | "type": "library", 4 | "description": "A PHP wrapper for the unix `at` queue", 5 | "keywords": ["queue","job queue","at","linux"], 6 | "homepage": "http://github.com/treffynnon/PHP-at-Job-Queue-Wrapper", 7 | "license": "BSD-2-Clause", 8 | "authors": [ 9 | { 10 | "name": "Simon Holywell", 11 | "email": "treffynnon@php.net", 12 | "homepage": "http://simonholywell.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.2" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^8.0", 20 | "phpstan/phpstan": "^0.12", 21 | "squizlabs/php_codesniffer": "^3.4", 22 | "friendsofphp/php-cs-fixer": "^2.16" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Treffynnon\\At\\": "lib/Treffynnon/At" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Treffynnon\\At\\Tests\\": "tests/Treffynnon/At" 32 | } 33 | }, 34 | "scripts": { 35 | "check": [ 36 | "@sniffer:check", 37 | "@cs:check", 38 | "@phpstan", 39 | "@test:coverage" 40 | ], 41 | "sniffer:check": "phpcs --standard=phpcs.xml", 42 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 43 | "cs:check": "php-cs-fixer fix --dry-run --format=txt --verbose --diff --diff-format=udiff --config=.cs.php", 44 | "cs:fix": "php-cs-fixer fix --config=.cs.php", 45 | "phpstan": "phpstan analyse lib --level=max -c phpstan.neon --no-progress --ansi", 46 | "test": "phpunit --do-not-cache-result --colors=always", 47 | "test:coverage": "phpunit --configuration phpunit.xml --do-not-cache-result --coverage-clover build/logs/clover.xml --coverage-html build/coverage" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/Treffynnon/At/Job.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * @version 16.11.2010 12 | */ 13 | class Job 14 | { 15 | /** 16 | * Data store for the job details. 17 | * 18 | * @var array 19 | */ 20 | protected $data = []; 21 | 22 | /** 23 | * Magic method to set a value in the $data 24 | * property of the class. 25 | * 26 | * @param string $name The key of data array 27 | * @param mixed $value The value of data array 28 | * 29 | * @return void 30 | */ 31 | public function __set(string $name, $value): void 32 | { 33 | $this->data[$name] = $value; 34 | } 35 | 36 | /** 37 | * Magic method to get a value in the $data property 38 | * of the class. 39 | * 40 | * @param string $name The key of data array 41 | * 42 | * @return mixed 43 | */ 44 | public function __get($name) 45 | { 46 | if (isset($this->data[$name])) { 47 | return $this->data[$name]; 48 | } 49 | 50 | $trace = debug_backtrace(); 51 | throw new UndefinedPropertyException( 52 | 'Undefined property via __get(): ' . $name . 53 | ' in ' . $trace[0]['file'] . 54 | ' on line ' . $trace[0]['line'] 55 | ); 56 | } 57 | 58 | /** 59 | * Magic method to check for the existence of an 60 | * index in the $data property of the class. 61 | * 62 | * @param string $name The key of data array 63 | * 64 | * @return bool 65 | */ 66 | public function __isset($name): bool 67 | { 68 | return isset($this->data[$name]); 69 | } 70 | 71 | /** 72 | * Magic method to unset an index in the $data property 73 | * of the class. 74 | * 75 | * @param string $name The key of data array 76 | * 77 | * @return void 78 | */ 79 | public function __unset($name): void 80 | { 81 | unset($this->data[$name]); 82 | } 83 | 84 | /** 85 | * Remove this job from the queue. 86 | * 87 | * @uses $this->remove() 88 | * 89 | * @return void 90 | */ 91 | public function rem() 92 | { 93 | $this->remove(); 94 | } 95 | 96 | /** 97 | * Remove this job from the queue. 98 | * 99 | * @return void 100 | */ 101 | public function remove() 102 | { 103 | if (isset($this->job_number)) { 104 | Wrapper::removeJob((int)$this->job_number); 105 | } 106 | } 107 | 108 | /** 109 | * Get a DateTime object for date and time extracted from 110 | * the output of `at`. 111 | * 112 | * @example echo $job->date()->format('d-m-Y'); 113 | * 114 | * @param string $date The date string 115 | * 116 | * @uses DateTime 117 | * 118 | * @return \DateTime A PHP DateTime object 119 | */ 120 | public function date(string $date) 121 | { 122 | return new \DateTime($date); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/Treffynnon/At/JobAddException.php: -------------------------------------------------------------------------------- 1 | 11 | * @license BSD 12 | */ 13 | class Wrapper 14 | { 15 | /** 16 | * The path to the `at` binary. 17 | * 18 | * @var string 19 | */ 20 | protected static $binary = 'at'; 21 | 22 | /** 23 | * Regular expression to get the details of a job from the add job response. 24 | * 25 | * @var string 26 | */ 27 | protected static $addRegex = '/^job (\d+) at ([\w\d\- :]+)$/'; 28 | 29 | /** 30 | * A map of the regex matches to their descriptive names. 31 | * 32 | * @var array 33 | */ 34 | protected static $addMap = [ 35 | 1 => 'job_number', 36 | 2 => 'date', 37 | ]; 38 | 39 | /** 40 | * Regex to get the vitals from the queue. 41 | * 42 | * @var string 43 | */ 44 | protected static $queueRegex = '/^(\d+)\s+([\w\d\- :]+) (\w) ([\w-]+)$/'; 45 | 46 | /** 47 | * A map of the regex matches to their descriptive names. 48 | * 49 | * @var array 50 | */ 51 | protected static $queueMap = [ 52 | 1 => 'job_number', 53 | 2 => 'date', 54 | 3 => 'queue', 55 | 4 => 'user', 56 | ]; 57 | 58 | /** 59 | * Set escape whether using the escapeshellcmd before add command. 60 | * 61 | * @var bool 62 | */ 63 | protected static $escape = true; 64 | 65 | /** 66 | * Location to pipe the output of at commands to. 67 | * 68 | * I need to combine STDERR and STDOUT for my machine as when adding a new 69 | * job `at` responds over STDERR because it wants to warn me 70 | * "warning: commands will be executed using /bin/sh". When getting a list 71 | * of jobs in the queue however it comes back over STDOUT. 72 | * 73 | * Combining the two allows me to use the same pipe command for both types 74 | * of interaction with `at`. I think it is also the safest way of 75 | * accommodating users who do not the have the problem of warning being 76 | * triggered when adding a new job. 77 | * 78 | * @var string 79 | */ 80 | protected static $pipeTo = '2>&1'; 81 | 82 | /** 83 | * Switches/arguments that at uses on the `at` command. 84 | * 85 | * @var array 86 | */ 87 | protected static $atSwitches = [ 88 | 'queue' => '-q', 89 | 'list_queue' => '-l', 90 | 'file' => '-f', 91 | 'remove' => '-d', 92 | ]; 93 | 94 | /** 95 | * @param string $command The current command 96 | * @param string $time Please see `man at` 97 | * @param string $queue Please look at a-zA-Z and see `man at` 98 | * 99 | * @uses self::addCommand 100 | * 101 | * @return Job 102 | */ 103 | public static function cmd($command, $time, $queue = null) 104 | { 105 | return self::addCommand($command, $time, $queue); 106 | } 107 | 108 | /** 109 | * @param string $file Full path to the file to be executed 110 | * @param string $time Please see `man at` 111 | * @param string $queue Please look at a-zA-Z and see `man at` 112 | * 113 | * @uses self::addFile 114 | * 115 | * @return Job 116 | */ 117 | public static function file($file, $time, $queue = null) 118 | { 119 | return self::addFile($file, $time, $queue); 120 | } 121 | 122 | /** 123 | * @param string $queue Please look at a-zA-Z and see `man at` 124 | * 125 | * @uses self::listQueue 126 | * 127 | * @return array 128 | */ 129 | public static function lq($queue = null) 130 | { 131 | return self::listQueue($queue); 132 | } 133 | 134 | /** 135 | * Set escape param. 136 | * 137 | * @param bool $escape The escaped command 138 | * 139 | * @return self 140 | */ 141 | public function setEscape($escape) 142 | { 143 | self::$escape = $escape; 144 | 145 | return $this; 146 | } 147 | 148 | /** 149 | * Get escape param. 150 | * 151 | * @return bool 152 | */ 153 | public static function getEscape() 154 | { 155 | return self::$escape; 156 | } 157 | 158 | /** 159 | * Add a job to the `at` queue. 160 | * 161 | * @param string $command The current command 162 | * @param string $time Please see `man at` 163 | * @param string $queue Please look at a-zA-Z and see `man at` 164 | * 165 | * @return Job 166 | */ 167 | public static function addCommand($command, $time, $queue = null) 168 | { 169 | if (true === self::$escape) { 170 | $command = self::escape($command); 171 | $time = self::escape($time); 172 | } 173 | $exec_string = "echo '$command' | " . self::$binary; 174 | if (null !== $queue) { 175 | $exec_string .= ' ' . self::$atSwitches['queue'] . " {$queue[0]}"; 176 | } 177 | $exec_string .= " $time "; 178 | 179 | return self::addJob($exec_string); 180 | } 181 | 182 | /** 183 | * Add a file job to the `at` queue. 184 | * 185 | * @param string $file Full path to the file to be executed 186 | * @param string $time Please see `man at` 187 | * @param string $queue Please look at a-zA-Z and see `man at` 188 | * 189 | * @return Job 190 | */ 191 | public static function addFile($file, $time, $queue = null) 192 | { 193 | if (true === self::$escape) { 194 | $file = self::escape($file); 195 | $time = self::escape($time); 196 | } 197 | $exec_string = self::$binary . ' ' . self::$atSwitches['file'] . " $file"; 198 | if (null !== $queue) { 199 | $exec_string .= ' ' . self::$atSwitches['queue'] . " {$queue[0]}"; 200 | } 201 | $exec_string .= " $time "; 202 | 203 | return self::addJob($exec_string); 204 | } 205 | 206 | /** 207 | * Return a list of the jobs currently in the queue. If you do not specify 208 | * a queue to look at then it will return all jobs in all queues. 209 | * 210 | * @param string $queue Please look at a-zA-Z and see `man at` 211 | * 212 | * @return array 213 | */ 214 | public static function listQueue($queue = null) 215 | { 216 | $exec_string = self::$binary . ' ' . self::$atSwitches['list_queue']; 217 | if (null !== $queue) { 218 | $exec_string .= ' ' . self::$atSwitches['queue'] . " {$queue[0]}"; 219 | } 220 | $result = self::exec($exec_string); 221 | 222 | return self::transform($result, 'queue'); 223 | } 224 | 225 | /** 226 | * Remove a job by job number. 227 | * 228 | * @param int $job_number The current job number 229 | * 230 | * @return void 231 | */ 232 | public static function removeJob($job_number) 233 | { 234 | if (true === self::$escape) { 235 | $job_number = self::escape((string)$job_number); 236 | } 237 | $exec_string = self::$binary . ' ' . self::$atSwitches['remove'] . " $job_number"; 238 | $output = self::exec($exec_string); 239 | if (count($output)) { 240 | throw new JobNotFoundException("The job number $job_number could not be found"); 241 | } 242 | } 243 | 244 | /** 245 | * Add a job to the at queue and return the. 246 | * 247 | * @param string $job_exec_string The job execution string 248 | * 249 | * @return Job 250 | */ 251 | protected static function addJob($job_exec_string) 252 | { 253 | $output = self::exec($job_exec_string); 254 | $job = self::transform($output); 255 | $failedMessageFormat = 'The job has failed to be added to the queue. Exec command: %s'; 256 | $failedMessage = sprintf($failedMessageFormat, $job_exec_string); 257 | if (!count($job)) { 258 | throw new JobAddException($failedMessage); 259 | } 260 | 261 | return reset($job); 262 | } 263 | 264 | /** 265 | * Transform the output of `at` into an array of objects. 266 | * 267 | * @param array $output_array The output with array 268 | * @param string $type Is this an add or list we are transforming? 269 | * 270 | * @return array An array of Treffynnon\At\Job objects 271 | * 272 | * @uses Treffynnon\At\Job 273 | */ 274 | protected static function transform($output_array, $type = 'add') 275 | { 276 | $jobs = []; 277 | 278 | // Get the appropriate regex class property for the type 279 | // of `at` switch/command being run at this point in time. 280 | $regex = $type . 'Regex'; 281 | $regex = self::$$regex; 282 | 283 | $map = $type . 'Map'; 284 | $map = self::$$map; 285 | 286 | foreach ($output_array as $line) { 287 | $matches = []; 288 | preg_match($regex, $line, $matches); 289 | if (count($matches) > count($map)) { 290 | $jobs[] = self::mapJob($matches, $map); 291 | } 292 | } 293 | 294 | return $jobs; 295 | } 296 | 297 | /** 298 | * Map the details matched with the regex to descriptively named properties 299 | * in a new Treffynnon\At\Job object. 300 | * 301 | * @param array $details The details about job description 302 | * @param array $map The mapped job array 303 | * 304 | * @return Job 305 | */ 306 | protected static function mapJob($details, $map) 307 | { 308 | $Job = new Job(); 309 | foreach ($details as $key => $detail) { 310 | if (isset($map[$key])) { 311 | $Job->{$map[$key]} = $detail; 312 | } 313 | } 314 | 315 | return $Job; 316 | } 317 | 318 | /** 319 | * Escape a string that will be passed to exec. 320 | * 321 | * @param string $string The executed command 322 | * 323 | * @return string 324 | */ 325 | protected static function escape($string) 326 | { 327 | return escapeshellcmd($string); 328 | } 329 | 330 | /** 331 | * Run the command via exec() and return each line of the output as an 332 | * array. 333 | * 334 | * @param string $string The executed string 335 | * 336 | * @return array Each line of output is an element in the array 337 | */ 338 | protected static function exec($string) 339 | { 340 | $output = []; 341 | $string .= ' ' . self::$pipeTo; 342 | exec($string, $output); 343 | 344 | return $output; 345 | } 346 | } 347 | --------------------------------------------------------------------------------