source: trunk/server/www/app/controllers/jobs_controller.php @ 297

Last change on this file since 297 was 297, checked in by sander, 11 years ago

Generate and display jobs and results for test suites

File size: 17.0 KB
Line 
1<?php
2/**
3 * Officeshots.org - Test your office documents in different applications
4 * Copyright (C) 2009 Stichting Lone Wolves
5 * Written by Sander Marechal <s.marechal@jejik.com>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20
21// We need to access the Request model statically for it's state constants
22App::import('Model', 'Request');
23
24/**
25 * The jobs controller
26 */
27class JobsController extends AppController
28{
29        /** @var array The components this controller uses */
30        public $components = array('AuthCert');
31       
32        /** @var array The helpers that will be available on the view */
33        public $helpers = array('Html', 'Form', 'JobModel');
34
35        /** @var array The models used by this controller */
36        public $uses = array('Job', 'Factory', 'Request');
37
38        /**
39         * You can't index jobs. Redirect to requests.
40         * @return void
41         */
42        public function index()
43        {
44                $this->redirect(array('controller' => 'requests', 'action' => 'index'));
45        }
46
47        /**
48         * View one of your own jobs
49         *
50         * @param string $id The job ID
51         * @return void
52         */
53        public function view($id = null)
54        {
55                if (!$id) {
56                        $this->Session->setFlash(__('Invalid Job.', true));
57                        $this->redirect(array('controller' => 'requests', 'action'=>'index'));
58                }
59               
60                $this->Job->contain(array(
61                        'Request',
62                        'Request.Gallery',
63                        'Platform',
64                        'Application',
65                        'Factory',
66                        'Result',
67                ));
68                $job = $this->Job->read(null, $id);
69               
70                $isValid = (
71                        !empty($job['Request']['Gallery']) ||
72                        $job['Request']['user_id'] == $this->AuthCert->user('id')
73                );
74
75                if (!$isValid) {
76                        $this->Session->setFlash(__('Invalid Job.', true));
77                        $this->redirect(array('controller' => 'requests', 'action'=>'index'));
78                }
79
80                $this->set('job', $job);
81        }
82
83        /**
84         * List all jobs
85         * @return void
86         */
87        public function admin_index()
88        {
89                $this->Job->recursive = 0;
90                $this->set('jobs', $this->paginate());
91        }
92
93        /**
94         * View a single job
95         *
96         * @param string $id The job ID
97         * @return void
98         */
99        public function admin_view($id = null)
100        {
101                if (!$id) {
102                        $this->Session->setFlash(__('Invalid Job.', true));
103                        $this->redirect(array('action'=>'index'));
104                }
105                $this->set('job', $this->Job->read(null, $id));
106        }
107
108        /**
109         * Add a new job manually
110         * @return void
111         */
112        public function admin_add()
113        {
114                if (!empty($this->data)) {
115                        $this->Job->create();
116                        if ($this->Job->save($this->data)) {
117                                $this->Session->setFlash(__('The Job has been saved', true));
118                                $this->redirect(array('action'=>'index'));
119                        } else {
120                                $this->Session->setFlash(__('The Job could not be saved. Please, try again.', true));
121                        }
122                }
123                $requests = $this->Job->Request->find('list');
124                $platforms = $this->Job->Platform->find('list');
125                $applications = $this->Job->Application->find('list');
126                $factories = $this->Job->Factory->find('list');
127                $results = $this->Job->Result->find('list');
128                $this->set(compact('requests', 'platforms', 'applications', 'factories', 'results'));
129        }
130
131        /**
132         * Edit a job
133         *
134         * @param string $id The job ID
135         * @return void
136         */
137        public function admin_edit($id = null)
138        {
139                if (!$id && empty($this->data)) {
140                        $this->Session->setFlash(__('Invalid Job', true));
141                        $this->redirect(array('action'=>'index'));
142                }
143                if (!empty($this->data)) {
144                        if ($this->Job->save($this->data)) {
145                                $this->Session->setFlash(__('The Job has been saved', true));
146                                $this->redirect(array('action'=>'index'));
147                        } else {
148                                $this->Session->setFlash(__('The Job could not be saved. Please, try again.', true));
149                        }
150                }
151                if (empty($this->data)) {
152                        $this->data = $this->Job->read(null, $id);
153                }
154                $requests = $this->Job->Request->find('list');
155                $platforms = $this->Job->Platform->find('list');
156                $applications = $this->Job->Application->find('list');
157                $factories = $this->Job->Factory->find('list');
158                $results = $this->Job->Result->find('list');
159                $this->set(compact('requests', 'platforms', 'applications', 'factories', 'results'));
160        }
161
162        /**
163         * Delete a job
164         *
165         * @param string $id The job ID
166         * @return void
167         */
168        public function admin_delete($id = null)
169        {
170                if (!$id) {
171                        $this->Session->setFlash(__('Invalid id for Job', true));
172                        $this->redirect(array('action'=>'index'));
173                }
174                if ($this->Job->del($id)) {
175                        $this->Session->setFlash(__('Job deleted', true));
176                        $this->redirect(array('action'=>'index'));
177                }
178        }
179
180        /**
181         * Find a job that matches any of the applications on a given factory and lock it
182         *
183         * Signature
184         * ~~~~~~~~~
185         * jobs.poll(factory_name)
186         *
187         * Arguments
188         * ~~~~~~~~~
189         * - factory_name (string): The name of the factory that you want to poll for
190         *
191         * Return value
192         * ~~~~~~~~~~~~
193         * When a job is found that matches any of the workers that are running on the
194         * requested factory then an array will be returned containing the following fields:
195         *
196         * - job (string): The GUID of the job
197         * - application (string): The name of the application to run, e.g. "OpenOffice.org Writer"
198         * - version (string): The version string of the application, e.g. "3.0" or "2003 SP2"
199         * - pageStart (int): The first page to render in the output
200         * - pageEnd (int): The last page to render in the output. If this is zero then it should be
201         *                  rendered to the end of the document.
202         * - format (string): 3-letter code specifying the desired output format (pdf, png or odf). If this
203         *                    is empty then the factory can decide.
204         * - filename (string): The original filename of the ODF document
205         * - doctype (string): 3-letter code specifying the input type (odt, ods, odp)
206         * - document (string): Base64-encoded contents of the document
207         *
208         * When no mathcing job has been found then an empty result is returned.
209         *
210         * Job locking
211         * ~~~~~~~~~~~
212         * The matching job is locked for five minutes. This is to make sure that no jobs are processed by two factories at
213         * the same time. If your factory takes longer to process a job, it is possible that somebody else will lock it. In
214         * this case, your upload will fail.
215         *
216         * Example request
217         * ~~~~~~~~~~~~~~~
218         * POST /xmlrpc HTTP/1.0
219         * Host: example.org
220         * Content-Type: text/xml
221         * Content-Length: 193
222         *
223         * <?xml version='1.0'?>
224         * <methodCall>
225         *   <methodName>jobs.finish</methodName>
226         *   <params>
227         *     <param><value><string>My factory</string></value></param>
228         *   </params>
229         * </methodCall>
230         *
231         * Example response
232         * ~~~~~~~~~~~~~~~~
233         * HTTP/1.1 200 OK
234         * Content-Type: application/xml
235         * Content-Length: 10164
236         * Connection: close
237         *
238         * <?xml version="1.0" encoding="iso-8859-1"?>
239         * <methodResponse>
240         *   <params>
241         *     <param>
242         *       <value>
243         *         <struct>
244         *           <member>
245         *             <name>job</name>
246         *             <value><string>4975c9c6-79a0-43a1-8134-0ba5c0a80105</string></value>
247         *           </member>
248         *           <member>
249         *             <name>application</name>
250         *             <value><string>OpenOffice.org Writer</string></value>
251         *           </member>
252         *           <member>
253         *             <name>version</name>
254         *             <value><string>3.0</string></value>
255         *           </member>
256         *           <member>
257         *             <name>pageStart</name>
258         *             <value><string>1</string></value>
259         *           </member>
260         *           <member>
261         *             <name>pageEnd</name>
262         *             <value><string>0</string></value>
263         *           </member>
264         *           <member>
265         *             <name>format</name>
266         *             <value><string>pdf</string></value>
267         *           </member>
268         *           <member>
269         *             <name>filename</name>
270         *             <value><string>mydocument.odt</string></value>
271         *           </member>
272         *           <member>
273         *             <name>doctype</name>
274         *             <value><string>odt</string></value>
275         *           </member>
276         *           <member>
277         *             <name>document</name>
278         *             <value><string>UEsDBBQA ... AAAAAA==</string></value>
279         *           </member>
280         *         </struct>
281         *       </value>
282         *     <param>
283         *   </params>
284         * </methodResponse>
285         *
286         * @param string $method The XMLRPC method name
287         * @param mixed $params The XMLRPC parameters
288         * @param mixed $userdata Data passed from the XMLRPC server
289         * @return array The XMLRPC result or a fault array
290         */
291        public function xmlrpc_poll($method, $params, $userdata = null)
292        {
293                if (!$factory = $this->Factory->findFactory($this->AuthCert->user('id'), $params[0])) {
294                        return array('faultCode' => 1, 'faultString' => 'Factory does not exist. Possible SSL authentication failure.');
295                }
296
297                $this->Factory->id = $factory['Factory']['id'];
298                $this->Factory->poll();
299
300                $jobs = array();
301                foreach ($factory['Worker'] as $worker) {
302                        $formats = array_merge(array(''), Set::extract('/Format/id', $worker));
303                        $doctypes = array_merge(array(''), Set::extract('/Application/Doctype/id', $worker));
304                        foreach ($formats as &$format) { $format = "'" . $format . "'"; }
305                        foreach ($doctypes as &$doctype) { $doctype = "'" . $doctype . "'"; }
306
307                        $jobSet = $this->Job->query("SELECT
308                                        `Job`.`id`,
309                                        `Job`.`version`,
310                                        `Application`.`name`,
311                                        `Request`.`id`,
312                                        `Request`.`page_start`,
313                                        `Request`.`page_end`,
314                                        `Request`.`filename`,
315                                        `Request`.`created`,
316                                        `Request`.`priority`,
317                                        `Request`.`own_factory`,
318                                        `Request`.`path`,
319                                        `Format`.`code`,
320                                        `Doctype`.`code`
321                                FROM `jobs` as `Job`
322                                        LEFT JOIN `requests` AS `Request` ON (`Job`.`request_id` = `Request`.`id`)
323                                        LEFT JOIN `applications` AS `Application` ON (`Job`.`application_id` = `Application`.`id`)
324                                        LEFT JOIN `formats` AS `Format` ON (`Job`.`format_id` = `Format`.`id`)
325                                        LEFT JOIN `mimetypes` AS `Mimetype` on (`Request`.`mimetype_id` = `Mimetype`.`id`)
326                                        LEFT JOIN `doctypes` AS `Doctype` on (`Mimetype`.`doctype_id` = `Doctype`.`id`)
327                                WHERE `Job`.`result_id` = ''
328                                        AND `Job`.`locked` < '" . date('Y-m-d H:i:s') . "'
329                                        AND `Job`.`platform_id` = '" . $factory['Operatingsystem']['platform_id'] . "'
330                                        AND `Job`.`application_id` = '" . $worker['application_id'] . "'
331                                        AND `Job`.`version` = '" . $worker['version'] . "'
332                                        AND `Job`.`format_id` IN  (" . implode(', ', $formats) . ")
333                                        AND `Request`.`state` = " . Request::STATE_QUEUED . "
334                                        AND (`Request`.`expire` > '" . date('Y-m-d H:i:s') . "' OR `Request`.`expire` IS NULL)
335                                        AND `Mimetype`.`doctype_id` IN (" . implode(', ', $doctypes) . ")
336                                        AND (`Request`.`own_factory` = 0
337                                                OR (`Request`.`own_factory` = 1 AND `Request`.`user_id` = '" . $this->AuthCert->user('id') . "')
338                                        )
339                                ORDER BY `Request`.`own_factory` DESC,
340                                        `Request`.`priority` ASC,
341                                        `Request`.`created` ASC
342                                LIMIT 0,1");
343                        $jobs = array_merge($jobs, $jobSet);
344                }
345
346                if (sizeof($jobs) == 0) {
347                        return null;
348                }
349
350                usort($jobs, array($this, '_cmpJobs'));
351                $job = array_shift($jobs);
352
353                $this->Request->id = $job['Request']['id'];
354                $file = $this->Request->getPath();
355                if (!file_exists($file) || !is_readable($file)) {
356                        // Something went wrong. Expire the entire request
357                        $this->Request->saveField('expire', date('Y-m-d H:i:s', time() -1));
358                        return array('faultCode' => 2, 'faultString' => 'Job document could not be read. Please poll again.');
359                }
360
361                $job['Job']['locked'] = date('Y-m-d H:i:s', time() + Configure::read('Job.locktime'));
362                $job['Job']['factory_id'] = $factory['Factory']['id'];
363                $this->Job->save($job);
364
365                $result = array(
366                        'job' => $job['Job']['id'],
367                        'application' => $job['Application']['name'],
368                        'version' => $job['Job']['version'],
369                        'pageStart' => $job['Request']['page_start'],
370                        'pageEnd' => $job['Request']['page_end'],
371                        'format' => ($job['Format'] ? $job['Format']['code'] : ''),
372                        'filename' => $job['Request']['filename'],
373                        'doctype' => $job['Doctype']['code'],
374                        'document' => base64_encode(file_get_contents($file))
375                );
376
377                return $result;
378        }
379
380        /**
381         * A helper function to sort jobs for xmlrpc_poll()
382         */
383        public function _cmpJobs($a, $b)
384        {
385                // First, look at the own_factory flag
386                if ($a['Request']['own_factory'] == 1 && $b['Request']['own_factory'] == 0) {
387                        return -1;
388                }
389                if ($a['Request']['own_factory'] == 0 && $b['Request']['own_factory'] == 1) {
390                        return 1;
391                }
392
393                // Next, look at the priority
394                if ($a['Request']['priority'] < $b['Request']['priority']) {
395                        return -1;
396                }
397                if ($a['Request']['priority'] > $b['Request']['priority']) {
398                        return 1;
399                }
400
401                // If the priorities are also equal, look at the creation date
402                return ($a['Request']['created'] < $b['Request']['created']) ? -1 : 1;
403        }
404
405        /**
406         * Finish a job by uploading the result
407         *
408         * Signature
409         * ~~~~~~~~~
410         * jobs.finish(factory_name, job, format, document)
411         *
412         * Arguments
413         * ~~~~~~~~~
414         * - factory_name (string): The name of the factory that processed the job.
415         * - job (string): The job ID that was returned by jobs.poll().
416         * - format (string): 3-letter code specifying the format of the result (pdf, png or odf).
417         * - document (string): Base64-encoded contents of the result document.
418         *
419         * Return value
420         * ~~~~~~~~~~~~
421         * - result (string): The ID of the result
422         *
423         * Example request
424         * ~~~~~~~~~~~~~~~
425         * POST /xmlrpc HTTP/1.0
426         * Host: example.org
427         * Content-Type: text/xml
428         * Content-Length: 10162
429         *
430         * <?xml version='1.0'?>
431         * <methodCall>
432         *   <methodName>jobs.finish</methodName>
433         *   <params>
434         *     <param><value><string>My factory</string></value></param>
435         *     <param><value><string>4975c9c6-79a0-43a1-8134-0ba5c0a80105</string></value></param>
436         *     <param><value><string>pdf</string></value></param>
437         *     <params><value><string>UEsDBBQA ... AAAAAA==</string></value></params>
438         *   </params>
439         * </methodCall>
440         *
441         * Example response
442         * ~~~~~~~~~~~~~~~~
443         * HTTP/1.1 200 OK
444         * Content-Type: application/xml
445         * Content-Length: 195
446         * Connection: close
447         *
448         * <?xml version="1.0" encoding="iso-8859-1"?>
449         * <methodResponse>
450         *   <params>
451         *     <param><value><string>497d9737-a2cc-4682-9d90-0136c0a80105</string></value></param>
452         *   </params>
453         * </methodResponse>
454         *
455         * @param string $method The XMLRPC method name
456         * @param mixed &$params The XMLRPC parameters
457         * @param mixed $userdata Data passed from the XMLRPC server
458         * @return array The XMLRPC result or a fault array
459         */
460        public function xmlrpc_finish($method, &$params, $userdata = null)
461        {
462                if (sizeof($params) != 4) {
463                        return array('faultCode' => 1, 'faultString' => 'Wrong parameter count.');
464                }
465                list($factory_name, $job_id, $format_code) = $params;
466
467                if (!$factory = $this->Factory->findFactory($this->AuthCert->user('id'), $factory_name)) {
468                        return array('faultCode' => 1, 'faultString' => 'Factory does not exist.');
469                }
470
471                // Check the job
472                $job = $this->Job->find('first', array(
473                        'conditions' => array('Job.id' => $job_id),
474                        'contain' => array(
475                                'Request',
476                                'Format',
477                                'Application',
478                                'Platform',
479                        )
480                ));
481
482                if (!$job) {
483                        return array('faultCode' => 1, 'faultString' => 'Job does not exist.');
484                }
485
486                if ($job['Job']['factory_id'] != $factory['Factory']['id']) {
487                        return array('faultCode' => 1, 'faultString' => 'Job lock expired and the job is taken by another factory.');
488                }
489
490                // Check the return format
491                $format = $this->Job->Format->find('first', array(
492                        'recursive' => -1,
493                        'conditions' => array('Format.code' => $format_code),
494                ));
495
496                if (!$format || ($job['Job']['format_id'] && $job['Format']['code'] != $format_code)) {
497                        return array('faultCode' => 1, 'faultString' => 'Wrong document format.');
498                }
499
500                // TODO: The file extension stuff is an ugly kludge. It should do this through the Mimetype model
501                $basename = basename($job['Request']['filename']);
502                $basename = substr($basename, 0, strrpos($basename, '.'));
503                if ($format_code == 'odf') {
504                        $filename = $basename . strrchr($job['Request']['filename'], '.');
505                } else {
506                        $filename = $basename . '.' . $format_code;
507                }
508
509                // Find out where to store the result
510                $path  = $job['Request']['root'] . DS;
511                $path .= Inflector::slug($job['Application']['name']) . '_';
512                $path .= Inflector::slug($job['Job']['version']) . '_';
513                $path .= Inflector::slug($job['Platform']['name']);
514
515                // Create the Result
516                $this->Job->Result->create();
517                $this->Job->Result->set(array(
518                        'format_id'  => $format['Format']['id'],
519                        'factory_id' => $factory['Factory']['id'],
520                        'path' => $path
521                ));
522
523                if (!$this->Job->Result->setFileBuffer($params[3], $filename, 'convert.base64-decode')) {
524                        $errors = $this->Job->Result->Behaviors->File->errors;
525                        array_unshift($errors, 'The result file could not be stored');
526                        return array('faultCode' => 1, 'faultString' => implode("\n", $errors));
527                }
528
529                if (!$this->Job->Result->save()) {
530                        return array('faultCode' => 1, 'faultString' => 'The result could not be saved');
531                }
532
533                // Update the Job with the new Result
534                $this->Job->id = $job['Job']['id'];
535                $this->Job->save(array('result_id' => $this->Job->Result->id));
536
537                return $this->Job->Result->id;
538        }
539}
540
541?>
Note: See TracBrowser for help on using the repository browser.