Work around cutycapt wait-timeout exit code bug
[phancap.git] / src / phancap / Adapter / Cutycapt.php
1 <?php
2 /**
3  * Part of phancap
4  *
5  * PHP version 5
6  *
7  * @category  Tools
8  * @package   Adapter_Cutycapt
9  * @author    Christian Weiske <cweiske@cweiske.de>
10  * @copyright 2014 Christian Weiske
11  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
12  * @link      http://cweiske.de/phancap.htm
13  */
14 namespace phancap;
15
16 /**
17  * Screenshot rendering using the "cutycapt" command line tool.
18  *
19  * @category  Tools
20  * @package   Adapter_Cutycapt
21  * @author    Christian Weiske <cweiske@cweiske.de>
22  * @copyright 2014 Christian Weiske
23  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
24  * @version   Release: @package_version@
25  * @link      http://cweiske.de/phancap.htm
26  */
27 class Adapter_Cutycapt
28 {
29     /**
30      * Lock file handle
31      * @var resourece
32      */
33     protected $lockHdl;
34
35     /**
36      * Lock file path
37      * @var string
38      */
39     protected $lockFile = null;
40
41     /**
42      * Check if all dependencies are available.
43      *
44      * @return mixed TRUE if all is fine, array with error messages otherwise
45      */
46     public function isAvailable()
47     {
48         $old = error_reporting(error_reporting() & ~E_STRICT);
49         $arErrors = array();
50         if (\System::which('xvfb-run') === false) {
51             $arErrors[] = '"xvfb-run" is not installed';
52         }
53         if (\System::which('cutycapt') === false) {
54             $arErrors[] = '"cutycapt" is not installed';
55         }
56         if (\System::which('convert') === false) {
57             $arErrors[] = '"convert" (imagemagick) is not installed';
58         }
59         if (\System::which('timeout') === false) {
60             $arErrors[] = '"timeout" (GNU coreutils) is not installed';
61         }
62
63         error_reporting($old);
64         if (count($arErrors)) {
65             return $arErrors;
66         }
67
68         return true;
69     }
70
71     /**
72      * Render a website screenshot
73      *
74      * @param Image   $img     Image configuration
75      * @param Options $options Screenshot configuration
76      *
77      * @return void
78      * @throws \Exception When something fails
79      */
80     public function render(Image $img, Options $options)
81     {
82         $format = $options->values['sformat'];
83         if ($format == 'jpg') {
84             $format = 'jpeg';
85         }
86
87         $maxWaitTime = 30;//seconds
88         if (isset($this->config->cutycapt['maxWaitTime'])) {
89             $maxWaitTime = (int) $this->config->cutycapt['maxWaitTime'];
90         }
91
92         $parameters = '';
93         if (isset($this->config->cutycapt['parameters'])) {
94             $parameters = $this->config->cutycapt['parameters'];
95         }
96
97         $serverNumber = $this->getServerNumber($options);
98         $tmpPath = $img->getPath() . '-tmp';
99         $cmd = 'cutycapt'
100             . ' --url=' . escapeshellarg($options->values['url'])
101             . ' --out-format=' . escapeshellarg($format)
102             . ' --out=' . escapeshellarg($tmpPath)
103             . ' --max-wait=' . (($maxWaitTime - 1) * 1000)
104             . ' --min-width=' . $options->values['bwidth'];
105         if ($options->values['bheight'] !== null) {
106             $cmd .= ' --min-height=' . $options->values['bheight'];
107         }
108         if (strlen($parameters) > 0) {
109             $cmd .= ' ' . $parameters;
110         }
111
112         $xvfbcmd = 'xvfb-run'
113             . ' -e /dev/stdout'
114             . ' --server-args="-screen 0, 1024x768x24"'
115             . ' --server-num=' . $serverNumber;
116         //cutycapt hangs sometimes - https://sourceforge.net/p/cutycapt/bugs/8/
117         // we kill it if it does not exit itself
118         Executor::runForSomeTime($xvfbcmd . ' ' . $cmd, $maxWaitTime);
119
120         //cutycapt does not report timeouts via exit status
121         if (!file_exists($tmpPath)) {
122             throw new \Exception('Error running cutycapt (wait timeout)', 1);
123         }
124
125         $this->resize($tmpPath, $img, $options);
126     }
127
128     /**
129      * Get a free X server number.
130      *
131      * Each xvfb-run process needs its own free server number.
132      * Needed for multiple parallel requests.
133      *
134      * @return integer Server number
135      */
136     protected function getServerNumber()
137     {
138         //clean stale lock files
139         $this->cleanup();
140
141         $num = 100;
142         $bFound = false;
143         do {
144             ++$num;
145             $f = $this->config->cacheDir . 'tmp-curlycapt-server-' . $num . '.lock';
146             $this->lockHdl = fopen($f, 'w');
147             if (flock($this->lockHdl, LOCK_EX | LOCK_NB)) {
148                 $this->lockFile = $f;
149                 $bFound = true;
150                 break;
151             } else {
152                 fclose($this->lockHdl);
153             }
154         } while ($num < 200);
155
156         if (!$bFound) {
157             throw new \Exception('Too many requests running');
158         }
159
160         $this->lockFile = $f;
161         return $num;
162     }
163
164     /**
165      * Unlock lock file and clean up old lock files
166      *
167      * @return void
168      */
169     public function cleanup()
170     {
171         if ($this->lockFile !== null && $this->lockHdl) {
172             flock($this->lockHdl, LOCK_UN);
173             unlink($this->lockFile);
174         }
175
176         $lockFiles = glob(
177             $this->config->cacheDir . 'tmp-curlycapt-server-*.lock'
178         );
179
180         $now = time();
181         foreach ($lockFiles as $file) {
182             if ($now - filemtime($file) > 120) {
183                 //delete stale lock file; probably something crashed.
184                 unlink($file);
185             }
186         }
187     }
188
189     /**
190      * Convert an image to the given size.
191      *
192      * Target file is the path of $img.
193      *
194      * @param string  $tmpPath Path of image to be scaled.
195      * @param Image   $img     Image configuration
196      * @param Options $options Screenshot configuration
197      *
198      * @return void
199      */
200     protected function resize($tmpPath, Image $img, Options $options)
201     {
202         if ($options->values['sformat'] == 'pdf') {
203             //nothing to resize.
204             rename($tmpPath, $img->getPath());
205             return;
206         }
207
208         $crop = '';
209         if ($options->values['smode'] == 'screen') {
210             $crop = ' -crop ' . $options->values['swidth']
211                 . 'x' . $options->values['sheight']
212                 . '+0x0';
213         }
214
215         $convertcmd = 'convert'
216             . ' ' . escapeshellarg($tmpPath)
217             . ' -resize ' . $options->values['swidth']
218             . $crop
219             . ' ' . escapeshellarg($img->getPath());
220         Executor::run($convertcmd);
221         //var_dump($convertcmd);die();
222         unlink($tmpPath);
223     }
224
225     /**
226      * Set phancap configuration
227      *
228      * @param Config $config Phancap configuration
229      *
230      * @return void
231      */
232     public function setConfig(Config $config)
233     {
234         $this->config = $config;
235     }
236 }
237 ?>